改造后台权限和APP权限

This commit is contained in:
super
2026-04-19 15:06:09 +08:00
parent ef0e0df0ac
commit 4b6295dd44
35 changed files with 1487 additions and 85 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -267,7 +267,8 @@ def admin_delete_user(uid):
@login_required
def admin_user_column_permissions(uid):
"""获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。"""
current_uid = session.get('user_id')
current_uid = session.get('user_id')
menu_type = (request.args.get('menu_type') or '').strip().lower()
if current_uid != uid:
role, _ = _get_current_admin_role()
if not role:
@@ -277,28 +278,44 @@ def admin_user_column_permissions(uid):
with conn.cursor() as cur:
cur.execute("SELECT role FROM users WHERE id = %s", (uid,))
user_row = cur.fetchone()
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
cur.execute("""
SELECT id, name, column_key, created_at FROM columns ORDER BY id
""")
rows = cur.fetchall()
else:
cur.execute("""
SELECT c.id, c.name, c.column_key, c.created_at
FROM columns c
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
WHERE ucp.user_id = %s
ORDER BY c.id
""", (uid,))
rows = cur.fetchall()
valid_menu_type = menu_type if menu_type in ('app', 'admin') else ''
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
sql = """
SELECT id, name, column_key, menu_type, route_path, sort_order, created_at
FROM columns
"""
params = []
if valid_menu_type:
sql += " WHERE menu_type = %s"
params.append(valid_menu_type)
sql += " ORDER BY sort_order ASC, id ASC"
cur.execute(sql, tuple(params))
rows = cur.fetchall()
else:
sql = """
SELECT c.id, c.name, c.column_key, c.menu_type, c.route_path, c.sort_order, c.created_at
FROM columns c
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
WHERE ucp.user_id = %s
"""
params = [uid]
if valid_menu_type:
sql += " AND c.menu_type = %s"
params.append(valid_menu_type)
sql += " ORDER BY c.sort_order ASC, c.id ASC"
cur.execute(sql, tuple(params))
rows = cur.fetchall()
conn.close()
items = [
{
'id': r['id'],
'name': r['name'],
'column_key': r['column_key'],
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
}
{
'id': r['id'],
'name': r['name'],
'column_key': r['column_key'],
'menu_type': r.get('menu_type') or 'app',
'route_path': r.get('route_path') or '',
'sort_order': r.get('sort_order') or 0,
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
}
for r in rows
]
return jsonify({'success': True, 'items': items})

View File

@@ -50,10 +50,10 @@ def wb():
return _render_html('index.html')
@main_bp.route('/brand')
@login_required
def brand_page():
return _render_html('brand.html')
@main_bp.route('/brand')
@login_required
def brand_page():
return _render_html('brand.html', user_id=session.get('user_id'))
@@ -161,4 +161,4 @@ def proxy(path):
return Response("无法连接到后端服务", status=502)
except Exception as e:
print(f"代理请求失败: {e}")
return Response(f"代理错误: {str(e)}", status=500)
return Response(f"代理错误: {str(e)}", status=500)

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>格式转换 - 数富AI</title>
<script type="module" crossorigin src="/assets/convert.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-D4gpiFjY.js">
<link rel="modulepreload" crossorigin href="/assets/brand-CXHemZwJ.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
</head>
<body>

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据去重 - 数富AI</title>
<script type="module" crossorigin src="/assets/dedupe.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-D4gpiFjY.js">
<link rel="modulepreload" crossorigin href="/assets/brand-CXHemZwJ.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
</head>
<body>

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>删除品牌 - 数富AI</title>
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-D4gpiFjY.js">
<link rel="modulepreload" crossorigin href="/assets/brand-CXHemZwJ.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-CdxGWtvK.css">
</head>
<body>

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据拆分 - 数富AI</title>
<script type="module" crossorigin src="/assets/split.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-D4gpiFjY.js">
<link rel="modulepreload" crossorigin href="/assets/brand-CXHemZwJ.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
</head>
<body>

View File

