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
This commit is contained in:
@@ -28,8 +28,11 @@
|
||||
<body class="min-h-screen bg-slate-50 text-slate-950">
|
||||
<div class="flex min-h-screen">
|
||||
<!-- 左侧工作台切换栏 -->
|
||||
<aside class="w-[72px] bg-slate-900 flex flex-col items-center py-6 gap-1 shrink-0" id="workspaceSidebar">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center mb-6 text-white font-bold text-sm">OPC</div>
|
||||
<aside class="w-[72px] bg-slate-900 flex flex-col items-center py-6 gap-1 shrink-0 sticky top-0 h-screen overflow-y-auto" id="workspaceSidebar">
|
||||
<div class="flex flex-col items-center mb-6 cursor-pointer" onclick="document.getElementById('userAvatar').click()">
|
||||
<div class="w-10 h-10 rounded-full bg-slate-700 flex items-center justify-center text-white font-bold text-sm hover:ring-2 hover:ring-blue-400 transition-all" id="userAvatar" title=""></div>
|
||||
<span class="text-[10px] text-slate-400 mt-1.5 max-w-[64px] truncate text-center" id="userDisplayName" title=""></span>
|
||||
</div>
|
||||
<div class="workspace-nav-item active" data-tenant="科普·无界" onclick="switchTenant('科普·无界')" title="科普·无界">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
||||
<span class="text-[10px] mt-1">科普</span>
|
||||
@@ -42,10 +45,13 @@
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
<span class="text-[10px] mt-1">医患</span>
|
||||
</div>
|
||||
<!-- 用户区 -->
|
||||
<div class="mt-auto flex flex-col items-center gap-2 pt-4">
|
||||
<div class="w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center text-white text-[11px] font-medium" id="userAvatar" title=""></div>
|
||||
<button class="text-[10px] text-slate-500 hover:text-red-400 transition-colors" onclick="doLogout()" title="退出登录">退出</button>
|
||||
<div class="workspace-nav-item" data-tenant="MCN·无界" onclick="switchTenant('MCN·无界')" title="MCN·无界">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="15" rx="2"/><path d="M17 2l-5 5-5-5"/></svg>
|
||||
<span class="text-[10px] mt-1">MCN</span>
|
||||
</div>
|
||||
<div class="workspace-nav-item" data-tenant="无界·无界" onclick="switchTenant('无界·无界')" title="无界·无界">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
<span class="text-[10px] mt-1">无界</span>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- 主内容区 -->
|
||||
@@ -91,7 +97,7 @@
|
||||
<input type="hidden" name="transfer_id" id="transfer-id" value="">
|
||||
<p id="transfer-title-text" class="text-sm text-slate-600"></p>
|
||||
<label class="block"><span class="text-xs font-medium text-slate-500">目标工作台</span>
|
||||
<select name="transfer_tenant" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm">
|
||||
<select name="transfer_tenant" class="form-ctrl mt-1">
|
||||
<option value="科普·无界">科普·无界</option>
|
||||
<option value="科研·无界">科研·无界</option>
|
||||
<option value="医患·无界">医患·无界</option>
|
||||
@@ -112,8 +118,8 @@
|
||||
<button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeNewProjectModal()"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<form onsubmit="createOperation(event)" class="p-6 grid gap-4">
|
||||
<label class="block"><span class="text-xs font-medium text-slate-500">项目名称</span><input name="project_name" required class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm"></label>
|
||||
<label class="block"><span class="text-xs font-medium text-slate-500">项目备注</span><textarea name="notes" rows="3" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm" placeholder="可选"></textarea></label>
|
||||
<label class="block"><span class="text-xs font-medium text-slate-500">项目名称</span><input name="project_name" required class="form-ctrl mt-1"></label>
|
||||
<label class="block"><span class="text-xs font-medium text-slate-500">项目备注</span><textarea name="notes" rows="3" class="form-ctrl mt-1" placeholder="可选"></textarea></label>
|
||||
<div class="flex justify-end gap-3 pt-3 border-t border-slate-100">
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick="closeNewProjectModal()">取消</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm">创建</button>
|
||||
@@ -121,6 +127,14 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='modules/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/home.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/projects.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/proposals.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/products.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/finance.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/drawer.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/admin.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,42 +4,99 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OPC 工作台 · 登录</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<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 class="min-h-screen bg-slate-50 flex items-center justify-center">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8 w-full max-w-sm mx-4">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-800">OPC 工作台</h1>
|
||||
<p class="text-sm text-slate-400 mt-1">请输入账号密码登录</p>
|
||||
</div>
|
||||
<form id="loginForm" onsubmit="doLogin(event)" class="grid gap-4">
|
||||
<label class="block">
|
||||
<span class="text-xs font-medium text-slate-500">账号</span>
|
||||
<input name="username" required class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm" placeholder="admin / kepu / keyan / yihuan" autofocus>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-xs font-medium text-slate-500">密码</span>
|
||||
<input name="password" type="password" required class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm">
|
||||
</label>
|
||||
<p id="loginError" class="text-red-500 text-xs hidden"></p>
|
||||
<button type="submit" class="btn w-full bg-slate-800 text-white rounded-lg py-2.5 text-sm font-medium hover:bg-slate-700">登 录</button>
|
||||
</form>
|
||||
<p class="text-xs text-slate-400 text-center mt-6">默认管理员:qiukai / yxcowork2026</p>
|
||||
<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>
|
||||
<script>
|
||||
async function doLogin(e) {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
try {
|
||||
const res = await (await fetch("/api/auth/login", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(data) })).json();
|
||||
if (res.error) { document.querySelector("#loginError").textContent = res.error; document.querySelector("#loginError").classList.remove("hidden"); return; }
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
document.querySelector("#loginError").textContent = "网络错误,请重试";
|
||||
document.querySelector("#loginError").classList.remove("hidden");
|
||||
}
|
||||
<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>
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user