1332 lines
45 KiB
HTML
1332 lines
45 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>工资单 · Damon by Maybe Lab</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-1: #e8dec5;
|
||
--bg-2: #d8c9a3;
|
||
--paper: #fbf6e7;
|
||
--paper-edge: #f3ead0;
|
||
--ink: #1b1c2b;
|
||
--ink-2: #383a55;
|
||
--ink-3: #6c6577;
|
||
--muted: #8b8273;
|
||
--line: #c9b88e;
|
||
--line-soft: rgba(184, 137, 90, 0.22);
|
||
--bronze: #8b6e3c;
|
||
--bronze-deep: #5e4a25;
|
||
--bronze-light: #d6b985;
|
||
--wine: #6b1e1e;
|
||
--wine-soft: #8a2a2a;
|
||
--green: #4a6e3a;
|
||
--seal: #a05248;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; padding: 0; }
|
||
body {
|
||
font-family: "Noto Serif SC", "Songti SC", "STSong", serif;
|
||
color: var(--ink);
|
||
font-feature-settings: "kern" 1, "palt" 1;
|
||
background:
|
||
radial-gradient(ellipse at top, #f3e9cf 0%, transparent 60%),
|
||
radial-gradient(ellipse at bottom right, #d8c69a 0%, transparent 50%),
|
||
linear-gradient(180deg, var(--bg-1), var(--bg-2));
|
||
background-attachment: fixed;
|
||
min-height: 100vh;
|
||
padding: 36px 16px 80px;
|
||
}
|
||
body::before {
|
||
content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
||
background-image:
|
||
repeating-linear-gradient(45deg, transparent 0 24px, rgba(94,74,37,0.022) 24px 25px),
|
||
repeating-linear-gradient(-45deg, transparent 0 24px, rgba(94,74,37,0.018) 24px 25px);
|
||
}
|
||
|
||
/* ============ Toolbar ============ */
|
||
.toolbar {
|
||
position: relative; z-index: 2;
|
||
max-width: 920px;
|
||
margin: 0 auto 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 14px 22px;
|
||
background: linear-gradient(180deg, #2a2c43 0%, #1b1c2b 100%);
|
||
color: #e9dfc4;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset, 0 12px 32px rgba(28,30,55,0.30);
|
||
border-top: 2px solid var(--bronze);
|
||
}
|
||
.toolbar .brand {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 500; font-size: 17px;
|
||
letter-spacing: 0.12em;
|
||
display: flex; align-items: center; gap: 12px;
|
||
}
|
||
.toolbar .brand .ornament { color: var(--bronze-light); font-size: 13px; letter-spacing: 0; }
|
||
.toolbar h1 {
|
||
margin: 0; font-size: 12px; font-weight: 400;
|
||
color: #c8b78a; letter-spacing: 0.14em;
|
||
font-family: "EB Garamond", serif;
|
||
}
|
||
.toolbar .actions { display: flex; gap: 8px; }
|
||
button {
|
||
padding: 8px 18px; font-size: 11px;
|
||
font-family: "EB Garamond", serif;
|
||
border: 1px solid var(--bronze);
|
||
background: var(--bronze); color: #fdf9ef;
|
||
border-radius: 2px; cursor: pointer;
|
||
letter-spacing: 0.16em; text-transform: uppercase;
|
||
transition: all 0.2s;
|
||
}
|
||
button:hover { background: var(--bronze-deep); border-color: var(--bronze-deep); }
|
||
button.ghost {
|
||
background: transparent; color: #c8b78a; border-color: #6a6b85;
|
||
}
|
||
button.ghost:hover { background: rgba(255,255,255,0.06); color: #fff; }
|
||
|
||
/* ============ Slip Card ============ */
|
||
.slip {
|
||
position: relative; z-index: 1;
|
||
max-width: 920px;
|
||
margin: 0 auto 28px;
|
||
background: radial-gradient(ellipse at top, #fffaec 0%, var(--paper) 50%, var(--paper-edge) 100%);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
box-shadow:
|
||
0 1px 0 rgba(255,255,255,0.7) inset,
|
||
0 24px 60px -24px rgba(28,30,55,0.4),
|
||
0 6px 16px rgba(28,30,55,0.10);
|
||
border: 1px solid var(--line);
|
||
}
|
||
.slip::before {
|
||
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 6px;
|
||
background: linear-gradient(90deg, var(--bronze-deep) 0%, var(--bronze) 20%, var(--bronze-light) 50%, var(--bronze) 80%, var(--bronze-deep) 100%);
|
||
}
|
||
.slip::after {
|
||
content: ""; position: absolute; top: 16px; left: 16px; right: 16px; bottom: 16px;
|
||
border: 1px solid var(--line-soft); border-radius: 2px; pointer-events: none;
|
||
}
|
||
.slip > .corner {
|
||
position: absolute; width: 22px; height: 22px;
|
||
color: var(--bronze); opacity: 0.55; pointer-events: none;
|
||
}
|
||
.slip > .corner.tl { top: 14px; left: 14px; border-top: 2px solid currentColor; border-left: 2px solid currentColor; }
|
||
.slip > .corner.tr { top: 14px; right: 14px; border-top: 2px solid currentColor; border-right: 2px solid currentColor; }
|
||
.slip > .corner.bl { bottom: 14px; left: 14px; border-bottom: 2px solid currentColor; border-left: 2px solid currentColor; }
|
||
.slip > .corner.br { bottom: 14px; right: 14px; border-bottom: 2px solid currentColor; border-right: 2px solid currentColor; }
|
||
|
||
/* Watermark */
|
||
.slip .watermark {
|
||
position: absolute; right: 32px; bottom: 80px;
|
||
font-family: "Noto Serif SC", "Songti SC", serif;
|
||
font-size: 240px; line-height: 0.85;
|
||
color: var(--bronze); opacity: 0.038;
|
||
pointer-events: none; user-select: none;
|
||
letter-spacing: -0.05em; z-index: 0;
|
||
}
|
||
|
||
/* ============ Header (compact, horizontal) ============ */
|
||
.slip-header {
|
||
position: relative;
|
||
padding: 22px 40px 14px;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
.header-left, .header-right { min-width: 0; }
|
||
.header-left { text-align: left; }
|
||
.header-right { text-align: right; }
|
||
.brand-line {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 400;
|
||
color: var(--bronze-deep);
|
||
font-size: 11px; letter-spacing: 0.14em;
|
||
margin-bottom: 4px; text-transform: uppercase;
|
||
}
|
||
.brand-title {
|
||
font-family: "Noto Serif SC", serif;
|
||
font-weight: 600; font-size: 22px;
|
||
color: var(--ink); letter-spacing: 0.1em;
|
||
margin-left: 0.28em;
|
||
}
|
||
.brand-title .of { font-weight: 300; color: var(--muted); font-size: 14px; letter-spacing: 0.1em; margin: 0 6px; }
|
||
.brand-meta {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 400;
|
||
color: var(--ink-3); font-size: 11px;
|
||
margin-top: 4px; letter-spacing: 0.16em;
|
||
}
|
||
.header-center {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
color: var(--bronze-deep);
|
||
}
|
||
.header-center .label {
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 11px; letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: var(--bronze-deep);
|
||
padding: 3px 14px;
|
||
border-top: 1px solid var(--bronze);
|
||
border-bottom: 1px solid var(--bronze);
|
||
white-space: nowrap;
|
||
}
|
||
.period {
|
||
font-family: "Noto Serif SC", serif;
|
||
font-size: 17px; font-weight: 500;
|
||
letter-spacing: 0.1em; color: var(--ink);
|
||
line-height: 1.2;
|
||
}
|
||
.period .en {
|
||
display: block;
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 400;
|
||
color: var(--ink-3); font-size: 11px;
|
||
letter-spacing: 0.1em; margin-top: 3px;
|
||
}
|
||
.doc-no {
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 10px; color: var(--muted);
|
||
letter-spacing: 0.1em; margin-top: 6px;
|
||
}
|
||
|
||
/* ============ Employee Row ============ */
|
||
.header-strip {
|
||
position: relative;
|
||
margin: 12px 40px 0;
|
||
padding: 14px 0 12px;
|
||
border-top: 1px solid var(--line);
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 20px; flex-wrap: wrap;
|
||
}
|
||
.header-strip::before, .header-strip::after {
|
||
content: ""; position: absolute; left: 0; right: 0;
|
||
border-top: 1px solid var(--line-soft);
|
||
}
|
||
.header-strip::before { top: 3px; }
|
||
.header-strip::after { bottom: 3px; }
|
||
.name-block {
|
||
display: flex; align-items: baseline; gap: 14px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.name-block .name {
|
||
font-size: 28px; font-weight: 700;
|
||
color: var(--ink); letter-spacing: 0.16em; line-height: 1;
|
||
}
|
||
.name-block .position {
|
||
font-size: 12px; color: var(--ink-2);
|
||
letter-spacing: 0.14em; font-weight: 400;
|
||
}
|
||
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.tag {
|
||
font-size: 10px; padding: 3px 10px 2px;
|
||
color: var(--bronze-deep); background: rgba(184, 137, 90, 0.10);
|
||
border: 1px solid var(--line-soft); border-radius: 1px;
|
||
letter-spacing: 0.1em; font-weight: 500;
|
||
}
|
||
|
||
/* ============ Metrics strip — full width, 7 cells ============ */
|
||
.metrics {
|
||
margin: 12px 40px 0;
|
||
display: grid; grid-template-columns: repeat(7, 1fr);
|
||
background: linear-gradient(180deg, rgba(216,201,163,0.22), rgba(216,201,163,0.08));
|
||
border: 1px solid var(--line-soft);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
.metric {
|
||
text-align: center; padding: 9px 4px;
|
||
position: relative; min-width: 0;
|
||
}
|
||
.metric + .metric::before {
|
||
content: ""; position: absolute; left: 0; top: 18%; bottom: 18%;
|
||
border-left: 1px solid var(--line-soft);
|
||
}
|
||
.metric .value {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 600; font-size: 19px;
|
||
color: var(--ink); line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
.metric.money .value { font-size: 15px; }
|
||
.metric .value .prefix {
|
||
font-size: 11px; color: var(--bronze);
|
||
margin-right: 1px; font-weight: 400;
|
||
vertical-align: 1px;
|
||
}
|
||
.metric .value.dash { color: var(--muted); font-style: italic; font-size: 17px; }
|
||
.metric .value-unit {
|
||
font-size: 11px; margin-left: 2px;
|
||
color: var(--bronze); font-weight: 400;
|
||
}
|
||
.metric .label {
|
||
font-size: 9.5px; letter-spacing: 0.14em;
|
||
color: var(--ink-2); margin-top: 5px;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
.metric .label-cn {
|
||
font-family: "Noto Serif SC", serif;
|
||
letter-spacing: 0.1em; color: var(--ink-2);
|
||
margin-right: 2px;
|
||
}
|
||
.metric .label .sep { color: var(--muted); margin: 0 2px; }
|
||
/* 业绩环比涨跌徽标(顶部业绩条) */
|
||
.metric .mom {
|
||
display: block; margin-top: 3px;
|
||
font-size: 10px; font-weight: 600;
|
||
letter-spacing: 0.01em; font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
.metric .mom.up { color: var(--green); }
|
||
.metric .mom.down { color: var(--wine); }
|
||
.metric .mom.flat, .metric .mom.new { color: var(--muted); font-weight: 500; }
|
||
.metric .mom .mom-cap {
|
||
display: block;
|
||
font-size: 8px; font-weight: 400;
|
||
color: var(--muted); letter-spacing: 0.08em;
|
||
text-transform: none; margin-bottom: 1px;
|
||
}
|
||
|
||
/* Revenue line */
|
||
.revenue-line {
|
||
margin: 10px 40px 0;
|
||
padding: 12px 18px 10px;
|
||
background:
|
||
linear-gradient(180deg, rgba(216,201,163,0.14), rgba(216,201,163,0.04));
|
||
border: 1px solid var(--line-soft);
|
||
border-radius: 2px;
|
||
text-align: center;
|
||
}
|
||
.revenue-line .rev-label {
|
||
display: block;
|
||
font-family: "EB Garamond", serif;
|
||
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.12em;
|
||
color: var(--bronze-deep);
|
||
text-transform: uppercase;
|
||
margin-bottom: 6px;
|
||
}
|
||
.revenue-line .rev-items {
|
||
display: flex; gap: 36px;
|
||
justify-content: center; align-items: baseline;
|
||
flex-wrap: wrap;
|
||
}
|
||
.revenue-line .rev-item {
|
||
display: inline-flex; flex-direction: column;
|
||
align-items: center; gap: 2px;
|
||
}
|
||
.revenue-line .rev-item .v {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 600;
|
||
font-size: 22px;
|
||
color: var(--ink);
|
||
line-height: 1;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.revenue-line .rev-item .v::before {
|
||
content: "¥";
|
||
margin-right: 3px;
|
||
color: var(--bronze);
|
||
font-size: 14px;
|
||
vertical-align: 2px;
|
||
font-weight: 500;
|
||
}
|
||
.revenue-line .rev-item .lbl {
|
||
font-size: 10px;
|
||
letter-spacing: 0.12em;
|
||
color: var(--ink-2);
|
||
text-transform: uppercase;
|
||
}
|
||
.revenue-line .rev-item .lbl .cn {
|
||
font-family: "Noto Serif SC", serif;
|
||
color: var(--ink-2);
|
||
letter-spacing: 0.1em;
|
||
margin-right: 4px;
|
||
}
|
||
/* 环比涨跌徽标 */
|
||
.revenue-line .rev-item .mom {
|
||
display: block;
|
||
margin-top: 3px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.revenue-line .rev-item .mom.up { color: var(--green); }
|
||
.revenue-line .rev-item .mom.down { color: var(--wine); }
|
||
.revenue-line .rev-item .mom.flat,
|
||
.revenue-line .rev-item .mom.new { color: var(--muted); font-weight: 500; }
|
||
.revenue-line .mom-note {
|
||
font-style: normal; font-size: 10px; color: var(--muted);
|
||
letter-spacing: 0.04em; margin-left: 4px;
|
||
}
|
||
|
||
/* ============ Body ============ */
|
||
.slip-body { position: relative; z-index: 1; padding: 6px 40px 6px; }
|
||
.slip-section { margin-top: 16px; }
|
||
.section-head {
|
||
display: flex; align-items: center; margin-bottom: 8px;
|
||
}
|
||
/* 2-column aux row (deductions + notes side by side) */
|
||
.aux-row {
|
||
display: flex; flex-wrap: wrap; gap: 24px;
|
||
}
|
||
.aux-row > .slip-section {
|
||
flex: 1; min-width: 320px; margin-top: 16px;
|
||
}
|
||
.section-no {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 500;
|
||
font-size: 22px; color: var(--bronze);
|
||
line-height: 1; width: 32px;
|
||
}
|
||
.section-title {
|
||
font-size: 12px; font-weight: 600;
|
||
color: var(--ink); letter-spacing: 0.14em;
|
||
padding: 0 14px 0 4px;
|
||
}
|
||
.section-title-en {
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 11px;
|
||
color: var(--muted); letter-spacing: 0.1em;
|
||
flex: 1; border-bottom: 1px solid var(--line-soft);
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
/* ============ 4-Column Matrix ============ */
|
||
.matrix {
|
||
margin-left: 36px;
|
||
background:
|
||
repeating-linear-gradient(0deg, transparent 0 30px, rgba(184,137,90,0.02) 30px 31px);
|
||
border-top: 1px solid var(--bronze);
|
||
border-bottom: 1px solid var(--bronze);
|
||
}
|
||
.matrix-head, .matrix-row {
|
||
display: grid;
|
||
grid-template-columns: 1.5fr 1fr 1.4fr 1fr;
|
||
align-items: baseline;
|
||
}
|
||
.matrix-head {
|
||
padding: 9px 0 8px;
|
||
border-bottom: 1px solid var(--bronze);
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 10.5px;
|
||
color: var(--bronze-deep);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.14em;
|
||
}
|
||
.matrix-head > div { padding: 0 14px; }
|
||
.matrix-head > div:nth-child(n+2) { text-align: right; }
|
||
.matrix-row {
|
||
padding: 6px 0;
|
||
font-size: 13px;
|
||
border-bottom: 1px dotted rgba(94,74,37,0.16);
|
||
}
|
||
.matrix-row:last-child { border-bottom: none; }
|
||
.matrix-row > div { padding: 0 14px; }
|
||
/* 未交社保 · 基本工资下方强调注明 */
|
||
.matrix-note {
|
||
margin: 4px 6px 8px;
|
||
padding: 9px 14px;
|
||
background: linear-gradient(135deg, rgba(155,41,35,0.10) 0%, rgba(155,41,35,0.03) 100%);
|
||
border-left: 3px solid var(--wine);
|
||
border-radius: 2px;
|
||
font-size: 12.5px;
|
||
line-height: 1.6;
|
||
color: var(--ink);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.matrix-note strong { color: var(--wine); font-weight: 700; }
|
||
.matrix-note .amt {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
color: var(--wine);
|
||
margin-left: 2px;
|
||
}
|
||
.matrix-row .m-item .cn {
|
||
color: var(--ink); font-weight: 500;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.matrix-row .m-item .en {
|
||
display: block;
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 10.5px;
|
||
color: var(--muted); letter-spacing: 0.12em;
|
||
margin-top: 1px;
|
||
}
|
||
.matrix-row > div.m-std,
|
||
.matrix-row > div.m-perf,
|
||
.matrix-row > div.m-act {
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.m-std { color: var(--ink-2); }
|
||
.m-std .money::before {
|
||
content: "¥"; color: var(--bronze); margin-right: 2px;
|
||
font-size: 11px; font-family: "EB Garamond", serif;
|
||
}
|
||
.m-perf { color: var(--ink-2); font-size: 12.5px; }
|
||
.m-perf .num {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 600; color: var(--bronze-deep);
|
||
font-size: 14px; margin-right: 1px;
|
||
}
|
||
.m-perf .unit { color: var(--muted); font-size: 11px; }
|
||
.m-perf .ok { color: var(--green); font-weight: 500; }
|
||
.m-perf .mini { font-size: 11px; color: var(--muted); }
|
||
.m-perf .mini .v { color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
||
.m-perf .mini .v::before { content: "¥"; margin-right: 1px; font-size: 9.5px; color: var(--bronze); }
|
||
.m-act { color: var(--ink); font-weight: 600; }
|
||
.m-act .money {
|
||
color: var(--wine);
|
||
}
|
||
.m-act .money::before {
|
||
content: "¥"; color: var(--wine-soft); margin-right: 2px;
|
||
font-size: 11px; font-family: "EB Garamond", serif; font-weight: 500;
|
||
}
|
||
.dash { color: var(--muted); font-style: italic; opacity: 0.6; }
|
||
|
||
/* ============ Deductions ============ */
|
||
.deductions {
|
||
margin-left: 32px;
|
||
display: flex; flex-wrap: wrap; gap: 18px;
|
||
padding: 4px 4px 0;
|
||
}
|
||
.deduction {
|
||
flex: 1; min-width: 140px;
|
||
border-left: 2px solid var(--wine-soft);
|
||
padding: 4px 12px;
|
||
}
|
||
.deduction .label {
|
||
font-size: 11px; color: var(--ink-2);
|
||
letter-spacing: 0.06em; margin-bottom: 2px;
|
||
}
|
||
.deduction .value {
|
||
font-size: 15px; color: var(--wine);
|
||
font-weight: 600; font-variant-numeric: tabular-nums;
|
||
}
|
||
.deduction .value::before {
|
||
content: "− ¥"; margin-right: 2px;
|
||
font-size: 11px; color: var(--wine-soft);
|
||
font-family: "EB Garamond", serif; font-weight: 500;
|
||
}
|
||
|
||
/* ============ Remark ============ */
|
||
.remark-block {
|
||
margin-left: 32px; margin-top: 10px;
|
||
padding: 10px 16px 11px;
|
||
background: linear-gradient(180deg, rgba(184,137,90,0.07), rgba(184,137,90,0.03));
|
||
border-left: 2px solid var(--bronze);
|
||
border-radius: 0 1px 1px 0;
|
||
}
|
||
.remark {
|
||
font-size: 12.5px; color: var(--ink-2);
|
||
line-height: 1.85; white-space: pre-line;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
/* ============ Total ============ */
|
||
.slip-total {
|
||
margin: 18px 40px 0;
|
||
padding: 16px 22px 14px;
|
||
background:
|
||
radial-gradient(circle at 100% 0%, rgba(214,185,133,0.18) 0%, transparent 50%),
|
||
linear-gradient(135deg, var(--ink) 0%, #252a47 100%);
|
||
color: #f0e5c8;
|
||
border-radius: 3px;
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.slip-total::before {
|
||
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||
background: linear-gradient(90deg, var(--bronze-deep), var(--bronze-light), var(--bronze-deep));
|
||
}
|
||
.slip-total::after {
|
||
content: ""; position: absolute; top: 10px; left: 10px; right: 10px; bottom: 10px;
|
||
border: 1px solid rgba(214, 185, 133, 0.2); border-radius: 2px; pointer-events: none;
|
||
}
|
||
.slip-total .total-row {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.slip-total .total-label {
|
||
font-family: "Noto Serif SC", serif;
|
||
font-size: 12px; letter-spacing: 0.14em;
|
||
color: #d1bd8c; font-weight: 500;
|
||
}
|
||
.slip-total .total-label .en {
|
||
display: block;
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 10px;
|
||
letter-spacing: 0.14em; color: #a89770;
|
||
margin-top: 4px; font-weight: 400;
|
||
}
|
||
.slip-total .total-value { text-align: right; }
|
||
.slip-total .total-value .num {
|
||
font-family: "EB Garamond", serif;
|
||
font-weight: 600; font-size: 36px;
|
||
color: var(--bronze-light); letter-spacing: 0.02em; line-height: 1;
|
||
}
|
||
.slip-total .total-value .num::before {
|
||
content: "¥"; font-size: 22px; margin-right: 4px;
|
||
vertical-align: 4px; color: var(--bronze);
|
||
}
|
||
.slip-total .total-value .num.empty { font-style: italic; color: #a89770; font-size: 22px; }
|
||
.slip-total .total-value .num.empty::before { display: none; }
|
||
.slip-total .capital {
|
||
margin-top: 10px; padding-top: 10px;
|
||
border-top: 1px dotted rgba(214, 185, 133, 0.25);
|
||
font-size: 11.5px; color: #c4ae7e;
|
||
letter-spacing: 0.16em; text-align: right;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.slip-total .capital .pre {
|
||
color: #a89770; margin-right: 6px;
|
||
font-family: "EB Garamond", serif;
|
||
letter-spacing: 0.1em;
|
||
}
|
||
.slip-total .sub-row {
|
||
margin-top: 8px;
|
||
display: flex; justify-content: space-between;
|
||
font-size: 12px; color: #b7a37a;
|
||
letter-spacing: 0.12em;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.slip-total .sub-row .value {
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 15px; color: #c4ae7e;
|
||
}
|
||
.slip-total .sub-row .value::before { content: "¥"; margin-right: 2px; color: var(--bronze); font-size: 12px; }
|
||
|
||
/* When insurance deduction exists, focus shifts to 剩余应发 */
|
||
.slip-total .breakdown {
|
||
position: relative; z-index: 1;
|
||
margin-bottom: 14px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px dotted rgba(214, 185, 133, 0.28);
|
||
}
|
||
.slip-total .bd-row {
|
||
display: flex; justify-content: space-between;
|
||
align-items: baseline;
|
||
font-size: 13px;
|
||
color: #c4ae7e;
|
||
padding: 3px 0;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.slip-total .bd-row .bd-label {
|
||
color: #a89770;
|
||
font-size: 12px;
|
||
letter-spacing: 0.12em;
|
||
}
|
||
.slip-total .bd-row .bd-label .en {
|
||
color: #847458;
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 11px;
|
||
margin-left: 6px;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
.slip-total .bd-row .bd-value {
|
||
font-family: "EB Garamond", serif;
|
||
font-size: 16px;
|
||
color: #c4ae7e;
|
||
font-variant-numeric: tabular-nums;
|
||
font-weight: 500;
|
||
}
|
||
.slip-total .bd-row .bd-value::before {
|
||
content: "¥";
|
||
color: var(--bronze);
|
||
margin-right: 2px;
|
||
font-size: 12px;
|
||
}
|
||
.slip-total .bd-row.deduction .bd-value { color: #d8a8a0; }
|
||
.slip-total .bd-row.deduction .bd-value::before {
|
||
content: "− ¥";
|
||
color: #c89898;
|
||
margin-right: 1px;
|
||
}
|
||
|
||
/* Company cost note */
|
||
.company-cost {
|
||
margin: 8px 38px 0;
|
||
padding: 0 4px;
|
||
font-size: 10.5px; color: var(--muted);
|
||
letter-spacing: 0.1em;
|
||
text-align: right;
|
||
|
||
}
|
||
.company-cost .v {
|
||
font-family: "EB Garamond", serif;
|
||
color: var(--ink-2); font-weight: 500;
|
||
margin-left: 6px; font-variant-numeric: tabular-nums;
|
||
}
|
||
.company-cost .v::before {
|
||
content: "¥"; margin-right: 1px; color: var(--bronze);
|
||
}
|
||
|
||
/* ============ Footer ============ */
|
||
.slip-footer {
|
||
position: relative; z-index: 1;
|
||
margin: 18px 40px 20px;
|
||
padding: 16px 0 0;
|
||
border-top: 1px solid var(--line-soft);
|
||
display: flex; align-items: flex-end; justify-content: space-between;
|
||
min-height: 80px;
|
||
}
|
||
.confirm-note {
|
||
flex: 1;
|
||
font-family: "Noto Serif SC", serif;
|
||
font-size: 15.5px;
|
||
color: var(--ink);
|
||
letter-spacing: 0.04em;
|
||
line-height: 1.6;
|
||
font-weight: 500;
|
||
padding: 14px 20px;
|
||
background:
|
||
linear-gradient(135deg, rgba(155, 41, 35, 0.08) 0%, rgba(155, 41, 35, 0.02) 100%);
|
||
border: 1px solid rgba(155, 41, 35, 0.18);
|
||
border-left: 3px solid var(--wine);
|
||
border-radius: 2px;
|
||
margin-right: 20px;
|
||
position: relative;
|
||
}
|
||
.confirm-note::before {
|
||
content: "✉";
|
||
position: absolute;
|
||
top: 10px; right: 14px;
|
||
font-size: 22px;
|
||
color: var(--wine);
|
||
opacity: 0.35;
|
||
}
|
||
.confirm-note .keyword {
|
||
display: inline-block;
|
||
color: #fff;
|
||
background: var(--wine);
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
padding: 2px 10px 3px;
|
||
margin: 0 3px;
|
||
border-radius: 2px;
|
||
letter-spacing: 0.08em;
|
||
transform: translateY(-1px);
|
||
}
|
||
.confirm-note .hint {
|
||
color: var(--ink-2);
|
||
font-size: 13px;
|
||
margin-top: 6px;
|
||
display: block;
|
||
font-weight: 400;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.seal {
|
||
position: relative; width: 84px; height: 84px;
|
||
border-radius: 50%;
|
||
border: 2px solid var(--seal);
|
||
color: var(--seal);
|
||
background: rgba(160, 82, 72, 0.04);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
opacity: 0.75;
|
||
transform: rotate(-6deg);
|
||
}
|
||
.seal::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 5px; left: 5px; right: 5px; bottom: 5px;
|
||
border-radius: 50%;
|
||
border: 1px solid var(--seal);
|
||
opacity: 0.45;
|
||
}
|
||
.seal::after {
|
||
content: "★";
|
||
position: absolute;
|
||
bottom: 11px; left: 50%; transform: translateX(-50%);
|
||
font-size: 9px; color: var(--seal);
|
||
opacity: 0.55;
|
||
}
|
||
.seal .seal-text {
|
||
font-family: "Noto Serif SC", "STSong", serif;
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
color: var(--seal);
|
||
letter-spacing: 0.06em;
|
||
line-height: 1.15;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
text-align: center;
|
||
gap: 1px 3px;
|
||
margin-top: -6px;
|
||
}
|
||
|
||
/* ============ Actions ============ */
|
||
.slip-actions {
|
||
padding: 10px 40px 14px; text-align: right;
|
||
background: rgba(216, 198, 154, 0.16);
|
||
border-top: 1px solid var(--line-soft);
|
||
position: relative; z-index: 2;
|
||
}
|
||
.slip-actions button { padding: 6px 16px; font-size: 10px; }
|
||
|
||
/* ============ Empty State ============ */
|
||
.empty-state {
|
||
position: relative; z-index: 1;
|
||
max-width: 480px; margin: 60px auto;
|
||
text-align: center; color: var(--ink-2);
|
||
background: var(--paper); padding: 40px;
|
||
border-radius: 3px; border: 1px solid var(--line);
|
||
}
|
||
.empty-state code {
|
||
background: rgba(28,30,55,0.06); padding: 2px 8px;
|
||
border-radius: 2px; font-size: 13px; font-family: "SF Mono", monospace;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="toolbar">
|
||
<div class="brand">DAMON <span class="ornament">·</span> MAYBE LAB <span class="ornament">·</span> 工资单</div>
|
||
<h1 id="title">—</h1>
|
||
<div class="actions">
|
||
<button class="ghost" onclick="location.reload()">REFRESH</button>
|
||
<button onclick="downloadAll()">EXPORT ALL</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="slips"></div>
|
||
|
||
<script src="data.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||
|
||
<script>
|
||
/* ===========================================================
|
||
* 字段统一定义:计薪明细 4 栏(项目 / 标准 / 完成度 / 实发)
|
||
* 每行 = 一个发薪项目,自动隐藏全空行
|
||
* =========================================================== */
|
||
const MATRIX = [
|
||
{
|
||
label: "基本工资", en: "Base Salary",
|
||
standardKey: "基本工资标准",
|
||
actualKey: "基本工资",
|
||
perf: (r) => {
|
||
const att = r["出勤天数"];
|
||
if (!nonEmpty(att)) return "";
|
||
return `<span class="num">${att}</span><span class="unit"> 天</span>`;
|
||
},
|
||
},
|
||
{
|
||
label: "KPI 绩效", en: "KPI Bonus",
|
||
standardKey: "KPI绩效标准",
|
||
actualKey: "KPI绩效结果",
|
||
perf: (r) => {
|
||
const s = r["KPI得分"];
|
||
if (!nonEmpty(s)) return "";
|
||
return `<span class="num">${s}</span> <span class="unit">档</span>`;
|
||
},
|
||
},
|
||
{
|
||
label: "管理绩效", en: "Management",
|
||
standardKey: "管理绩效标准",
|
||
actualKey: "管理绩效奖金",
|
||
perf: (r) => {
|
||
const std = parseFloat(r["管理绩效标准"]);
|
||
if (!Number.isFinite(std) || std <= 0) return ""; // 没有管理绩效则不填完成度
|
||
const shift = r["班次业绩"];
|
||
if (!nonEmpty(shift) || isZero(shift)) return "";
|
||
return `<span class="mini">班次业绩 <span class="v">${fmtNumber(shift)}</span></span>`;
|
||
},
|
||
hideIfZeroStd: true,
|
||
},
|
||
{
|
||
label: "行为规范", en: "Conduct",
|
||
standardKey: "行为规范绩效",
|
||
actualKey: "行为规范绩效结果",
|
||
perf: (r) => {
|
||
const act = parseFloat(r["行为规范绩效结果"]);
|
||
const std = parseFloat(r["行为规范绩效"]);
|
||
if (!Number.isFinite(act) || !Number.isFinite(std)) return "";
|
||
return act >= std ? `<span class="ok">✓ 合格</span>` : `<span style="color:var(--wine)">未达标</span>`;
|
||
},
|
||
},
|
||
{
|
||
label: "加班工资", en: "Overtime",
|
||
actualKey: "加班工资",
|
||
perf: (r) => {
|
||
if (isOtBanked(r)) return "";
|
||
const h = r["加班小时数"];
|
||
if (!nonEmpty(h) || isZero(h)) return "";
|
||
return `<span class="num">${h}</span> <span class="unit">小时</span>`;
|
||
},
|
||
},
|
||
{
|
||
label: "出品提成", en: "Commission",
|
||
actualKey: "出品提成",
|
||
perf: (r) => {
|
||
// 无实际提成则不展示完成度(整行随之隐藏)
|
||
if (!nonEmpty(r["出品提成"]) || isZero(r["出品提成"])) return "";
|
||
const dept = r["部门业绩"];
|
||
if (!nonEmpty(dept) || isZero(dept)) return "";
|
||
return `<span class="mini">部门业绩 <span class="v">${fmtNumber(dept)}</span></span>`;
|
||
},
|
||
},
|
||
{
|
||
label: "法定假期补贴", en: "Holiday Pay",
|
||
actualKey: "节假日出勤补贴",
|
||
perf: (r) => {
|
||
const d = r["法定假期天数"];
|
||
if (!nonEmpty(d) || parseFloat(d) <= 0) return "";
|
||
return `<span class="num">${d}</span> <span class="unit">天</span>`;
|
||
},
|
||
},
|
||
{ label: "串店交通补贴", en: "Transit", actualKey: "串店交通补贴" },
|
||
{ label: "特别奖金", en: "Special Bonus", actualKey: "特别奖金" },
|
||
];
|
||
|
||
/* 扣款项 */
|
||
const DEDUCTIONS = [
|
||
{ label: "社保(公司转个人)", key: "社保-公司部分的个人承担" },
|
||
{ label: "个人社保(公账代扣)", key: "职工社保个人承担部分(公账代扣)" },
|
||
{ label: "员工餐分担", key: "员工餐分担金额" },
|
||
];
|
||
|
||
const ROMAN = ["Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ"];
|
||
|
||
/* ============ Helpers ============ */
|
||
function nonEmpty(v) {
|
||
if (v === undefined || v === null) return false;
|
||
const s = String(v).trim();
|
||
return s !== "" && s !== "—";
|
||
}
|
||
function isZero(v) {
|
||
const n = parseFloat(v);
|
||
return Number.isFinite(n) && n === 0;
|
||
}
|
||
// 加班是否"存"(不发钱,只入备注):备注中出现"加班…存"
|
||
function isOtBanked(r) {
|
||
const note = String(r["备注"] || "");
|
||
return /加班[^;;。\s]*存/.test(note) || /\d+\s*小?时?\s*存/.test(note);
|
||
}
|
||
function fmtNumber(v) {
|
||
const s = String(v).trim();
|
||
if (/^-?\d+(\.\d+)?$/.test(s)) {
|
||
return parseFloat(s).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
|
||
}
|
||
return v;
|
||
}
|
||
function moneyHtml(v) {
|
||
if (!nonEmpty(v) || isZero(v)) return `<span class="dash">—</span>`;
|
||
return `<span class="money">${fmtNumber(v)}</span>`;
|
||
}
|
||
|
||
/* 大写金额 */
|
||
function toChineseCapital(num) {
|
||
const n = parseFloat(num);
|
||
if (!Number.isFinite(n)) return "";
|
||
if (n === 0) return "零元整";
|
||
const digits = "零壹贰叁肆伍陆柒捌玖";
|
||
const intUnits = ["", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿"];
|
||
const sign = n < 0 ? "负" : "";
|
||
const abs = Math.abs(n);
|
||
const intPart = Math.floor(abs);
|
||
const decPart = Math.round((abs - intPart) * 100);
|
||
let result = "";
|
||
if (intPart === 0) result = "零";
|
||
else {
|
||
const s = String(intPart);
|
||
let prevZero = false;
|
||
for (let i = 0; i < s.length; i++) {
|
||
const d = parseInt(s[i], 10);
|
||
const unitIdx = s.length - 1 - i;
|
||
if (d === 0) {
|
||
prevZero = true;
|
||
if (unitIdx === 4 && result && !result.endsWith("万") && !result.endsWith("亿")) {
|
||
result += "万";
|
||
}
|
||
} else {
|
||
if (prevZero) result += "零";
|
||
result += digits[d] + intUnits[unitIdx];
|
||
prevZero = false;
|
||
}
|
||
}
|
||
}
|
||
result += "元";
|
||
if (decPart === 0) {
|
||
result += "整";
|
||
} else {
|
||
const jiao = Math.floor(decPart / 10);
|
||
const fen = decPart % 10;
|
||
if (jiao > 0) result += digits[jiao] + "角";
|
||
if (fen > 0) result += digits[fen] + "分";
|
||
}
|
||
return sign + result;
|
||
}
|
||
|
||
function monthLabel(m) {
|
||
const s = String(m || "");
|
||
if (s.length >= 6) return `${s.slice(0,4)} 年 ${parseInt(s.slice(4),10)} 月`;
|
||
return s;
|
||
}
|
||
function monthLabelEn(m) {
|
||
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||
const s = String(m || "");
|
||
if (s.length >= 6) return `${months[parseInt(s.slice(4),10)-1]} · ${s.slice(0,4)}`;
|
||
return s;
|
||
}
|
||
function docNumber(rec) {
|
||
const m = String(rec["月份"] || "");
|
||
const name = rec["姓名"] || "";
|
||
const hash = (name.charCodeAt(0) * 137 + (name.charCodeAt(1)||0) * 31 + (name.charCodeAt(2)||0) * 7) % 10000;
|
||
return `NO. ${m} — ${String(hash).padStart(4, "0")}`;
|
||
}
|
||
|
||
/* ============ Renderers ============ */
|
||
// 社保两项金额(交了社保员工的标准):公司承担个人部分 + 个人代扣
|
||
const INSURANCE_COMPANY = 1222.25;
|
||
const INSURANCE_PERSONAL = 523.53;
|
||
const INSURANCE_TOTAL = INSURANCE_COMPANY + INSURANCE_PERSONAL; // 1745.78
|
||
|
||
function isInsured(r) {
|
||
return (nonEmpty(r["社保-公司部分的个人承担"]) && !isZero(r["社保-公司部分的个人承担"]))
|
||
|| (nonEmpty(r["职工社保个人承担部分(公账代扣)"]) && !isZero(r["职工社保个人承担部分(公账代扣)"]));
|
||
}
|
||
|
||
function renderMatrix(r) {
|
||
const noInsuranceNote = !isInsured(r) ? `
|
||
<div class="matrix-note">
|
||
⚠ 你的基本工资中已包含<strong>公司应承担的社保金额和个人社保金额</strong>,总计
|
||
<span class="amt">¥${INSURANCE_TOTAL.toLocaleString("zh-CN",{minimumFractionDigits:2,maximumFractionDigits:2})}</span>
|
||
</div>` : "";
|
||
|
||
const rows = MATRIX.map((item) => {
|
||
const std = item.standardKey ? r[item.standardKey] : null;
|
||
const act = item.actualKey ? r[item.actualKey] : null;
|
||
const perf = item.perf ? item.perf(r) : "";
|
||
const hasStd = nonEmpty(std) && !isZero(std);
|
||
const hasAct = nonEmpty(act) && !isZero(act);
|
||
const hasPerf = perf && perf.length;
|
||
if (item.hideIfZeroStd && !hasStd && !hasAct && !hasPerf) return null;
|
||
if (!hasStd && !hasAct && !hasPerf) return null;
|
||
|
||
const stdCell = item.standardKey ? moneyHtml(std) : `<span class="dash">—</span>`;
|
||
const actCell = item.actualKey ? moneyHtml(act) : `<span class="dash">—</span>`;
|
||
const perfCell = hasPerf ? perf : `<span class="dash">—</span>`;
|
||
|
||
// 基本工资行下方:未交社保者插入强调注明
|
||
const note = (item.actualKey === "基本工资") ? noInsuranceNote : "";
|
||
|
||
return `
|
||
<div class="matrix-row">
|
||
<div class="m-item">
|
||
<span class="cn">${item.label}</span>
|
||
<span class="en">${item.en}</span>
|
||
</div>
|
||
<div class="m-std">${stdCell}</div>
|
||
<div class="m-perf">${perfCell}</div>
|
||
<div class="m-act">${actCell}</div>
|
||
</div>${note}`;
|
||
}).filter(Boolean).join("");
|
||
|
||
if (!rows) return "";
|
||
return `
|
||
<div class="matrix">
|
||
<div class="matrix-head">
|
||
<div>项目 / Item</div>
|
||
<div>标准 / Standard</div>
|
||
<div>完成度 / Performance</div>
|
||
<div>实发 / Actual</div>
|
||
</div>
|
||
${rows}
|
||
</div>`;
|
||
}
|
||
|
||
function renderMetrics(r) {
|
||
const att = r["出勤天数"];
|
||
const ot = isOtBanked(r) ? null : r["加班小时数"];
|
||
const kpi = r["KPI得分"];
|
||
const lh = r["法定假期天数"];
|
||
const dept = r["部门业绩"];
|
||
const shift = r["班次业绩"];
|
||
const cross = r["部门x班次业绩"];
|
||
const prev = r["_prev"] || {};
|
||
|
||
function cell(value, unit, label, labelCn, money, prevVal) {
|
||
const moneyZero = money && nonEmpty(value) && parseFloat(value) === 0;
|
||
const has = nonEmpty(value) && !moneyZero;
|
||
const display = money && has ? fmtNumber(value) : value;
|
||
const prefix = money && has ? '<span class="prefix">¥</span>' : '';
|
||
const unitHtml = has && unit && !money ? `<span class="value-unit">${unit}</span>` : '';
|
||
// 业绩类(money)在标签下方加环比涨跌徽标; 无上月数据(新员工)显示"新增"
|
||
const mom = (money && has) ? momBadge(value, prevVal) : '';
|
||
return `
|
||
<div class="metric ${money ? 'money' : ''}">
|
||
<div class="value ${has ? '' : 'dash'}">${has ? prefix + display + unitHtml : "—"}</div>
|
||
<div class="label"><span class="label-cn">${labelCn}</span><span class="sep">·</span>${label}</div>
|
||
${mom}
|
||
</div>`;
|
||
}
|
||
return `
|
||
${cell(att, "天", "Att.", "出勤")}
|
||
${cell(ot, "h", "OT", "加班")}
|
||
${cell(kpi, "档", "KPI", "绩效")}
|
||
${cell(lh, "天", "Holiday", "法假")}
|
||
${cell(dept, null, "Dept", "部门", true, prev["部门业绩"])}
|
||
${cell(shift, null, "Shift", "班次", true, prev["班次业绩"])}
|
||
${cell(cross, null, "Cross", "交叉", true, prev["部门x班次业绩"])}`;
|
||
}
|
||
|
||
// 环比涨跌徽标: 对比上月同口径业绩(均标注"环比上月")
|
||
function momBadge(cur, prevRaw) {
|
||
const c = parseFloat(cur);
|
||
const p = prevRaw === undefined || prevRaw === null ? NaN : parseFloat(prevRaw);
|
||
if (!Number.isFinite(c)) return "";
|
||
const cap = `<span class="mom-cap">环比上月</span>`;
|
||
if (!Number.isFinite(p) || p === 0)
|
||
return `<span class="mom new">${cap}— 新增</span>`;
|
||
const pct = (c - p) / p * 100;
|
||
if (Math.abs(pct) < 0.05)
|
||
return `<span class="mom flat">${cap}≈ 持平</span>`;
|
||
const up = pct > 0;
|
||
const arrow = up ? "▲" : "▼";
|
||
return `<span class="mom ${up ? "up" : "down"}">${cap}${arrow} ${Math.abs(pct).toFixed(1)}%</span>`;
|
||
}
|
||
|
||
function renderRevenueLine(r) {
|
||
const dep = r["部门业绩"], sft = r["班次业绩"], cross = r["部门x班次业绩"];
|
||
const prev = r["_prev"] || {};
|
||
const hasDep = nonEmpty(dep) && !isZero(dep);
|
||
const hasSft = nonEmpty(sft) && !isZero(sft);
|
||
const hasCross = nonEmpty(cross) && !isZero(cross);
|
||
if (!hasDep && !hasSft && !hasCross) return "";
|
||
const cell = (v, prevV, cn, en) =>
|
||
`<div class="rev-item">
|
||
<span class="v">${fmtNumber(v)}</span>
|
||
${momBadge(v, prevV)}
|
||
<span class="lbl"><span class="cn">${cn}</span>${en}</span>
|
||
</div>`;
|
||
const items = [];
|
||
if (hasDep) items.push(cell(dep, prev["部门业绩"], "部门", "Dept"));
|
||
if (hasSft) items.push(cell(sft, prev["班次业绩"], "班次", "Shift"));
|
||
if (hasCross) items.push(cell(cross, prev["部门x班次业绩"], "部门×班次", "Cross"));
|
||
return `<div class="revenue-line"><span class="rev-label">本月业绩 · Monthly Revenue <span class="mom-note">(含环比上月)</span></span><div class="rev-items">${items.join("")}</div></div>`;
|
||
}
|
||
|
||
function renderDeductions(r) {
|
||
const items = DEDUCTIONS
|
||
.filter(d => nonEmpty(r[d.key]) && !isZero(r[d.key]))
|
||
.map(d => `
|
||
<div class="deduction">
|
||
<div class="label">${d.label}</div>
|
||
<div class="value">${fmtNumber(r[d.key])}</div>
|
||
</div>`).join("");
|
||
if (!items) return "";
|
||
return `
|
||
<div class="slip-section">
|
||
<div class="section-head">
|
||
<div class="section-no">${ROMAN[1]}</div>
|
||
<div class="section-title">扣 款</div>
|
||
<div class="section-title-en">Deductions</div>
|
||
</div>
|
||
<div class="deductions">${items}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderRemark(r) {
|
||
const remark = (r["备注"] || "").trim();
|
||
if (!remark) return "";
|
||
return `
|
||
<div class="slip-section">
|
||
<div class="section-head">
|
||
<div class="section-no">${ROMAN[2]}</div>
|
||
<div class="section-title">备 注</div>
|
||
<div class="section-title-en">Notes</div>
|
||
</div>
|
||
<div class="remark-block"><div class="remark">${remark.replace(/;/g, ";\n")}</div></div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderTotal(r) {
|
||
const total = r["工资汇总"];
|
||
const remaining = r["剩余应发"];
|
||
const insurance = r["职工社保个人承担部分(公账代扣)"];
|
||
const hasTotal = nonEmpty(total) && !isZero(total);
|
||
const hasInsurance = nonEmpty(insurance) && !isZero(insurance);
|
||
|
||
if (!hasTotal) {
|
||
return `
|
||
<div class="slip-total">
|
||
<div class="total-row">
|
||
<div class="total-label">应 发 合 计<span class="en">Total Payable</span></div>
|
||
<div class="total-value"><div class="num empty">待计算 / pending</div></div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// 有社保代扣:剩余应发作为重点
|
||
if (hasInsurance && nonEmpty(remaining)) {
|
||
const capital = toChineseCapital(remaining);
|
||
return `
|
||
<div class="slip-total">
|
||
<div class="breakdown">
|
||
<div class="bd-row">
|
||
<span class="bd-label">应发合计 <span class="en">Gross</span></span>
|
||
<span class="bd-value">${fmtNumber(total)}</span>
|
||
</div>
|
||
<div class="bd-row deduction">
|
||
<span class="bd-label">代扣社保 <span class="en">Insurance</span></span>
|
||
<span class="bd-value">${fmtNumber(insurance)}</span>
|
||
</div>
|
||
</div>
|
||
<div class="total-row">
|
||
<div class="total-label">实 发 工 资<span class="en">Net Pay</span></div>
|
||
<div class="total-value"><div class="num">${fmtNumber(remaining)}</div></div>
|
||
</div>
|
||
${capital ? `<div class="capital"><span class="pre">人民币(大写)</span>${capital}</div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
// 无社保代扣:应发合计即实发
|
||
const capital = toChineseCapital(total);
|
||
return `
|
||
<div class="slip-total">
|
||
<div class="total-row">
|
||
<div class="total-label">应 发 合 计<span class="en">Total Payable</span></div>
|
||
<div class="total-value"><div class="num">${fmtNumber(total)}</div></div>
|
||
</div>
|
||
${capital ? `<div class="capital"><span class="pre">人民币(大写)</span>${capital}</div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
function renderCompanyCost(r) {
|
||
const v = r["社保-公司承担"];
|
||
if (!nonEmpty(v) || isZero(v)) return "";
|
||
return `<div class="company-cost">公司承担社保(员工成本,不计入实发)<span class="v">${fmtNumber(v)}</span></div>`;
|
||
}
|
||
|
||
function renderSlip(r) {
|
||
const tags = ["归属", "部门", "班次", "兼任岗位", "当前状态", "岗位级别"]
|
||
.map(k => (r[k] || "").trim()).filter(Boolean)
|
||
.map(t => `<span class="tag">${t}</span>`).join("");
|
||
const position = (r["岗位"] || "").trim();
|
||
const name = r["姓名"] || "—";
|
||
const ded = renderDeductions(r);
|
||
const rem = renderRemark(r);
|
||
const auxHtml = (ded || rem) ? `<div class="aux-row">${ded}${rem}</div>` : "";
|
||
|
||
return `
|
||
<div class="slip" data-name="${name}_${r["月份"] || ""}">
|
||
<span class="corner tl"></span><span class="corner tr"></span>
|
||
<span class="corner bl"></span><span class="corner br"></span>
|
||
<div class="watermark">${name.charAt(0)}</div>
|
||
|
||
<div class="slip-header">
|
||
<div class="header-left">
|
||
<div class="brand-line">— Damon · By Maybe Lab —</div>
|
||
<div class="brand-title">大 梦 <span class="of">by</span> 可 能 实 验 室</div>
|
||
<div class="brand-meta">Hangzhou · Crafted Beverage & Cuisine</div>
|
||
</div>
|
||
<div class="header-center">
|
||
<div class="label">月度工资单</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<div class="period">${monthLabel(r["月份"])}<span class="en">${monthLabelEn(r["月份"])}</span></div>
|
||
<div class="doc-no">${docNumber(r)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-strip">
|
||
<div class="name-block">
|
||
<div class="name">${name}</div>
|
||
${position ? `<div class="position">${position}</div>` : ""}
|
||
</div>
|
||
<div class="tags">${tags}</div>
|
||
</div>
|
||
|
||
<div class="metrics">${renderMetrics(r)}</div>
|
||
|
||
<div class="slip-body">
|
||
<div class="slip-section">
|
||
<div class="section-head">
|
||
<div class="section-no">${ROMAN[0]}</div>
|
||
<div class="section-title">计 薪 明 细</div>
|
||
<div class="section-title-en">Computation Detail</div>
|
||
</div>
|
||
${renderMatrix(r)}
|
||
</div>
|
||
${auxHtml}
|
||
</div>
|
||
|
||
${renderTotal(r)}
|
||
${renderCompanyCost(r)}
|
||
|
||
<div class="slip-footer">
|
||
<div class="confirm-note">
|
||
回复 <span class="keyword">「确认」</span> 表示认可此工资单,请及时回复。
|
||
<span class="hint">回复后老汪会尽快发工资。</span>
|
||
</div>
|
||
<div class="seal" aria-label="大梦财务章">
|
||
<div class="seal-text">
|
||
<span>大</span><span>梦</span><span>财</span><span>章</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="slip-actions">
|
||
<button onclick="exportSlip(this.closest('.slip'))">DOWNLOAD PNG</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ============ Export ============ */
|
||
async function exportSlip(elem) {
|
||
const actions = elem.querySelector(".slip-actions");
|
||
actions.style.visibility = "hidden";
|
||
try {
|
||
const canvas = await html2canvas(elem, { scale: 2, backgroundColor: null, useCORS: true });
|
||
const a = document.createElement("a");
|
||
a.download = elem.dataset.name + ".png";
|
||
a.href = canvas.toDataURL("image/png");
|
||
a.click();
|
||
} finally {
|
||
actions.style.visibility = "";
|
||
}
|
||
}
|
||
async function downloadAll() {
|
||
for (const slip of document.querySelectorAll(".slip")) {
|
||
await exportSlip(slip);
|
||
await new Promise(r => setTimeout(r, 400));
|
||
}
|
||
}
|
||
|
||
/* ============ Init ============ */
|
||
(function init() {
|
||
const data = window.SALARY_DATA;
|
||
const container = document.getElementById("slips");
|
||
const title = document.getElementById("title");
|
||
|
||
if (!data || !data.records || !data.records.length) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<p>还没有数据。</p>
|
||
<p>请运行:<code>python3 fetch_data.py 202604</code></p>
|
||
<p>然后刷新本页。</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
title.textContent = `${monthLabel(data.month)} · ${data.records.length} PEOPLE`;
|
||
container.innerHTML = data.records.map(renderSlip).join("");
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|