修复仪表盘右下角三个模块:期初库存、采购需求、智能建议

This commit is contained in:
zzh 2026-03-26 10:33:19 +08:00
parent 2f4b346a18
commit 92062262a6
144 changed files with 810 additions and 94 deletions

0
.env.example Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/charts.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/colors.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/landing.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/products.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/prompts.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/flutter.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/nextjs.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/react-native.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/react.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/svelte.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/swiftui.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/stacks/vue.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/styles.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/typography.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/data/ux-guidelines.csv Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/scripts/core.py Normal file → Executable file
View File

0
.shared/ui-ux-pro-max/scripts/search.py Normal file → Executable file
View File

0
.windsurf/workflows/ui-ux-pro-max.md Normal file → Executable file
View File

0
AI_ANALYSIS_README.md Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
add_customer_name_column.py Normal file → Executable file
View File

0
backend/.env.example Normal file → Executable file
View File

0
backend/ai_service.py Normal file → Executable file
View File

0
backend/api_ai.py Normal file → Executable file
View File

0
backend/requirements_ai.txt Normal file → Executable file
View File

0
backend/test_ai.py Normal file → Executable file
View File

0
check_reconciliations.py Normal file → Executable file
View File

0
cookies.txt Normal file → Executable file
View File

0
dashboard.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
date-dark.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

0
date.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

0
deploy/nginx/prod-mgmt.conf Normal file → Executable file
View File

4
deploy/systemd/prod-mgmt.service Normal file → Executable file
View File

@ -3,9 +3,9 @@ Description=Production Management Flask App
After=network.target
[Service]
WorkingDirectory=/home/hyx/work/生产管理系统
WorkingDirectory=/opt/生产管理系统
# 使用Flask但启用多线程和Keep-Alive
ExecStart=/home/hyx/work/.venv/bin/python server/app.py
ExecStart=/usr/bin/python3 server/app.py
Restart=always
Environment=APP_SECRET=please-change-to-strong ADMIN_PASSWORD=change-this SUPERADMIN_USERNAME=zzh SUPERADMIN_PASSWORD=Zzh08165511 REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD=Zzh08165511

0
frontend/assets/ai-icon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 412 B

0
frontend/assets/avatars/.gitignore vendored Normal file → Executable file
View File

0
frontend/assets/calendar-dark.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

0
frontend/assets/calendar.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

0
frontend/assets/dashboard-enhancements.css Normal file → Executable file
View File

0
frontend/assets/dashboard.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

0
frontend/assets/favicon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
frontend/assets/icon-tuxi.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

91
frontend/assets/login.css Normal file → Executable file
View File

