Files

1332 lines
45 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 &amp; 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>