新文件增加
This commit is contained in:
parent
b4ab6be6b8
commit
d47b0beefc
944
frontend/assets/mod.css
Normal file
944
frontend/assets/mod.css
Normal 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
34
frontend/assets/mod.html
Normal 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
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
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 |
369
frontend/js/components/bom.js
Normal file
369
frontend/js/components/bom.js
Normal 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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.BOM = {
|
||||
search,
|
||||
resetSearch,
|
||||
edit,
|
||||
delete: deleteBom,
|
||||
closeModal,
|
||||
save,
|
||||
toggleSelectAll,
|
||||
goPage
|
||||
};
|
||||
})();
|
||||
365
frontend/js/components/initial-stock.js
Normal file
365
frontend/js/components/initial-stock.js
Normal 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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.InitialStock = {
|
||||
search,
|
||||
resetSearch,
|
||||
edit,
|
||||
delete: deleteStock,
|
||||
closeModal,
|
||||
save,
|
||||
toggleSelectAll,
|
||||
goPage
|
||||
};
|
||||
})();
|
||||
419
frontend/js/components/purchase-demand.js
Normal file
419
frontend/js/components/purchase-demand.js
Normal 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()">×</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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.PurchaseDemand = {
|
||||
search,
|
||||
resetSearch,
|
||||
delete: deleteDemand,
|
||||
updateStatus,
|
||||
closeCalcModal,
|
||||
closeStatusModal,
|
||||
doCalculate,
|
||||
doUpdateStatus,
|
||||
toggleSelectAll,
|
||||
goPage
|
||||
};
|
||||
})();
|
||||
1
圆通@3x.svg
Normal file
1
圆通@3x.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
Loading…
Reference in New Issue
Block a user