@ -262,6 +262,10 @@ body {
z-index: 1;
}
.input-block:has(.password-toggle) {
position: relative;
}
.input,
button {
background: rgba(255, 255, 255, 0.1);
@ -304,11 +308,58 @@ button {
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.2);
}
.input-block:has(.password-toggle) .input {
padding-right: 45px;
}
.input {
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.2),
2px 2px 4px rgba(138, 43, 226, 0.1);
}
/* 密码显示/隐藏按钮 */
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.6);
transition: all 0.3s ease;
z-index: 10;
margin: 0;
width: auto;
height: auto;
box-shadow: none;
}
.password-toggle:hover {
color: rgba(255, 255, 255, 0.9);
background: transparent;
transform: translateY(-50%) scale(1.1);
box-shadow: none;
}
.password-toggle:active {
transform: translateY(-50%) scale(0.95);
}
.password-toggle::before {
display: none;
}
.eye-icon {
width: 20px;
height: 20px;
pointer-events: none;
}
/* 验证码区域 */
.captcha-block {
display: flex;
@ -526,6 +577,46 @@ button:disabled {
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
}
/* 角色容器 */
.character-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.character {
width: 200px;
height: 200px;
filter: drop-shadow(0 4px 12px rgba(138, 43, 226, 0.3));
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
#char-head,
#char-body,
#char-hands ellipse,
#char-eyes ellipse {
transition: all 0.3s ease;
}
#char-mouth {
transition: d 0.3s ease;
}
#hand-left,
#hand-right {
transition: cx 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55),
cy 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55),
transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
#eye-left,
#eye-right {
transition: ry 0.15s ease, cx 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {

0
frontend/assets/meituan-test.css Normal file → Executable file
View File

0
frontend/assets/mod.css Normal file → Executable file
View File

0
frontend/assets/mod.html Normal file → Executable file
View File

0
frontend/assets/moon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

0
frontend/assets/pdd.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
frontend/assets/sun.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 595 B

0
frontend/assets/tuxi-station.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

0
frontend/assets/user-avatar.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

0
frontend/assets/xlsx.min.js vendored Normal file → Executable file
View File

0
frontend/assets/yt.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

0
frontend/js/api.js Normal file → Executable file
View File

0
frontend/js/app.js Normal file → Executable file
View File

0
frontend/js/bundle.js Normal file → Executable file
View File

0
frontend/js/components/ai-report.js Normal file → Executable file
View File

0
frontend/js/components/bom.js Normal file → Executable file
View File

0
frontend/js/components/customer-order.js Normal file → Executable file
View File

187
frontend/js/components/dashboard.js Normal file → Executable file
View File

@ -642,12 +642,8 @@ const Dashboard = (() => {
const txCounts = countByTime(txData);
const mtCounts = countByTime(mtData);
const actualMax = Math.max(...pddCounts, ...ytCounts, ...txCounts, ...mtCounts, 0);
// 动态调整Y轴让小数据更明显
let maxCount;
if (actualMax <= 2) maxCount = 3; // 数据是1-2时Y轴最大值设为3让数据占据1/3到2/3的高度
else if (actualMax <= 5) maxCount = 8; // 数据是3-5时Y轴最大值设为8
else if (actualMax <= 10) maxCount = 15; // 数据是6-10时Y轴最大值设为15
else maxCount = actualMax;
// 根据最高数据点动态调整Y轴最大值增加10%的上边距)
const maxCount = actualMax === 0 ? 5 : Math.ceil(actualMax * 1.1);
const maxDigits = Math.max(maxCount.toString().length, 2);
const dynamicLeftPadding = Math.max(50, 25 + maxDigits * 10);
const padding = {left: dynamicLeftPadding, right: 20, top: 20, bottom: 30};
@ -1946,6 +1942,52 @@ const Dashboard = (() => {
productionFillEl.style.width = fillWidth + '%';
productionFillEl.style.backgroundColor = color;
}
// 更新期初库存数据
if(dashRes.inventory) {
const invTotal = document.getElementById('inventory-total');
const invZero = document.getElementById('inventory-zero');
const invLow = document.getElementById('inventory-low');
const invQty = document.getElementById('inventory-qty');
if(invTotal) invTotal.textContent = dashRes.inventory.total_items || 0;
if(invZero) invZero.textContent = dashRes.inventory.zero_stock || 0;
if(invLow) invLow.textContent = dashRes.inventory.low_stock || 0;
if(invQty) invQty.textContent = dashRes.inventory.total_qty || 0;
}
// 更新采购需求数据
if(dashRes.purchase) {
const purTotal = document.getElementById('purchase-total');
const purPending = document.getElementById('purchase-pending');
const purUrgent = document.getElementById('purchase-urgent');
const purRate = document.getElementById('purchase-rate');
if(purTotal) purTotal.textContent = dashRes.purchase.total_demands || 0;
if(purPending) purPending.textContent = dashRes.purchase.pending || 0;
if(purUrgent) purUrgent.textContent = dashRes.purchase.urgent || 0;
if(purRate) purRate.textContent = (dashRes.purchase.completion_rate || 0) + '%';
}
// 更新智能建议
if(dashRes.smartSuggestions) {
const suggestionsEl = document.getElementById('smart-suggestions');
if(suggestionsEl) {
if(dashRes.smartSuggestions.length > 0) {
suggestionsEl.innerHTML = dashRes.smartSuggestions.map(s => `
<div style="margin-bottom:10px;padding:12px;background:var(--bg);border-radius:10px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:6px;color:var(--text);font-size:13px;display:flex;align-items:center;gap:6px">
<span>${s.icon}</span>
<span>${s.title}</span>
</div>
<div style="color:var(--text-2);font-size:12px;line-height:1.5">${s.message}</div>
</div>
`).join('');
} else {
suggestionsEl.innerHTML = '<div style="text-align:center;padding:30px 20px;background:var(--bg);border-radius:10px;border:1px solid var(--border)"><div style="font-size:28px;margin-bottom:8px">✨</div><div style="color:var(--text);font-size:14px;font-weight:600">暂无建议</div><div style="color:var(--text-2);font-size:12px;margin-top:4px">一切正常运行中</div></div>';
}
}
}
}
clearTimeout(timeoutId);
@ -2055,6 +2097,8 @@ const Dashboard = (() => {
${metricsCard('发货数量', data.shipments, 'warning')}
${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
</div>
<!-- 趋势图 + 环形图 -->
<div style="display:flex;gap:12px;margin-top:12px;flex-shrink:0">
<!-- 左侧趋势卡片 -->
@ -2135,15 +2179,15 @@ const Dashboard = (() => {
</div>
</div>
<!-- 右侧环形图卡片 -->
<div class="card" style="flex:1;padding:20px;border-radius:16px;display:flex;flex-direction:column;background:var(--surface);min-height:520px">
<div class="card" style="flex:1;padding:20px;border-radius:16px;display:flex;flex-direction:column;background:var(--surface);min-height:340px">
<div style="font-weight:600;font-size:14px;margin-bottom:12px">产量占比</div>
<div style="flex:1;display:grid;grid-template-columns:repeat(2,1fr);gap:16px;align-items:center">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;min-width:0">
<div style="flex:1;display:grid;grid-template-columns:repeat(2,1fr);gap:16px;align-items:start;align-content:center">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:flex-start;position:relative;min-width:0">
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">审计(本月)</div>
<div style="display:flex;align-items:center;justify-content:center;position:relative">
<div style="display:flex;align-items:center;justify-content:center;position:relative;height:160px;width:160px;flex-shrink:0">
<canvas id="donut-chart" style="width:160px;height:160px"></canvas>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:12px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:12px;width:100%">
<div style="text-align:center">
<div id="donut-pdd-pct" style="font-size:20px;font-weight:700;color:#3B82F6">0%</div>
<div style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px">
@ -2160,19 +2204,19 @@ const Dashboard = (() => {
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;min-width:0">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:flex-start;position:relative;min-width:0">
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">发货(按机种)</div>
<div style="display:flex;align-items:center;justify-content:center;position:relative">
<div style="display:flex;align-items:center;justify-content:center;position:relative;height:160px;width:160px;flex-shrink:0">
<canvas id="shipment-donut-chart" style="width:160px;height:160px"></canvas>
</div>
<div id="shipment-donut-legend" style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:12px"></div>
<div id="shipment-donut-legend" style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:12px;width:100%"></div>
</div>
</div>
</div>
</div>
<div style="margin-top:12px;flex:1;min-height:400px;display:flex;gap:12px">
<!-- 审计看板压缩宽度 -->
<div id="audit-card" class="card" style="flex:1;display:flex;flex-direction:column;min-height:260px;background:var(--surface);cursor:pointer" onclick="window.Dashboard.showAuditModal()">
<div id="audit-card" class="card" style="flex:1;display:flex;flex-direction:column;min-height:260px;background:var(--surface);cursor:pointer;border-radius:16px" onclick="window.Dashboard.showAuditModal()">
<div style="font-weight:600;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:8px">
<span style="display:flex;align-items:center;gap:8px">审计看板 <span style="font-size:11px;color:var(--text-2);font-weight:400">点击查看完整数据</span></span>
<div style="display:flex;gap:8px;align-items:center" onclick="event.stopPropagation()">
@ -2197,8 +2241,117 @@ const Dashboard = (() => {
</div>
</div>
<!-- AI智能报表 -->
${AIReport.generateAICard()}
<!-- 智能报表期初库存和采购需求 -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;flex:2">
<!-- 期初库存看板 -->
<div class="card" onclick="location.hash='#/plan-mgmt/initial-stock'" style="padding:20px;border-radius:16px;transition:all 0.3s;cursor:pointer;background:var(--surface);border:1px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.04);display:flex;flex-direction:column">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px">
<div>
<div style="font-size:13px;font-weight:500;color:var(--text-2);margin-bottom:4px">📦 库存管理</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">期初库存</div>
</div>
<div style="width:44px;height:44px;background:var(--bg);border-radius:12px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
</div>
<div style="background:var(--bg);border-radius:12px;padding:14px;margin-bottom:12px;border:1px solid var(--border)">
<div style="display:flex;align-items:baseline;gap:8px">
<span style="font-size:36px;font-weight:800;color:var(--text);letter-spacing:-1px;line-height:1" id="inventory-total">${data.inventory?.total_items || 0}</span>
<span style="font-size:14px;color:var(--text-2);font-weight:600">种物料</span>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">零库存</div>
<div style="font-size:20px;font-weight:700;color:#ef4444" id="inventory-zero">${data.inventory?.zero_stock || 0}</div>
</div>
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">低库存</div>
<div style="font-size:20px;font-weight:700;color:#f59e0b" id="inventory-low">${data.inventory?.low_stock || 0}</div>
</div>
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">总库存</div>
<div style="font-size:20px;font-weight:700;color:#10b981" id="inventory-qty">${data.inventory?.total_qty || 0}</div>
</div>
</div>
</div>
<!-- 采购需求看板 -->
<div class="card" onclick="location.hash='#/plan-mgmt/purchase-demand'" style="padding:20px;border-radius:16px;transition:all 0.3s;cursor:pointer;background:var(--surface);border:1px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.04);display:flex;flex-direction:column">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px">
<div>
<div style="font-size:13px;font-weight:500;color:var(--text-2);margin-bottom:4px">🛒 采购管理</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">采购需求</div>
</div>
<div style="width:44px;height:44px;background:var(--bg);border-radius:12px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="2">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
<line x1="3" y1="6" x2="21" y2="6"></line>
<path d="M16 10a4 4 0 0 1-8 0"></path>
</svg>
</div>
</div>
<div style="background:var(--bg);border-radius:12px;padding:14px;margin-bottom:12px;border:1px solid var(--border)">
<div style="display:flex;align-items:baseline;gap:8px">
<span style="font-size:36px;font-weight:800;color:var(--text);letter-spacing:-1px;line-height:1" id="purchase-total">${data.purchase?.total_demands || 0}</span>
<span style="font-size:14px;color:var(--text-2);font-weight:600">个需求</span>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">待处理</div>
<div style="font-size:20px;font-weight:700;color:#f59e0b" id="purchase-pending">${data.purchase?.pending || 0}</div>
</div>
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">紧急</div>
<div style="font-size:20px;font-weight:700;color:#ef4444" id="purchase-urgent">${data.purchase?.urgent || 0}</div>
</div>
<div style="background:var(--bg);border-radius:10px;padding:10px;border:1px solid var(--border)">
<div style="font-size:11px;color:var(--text-2);margin-bottom:4px;font-weight:500">完成率</div>
<div style="font-size:20px;font-weight:700;color:#10b981" id="purchase-rate">${data.purchase?.completion_rate || 0}%</div>
</div>
</div>
</div>
<!-- 智能建议卡片 -->
<div class="card" style="padding:20px;border-radius:16px;background:var(--surface);border:1px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.04);display:flex;flex-direction:column">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px">
<div>
<div style="font-size:13px;font-weight:500;color:var(--text-2);margin-bottom:4px">💡 AI助手</div>
<div style="font-size:16px;font-weight:700;color:var(--text)">智能建议</div>
</div>
<div style="width:44px;height:44px;background:var(--bg);border-radius:12px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
</div>
</div>
<div id="smart-suggestions" style="flex:1;overflow-y:auto;min-height:0">
${(data.smartSuggestions || []).length > 0 ?
data.smartSuggestions.map(s => `
<div style="margin-bottom:10px;padding:12px;background:var(--bg);border-radius:10px;border:1px solid var(--border)">
<div style="font-weight:600;margin-bottom:6px;color:var(--text);font-size:13px;display:flex;align-items:center;gap:6px">
<span>${s.icon}</span>
<span>${s.title}</span>
</div>
<div style="color:var(--text-2);font-size:12px;line-height:1.5">${s.message}</div>
</div>
`).join('') :
'<div style="text-align:center;padding:30px 20px;background:var(--bg);border-radius:10px;border:1px solid var(--border)"><div style="font-size:28px;margin-bottom:8px">✨</div><div style="color:var(--text);font-size:14px;font-weight:600">暂无建议</div><div style="color:var(--text-2);font-size:12px;margin-top:4px">一切正常运行中</div></div>'
}
</div>
</div>
</div>
<!-- 隐藏的旧卡片 -->
<div id="audit-pdd-card" class="card" style="display:none;flex-direction:column;flex:1;min-height:0;background:var(--surface)">

0
frontend/js/components/defects.js Normal file → Executable file
View File

0
frontend/js/components/devices.js Normal file → Executable file
View File

0
frontend/js/components/environment.js Normal file → Executable file
View File

0
frontend/js/components/export.js Normal file → Executable file
View File

0
frontend/js/components/finished-goods-receipt.js Normal file → Executable file
View File

0
frontend/js/components/initial-stock.js Normal file → Executable file
View File

2
frontend/js/components/login.js Normal file → Executable file
View File

@ -10,7 +10,7 @@ Router.register('/login', async () => {
const info = document.getElementById('user-info');
if (info) info.textContent = user?.username || '未登录';
}).catch(()=>{});
location.href = '#/dashboard';
window.location.hash = '#/dashboard';
} catch(e) {}
});
}, 0);

0
frontend/js/components/material-purchase.js Normal file → Executable file
View File

0
frontend/js/components/meituan-test.js Normal file → Executable file
View File

0
frontend/js/components/menu-search.js Normal file → Executable file
View File

0
frontend/js/components/notifications.js Normal file → Executable file
View File

0
frontend/js/components/operations-log.js Normal file → Executable file
View File

0
frontend/js/components/outsourcing-material-issue.js Normal file → Executable file
View File

0
frontend/js/components/outsourcing-orders.js Normal file → Executable file
View File

0
frontend/js/components/outsourcing-wip-stock.js Normal file → Executable file
View File

0
frontend/js/components/personnel.js Normal file → Executable file
View File

0
frontend/js/components/product-intro.js Normal file → Executable file
View File

0
frontend/js/components/production.js Normal file → Executable file
View File

0
frontend/js/components/purchase-demand.js Normal file → Executable file
View File

0
frontend/js/components/qa.js Normal file → Executable file
View File

0
frontend/js/components/reconciliation.js Normal file → Executable file
View File

0
frontend/js/components/settings.js Normal file → Executable file
View File

0
frontend/js/components/sidebar.js Normal file → Executable file
View File

0
frontend/js/components/work-order.js Normal file → Executable file
View File

0
frontend/js/router.js Normal file → Executable file
View File

0
frontend/js/utils/memory-monitor.js Normal file → Executable file
View File

458
frontend/login.html Normal file → Executable file
View File

@ -4,95 +4,128 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>登录 - 韬智生产管理系统</title>
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<link rel="icon" type="image/x-icon" href="../icon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./assets/login.css?v=20251231-1425" />
<link rel="stylesheet" href="./assets/login-animated.css?v=20251231-1427" />
</head>
<body>
<div class="login-wrapper">
<div class="container">
<div class="left">
<div class="form-header">
<h1 class="system-title">韬智生产管理系统</h1>
<p class="system-subtitle">Production Management System</p>
<div class="login-page">
<!-- 左侧角色动画区域 -->
<div class="characters-section">
<div class="brand-logo">
<img src="../icon.ico" alt="Logo" class="logo-icon" />
<span>韬智生产管理系统</span>
</div>
<div class="characters-container" id="characters-container">
<!-- 紫色角色 -->
<div class="character purple" id="char-purple">
<div class="eyes">
<div class="eye" id="purple-eye-left">
<div class="pupil" id="purple-pupil-left"></div>
</div>
<div class="eye" id="purple-eye-right">
<div class="pupil" id="purple-pupil-right"></div>
</div>
</div>
</div>
<form class="form" onsubmit="return false;">
<div class="input-block">
<input class="input" type="text" id="username" required autocomplete="username">
<!-- 黑色角色 -->
<div class="character black" id="char-black">
<div class="eyes">
<div class="eye" id="black-eye-left">
<div class="pupil" id="black-pupil-left"></div>
</div>
<div class="eye" id="black-eye-right">
<div class="pupil" id="black-pupil-right"></div>
</div>
</div>
</div>
<!-- 橙色角色 -->
<div class="character orange" id="char-orange">
<div class="eyes-simple">
<div class="pupil-simple" id="orange-pupil-left"></div>
<div class="pupil-simple" id="orange-pupil-right"></div>
</div>
</div>
<!-- 黄色角色 -->
<div class="character yellow" id="char-yellow">
<div class="eyes-simple">
<div class="pupil-simple" id="yellow-pupil-left"></div>
<div class="pupil-simple" id="yellow-pupil-right"></div>
</div>
<div class="mouth"></div>
</div>
</div>
<div class="footer-links">
<a href="#">隐私政策</a>
<a href="#">服务条款</a>
<a href="#">联系我们</a>
</div>
</div>
<!-- 右侧登录表单区域 -->
<div class="login-section">
<div class="login-container">
<div class="login-header">
<h1>欢迎回来!</h1>
<p>请输入您的登录信息</p>
</div>
<form class="login-form" onsubmit="return false;">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" class="form-input" placeholder="请输入用户名" required autocomplete="username">
</div>
<div class="input-block">
<input class="input" type="password" id="password" required autocomplete="current-password">
<div class="form-group">
<label for="password">密码</label>
<div class="password-wrapper">
<input type="text" id="password" class="form-input" placeholder="请输入密码" required autocomplete="current-password">
<button type="button" class="password-toggle" id="password-toggle" aria-label="隐藏密码">
<svg class="eye-icon eye-open" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-icon eye-closed" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
</div>
<div class="input-block captcha-block">
<input class="input captcha-input" type="text" id="captcha" required maxlength="4" autocomplete="off">
<label for="captcha">验证码</label>
<div class="form-group captcha-group">
<div class="captcha-input-wrapper">
<label for="captcha">验证码</label>
<input type="text" id="captcha" class="form-input" placeholder="请输入验证码" required maxlength="4" autocomplete="off">
</div>
<div class="captcha-image-wrapper" id="captcha-image-wrapper" title="点击刷新验证码">
<img id="captcha-image" class="captcha-image" alt="验证码" />
</div>
</div>
<div id="error-message" class="error-message" style="display:none;"></div>
<div class="input-block">
<span class="forgot"><a href="#">© 2025 韬智科技</a></span>
<button type="button" id="login-btn">
<span id="login-text">登录</span>
<span id="login-loader" class="btn-loader" style="display:none;">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
</button>
<button type="button" id="login-btn" class="login-button">
<span id="login-text">登录</span>
<span id="login-loader" class="btn-loader" style="display:none;">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
</button>
<div class="form-footer">
<span class="copyright">© 2025 韬智科技</span>
</div>
</form>
</div>
<div class="right">
<div class="img">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 731.67004 550.61784" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,334.13393c0,.66003,.53003,1.19,1.19006,1.19H730.48004c.65997,0,1.19-.52997,1.19-1.19,0-.65997-.53003-1.19-1.19-1.19H1.19006c-.66003,0-1.19006,.53003-1.19006,1.19Z" fill="#3f3d56"></path>
<polygon points="466.98463 81.60598 470.81118 130.55703 526.26809 107.39339 494.98463 57.60598 466.98463 81.60598" fill="#a0616a"></polygon>
<circle cx="465.32321" cy="55.18079" r="41.33858" fill="#a0616a"></circle>
<polygon points="387.98463 440.60598 394.98463 503.39339 345.98463 496.60598 361.98463 438.60598 387.98463 440.60598" fill="#a0616a"></polygon>
<polygon points="578.98463 449.60598 585.98463 512.39339 536.98463 505.60598 552.98463 447.60598 578.98463 449.60598" fill="#a0616a"></polygon>
<path d="M462.48463,260.10598c-.66897,0-54.14584,2.68515-89.47714,4.46286-16.72275,.84141-29.45202,15.31527-28.15459,32.00884l12.63173,162.5283,36,1,.87795-131,71.12205,4-3-73Z" fill="#2f2e41"></path>
<path d="M619.48463,259.10598s9,69,2,76c-7,7-226.5-5.5-226.5-5.5,0,0,48.15354-69.53704,56.82677-71.51852,8.67323-1.98148,146.67323-8.98148,146.67323-8.98148l21,10Z" fill="#2f2e41"></path>
<path id="uuid-91047c5b-47d7-4179-8a16-40bd6d529b28-203" d="M335.12666,172.23337c-8.35907-11.69074-9.10267-25.48009-1.66174-30.79863,7.44093-5.31854,20.24665-.15219,28.60713,11.54383,3.40375,4.62627,5.65012,10.00041,6.55111,15.67279l34.79215,49.9814-19.8001,13.70807-35.7745-48.83421c-5.07753-2.68845-9.43721-6.55406-12.71405-11.27326Z" fill="#a0616a"></path>
<path d="M464.98463,112.60598l51-21,96,148s-67,15-90,18c-23,3-49-9-49-9l-8-136Z" fill="#5e7eb6"></path>
<path d="M526.98463,137.60598l-18.5-57.70866,24,18.20866s68,45,68,64c0,19,21,77,21,77,0,0,23.5,19.5,15.5,37.5-8,18,10.5,15.5,12.5,28.5,2,13-28.5,30.5-28.5,30.5,0,0-7.5-73.5-31.5-73.5-24,0-62.5-124.5-62.5-124.5Z" fill="#3f3d56"></path>
<path d="M468.56831,111.13035l-25.08368,9.97563s4,70,8,76c4,6,18,38,18,38v10.42913s-28,8.57087-27,13.57087c1,5,66,19,66,19,0,0-13-40-21-53-8-13-18.91632-113.97563-18.91632-113.97563Z" fill="#3f3d56"></path>
<path d="M452.48463,121.10598s-29-4-34,30c-5,34-1.82283,38.5-1.82283,38.5l-8.17717,19.5-27-30-26,17s47,76,66,74c19-2,47-57,47-57l-16-92Z" fill="#3f3d56"></path>
<path d="M597.32321,270.14478l-14.83858,209.96121-38.5-1.5s-8.5-198.5-8.5-201.5c0-3,4-20,29-21,25-1,32.83858,14.03879,32.83858,14.03879Z" fill="#2f2e41"></path>
<path d="M541.48463,484.10598s20-6,23-2c3,4,20,6,20,6l5,49s-14,10-16,12-55,4-56-8c-1-12,14-27,14-27l10-30Z" fill="#2f2e41"></path>
<path d="M394.48463,470.10598s6-5,8,9c2,14,9,37-1,40-10,3-110,4-110-5v-9l9-7,18.00394-2.869s34.99606-32.131,38.99606-32.131c4,0,17,13,17,13l20-6Z" fill="#2f2e41"></path>
<path d="M505.98463,77.60598s-20-24-28-22-3,5-3,5l-20-22s-16-6-31,13c0,0-9-16,0-25,9-9,12-8,14-13,2-5,16-9,16-9,0,0-.80315-7.19685,3.59843-3.59843s15.3937,3.59843,15.3937,3.59843c0,0,.06299-4,4.53543,0,4.47244,4,9.47244,2,9.47244,2,0,0,0,6.92126,3.5,6.96063,3.5,.03937,9.5-4.96063,10.5-.96063,1,4,8,6,9,18,1,12-4,47-4,47Z" fill="#2f2e41"></path>
<g>
<path d="M342.99463,178.84874l-114.2362,78.82694c-3.94205,2.72015-9.36214,1.72624-12.08229-2.21581l-32.16176-46.60891c-2.72015-3.94205-1.7259-9.36208,2.21615-12.08223l114.2362-78.82694c3.94205-2.72015,9.36214-1.72624,12.08229,2.21581l32.16176,46.60891c2.72015,3.94205,1.7259,9.36208-2.21615,12.08223Z" fill="#fff"></path>
<path d="M312.83914,120.30274l32.16148,46.6085c2.64627,3.83499,1.68408,9.08121-2.15091,11.72749l-56.06388,38.68602c-14.78562-4.04015-28.2774-13.11486-37.66263-26.71596-6.14766-8.9092-9.85314-18.77211-11.26649-28.80885l63.25494-43.6481c3.83499-2.64627,9.08121-1.68408,11.72749,2.15091Z" fill="#e6e6e6"></path>
<path d="M223.84012,260.20913c-3.0791,0-6.10938-1.46094-7.9873-4.18066l-32.16211-46.60938c-1.4668-2.12695-2.01758-4.7002-1.5498-7.24805,.4668-2.54785,1.89551-4.75879,4.02246-6.22559l114.23535-78.82715c4.39746-3.03223,10.44043-1.92285,13.47363,2.4707l32.16211,46.60938c1.4668,2.12695,2.01758,4.7002,1.5498,7.24805-.4668,2.54688-1.89551,4.75879-4.02148,6.22559l-114.23633,78.82715c-1.67578,1.15527-3.59082,1.70996-5.48633,1.70996Zm82.04785-142.80176c-1.50391,0-3.02344,.44043-4.35254,1.35742l-114.23633,78.82715c-1.6875,1.16309-2.82031,2.91797-3.19141,4.94043-.37109,2.02148,.06543,4.06445,1.22949,5.75l32.16211,46.60938c2.40625,3.48633,7.20215,4.36816,10.69043,1.96094l114.2373-78.82715c1.68652-1.16309,2.81934-2.91797,3.19043-4.94043,.37109-2.02148-.06543-4.06445-1.22949-5.75l-32.16211-46.60938c-1.48926-2.1582-3.89453-3.31836-6.33789-3.31836Z" fill="#3f3d56"></path>
<path d="M224.6666,236.93718c-2.89521,1.9978-3.6253,5.97848-1.6275,8.87369,1.9978,2.89521,5.97848,3.6253,8.87369,1.6275l11.76134-8.11573c2.89521-1.9978,3.6253-5.97848,1.6275-8.87369-1.9978-2.89521-5.97848-3.6253-8.87369-1.6275l-11.76134,8.11573Z" fill="#5e7eb6"></path>
<path d="M232.63862,171.91114c-4.56802,3.15209-5.71978,9.43286-2.56769,14.00088,3.15209,4.56802,9.43252,5.71972,14.00054,2.56763l18.29546-12.6245c4.56802-3.15209,5.72007-9.43245,2.56797-14.00047-3.15209-4.56802-9.4328-5.72013-14.00082-2.56804l-18.29546,12.6245Z" fill="#5e7eb6"></path>
</g>
<g>
<path d="M340.25926,185.80874H201.4659c-4.78947,0-8.68608-3.89636-8.68608-8.68583v-56.62834c0-4.78947,3.89661-8.68583,8.68608-8.68583h138.79336c4.78947,0,8.68608,3.89636,8.68608,8.68583v56.62834c0,4.78947-3.89661,8.68583-8.68608,8.68583Z" fill="#fff"></path>
<path d="M348.69017,120.49482v56.62784c0,4.65939-3.77152,8.43091-8.43091,8.43091h-68.11583c-9.87497-11.72273-15.82567-26.8544-15.82567-43.37931,0-10.82439,2.55172-21.04674,7.08876-30.11034h76.85275c4.65939,0,8.43091,3.77152,8.43091,8.43091Z" fill="#e6e6e6"></path>
<path d="M340.25907,186.80874H201.4661c-5.34082,0-9.68652-4.34473-9.68652-9.68555v-56.62891c0-5.34082,4.3457-9.68555,9.68652-9.68555h138.79297c5.34082,0,9.68652,4.34473,9.68652,9.68555v56.62891c0,5.34082-4.3457,9.68555-9.68652,9.68555ZM201.4661,112.80874c-4.23828,0-7.68652,3.44727-7.68652,7.68555v56.62891c0,4.23828,3.44824,7.68555,7.68652,7.68555h138.79297c4.23828,0,7.68652-3.44727,7.68652-7.68555v-56.62891c0-4.23828-3.44824-7.68555-7.68652-7.68555H201.4661Z" fill="#3f3d56"></path>
<path d="M209.87637,166.41564c-3.51759,0-6.37931,2.86172-6.37931,6.37931s2.86172,6.37931,6.37931,6.37931h14.28966c3.51759,0,6.37931-2.86172,6.37931-6.37931s-2.86172-6.37931-6.37931-6.37931h-14.28966Z" fill="#5e7eb6"></path>
<path d="M253.36907,117.42253c-5.55,0-10.06511,4.51536-10.06511,10.06536s4.51511,10.06486,10.06511,10.06486h22.22841c5.55,0,10.06511-4.51486,10.06511-10.06486s-4.51511-10.06536-10.06511-10.06536h-22.22841Z" fill="#5e7eb6"></path>
</g>
<g>
<path d="M456.25926,381.80874h-138.79336c-4.78947,0-8.68608-3.89636-8.68608-8.68583v-56.62834c0-4.78947,3.89661-8.68583,8.68608-8.68583h138.79336c4.78947,0,8.68608,3.89636,8.68608,8.68583v56.62834c0,4.78947-3.89661,8.68583-8.68608,8.68583Z" fill="#fff"></path>
<path d="M464.69017,316.49482v56.62784c0,4.65939-3.77152,8.43091-8.43091,8.43091h-68.11583c-9.87497-11.72273-15.82567-26.8544-15.82567-43.37931,0-10.82439,2.55172-21.04674,7.08876-30.11034h76.85275c4.65939,0,8.43091,3.77152,8.43091,8.43091Z" fill="#e6e6e6"></path>
<path d="M456.25907,382.80874h-138.79297c-5.34082,0-9.68652-4.34473-9.68652-9.68555v-56.62891c0-5.34082,4.3457-9.68555,9.68652-9.68555h138.79297c5.34082,0,9.68652,4.34473,9.68652,9.68555v56.62891c0,5.34082-4.3457,9.68555-9.68652,9.68555Zm-138.79297-74c-4.23828,0-7.68652,3.44727-7.68652,7.68555v56.62891c0,4.23828,3.44824,7.68555,7.68652,7.68555h138.79297c4.23828,0,7.68652-3.44727,7.68652-7.68555v-56.62891c0-4.23828-3.44824-7.68555-7.68652-7.68555h-138.79297Z" fill="#3f3d56"></path>
<path d="M325.87637,362.41564c-3.51759,0-6.37931,2.86172-6.37931,6.37931s2.86172,6.37931,6.37931,6.37931h14.28966c3.51759,0,6.37931-2.86172,6.37931-6.37931s-2.86172-6.37931-6.37931-6.37931h-14.28966Z" fill="#5e7eb6"></path>
<path d="M369.36907,313.42253c-5.55,0-10.06511,4.51536-10.06511,10.06536s4.51511,10.06486,10.06511,10.06486h22.22841c5.55,0,10.06511-4.51486,10.06511-10.06486s-4.51511-10.06536-10.06511-10.06536h-22.22841Z" fill="#5e7eb6"></path>
</g>
<path id="uuid-c026fd96-7d81-4b34-bb39-0646c0e08e96-204" d="M465.67391,331.01678c-12.74718,6.63753-26.5046,5.44058-30.72743-2.67249-4.22283-8.11308,2.6878-20.06802,15.44041-26.70621,5.05777-2.72156,10.69376-4.19231,16.43644-4.28916l54.36547-27.44139,10.79681,21.52636-53.36733,28.57487c-3.37375,4.65048-7.81238,8.42516-12.94437,11.00803Z" fill="#a0616a"></path>
<path d="M527.48463,97.10598s56-3,68,27c12,30,22,128,22,128l-122,66.37402-21-32.37402,82-64-29-125Z" fill="#3f3d56"></path>
</svg>
</div>
</div>
</div>
</div>
@ -106,7 +139,7 @@ async function checkAuth() {
if (response.ok) {
const user = await response.json();
if (user && user.username) {
window.location.replace('/index.html#/dashboard');
window.location.replace('./index.html#/dashboard');
}
}
} catch(e) {
@ -120,6 +153,7 @@ checkAuth();
// 登录处理
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const passwordToggle = document.getElementById('password-toggle');
const captchaInput = document.getElementById('captcha');
const captchaImage = document.getElementById('captcha-image');
const captchaImageWrapper = document.getElementById('captcha-image-wrapper');
@ -227,7 +261,7 @@ async function handleLogin() {
if (result.ok) {
// 登录成功,跳转到主页面
// 使用 replace 避免 Safari 缓存问题
window.location.replace('/index.html#/dashboard');
window.location.replace('./index.html#/dashboard');
} else {
throw new Error('登录失败');
}
@ -263,6 +297,284 @@ captchaInput.addEventListener('keypress', (e) => {
// 自动聚焦用户名输入框
usernameInput.focus();
// ============ 角色动画系统 ============
let mouseX = 0, mouseY = 0;
let isTyping = false;
let showPassword = false;
// 获取所有角色元素
const charPurple = document.getElementById('char-purple');
const charBlack = document.getElementById('char-black');
const charOrange = document.getElementById('char-orange');
const charYellow = document.getElementById('char-yellow');
const purpleEyeLeft = document.getElementById('purple-eye-left');
const purpleEyeRight = document.getElementById('purple-eye-right');
const purplePupilLeft = document.getElementById('purple-pupil-left');
const purplePupilRight = document.getElementById('purple-pupil-right');
const blackEyeLeft = document.getElementById('black-eye-left');
const blackEyeRight = document.getElementById('black-eye-right');
const blackPupilLeft = document.getElementById('black-pupil-left');
const blackPupilRight = document.getElementById('black-pupil-right');
const orangePupilLeft = document.getElementById('orange-pupil-left');
const orangePupilRight = document.getElementById('orange-pupil-right');
const yellowPupilLeft = document.getElementById('yellow-pupil-left');
const yellowPupilRight = document.getElementById('yellow-pupil-right');
// 鼠标跟踪 - 使用requestAnimationFrame节流
let rafId = null;
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
if (!rafId) {
rafId = requestAnimationFrame(() => {
updateCharacterPositions();
updatePupilPositions();
rafId = null;
});
}
});
// 计算瞳孔位置
function calculatePupilPosition(element, maxDistance = 5) {
if (!element) return { x: 0, y: 0 };
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = mouseX - centerX;
const deltaY = mouseY - centerY;
const distance = Math.min(Math.sqrt(deltaX ** 2 + deltaY ** 2), maxDistance);
const angle = Math.atan2(deltaY, deltaX);
return {
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance
};
}
// 计算身体倾斜角度和脸部位置
function calculatePosition(element) {
if (!element) return { faceX: 0, faceY: 0, bodySkew: 0 };
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 3; // 聚焦头部区域
const deltaX = mouseX - centerX;
const deltaY = mouseY - centerY;
// 脸部移动(用于眼睛位置)
const faceX = Math.max(-15, Math.min(15, deltaX / 20));
const faceY = Math.max(-10, Math.min(10, deltaY / 30));
// 身体倾斜(负值表示向鼠标方向倾斜)
const bodySkew = Math.max(-6, Math.min(6, -deltaX / 120));
return { faceX, faceY, bodySkew };
}
// 更新角色身体位置
function updateCharacterPositions() {
const hasPassword = passwordInput.value.length > 0;
const isPasswordVisible = passwordInput.getAttribute('type') === 'text';
// 紫色角色
const purplePos = calculatePosition(charPurple);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 站高,不倾斜
charPurple.style.height = '440px';
charPurple.style.transform = `skewX(0deg)`;
} else if (isTyping || (hasPassword && !isPasswordVisible)) {
// 输入邮箱或密码隐藏时 - 站高并倾斜
charPurple.style.height = '440px';
charPurple.style.transform = `skewX(${purplePos.bodySkew - 12}deg) translateX(40px)`;
} else {
// 正常状态 - 根据鼠标晃动
charPurple.style.height = '400px';
charPurple.style.transform = `skewX(${purplePos.bodySkew}deg)`;
}
// 黑色角色
const blackPos = calculatePosition(charBlack);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 不倾斜
charBlack.style.transform = `skewX(0deg)`;
} else if (isLookingAtEachOther) {
// 互看时 - 倾斜并移动
charBlack.style.transform = `skewX(${blackPos.bodySkew * 1.5 + 10}deg) translateX(20px)`;
} else if (isTyping || (hasPassword && !isPasswordVisible)) {
charBlack.style.transform = `skewX(${blackPos.bodySkew * 1.5}deg)`;
} else {
charBlack.style.transform = `skewX(${blackPos.bodySkew}deg)`;
}
// 橙色和黄色角色
const orangePos = calculatePosition(charOrange);
const yellowPos = calculatePosition(charYellow);
if (hasPassword && isPasswordVisible) {
// 密码可见时 - 不倾斜
charOrange.style.transform = `skewX(0deg)`;
charYellow.style.transform = `skewX(0deg)`;
} else {
charOrange.style.transform = `skewX(${orangePos.bodySkew}deg)`;
charYellow.style.transform = `skewX(${yellowPos.bodySkew}deg)`;
}
}
// 更新瞳孔位置
function updatePupilPositions() {
const hasPassword = passwordInput.value.length > 0;
const isPasswordVisible = passwordInput.getAttribute('type') === 'text';
// 根据状态决定眼睛方向
if (isLookingAtEachOther) {
// 输入邮箱时 - 互相看向对方
purplePupilLeft.style.transform = 'translate(3px, 4px)';
purplePupilRight.style.transform = 'translate(3px, 4px)';
blackPupilLeft.style.transform = 'translate(0px, -4px)';
blackPupilRight.style.transform = 'translate(0px, -4px)';
// 橙色和黄色跟随鼠标
const orangeLeftPos = calculatePupilPosition(orangePupilLeft, 5);
const orangeRightPos = calculatePupilPosition(orangePupilRight, 5);
orangePupilLeft.style.transform = `translate(${orangeLeftPos.x}px, ${orangeLeftPos.y}px)`;
orangePupilRight.style.transform = `translate(${orangeRightPos.x}px, ${orangeRightPos.y}px)`;
const yellowLeftPos = calculatePupilPosition(yellowPupilLeft, 5);
const yellowRightPos = calculatePupilPosition(yellowPupilRight, 5);
yellowPupilLeft.style.transform = `translate(${yellowLeftPos.x}px, ${yellowLeftPos.y}px)`;
yellowPupilRight.style.transform = `translate(${yellowRightPos.x}px, ${yellowRightPos.y}px)`;
} else if (hasPassword && isPasswordVisible) {
// 密码可见时 - 所有小人看向左侧
purplePupilLeft.style.transform = 'translate(-5px, 0px)';
purplePupilRight.style.transform = 'translate(-5px, 0px)';
blackPupilLeft.style.transform = 'translate(-4px, 0px)';
blackPupilRight.style.transform = 'translate(-4px, 0px)';
orangePupilLeft.style.transform = 'translate(-5px, 0px)';
orangePupilRight.style.transform = 'translate(-5px, 0px)';
yellowPupilLeft.style.transform = 'translate(-5px, 0px)';
yellowPupilRight.style.transform = 'translate(-5px, 0px)';
} else if (hasPassword && !isPasswordVisible) {
// 密码隐藏时 - 所有小人看向右下侧
purplePupilLeft.style.transform = 'translate(4px, 4px)';
purplePupilRight.style.transform = 'translate(4px, 4px)';
blackPupilLeft.style.transform = 'translate(4px, 4px)';
blackPupilRight.style.transform = 'translate(4px, 4px)';
orangePupilLeft.style.transform = 'translate(5px, 4px)';
orangePupilRight.style.transform = 'translate(5px, 4px)';
yellowPupilLeft.style.transform = 'translate(5px, 4px)';
yellowPupilRight.style.transform = 'translate(5px, 4px)';
} else {
// 正常状态 - 跟随鼠标
const purpleLeftPos = calculatePupilPosition(purpleEyeLeft, 5);
const purpleRightPos = calculatePupilPosition(purpleEyeRight, 5);
purplePupilLeft.style.transform = `translate(${purpleLeftPos.x}px, ${purpleLeftPos.y}px)`;
purplePupilRight.style.transform = `translate(${purpleRightPos.x}px, ${purpleRightPos.y}px)`;
const blackLeftPos = calculatePupilPosition(blackEyeLeft, 4);
const blackRightPos = calculatePupilPosition(blackEyeRight, 4);
blackPupilLeft.style.transform = `translate(${blackLeftPos.x}px, ${blackLeftPos.y}px)`;
blackPupilRight.style.transform = `translate(${blackRightPos.x}px, ${blackRightPos.y}px)`;
const orangeLeftPos = calculatePupilPosition(orangePupilLeft, 5);
const orangeRightPos = calculatePupilPosition(orangePupilRight, 5);
orangePupilLeft.style.transform = `translate(${orangeLeftPos.x}px, ${orangeLeftPos.y}px)`;
orangePupilRight.style.transform = `translate(${orangeRightPos.x}px, ${orangeRightPos.y}px)`;
const yellowLeftPos = calculatePupilPosition(yellowPupilLeft, 5);
const yellowRightPos = calculatePupilPosition(yellowPupilRight, 5);
yellowPupilLeft.style.transform = `translate(${yellowLeftPos.x}px, ${yellowLeftPos.y}px)`;
yellowPupilRight.style.transform = `translate(${yellowRightPos.x}px, ${yellowRightPos.y}px)`;
}
}
// 眨眼动画
function blinkEye(eyeLeft, eyeRight) {
eyeLeft.style.height = '2px';
eyeRight.style.height = '2px';
setTimeout(() => {
eyeLeft.style.height = '';
eyeRight.style.height = '';
}, 150);
}
// 随机眨眼
function scheduleRandomBlink(eyeLeft, eyeRight) {
const delay = Math.random() * 4000 + 3000;
setTimeout(() => {
if (Math.random() > 0.3) {
blinkEye(eyeLeft, eyeRight);
}
scheduleRandomBlink(eyeLeft, eyeRight);
}, delay);
}
scheduleRandomBlink(purpleEyeLeft, purpleEyeRight);
scheduleRandomBlink(blackEyeLeft, blackEyeRight);
// 添加isLookingAtEachOther状态
let isLookingAtEachOther = false;
// 输入框聚焦 - 角色互看
usernameInput.addEventListener('focus', () => {
isTyping = true;
isLookingAtEachOther = true;
// 短暂互看后恢复
setTimeout(() => {
isLookingAtEachOther = false;
}, 800);
});
usernameInput.addEventListener('blur', () => {
if (!passwordInput.value) {
isTyping = false;
}
isLookingAtEachOther = false;
});
// 密码输入
passwordInput.addEventListener('focus', () => {
isTyping = true;
});
passwordInput.addEventListener('blur', () => {
if (!passwordInput.value) {
isTyping = false;
}
});
passwordInput.addEventListener('input', () => {
updateCharacterPositions();
updatePupilPositions();
});
// 密码可见性切换(默认显示,点击隐藏)
passwordToggle.addEventListener('click', () => {
const type = passwordInput.getAttribute('type');
const eyeOpen = passwordToggle.querySelector('.eye-open');
const eyeClosed = passwordToggle.querySelector('.eye-closed');
if (type === 'text') {
// 点击后隐藏密码
passwordInput.setAttribute('type', 'password');
eyeOpen.style.display = 'none';
eyeClosed.style.display = 'block';
passwordToggle.setAttribute('aria-label', '显示密码');
} else {
// 点击后显示密码
passwordInput.setAttribute('type', 'text');
eyeOpen.style.display = 'block';
eyeClosed.style.display = 'none';
passwordToggle.setAttribute('aria-label', '隐藏密码');
}
updatePupilPositions();
});
</script>
</body>
</html>

0
frontend/sop_files/sop_20251122172515_xlsx Normal file → Executable file
View File

0
frontend/sop_files/sop_20251122172848_file.xlsx Normal file → Executable file
View File

View File

0
import_reconciliation_excel.py Normal file → Executable file
View File

0
init_customer_orders.py Normal file → Executable file
View File

0
init_reconciliations.py Normal file → Executable file
View File

0
manager.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

0
material_purchase_template.csv Normal file → Executable file
View File

0
query_shipment_sn.sh Normal file → Executable file
View File

0
server/=2.0.0 Normal file → Executable file
View File

0
server/ai_routes.py Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More