Files
opc-manager/templates/login.html
mac 9b6257ff19 v1.1.0-beta: 安全/性能/架构优化 + 账号管理后台 + 视图切换
## 安全与性能
- .env 环境变量、debug=False、except 改 mysql.connector.Error+logging
- attach_common 批量 IN 查询消除 N+1
- 批量 esc() XSS 转义

## 架构
- app.js 拆分为 7 模块 + admin.js
- .form-ctrl 统一表单控件

## 经营管理
- 字段改名:客户名称→项目名称、销售人员→商务负责人
- 必填:项目名称/商务负责人/经营负责人/签约月份/签约金额>0
- 视图切换:确收/毛利 ↔ 回款/费用

## 重点工作与台账
- 统计卡片样式与经营管理统一
- 任务状态简化 3 态
- 优先级点击切换、右键菜单(重命名/副本)
- 修复新建任务绑定错误项目 bug

## 用户体系
- 新增工作台:MCN·无界、无界·无界
- 新增账号:mcn/wuji
- 账号管理后台(admin 限定)
- sidebar 顶部头像+显示名,点击弹菜单
- sidebar sticky 定位

## 其他
- 登录页样式优化(参考 UOC 平台)
- 首页财务趋势拆 3 图
- 业务方案标准资料库双 Tab
2026-06-23 15:54:03 +08:00

103 lines
5.4 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPC 工作台 · 登录</title>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/lucide.js') }}" onerror="this.remove()">
<script src="{{ url_for('static', filename='vendor/lucide.js') }}"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:PingFang SC,Microsoft YaHei,-apple-system,sans-serif;background:#f8fafc;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#fff;border-radius:16px;padding:40px 44px;width:400px;max-width:94vw;box-shadow:0 16px 48px rgba(15,23,42,.12)}
.logo{text-align:center;margin-bottom:32px}
.logo .icon{width:56px;height:56px;margin:0 auto 10px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#1e293b,#0f172a);border-radius:14px;color:#fff}
.logo h1{font-size:20px;font-weight:700;color:#1e293b}
.logo p{font-size:13px;color:#94a3b8;margin-top:4px}
.form-group{margin-bottom:18px}
label{display:block;font-size:13px;font-weight:500;color:#475569;margin-bottom:6px}
.input-wrap{position:relative}
input[type=text],input[type=password]{width:100%;height:42px;padding:0 14px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none;transition:.2s;font-family:inherit;color:#1e293b;background:#fff}
.input-wrap input{padding:0 40px 0 14px}
input[type=text]:focus,input[type=password]:focus,.input-wrap input:focus{border-color:#334155;box-shadow:0 0 0 3px rgba(51,65,85,.12)}
input::placeholder{color:#94a3b8}
.toggle-pwd{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:#94a3b8;padding:4px;display:flex;align-items:center;justify-content:center;transition:.2s}
.toggle-pwd:hover{color:#334155}
.btn{width:100%;height:44px;background:#1e293b;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:.2s;margin-top:8px}
.btn:hover{background:#0f172a}
.btn:active{transform:scale(.98)}
.err{background:#fef2f2;border:1px solid #fecaca;color:#dc2626;border-radius:8px;padding:10px 14px;font-size:13px;margin-bottom:16px;display:none}
.err.show{display:block}
.footer{text-align:center;margin-top:24px;font-size:11px;color:#94a3b8}
.footer strong{color:#334155}
</style>
</head>
<body>
<div class="card">
<div class="logo">
<div class="icon"><i data-lucide="layout-dashboard" style="width:28px;height:28px;stroke-width:2"></i></div>
<h1>OPC 工作台</h1>
<p>请登录后继续</p>
</div>
<div class="err" id="errMsg"></div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" placeholder="请输入用户名" autocomplete="username" autofocus>
</div>
<div class="form-group">
<label>密码</label>
<div class="input-wrap">
<input type="password" id="password" placeholder="请输入密码" autocomplete="current-password">
<button type="button" class="toggle-pwd" onclick="togglePwd('password', this)" title="显示/隐藏密码"><i data-lucide="eye" style="width:18px;height:18px"></i></button>
</div>
</div>
<button class="btn" id="loginBtn" onclick="doLogin()">登 录</button>
<div class="footer">默认管理员:<strong>qiukai / yxcowork2026</strong></div>
</div>
<script>
if (window.lucide) lucide.createIcons();
function togglePwd(id, btn) {
const input = document.getElementById(id);
const isPwd = input.type === 'password';
input.type = isPwd ? 'text' : 'password';
btn.innerHTML = isPwd
? '<i data-lucide="eye-off" style="width:18px;height:18px"></i>'
: '<i data-lucide="eye" style="width:18px;height:18px"></i>';
if (window.lucide) lucide.createIcons();
}
const errEl = document.getElementById('errMsg');
function showErr(msg) { errEl.textContent = msg; errEl.classList.add('show'); }
function clearErr() { errEl.classList.remove('show'); }
document.getElementById('username').addEventListener('input', clearErr);
document.getElementById('password').addEventListener('input', clearErr);
// 回车提交
document.getElementById('password').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
document.getElementById('username').addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('password').focus(); });
async function doLogin() {
clearErr();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) { showErr('请输入用户名和密码'); return; }
const btn = document.getElementById('loginBtn');
btn.disabled = true; btn.style.opacity = .7; btn.textContent = '登录中...';
try {
const res = await (await fetch('/api/auth/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})).json();
if (res.error) { showErr(res.error); btn.disabled = false; btn.style.opacity = 1; btn.textContent = '登 录'; return; }
window.location.href = '/';
} catch (err) {
showErr('网络错误,请重试');
btn.disabled = false; btn.style.opacity = 1; btn.textContent = '登 录';
}
}
</script>
</body>
</html>