chore: rename procurement management system
This commit is contained in:
parent
9e77a7cdc0
commit
c88182bcd8
662
templates/base.html
Normal file
662
templates/base.html
Normal file
@ -0,0 +1,662 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>蝶雅采购管理系统</title>
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<meta name="theme-color" content="#1677ff">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="蝶雅数据">
|
||||
<link rel="icon" type="image/png" href="/static/logo-small.png">
|
||||
<link rel="apple-touch-icon" href="/static/logo-small.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1785481700">
|
||||
<script src="/static/js/chart.umd.min.js"></script>
|
||||
<style>
|
||||
.nav-hidden { display: none !important; }
|
||||
/* 侧边栏折叠时,子菜单弹出面板 */
|
||||
.sidebar.collapsed .nav-group {
|
||||
position: relative;
|
||||
}
|
||||
.sidebar-popup-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 60px;
|
||||
min-width: 180px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
z-index: 99999;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.sidebar-popup-menu.open {
|
||||
display: block;
|
||||
}
|
||||
.sidebar-popup-menu a {
|
||||
display: block;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sidebar-popup-menu a:hover {
|
||||
background: #f5f5f5;
|
||||
color: #1677ff;
|
||||
}
|
||||
/* BOM树形表格 */
|
||||
#bomTable .bom-product-row td { border-bottom: 1px solid var(--border); }
|
||||
#bomTable .bom-material-row td { border-bottom: 1px dashed #e0e0e0; font-size: 13px; }
|
||||
#bomTable .bom-material-row:last-child td { border-bottom: 1px solid var(--border); }
|
||||
#bomTable .bom-product-row:hover { background: #e8f0fe !important; }
|
||||
#bomTable .bom-material-row:hover { background: #f0f4f8 !important; }
|
||||
|
||||
.filter-wrap .filter-clear{position:absolute;right:5px;top:50%;transform:translateY(-50%);cursor:pointer;color:#bfbfbf;font-size:13px;display:none;z-index:2;padding:1px 4px;border-radius:3px;line-height:1;background:var(--bg,#fff)}
|
||||
.filter-wrap.has-value:hover .filter-clear{display:block}
|
||||
.filter-clear:hover{color:#cf1322;background:#eee}
|
||||
|
||||
/* 产品详情明细弹窗 - 美化样式 */
|
||||
.detail-two-col{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}
|
||||
.detail-col{display:flex;flex-direction:column;gap:6px}
|
||||
.detail-pair{display:flex;border:1px solid #d0d0d0;border-radius:5px;overflow:hidden;min-height:36px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.05)}
|
||||
.detail-lbl{width:115px;min-width:115px;padding:6px 10px;background:#b0b0b0;color:#333;font-size:12px;font-weight:600;white-space:nowrap;display:flex;align-items:center;justify-content:flex-end;letter-spacing:0.3px;border-right:1px solid #999}
|
||||
.detail-val-wrap{flex:1;padding:4px 10px;display:flex;align-items:center}
|
||||
.detail-val{font-size:13px;color:#333;word-break:break-all;line-height:1.5}
|
||||
.detail-input{width:100%;box-sizing:border-box;font-size:13px;border:1px solid #ccc;border-radius:3px;padding:5px 8px;color:#333;background:#fff;min-width:0;transition:border-color .2s,box-shadow .2s}
|
||||
.detail-input:hover{border-color:#999}
|
||||
.detail-input:focus{border-color:#1890ff;outline:none;box-shadow:0 0 0 2px rgba(24,144,255,0.15)}
|
||||
.detail-select{appearance:auto;-webkit-appearance:auto;padding:5px 6px!important;cursor:pointer;background:#fff!important}
|
||||
.detail-textarea:focus{border-color:#1890ff!important;outline:none;box-shadow:0 0 0 2px rgba(24,144,255,0.15)}
|
||||
.orderer-select{background:#fff;cursor:pointer;min-height:28px}
|
||||
.orderer-select option{padding:3px 6px}
|
||||
.ac{width:auto;min-width:0}
|
||||
.col-resize-handle{cursor:col-resize;transition:background .15s}
|
||||
.col-resize-handle:hover{background:rgba(102,126,234,0.3)!important}
|
||||
|
||||
.filter-wrap.has-value::after{content:"▾";position:absolute;right:5px;top:50%;transform:translateY(-50%);font-size:11px;color:#999;pointer-events:none;transition:opacity .15s}
|
||||
.filter-wrap.has-value:hover::after{opacity:0}
|
||||
|
||||
.pd-fields-2col{display:grid;grid-template-columns:1fr 1fr;gap:0}
|
||||
.pd-fields-3col{display:grid;grid-template-columns:1fr 1fr 1fr;gap:0}
|
||||
.pd-fields-4col{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:0}
|
||||
.pd-row-2col{border-bottom:1px solid #f0f0f0}
|
||||
.pd-row-3col{border-bottom:1px solid #f0f0f0}
|
||||
.pd-row-4col{border-bottom:1px solid #f0f0f0}
|
||||
|
||||
.filter-wrap{position:relative;display:inline-block;vertical-align:middle}
|
||||
.filter-wrap select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:22px!important}
|
||||
.filter-wrap::after{content:"\25be";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-size:11px;color:#999;pointer-events:none;transition:opacity .15s}
|
||||
.filter-wrap.has-value:hover::after{opacity:0}
|
||||
.filter-wrap .filter-clear{position:absolute;right:4px;top:50%;transform:translateY(-50%);cursor:pointer;color:#999;font-size:14px;display:none;z-index:3;line-height:1;padding:0 3px}
|
||||
.filter-wrap.has-value:hover .filter-clear{display:block}
|
||||
.filter-clear:hover{color:#cf1322}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<img src="/static/logo-small.png" alt="Logo" style="width:32px;height:32px;border-radius:6px;">
|
||||
</div>
|
||||
<span class="sidebar-title">采购管理系统</span>
|
||||
<button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()" aria-label="切换菜单">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
{# 所有用户可见 #}
|
||||
<a href="/dashboard" class="nav-item {% if page == 'dashboard' %}active{% endif %}" data-page="dashboard" data-roles="all">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">数据概览</span>
|
||||
</a>
|
||||
{# 采销协同(含子菜单): 所有用户可见 #}
|
||||
<div class="nav-group" data-roles="all">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="collaboration" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">采销协同</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-collaboration">
|
||||
<a href="/collaboration/inventory-dashboard" class="nav-sub-item {% if page == 'collaboration_inventory_dashboard' %}active{% endif %}" data-page="collaboration_inventory_dashboard" data-roles="all"><span class="nav-sub-icon">●</span><span>库存数据看板</span></a>
|
||||
<a href="/collaboration/key-sku-forecast" class="nav-sub-item {% if page == 'collaboration_key_sku_forecast' %}active{% endif %}" data-page="collaboration_key_sku_forecast" data-roles="all"><span class="nav-sub-icon">●</span><span>重点SKU预测</span></a>
|
||||
<a href="/collaboration/sku-health-index" class="nav-sub-item {% if page == 'collaboration_sku_health_index' %}active{% endif %}" data-page="collaboration_sku_health_index" data-roles="all"><span class="nav-sub-icon">●</span><span>SKU库存健康指数</span></a>
|
||||
<a href="/collaboration/slow-moving-sku" class="nav-sub-item {% if page == 'collaboration_slow_moving_sku' %}active{% endif %}" data-page="collaboration_slow_moving_sku" data-roles="all"><span class="nav-sub-icon">●</span><span>呆滞SKU清单</span></a>
|
||||
<a href="/collaboration/sales-management" class="nav-sub-item {% if page == 'collaboration_sales_management' %}active{% endif %}" data-page="collaboration_sales_management" data-roles="all"><span class="nav-sub-icon">●</span><span>动销管理</span></a>
|
||||
</div>
|
||||
</div>
|
||||
{# 基础数据(含子菜单): 所有非管理员也可查看 #}
|
||||
<div class="nav-group" data-roles="admin,super_admin,procurement_planner,procurement_follower,product_developer,operator,viewer,查看者">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="basic" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">基础数据</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-basic">
|
||||
<a href="/products" class="nav-sub-item {% if page == 'products' %}active{% endif %}" data-page="products">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>产品信息管理</span>
|
||||
</a>
|
||||
<a href="/brand-management" class="nav-sub-item {% if page == 'brand_management' %}active{% endif %}" data-page="brand_management">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>品牌信息管理</span>
|
||||
</a>
|
||||
<a href="/bom" class="nav-sub-item {% if page == 'bom' %}active{% endif %}" data-page="bom">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>物料清单管理</span>
|
||||
</a>
|
||||
{# 仓库管理: 所有非管理员也可查看 #}
|
||||
<a href="/warehouse" class="nav-sub-item {% if page == 'warehouse' %}active{% endif %}" data-page="warehouse" data-roles="admin,super_admin,viewer,查看者">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>仓库信息管理</span>
|
||||
</a>
|
||||
{# 供应商管理: 所有非管理员也可查看 #}
|
||||
<a href="/suppliers" class="nav-sub-item {% if page == 'suppliers' %}active{% endif %}" data-page="suppliers" data-roles="admin,super_admin,procurement_planner,procurement_follower,viewer,查看者">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>供应商管理</span>
|
||||
</a>
|
||||
{# 人员信息管理: 所有非管理员也可查看 #}
|
||||
<a href="/personnel" class="nav-sub-item {% if page == 'personnel' %}active{% endif %}" data-page="personnel" data-roles="admin,super_admin,procurement_planner,procurement_follower,viewer,查看者">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>人员信息管理</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{# 库存与出库(含子菜单): 所有人 #}
|
||||
<div class="nav-group" data-roles="all">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="inventory" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">库存与出库</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-inventory">
|
||||
<a href="/inventory/finished" class="nav-sub-item {% if page == 'inventory_finished' %}active{% endif %}" data-page="inventory_finished">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>成品库存</span>
|
||||
</a>
|
||||
<a href="/inventory/packaging" class="nav-sub-item {% if page == 'inventory_packaging' %}active{% endif %}" data-page="inventory_packaging">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>包材库存</span>
|
||||
</a>
|
||||
<a href="/data/summary" class="nav-sub-item {% if page == 'data_summary' %}active{% endif %}" data-page="data_summary">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>出库数据</span>
|
||||
</a>
|
||||
<a href="/data/detail" class="nav-sub-item {% if page == 'data_detail' %}active{% endif %}" data-page="data_detail">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>出库明细</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{# 计划预测: 所有非管理员也可查看 #}
|
||||
<a href="/planning" data-roles="admin,super_admin,procurement_planner,procurement_follower,product_developer,operator,viewer,查看者" class="nav-item {% if page == 'planning' %}active{% endif %}" data-page="planning">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">计划预测</span>
|
||||
</a>
|
||||
{# 现货模式(含子菜单): 所有非管理员也可查看 #}
|
||||
<div class="nav-group" data-roles="admin,super_admin,procurement_planner,procurement_follower,product_developer,operator,viewer,查看者">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="spot" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">现货模式</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-spot">
|
||||
<a href="/spot/dashboard" class="nav-sub-item {% if page == 'spot_dashboard' %}active{% endif %}" data-page="spot_dashboard">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>现货看板</span>
|
||||
</a>
|
||||
<a href="/spot/application" class="nav-sub-item {% if page == 'spot_application' %}active{% endif %}" data-page="spot_application">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>现货申请</span>
|
||||
</a>
|
||||
<a href="/spot/confirmation" class="nav-sub-item {% if page == 'spot_confirmation' %}active{% endif %}" data-page="spot_confirmation">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>现货确认</span>
|
||||
</a>
|
||||
<a href="/spot/sales" class="nav-sub-item {% if page == 'spot_sales' %}active{% endif %}" data-page="spot_sales">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>销量管理</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{# 采购订单(含子菜单): 所有非管理员也可查看 #}
|
||||
<div class="nav-group" data-roles="admin,super_admin,procurement_planner,procurement_follower,viewer,查看者">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="purchase" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">订单管理</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-purchase">
|
||||
<a href="/purchase" class="nav-sub-item {% if page == 'purchase' %}active{% endif %}" data-page="purchase">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>待下单管理</span>
|
||||
</a>
|
||||
<a href="/orders/domestic" class="nav-sub-item {% if page == 'orders_domestic' %}active{% endif %}" data-page="orders_domestic">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>国内订单管理</span>
|
||||
</a>
|
||||
<a href="/orders/overseas" class="nav-sub-item {% if page == 'orders_overseas' %}active{% endif %}" data-page="orders_overseas">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>海外订单管理</span>
|
||||
</a>
|
||||
<a href="/orders/packaging" class="nav-sub-item {% if page == 'orders_packaging' %}active{% endif %}" data-page="orders_packaging">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>包材订单管理</span>
|
||||
</a>
|
||||
<a href="/orders/finished" class="nav-sub-item {% if page == 'orders_finished' %}active{% endif %}" data-page="orders_finished">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>成品提货管理</span>
|
||||
</a>
|
||||
<a href="/purchase/ready" class="nav-sub-item {% if page == 'purchase_ready' %}active{% endif %}" data-page="purchase_ready">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>下单备份管理</span>
|
||||
</a>
|
||||
{# 合同模板: 所有用户可见 #}
|
||||
<a href="/contract-templates" class="nav-sub-item {% if page == 'contract_templates' %}active{% endif %}" data-page="contract_templates">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>合同模板管理</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{# 产品下单(含子菜单): 所有用户可见 #}
|
||||
<div class="nav-group" data-roles="all">
|
||||
<a href="#" class="nav-item nav-item-accent nav-group-header" data-group="product-order" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">新品下单</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-product-order">
|
||||
<a href="/product-new" class="nav-sub-item {% if page == 'product_new' %}active{% endif %}" data-page="product_new" data-roles="all">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>新品成品管理</span>
|
||||
</a>
|
||||
<a href="/product-packaging" class="nav-sub-item {% if page == 'product_packaging' %}active{% endif %}" data-page="product_packaging" data-roles="all">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>新品包材管理</span>
|
||||
</a>
|
||||
<a href="/product-category" class="nav-sub-item {% if page == 'product_category' %}active{% endif %}" data-page="product_category" data-roles="all">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>类目管理</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{# 知识库: 所有用户可见 #}
|
||||
<a href="/knowledge" class="nav-item {% if page == 'knowledge' %}active{% endif %}" data-page="knowledge" data-roles="all">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><path d="M12 6v6l2-1 2 1V6"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">知识库</span>
|
||||
</a>
|
||||
{# 测试问题: 所有用户可见 #}
|
||||
<a href="/feedback" class="nav-item {% if page == 'feedback' %}active{% endif %}" data-page="feedback" data-roles="all">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">测试问题</span>
|
||||
</a>
|
||||
{# AI分析: 所有用户可见 #}
|
||||
<a href="/ai-analysis" class="nav-item {% if page == 'ai_analysis' %}active{% endif %}" data-page="ai_analysis" data-roles="all">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">AI分析</span>
|
||||
</a>
|
||||
{# 系统设置(含子菜单): 仅管理员 #}
|
||||
<div class="nav-group" data-roles="admin,super_admin">
|
||||
<a href="#" class="nav-item nav-group-header" data-group="sysconfig" onclick="toggleSubmenu(event)">
|
||||
<span class="nav-icon nav-icon-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-text">系统设置</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</a>
|
||||
<div class="nav-submenu" id="submenu-sysconfig">
|
||||
<a href="/users" class="nav-sub-item {% if page == 'users' %}active{% endif %}" data-page="users">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>用户管理</span>
|
||||
</a>
|
||||
<a href="/permissions" class="nav-sub-item {% if page == 'permissions' %}active{% endif %}" data-page="permissions">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>权限配置</span>
|
||||
</a>
|
||||
<a href="/config" class="nav-sub-item {% if page == 'config' %}active{% endif %}" data-page="config">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>参数配置</span>
|
||||
</a>
|
||||
<a href="/logs" class="nav-sub-item {% if page == 'logs' %}active{% endif %}" data-page="logs">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>操作日志</span>
|
||||
</a>
|
||||
<a href="/login-records" class="nav-sub-item {% if page == 'login_records' %}active{% endif %}" data-page="login_records">
|
||||
<span class="nav-sub-icon">●</span>
|
||||
<span>访问记录</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
<!-- 底部用户信息 -->
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-avatar">
|
||||
<img src="/static/logo-small.png" alt="avatar" style="width:32px;height:32px;border-radius:50%">
|
||||
</div>
|
||||
<div class="sidebar-user-info" style="flex:1;min-width:0">
|
||||
<div style="font-size:13px;font-weight:600;color:rgba(255,255,255,0.85);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ session.get('real_name', session.get('username', '用户')) }}</div>
|
||||
<div style="font-size:11px;color:rgba(255,255,255,0.45)">
|
||||
{% set role = session.get('role', '') %}
|
||||
{% set role_names = {'admin':'管理员','procurement_planner':'采购计划','procurement_follower':'采购跟单','product_developer':'产品开发','operator':'运营专员'} %}
|
||||
{{ role_names.get(role, '') }}
|
||||
{% set level = session.get('user_level', '') %}
|
||||
{% set level_names = {'employee':'','leader':'·组长','director':'·主管'} %}
|
||||
{{ level_names.get(level, '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="handleLogout()" title="退出登录" style="cursor:pointer;color:rgba(255,255,255,0.5);padding:4px;border-radius:4px;display:flex;align-items:center" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,0.5)'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content" id="mainContent">
|
||||
<div class="page-loading" id="pageLoading">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
window._currentRole = '{{ session.get('role', '') }}';
|
||||
window._currentUserLevel = '{{ session.get('user_level', 'employee') }}';
|
||||
window._currentUser = {
|
||||
user_id: {{ session.get('user_id', 0) }},
|
||||
username: '{{ session.get('username', '') }}',
|
||||
role: '{{ session.get('role', '') }}',
|
||||
permissions: {{ _user_permissions_json|safe }}
|
||||
};
|
||||
</script>
|
||||
<script src="/static/js/marked.min.js"></script>
|
||||
<script src="/static/js/dompurify.min.js"></script>
|
||||
<script src="/static/js/app.js?v={{ app_js_version }}"></script>
|
||||
<script src="/static/js/feedback.js?v=1786347000"></script>
|
||||
<script src="/static/js/knowledge-actions.js?v=1"></script>
|
||||
<script>
|
||||
// 根据角色隐藏无权限的菜单
|
||||
(function() {
|
||||
var role = window._currentRole || '';
|
||||
document.querySelectorAll('[data-roles]').forEach(function(el) {
|
||||
var roles = el.getAttribute('data-roles');
|
||||
if (roles === 'all') return;
|
||||
var allowed = roles.split(',').map(function(r) { return r.trim(); });
|
||||
if (role === 'super_admin') return; // 超级管理员全可见
|
||||
if (allowed.indexOf(role) === -1) {
|
||||
el.classList.add('nav-hidden');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
function handleLogout() {
|
||||
fetch('/api/logout', {method: 'POST'})
|
||||
.then(function() { window.location.href = '/login'; });
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
var overlay = document.getElementById('sidebarOverlay');
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
overlay.style.display = 'none';
|
||||
// 侧边栏缩进后,给图标添加title提示
|
||||
sidebar.querySelectorAll('.nav-item').forEach(function(item) {
|
||||
var textEl = item.querySelector('.nav-text');
|
||||
if (textEl && textEl.textContent.trim()) {
|
||||
item.setAttribute('title', textEl.textContent.trim());
|
||||
}
|
||||
});
|
||||
sidebar.querySelectorAll('.nav-sub-item').forEach(function(item) {
|
||||
var textEl = item.querySelector('span:not(.nav-sub-icon)');
|
||||
if (textEl && textEl.textContent.trim()) {
|
||||
item.setAttribute('title', textEl.textContent.trim());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 展开后移除title
|
||||
sidebar.querySelectorAll('.nav-item, .nav-sub-item').forEach(function(item) {
|
||||
item.removeAttribute('title');
|
||||
});
|
||||
if (window.innerWidth <= 768) {
|
||||
overlay.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
function toggleSubmenu(event) {
|
||||
event.preventDefault();
|
||||
var header = event.currentTarget;
|
||||
var group = header.dataset.group;
|
||||
var submenu = document.getElementById('submenu-' + group);
|
||||
var arrow = header.querySelector('.nav-arrow');
|
||||
if (!submenu) return;
|
||||
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar && sidebar.classList.contains('collapsed')) {
|
||||
// 折叠状态:直接打开独立的弹出面板
|
||||
var popup = document.querySelector('.sidebar-popup-menu');
|
||||
if (popup && popup.dataset.group === group) {
|
||||
popup.remove();
|
||||
return;
|
||||
}
|
||||
if (popup) popup.remove();
|
||||
|
||||
// 收集子菜单链接
|
||||
var itemsHtml = '';
|
||||
submenu.querySelectorAll('a').forEach(function(a) {
|
||||
var href = a.getAttribute('href') || '#';
|
||||
var text = a.textContent.trim();
|
||||
var isActive = window.location.pathname === href;
|
||||
itemsHtml += '<a href="' + href + '" style="display:block;padding:10px 16px;font-size:14px;text-decoration:none;' + (isActive ? 'color:#1677ff;font-weight:600;background:#e6f7ff' : 'color:#333') + '">' + text + '</a>';
|
||||
});
|
||||
|
||||
popup = document.createElement('div');
|
||||
popup.className = 'sidebar-popup-menu';
|
||||
popup.dataset.group = group;
|
||||
popup.innerHTML = itemsHtml;
|
||||
var rect = header.getBoundingClientRect();
|
||||
popup.style.top = (rect.top - 4) + 'px';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// 延迟后再添加 open 类确保过渡效果
|
||||
setTimeout(function() { popup.classList.add('open'); }, 10);
|
||||
|
||||
// SPA导航
|
||||
popup.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('a');
|
||||
if (link) {
|
||||
e.preventDefault();
|
||||
popup.remove();
|
||||
var href = link.getAttribute('href');
|
||||
if (href && typeof navigateTo === 'function') navigateTo(href);
|
||||
else window.location.href = href;
|
||||
}
|
||||
});
|
||||
// 点击外部关闭
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', function popupOutside(e) {
|
||||
if (!popup.contains(e.target) && e.target !== header && !header.contains(e.target)) {
|
||||
if (popup.parentNode) popup.remove();
|
||||
document.removeEventListener('click', popupOutside);
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
} else {
|
||||
submenu.classList.toggle('open');
|
||||
if (arrow) arrow.classList.toggle('rotated');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查侧边栏状态
|
||||
function updateSidebarTitles() {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
sidebar.querySelectorAll('.nav-item').forEach(function(item) {
|
||||
var textEl = item.querySelector('.nav-text');
|
||||
if (textEl && textEl.textContent.trim()) {
|
||||
item.setAttribute('title', textEl.textContent.trim());
|
||||
}
|
||||
});
|
||||
sidebar.querySelectorAll('.nav-sub-item').forEach(function(item) {
|
||||
var textEl = item.querySelector('span:not(.nav-sub-icon)');
|
||||
if (textEl && textEl.textContent.trim()) {
|
||||
item.setAttribute('title', textEl.textContent.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSidebarTitles();
|
||||
var page = '{{ page }}';
|
||||
// 自动展开当前页面对应的子菜单
|
||||
var activeSubItem = document.querySelector('.nav-sub-item.active');
|
||||
if (activeSubItem) {
|
||||
var submenu = activeSubItem.closest('.nav-submenu');
|
||||
if (submenu) {
|
||||
submenu.classList.add('open');
|
||||
var arrow = submenu.closest('.nav-group') ? submenu.closest('.nav-group').querySelector('.nav-arrow') : null;
|
||||
if (arrow) arrow.classList.add('rotated');
|
||||
}
|
||||
}
|
||||
// 展开基础数据子菜单
|
||||
var basicActive = document.querySelector('#submenu-basic .nav-sub-item.active');
|
||||
if (basicActive) {
|
||||
var bMenu = document.getElementById('submenu-basic');
|
||||
if (bMenu) bMenu.classList.add('open');
|
||||
var bArrow = document.querySelector('.nav-group-header[data-group="basic"] .nav-arrow');
|
||||
if (bArrow) bArrow.classList.add('rotated');
|
||||
}
|
||||
// 展开库存与出库子菜单
|
||||
var invActive = document.querySelector('#submenu-inventory .nav-sub-item.active');
|
||||
if (invActive) {
|
||||
var iMenu = document.getElementById('submenu-inventory');
|
||||
if (iMenu) iMenu.classList.add('open');
|
||||
var iArrow = document.querySelector('.nav-group-header[data-group="inventory"] .nav-arrow');
|
||||
if (iArrow) iArrow.classList.add('rotated');
|
||||
}
|
||||
// 展开采购订单菜单
|
||||
var purchaseActive = document.querySelector('#submenu-purchase .nav-sub-item.active');
|
||||
if (purchaseActive) {
|
||||
var pMenu = document.getElementById('submenu-purchase');
|
||||
if (pMenu) pMenu.classList.add('open');
|
||||
var pArrow = document.querySelector('.nav-group-header[data-group="purchase"] .nav-arrow');
|
||||
if (pArrow) pArrow.classList.add('rotated');
|
||||
}
|
||||
// 如果当前在高权限页面,展开系统设置菜单
|
||||
var sysConfigActive = document.querySelector('#submenu-sysconfig .nav-sub-item.active');
|
||||
if (sysConfigActive) {
|
||||
var sysMenu = document.getElementById('submenu-sysconfig');
|
||||
if (sysMenu) sysMenu.classList.add('open');
|
||||
var sysArrow = document.querySelector('.nav-group-header[data-group="sysconfig"] .nav-arrow');
|
||||
if (sysArrow) sysArrow.classList.add('rotated');
|
||||
}
|
||||
// 权限检查:如果初始页面无查看权限,跳转到第一个可访问的页面
|
||||
var startPage = page;
|
||||
if (page !== 'login' && window._currentUser && window._currentUser.role !== 'super_admin') {
|
||||
var perms = window._currentUser.permissions || {};
|
||||
var pagePerms = perms[page] || (page === 'product_packaging' ? (perms.product_new || {}) : {});
|
||||
if (pagePerms['view'] === 0 || (page === 'contract_templates' && pagePerms['view'] !== 1)) {
|
||||
// 找第一个有权限的页面
|
||||
var accessiblePages = ['planning','purchase','products','bom','warehouse','suppliers','knowledge','feedback','users','permissions','logs','config','login_records','personnel','contract_templates','purchase_ready','orders_domestic','orders_overseas','orders_packaging','orders_finished','inventory_finished','inventory_packaging','data_summary','data_detail','product_new','product_packaging','product_category','brand_management'];
|
||||
for (var pi = 0; pi < accessiblePages.length; pi++) {
|
||||
var ap = accessiblePages[pi];
|
||||
var apPerms = perms[ap] || {};
|
||||
if (apPerms['view'] !== 0) {
|
||||
startPage = ap;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
window._currentPage = startPage;
|
||||
loadPage(startPage);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
// 先清除所有旧 SW 缓存
|
||||
if (caches) {
|
||||
caches.keys().then(function(keys) {
|
||||
keys.forEach(function(k) { if (k.startsWith('dy-dashboard-')) caches.delete(k); });
|
||||
});
|
||||
}
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/static/sw.js?v=21').then(function(reg) {
|
||||
reg.update();
|
||||
if (reg.installing) {
|
||||
reg.installing.addEventListener('statechange', function() {
|
||||
if (this.state === 'activated') location.reload();
|
||||
});
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.log('SW failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<div style="position:fixed;bottom:4px;right:8px;font-size:10px;color:#ccc;z-index:99999">v202607311130</div>
|
||||
</body>
|
||||
</html>
|
||||
202
templates/login.html
Normal file
202
templates/login.html
Normal file
@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>蝶雅采购管理系统 - 登录</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=317">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="login-logo">
|
||||
<img src="/static/logo-small.png" alt="Logo" style="width:64px;height:64px;border-radius:12px;">
|
||||
</div>
|
||||
<h1>蝶雅采购管理系统</h1>
|
||||
<p class="login-subtitle">采购智能管理平台</p>
|
||||
</div>
|
||||
<form id="loginForm" class="login-form" onsubmit="return false;">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="username" class="form-input" placeholder="请输入用户名" autocomplete="off" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<div class="password-input-wrap">
|
||||
<input type="password" id="password" class="form-input" placeholder="请输入密码" autocomplete="off">
|
||||
<button type="button" class="password-toggle" onclick="togglePassword('password', this)" aria-label="显示密码" title="显示密码">
|
||||
<svg class="eye-open" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<svg class="eye-closed" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 3 18 18M10.6 10.7a2 2 0 0 0 2.7 2.7M9.9 5.2A11.7 11.7 0 0 1 12 5c6.5 0 10 7 10 7a18.8 18.8 0 0 1-2.1 3M6.6 6.7C3.7 8.5 2 12 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.1-.8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-error" id="loginError"></div>
|
||||
<button type="submit" class="btn btn-primary login-btn" onclick="handleLogin()">
|
||||
<span class="btn-text">登 录</span>
|
||||
<span class="btn-loading" style="display:none">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" stroke-dasharray="31.4 31.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<form id="changePasswordForm" class="login-form" onsubmit="return false;" style="display:none">
|
||||
<p class="password-notice">首次登录或密码重置后,必须先设置新密码。</p>
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<div class="password-input-wrap">
|
||||
<input type="password" id="newPassword" class="form-input" placeholder="至少10位,包含字母、数字和特殊字符" autocomplete="off">
|
||||
<button type="button" class="password-toggle" onclick="togglePassword('newPassword', this)" aria-label="显示密码" title="显示密码">
|
||||
<svg class="eye-open" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<svg class="eye-closed" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 3 18 18M10.6 10.7a2 2 0 0 0 2.7 2.7M9.9 5.2A11.7 11.7 0 0 1 12 5c6.5 0 10 7 10 7a18.8 18.8 0 0 1-2.1 3M6.6 6.7C3.7 8.5 2 12 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.1-.8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码</label>
|
||||
<div class="password-input-wrap">
|
||||
<input type="password" id="confirmPassword" class="form-input" placeholder="请再次输入新密码" autocomplete="off">
|
||||
<button type="button" class="password-toggle" onclick="togglePassword('confirmPassword', this)" aria-label="显示密码" title="显示密码">
|
||||
<svg class="eye-open" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<svg class="eye-closed" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 3 18 18M10.6 10.7a2 2 0 0 0 2.7 2.7M9.9 5.2A11.7 11.7 0 0 1 12 5c6.5 0 10 7 10 7a18.8 18.8 0 0 1-2.1 3M6.6 6.7C3.7 8.5 2 12 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.1-.8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-error" id="changePasswordError"></div>
|
||||
<button type="submit" class="btn btn-primary login-btn" onclick="handleChangePassword()">
|
||||
<span class="btn-text">确认修改</span>
|
||||
<span class="btn-loading" style="display:none">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" stroke-dasharray="31.4 31.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="passwordPrompt" class="password-prompt-overlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="passwordPromptTitle">
|
||||
<div class="password-prompt-card">
|
||||
<div class="password-prompt-icon">!</div>
|
||||
<h2 id="passwordPromptTitle">需要修改密码</h2>
|
||||
<p>为了账号安全,本次登录需要先设置新密码。修改完成后才能正常访问系统。</p>
|
||||
<button type="button" class="btn btn-primary password-prompt-btn" onclick="enterChangePassword()">我知道了,去修改</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function handleLogin() {
|
||||
var btn = document.querySelector('.login-btn');
|
||||
var text = btn.querySelector('.btn-text');
|
||||
var loading = btn.querySelector('.btn-loading');
|
||||
var error = document.getElementById('loginError');
|
||||
|
||||
btn.disabled = true;
|
||||
text.style.display = 'none';
|
||||
loading.style.display = 'inline';
|
||||
error.textContent = '';
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.code === 0) {
|
||||
if (data.data && data.data.must_change_password) {
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('passwordPrompt').style.display = 'flex';
|
||||
document.querySelector('.password-prompt-btn').focus();
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} else {
|
||||
error.textContent = data.msg;
|
||||
btn.disabled = false;
|
||||
text.style.display = 'inline';
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
error.textContent = '网络错误,请重试';
|
||||
btn.disabled = false;
|
||||
text.style.display = 'inline';
|
||||
loading.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function enterChangePassword() {
|
||||
document.getElementById('passwordPrompt').style.display = 'none';
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
document.getElementById('changePasswordForm').style.display = 'block';
|
||||
document.querySelector('.login-subtitle').textContent = '请设置新的登录密码';
|
||||
document.getElementById('newPassword').focus();
|
||||
}
|
||||
|
||||
function togglePassword(inputId, button) {
|
||||
var input = document.getElementById(inputId);
|
||||
var show = input.type === 'password';
|
||||
input.type = show ? 'text' : 'password';
|
||||
button.classList.toggle('is-visible', show);
|
||||
button.setAttribute('aria-label', show ? '隐藏密码' : '显示密码');
|
||||
button.setAttribute('title', show ? '隐藏密码' : '显示密码');
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function handleChangePassword() {
|
||||
var form = document.getElementById('changePasswordForm');
|
||||
var btn = form.querySelector('.login-btn');
|
||||
var text = btn.querySelector('.btn-text');
|
||||
var loading = btn.querySelector('.btn-loading');
|
||||
var error = document.getElementById('changePasswordError');
|
||||
var newPassword = document.getElementById('newPassword').value;
|
||||
var confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
error.textContent = '';
|
||||
if (newPassword !== confirmPassword) {
|
||||
error.textContent = '两次输入的密码不一致';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
text.style.display = 'none';
|
||||
loading.style.display = 'inline';
|
||||
|
||||
fetch('/api/change-password', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
new_password: newPassword,
|
||||
confirm_password: confirmPassword
|
||||
})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.code === 0) {
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
error.textContent = data.msg;
|
||||
btn.disabled = false;
|
||||
text.style.display = 'inline';
|
||||
loading.style.display = 'none';
|
||||
})
|
||||
.catch(function() {
|
||||
error.textContent = '网络错误,请重试';
|
||||
btn.disabled = false;
|
||||
text.style.display = 'inline';
|
||||
loading.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loginForm').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleLogin();
|
||||
});
|
||||
document.getElementById('changePasswordForm').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleChangePassword();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
24
tests/test_system_branding.py
Normal file
24
tests/test_system_branding.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""登录页与系统导航名称回归测试。"""
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOGIN_HTML = (ROOT / 'templates/login.html').read_text(encoding='utf-8')
|
||||
BASE_HTML = (ROOT / 'templates/base.html').read_text(encoding='utf-8')
|
||||
|
||||
|
||||
class SystemBrandingTest(unittest.TestCase):
|
||||
def test_login_uses_full_management_system_name(self):
|
||||
self.assertIn('<h1>蝶雅采购管理系统</h1>', LOGIN_HTML)
|
||||
self.assertIn('<title>蝶雅采购管理系统 - 登录</title>', LOGIN_HTML)
|
||||
self.assertNotIn('蝶雅采购智能体平台', LOGIN_HTML)
|
||||
|
||||
def test_sidebar_uses_short_management_system_name(self):
|
||||
self.assertIn('<span class="sidebar-title">采购管理系统</span>', BASE_HTML)
|
||||
self.assertIn('<title>蝶雅采购管理系统</title>', BASE_HTML)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user