<!doctype html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8" />
|
<title>稳定性销毁申请单</title>
|
<style>
|
* {
|
box-sizing: border-box;
|
margin: 0;
|
padding: 0;
|
}
|
|
body {
|
color: #1f2933;
|
font-family: 'SimSun', 'Microsoft YaHei', sans-serif;
|
font-size: 3.6mm;
|
line-height: 1.45;
|
}
|
|
.page {
|
width: 100%;
|
}
|
|
.doc-header {
|
margin-bottom: 5mm;
|
border-bottom: 0.35mm solid #111827;
|
padding-bottom: 3mm;
|
text-align: center;
|
}
|
|
.doc-title {
|
font-size: 6mm;
|
font-weight: 700;
|
letter-spacing: 0;
|
}
|
|
.doc-subtitle {
|
margin-top: 1mm;
|
color: #6b7280;
|
font-size: 3.2mm;
|
}
|
|
.section {
|
margin-top: 4mm;
|
break-inside: avoid;
|
page-break-inside: avoid;
|
}
|
|
.section-title {
|
margin-bottom: 1.6mm;
|
border-left: 1.2mm solid #111827;
|
padding-left: 2mm;
|
font-size: 4mm;
|
font-weight: 700;
|
}
|
|
table {
|
width: 100%;
|
border-collapse: collapse;
|
table-layout: fixed;
|
}
|
|
th,
|
td {
|
border: 0.2mm solid #5b6470;
|
padding: 1.6mm 1.3mm;
|
vertical-align: middle;
|
overflow-wrap: anywhere;
|
word-break: normal;
|
}
|
|
th {
|
background: #fafafa;
|
font-weight: 700;
|
text-align: center;
|
}
|
|
.info-table th {
|
width: 26mm;
|
text-align: center;
|
}
|
|
.info-table td {
|
min-height: 8mm;
|
}
|
|
.items-table {
|
font-size: 3.15mm;
|
}
|
|
.items-table th,
|
.items-table td {
|
padding: 1.3mm 0.9mm;
|
text-align: center;
|
}
|
|
.items-table tbody tr {
|
break-inside: avoid;
|
page-break-inside: avoid;
|
}
|
|
.align-left {
|
text-align: left !important;
|
}
|
|
.opinion-cell,
|
.record-cell {
|
height: 20mm;
|
vertical-align: top;
|
}
|
|
.checkbox-line {
|
display: inline-flex;
|
align-items: center;
|
margin-right: 8mm;
|
white-space: nowrap;
|
}
|
|
.box {
|
display: inline-flex;
|
width: 3.8mm;
|
height: 3.8mm;
|
align-items: center;
|
justify-content: center;
|
margin-right: 1.2mm;
|
border: 0.25mm solid #111827;
|
font-size: 3.2mm;
|
line-height: 1;
|
}
|
|
.audit-meta-grid {
|
display: grid;
|
grid-template-columns: repeat(2, 1fr);
|
gap: 0;
|
}
|
|
.audit-meta-grid > div {
|
min-height: 10mm;
|
border-right: 0.2mm solid #5b6470;
|
padding: 1.6mm 1.3mm;
|
}
|
|
.audit-meta-grid > div:last-child {
|
border-right: none;
|
}
|
|
.muted {
|
color: #6b7280;
|
}
|
|
.empty-state {
|
padding-top: 35mm;
|
color: #999;
|
text-align: center;
|
}
|
|
@media screen {
|
body {
|
background: #eceff3;
|
padding: 10mm 0;
|
}
|
|
#print-container {
|
width: 210mm;
|
min-height: 297mm;
|
margin: 0 auto;
|
padding: 14mm 13mm;
|
background: #fff;
|
box-shadow: 0 4mm 14mm rgba(15, 23, 42, 0.14);
|
}
|
}
|
|
@media print {
|
@page {
|
size: A4 portrait;
|
margin: 14mm 13mm;
|
}
|
|
body {
|
background: #fff;
|
}
|
|
.section {
|
break-inside: avoid;
|
page-break-inside: avoid;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<main id="print-container"></main>
|
|
<script>
|
(function () {
|
const demoData = {
|
companyName: '江苏某某药业有限公司',
|
docNumber: 'REC-STB-DEST-001',
|
revision: 'A/0',
|
shenqing_bumen_id: '质量控制部',
|
shenqing_shijian: '2026-08-19',
|
kaoca_bianhao: 'WD-KC-2026-001',
|
shenqingren_id: '张三',
|
shifou_xuyao_mah: '是',
|
mah_yijian_kaiguan: '同意',
|
shenqing_beizhu: '稳定性考察样品已完成留样观察,现申请按规程集中销毁。',
|
bumen_shenhe_beizhu: '样品信息核对无误,同意提交质量保证部审核。',
|
bumen_shenheren_id: '李四',
|
bumen_shenhe_shijian: '2026-08-19',
|
zhiliang_baozhengbu_shenhe_beizhu: '同意销毁,请按样品销毁 SOP 执行并留存记录。',
|
zhiliang_baozhengbu_shenheren_id: '王五',
|
zhiliang_baozhengbu_shenhe_shijian: '2026-08-19',
|
mah_yijian: '已确认,无需继续保存。',
|
xiaohui_zhixingren_id: '赵六',
|
xiaohui_beizhu: '已按规定完成销毁。',
|
xiaohuiren_id: '赵六',
|
xiaohui_shijian: '2026-08-20',
|
xiaohui_jilu: '样品经粉碎后作为实验废弃物交由指定单位处理。',
|
zixiang: [
|
{
|
yangpin_mingcheng: '稳定性样品 A',
|
guige: '10mg',
|
pihao: '240801',
|
kaoca_shijian: '长期 12 月',
|
xiancunliang: '18',
|
cunliang_danwei: '盒',
|
xiaohui_yuanyin: '考察周期结束',
|
},
|
{
|
yangpin_mingcheng: '稳定性样品 B',
|
guige: '20mg',
|
pihao: '240802',
|
kaoca_shijian: '加速 6 月',
|
xiancunliang: '12',
|
cunliang_danwei: '盒',
|
xiaohui_yuanyin: '考察周期结束',
|
},
|
],
|
};
|
|
function hasOwn(object, key) {
|
return Object.prototype.hasOwnProperty.call(object || {}, key);
|
}
|
|
function firstValue() {
|
for (let index = 0; index < arguments.length; index++) {
|
const value = arguments[index];
|
if (value !== null && value !== undefined && value !== '') return value;
|
}
|
return '';
|
}
|
|
function escapeHtml(value) {
|
return String(value === null || value === undefined ? '' : value)
|
.replace(/&/g, '&')
|
.replace(/</g, '<')
|
.replace(/>/g, '>')
|
.replace(/"/g, '"')
|
.replace(/'/g, ''');
|
}
|
|
function toList(value) {
|
return Array.isArray(value) ? value : [];
|
}
|
|
function getValue(object, keys) {
|
for (let index = 0; index < keys.length; index++) {
|
const key = keys[index];
|
if (hasOwn(object, key)) return object[key];
|
}
|
return '';
|
}
|
|
function displayValue(object, key) {
|
return firstValue(
|
object[key + '_label'],
|
object[key + '_name'],
|
object[key + 'Name'],
|
object[key + '_fullName'],
|
object[key + 'FullName'],
|
object[key + '_text'],
|
object[key],
|
);
|
}
|
|
function getObjectDisplay(value) {
|
if (!value || typeof value !== 'object') return '';
|
return firstValue(value.fullName, value.realName, value.userName, value.name, value.label, value.text, value.enCode, value.id);
|
}
|
|
function getDisplayValue(object, key) {
|
const candidates = [
|
object[key + '_label'],
|
object[key + '_name'],
|
object[key + 'Name'],
|
object[key + '_fullName'],
|
object[key + 'FullName'],
|
object[key + '_text'],
|
object[key],
|
];
|
for (let index = 0; index < candidates.length; index++) {
|
const value = candidates[index];
|
if (Array.isArray(value)) {
|
const text = value.map(getObjectDisplay).filter(Boolean).join('、');
|
if (text) return text;
|
} else if (value && typeof value === 'object') {
|
const text = getObjectDisplay(value);
|
if (text) return text;
|
} else if (value !== null && value !== undefined && value !== '') {
|
return value;
|
}
|
}
|
return '';
|
}
|
|
function userHtml(object, key) {
|
const value = getDisplayValue(object, key);
|
if (!value) return '';
|
return '<span class="js-user-name" data-user-id="' + escapeHtml(value) + '">' + escapeHtml(value) + '</span>';
|
}
|
|
function formatDate(value) {
|
if (!value) return '';
|
const text = String(value);
|
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return text.slice(0, 10);
|
if (/^\d{4}\/\d{1,2}\/\d{1,2}/.test(text)) {
|
const parts = text.split(/[\/\s]/);
|
return [parts[0], parts[1].padStart(2, '0'), parts[2].padStart(2, '0')].join('-');
|
}
|
if (/^\d{10}$/.test(text) || /^\d{13}$/.test(text)) {
|
const timestamp = Number(text.length === 10 ? text + '000' : text);
|
const date = new Date(timestamp);
|
if (!Number.isNaN(date.getTime())) {
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
return year + '-' + month + '-' + day;
|
}
|
}
|
return text;
|
}
|
|
function yesLike(value) {
|
const text = String(value === null || value === undefined ? '' : value).toLowerCase();
|
return ['1', 'true', 'yes', 'y', '是', '需要', '同意'].indexOf(text) >= 0;
|
}
|
|
function checkLabel(label, checked) {
|
return '<span class="checkbox-line"><span class="box">' + (checked ? '√' : '') + '</span>' + escapeHtml(label) + '</span>';
|
}
|
|
function infoCell(label, value) {
|
return '<th>' + escapeHtml(label) + '</th><td>' + escapeHtml(value) + '</td>';
|
}
|
|
function rawInfoCellSpan(label, html, colspan) {
|
return '<th>' + escapeHtml(label) + '</th><td colspan="' + colspan + '">' + html + '</td>';
|
}
|
|
function rawInfoCell(label, html) {
|
return '<th>' + escapeHtml(label) + '</th><td>' + html + '</td>';
|
}
|
|
function findItems(root, data) {
|
const direct = firstValue(
|
data.items,
|
data.details,
|
data.children,
|
data.zixiang,
|
data.xiaohui_zixiang,
|
data.lims_wendingxing_xiaohui_shenqing_zixiang,
|
root.items,
|
root.details,
|
root.children,
|
root.zixiang,
|
root.xiaohui_zixiang,
|
root.lims_wendingxing_xiaohui_shenqing_zixiang,
|
);
|
if (Array.isArray(direct)) return direct;
|
|
const objects = [data, root];
|
for (let objIndex = 0; objIndex < objects.length; objIndex++) {
|
const object = objects[objIndex] || {};
|
const keys = Object.keys(object);
|
for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) {
|
const value = object[keys[keyIndex]];
|
if (
|
Array.isArray(value) &&
|
value.some(function (row) {
|
return row && typeof row === 'object' && (hasOwn(row, 'yangpin_mingcheng') || hasOwn(row, 'pihao') || hasOwn(row, 'xiaohui_yuanyin'));
|
})
|
) {
|
return value;
|
}
|
}
|
}
|
return [];
|
}
|
|
function renderItems(items) {
|
if (!items.length) {
|
return '<tr><td colspan="7" class="muted">暂无销毁样品明细</td></tr>';
|
}
|
|
return items
|
.map(function (row, index) {
|
const quantity = [getValue(row, ['xiancunliang', 'xian_cun_liang', 'quantity']), getValue(row, ['cunliang_danwei', 'danwei', 'unit'])]
|
.filter(Boolean)
|
.join(' ');
|
const studyTime = [
|
firstValue(formatDate(getValue(row, ['kaoca_shijian'])), getValue(row, ['period'])),
|
getValue(row, ['kaoca_danwei', 'zhouqi_danwei', 'studyUnit', 'periodUnit']),
|
]
|
.filter(Boolean)
|
.join(' ');
|
|
return (
|
'<tr>' +
|
'<td>' +
|
(index + 1) +
|
'</td>' +
|
'<td class="align-left">' +
|
escapeHtml(firstValue(getValue(row, ['yangpin_mingcheng', 'sampleName', 'name']), displayValue(row, 'yangpin_id'))) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(getValue(row, ['guige', 'specification'])) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(getValue(row, ['pihao', 'batchNo', 'batchNumber'])) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(studyTime) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(quantity) +
|
'</td>' +
|
'<td class="align-left">' +
|
escapeHtml(getValue(row, ['xiaohui_yuanyin', 'reason'])) +
|
'</td>' +
|
'</tr>'
|
);
|
})
|
.join('');
|
}
|
|
function normalizeFiles(value) {
|
if (!value) return [];
|
if (Array.isArray(value)) return value;
|
if (typeof value === 'string') {
|
try {
|
const parsed = JSON.parse(value);
|
if (Array.isArray(parsed)) return parsed;
|
if (parsed && typeof parsed === 'object') return [parsed];
|
} catch (error) {
|
return value ? [{ name: value }] : [];
|
}
|
}
|
if (value && typeof value === 'object') {
|
if (Array.isArray(value.fileList)) return value.fileList;
|
if (Array.isArray(value.files)) return value.files;
|
if (Array.isArray(value.list)) return value.list;
|
return [value];
|
}
|
return [];
|
}
|
|
function fileDisplayName(file) {
|
if (!file || typeof file !== 'object') return file || '';
|
return firstValue(
|
file.name,
|
file.fileName,
|
file.fullName,
|
file.originalName,
|
file.originFileName,
|
file.url && String(file.url).split('/').pop(),
|
file.fileId,
|
file.id,
|
);
|
}
|
|
function fileNamesText(value) {
|
return normalizeFiles(value).map(fileDisplayName).filter(Boolean).join('、');
|
}
|
|
const userNameCache = {};
|
|
function readAccessTokenFromStorage(storage) {
|
if (!storage) return '';
|
for (let index = 0; index < storage.length; index++) {
|
const key = storage.key(index);
|
if (!key || key.indexOf('core-access') < 0) continue;
|
try {
|
const value = JSON.parse(storage.getItem(key) || '{}');
|
if (value && value.accessToken) return value.accessToken;
|
} catch (error) {}
|
}
|
return '';
|
}
|
|
function getAccessToken() {
|
return (
|
readAccessTokenFromStorage(window.localStorage) ||
|
readAccessTokenFromStorage(window.parent && window.parent.localStorage) ||
|
''
|
);
|
}
|
|
function normalizeUserList(payload) {
|
const data = payload && payload.data !== undefined ? payload.data : payload;
|
if (Array.isArray(data)) return data;
|
if (Array.isArray(data && data.list)) return data.list;
|
if (Array.isArray(data && data.data)) return data.data;
|
if (Array.isArray(payload && payload.list)) return payload.list;
|
return [];
|
}
|
|
function pickUserName(user) {
|
return firstValue(user && user.fullName, user && user.realName, user && user.userName, user && user.account, user && user.id);
|
}
|
|
async function requestUserNames(ids) {
|
const token = getAccessToken();
|
const headers = {
|
'Accept-Language': 'zh-CN',
|
'App-Code': '',
|
'Content-Type': 'application/json',
|
'Jnpf-Origin': 'pc',
|
'Vue-Version': '3',
|
};
|
if (token) headers.Authorization = token;
|
|
const requests = [
|
{ url: '/dev/api/permission/Users/getUserList', body: { ids: ids } },
|
{ url: '/api/permission/Users/getUserList', body: { ids: ids } },
|
{ url: '/dev/api/permission/Users/getSelectedUserList', body: { ids: ids, currentPage: 1, pageSize: ids.length } },
|
{ url: '/api/permission/Users/getSelectedUserList', body: { ids: ids, currentPage: 1, pageSize: ids.length } },
|
];
|
|
for (let index = 0; index < requests.length; index++) {
|
try {
|
const response = await fetch(requests[index].url, {
|
body: JSON.stringify(requests[index].body),
|
headers: headers,
|
method: 'POST',
|
});
|
if (!response.ok) continue;
|
const payload = await response.json();
|
const users = normalizeUserList(payload);
|
if (!users.length) continue;
|
users.forEach(function (user) {
|
const id = firstValue(user && user.id, user && user.userId);
|
const name = pickUserName(user);
|
if (id && name) userNameCache[id] = name;
|
});
|
return;
|
} catch (error) {}
|
}
|
}
|
|
async function hydrateUserNames(container) {
|
const nodes = Array.prototype.slice.call(container.querySelectorAll('.js-user-name[data-user-id]'));
|
const ids = nodes
|
.map(function (node) {
|
return node.getAttribute('data-user-id');
|
})
|
.filter(function (id) {
|
return id && !userNameCache[id];
|
});
|
const uniqueIds = Array.from(new Set(ids));
|
if (uniqueIds.length) await requestUserNames(uniqueIds);
|
nodes.forEach(function (node) {
|
const id = node.getAttribute('data-user-id');
|
if (id && userNameCache[id]) node.textContent = userNameCache[id];
|
});
|
}
|
|
function render(data) {
|
const source = data || {};
|
const root = source.formData || source.data || source || {};
|
const items = findItems(root, source);
|
const container = document.getElementById('print-container');
|
const hasData = Object.keys(root).length > 0 || items.length > 0;
|
|
if (!hasData) {
|
container.innerHTML = '<div class="empty-state">暂无打印数据</div>';
|
return;
|
}
|
|
const mahRequired = firstValue(root.shifou_xuyao_mah, root.needMah, root.need_mah);
|
const mahOpinion = firstValue(root.mah_yijian, root.mahOpinion);
|
const mahOpinionSection = '<tr><th>MAH 意见</th><td class="opinion-cell" colspan="3">' + escapeHtml(mahOpinion) + '</td></tr>';
|
|
container.innerHTML =
|
'<article class="page">' +
|
'<header class="doc-header">' +
|
'<div class="doc-title">稳定性销毁申请单</div>' +
|
'<div class="doc-subtitle">Stability Sample Destruction Application</div>' +
|
'</header>' +
|
'<section class="section">' +
|
'<div class="section-title">申请信息</div>' +
|
'<table class="info-table"><tbody>' +
|
'<tr>' +
|
infoCell('考察编号', firstValue(root.kaoca_bianhao, root.studyNo)) +
|
infoCell('申请日期', formatDate(root.shenqing_shijian)) +
|
'</tr><tr>' +
|
infoCell('申请部门', displayValue(root, 'shenqing_bumen_id')) +
|
rawInfoCell('申请人', userHtml(root, 'shenqingren_id')) +
|
'</tr><tr>' +
|
rawInfoCellSpan('是否需 MAH', checkLabel('是', yesLike(mahRequired)) + checkLabel('否', mahRequired !== '' && !yesLike(mahRequired)), 3) +
|
'</tr><tr>' +
|
'<th>申请备注</th><td colspan="3">' +
|
escapeHtml(root.shenqing_beizhu) +
|
'</td></tr></tbody></table>' +
|
'</section>' +
|
'<section class="section">' +
|
'<div class="section-title">销毁样品明细</div>' +
|
'<table class="items-table"><colgroup><col style="width: 8mm" /><col style="width: 38mm" /><col style="width: 24mm" /><col style="width: 22mm" /><col style="width: 25mm" /><col style="width: 20mm" /><col style="width: 45mm" /></colgroup>' +
|
'<thead><tr><th>序号</th><th>样品名称</th><th>规格</th><th>批号</th><th>考察时间</th><th>现存量</th><th>销毁原因</th></tr></thead><tbody>' +
|
renderItems(items) +
|
'</tbody></table>' +
|
'</section>' +
|
'<section class="section">' +
|
'<div class="section-title">审核意见</div>' +
|
'<table><tbody>' +
|
'<tr><th style="width: 32mm">部门审核</th><td class="opinion-cell" colspan="3">' +
|
escapeHtml(root.bumen_shenhe_beizhu) +
|
'</td></tr><tr><td colspan="4" style="padding: 0"><div class="audit-meta-grid"><div>审核人:' +
|
userHtml(root, 'bumen_shenheren_id') +
|
'</div><div>审核日期:' +
|
escapeHtml(formatDate(root.bumen_shenhe_shijian)) +
|
'</div></div></td></tr>' +
|
'<tr><th>质量保证部审核</th><td class="opinion-cell" colspan="3">' +
|
escapeHtml(root.zhiliang_baozhengbu_shenhe_beizhu) +
|
'</td></tr><tr><td colspan="4" style="padding: 0"><div class="audit-meta-grid"><div>审核人:' +
|
userHtml(root, 'zhiliang_baozhengbu_shenheren_id') +
|
'</div><div>审核日期:' +
|
escapeHtml(formatDate(root.zhiliang_baozhengbu_shenhe_shijian)) +
|
'</div></div></td></tr>' +
|
mahOpinionSection +
|
'</tbody></table>' +
|
'</section>' +
|
'<section class="section">' +
|
'<div class="section-title">销毁执行记录</div>' +
|
'<table><tbody>' +
|
'<tr><th style="width: 32mm">执行记录</th><td class="record-cell" colspan="3">' +
|
escapeHtml(root.xiaohui_beizhu) +
|
'</td></tr><tr>' +
|
'<th>附件</th><td colspan="3">' +
|
escapeHtml(fileNamesText(root.xiaohui_jilu)) +
|
'</td></tr><tr>' +
|
rawInfoCell('销毁执行人', userHtml(root, 'xiaohui_zhixingren_id')) +
|
infoCell('销毁日期', formatDate(root.xiaohui_shijian)) +
|
'</tr></tbody></table>' +
|
'</section>' +
|
'</article>';
|
|
hydrateUserNames(container);
|
}
|
|
const injectedData = typeof window.templateData === 'undefined' ? demoData : window.templateData || {};
|
window.renderStabilityDestroyApply = render;
|
render(injectedData);
|
})();
|
</script>
|
</body>
|
</html>
|