@@ -62,6 +62,10 @@
position: relative;
}
body.nav-permissions-loading .nav-section[data-column-key] {
display: none;
}
.nav-section + .nav-section::before {
content: '';
position: absolute;
@@ -383,7 +387,7 @@
.toast.show { opacity: 1; }
</style>
</head>
<body>
<body data-user-id="{{ user_id or '' }}" class="nav-permissions-loading">
<header class="top-bar">
<div class="logo-area">
<span class="app-name">数富AI-亚马逊</span>
@@ -391,33 +395,33 @@
</div>
<nav class="nav-tabs">
<div class="nav-tab-group">
<div class="nav-section">
<div class="nav-section-title">&#21069;&#31471;&#24037;&#20855;</div>
<div class="nav-section" data-column-key="brand_front_tools">
<div class="nav-section-title">前端工具</div>
<div class="nav-section-items">
<button type="button" class="nav-item active" data-panel="brandCheck">&#21697;&#29260;&#26816;&#27979;</button>
<span class="nav-item disabled">&#37319;&#38598;&#25968;&#25454;</span>
<span class="nav-item disabled">&#21464;&#20307;&#20998;&#26512;</span>
<a class="nav-item" href="/new_web_source/dedupe.html">&#25968;&#25454;&#21435;&#37325;</a>
<a class="nav-item" href="/new_web_source/split.html">&#25968;&#25454;&#25286;&#20998;</a>
<a class="nav-item" href="/new_web_source/convert.html">&#26684;&#24335;&#36716;&#25442;</a>
<span class="nav-item disabled">采集数据</span>
<span class="nav-item disabled">变体分析</span>
<button type="button" class="nav-item active" data-panel="brandCheck">品牌检测</button>
<a class="nav-item" href="/new_web_source/dedupe.html">数据去重</a>
<a class="nav-item" href="/new_web_source/split.html">数据拆分</a>
<a class="nav-item" href="/new_web_source/convert.html">格式转换</a>
</div>
</div>
<div class="nav-section">
<div class="nav-section-title">&#36816;&#33829;&#24037;&#20855;</div>
<div class="nav-section" data-column-key="brand_operation_tools">
<div class="nav-section-title">运营工具</div>
<div class="nav-section-items">
<a class="nav-item" href="/new_web_source/delete-brand.html">&#21024;&#38500;ASIN</a>
<a class="nav-item" href="/new_web_source/product-risk.html">&#21830;&#21697;&#39118;&#38505;&#35299;&#20915;</a>
<a class="nav-item" href="/new_web_source/shop-match.html">&#23450;&#26102;&#21305;&#37197;</a>
<span class="nav-item disabled">&#36319;&#20215;</span>
<span class="nav-item disabled">&#24033;&#24215;&#21024;&#38500;</span>
<span class="nav-item disabled">&#21462;&#27454;</span>
<span class="nav-item disabled">&#24215;&#38138;&#29366;&#24577;&#26597;&#35810;</span>
<a class="nav-item" href="/new_web_source/delete-brand.html">删除ASIN</a>
<a class="nav-item" href="/new_web_source/product-risk.html">商品风险解决</a>
<a class="nav-item" href="/new_web_source/shop-match.html">定时匹配</a>
<a class="nav-item" href="/new_web_source/price-track.html">跟价</a>
<span class="nav-item disabled">巡店删除</span>
<span class="nav-item disabled">取款</span>
<span class="nav-item disabled">店铺状态查询</span>
</div>
</div>
<div class="nav-section">
<div class="nav-section-title">&#21518;&#21220;&#24037;&#20855;</div>
<div class="nav-section" data-column-key="brand_logistics_tools">
<div class="nav-section-title">后勤工具</div>
<div class="nav-section-items">
<span class="nav-item disabled">&#37319;&#36141;</span>
<span class="nav-item disabled">采购</span>
<span class="nav-item disabled">ERP</span>
</div>
</div>
@@ -535,9 +539,13 @@
<script>
(function() {
var selectedPaths = [];
var cachedTasks = [];
var api = window.pywebview && window.pywebview.api;
var selectedPaths = [];
var cachedTasks = [];
var api = window.pywebview && window.pywebview.api;
function getAppPermissionCacheKey(uid) {
return 'app_column_permissions:' + String(uid || '');
}
function getUid() {
return localStorage.getItem('uid') || '';
@@ -550,20 +558,80 @@
}
// 顶部栏目切换:点击当前页菜单项时显示对应 data-panel 的 tab-panel
document.querySelectorAll('.nav-item[data-panel]').forEach(function(tab) {
tab.addEventListener('click', function() {
var panelId = this.getAttribute('data-panel');
document.querySelectorAll('.nav-item[data-panel]').forEach(function(tab) {
tab.addEventListener('click', function() {
var panelId = this.getAttribute('data-panel');
if (!panelId) return;
document.querySelectorAll('.nav-item[data-panel]').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) {
p.classList.toggle('active', p.getAttribute('data-panel') === panelId);
});
this.classList.add('active');
});
});
function showToast(msg) {
var el = document.getElementById('toast');
this.classList.add('active');
});
});
(function applyNavSectionPermissions() {
function finishNavPermissionLoading() {
document.body.classList.remove('nav-permissions-loading');
}
function showAllNavSections() {
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
section.style.display = '';
});
}
var uid = document.body.getAttribute('data-user-id');
if (!uid) {
showAllNavSections();
finishNavPermissionLoading();
return;
}
localStorage.setItem('uid', uid);
var cacheKey = getAppPermissionCacheKey(uid);
try {
var cachedItems = JSON.parse(localStorage.getItem(cacheKey) || 'null');
if (Array.isArray(cachedItems)) {
showAllNavSections();
var cachedKeys = cachedItems.map(function(item) {
return (item.column_key || '').toLowerCase();
});
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
var key = (section.getAttribute('data-column-key') || '').toLowerCase();
section.style.display = cachedKeys.indexOf(key) >= 0 ? '' : 'none';
});
finishNavPermissionLoading();
return;
}
} catch (e) {}
fetch('/api/admin/user/' + uid + '/column-permissions?menu_type=app', { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(res) {
showAllNavSections();
if (!res || !res.success || !Array.isArray(res.items)) {
finishNavPermissionLoading();
return;
}
try {
localStorage.setItem(cacheKey, JSON.stringify(res.items));
} catch (e) {}
var allowedKeys = res.items.map(function(item) {
return (item.column_key || '').toLowerCase();
});
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
var key = (section.getAttribute('data-column-key') || '').toLowerCase();
section.style.display = allowedKeys.indexOf(key) >= 0 ? '' : 'none';
});
finishNavPermissionLoading();
})
.catch(function() {
showAllNavSections();
finishNavPermissionLoading();
});
})();
function showToast(msg) {
var el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(function() { el.classList.remove('show'); }, 2500);

View File

@@ -217,17 +217,24 @@
<div class="toast" id="toast"></div>
<script>
function getAppPermissionCacheKey(uid) {
return 'app_column_permissions:' + String(uid || '');
}
// 权限校验:根据 column-permissions 接口按 column_key 显示入口
(function() {
localStorage.setItem("uid",{{ user_id }})
var uid = document.body.getAttribute('data-user-id');
if (!uid) return;
var cacheKey = getAppPermissionCacheKey(uid);
var baseUrl = window.location.origin;
var apiUrl = baseUrl + '/api/admin/user/' + uid + '/column-permissions';
fetch(apiUrl, { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res || !res.success || !Array.isArray(res.items)) return;
try {
localStorage.setItem(cacheKey, JSON.stringify(res.items));
} catch (e) {}
var allowedKeys = res.items.map(function(item) { return (item.column_key || '').toLowerCase(); });
document.querySelectorAll('.entrances .entrance-btn[data-column-key]').forEach(function(btn) {
var key = (btn.getAttribute('data-column-key') || '').toLowerCase();
@@ -251,6 +258,10 @@
function handleLogout() {
try{
localStorage.removeItem('maixiang_api_key');
var uid = document.body.getAttribute('data-user-id');
if (uid) {
localStorage.removeItem(getAppPermissionCacheKey(uid));
}
}catch (e) {
console.log(e)
}

View File

@@ -117,6 +117,21 @@
</form>
</div>
<script>
function clearAppPermissionCaches() {
try {
var keysToRemove = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.indexOf('app_column_permissions:') === 0) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(function(key) {
localStorage.removeItem(key);
});
} catch (e) {}
}
(function() {
fetch('/api/auth/check', { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
@@ -142,6 +157,7 @@
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
clearAppPermissionCaches();
window.location.href = res.redirect || '/home';
} else {
var errEl = document.querySelector('.error-msg');