小问题修复

This commit is contained in:
zzh 2025-12-18 09:30:09 +08:00
parent 2ba1f32a67
commit 512440a2c1
13 changed files with 1218 additions and 171 deletions

View File

@ -166,6 +166,168 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.loader .dot:nth-child(2){animation-delay:.15s}
.loader .dot:nth-child(3){animation-delay:.3s}
@keyframes bounce{0%,100%{transform:translateY(0);opacity:.7}50%{transform:translateY(-8px);opacity:1}}
.three-body {
--uib-size: 35px;
--uib-speed: 0.8s;
--uib-color: #5D3FD3;
position: relative;
display: inline-block;
height: var(--uib-size);
width: var(--uib-size);
animation: spin78236 calc(var(--uib-speed) * 2.5) infinite linear;
}
.three-body__dot {
position: absolute;
height: 100%;
width: 30%;
}
.three-body__dot:after {
content: '';
position: absolute;
height: 0%;
width: 100%;
padding-bottom: 100%;
background-color: var(--uib-color);
border-radius: 50%;
}
.three-body__dot:nth-child(1) {
bottom: 5%;
left: 0;
transform: rotate(60deg);
transform-origin: 50% 85%;
}
.three-body__dot:nth-child(1)::after {
bottom: 0;
left: 0;
animation: wobble1 var(--uib-speed) infinite ease-in-out;
animation-delay: calc(var(--uib-speed) * -0.3);
}
.three-body__dot:nth-child(2) {
bottom: 5%;
right: 0;
transform: rotate(-60deg);
transform-origin: 50% 85%;
}
.three-body__dot:nth-child(2)::after {
bottom: 0;
left: 0;
animation: wobble1 var(--uib-speed) infinite
calc(var(--uib-speed) * -0.15) ease-in-out;
}
.three-body__dot:nth-child(3) {
bottom: -5%;
left: 0;
transform: translateX(116.666%);
}
.three-body__dot:nth-child(3)::after {
top: 0;
left: 0;
animation: wobble2 var(--uib-speed) infinite ease-in-out;
}
@keyframes spin78236 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes wobble1 {
0%,
100% {
transform: translateY(0%) scale(1);
opacity: 1;
}
50% {
transform: translateY(-66%) scale(0.65);
opacity: 0.8;
}
}
@keyframes wobble2 {
0%,
100% {
transform: translateY(0%) scale(1);
opacity: 1;
}
50% {
transform: translateY(66%) scale(0.65);
opacity: 0.8;
}
}
.spinner {
width: 44px;
height: 44px;
position: relative;
perspective: 800px;
animation: spinner-y0fdc1 2s infinite ease;
transform-style: preserve-3d;
}
.spinner > div {
background-color: rgba(0,77,255,0.2);
height: 100%;
position: absolute;
width: 100%;
border: 2px solid #004dff;
}
.spinner div:nth-of-type(1) {
transform: translateZ(-22px) rotateY(180deg);
}
.spinner div:nth-of-type(2) {
transform: rotateY(-270deg) translateX(50%);
transform-origin: top right;
}
.spinner div:nth-of-type(3) {
transform: rotateY(270deg) translateX(-50%);
transform-origin: center left;
}
.spinner div:nth-of-type(4) {
transform: rotateX(90deg) translateY(-50%);
transform-origin: top center;
}
.spinner div:nth-of-type(5) {
transform: rotateX(-90deg) translateY(50%);
transform-origin: bottom center;
}
.spinner div:nth-of-type(6) {
transform: translateZ(22px);
}
@keyframes spinner-y0fdc1 {
0% {
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
}
50% {
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
}
100% {
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
}
}
.fade-enter{opacity:0;transform:translateY(8px)}
.fade-enter-active{transition:opacity .25s ease-out,transform .25s ease-out;opacity:1;transform:translateY(0)}
.error{color:#ffb4b4}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765851714875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3726" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1024 514.1c0 275.8-223.6 499.3-499.3 499.3-275.8 0-499.3-223.5-499.3-499.3S248.9 14.8 524.7 14.8c275.7 0 499.3 223.5 499.3 499.3z" fill="#dbdbdb" p-id="3727" data-spm-anchor-id="a313x.search_index.0.i0.1a5e3a819i9fxq" class="selected"></path><path d="M418.5 674.5l-71.3 338.9h-71.4l71.4-338.9zM631.7 674.5l71.3 338.9h71.3L703 674.5zM382 86.1h285.3v53.5H382z" fill="#404040" p-id="3728"></path><path d="M96.7 157.4h856v588.5h-856z" fill="#FFFFFF" p-id="3729"></path><path d="M577.3 387.5l51.8-72.6c-29.2-20.9-65-33.2-103.6-33.2v89.2c19.3-0.1 37.2 6.1 51.8 16.6z" fill="#FBD764" p-id="3730"></path><path d="M614.7 460c0 16.9-4.8 32.7-13 46.2l76.4 46.3c16.4-27 25.8-58.6 25.8-92.4 0-59.8-29.5-112.7-74.7-145.1l-51.8 72.6c22.5 16.1 37.3 42.5 37.3 72.4z" fill="#828282" p-id="3731"></path><path d="M601.7 506.1c-15.6 25.7-43.8 43-76.2 43-49.2 0-89.2-39.9-89.2-89.2 0-49.2 39.9-89.2 89.2-89.2v-89.2c-98.5 0-178.3 79.8-178.3 178.3S427 638.3 525.5 638.3c64.6 0 121.3-34.4 152.5-85.9l-76.3-46.3zM311.5 242.1h285.3v8.9H311.5zM311.5 224.3h214v8.9h-214z" fill="#404040" p-id="3732"></path><path d="M311.5 674.5h35.7v35.7h-35.7z" fill="#FBD764" p-id="3733"></path><path d="M436.3 674.5H472v35.7h-35.7z" fill="#828282" p-id="3734"></path><path d="M561.6 674.9h34.9v34.9h-34.9zM614.7 687.9h124.8v8.9H614.7zM489.8 687.9h53.5v8.9h-53.5z" fill="#404040" p-id="3735"></path><path d="M365 687.9h53.5v8.9H365z" fill="#404040" p-id="3736"></path><path d="M311.5 184.2h428.8V202H311.5z" fill="#404040" p-id="3737"></path><path d="M61.1 745.9h927.3v35.7H61.1z" fill="#545454" p-id="3738"></path><path d="M61.1 121.7h927.3v35.7H61.1z" fill="#545454" p-id="3739"></path><path d="M346.4 864H703v17.8H346.4z" fill="#404040" p-id="3740"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -167,10 +167,13 @@
</div>
<div id="toast" class="toast"></div>
<div id="overlay" class="overlay hidden">
<div class="loader">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<script src="./js/router.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,34 @@
</div>
</div>
<div id="export-date-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="window.Reconciliation.closeExportDateModal()">&times;</button>
</div>
<div class="modal-body">
<div style="padding: 20px 0;">
<div class="field" style="margin-bottom: 15px;">
<label>交货开始日期</label>
<input type="date" id="export-start-date" class="input" placeholder="留空表示不限" />
</div>
<div class="field" style="margin-bottom: 15px;">
<label>交货结束日期</label>
<input type="date" id="export-end-date" class="input" placeholder="留空表示不限" />
</div>
<div style="padding: 10px; background: #f0f8ff; border-radius: 4px; font-size: 14px; color: #666;">
<strong>提示</strong>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="window.Reconciliation.closeExportDateModal()">取消</button>
<button class="btn btn-primary" onclick="window.Reconciliation.confirmExport()">确定导出</button>
</div>
</div>
</div>
<div id="reconciliation-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
@ -149,47 +177,8 @@
const exportBtn = document.getElementById('export-reconciliation-btn');
if (exportBtn) {
exportBtn.addEventListener('click', async () => {
try {
API.toast('正在导出对账单...', 'info');
const res = await fetch('/api/reconciliations/export');
if (!res.ok) {
const data = await res.json();
API.toast(data.error || '导出失败', 'error');
return;
}
// 下载文件
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 从响应头获取文件名,如果没有则使用默认名称
const contentDisposition = res.headers.get('Content-Disposition');
let filename = '对账单.xlsx';
if (contentDisposition) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
// 解码 URL 编码的文件名
filename = decodeURIComponent(filename);
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
API.toast('导出成功', 'success');
} catch (err) {
console.error('导出对账单失败:', err);
API.toast('导出失败', 'error');
}
exportBtn.addEventListener('click', () => {
openExportDateModal();
});
}
@ -548,12 +537,94 @@
}
}
// 打开导出日期选择弹窗
function openExportDateModal() {
const modal = document.getElementById('export-date-modal');
document.getElementById('export-start-date').value = '';
document.getElementById('export-end-date').value = '';
modal.style.display = 'flex';
}
// 关闭导出日期选择弹窗
function closeExportDateModal() {
const modal = document.getElementById('export-date-modal');
modal.style.display = 'none';
}
// 确认导出
async function confirmExport() {
try {
const startDate = document.getElementById('export-start-date').value;
const endDate = document.getElementById('export-end-date').value;
// 验证日期范围
if (startDate && endDate && startDate > endDate) {
API.toast('开始日期不能晚于结束日期', 'error');
return;
}
// 关闭弹窗
closeExportDateModal();
API.toast('正在导出对账单...', 'info');
// 构建URL参数
let url = '/api/reconciliations/export';
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (params.toString()) {
url += '?' + params.toString();
}
const res = await fetch(url);
if (!res.ok) {
const data = await res.json();
API.toast(data.error || '导出失败', 'error');
return;
}
// 下载文件
const blob = await res.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
// 从响应头获取文件名,如果没有则使用默认名称
const contentDisposition = res.headers.get('Content-Disposition');
let filename = '对账单.xlsx';
if (contentDisposition) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
// 解码 URL 编码的文件名
filename = decodeURIComponent(filename);
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
API.toast('导出成功', 'success');
} catch (err) {
console.error('导出对账单失败:', err);
API.toast('导出失败', 'error');
}
}
window.Reconciliation = {
openModal,
closeModal,
saveReconciliation,
editReconciliation,
deleteReconciliation,
updateBatchDeleteButton
updateBatchDeleteButton,
openExportDateModal,
closeExportDateModal,
confirmExport
};
})();

View File

@ -873,8 +873,8 @@ def dashboard():
ten_min_ago = now_bj - timedelta(minutes=10)
# 从 Redis 获取审计数据来计算今日产量和活跃状态
today_pdd = 0
today_yt = 0
today_pdd_macs = set()
today_yt_macs = set()
pdd_active = False
yt_active = False
@ -900,8 +900,9 @@ def dashboard():
try:
parsed = parse_audit_line(item)
ts_str = parsed.get('ts_cn') or ''
if ts_str.startswith(today_bj):
today_pdd += 1
mac = parsed.get('mac') or ''
if ts_str.startswith(today_bj) and mac:
today_pdd_macs.add(mac)
# 检查是否在最近10分钟内ts_cn 格式为 YYYY-MM-DD HH:MM:SS
try:
item_time = datetime.strptime(ts_str, '%Y-%m-%d %H:%M:%S')
@ -918,8 +919,9 @@ def dashboard():
try:
parsed = parse_audit_line(item)
ts_str = parsed.get('ts_cn') or ''
if ts_str.startswith(today_bj):
today_yt += 1
mac = parsed.get('mac') or ''
if ts_str.startswith(today_bj) and mac:
today_yt_macs.add(mac)
# 检查是否在最近10分钟内ts_cn 格式为 YYYY-MM-DD HH:MM:SS
try:
item_time = datetime.strptime(ts_str, '%Y-%m-%d %H:%M:%S')
@ -933,6 +935,9 @@ def dashboard():
except Exception as e:
log('dashboard_audit_error', str(e))
today_pdd = len(today_pdd_macs)
today_yt = len(today_yt_macs)
# 调试日志
log('dashboard_platform_debug', json.dumps({
'today_pdd': today_pdd,
@ -1007,20 +1012,18 @@ def audit_pdd():
r.connection_pool.connection_kwargs['socket_timeout'] = 5
items = []
# 限制最大返回数量,避免数据过大
max_items = 500 if has_filter else 200
# 支持大数据量默认返回全部数据可通过limit参数限制
max_items = int(q_limit) if q_limit else 50000
for key in ['mac_batch_audit_pdd', 'audit:pdd', 'pdd:audit']:
try:
if r.exists(key):
t = r.type(key)
if t == 'list':
# 限制最大查询数量
# 获取全部数据或限制数量
total = r.llen(key)
if has_filter:
items = r.lrange(key, max(0, total - max_items), -1)
else:
items = r.lrange(key, -200, -1)
fetch_count = min(total, max_items)
items = r.lrange(key, -fetch_count, -1)
elif t == 'zset':
items = r.zrevrange(key, 0, max_items - 1)
elif t == 'stream':
@ -1137,20 +1140,18 @@ def audit_yt():
r.connection_pool.connection_kwargs['socket_timeout'] = 5
items = []
# 限制最大返回数量,避免数据过大
max_items = 500 if has_filter else 200
# 支持大数据量默认返回全部数据可通过limit参数限制
max_items = int(q_limit) if q_limit else 50000
for key in ['mac_batch_audit_yt', 'audit:yt', 'yt:audit']:
try:
if r.exists(key):
t = r.type(key)
if t == 'list':
# 限制最大查询数量
# 获取全部数据或限制数量
total = r.llen(key)
if has_filter:
items = r.lrange(key, max(0, total - max_items), -1)
else:
items = r.lrange(key, -200, -1)
fetch_count = min(total, max_items)
items = r.lrange(key, -fetch_count, -1)
elif t == 'zset':
items = r.zrevrange(key, 0, max_items - 1)
elif t == 'stream':
@ -3243,6 +3244,59 @@ def shipments_redis_stats():
return jsonify({'error': f'获取统计失败:{str(e)}'}), 500
@app.get('/api/shipments/platform-stats')
@require_login
def shipments_platform_stats():
try:
r = get_redis()
redis_key = 'shipment_sn_mapping'
# 使用 HSCAN 避免一次性拉取全部数据
cursor = 0
total = 0
counts = {}
while True:
cursor, data = r.hscan(redis_key, cursor=cursor, count=1000)
if data:
for _sn, raw in data.items():
total += 1
platform = 'unknown'
try:
if isinstance(raw, bytes):
raw = raw.decode('utf-8', errors='ignore')
info = json.loads(raw)
platform = info.get('platform') or 'unknown'
except Exception:
platform = 'unknown'
counts[platform] = counts.get(platform, 0) + 1
if cursor == 0:
break
return jsonify({
'ok': True,
'total': total,
'by_platform': counts
})
except Exception as e:
log('shipments_platform_stats_error', str(e))
# Redis 失败时回退到 SQLite仅能返回总量无法区分platform
try:
conn = get_db()
c = conn.cursor()
c.execute('SELECT SUM(qty) AS total FROM shipments')
row = c.fetchone()
conn.close()
total = (row['total'] or 0) if row else 0
return jsonify({
'ok': True,
'total': total,
'by_platform': {}
})
except Exception as e2:
return jsonify({'error': f'获取统计失败:{str(e2)}'}), 500
@app.get('/api/shipments/summary')
@require_login
def shipments_summary():
@ -4755,14 +4809,41 @@ def export_reconciliations():
from io import BytesIO
from flask import send_file
# 获取日期范围参数
start_date = request.args.get('start_date', '').strip()
end_date = request.args.get('end_date', '').strip()
conn = get_db()
c = conn.cursor()
c.execute('''
# 构建查询语句
query = '''
SELECT order_date, contract_no, material_name, spec_model, transport_no,
quantity, unit, unit_price, total_amount, delivery_date, shipment_date
FROM reconciliations
ORDER BY id ASC
''')
'''
params = []
where_clauses = []
# 添加日期筛选条件
if start_date:
# 转换为斜杠格式进行比较
start_date_slash = start_date.replace('-', '/')
where_clauses.append('delivery_date >= ?')
params.append(start_date_slash)
if end_date:
# 转换为斜杠格式进行比较
end_date_slash = end_date.replace('-', '/')
where_clauses.append('delivery_date <= ?')
params.append(end_date_slash)
if where_clauses:
query += ' WHERE ' + ' AND '.join(where_clauses)
query += ' ORDER BY id ASC'
c.execute(query, params)
rows = c.fetchall()
conn.close()
@ -4834,11 +4915,20 @@ def export_reconciliations():
output.seek(0)
# 生成文件名(包含当前日期
# 生成文件名(包含当前日期和筛选范围
from datetime import datetime
filename = f'对账单_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx'
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if start_date and end_date:
filename = f'对账单_{start_date}{end_date}_{timestamp}.xlsx'
elif start_date:
filename = f'对账单_{start_date}起_{timestamp}.xlsx'
elif end_date:
filename = f'对账单_至{end_date}_{timestamp}.xlsx'
else:
filename = f'对账单_{timestamp}.xlsx'
log('export_reconciliations', f'导出对账单,共 {len(rows)} 条记录')
date_info = f',日期范围: {start_date or "不限"}{end_date or "不限"}' if (start_date or end_date) else ''
log('export_reconciliations', f'导出对账单,共 {len(rows)} 条记录{date_info}')
return send_file(
output,
@ -5987,11 +6077,12 @@ def get_operations_log():
c = conn.cursor()
# 构建查询 - 按操作详情搜索
where_clause = ''
params = []
where_parts = ["o.action != ?"]
params = ['dashboard_platform_debug']
if keyword:
where_clause = 'WHERE o.detail LIKE ?'
where_parts.append('o.detail LIKE ?')
params.append(f'%{keyword}%')
where_clause = 'WHERE ' + ' AND '.join(where_parts)
# 获取总数
c.execute(f'SELECT COUNT(*) FROM operations_log o {where_clause}', params)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

1
汇总统计.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765851714875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3726" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1024 514.1c0 275.8-223.6 499.3-499.3 499.3-275.8 0-499.3-223.5-499.3-499.3S248.9 14.8 524.7 14.8c275.7 0 499.3 223.5 499.3 499.3z" fill="#dbdbdb" p-id="3727" data-spm-anchor-id="a313x.search_index.0.i0.1a5e3a819i9fxq" class="selected"></path><path d="M418.5 674.5l-71.3 338.9h-71.4l71.4-338.9zM631.7 674.5l71.3 338.9h71.3L703 674.5zM382 86.1h285.3v53.5H382z" fill="#404040" p-id="3728"></path><path d="M96.7 157.4h856v588.5h-856z" fill="#FFFFFF" p-id="3729"></path><path d="M577.3 387.5l51.8-72.6c-29.2-20.9-65-33.2-103.6-33.2v89.2c19.3-0.1 37.2 6.1 51.8 16.6z" fill="#FBD764" p-id="3730"></path><path d="M614.7 460c0 16.9-4.8 32.7-13 46.2l76.4 46.3c16.4-27 25.8-58.6 25.8-92.4 0-59.8-29.5-112.7-74.7-145.1l-51.8 72.6c22.5 16.1 37.3 42.5 37.3 72.4z" fill="#828282" p-id="3731"></path><path d="M601.7 506.1c-15.6 25.7-43.8 43-76.2 43-49.2 0-89.2-39.9-89.2-89.2 0-49.2 39.9-89.2 89.2-89.2v-89.2c-98.5 0-178.3 79.8-178.3 178.3S427 638.3 525.5 638.3c64.6 0 121.3-34.4 152.5-85.9l-76.3-46.3zM311.5 242.1h285.3v8.9H311.5zM311.5 224.3h214v8.9h-214z" fill="#404040" p-id="3732"></path><path d="M311.5 674.5h35.7v35.7h-35.7z" fill="#FBD764" p-id="3733"></path><path d="M436.3 674.5H472v35.7h-35.7z" fill="#828282" p-id="3734"></path><path d="M561.6 674.9h34.9v34.9h-34.9zM614.7 687.9h124.8v8.9H614.7zM489.8 687.9h53.5v8.9h-53.5z" fill="#404040" p-id="3735"></path><path d="M365 687.9h53.5v8.9H365z" fill="#404040" p-id="3736"></path><path d="M311.5 184.2h428.8V202H311.5z" fill="#404040" p-id="3737"></path><path d="M61.1 745.9h927.3v35.7H61.1z" fill="#545454" p-id="3738"></path><path d="M61.1 121.7h927.3v35.7H61.1z" fill="#545454" p-id="3739"></path><path d="M346.4 864H703v17.8H346.4z" fill="#404040" p-id="3740"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB