新文件增加

This commit is contained in:
zzh 2025-12-08 11:20:47 +08:00
parent b4ab6be6b8
commit d47b0beefc
9 changed files with 2135 additions and 0 deletions

944
frontend/assets/mod.css Normal file
View File

@ -0,0 +1,944 @@
/* From Uiverse.io by Galahhad */
/* REMASTERED */
/* RTX-ON */
/* completely redone toggle and droid */
.bb8-toggle {
--toggle-size: 10px;
/* finally I removed the scale now everything depends on the font-size */
/* --margin-top-for-head: 1.75em; */
/* it's just in case 👆 */
--toggle-width: 10.625em;
--toggle-height: 5.625em;
--toggle-offset: calc((var(--toggle-height) - var(--bb8-diameter)) / 2);
--toggle-bg: linear-gradient(#2c4770, #070e2b 35%, #628cac 50% 70%, #a6c5d4)
no-repeat;
--bb8-diameter: 4.375em;
--radius: 99em;
--transition: 0.4s;
--accent: #de7d2f;
--bb8-bg: #fff;
}
.bb8-toggle,
.bb8-toggle *,
.bb8-toggle *::before,
.bb8-toggle *::after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.bb8-toggle {
cursor: pointer;
margin-top: var(--margin-top-for-head);
font-size: var(--toggle-size);
}
.bb8-toggle__checkbox {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: none;
}
.bb8-toggle__container {
width: var(--toggle-width);
height: var(--toggle-height);
background: var(--toggle-bg);
background-size: 100% 11.25em;
background-position-y: -5.625em;
border-radius: var(--radius);
position: relative;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8 {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: absolute;
top: calc(var(--toggle-offset) - 1.688em + 0.188em);
left: var(--toggle-offset);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
z-index: 2;
}
.bb8__head-container {
position: relative;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
z-index: 2;
-webkit-transform-origin: 1.25em 3.75em;
-ms-transform-origin: 1.25em 3.75em;
transform-origin: 1.25em 3.75em;
}
.bb8__head {
overflow: hidden;
margin-bottom: -0.188em;
width: 2.5em;
height: 1.688em;
background: -o-linear-gradient(
transparent 0.063em,
dimgray 0.063em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.5em,
transparent 0.5em 1.313em,
silver 1.313em 1.438em,
transparent 1.438em
),
-o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
background: -o-linear-gradient(
transparent 0.063em,
dimgray 0.063em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.5em,
transparent 0.5em 1.313em,
silver 1.313em 1.438em,
transparent 1.438em
),
-o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
background: -o-linear-gradient(
transparent 0.063em,
dimgray 0.063em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.5em,
transparent 0.5em 1.313em,
silver 1.313em 1.438em,
transparent 1.438em
),
-o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
background: -o-linear-gradient(
transparent 0.063em,
dimgray 0.063em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.5em,
transparent 0.5em 1.313em,
silver 1.313em 1.438em,
transparent 1.438em
),
-o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
1.25em),
-o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
background: linear-gradient(
transparent 0.063em,
dimgray 0.063em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.5em,
transparent 0.5em 1.313em,
silver 1.313em 1.438em,
transparent 1.438em
),
linear-gradient(
45deg,
transparent 0.188em,
var(--bb8-bg) 0.188em 1.25em,
transparent 1.25em
),
linear-gradient(
-45deg,
transparent 0.188em,
var(--bb8-bg) 0.188em 1.25em,
transparent 1.25em
),
linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
border-radius: var(--radius) var(--radius) 0 0;
position: relative;
z-index: 1;
-webkit-filter: drop-shadow(0 0.063em 0.125em gray);
filter: drop-shadow(0 0.063em 0.125em gray);
}
.bb8__head::before {
content: "";
position: absolute;
width: 0.563em;
height: 0.563em;
background: -o-radial-gradient(
0.25em 0.375em,
0.125em circle,
red,
transparent
),
-o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
100%),
-o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
background: -o-radial-gradient(
0.25em 0.375em,
0.125em circle,
red,
transparent
),
-o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
100%),
-o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
background: -o-radial-gradient(
0.25em 0.375em,
0.125em circle,
red,
transparent
),
-o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
100%),
-o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
background: -o-radial-gradient(
0.25em 0.375em,
0.125em circle,
red,
transparent
),
-o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
100%),
-o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
background: radial-gradient(
0.125em circle at 0.25em 0.375em,
red,
transparent
),
radial-gradient(
0.063em circle at 0.375em 0.188em,
var(--bb8-bg) 50%,
transparent 100%
),
linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
border-radius: var(--radius);
top: 0.413em;
left: 50%;
-webkit-transform: translate(-50%);
-ms-transform: translate(-50%);
transform: translate(-50%);
-webkit-box-shadow: 0 0 0 0.089em lightgray, 0.563em 0.281em 0 -0.148em,
0.563em 0.281em 0 -0.1em var(--bb8-bg), 0.563em 0.281em 0 -0.063em;
box-shadow: 0 0 0 0.089em lightgray, 0.563em 0.281em 0 -0.148em,
0.563em 0.281em 0 -0.1em var(--bb8-bg), 0.563em 0.281em 0 -0.063em;
z-index: 1;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8__head::after {
content: "";
position: absolute;
bottom: 0.375em;
left: 0;
width: 100%;
height: 0.188em;
background: -o-linear-gradient(
left,
var(--accent) 0.125em,
transparent 0.125em 0.188em,
var(--accent) 0.188em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.938em,
transparent 0.938em 1em,
var(--accent) 1em 1.125em,
transparent 1.125em 1.875em,
var(--accent) 1.875em 2em,
transparent 2em 2.063em,
var(--accent) 2.063em 2.25em,
transparent 2.25em 2.313em,
var(--accent) 2.313em 2.375em,
transparent 2.375em 2.438em,
var(--accent) 2.438em
);
background: -webkit-gradient(
linear,
left top,
right top,
color-stop(0.125em, var(--accent)),
color-stop(0.125em, transparent),
color-stop(0.188em, var(--accent)),
color-stop(0.313em, transparent),
color-stop(0.375em, var(--accent)),
color-stop(0.938em, transparent),
color-stop(1em, var(--accent)),
color-stop(1.125em, transparent),
color-stop(1.875em, var(--accent)),
color-stop(2em, transparent),
color-stop(2.063em, var(--accent)),
color-stop(2.25em, transparent),
color-stop(2.313em, var(--accent)),
color-stop(2.375em, transparent),
color-stop(2.438em, var(--accent))
);
background: linear-gradient(
to right,
var(--accent) 0.125em,
transparent 0.125em 0.188em,
var(--accent) 0.188em 0.313em,
transparent 0.313em 0.375em,
var(--accent) 0.375em 0.938em,
transparent 0.938em 1em,
var(--accent) 1em 1.125em,
transparent 1.125em 1.875em,
var(--accent) 1.875em 2em,
transparent 2em 2.063em,
var(--accent) 2.063em 2.25em,
transparent 2.25em 2.313em,
var(--accent) 2.313em 2.375em,
transparent 2.375em 2.438em,
var(--accent) 2.438em
);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8__antenna {
position: absolute;
-webkit-transform: translateY(-90%);
-ms-transform: translateY(-90%);
transform: translateY(-90%);
width: 0.059em;
border-radius: var(--radius) var(--radius) 0 0;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8__antenna:nth-child(1) {
height: 0.938em;
right: 0.938em;
background: -o-linear-gradient(#000 0.188em, silver 0.188em);
background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0.188em, #000),
color-stop(0.188em, silver)
);
background: linear-gradient(#000 0.188em, silver 0.188em);
}
.bb8__antenna:nth-child(2) {
height: 0.375em;
left: 50%;
-webkit-transform: translate(-50%, -90%);
-ms-transform: translate(-50%, -90%);
transform: translate(-50%, -90%);
background: silver;
}
.bb8__body {
width: 4.375em;
height: 4.375em;
background: var(--bb8-bg);
border-radius: var(--radius);
position: relative;
overflow: hidden;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
z-index: 1;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: -webkit-gradient(
linear,
right top,
left top,
color-stop(4%, var(--bb8-bg)),
color-stop(4%, var(--accent)),
color-stop(10%, transparent),
color-stop(90%, var(--accent)),
color-stop(96%, var(--bb8-bg))
),
-webkit-gradient(linear, left top, left bottom, color-stop(4%, var(--bb8-bg)), color-stop(4%, var(--accent)), color-stop(10%, transparent), color-stop(90%, var(--accent)), color-stop(96%, var(--bb8-bg))),
-webkit-gradient(linear, left top, right top, color-stop(2.156em, transparent), color-stop(2.156em, silver), color-stop(2.188em, transparent)),
-webkit-gradient(linear, left top, left bottom, color-stop(2.156em, transparent), color-stop(2.156em, silver), color-stop(2.188em, transparent));
background: -o-linear-gradient(
right,
var(--bb8-bg) 4%,
var(--accent) 4% 10%,
transparent 10% 90%,
var(--accent) 90% 96%,
var(--bb8-bg) 96%
),
-o-linear-gradient(var(--bb8-bg) 4%, var(--accent) 4% 10%, transparent 10%
90%, var(--accent) 90% 96%, var(--bb8-bg) 96%),
-o-linear-gradient(left, transparent 2.156em, silver 2.156em 2.219em, transparent
2.188em),
-o-linear-gradient(transparent 2.156em, silver 2.156em 2.219em, transparent
2.188em);
background: linear-gradient(
-90deg,
var(--bb8-bg) 4%,
var(--accent) 4% 10%,
transparent 10% 90%,
var(--accent) 90% 96%,
var(--bb8-bg) 96%
),
linear-gradient(
var(--bb8-bg) 4%,
var(--accent) 4% 10%,
transparent 10% 90%,
var(--accent) 90% 96%,
var(--bb8-bg) 96%
),
linear-gradient(
to right,
transparent 2.156em,
silver 2.156em 2.219em,
transparent 2.188em
),
linear-gradient(
transparent 2.156em,
silver 2.156em 2.219em,
transparent 2.188em
);
background-color: var(--bb8-bg);
}
.bb8__body::after {
content: "";
bottom: 1.5em;
left: 0.563em;
position: absolute;
width: 0.188em;
height: 0.188em;
background: rgb(236, 236, 236);
color: rgb(236, 236, 236);
border-radius: 50%;
-webkit-box-shadow: 0.875em 0.938em, 0 -1.25em, 0.875em -2.125em,
2.125em -2.125em, 3.063em -1.25em, 3.063em 0, 2.125em 0.938em;
box-shadow: 0.875em 0.938em, 0 -1.25em, 0.875em -2.125em, 2.125em -2.125em,
3.063em -1.25em, 3.063em 0, 2.125em 0.938em;
}
.bb8__body::before {
content: "";
width: 2.625em;
height: 2.625em;
position: absolute;
border-radius: 50%;
z-index: 0.1;
overflow: hidden;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
border: 0.313em solid var(--accent);
background: -o-radial-gradient(
center,
1em circle,
rgb(236, 236, 236) 50%,
transparent 51%
),
-o-radial-gradient(center, 1.25em circle, var(--bb8-bg) 50%, transparent 51%),
-o-linear-gradient(right, transparent 42%, var(--accent) 42% 58%, transparent
58%),
-o-linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg)
58%);
background: -o-radial-gradient(
center,
1em circle,
rgb(236, 236, 236) 50%,
transparent 51%
),
-o-radial-gradient(center, 1.25em circle, var(--bb8-bg) 50%, transparent 51%),
-o-linear-gradient(right, transparent 42%, var(--accent) 42% 58%, transparent
58%),
-o-linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg)
58%);
background: radial-gradient(
1em circle at center,
rgb(236, 236, 236) 50%,
transparent 51%
),
radial-gradient(1.25em circle at center, var(--bb8-bg) 50%, transparent 51%),
-webkit-gradient(linear, right top, left top, color-stop(42%, transparent), color-stop(42%, var(--accent)), color-stop(58%, transparent)),
-webkit-gradient(linear, left top, left bottom, color-stop(42%, var(--bb8-bg)), color-stop(42%, var(--accent)), color-stop(58%, var(--bb8-bg)));
background: radial-gradient(
1em circle at center,
rgb(236, 236, 236) 50%,
transparent 51%
),
radial-gradient(1.25em circle at center, var(--bb8-bg) 50%, transparent 51%),
linear-gradient(
-90deg,
transparent 42%,
var(--accent) 42% 58%,
transparent 58%
),
linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg) 58%);
}
.artificial__hidden {
position: absolute;
border-radius: inherit;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.bb8__shadow {
content: "";
width: var(--bb8-diameter);
height: 20%;
border-radius: 50%;
background: #3a271c;
-webkit-box-shadow: 0.313em 0 3.125em #3a271c;
box-shadow: 0.313em 0 3.125em #3a271c;
opacity: 0.25;
position: absolute;
bottom: 0;
left: calc(var(--toggle-offset) - 0.938em);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
-webkit-transform: skew(-70deg);
-ms-transform: skew(-70deg);
transform: skew(-70deg);
z-index: 1;
}
.bb8-toggle__scenery {
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
position: relative;
border-radius: inherit;
}
.bb8-toggle__scenery::before {
content: "";
position: absolute;
width: 100%;
height: 30%;
bottom: 0;
background: #b18d71;
z-index: 1;
}
.bb8-toggle__cloud {
z-index: 1;
position: absolute;
border-radius: 50%;
}
.bb8-toggle__cloud:nth-last-child(1) {
width: 0.875em;
height: 0.625em;
-webkit-filter: blur(0.125em) drop-shadow(0.313em 0.313em #ffffffae)
drop-shadow(-0.625em 0 #fff) drop-shadow(-0.938em -0.125em #fff);
filter: blur(0.125em) drop-shadow(0.313em 0.313em #ffffffae)
drop-shadow(-0.625em 0 #fff) drop-shadow(-0.938em -0.125em #fff);
right: 1.875em;
top: 2.813em;
background: -o-linear-gradient(bottom left, #ffffffae, #ffffffae);
background: -webkit-gradient(
linear,
left bottom,
right top,
from(#ffffffae),
to(#ffffffae)
);
background: linear-gradient(to top right, #ffffffae, #ffffffae);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8-toggle__cloud:nth-last-child(2) {
top: 0.625em;
right: 4.375em;
width: 0.875em;
height: 0.375em;
background: #dfdedeae;
-webkit-filter: blur(0.125em) drop-shadow(-0.313em -0.188em #e0dfdfae)
drop-shadow(-0.625em -0.188em #bbbbbbae) drop-shadow(-1em 0.063em #cfcfcfae);
filter: blur(0.125em) drop-shadow(-0.313em -0.188em #e0dfdfae)
drop-shadow(-0.625em -0.188em #bbbbbbae) drop-shadow(-1em 0.063em #cfcfcfae);
-webkit-transition: 0.6s;
-o-transition: 0.6s;
transition: 0.6s;
}
.bb8-toggle__cloud:nth-last-child(3) {
top: 1.25em;
right: 0.938em;
width: 0.875em;
height: 0.375em;
background: #ffffffae;
-webkit-filter: blur(0.125em) drop-shadow(0.438em 0.188em #ffffffae)
drop-shadow(-0.625em 0.313em #ffffffae);
filter: blur(0.125em) drop-shadow(0.438em 0.188em #ffffffae)
drop-shadow(-0.625em 0.313em #ffffffae);
-webkit-transition: 0.8s;
-o-transition: 0.8s;
transition: 0.8s;
}
.gomrassen,
.hermes,
.chenini {
position: absolute;
border-radius: var(--radius);
background: -o-linear-gradient(#fff, #6e8ea2);
background: -webkit-gradient(
linear,
left top,
left bottom,
from(#fff),
to(#6e8ea2)
);
background: linear-gradient(#fff, #6e8ea2);
top: 100%;
}
.gomrassen {
left: 0.938em;
width: 1.875em;
height: 1.875em;
-webkit-box-shadow: 0 0 0.188em #ffffff52, 0 0 0.188em #6e8ea24b;
box-shadow: 0 0 0.188em #ffffff52, 0 0 0.188em #6e8ea24b;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.gomrassen::before,
.gomrassen::after {
content: "";
position: absolute;
border-radius: inherit;
-webkit-box-shadow: inset 0 0 0.063em rgb(140, 162, 169);
box-shadow: inset 0 0 0.063em rgb(140, 162, 169);
background: rgb(184, 196, 200);
}
.gomrassen::before {
left: 0.313em;
top: 0.313em;
width: 0.438em;
height: 0.438em;
}
.gomrassen::after {
width: 0.25em;
height: 0.25em;
left: 1.25em;
top: 0.75em;
}
.hermes {
left: 3.438em;
width: 0.625em;
height: 0.625em;
-webkit-box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
-webkit-transition: 0.6s;
-o-transition: 0.6s;
transition: 0.6s;
}
.chenini {
left: 4.375em;
width: 0.5em;
height: 0.5em;
-webkit-box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
-webkit-transition: 0.8s;
-o-transition: 0.8s;
transition: 0.8s;
}
.tatto-1,
.tatto-2 {
position: absolute;
width: 1.25em;
height: 1.25em;
border-radius: var(--radius);
}
.tatto-1 {
background: #fefefe;
right: 3.125em;
top: 0.625em;
-webkit-box-shadow: 0 0 0.438em #fdf4e1;
box-shadow: 0 0 0.438em #fdf4e1;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.tatto-2 {
background: -o-linear-gradient(#e6ac5c, #d75449);
background: -webkit-gradient(
linear,
left top,
left bottom,
from(#e6ac5c),
to(#d75449)
);
background: linear-gradient(#e6ac5c, #d75449);
right: 1.25em;
top: 2.188em;
-webkit-box-shadow: 0 0 0.438em #e6ad5c3d, 0 0 0.438em #d755494f;
box-shadow: 0 0 0.438em #e6ad5c3d, 0 0 0.438em #d755494f;
-webkit-transition: 0.7s;
-o-transition: 0.7s;
transition: 0.7s;
}
.bb8-toggle__star {
position: absolute;
width: 0.063em;
height: 0.063em;
background: #fff;
border-radius: var(--radius);
-webkit-filter: drop-shadow(0 0 0.063em #fff);
filter: drop-shadow(0 0 0.063em #fff);
color: #fff;
top: 100%;
}
.bb8-toggle__star:nth-child(1) {
left: 3.75em;
-webkit-box-shadow: 1.25em 0.938em, -1.25em 2.5em, 0 1.25em, 1.875em 0.625em,
-3.125em 1.875em, 1.25em 2.813em;
box-shadow: 1.25em 0.938em, -1.25em 2.5em, 0 1.25em, 1.875em 0.625em,
-3.125em 1.875em, 1.25em 2.813em;
-webkit-transition: 0.2s;
-o-transition: 0.2s;
transition: 0.2s;
}
.bb8-toggle__star:nth-child(2) {
left: 4.688em;
-webkit-box-shadow: 0.625em 0, 0 0.625em, -0.625em -0.625em, 0.625em 0.938em,
-3.125em 1.25em, 1.25em -1.563em;
box-shadow: 0.625em 0, 0 0.625em, -0.625em -0.625em, 0.625em 0.938em,
-3.125em 1.25em, 1.25em -1.563em;
-webkit-transition: 0.3s;
-o-transition: 0.3s;
transition: 0.3s;
}
.bb8-toggle__star:nth-child(3) {
left: 5.313em;
-webkit-box-shadow: -0.625em -0.625em, -2.188em 1.25em, -2.188em 0,
-3.75em -0.625em, -3.125em -0.625em, -2.5em -0.313em, 0.75em -0.625em;
box-shadow: -0.625em -0.625em, -2.188em 1.25em, -2.188em 0, -3.75em -0.625em,
-3.125em -0.625em, -2.5em -0.313em, 0.75em -0.625em;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.bb8-toggle__star:nth-child(4) {
left: 1.875em;
width: 0.125em;
height: 0.125em;
-webkit-transition: 0.5s;
-o-transition: 0.5s;
transition: 0.5s;
}
.bb8-toggle__star:nth-child(5) {
left: 5em;
width: 0.125em;
height: 0.125em;
-webkit-transition: 0.6s;
-o-transition: 0.6s;
transition: 0.6s;
}
.bb8-toggle__star:nth-child(6) {
left: 2.5em;
width: 0.125em;
height: 0.125em;
-webkit-transition: 0.7s;
-o-transition: 0.7s;
transition: 0.7s;
}
.bb8-toggle__star:nth-child(7) {
left: 3.438em;
width: 0.125em;
height: 0.125em;
-webkit-transition: 0.8s;
-o-transition: 0.8s;
transition: 0.8s;
}
/* actions */
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(1) {
top: 0.625em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(2) {
top: 1.875em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(3) {
top: 1.25em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(4) {
top: 3.438em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(5) {
top: 3.438em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(6) {
top: 0.313em;
}
.bb8-toggle__checkbox:checked
+ .bb8-toggle__container
.bb8-toggle__star:nth-child(7) {
top: 1.875em;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__cloud {
right: -100%;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .gomrassen {
top: 0.938em;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .hermes {
top: 2.5em;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .chenini {
top: 2.75em;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container {
background-position-y: 0;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-1 {
top: 100%;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-2 {
top: 100%;
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8 {
left: calc(100% - var(--bb8-diameter) - var(--toggle-offset));
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__shadow {
left: calc(100% - var(--bb8-diameter) - var(--toggle-offset) + 0.938em);
-webkit-transform: skew(70deg);
-ms-transform: skew(70deg);
transform: skew(70deg);
}
.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__body {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(225deg);
}
.bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::before {
left: 100%;
}
.bb8-toggle__checkbox:not(:checked):hover
+ .bb8-toggle__container
.bb8__antenna:nth-child(1) {
right: 1.5em;
}
.bb8-toggle__checkbox:hover
+ .bb8-toggle__container
.bb8__antenna:nth-child(2) {
left: 0.938em;
}
.bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::after {
background-position: 1.375em 0;
}
.bb8-toggle__checkbox:checked:hover
+ .bb8-toggle__container
.bb8__head::before {
left: 0;
}
.bb8-toggle__checkbox:checked:hover
+ .bb8-toggle__container
.bb8__antenna:nth-child(2) {
left: calc(100% - 0.938em);
}
.bb8-toggle__checkbox:checked:hover + .bb8-toggle__container .bb8__head::after {
background-position: -1.375em 0;
}
.bb8-toggle__checkbox:active + .bb8-toggle__container .bb8__head-container {
-webkit-transform: rotate(25deg);
-ms-transform: rotate(25deg);
transform: rotate(25deg);
}
.bb8-toggle__checkbox:checked:active
+ .bb8-toggle__container
.bb8__head-container {
-webkit-transform: rotate(-25deg);
-ms-transform: rotate(-25deg);
transform: rotate(-25deg);
}
.bb8:hover .bb8__head::before,
.bb8:hover .bb8__antenna:nth-child(2) {
left: 50% !important;
}
.bb8:hover .bb8__antenna:nth-child(1) {
right: 0.938em !important;
}
.bb8:hover .bb8__head::after {
background-position: 0 0 !important;
}

34
frontend/assets/mod.html Normal file
View File

@ -0,0 +1,34 @@
<!-- From Uiverse.io by Galahhad -->
<label class="bb8-toggle">
<input class="bb8-toggle__checkbox" type="checkbox">
<div class="bb8-toggle__container">
<div class="bb8-toggle__scenery">
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="tatto-1"></div>
<div class="tatto-2"></div>
<div class="gomrassen"></div>
<div class="hermes"></div>
<div class="chenini"></div>
<div class="bb8-toggle__cloud"></div>
<div class="bb8-toggle__cloud"></div>
<div class="bb8-toggle__cloud"></div>
</div>
<div class="bb8">
<div class="bb8__head-container">
<div class="bb8__antenna"></div>
<div class="bb8__antenna"></div>
<div class="bb8__head"></div>
</div>
<div class="bb8__body"></div>
</div>
<div class="artificial__hidden">
<div class="bb8__shadow"></div>
</div>
</div>
</label>

1
frontend/assets/pdd.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

1
frontend/assets/yt.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,369 @@
// BOM物料清单管理
(() => {
let bomList = [];
let currentPage = 1;
const pageSize = 20;
let editingId = null;
Router.register('/plan-mgmt/bom', async () => {
const html = `
<div class="page-header">
<h1>BOM物料清单</h1>
<div class="page-actions">
<button id="add-bom-btn" class="btn btn-primary">新增BOM</button>
<button id="import-bom-btn" class="btn btn-secondary">📥 导入Excel</button>
<button id="download-template-btn" class="btn btn-secondary">📄 下载模板</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="filter-section" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<input type="text" id="search-keyword" class="input" placeholder="搜索产品编码/名称/物料..." style="width: 300px;" />
<button class="btn btn-primary" onclick="BOM.search()">搜索</button>
<button class="btn btn-secondary" onclick="BOM.resetSearch()">重置</button>
</div>
</div>
<div class="table-container" style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="BOM.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>产品编码</th>
<th>产品名称</th>
<th>物料编码</th>
<th>物料名称</th>
<th>单机用量</th>
<th>单位</th>
<th>最小包装</th>
<th>供应商</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody id="bom-list">
<tr><td colspan="11" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 新增/编辑BOM弹窗 -->
<div id="bom-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2 id="modal-title">新增BOM</h2>
<button class="modal-close" onclick="BOM.closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="bom-form">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="field">
<label>产品编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="product-code" class="input" required />
</div>
<div class="field">
<label>产品名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="product-name" class="input" required />
</div>
<div class="field">
<label>物料编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-code" class="input" required />
</div>
<div class="field">
<label>物料名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-name" class="input" required />
</div>
<div class="field">
<label>单机用量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="unit-qty" class="input" step="0.01" min="0.01" value="1" required />
</div>
<div class="field">
<label>单位</label>
<input type="text" id="unit" class="input" value="pcs" />
</div>
<div class="field">
<label>最小包装</label>
<input type="number" id="min-package" class="input" min="1" value="1" />
</div>
<div class="field">
<label>供应商</label>
<input type="text" id="supplier" class="input" />
</div>
</div>
<div class="field" style="margin-top: 16px;">
<label>备注</label>
<textarea id="remark" class="input" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="BOM.closeModal()">取消</button>
<button class="btn btn-primary" onclick="BOM.save()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-bom-btn')?.addEventListener('click', () => openModal());
document.getElementById('import-bom-btn')?.addEventListener('click', () => showImportDialog());
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') BOM.search();
});
loadList();
}, 100);
return html;
});
function showImportDialog() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xlsx,.xls,.csv';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const overlay = document.getElementById('overlay');
overlay.classList.remove('hidden');
const res = await fetch('/api/bom/import', {
method: 'POST',
body: formData,
credentials: 'include'
});
overlay.classList.add('hidden');
const data = await res.json();
if (data.ok) {
alert(data.message || '导入成功');
loadList();
} else {
alert(data.error || '导入失败');
}
} catch (err) {
document.getElementById('overlay').classList.add('hidden');
alert('导入失败: ' + err.message);
}
};
input.click();
}
function downloadTemplate() {
const headers = ['产品编码', '产品名称', '物料编码', '物料名称', '单机用量', '单位', '最小包装', '供应商', '备注'];
const example = ['AP05', 'AP05基站', 'PCB-001', '主控PCB板', '1', 'pcs', '10', '深圳电子', ''];
const csvContent = [headers.join(','), example.join(',')].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'BOM导入模板.csv';
link.click();
}
async function loadList() {
try {
const res = await API.get('/api/bom');
bomList = res.list || [];
renderList();
} catch (e) {
console.error('加载BOM列表失败:', e);
document.getElementById('bom-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function renderList() {
const tbody = document.getElementById('bom-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = bomList;
if (keyword) {
filtered = bomList.filter(item =>
(item.product_code || '').toLowerCase().includes(keyword) ||
(item.product_name || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="11" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td>${escapeHtml(item.product_code || '')}</td>
<td>${escapeHtml(item.product_name || '')}</td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td>${item.unit_qty || 1}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${item.min_package || 1}</td>
<td>${escapeHtml(item.supplier || '-')}</td>
<td>${escapeHtml(item.remark || '-')}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="BOM.edit(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="BOM.delete(${item.id})">删除</button>
</td>
</tr>
`).join('');
}
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="BOM.goPage(${currentPage - 1})">上一页</button>`;
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="BOM.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal(item = null) {
editingId = item?.id || null;
document.getElementById('modal-title').textContent = item ? '编辑BOM' : '新增BOM';
document.getElementById('product-code').value = item?.product_code || '';
document.getElementById('product-name').value = item?.product_name || '';
document.getElementById('material-code').value = item?.material_code || '';
document.getElementById('material-name').value = item?.material_name || '';
document.getElementById('unit-qty').value = item?.unit_qty || 1;
document.getElementById('unit').value = item?.unit || 'pcs';
document.getElementById('min-package').value = item?.min_package || 1;
document.getElementById('supplier').value = item?.supplier || '';
document.getElementById('remark').value = item?.remark || '';
document.getElementById('bom-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('bom-modal').style.display = 'none';
editingId = null;
}
async function save() {
const data = {
product_code: document.getElementById('product-code').value.trim(),
product_name: document.getElementById('product-name').value.trim(),
material_code: document.getElementById('material-code').value.trim(),
material_name: document.getElementById('material-name').value.trim(),
unit_qty: parseFloat(document.getElementById('unit-qty').value) || 1,
unit: document.getElementById('unit').value.trim() || 'pcs',
min_package: parseInt(document.getElementById('min-package').value) || 1,
supplier: document.getElementById('supplier').value.trim(),
remark: document.getElementById('remark').value.trim()
};
if (!data.product_code || !data.product_name || !data.material_code || !data.material_name) {
alert('请填写所有必填项');
return;
}
try {
if (editingId) {
await API.put(`/api/bom/${editingId}`, data);
alert('更新成功');
} else {
await API.post('/api/bom', data);
alert('创建成功');
}
closeModal();
loadList();
} catch (e) {
alert(e.message || '操作失败');
}
}
async function edit(id) {
const item = bomList.find(x => x.id === id);
if (item) openModal(item);
}
async function deleteBom(id) {
if (!confirm('确定要删除这条BOM吗')) return;
try {
await API.delete(`/api/bom/${id}`);
alert('删除成功');
loadList();
} catch (e) {
alert(e.message || '删除失败');
}
}
async function batchDelete() {
const checked = document.querySelectorAll('.row-checkbox:checked');
if (checked.length === 0) {
alert('请先选择要删除的项');
return;
}
if (!confirm(`确定要删除选中的 ${checked.length} 条BOM吗`)) return;
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
try {
await API.post('/api/bom/batch-delete', { ids });
alert('批量删除成功');
loadList();
} catch (e) {
alert(e.message || '批量删除失败');
}
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.BOM = {
search,
resetSearch,
edit,
delete: deleteBom,
closeModal,
save,
toggleSelectAll,
goPage
};
})();

View File

@ -0,0 +1,365 @@
// 期初库存管理
(() => {
let stockList = [];
let currentPage = 1;
const pageSize = 20;
let editingId = null;
Router.register('/plan-mgmt/initial-stock', async () => {
const html = `
<div class="page-header">
<h1>期初库存管理</h1>
<div class="page-actions">
<button id="add-stock-btn" class="btn btn-primary">新增库存</button>
<button id="import-stock-btn" class="btn btn-secondary">📥 导入Excel</button>
<button id="download-template-btn" class="btn btn-secondary">📄 下载模板</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="filter-section" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码/名称..." style="width: 300px;" />
<button class="btn btn-primary" onclick="InitialStock.search()">搜索</button>
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
</div>
</div>
<div class="table-container" style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="InitialStock.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>物料编码</th>
<th>物料名称</th>
<th>库存数量</th>
<th>单位</th>
<th>最小包装</th>
<th>供应商</th>
<th>备注</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="stock-list">
<tr><td colspan="10" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 新增/编辑库存弹窗 -->
<div id="stock-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2 id="modal-title">新增期初库存</h2>
<button class="modal-close" onclick="InitialStock.closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="stock-form">
<div class="field">
<label>物料编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-code" class="input" required />
</div>
<div class="field">
<label>物料名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-name" class="input" required />
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="field">
<label>库存数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="stock-qty" class="input" min="0" value="0" required />
</div>
<div class="field">
<label>单位</label>
<input type="text" id="unit" class="input" value="pcs" />
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="field">
<label>最小包装</label>
<input type="number" id="min-package" class="input" min="1" value="1" />
</div>
<div class="field">
<label>供应商</label>
<input type="text" id="supplier" class="input" />
</div>
</div>
<div class="field">
<label>备注</label>
<textarea id="remark" class="input" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="InitialStock.closeModal()">取消</button>
<button class="btn btn-primary" onclick="InitialStock.save()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
document.getElementById('import-stock-btn')?.addEventListener('click', () => showImportDialog());
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') InitialStock.search();
});
loadList();
}, 100);
return html;
});
function showImportDialog() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xlsx,.xls,.csv';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const overlay = document.getElementById('overlay');
overlay.classList.remove('hidden');
const res = await fetch('/api/initial-stock/import', {
method: 'POST',
body: formData,
credentials: 'include'
});
overlay.classList.add('hidden');
const data = await res.json();
if (data.ok) {
alert(data.message || '导入成功');
loadList();
} else {
alert(data.error || '导入失败');
}
} catch (err) {
document.getElementById('overlay').classList.add('hidden');
alert('导入失败: ' + err.message);
}
};
input.click();
}
function downloadTemplate() {
const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '备注'];
const example = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', ''];
const csvContent = [headers.join(','), example.join(',')].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = '期初库存导入模板.csv';
link.click();
}
async function loadList() {
try {
const res = await API.get('/api/initial-stock');
stockList = res.list || [];
renderList();
} catch (e) {
console.error('加载库存列表失败:', e);
document.getElementById('stock-list').innerHTML = '<tr><td colspan="10" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function renderList() {
const tbody = document.getElementById('stock-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = stockList;
if (keyword) {
filtered = stockList.filter(item =>
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td style="font-weight: 600; color: var(--primary);">${item.stock_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${item.min_package || 1}</td>
<td>${escapeHtml(item.supplier || '-')}</td>
<td>${escapeHtml(item.remark || '-')}</td>
<td>${formatTime(item.updated_at)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="InitialStock.edit(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="InitialStock.delete(${item.id})">删除</button>
</td>
</tr>
`).join('');
}
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="InitialStock.goPage(${currentPage - 1})">上一页</button>`;
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="InitialStock.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal(item = null) {
editingId = item?.id || null;
document.getElementById('modal-title').textContent = item ? '编辑期初库存' : '新增期初库存';
document.getElementById('material-code').value = item?.material_code || '';
document.getElementById('material-name').value = item?.material_name || '';
document.getElementById('stock-qty').value = item?.stock_qty || 0;
document.getElementById('unit').value = item?.unit || 'pcs';
document.getElementById('min-package').value = item?.min_package || 1;
document.getElementById('supplier').value = item?.supplier || '';
document.getElementById('remark').value = item?.remark || '';
document.getElementById('stock-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('stock-modal').style.display = 'none';
editingId = null;
}
async function save() {
const data = {
material_code: document.getElementById('material-code').value.trim(),
material_name: document.getElementById('material-name').value.trim(),
stock_qty: parseInt(document.getElementById('stock-qty').value) || 0,
unit: document.getElementById('unit').value.trim() || 'pcs',
min_package: parseInt(document.getElementById('min-package').value) || 1,
supplier: document.getElementById('supplier').value.trim(),
remark: document.getElementById('remark').value.trim()
};
if (!data.material_code || !data.material_name) {
alert('请填写物料编码和物料名称');
return;
}
try {
if (editingId) {
await API.put(`/api/initial-stock/${editingId}`, data);
alert('更新成功');
} else {
await API.post('/api/initial-stock', data);
alert('创建成功');
}
closeModal();
loadList();
} catch (e) {
alert(e.message || '操作失败');
}
}
async function edit(id) {
const item = stockList.find(x => x.id === id);
if (item) openModal(item);
}
async function deleteStock(id) {
if (!confirm('确定要删除这条库存记录吗?')) return;
try {
await API.delete(`/api/initial-stock/${id}`);
alert('删除成功');
loadList();
} catch (e) {
alert(e.message || '删除失败');
}
}
async function batchDelete() {
const checked = document.querySelectorAll('.row-checkbox:checked');
if (checked.length === 0) {
alert('请先选择要删除的项');
return;
}
if (!confirm(`确定要删除选中的 ${checked.length} 条库存记录吗?`)) return;
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
try {
await API.post('/api/initial-stock/batch-delete', { ids });
alert('批量删除成功');
loadList();
} catch (e) {
alert(e.message || '批量删除失败');
}
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function formatTime(ts) {
if (!ts) return '-';
try {
const d = new Date(ts);
return d.toLocaleString('zh-CN', { hour12: false });
} catch {
return ts;
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.InitialStock = {
search,
resetSearch,
edit,
delete: deleteStock,
closeModal,
save,
toggleSelectAll,
goPage
};
})();

View File

@ -0,0 +1,419 @@
// 采购需求清单管理
(() => {
let demandList = [];
let productList = [];
let currentPage = 1;
const pageSize = 20;
const statusMap = {
'pending': { text: '待处理', color: 'var(--warning)' },
'ordered': { text: '已下单', color: 'var(--info)' },
'received': { text: '已收货', color: 'var(--primary)' },
'completed': { text: '已完成', color: 'var(--success)' },
'cancelled': { text: '已取消', color: 'var(--text-2)' }
};
Router.register('/plan-mgmt/purchase-demand', async () => {
const html = `
<div class="page-header">
<h1>采购需求清单</h1>
<div class="page-actions">
<button id="calc-demand-btn" class="btn btn-primary">计算采购需求</button>
<button id="calc-from-orders-btn" class="btn btn-secondary">从订单生成</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<!-- 计算说明卡片 -->
<div class="card" style="margin-bottom: 16px; background: var(--info-bg); border: 1px solid var(--info);">
<div class="card-body" style="padding: 12px 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">💡</span>
<div>
<strong style="color: var(--text);">采购需求计算公式</strong>
<span style="color: var(--text);">客户订单数量 × BOM单机用量 - 期初库存 = 净需求 按最小包装向上取整 = 实际采购数量</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="filter-section" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码/名称/需求编号..." style="width: 280px;" />
<select id="filter-status" class="input" style="width: 120px;">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
<button class="btn btn-primary" onclick="PurchaseDemand.search()">搜索</button>
<button class="btn btn-secondary" onclick="PurchaseDemand.resetSearch()">重置</button>
</div>
</div>
<div class="table-container" style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="PurchaseDemand.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>需求编号</th>
<th>物料编码</th>
<th>物料名称</th>
<th>订单数量</th>
<th>BOM用量</th>
<th>总需求</th>
<th>期初库存</th>
<th>净需求</th>
<th>最小包装</th>
<th style="background: var(--success-bg); color: var(--text);">实际采购</th>
<th>单位</th>
<th>供应商</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="demand-list">
<tr><td colspan="15" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 计算采购需求弹窗 -->
<div id="calc-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2>计算采购需求</h2>
<button class="modal-close" onclick="PurchaseDemand.closeCalcModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>选择产品 <span style="color: var(--danger);">*</span></label>
<select id="product-select" class="input">
<option value="">请选择产品</option>
</select>
</div>
<div class="field">
<label>订单数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="order-qty" class="input" min="1" value="1" placeholder="输入客户订单数量" />
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 14px; color: var(--text-2);">
<p style="margin: 0 0 8px 0;">📋 计算将</p>
<ul style="margin: 0; padding-left: 20px;">
<li>根据产品的BOM展开所有物料</li>
<li>计算每种物料的总需求量</li>
<li>扣减期初库存得出净需求</li>
<li>按最小包装取整得出实际采购量</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeCalcModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doCalculate()">开始计算</button>
</div>
</div>
</div>
<!-- 更新状态弹窗 -->
<div id="status-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>更新状态</h2>
<button class="modal-close" onclick="PurchaseDemand.closeStatusModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>状态</label>
<select id="update-status" class="input">
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
<div class="field">
<label>备注</label>
<textarea id="update-remark" class="input" rows="3" placeholder="可选填写备注信息"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeStatusModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doUpdateStatus()">确定</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('calc-demand-btn')?.addEventListener('click', () => openCalcModal());
document.getElementById('calc-from-orders-btn')?.addEventListener('click', () => calcFromOrders());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') PurchaseDemand.search();
});
loadList();
loadProducts();
}, 100);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/purchase-demand');
demandList = res.list || [];
renderList();
} catch (e) {
console.error('加载采购需求失败:', e);
document.getElementById('demand-list').innerHTML = '<tr><td colspan="15" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
async function loadProducts() {
try {
const res = await API.get('/api/bom/products');
productList = res.list || [];
const select = document.getElementById('product-select');
if (select) {
select.innerHTML = '<option value="">请选择产品</option>' +
productList.map(p => `<option value="${escapeHtml(p.product_code)}">${escapeHtml(p.product_code)} - ${escapeHtml(p.product_name)}</option>`).join('');
}
} catch (e) {
console.error('加载产品列表失败:', e);
}
}
function renderList() {
const tbody = document.getElementById('demand-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
const filterStatus = document.getElementById('filter-status')?.value || '';
let filtered = demandList;
if (keyword) {
filtered = filtered.filter(item =>
(item.demand_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
if (filterStatus) {
filtered = filtered.filter(item => item.status === filterStatus);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="15" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => {
const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
return `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.demand_no || '')}</span></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td>${item.order_qty || 0}</td>
<td>${item.bom_unit_qty || 1}</td>
<td>${item.total_demand || 0}</td>
<td style="color: var(--info);">${item.initial_stock || 0}</td>
<td style="color: ${item.net_demand > 0 ? 'var(--warning)' : 'var(--success)'};">${item.net_demand || 0}</td>
<td>${item.min_package || 1}</td>
<td style="font-weight: 700; color: var(--text); background: var(--success-bg);">${item.actual_purchase_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${escapeHtml(item.supplier || '-')}</td>
<td><span style="padding: 2px 8px; border-radius: 4px; background: ${status.color}20; color: ${status.color}; font-size: 12px;">${status.text}</span></td>
<td>
<button class="btn btn-sm btn-secondary" onclick="PurchaseDemand.updateStatus(${item.id})">状态</button>
<button class="btn btn-sm btn-danger" onclick="PurchaseDemand.delete(${item.id})">删除</button>
</td>
</tr>
`;
}).join('');
}
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="PurchaseDemand.goPage(${currentPage - 1})">上一页</button>`;
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="PurchaseDemand.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openCalcModal() {
document.getElementById('product-select').value = '';
document.getElementById('order-qty').value = 1;
document.getElementById('calc-modal').style.display = 'flex';
}
function closeCalcModal() {
document.getElementById('calc-modal').style.display = 'none';
}
async function doCalculate() {
const productCode = document.getElementById('product-select').value;
const orderQty = parseInt(document.getElementById('order-qty').value) || 0;
if (!productCode) {
alert('请选择产品');
return;
}
if (orderQty <= 0) {
alert('订单数量必须大于0');
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate', {
product_code: productCode,
order_qty: orderQty
});
closeCalcModal();
alert(res.message || '计算完成');
loadList();
// 显示计算结果摘要
if (res.list && res.list.length > 0) {
const totalPurchase = res.list.reduce((sum, item) => sum + (item.actual_purchase_qty || 0), 0);
console.log(`采购需求计算完成,需求编号: ${res.demand_no},共 ${res.list.length} 种物料,总采购数量: ${totalPurchase}`);
}
} catch (e) {
alert(e.message || '计算失败');
}
}
async function calcFromOrders() {
if (!confirm('将根据所有客户订单自动计算采购需求,确定继续吗?')) {
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate-from-orders', {});
alert(res.message || '计算完成');
loadList();
} catch (e) {
alert(e.message || '计算失败');
}
}
let updatingId = null;
function updateStatus(id) {
updatingId = id;
const item = demandList.find(x => x.id === id);
document.getElementById('update-status').value = item?.status || 'pending';
document.getElementById('update-remark').value = item?.remark || '';
document.getElementById('status-modal').style.display = 'flex';
}
function closeStatusModal() {
document.getElementById('status-modal').style.display = 'none';
updatingId = null;
}
async function doUpdateStatus() {
if (!updatingId) return;
const status = document.getElementById('update-status').value;
const remark = document.getElementById('update-remark').value.trim();
try {
await API.put(`/api/purchase-demand/${updatingId}`, { status, remark });
closeStatusModal();
alert('更新成功');
loadList();
} catch (e) {
alert(e.message || '更新失败');
}
}
async function deleteDemand(id) {
if (!confirm('确定要删除这条采购需求吗?')) return;
try {
await API.delete(`/api/purchase-demand/${id}`);
alert('删除成功');
loadList();
} catch (e) {
alert(e.message || '删除失败');
}
}
async function batchDelete() {
const checked = document.querySelectorAll('.row-checkbox:checked');
if (checked.length === 0) {
alert('请先选择要删除的项');
return;
}
if (!confirm(`确定要删除选中的 ${checked.length} 条采购需求吗?`)) return;
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
try {
await API.post('/api/purchase-demand/batch-delete', { ids });
alert('批量删除成功');
loadList();
} catch (e) {
alert(e.message || '批量删除失败');
}
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
document.getElementById('filter-status').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.PurchaseDemand = {
search,
resetSearch,
delete: deleteDemand,
updateStatus,
closeCalcModal,
closeStatusModal,
doCalculate,
doUpdateStatus,
toggleSelectAll,
goPage
};
})();

1
圆通@3x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

1
拼多多.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB