小问题修复
@ -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}
|
||||
|
||||
1
frontend/assets/汇总统计.svg
Normal 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 |
@ -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>
|
||||
|
||||
@ -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()">×</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
|
||||
};
|
||||
})();
|
||||
|
||||
149
server/app.py
@ -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)
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 680 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 225 KiB |
1
汇总统计.svg
Normal 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 |