This commit is contained in:
nanguaboss
2026-08-03 00:07:01 +08:00
commit 71c6e8d9c1
112 changed files with 7815 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
/* login.css - 登录页样式 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #6dd5ed 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-wrap {
width: 400px;
background: #fff;
border-radius: 10px;
box-shadow: 0 12px 40px rgba(0,0,0,.25);
padding: 40px 36px 30px;
}
.login-title {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #1e3c72;
margin-bottom: 6px;
}
.login-sub {
text-align: center;
font-size: 13px;
color: #8a94a6;
margin-bottom: 26px;
}
.login-form .form-item {
margin-bottom: 18px;
}
.login-form label {
display: block;
font-size: 13px;
color: #5a6472;
margin-bottom: 6px;
}
.login-form input[type="text"],
.login-form input[type="password"] {
width: 100%;
height: 42px;
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
outline: none;
transition: border-color .2s;
}
.login-form input:focus {
border-color: #2a5298;
}
.login-btn {
width: 100%;
height: 44px;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: #fff;
border: none;
border-radius: 4px;
font-size: 15px;
cursor: pointer;
margin-top: 6px;
transition: opacity .2s;
}
.login-btn:hover { opacity: .9; }
.login-btn:disabled { opacity: .5; cursor: not-allowed; }
.login-links {
margin-top: 16px;
text-align: right;
font-size: 13px;
}
.login-links a {
color: #2a5298;
text-decoration: none;
}
.login-links a:hover { text-decoration: underline; }
.login-msg {
display: none;
margin-bottom: 14px;
padding: 10px 12px;
border-radius: 4px;
font-size: 13px;
}
.login-msg.error { display: block; background: #fdecea; color: #c0392b; border: 1px solid #f5c6cb; }
.login-msg.success { display: block; background: #eafaf1; color: #27ae60; border: 1px solid #a9dfbf; }
.login-copyright {
margin-top: 22px;
text-align: center;
font-size: 12px;
color: #b6bfcc;
}
+56
View File
@@ -0,0 +1,56 @@
/* slider.css - 滑块验证样式 */
.slider-box {
position: relative;
width: 100%;
height: 42px;
background: #eef1f6;
border: 1px solid #d9dee8;
border-radius: 4px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.slider-bg {
position: absolute;
left: 0;
top: 0;
width: 50px;
height: 100%;
background: #7ac243;
opacity: .45;
}
.slider-text {
position: absolute;
width: 100%;
height: 100%;
line-height: 42px;
text-align: center;
font-size: 13px;
color: #8a94a6;
z-index: 2;
pointer-events: none;
}
.slider-text.slider-success {
color: #3d8b1b;
font-weight: bold;
}
.slider-btn {
position: absolute;
left: 0;
top: 0;
width: 50px;
height: 40px;
line-height: 40px;
text-align: center;
background: #fff;
border: 1px solid #d9dee8;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
color: #5a6472;
z-index: 3;
box-shadow: 0 1px 3px rgba(0,0,0,.15);
}
.slider-btn:active {
background: #f2f6fd;
}
+523
View File
@@ -0,0 +1,523 @@
/* style.css - 全局样式(左侧导航 + 内容区自适应) */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
background: #f2f4f8;
color: #333;
font-size: 14px;
}
/* ---------- 布局 ---------- */
.layout { display: flex; min-height: 100vh; }
/* 左侧导航 */
.sidebar {
width: 220px;
background: #1e2a3a;
color: #cfd8e3;
flex-shrink: 0;
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.sidebar .logo {
display: block;
padding: 20px 18px;
font-size: 18px;
font-weight: bold;
color: #fff;
border-bottom: 1px solid rgba(255,255,255,.08);
letter-spacing: 1px;
text-decoration: none;
cursor: pointer;
}
.sidebar .logo:hover { color: #fff; }
.sidebar .logo .version {
font-size: 11px;
font-weight: normal;
color: #8fa3bd;
margin-left: 6px;
letter-spacing: 0;
}
.sidebar .logo span { color: #4fa3ff; }
.sidebar .menu { flex: 1; padding: 10px 0; }
.sidebar .menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
color: #cfd8e3;
text-decoration: none;
font-size: 14px;
cursor: pointer;
border-left: 3px solid transparent;
transition: background .2s;
}
.sidebar .menu-item:hover { background: rgba(255,255,255,.06); color: #fff; }
.sidebar .menu-item.active {
background: rgba(79,163,255,.15);
color: #fff;
border-left-color: #4fa3ff;
}
.sidebar .menu-item .icon { display: inline-flex; flex-shrink: 0; }
.sidebar .menu-item .label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar .menu-item .badge {
float: right;
background: #4fa3ff;
color: #fff;
border-radius: 10px;
font-size: 11px;
padding: 1px 7px;
}
/* 分组菜单(一级分组标题 + 二级子菜单) */
.sidebar .menu-group { border-left: 3px solid transparent; }
.sidebar .menu-group.open { border-left-color: #4fa3ff; }
.sidebar .menu-group-title {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
color: #cfd8e3;
font-size: 14px;
cursor: pointer;
user-select: none;
transition: background .2s;
}
.sidebar .menu-group-title:hover { background: rgba(255,255,255,.06); color: #fff; }
.sidebar .menu-group.open > .menu-group-title { color: #fff; }
.sidebar .menu-group-title .icon { display: inline-flex; flex-shrink: 0; }
.sidebar .menu-group-title .label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar .menu-group-title .arrow {
color: #8fa3bd;
font-size: 10px;
transition: transform .2s;
}
.sidebar .menu-group.open > .menu-group-title .arrow { transform: rotate(90deg); }
.sidebar .menu-group-body { display: none; }
.sidebar .menu-item.sub {
padding-left: 40px;
font-size: 13px;
border-left: none;
gap: 0;
}
/* ---------- 侧边栏折叠态(桌面手动折叠 / 移动端自动折叠,仅剩图标) ---------- */
.layout.sidebar-collapsed .sidebar { width: 64px; }
.layout.sidebar-collapsed .sidebar .logo {
padding: 20px 4px;
text-align: center;
font-size: 0;
letter-spacing: 0;
}
.layout.sidebar-collapsed .sidebar .logo .version { display: none; }
.layout.sidebar-collapsed .menu-item,
.layout.sidebar-collapsed .menu-group-title {
padding: 13px 0;
justify-content: center;
gap: 0;
}
.layout.sidebar-collapsed .menu-item .label,
.layout.sidebar-collapsed .menu-group-title .label,
.layout.sidebar-collapsed .menu-group-title .arrow { display: none; }
.layout.sidebar-collapsed .menu-group-body { display: none !important; }
.layout.sidebar-collapsed .menu-item.sub { padding-left: 0; }
.layout.sidebar-collapsed .sidebar .menu { padding: 10px 0; }
.sidebar .user-info {
padding: 14px 18px;
border-top: 1px solid rgba(255,255,255,.08);
font-size: 13px;
}
.sidebar .user-info .name { color: #fff; font-weight: bold; }
.sidebar .user-info .logout {
float: right;
color: #8fa3bd;
cursor: pointer;
text-decoration: none;
}
.sidebar .user-info .logout:hover { color: #ff6b6b; }
/* 内容区 */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
height: 54px;
background: #fff;
border-bottom: 1px solid #e5e9f0;
display: flex;
align-items: center;
padding: 0 22px;
position: sticky;
top: 0;
z-index: 100;
}
.topbar .page-title { font-size: 16px; font-weight: bold; color: #1e2a3a; }
.topbar .breadcrumb { margin-left: 14px; font-size: 13px; color: #8a94a6; }
.topbar .topbar-spacer { flex: 1; }
.topbar .sidebar-toggle {
display: inline-flex;
align-items: center;
cursor: pointer;
color: #5a6472;
margin-right: 12px;
padding: 4px;
border-radius: 4px;
}
.topbar .sidebar-toggle:hover { background: #f2f4f8; color: #2a5298; }
/* ---------- 右上角用户菜单(登录/登出、角色、登录状态) ---------- */
.user-menu { position: relative; margin-left: auto; }
.user-menu-trigger {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
color: #4a5568;
user-select: none;
-webkit-user-select: none;
}
.user-menu-trigger:hover { background: #f2f4f8; }
.user-menu-trigger .avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: #2a5298;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.user-menu-trigger .caret { color: #8a94a6; font-size: 10px; }
.user-menu-dropdown {
display: none;
position: absolute;
right: 0;
top: 100%;
min-width: 230px;
background: #fff;
border: 1px solid #e5e9f0;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0,0,0,.12);
z-index: 1000;
padding: 6px 0;
}
.user-menu.open .user-menu-dropdown { display: block; }
.user-menu-dropdown .um-head {
padding: 10px 14px;
border-bottom: 1px solid #f0f2f6;
}
.user-menu-dropdown .um-head .um-name { font-weight: bold; color: #1e2a3a; font-size: 14px; }
.user-menu-dropdown .um-head .um-sub { font-size: 12px; color: #8a94a6; margin-top: 2px; }
.user-menu-dropdown .um-item {
padding: 8px 14px;
font-size: 13px;
color: #5a6472;
display: flex;
justify-content: space-between;
gap: 12px;
}
.user-menu-dropdown .um-item .um-label { color: #8a94a6; }
.user-menu-dropdown .um-item b { font-weight: 600; }
.user-menu-dropdown .um-ok { color: #27ae60; }
.user-menu-dropdown .um-off { color: #e74c3c; }
.user-menu-dropdown .um-divider { border-top: 1px solid #f0f2f6; margin: 4px 0; }
.user-menu-dropdown .um-action {
display: block;
padding: 9px 14px;
font-size: 13px;
color: #2a5298;
cursor: pointer;
text-decoration: none;
}
.user-menu-dropdown .um-action:hover { background: #f7f9fc; }
.user-menu-dropdown .um-action.um-danger { color: #e74c3c; }
.content {
padding: 20px 22px;
flex: 1;
}
/* ---------- 卡片 ---------- */
.card {
background: #fff;
border-radius: 6px;
border: 1px solid #e5e9f0;
padding: 18px;
margin-bottom: 16px;
}
.card-title {
font-size: 15px;
font-weight: bold;
color: #1e2a3a;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f2f6;
}
/* ---------- 工具栏 ---------- */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.toolbar input[type="text"],
.toolbar select {
height: 34px;
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 0 10px;
font-size: 13px;
outline: none;
min-width: 130px;
background: #fff;
}
.toolbar input:focus, .toolbar select:focus { border-color: #4fa3ff; }
.toolbar .spacer { flex: 1; }
/* ---------- 按钮 ---------- */
.btn {
display: inline-block;
height: 34px;
line-height: 32px;
padding: 0 16px;
border: 1px solid #d9dee8;
border-radius: 4px;
background: #fff;
color: #333;
font-size: 13px;
cursor: pointer;
text-decoration: none;
transition: all .15s;
}
.btn:hover { border-color: #4fa3ff; color: #2a5298; }
.btn-primary { background: #2a5298; border-color: #2a5298; color: #fff; }
.btn-primary:hover { background: #1e3c72; border-color: #1e3c72; color: #fff; }
.btn-success { background: #27ae60; border-color: #27ae60; color: #fff; }
.btn-success:hover { background: #219150; border-color: #219150; color: #fff; }
.btn-danger { background: #e74c3c; border-color: #e74c3c; color: #fff; }
.btn-danger:hover { background: #c0392b; border-color: #c0392b; color: #fff; }
.btn-warning { background: #f39c12; border-color: #f39c12; color: #fff; }
.btn-warning:hover { background: #d68910; border-color: #d68910; color: #fff; }
.btn-sm { height: 26px; line-height: 24px; padding: 0 10px; font-size: 12px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
/* ---------- 表格 ---------- */
.table-wrap { overflow-x: auto; }
table.grid {
width: 100%;
border-collapse: collapse;
background: #fff;
font-size: 13px;
}
table.grid th, table.grid td {
border: 1px solid #eef0f4;
padding: 9px 10px;
text-align: left;
white-space: nowrap;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
}
table.grid th {
background: #f7f9fc;
color: #4a5568;
font-weight: bold;
}
table.grid tr:hover td { background: #f7faff; }
table.grid .ops a {
margin-right: 8px;
cursor: pointer;
color: #2a5298;
text-decoration: none;
font-size: 12px;
}
table.grid .ops a.danger { color: #e74c3c; }
table.grid .ops a:hover { text-decoration: underline; }
/* ---------- 分页 ---------- */
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 14px;
font-size: 13px;
color: #5a6472;
gap: 6px;
}
.pagination a, .pagination span.pg {
display: inline-block;
min-width: 30px;
height: 30px;
line-height: 28px;
text-align: center;
border: 1px solid #d9dee8;
border-radius: 4px;
background: #fff;
cursor: pointer;
color: #333;
text-decoration: none;
padding: 0 6px;
}
.pagination a:hover { border-color: #4fa3ff; color: #2a5298; }
.pagination span.pg.current { background: #2a5298; border-color: #2a5298; color: #fff; }
.pagination span.pg.disabled { color: #b6bfcc; cursor: not-allowed; }
/* ---------- 表单弹窗 ---------- */
.form-grid { display: flex; flex-wrap: wrap; gap: 14px 20px; }
.form-grid .form-item { width: calc(50% - 10px); }
.form-grid .form-item.full { width: 100%; }
.form-item label {
display: block;
font-size: 13px;
color: #5a6472;
margin-bottom: 6px;
}
.form-item label .req { color: #e74c3c; margin-left: 2px; }
.form-item input[type="text"],
.form-item input[type="password"],
.form-item input[type="number"],
.form-item input[type="date"],
.form-item select,
.form-item textarea {
width: 100%;
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 8px 10px;
font-size: 13px;
outline: none;
background: #fff;
font-family: inherit;
}
.form-item textarea { min-height: 70px; resize: vertical; }
.form-item input:focus, .form-item select:focus, .form-item textarea:focus { border-color: #4fa3ff; }
.dialog-footer {
margin-top: 18px;
text-align: right;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* ---------- 指标卡 ---------- */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 16px;
}
.stat-card {
background: #fff;
border-radius: 6px;
border: 1px solid #e5e9f0;
border-left: 4px solid #2a5298;
padding: 16px 18px;
}
.stat-card .num { font-size: 26px; font-weight: bold; color: #1e2a3a; }
.stat-card .label { font-size: 13px; color: #8a94a6; margin-top: 4px; }
.stat-card.c-green { border-left-color: #27ae60; }
.stat-card.c-orange { border-left-color: #f39c12; }
.stat-card.c-red { border-left-color: #e74c3c; }
.stat-card.c-blue { border-left-color: #2a5298; }
.stat-card.c-purple { border-left-color: #8e44ad; }
.stat-card.c-teal { border-left-color: #16a085; }
.stat-card.c-cyan { border-left-color: #00a8c5; }
/* ---------- 图表容器 ---------- */
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.chart-box { background: #fff; border-radius: 6px; border: 1px solid #e5e9f0; padding: 16px; }
.chart-box.full { grid-column: 1 / -1; }
.chart-box .chart-title { font-size: 14px; font-weight: bold; color: #1e2a3a; margin-bottom: 10px; }
.chart-box .chart { height: 320px; }
/* 图表标题内的 Tab 切换 */
.chart-title .tabs { float: right; font-weight: normal; font-size: 12px; }
.chart-title .tabs .tab {
display: inline-block;
padding: 2px 12px;
margin-left: 4px;
border: 1px solid #d9dee8;
border-radius: 3px;
color: #5a6472;
cursor: pointer;
background: #fff;
user-select: none;
}
.chart-title .tabs .tab:hover { border-color: #4fa3ff; color: #2a5298; }
.chart-title .tabs .tab.active {
background: #2a5298;
border-color: #2a5298;
color: #fff;
}
/* ---------- 页脚(版权/备案) ---------- */
.footer {
padding: 14px 22px;
text-align: center;
color: #8a94a6;
font-size: 12px;
border-top: 1px solid #e5e9f0;
background: #fff;
}
/* ---------- 占位图(扁平图标放大) ---------- */
.placeholder-box .big svg { width: 48px; height: 48px; color: #b6bfcc; }
/* ---------- 标签 ---------- */
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
background: #eef4ff;
color: #2a5298;
}
.tag-green { background: #eafaf1; color: #27ae60; }
.tag-red { background: #fdecea; color: #e74c3c; }
.tag-orange { background: #fef5e7; color: #f39c12; }
.tag-gray { background: #f0f2f6; color: #8a94a6; }
/* ---------- 空状态 ---------- */
.empty-tip {
text-align: center;
color: #b6bfcc;
padding: 60px 0;
font-size: 14px;
}
.empty-tip .big { font-size: 40px; margin-bottom: 10px; }
/* ---------- 占位说明 ---------- */
.placeholder-box {
text-align: center;
padding: 60px 20px;
color: #8a94a6;
}
.placeholder-box .big { font-size: 46px; margin-bottom: 14px; }
.placeholder-box h3 { color: #4a5568; margin-bottom: 8px; }
/* 滚动条美化 */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #c8d0dc; border-radius: 4px; }
::-webkit-scrollbar-track { background: transparent; }
@media (max-width: 900px) {
.sidebar { width: 64px; }
.sidebar .menu-item { text-align: center; padding: 12px 6px; font-size: 12px; }
.sidebar .menu-item .icon { margin-right: 0; }
.sidebar .logo { font-size: 14px; text-align: center; padding: 16px 4px; }
.sidebar .user-info { display: none; }
.stat-cards { grid-template-columns: repeat(2, 1fr); }
.chart-grid { grid-template-columns: 1fr; }
}
+173
View File
@@ -0,0 +1,173 @@
/**
* channel.js - 渠道管理逻辑(渠道列表 + 年度统计 + 效率数据)
*/
$(function () {
renderShell('渠道管理', '业务模块 / 渠道管理');
renderPage();
initPage('channel', function (user) {
if (!checkPagePermission('channel')) return;
loadList(1);
loadStats();
});
var currentPage = 1;
var pageRows = [];
/** 渲染页面内容(统计卡+工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="stat-cards" id="stat-summary"></div>' +
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="渠道名称/联系人关键词">' +
' <input type="text" data-filter="channel_type" placeholder="渠道类型">' +
' <select id="year-select" data-filter="year"><option value="">选择年度</option></select>' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>渠道名称</th><th>类型</th><th>联系人</th><th>电话</th><th>邮箱</th><th>效率评分</th><th>备注</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="channel-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
}
/* ---------- 列表 ---------- */
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
delete params.year; // 年度仅用于统计
params.page = page;
params.limit = PAGE_SIZE;
httpGet('channel/list.php', params).then(function (data) {
var rows = data.list || [];
pageRows = rows;
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.channel_name) + '</td>' +
'<td>' + escHtml(r.channel_type || '-') + '</td>' +
'<td>' + escHtml(r.contact_person || '-') + '</td>' +
'<td>' + escHtml(r.contact_phone || '-') + '</td>' +
'<td>' + escHtml(r.contact_email || '-') + '</td>' +
'<td>' + escHtml(r.efficiency_score) + '</td>' +
'<td title="' + escHtml(r.remark || '') + '">' + escHtml((r.remark || '-').substring(0, 20)) + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops"><a onclick="editChannel(' + r.id + ')">编辑</a>' +
'<a class="danger" onclick="delChannel(' + r.id + ')">删除</a></td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="10" class="empty-tip" style="padding:40px 0;">暂无渠道数据(渠道表为新增加的业务表,可在 sql/system_tables.sql 中创建)</td></tr>';
}
$('#channel-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
/* ---------- 年度统计 ---------- */
function loadStats() {
httpGet('channel/stats.php').then(function (data) {
var years = data.years || [];
var html = '<option value="">选择年度</option>';
years.forEach(function (y) {
html += '<option value="' + y + '"' + (y == data.year ? ' selected' : '') + '>' + y + '年</option>';
});
$('#year-select').html(html);
$('#stat-summary').html(
'<div class="stat-card c-blue"><div class="num">' + data.summary.channel_count + '</div><div class="label">' + data.year + '年渠道数</div></div>' +
'<div class="stat-card c-green"><div class="num">' + data.summary.avg_score + '</div><div class="label">平均效率评分</div></div>' +
'<div class="stat-card c-orange"><div class="num">' + data.summary.max_score + '</div><div class="label">最高评分</div></div>' +
'<div class="stat-card c-red"><div class="num">' + data.summary.min_score + '</div><div class="label">最低评分</div></div>'
);
window._channelStatData = data;
});
}
$('#year-select').on('change', function () {
var y = $(this).val();
if (!y) return;
httpGet('channel/stats.php', { year: y }).then(function (data) {
$('#stat-summary').html(
'<div class="stat-card c-blue"><div class="num">' + data.summary.channel_count + '</div><div class="label">' + data.year + '年渠道数</div></div>' +
'<div class="stat-card c-green"><div class="num">' + data.summary.avg_score + '</div><div class="label">平均效率评分</div></div>' +
'<div class="stat-card c-orange"><div class="num">' + data.summary.max_score + '</div><div class="label">最高评分</div></div>' +
'<div class="stat-card c-red"><div class="num">' + data.summary.min_score + '</div><div class="label">最低评分</div></div>'
);
window._channelStatData = data;
});
});
/* ---------- 新增/编辑 ---------- */
$('#btn-add').on('click', function () { openForm(null); });
window.editChannel = function (id) {
var row = null;
pageRows.forEach(function (r) { if (r.id == id) row = r; });
openForm(row);
};
function openForm(ch) {
var isEdit = !!ch;
var c = ch || {};
var content =
'<form id="channel-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>渠道名称<span class="req">*</span></label><input type="text" name="channel_name" value="' + escHtml(c.channel_name) + '"></div>' +
' <div class="form-item"><label>渠道类型</label><input type="text" name="channel_type" value="' + escHtml(c.channel_type) + '" placeholder="线上/线下/展会/转介绍"></div>' +
' <div class="form-item"><label>联系人</label><input type="text" name="contact_person" value="' + escHtml(c.contact_person) + '"></div>' +
' <div class="form-item"><label>联系电话</label><input type="text" name="contact_phone" value="' + escHtml(c.contact_phone) + '"></div>' +
' <div class="form-item"><label>联系邮箱</label><input type="text" name="contact_email" value="' + escHtml(c.contact_email) + '"></div>' +
' <div class="form-item"><label>效率评分(0-100)</label><input type="number" name="efficiency_score" min="0" max="100" step="0.01" value="' + escHtml(c.efficiency_score) + '"></div>' +
' <div class="form-item full"><label>备注</label><textarea name="remark">' + escHtml(c.remark) + '</textarea></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑渠道' : '新增渠道',
area: ['680px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#channel-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, textarea').each(function () {
var name = $(this).attr('name');
if (!name) return;
data[name] = $(this).val();
});
if (!data.channel_name) { Dialog.error('渠道名称为必填项'); return; }
if (isEdit) data.id = c.id;
httpPost(isEdit ? 'channel/save.php' : 'channel/save.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
loadStats();
});
});
});
}
window.delChannel = function (id) {
Dialog.confirm('确定删除该渠道吗?', function () {
httpPost('channel/delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () {
loadList(currentPage);
loadStats();
});
});
});
};
});
+415
View File
@@ -0,0 +1,415 @@
/**
* common.js - 通用函数(httpGet/httpPost/弹窗/分页/日期格式化/菜单渲染/侧边栏/用户菜单)
*/
/* ============ HTTP 封装 ============ */
/**
* GET 请求(返回 Promise<data>)
*/
function httpGet(url, params) {
var full = BASE_URL + url;
if (params) {
var qs = $.param(params);
full += (full.indexOf('?') >= 0 ? '&' : '?') + qs;
}
return $.ajax({
url: full,
method: 'GET',
dataType: 'json'
}).then(handleResp, handleError);
}
/**
* POST 请求(写类操作自动携带 X-Requested-With 头,返回 Promise<data>)
*/
function httpPost(url, data, isFormData) {
var opts = {
url: BASE_URL + url,
method: 'POST',
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
};
if (isFormData) {
opts.data = data;
opts.processData = false;
opts.contentType = false;
} else {
opts.data = data;
}
return $.ajax(opts).then(handleResp, handleError);
}
/** 统一处理响应:code!=0 弹错并 reject;401 立即回登录页 */
function handleResp(resp) {
if (resp && resp.code === 0) {
return resp.data;
}
if (resp && resp.code === 401) {
location.href = 'index.html';
return $.Deferred().reject(resp).promise();
}
var msg = (resp && resp.msg) ? resp.msg : '请求失败';
layer.msg(msg, { icon: 2 });
return $.Deferred().reject(resp).promise();
}
function handleError(xhr) {
if (xhr.status === 401) {
location.href = 'index.html';
return $.Deferred().reject(xhr).promise();
}
var msg = '网络错误';
try {
var resp = xhr.responseJSON;
if (resp && resp.msg) msg = resp.msg;
} catch (e) {}
layer.msg(msg, { icon: 2 });
return $.Deferred().reject(xhr).promise();
}
/* ============ 弹窗封装(基于 layer) ============ */
var Dialog = {
alert: function (msg, cb) { layer.alert(msg, { icon: 0 }, cb || function () {}); },
success: function (msg, cb) { layer.msg(msg, { icon: 1, time: 1200 }, cb || function () {}); },
error: function (msg) { layer.msg(msg, { icon: 2 }); },
confirm: function (msg, cb) {
layer.confirm(msg, { icon: 3, title: '确认操作' }, function (idx) {
layer.close(idx);
cb && cb();
});
},
open: function (opts) {
return layer.open({
type: 1,
title: opts.title || '操作',
area: opts.area || ['720px', 'auto'],
content: opts.content,
btn: opts.btn !== false ? ['确定', '取消'] : null,
yes: opts.yes || null,
end: opts.end || null
});
},
close: function (idx) { layer.close(idx); }
};
/* ============ 日期格式化 ============ */
function fmtDate(str, withTime) {
if (!str) return '-';
var d = new Date(str.replace(/-/g, '/'));
if (isNaN(d.getTime())) return str;
var p = function (n) { return n < 10 ? '0' + n : '' + n; };
var s = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
if (withTime) s += ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
return s;
}
function escHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/* ============ 会话与权限 ============ */
/**
* 校验当前页面权限,无权限则提示并跳回首页
* @param {string} key 菜单标识
* @returns {boolean} 是否有权限
*/
function checkPagePermission(key) {
var perms = (window.CURRENT_USER && window.CURRENT_USER.permissions) || [];
if (perms.indexOf(key) < 0) {
Dialog.alert('您没有访问该模块的权限', function () {
location.href = 'dashboard.html';
});
return false;
}
return true;
}
/**
* 页面初始化:校验登录态 + 渲染左侧菜单 + 填充右上角用户信息
* 注意:成功回调内包裹 try/catch——渲染异常只记日志,不触发登录页跳转;
* .fail 仅在会话真实失效(401)时才跳转登录页。
*/
function initPage(activeKey, cb) {
httpGet('auth/session.php').then(function (user) {
window.CURRENT_USER = user;
try {
renderMenu(activeKey, user.permissions || []);
// 右上角用户菜单
var name = user.real_name || user.username;
$('#um-name').text(name);
$('#um-detail-name').text(name);
$('#um-detail-account').text('@' + user.username);
$('#um-avatar').text(name.charAt(0).toUpperCase());
$('#um-role').text(user.role_name || '-');
$('#um-status').text('已登录').removeClass('um-off').addClass('um-ok');
$('#um-logout').show();
$('#um-login').hide();
cb && cb(user);
} catch (e) {
console.error('initPage render error:', e);
}
}).fail(function () {
// 会话失效/未登录:立即回登录页,不允许停留在当前页面继续操作
try {
var a = arguments[0];
var desc;
if (a && a.responseText) desc = 'HTTP/' + a.status + ' respText=' + a.responseText.substring(0, 300);
else if (a instanceof Error) desc = 'Error: ' + a.message;
else if (a && a.readyState !== undefined) desc = 'jqXHR status=' + a.status + ' statusText=' + a.statusText;
else desc = 'arg0=' + String(a);
console.error('initPage session check failed:', desc);
} catch (e) {
console.error('initPage fail (unserializable)', String(e));
}
location.href = 'index.html';
});
}
/* ============ 侧边栏渲染 ============ */
/**
* 渲染左侧导航菜单(分级,扁平图标;二级菜单不带图标)
* - 分组:拥有任意子项权限即显示;子项逐个按权限过滤
* - 高亮:子项按“当前页面文件名”匹配(避免营销服务共用权限时全部选中);一级项按 key/url
*/
function renderMenu(activeKey, permissions) {
var currentPage = location.pathname.split('/').pop();
var html = '';
MENU_GROUPS.forEach(function (g) {
if (g.children) {
// 过滤出当前用户有权限的子项
var visibleChildren = g.children.filter(function (c) {
return permissions.indexOf(c.key) >= 0;
});
if (!visibleChildren.length) return;
var isOpen = visibleChildren.some(function (c) {
return c.url === currentPage;
});
html += '<div class="menu-group' + (isOpen ? ' open' : '') + '">' +
'<div class="menu-group-title" onclick="toggleMenuGroup(this)">' +
'<span class="icon">' + (ICONS[g.icon] || '') + '</span>' +
'<span class="label">' + g.name + '</span>' +
'<span class="arrow">▸</span></div>' +
'<div class="menu-group-body"' + (isOpen ? ' style="display:block;"' : '') + '>';
visibleChildren.forEach(function (c) {
var isActive = (c.url === currentPage);
html += '<a class="menu-item sub' + (isActive ? ' active' : '') + '" href="' + c.url + '">' +
'<span class="label">' + c.name + '</span></a>';
});
html += '</div></div>';
} else {
if (permissions.indexOf(g.key) < 0) return;
var isActive = (g.key === activeKey || g.url === currentPage);
html += '<a class="menu-item' + (isActive ? ' active' : '') + '" href="' + g.url + '">' +
'<span class="icon">' + (ICONS[g.icon] || '') + '</span>' +
'<span class="label">' + g.name + '</span></a>';
}
});
$('#sidebar-menu').html(html);
}
/** 展开/收起分组菜单(折叠态点击分组时先展开侧边栏) */
window.toggleMenuGroup = function (el) {
var $layout = $('.layout');
if ($layout.hasClass('sidebar-collapsed')) {
$layout.removeClass('sidebar-collapsed');
if (!window.matchMedia('(max-width: 768px)').matches) {
localStorage.setItem('sl_sidebar_collapsed', '0');
}
}
var $group = $(el).closest('.menu-group');
var $body = $group.find('.menu-group-body');
var isOpen = $group.hasClass('open');
if (isOpen) {
$group.removeClass('open');
$body.slideUp(150);
} else {
$group.addClass('open');
$body.slideDown(150);
}
};
/** 手动折叠/展开侧边栏 */
window.toggleSidebar = function () {
var $layout = $('.layout');
$layout.toggleClass('sidebar-collapsed');
var collapsed = $layout.hasClass('sidebar-collapsed');
if (!window.matchMedia('(max-width: 768px)').matches) {
localStorage.setItem('sl_sidebar_collapsed', collapsed ? '1' : '0');
}
};
/** 初始化侧边栏状态:桌面读用户偏好;移动端(<768px)自动折叠 */
function initSidebar() {
var mq = window.matchMedia('(max-width: 768px)');
function apply() {
var $layout = $('.layout');
if (mq.matches) {
$layout.addClass('sidebar-collapsed');
} else if (localStorage.getItem('sl_sidebar_collapsed') === '1') {
$layout.addClass('sidebar-collapsed');
} else {
$layout.removeClass('sidebar-collapsed');
}
}
apply();
if (mq.addEventListener) mq.addEventListener('change', apply);
else if (mq.addListener) mq.addListener(apply);
}
/* ============ 页面通用骨架(侧边栏+顶栏+右上角用户菜单+页脚) ============ */
function renderShell(pageTitle, breadcrumb) {
var html =
'<div class="layout">' +
' <aside class="sidebar">' +
' <a class="logo" href="dashboard.html" title="返回综合概览">Super<span>Link</span><span class="version">' + (window.APP_VERSION || 'v1.0.00') + '</span></a>' +
' <nav class="menu" id="sidebar-menu"></nav>' +
' </aside>' +
' <div class="main">' +
' <div class="topbar">' +
' <span class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()" title="折叠/展开菜单">' + (ICONS.menu || '') + '</span>' +
' <div class="page-title">' + pageTitle + '</div>' +
(breadcrumb ? '<div class="breadcrumb">' + breadcrumb + '</div>' : '') +
' <div class="topbar-spacer"></div>' +
' <div class="user-menu" id="user-menu">' +
' <span class="user-menu-trigger">' +
' <span class="avatar" id="um-avatar">U</span>' +
' <span id="um-name">加载中...</span>' +
' <span class="caret">▾</span>' +
' </span>' +
' <div class="user-menu-dropdown">' +
' <div class="um-head">' +
' <div class="um-name" id="um-detail-name"></div>' +
' <div class="um-sub" id="um-detail-account"></div>' +
' </div>' +
' <div class="um-item"><span class="um-label">角色</span><b id="um-role">-</b></div>' +
' <div class="um-item"><span class="um-label">登录状态</span><b id="um-status">-</b></div>' +
' <div class="um-divider"></div>' +
' <a class="um-action" id="um-login" style="display:none;">登 录</a>' +
' <a class="um-action um-danger" id="um-logout" style="display:none;">退出登录</a>' +
' </div>' +
' </div>' +
' </div>' +
' <div class="content" id="page-content"></div>' +
' <footer class="footer">' + (window.FOOTER_TEXT || '© SuperLink 管理系统') + '</footer>' +
' </div>' +
'</div>';
document.body.innerHTML = html;
initSidebar();
// 右上角用户菜单:悬停显示(延迟关闭,保证能移入下拉点击)+ 点击切换(触屏)
var menuTimer = null;
$('#user-menu').on('mouseenter', function () {
clearTimeout(menuTimer);
$(this).addClass('open');
});
$('#user-menu').on('mouseleave', function () {
var self = this;
clearTimeout(menuTimer);
menuTimer = setTimeout(function () { $(self).removeClass('open'); }, 250);
});
$('#user-menu .user-menu-trigger').on('click', function (e) {
e.stopPropagation();
$('#user-menu').toggleClass('open');
});
$(document).on('click', function () {
$('#user-menu').removeClass('open');
});
$('#um-logout').on('click', function () {
Dialog.confirm('确定要退出登录吗?', function () {
httpPost('auth/logout.php').then(function () {
location.href = 'index.html';
});
});
});
$('#um-login').on('click', function () {
location.href = 'index.html';
});
}
/* ============ 分页组件 ============ */
/**
* 渲染分页条
* @param {object} pager { total, page, limit }
* @param {function} onPage 翻页回调(page)
*/
function renderPagination($el, pager, onPage) {
var total = pager.total || 0;
var page = pager.page || 1;
var limit = pager.limit || PAGE_SIZE;
var pages = Math.max(1, Math.ceil(total / limit));
var html = '<span>共 ' + total + ' 条 / ' + pages + ' 页</span>';
html += '<span class="pg' + (page <= 1 ? ' disabled' : '') + '" data-p="' + (page - 1) + '">上一页</span>';
var start = Math.max(1, page - 2);
var end = Math.min(pages, start + 4);
start = Math.max(1, end - 4);
for (var i = start; i <= end; i++) {
html += '<span class="pg' + (i === page ? ' current' : '') + '" data-p="' + i + '">' + i + '</span>';
}
html += '<span class="pg' + (page >= pages ? ' disabled' : '') + '" data-p="' + (page + 1) + '">下一页</span>';
$el.html(html);
$el.find('span.pg').each(function () {
var $this = $(this);
if ($this.hasClass('disabled')) return;
$this.on('click', function () {
onPage && onPage(parseInt($this.data('p'), 10));
});
});
}
/* ============ 文件下载(导出) ============ */
function download(url, params) {
var full = BASE_URL + url;
if (params) {
full += (full.indexOf('?') >= 0 ? '&' : '?') + $.param(params);
}
location.href = full;
}
/* ============ 通用表格渲染辅助 ============ */
function buildFilterParam() {
var obj = {};
$('.toolbar [data-filter]').each(function () {
var $this = $(this);
var key = $this.data('filter');
var val = $this.val();
if (val !== '' && val !== null && val !== undefined) {
obj[key] = val;
}
});
return obj;
}
/* ============ 文件上传输入 ============ */
function triggerFileInput(accept, cb) {
var input = document.createElement('input');
input.type = 'file';
input.accept = accept || '.csv';
input.style.display = 'none';
input.onchange = function () {
if (input.files.length > 0) {
cb(input.files[0]);
}
document.body.removeChild(input);
};
document.body.appendChild(input);
input.click();
}
+326
View File
@@ -0,0 +1,326 @@
/**
* company.js - 企业数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 导入导出 / 详情)
*/
$(function () {
renderShell('企业数据', '数据管理 / 企业数据');
renderPage();
initPage('company', function (user) {
if (!checkPagePermission('company')) return;
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="公司名/注册号关键词">' +
' <input type="text" data-filter="industry" placeholder="行业(精确匹配)">' +
' <select data-filter="business_role"><option value="">全部业务角色</option>' +
' <option value="manufacturer">生产型</option><option value="integrator">集成商</option>' +
' <option value="distributor">代理商</option><option value="brand_operator">品牌运营</option>' +
' <option value="others">其他</option></select>' +
' <input type="text" data-filter="country" placeholder="国家/地区">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' <button class="btn btn-warning" id="btn-import">导入CSV</button>' +
' <button class="btn" id="btn-export">导出</button>' +
' <button class="btn btn-danger" id="btn-del-batch">批量删除</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>公司名称</th><th>行业</th><th>行业细分</th><th>国家</th><th>业务角色</th><th>注册号</th><th>法人</th><th>上市</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="company-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
/* ---------- 列表 ---------- */
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('company/list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.id + '"></td>' +
'<td>' + r.id + '</td>' +
'<td title="' + escHtml(r.name_zh || r.name_en || '') + '">' + escHtml(r.name_zh || r.name_en || '-') + '</td>' +
'<td>' + escHtml(r.industry || '-') + '</td>' +
'<td>' + escHtml(r.industry_subdivision || '-') + '</td>' +
'<td>' + escHtml(r.country || '-') + '</td>' +
'<td>' + escHtml(r.business_role || '-') + '</td>' +
'<td>' + escHtml(r.registration_number || '-') + '</td>' +
'<td>' + escHtml(r.legal_representative || '-') + '</td>' +
'<td>' + (r.is_listed ? '<span class="tag">上市</span>' : '-') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' +
' <a onclick="viewCompany(' + r.id + ')">详情</a>' +
' <a onclick="viewEmployees(' + r.id + ')">员工</a>' +
' <a onclick="editCompany(' + r.id + ')">编辑</a>' +
' <a class="danger" onclick="delCompany(' + r.id + ')">删除</a>' +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="12" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>';
}
$('#company-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
/* ---------- 筛选 ---------- */
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
/* ---------- 新增/编辑 ---------- */
$('#btn-add').on('click', function () { openForm(null); });
window.editCompany = function (id) {
httpGet('company/detail.php', { id: id }).then(function (d) {
openForm(d.company);
});
};
function openForm(company) {
var isEdit = !!company;
var c = company || {};
var content =
'<form id="company-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>公司名称<span class="req">*</span></label><input type="text" name="name_zh" value="' + escHtml(c.name_zh) + '"></div>' +
' <div class="form-item"><label>英文名称</label><input type="text" name="name_en" value="' + escHtml(c.name_en) + '"></div>' +
' <div class="form-item"><label>业务角色</label><select name="business_role">' + roleOptions(c.business_role) + '</select></div>' +
' <div class="form-item"><label>国家/地区</label><input type="text" name="country" value="' + escHtml(c.country) + '"></div>' +
' <div class="form-item"><label>注册号</label><input type="text" name="registration_number" value="' + escHtml(c.registration_number) + '"></div>' +
' <div class="form-item"><label>注册地址</label><input type="text" name="address" value="' + escHtml(c.address) + '"></div>' +
' <div class="form-item"><label>法定代表人</label><input type="text" name="legal_representative" value="' + escHtml(c.legal_representative) + '"></div>' +
' <div class="form-item"><label>行业</label><input type="text" name="industry" value="' + escHtml(c.industry) + '"></div>' +
' <div class="form-item"><label>行业细分</label><input type="text" name="industry_subdivision" value="' + escHtml(c.industry_subdivision) + '"></div>' +
' <div class="form-item"><label>官网</label><input type="text" name="website" value="' + escHtml(c.website) + '"></div>' +
' <div class="form-item"><label>注册资本(万元)</label><input type="text" name="registered_capital" value="' + escHtml(c.registered_capital) + '"></div>' +
' <div class="form-item"><label>成立日期</label><input type="date" name="established_date" value="' + escHtml(c.established_date) + '"></div>' +
' <div class="form-item"><label>员工数</label><input type="number" name="latest_employee_count" value="' + escHtml(c.latest_employee_count) + '"></div>' +
' <div class="form-item"><label>最新年营收</label><input type="text" name="latest_annual_revenue" value="' + escHtml(c.latest_annual_revenue) + '"></div>' +
' <div class="form-item"><label>是否上市</label><select name="is_listed">' +
' <option value="0"' + (c.is_listed ? '' : ' selected') + '>否</option>' +
' <option value="1"' + (c.is_listed ? ' selected' : '') + '>是</option></select></div>' +
' <div class="form-item"><label>股票代码</label><input type="text" name="stock_code" value="' + escHtml(c.stock_code) + '"></div>' +
' <div class="form-item full"><label>经营范围</label><textarea name="business_scope">' + escHtml(c.business_scope) + '</textarea></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑企业' : '新增企业',
area: ['760px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#company-form').on('submit', function (e) {
e.preventDefault();
var fd = new FormData(this);
var nameZh = fd.get('name_zh');
var nameEn = fd.get('name_en');
if (isEdit) fd.delete('display_name'); // 编辑不允许改显示名称
if (!isEdit && !nameZh && !nameEn) {
Dialog.error('公司名称或英文名称至少填写一项');
return;
}
httpPost(isEdit ? 'company/update.php' : 'company/add.php', fd, true).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
});
});
});
}
function roleOptions(sel) {
var opts = ['', 'manufacturer', 'integrator', 'distributor', 'brand_operator', 'others'];
var labels = { 'manufacturer': '生产型', 'integrator': '集成商', 'distributor': '代理商', 'brand_operator': '品牌运营', 'others': '其他' };
var html = '<option value="">-</option>';
opts.forEach(function (v) {
if (v === '') return;
html += '<option value="' + v + '"' + (sel === v ? ' selected' : '') + '>' + (labels[v] || v) + '</option>';
});
return html;
}
/* ---------- 删除(软删除) ---------- */
window.delCompany = function (id) {
Dialog.confirm('确定删除该企业吗?(软删除,可恢复)', function () {
httpPost('company/delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
};
/* ---------- 批量删除 ---------- */
$('#btn-del-batch').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) { Dialog.error('请先勾选记录'); return; }
Dialog.confirm('确定删除选中的 ' + ids.length + ' 条记录吗?', function () {
httpPost('company/delete.php', { ids: ids.join(',') }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
});
/* ---------- 导入 ---------- */
$('#btn-import').on('click', function () {
triggerFileInput('.csv', function (file) {
var fd = new FormData();
fd.append('file', file);
httpPost('company/import.php', fd, true).then(function (d) {
Dialog.alert('导入结果:成功 ' + d.inserted + ' 条,失败 ' + d.failed + ' 条', function () {
loadList(1);
});
});
});
});
/* ---------- 导出(仅导出勾选的企业,未勾选弹窗提醒) ---------- */
$('#btn-export').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) {
layer.alert('请先选择需要导出的企业数据', { icon: 2, title: '提示' });
return;
}
download('company/export.php', { ids: ids.join(',') });
});
/* ---------- 详情 ---------- */
window.viewCompany = function (id) {
httpGet('company/detail.php', { id: id }).then(function (d) {
var c = d.company;
var html =
'<div style="padding:18px 22px;">' +
' <h3 style="margin-bottom:14px;">' + escHtml(c.name_zh || c.name_en || '企业 #' + c.id) + '</h3>' +
' <div class="detail-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;font-size:13px;">' +
detailRow('公司名称', c.name_zh) + detailRow('英文名称', c.name_en) +
detailRow('业务角色', c.business_role) + detailRow('国家/地区', c.country) +
detailRow('注册号', c.registration_number) + detailRow('地址', c.address) +
detailRow('法定代表人', c.legal_representative) + detailRow('行业', c.industry) +
detailRow('行业细分', c.industry_subdivision) + detailRow('官网', c.website) +
detailRow('注册资本', c.registered_capital) + detailRow('成立日期', c.established_date) +
detailRow('员工数', c.latest_employee_count) + detailRow('年营收', c.latest_annual_revenue) +
detailRow('是否上市', c.is_listed ? '是' : '否') + detailRow('股票代码', c.stock_code) +
'</div>' +
' <h4 style="margin:16px 0 8px;">经营范围</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(c.business_scope || '-') + '</div>' +
' <h4 style="margin:16px 0 8px;">产品品类</h4>' + miniTable(['品类', '描述', '核心'], d.products.map(function (p) { return [p.category_name, p.category_description || '-', p.is_core ? '是' : '否']; })) +
' <h4 style="margin:16px 0 8px;">关联需求</h4>' + miniTable(['联系人', '需求大类', '意向品类', '场景'], d.needs.map(function (n) { return [n.contact_person || '-', n.need_category || '-', n.target_product_category || '-', n.application_scenario || '-']; })) +
' <h4 style="margin:16px 0 8px;">关联联系人</h4>' + miniTable(['姓名', '职位', '联系方式'], d.contacts.map(function (p) { return [p.full_name || '-', p.position || '-', p.contacts || '-']; })) +
'</div>';
Dialog.open({ title: '企业详情', area: ['820px', 'auto'], content: html, btn: ['关闭'] });
});
};
function detailRow(label, val) {
return '<div><b>' + label + ':</b>' + escHtml(val || '-') + '</div>';
}
function miniTable(headers, rows) {
if (!rows.length) return '<div style="font-size:13px;color:#b6bfcc;">暂无数据</div>';
var html = '<table class="grid" style="font-size:12px;">';
html += '<tr>' + headers.map(function (h) { return '<th>' + h + '</th>'; }).join('') + '</tr>';
rows.forEach(function (r) {
html += '<tr>' + r.map(function (v) { return '<td>' + escHtml(v) + '</td>'; }).join('') + '</tr>';
});
return html + '</table>';
}
/* ---------- 员工列表弹窗(按公司查看员工,分页 + 姓名/职级筛选) ---------- */
var empCompanyId = 0;
var empPage = 1;
window.viewEmployees = function (companyId) {
empCompanyId = companyId;
empPage = 1;
var content =
'<div style="padding:16px 18px 6px;">' +
' <div class="toolbar">' +
' <input type="text" id="emp-keyword" placeholder="姓名关键词">' +
' <select id="emp-level"><option value="">全部职级</option></select>' +
' <button class="btn btn-primary btn-sm" id="emp-search">搜索</button>' +
' <button class="btn btn-sm" id="emp-reset">重置</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>姓名</th><th>联系方式</th><th>邮箱</th><th>职级</th><th>部门</th><th>岗位</th><th>入职日期</th><th>是否在职</th></tr></thead>' +
' <tbody id="emp-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="emp-pagination"></div>' +
'</div>';
var idx = Dialog.open({ title: '员工列表', area: ['920px', 'auto'], content: content, btn: ['关闭'] });
window.__empDialogIdx = idx;
loadEmployees(1);
$('#emp-search').on('click', function () { loadEmployees(1); });
$('#emp-reset').on('click', function () {
$('#emp-keyword').val('');
$('#emp-level').val('');
loadEmployees(1);
});
};
function loadEmployees(page) {
empPage = page;
var params = { company_id: empCompanyId, page: page, limit: 5 };
var kw = $.trim($('#emp-keyword').val());
var lv = $('#emp-level').val();
if (kw) params.keyword = kw;
if (lv) params.job_level = lv;
httpGet('company/employees.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + escHtml(r.full_name) + '</td>' +
'<td>' + escHtml(r.phone || '-') + '</td>' +
'<td>' + escHtml(r.email || '-') + '</td>' +
'<td>' + escHtml(r.job_level || '-') + '</td>' +
'<td>' + escHtml(r.department || '-') + '</td>' +
'<td>' + escHtml(r.position || '-') + '</td>' +
'<td>' + escHtml(r.start_date || '-') + '</td>' +
'<td>' + (r.is_current ? '<span class="tag tag-green">在职</span>' : '<span class="tag tag-gray">离职</span>') + '</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="8" class="empty-tip" style="padding:30px 0;">暂无员工</td></tr>';
}
$('#emp-tbody').html(html);
// 职级下拉(仅首次填充)
var $lv = $('#emp-level');
if ($lv.find('option').length <= 1) {
var optHtml = '<option value="">全部职级</option>';
(data.levels || []).forEach(function (l) {
optHtml += '<option value="' + escHtml(l) + '">' + escHtml(l) + '</option>';
});
$lv.html(optHtml);
}
renderPagination($('#emp-pagination'), data, loadEmployees);
});
}
});
+123
View File
@@ -0,0 +1,123 @@
/**
* config.js - 全局配置
*/
var BASE_URL = '/api/';
var PAGE_SIZE = 20;
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
var APP_VERSION = 'v1.0.10';
/** 页脚版权/备案信息(在 config.js 中修改) */
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
/**
* 扁平化图标库(单色 SVG,随文字颜色 currentColor)
* key -> SVG 字符串;菜单/占位图等统一从这里取,不用彩色 emoji
*/
var ICONS = {
dashboard: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
preliminary: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" 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"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="15" y2="16"/></svg>',
data: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
company: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="1"/><path d="M9 22v-4h6v4"/><line x1="8" y1="6" x2="10" y2="6"/><line x1="14" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/></svg>',
person: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
media: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>',
need: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
marketing: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
channel: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
document: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
system: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
log: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" 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"/></svg>',
users: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" 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>',
role: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>',
menu: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" 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>'
};
/** 菜单标识 -> 权限显示信息(角色权限标签等用) */
var MENU_MAP = {
'dashboard': { name: '综合概览', icon: 'dashboard', url: 'dashboard.html' },
'preliminary': { name: '碎片处理', icon: 'preliminary', url: 'preliminary.html' },
'company': { name: '企业数据', icon: 'company', url: 'company.html' },
'person': { name: '人员数据', icon: 'person', url: 'person.html' },
'media': { name: '媒体数据', icon: 'media', url: 'media.html' },
'need': { name: '需求转盘', icon: 'need', url: 'need.html' },
'marketing': { name: '营销服务', icon: 'marketing', url: 'edm.html' },
'channel': { name: '渠道管理', icon: 'channel', url: 'channel.html' },
'document': { name: '文件管理', icon: 'document', url: 'document.html' },
'system': { name: '权限管理', icon: 'system', url: 'user.html' },
'log': { name: '操作日志', icon: 'log', url: 'log.html' }
};
/**
* 左侧导航菜单分组结构(一级/二级)
* - 一级项:直接渲染菜单链接;分组项:children 为二级菜单(二级不显示图标)
* - 子项 key 为其权限标识,多个子项可共用一个权限(如营销服务三个子页)
*/
var MENU_GROUPS = [
{ key: 'dashboard', name: '综合概览', icon: 'dashboard', url: 'dashboard.html' },
{ key: 'preliminary', name: '碎片处理', icon: 'preliminary', url: 'preliminary.html' },
{
key: 'data', name: '数据管理', icon: 'data', children: [
{ key: 'company', name: '企业数据', icon: 'company', url: 'company.html' },
{ key: 'person', name: '人员数据', icon: 'person', url: 'person.html' },
{ key: 'media', name: '媒体数据', icon: 'media', url: 'media.html' }
]
},
{ key: 'need', name: '需求转盘', icon: 'need', url: 'need.html' },
{
key: 'marketing', name: '营销服务', icon: 'marketing', children: [
{ key: 'marketing', name: 'EDM', icon: 'marketing', url: 'edm.html' },
{ key: 'marketing', name: '数据定制', icon: 'marketing', url: 'data_custom.html' },
{ key: 'marketing', name: '媒体批发', icon: 'marketing', url: 'media_wholesale.html' }
]
},
{ key: 'channel', name: '渠道管理', icon: 'channel', url: 'channel.html' },
{ key: 'document', name: '文件管理', icon: 'document', url: 'document.html' },
{
key: 'system', name: '权限管理', icon: 'system', children: [
{ key: 'system', name: '用户管理', icon: 'users', url: 'user.html' },
{ key: 'system', name: '角色管理', icon: 'role', url: 'role.html' }
]
},
{ key: 'log', name: '操作日志', icon: 'log', url: 'log.html' }
];
/**
* 国家/地区 中文 -> 英文(世界地图 ECharts world.js 匹配用)
*/
var CN_TO_EN = {
'中国': 'China', '中国大陆': 'China', '中国香港': 'China', '香港': 'China',
'中国澳门': 'China', '澳门': 'China', '中国台湾': 'China', '台湾': 'China',
'美国': 'United States', '日本': 'Japan', '韩国': 'South Korea', '朝鲜': 'North Korea',
'英国': 'United Kingdom', '法国': 'France', '德国': 'Germany', '意大利': 'Italy',
'西班牙': 'Spain', '葡萄牙': 'Portugal', '荷兰': 'Netherlands', '比利时': 'Belgium',
'瑞士': 'Switzerland', '瑞典': 'Sweden', '挪威': 'Norway', '芬兰': 'Finland',
'丹麦': 'Denmark', '波兰': 'Poland', '俄罗斯': 'Russia', '乌克兰': 'Ukraine',
'土耳其': 'Turkey', '希腊': 'Greece', '奥地利': 'Austria', '爱尔兰': 'Ireland',
'捷克': 'Czech Republic', '匈牙利': 'Hungary', '罗马尼亚': 'Romania', '保加利亚': 'Bulgaria',
'加拿大': 'Canada', '墨西哥': 'Mexico', '巴西': 'Brazil', '阿根廷': 'Argentina',
'智利': 'Chile', '秘鲁': 'Peru', '哥伦比亚': 'Colombia', '委内瑞拉': 'Venezuela',
'印度': 'India', '巴基斯坦': 'Pakistan', '印度尼西亚': 'Indonesia', '马来西亚': 'Malaysia',
'新加坡': 'Singapore', '泰国': 'Thailand', '越南': 'Vietnam', '菲律宾': 'Philippines',
'缅甸': 'Myanmar', '孟加拉国': 'Bangladesh', '斯里兰卡': 'Sri Lanka', '尼泊尔': 'Nepal',
'沙特阿拉伯': 'Saudi Arabia', '阿联酋': 'United Arab Emirates', '卡塔尔': 'Qatar',
'以色列': 'Israel', '伊朗': 'Iran', '伊拉克': 'Iraq', '哈萨克斯坦': 'Kazakhstan',
'澳大利亚': 'Australia', '新西兰': 'New Zealand', '南非': 'South Africa',
'埃及': 'Egypt', '尼日利亚': 'Nigeria', '肯尼亚': 'Kenya', '摩洛哥': 'Morocco',
'阿尔及利亚': 'Algeria', '埃塞俄比亚': 'Ethiopia', '蒙古': 'Mongolia',
'古巴': 'Cuba', '巴拿马': 'Panama', '乌拉圭': 'Uruguay', '厄瓜多尔': 'Ecuador'
};
/** 角色权限可勾选的菜单列表(角色管理弹窗用) */
var PERMISSION_OPTIONS = [
{ key: 'dashboard', name: '综合概览' },
{ key: 'preliminary', name: '碎片处理' },
{ key: 'company', name: '企业数据' },
{ key: 'person', name: '人员数据' },
{ key: 'media', name: '媒体数据' },
{ key: 'need', name: '需求转盘' },
{ key: 'marketing', name: '营销服务' },
{ key: 'channel', name: '渠道管理' },
{ key: 'document', name: '文件管理' },
{ key: 'system', name: '权限管理' },
{ key: 'log', name: '操作日志' }
];
+252
View File
@@ -0,0 +1,252 @@
/**
* dashboard.js - 首页逻辑
* 8个指标卡 + 区域分布(国内=中国地图按省份 / 全球=世界地图按国家,各含企业/人员Tab)
* + 行业分布(企业/人员)+ 需求关键词词云(占位)
*/
$(function () {
renderShell('综合概览', '首页 / 数据看板');
initPage('dashboard', function (user) {
loadStats();
});
// 图表状态(Tab 选中态与数据维度)
var pageData = null;
var regionState = { china: { dim: 'company' }, world: { dim: 'company' } };
var industryState = { dim: 'company' };
/** 数值安全转换:NaN / 空值一律转 0 */
function toNum(v) {
var n = parseInt(v, 10);
return isNaN(n) ? 0 : n;
}
function loadStats() {
httpGet('dashboard/stats.php').then(function (data) {
renderMetricCards(data.metrics || {});
pageData = data;
renderRegionChart('china');
renderRegionChart('world');
renderIndustryChart();
renderWordCloud(data.wordcloud || []);
});
}
/* ============ 8 个指标卡 + 图表容器 ============ */
function renderMetricCards(m) {
var cards = [
{ label: '累计企业', num: toNum(m.company_total), cls: 'c-blue' },
{ label: '累计人员', num: toNum(m.person_total), cls: 'c-green' },
{ label: '昨日新增数据(企业/人员)', num: toNum(m.company_new) + '/' + toNum(m.person_new), cls: 'c-orange' },
{ label: '待处理数据碎片', num: toNum(m.preliminary), cls: 'c-red' },
{ label: '覆盖行业', num: toNum(m.industry_count), cls: 'c-purple' },
{ label: '生产型企业', num: toNum(m.manufacturer), cls: 'c-blue' },
{ label: '集成商/代理商', num: toNum(m.integrator), cls: 'c-cyan' },
{ label: '行业媒体资源', num: toNum(m.media_resource), cls: 'c-teal' }
];
var html = '';
cards.forEach(function (c) {
html += '<div class="stat-card ' + c.cls + '">' +
'<div class="num">' + (c.num === undefined ? '-' : c.num) + '</div>' +
'<div class="label">' + c.label + '</div></div>';
});
$('#page-content').html(
'<div class="stat-cards">' + html + '</div>' +
'<div class="chart-grid">' +
' <div class="chart-box"><div class="chart-title">区域分布(国内)' +
' <span class="tabs" data-group="region-china">' +
' <a class="tab' + (regionState.china.dim === 'company' ? ' active' : '') + '" data-val="company" onclick="switchRegionDim(\'china\',\'company\')">企业</a>' +
' <a class="tab' + (regionState.china.dim === 'person' ? ' active' : '') + '" data-val="person" onclick="switchRegionDim(\'china\',\'person\')">人员</a>' +
' </span></div><div class="chart" id="chart-region-china"></div></div>' +
' <div class="chart-box"><div class="chart-title">区域分布(全球)' +
' <span class="tabs" data-group="region-world">' +
' <a class="tab' + (regionState.world.dim === 'company' ? ' active' : '') + '" data-val="company" onclick="switchRegionDim(\'world\',\'company\')">企业</a>' +
' <a class="tab' + (regionState.world.dim === 'person' ? ' active' : '') + '" data-val="person" onclick="switchRegionDim(\'world\',\'person\')">人员</a>' +
' </span></div><div class="chart" id="chart-region-world"></div></div>' +
' <div class="chart-box"><div class="chart-title">行业分布' +
' <span class="tabs" data-group="industry">' +
' <a class="tab' + (industryState.dim === 'company' ? ' active' : '') + '" data-val="company" onclick="switchIndustryDim(\'company\')">企业</a>' +
' <a class="tab' + (industryState.dim === 'person' ? ' active' : '') + '" data-val="person" onclick="switchIndustryDim(\'person\')">人员</a>' +
' </span></div><div class="chart" id="chart-industry"></div></div>' +
' <div class="chart-box"><div class="chart-title">需求关键词(词云,简化提取,后续接入分词服务)</div><div class="chart" id="chart-wordcloud"></div></div>' +
'</div>'
);
}
/* ============ Tab 切换(内联 onclick,数据属性精确匹配选中态) ============ */
window.switchRegionDim = function (box, dim) {
regionState[box].dim = dim;
syncTabs();
renderRegionChart(box);
};
window.switchIndustryDim = function (dim) {
industryState.dim = dim;
syncTabs();
renderIndustryChart();
};
/** 同步所有 Tab 的选中色块 */
function syncTabs() {
$('.chart-title .tabs').each(function () {
var $tabs = $(this);
var group = $tabs.data('group');
$tabs.find('.tab').each(function () {
var $t = $(this);
var val = $t.data('val');
var active = false;
if (group === 'region-china') active = (regionState.china.dim === val);
else if (group === 'region-world') active = (regionState.world.dim === val);
else if (group === 'industry') active = (industryState.dim === val);
$t.toggleClass('active', active);
});
});
}
/* ============ 区域分布(china=中国地图按省份 / world=世界地图按国家) ============ */
function renderRegionChart(box) {
var el = document.getElementById('chart-region-' + box);
if (!el || !window.echarts) return;
try {
var dim = regionState[box].dim;
var rows = ((pageData.region || {})[dim] || {})[box] || [];
rows = rows.map(function (r) {
return { name: r.name, value: toNum(r.value) };
});
var chart = echarts.getInstanceByDom(el) || echarts.init(el);
var mapName = box === 'china' ? 'china' : 'world';
var hasMap = !!(echarts.getMap && echarts.getMap(mapName));
// 世界地图:ECharts5 不支持 series.nameMap,需把中文国家名转英文(地图用英文名匹配);中文名保留给 tooltip
if (box === 'world') {
rows = rows.map(function (r) {
var en = CN_TO_EN[r.name] || r.name;
return { name: en, value: r.value, displayName: r.name };
});
}
if (!hasMap || !rows.length) {
// 地图未加载或无数据时降级为柱状图
if (!rows.length) {
chart.setOption({
title: { text: '暂无' + (dim === 'company' ? '企业' : '人员') + '数据', left: 'center', top: 'middle', textStyle: { color: '#b6bfcc', fontSize: 14 } }
});
} else {
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, top: 30, bottom: 80 },
xAxis: { type: 'category', data: rows.map(function (r) { return r.name; }), axisLabel: { rotate: 45 } },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: rows.map(function (r) { return r.value; }), itemStyle: { color: '#16a085' }, barMaxWidth: 40 }]
});
}
return;
}
var values = rows.map(function (r) { return r.value; });
var maxVal = Math.max.apply(null, values);
var option = {
tooltip: {
trigger: 'item',
formatter: function (p) {
// 无数据区域 p.value 为 NaN/undefined,统一显示 0
var v = p.value;
if (v === undefined || v === null || isNaN(v)) v = 0;
return ((p.data && p.data.displayName) || p.name) + ':' + v;
}
},
// 颜色深浅按数据量(数值越大颜色越深),不显示左下角图例
visualMap: {
show: false,
min: 0,
max: maxVal,
inRange: { color: ['#e8f1ff', '#1e3c72'] }
},
series: [{
type: 'map',
map: mapName,
roam: true,
label: { show: false },
emphasis: {
label: { show: true, formatter: function (p) { return ((p.data && p.data.displayName) || p.name); } },
itemStyle: { areaColor: '#f39c12' }
},
data: rows
}]
};
chart.setOption(option);
window.addEventListener('resize', function () { chart.resize(); });
} catch (e) {
console && console.error('region chart error', e);
}
}
/* ============ 行业分布(柱状图,企业/人员) ============ */
function renderIndustryChart() {
var el = document.getElementById('chart-industry');
if (!el || !window.echarts) return;
try {
var dim = industryState.dim;
var rows = ((pageData.industry || {})[dim] || []).map(function (r) {
return { name: r.name, value: toNum(r.value) };
});
var chart = echarts.getInstanceByDom(el) || echarts.init(el);
if (!rows.length) {
chart.setOption({
title: { text: '暂无' + (dim === 'company' ? '企业' : '人员') + '行业数据', left: 'center', top: 'middle', textStyle: { color: '#b6bfcc', fontSize: 14 } }
});
return;
}
chart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 70, right: 20, top: 30, bottom: 50 },
xAxis: { type: 'category', data: rows.map(function (r) { return r.name; }), axisLabel: { rotate: 30 } },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: rows.map(function (r) { return r.value; }),
itemStyle: { color: '#2a5298', borderRadius: [3, 3, 0, 0] },
barMaxWidth: 40
}]
});
window.addEventListener('resize', function () { chart.resize(); });
} catch (e) {
console && console.error('industry chart error', e);
}
}
/* ============ 需求关键词词云(占位) ============ */
function renderWordCloud(words) {
var el = document.getElementById('chart-wordcloud');
if (!el || !window.echarts) return;
try {
var chart = echarts.getInstanceByDom(el) || echarts.init(el);
if (!words.length) {
chart.setOption({
title: { text: '暂无关键词数据(占位,后续接入分词服务)', left: 'center', top: 'middle', textStyle: { color: '#b6bfcc', fontSize: 14 } }
});
return;
}
var data = words.map(function (w) {
return {
name: w.name,
value: w.value,
symbolSize: Math.min(60, 14 + w.value * 2),
itemStyle: { color: 'hsl(' + (w.name.length * 37 % 360) + ',65%,55%)' }
};
});
chart.setOption({
tooltip: { formatter: function (p) { return p.name + ':' + toNum(p.value) + '次'; } },
series: [{
type: 'scatter',
data: data,
label: { show: true, formatter: function (p) { return p.name; }, fontSize: 12, color: '#333' },
symbol: 'circle'
}]
});
window.addEventListener('resize', function () { chart.resize(); });
} catch (e) {
console && console.error('wordcloud error', e);
}
}
});
+104
View File
@@ -0,0 +1,104 @@
/**
* document.js - 文件管理逻辑(表格展示 / 上传 / 删除)
*/
$(function () {
renderShell('文件管理', '系统工具 / 文件管理');
renderPage();
initPage('document', function (user) {
if (!checkPagePermission('document')) return;
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="文档名称关键词">' +
' <input type="text" data-filter="file_type" placeholder="文件类型(如 application/pdf)">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-upload">上传文件</button>' +
' <button class="btn btn-danger" id="btn-del-batch">批量删除</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>文档名称</th><th>文件类型</th><th>文件</th><th>版本</th><th>发布日期</th><th>上传时间</th><th>操作</th></tr></thead>' +
' <tbody id="doc-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('document/list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
var isLocal = (r.storage_path || '').indexOf('static/uploads/') === 0;
var link = isLocal ? r.storage_path : (r.storage_path || '#');
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.id + '"></td>' +
'<td>' + r.id + '</td>' +
'<td title="' + escHtml(r.doc_name) + '">' + escHtml(r.doc_name) + '</td>' +
'<td>' + escHtml(r.file_type || '-') + '</td>' +
'<td><a href="' + escHtml(link) + '" target="_blank" class="btn btn-sm">查看/下载</a></td>' +
'<td>' + (r.is_current ? '<span class="tag tag-green">当前版本</span>' : '<span class="tag tag-gray">历史</span>') + '</td>' +
'<td>' + escHtml(r.publish_date || '-') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops"><a class="danger" onclick="delDoc(' + r.id + ')">删除</a></td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="9" class="empty-tip" style="padding:40px 0;">暂无文件</td></tr>';
}
$('#doc-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
/* ---------- 上传 ---------- */
$('#btn-upload').on('click', function () {
triggerFileInput('.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.png,.jpg,.jpeg,.gif,.zip,.rar', function (file) {
var fd = new FormData();
fd.append('file', file);
httpPost('document/upload.php', fd, true).then(function () {
Dialog.success('上传成功', function () { loadList(1); });
});
});
});
/* ---------- 删除(同时删除物理文件) ---------- */
window.delDoc = function (id) {
Dialog.confirm('确定删除该文件吗?将同时删除物理文件,不可恢复!', function () {
httpPost('document/delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
};
$('#btn-del-batch').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) { Dialog.error('请先勾选记录'); return; }
Dialog.confirm('确定删除选中的 ' + ids.length + ' 个文件吗?将同时删除物理文件,不可恢复!', function () {
httpPost('document/delete.php', { ids: ids.join(',') }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
});
});
+97
View File
@@ -0,0 +1,97 @@
/**
* login.js - 登录页逻辑(滑块验证 + 表单提交 + 找回密码)
*/
$(function () {
var sliderToken = null;
var slider = new SliderVerify('#slider', function (token) {
sliderToken = token;
$('#login-btn').prop('disabled', false);
});
// 回车提交
$('#password').on('keydown', function (e) {
if (e.keyCode === 13) doLogin();
});
$('#login-btn').on('click', doLogin);
function doLogin() {
var username = $.trim($('#username').val());
var password = $('#password').val();
if (!username) { showMsg('请输入账号', 'error'); return; }
if (!password) { showMsg('请输入密码', 'error'); return; }
if (!sliderToken) { showMsg('请先完成滑块验证', 'error'); return; }
$.ajax({
url: BASE_URL + 'auth/login.php',
method: 'POST',
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
data: { username: username, password: password, slider_token: sliderToken }
}).done(function (resp) {
if (resp.code === 0) {
showMsg('登录成功,正在跳转...', 'success');
setTimeout(function () {
location.href = 'dashboard.html';
}, 500);
} else {
showMsg(resp.msg || '登录失败', 'error');
slider.reset();
sliderToken = null;
$('#login-btn').prop('disabled', true);
}
}).fail(function () {
showMsg('网络错误,请重试', 'error');
});
}
function showMsg(msg, type) {
$('#login-msg').attr('class', 'login-msg ' + type).text(msg);
}
// 找回密码弹窗
$('#btn-forgot').on('click', function () {
var idx = layer.open({
type: 1,
title: '找回密码',
area: ['380px', 'auto'],
content:
'<div style="padding:20px 24px;">' +
' <div class="form-item" style="margin-bottom:14px;">' +
' <label>登录账号</label>' +
' <input type="text" id="f-username" style="width:100%;height:38px;border:1px solid #d9dee8;border-radius:4px;padding:0 10px;" placeholder="请输入登录账号">' +
' </div>' +
' <div class="form-item" style="margin-bottom:14px;">' +
' <label>联系邮箱</label>' +
' <input type="text" id="f-email" style="width:100%;height:38px;border:1px solid #d9dee8;border-radius:4px;padding:0 10px;" placeholder="请输入注册邮箱">' +
' </div>' +
' <div id="f-msg" style="display:none;padding:8px 10px;border-radius:4px;font-size:13px;margin-bottom:10px;"></div>' +
'</div>',
btn: ['提交', '取消'],
yes: function (idx) {
var username = $.trim($('#f-username').val());
var email = $.trim($('#f-email').val());
if (!username || !email) {
$('#f-msg').attr('style', 'display:block;padding:8px 10px;border-radius:4px;font-size:13px;margin-bottom:10px;background:#fdecea;color:#c0392b;border:1px solid #f5c6cb;').text('请填写账号和邮箱');
return;
}
$.ajax({
url: BASE_URL + 'auth/forgot.php',
method: 'POST',
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
data: { username: username, contact_email: email }
}).done(function (resp) {
var color = resp.code === 0 ? '#27ae60;background:#eafaf1;border:1px solid #a9dfbf' : '#c0392b;background:#fdecea;border:1px solid #f5c6cb';
$('#f-msg').attr('style', 'display:block;padding:8px 10px;border-radius:4px;font-size:13px;margin-bottom:10px;color:' + color).text(resp.msg || '操作完成');
if (resp.code === 0) {
setTimeout(function () { layer.close(idx); }, 1500);
}
}).fail(function () {
$('#f-msg').attr('style', 'display:block;padding:8px 10px;border-radius:4px;font-size:13px;margin-bottom:10px;color:#c0392b;background:#fdecea;border:1px solid #f5c6cb;').text('网络错误,请重试');
});
}
});
});
});
+197
View File
@@ -0,0 +1,197 @@
/**
* marketing.js - 营销服务逻辑(EDM行业+区域二级联动筛选+导出邮箱列表;数据定制/媒体批发占位)
* 页面:edm.html / data_custom.html / media_wholesale.html
*/
$(function () {
// 根据当前页面文件名决定初始化方式
var pageFile = location.pathname.split('/').pop();
if (pageFile === 'data_custom.html') {
initDataCustom();
return;
}
if (pageFile === 'media_wholesale.html') {
initMediaWholesale();
return;
}
/* ============ EDM 页面 ============ */
renderShell('营销服务', '营销服务 / EDM筛选');
renderEdmPage();
initPage('marketing', function (user) {
if (!checkPagePermission('marketing')) return;
loadIndustries();
loadRegions();
loadEmails(1);
});
var currentPage = 1;
/** 渲染 EDM 页面内容(工具栏+表格+分页) */
function renderEdmPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <select data-filter="industry" id="filter-industry"><option value="">全部行业</option></select>' +
' <select data-filter="province" id="filter-province"><option value="">选择省份/州</option></select>' +
' <select data-filter="city" id="filter-city"><option value="">选择城市/区</option></select>' +
' <input type="text" data-filter="keyword" placeholder="姓名/邮箱关键词">' +
' <button class="btn btn-primary" id="btn-search">筛选</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <span style="font-size:13px;color:#8a94a6;">命中 <b id="email-total" style="color:#2a5298;">0</b> 个邮箱</span>' +
' <button class="btn btn-success" id="btn-export">导出CSV</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>邮箱</th><th>姓名</th><th>当前公司</th><th>工作所在地</th><th>家乡</th></tr></thead>' +
' <tbody id="edm-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
function loadIndustries() {
httpGet('marketing/edm.php', { action: 'industries' }).then(function (d) {
var html = '<option value="">全部行业</option>';
(d.industries || []).forEach(function (i) {
html += '<option value="' + escHtml(i) + '">' + escHtml(i) + '</option>';
});
$('#filter-industry').html(html);
});
}
function loadRegions() {
httpGet('marketing/edm.php', { action: 'regions' }).then(function (d) {
var regions = d.regions || {};
var html = '<option value="">选择省份/州</option>';
Object.keys(regions).forEach(function (p) {
html += '<option value="' + escHtml(p) + '">' + escHtml(p) + '</option>';
});
$('#filter-province').html(html).data('regions', regions);
$('#filter-city').html('<option value="">选择城市/区</option>');
});
}
// 二级联动:选省后加载市
$('#filter-province').on('change', function () {
var regions = $(this).data('regions') || {};
var province = $(this).val();
var html = '<option value="">选择城市/区</option>';
(regions[province] || []).forEach(function (c) {
html += '<option value="' + escHtml(c) + '">' + escHtml(c) + '</option>';
});
$('#filter-city').html(html);
});
function loadEmails(page) {
currentPage = page;
var params = buildFilterParam();
params.action = 'filter';
params.page = page;
params.limit = PAGE_SIZE;
httpGet('marketing/edm.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.email + '"></td>' +
'<td>' + escHtml(r.email) + '</td>' +
'<td>' + escHtml(r.full_name || '-') + '</td>' +
'<td>' + escHtml(r.company_name || '-') + '</td>' +
'<td>' + escHtml(r.work_location || '-') + '</td>' +
'<td>' + escHtml(r.hometown || '-') + '</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="6" class="empty-tip" style="padding:40px 0;">暂无匹配邮箱</td></tr>';
}
$('#edm-tbody').html(html);
renderPagination($('#pagination'), data, loadEmails);
$('#email-total').text(data.total || 0);
});
}
$('#btn-search').on('click', function () { loadEmails(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
$('#filter-city').html('<option value="">选择城市/区</option>');
loadEmails(1);
});
/* ---------- 导出CSV(仅导出勾选的邮箱,未勾选弹窗提醒) ---------- */
$('#btn-export').on('click', function () {
var emails = [];
$('.row-check:checked').each(function () { emails.push($(this).val()); });
if (!emails.length) {
layer.alert('请先选择需要导出的邮箱', { icon: 2, title: '提示' });
return;
}
download('marketing/edm_export.php', { emails: emails.join(',') });
});
/* ============ 数据定制(占位) ============ */
function initDataCustom() {
renderShell('数据定制', '营销服务 / 数据定制');
initPage('marketing', function (user) {
if (!checkPagePermission('marketing')) return;
httpGet('marketing/data_custom.php').then(function (d) {
$('#page-content').html(
'<div class="card"><div class="placeholder-box">' +
'<div class="big">' + (ICONS.data || '') + '</div>' +
'<h3>' + escHtml(d.title || '数据定制') + '</h3>' +
'<p>' + escHtml(d.description || '功能建设中') + '</p>' +
'</div>' +
'<div class="form-grid" style="max-width:720px;margin:0 auto;">' +
' <div class="form-item"><label>联系人</label><input type="text" id="dc-name"></div>' +
' <div class="form-item"><label>联系电话</label><input type="text" id="dc-phone"></div>' +
' <div class="form-item full"><label>定制需求说明</label><textarea id="dc-req" style="min-height:90px;"></textarea></div>' +
' <div class="form-item full" style="text-align:right;"><button class="btn btn-primary" id="dc-submit">提交需求(占位)</button></div>' +
'</div></div>'
);
$('#dc-submit').on('click', function () {
httpPost('marketing/data_custom.php', {
action: 'submit',
contact_name: $('#dc-name').val(),
contact_phone: $('#dc-phone').val(),
requirements: $('#dc-req').val()
}).then(function (d) {
Dialog.alert(d.msg || '已提交');
});
});
});
});
}
/* ============ 媒体批发(占位) ============ */
function initMediaWholesale() {
renderShell('媒体批发', '营销服务 / 媒体批发');
initPage('marketing', function (user) {
if (!checkPagePermission('marketing')) return;
httpGet('marketing/media_wholesale.php').then(function (d) {
$('#page-content').html(
'<div class="card"><div class="placeholder-box">' +
'<div class="big">' + (ICONS.media || '') + '</div>' +
'<h3>' + escHtml(d.title || '媒体批发') + '</h3>' +
'<p>' + escHtml(d.description || '功能建设中') + '</p>' +
'</div>' +
'<div class="form-grid" style="max-width:720px;margin:0 auto;">' +
' <div class="form-item full"><label>媒体账号ID(逗号分隔)</label><input type="text" id="mw-ids" placeholder="如 1,2,3"></div>' +
' <div class="form-item full"><label>需求说明</label><textarea id="mw-req" style="min-height:90px;"></textarea></div>' +
' <div class="form-item full" style="text-align:right;"><button class="btn btn-primary" id="mw-submit">提交询价(占位)</button></div>' +
'</div></div>'
);
$('#mw-submit').on('click', function () {
httpPost('marketing/media_wholesale.php', {
action: 'inquiry',
media_ids: $('#mw-ids').val(),
remark: $('#mw-req').val()
}).then(function (d) {
Dialog.alert(d.msg || '已提交');
});
});
});
});
}
});
+263
View File
@@ -0,0 +1,263 @@
/**
* media.js - 媒体数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 导入导出 / 详情)
*/
$(function () {
renderShell('媒体数据', '数据管理 / 媒体数据');
renderPage();
initPage('media', function (user) {
if (!checkPagePermission('media')) return;
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="账号/归属名称关键词">' +
' <select data-filter="platform" id="filter-platform"><option value="">全部平台</option></select>' +
' <select data-filter="certification_type" id="filter-cert"><option value="">全部认证类型</option></select>' +
' <select data-filter="account_level" id="filter-level"><option value="">全部等级</option></select>' +
' <select data-filter="owner_type"><option value="">全部归属</option>' +
' <option value="person">个人</option><option value="company">企业</option></select>' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' <button class="btn btn-warning" id="btn-import">导入CSV</button>' +
' <button class="btn" id="btn-export">导出</button>' +
' <button class="btn btn-danger" id="btn-del-batch">批量删除</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>归属</th><th>平台</th><th>账号</th><th>认证类型</th><th>等级</th><th>粉丝数</th><th>平均阅读</th><th>内容领域</th><th>操作</th></tr></thead>' +
' <tbody id="media-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('media/list.php', params).then(function (data) {
var rows = data.list || [];
var dict = data.dict || {};
fillDict(dict);
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.id + '"></td>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.owner_name || r.owner_type + '#' + r.owner_id) + '</td>' +
'<td>' + escHtml(r.platform || '-') + '</td>' +
'<td title="' + escHtml(r.account_id) + '">' + escHtml(r.account_id) + '</td>' +
'<td>' + escHtml(r.certification_type || '-') + '</td>' +
'<td>' + escHtml(r.account_level || '-') + '</td>' +
'<td>' + (r.follower_count || 0) + '</td>' +
'<td>' + (r.avg_read_count || 0) + '</td>' +
'<td>' + escHtml(r.content_categories || '-') + '</td>' +
'<td class="ops">' +
' <a onclick="viewMedia(' + r.id + ')">详情</a>' +
' <a onclick="editMedia(' + r.id + ')">编辑</a>' +
' <a class="danger" onclick="delMedia(' + r.id + ')">删除</a>' +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>';
}
$('#media-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
function fillDict(dict) {
if (dict.platforms && dict.platforms.length) {
var html = '<option value="">全部平台</option>';
dict.platforms.forEach(function (p) { html += '<option value="' + escHtml(p) + '">' + escHtml(p) + '</option>'; });
$('#filter-platform').html(html);
}
if (dict.certification_types && dict.certification_types.length) {
var h2 = '<option value="">全部认证类型</option>';
dict.certification_types.forEach(function (p) { h2 += '<option value="' + escHtml(p) + '">' + escHtml(p) + '</option>'; });
$('#filter-cert').html(h2);
}
if (dict.account_levels && dict.account_levels.length) {
var h3 = '<option value="">全部等级</option>';
dict.account_levels.forEach(function (p) { h3 += '<option value="' + escHtml(p) + '">' + escHtml(p) + '</option>'; });
$('#filter-level').html(h3);
}
}
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
$('#btn-add').on('click', function () { openForm(null); });
window.editMedia = function (id) {
httpGet('media/detail.php', { id: id }).then(function (d) {
openForm(d.media);
});
};
function openForm(media) {
var isEdit = !!media;
var m = media || {};
var content =
'<form id="media-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>归属类型<span class="req">*</span></label><select name="owner_type" ' + (isEdit ? 'disabled' : '') + '>' +
' <option value="person"' + (m.owner_type === 'person' ? ' selected' : '') + '>个人</option>' +
' <option value="company"' + (m.owner_type === 'company' ? ' selected' : '') + '>企业</option></select></div>' +
' <div class="form-item"><label>归属ID<span class="req">*</span></label><input type="number" name="owner_id" value="' + escHtml(m.owner_id) + '" ' + (isEdit ? 'disabled' : '') + '></div>' +
' <div class="form-item"><label>平台<span class="req">*</span></label>' +
' <select name="platform">' + platformOpts(m.platform) + '</select></div>' +
' <div class="form-item"><label>账号/号码<span class="req">*</span></label><input type="text" name="account_id" value="' + escHtml(m.account_id) + '"></div>' +
' <div class="form-item"><label>主页链接</label><input type="text" name="profile_url" value="' + escHtml(m.profile_url) + '"></div>' +
' <div class="form-item"><label>默认联系方式</label><select name="is_primary">' +
' <option value="0"' + (!m.is_primary ? ' selected' : '') + '>否</option>' +
' <option value="1"' + (m.is_primary ? ' selected' : '') + '>是</option></select></div>' +
' <div class="form-item"><label>账号等级</label><select name="account_level">' + levelOpts(m.account_level) + '</select></div>' +
' <div class="form-item"><label>认证类型</label><select name="certification_type">' + certOpts(m.certification_type) + '</select></div>' +
' <div class="form-item"><label>粉丝数</label><input type="number" name="follower_count" value="' + escHtml(m.follower_count) + '"></div>' +
' <div class="form-item"><label>平均阅读量</label><input type="number" name="avg_read_count" value="' + escHtml(m.avg_read_count) + '"></div>' +
' <div class="form-item full"><label>内容领域(逗号分隔)</label><input type="text" name="content_categories" value="' + escHtml(m.content_categories) + '"></div>' +
' <div class="form-item full"><label>备注</label><textarea name="remark">' + escHtml(m.remark) + '</textarea></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑媒体账号' : '新增媒体账号',
area: ['720px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#media-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select, textarea').each(function () {
var name = $(this).attr('name');
if (!name) return;
if ($(this).attr('type') === 'number') {
data[name] = $(this).val() === '' ? '' : parseInt($(this).val(), 10);
} else {
data[name] = $(this).val();
}
});
if (!data.platform || !data.account_id) { Dialog.error('平台和账号为必填项'); return; }
if (!isEdit && (!data.owner_type || !data.owner_id)) { Dialog.error('归属类型和归属ID为必填项'); return; }
if (isEdit) data.id = m.id;
httpPost(isEdit ? 'media/update.php' : 'media/add.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
});
});
});
}
function platformOpts(sel) {
var list = ['phone', 'email', 'wechat', 'douyin', 'linkedin', 'x', 'facebook', 'instagram', 'tiktok', 'other'];
var html = '';
list.forEach(function (p) {
html += '<option value="' + p + '"' + (sel === p ? ' selected' : '') + '>' + p + '</option>';
});
return html;
}
function levelOpts(sel) {
var list = ['头部', '腰部', '尾部', '达人', '素人', '媒体号'];
var html = '<option value="">-</option>';
list.forEach(function (p) {
html += '<option value="' + p + '"' + (sel === p ? ' selected' : '') + '>' + p + '</option>';
});
return html;
}
function certOpts(sel) {
var list = ['个人认证', '企业认证', '媒体机构认证', '未认证'];
var html = '<option value="">-</option>';
list.forEach(function (p) {
html += '<option value="' + p + '"' + (sel === p ? ' selected' : '') + '>' + p + '</option>';
});
return html;
}
window.delMedia = function (id) {
Dialog.confirm('确定删除该媒体账号吗?(软删除)', function () {
httpPost('media/delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
};
$('#btn-del-batch').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) { Dialog.error('请先勾选记录'); return; }
Dialog.confirm('确定删除选中的 ' + ids.length + ' 条记录吗?', function () {
httpPost('media/delete.php', { ids: ids.join(',') }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
});
$('#btn-import').on('click', function () {
triggerFileInput('.csv', function (file) {
var fd = new FormData();
fd.append('file', file);
httpPost('media/import.php', fd, true).then(function (d) {
Dialog.alert('导入结果:成功 ' + d.inserted + ' 条,失败 ' + d.failed + ' 条', function () {
loadList(1);
});
});
});
});
/* ---------- 导出(仅导出勾选的媒体数据,未勾选弹窗提醒) ---------- */
$('#btn-export').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) {
layer.alert('请先选择需要导出的媒体数据', { icon: 2, title: '提示' });
return;
}
download('media/export.php', { ids: ids.join(',') });
});
window.viewMedia = function (id) {
httpGet('media/detail.php', { id: id }).then(function (d) {
var m = d.media;
var html =
'<div style="padding:18px 22px;">' +
' <h3 style="margin-bottom:14px;">' + escHtml(m.platform) + ':' + escHtml(m.account_id) + '</h3>' +
' <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;font-size:13px;">' +
' <div><b>归属:</b>' + escHtml(m.owner_type === 'person' ? '个人' : '企业') + ' #' + m.owner_id + '</div>' +
' <div><b>主页:</b>' + (m.profile_url ? '<a href="' + escHtml(m.profile_url) + '" target="_blank">打开</a>' : '-') + '</div>' +
' <div><b>等级:</b>' + escHtml(m.account_level || '-') + '</div>' +
' <div><b>认证:</b>' + escHtml(m.certification_type || '-') + '</div>' +
' <div><b>粉丝数:</b>' + (m.follower_count || 0) + '</div>' +
' <div><b>平均阅读:</b>' + (m.avg_read_count || 0) + '</div>' +
' <div><b>内容领域:</b>' + escHtml(m.content_categories || '-') + '</div>' +
' <div><b>创建时间:</b>' + fmtDate(m.created_at) + '</div>' +
' </div>' +
' <h4 style="margin:16px 0 8px;">备注</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(m.remark || '-') + '</div>' +
' <h4 style="margin:16px 0 8px;">特殊要求</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(m.special_requirements || '-') + '</div>' +
' <h4 style="margin:16px 0 8px;">内部评估</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(m.media_remark || '-') + '</div>' +
'</div>';
Dialog.open({ title: '媒体账号详情', area: ['680px', 'auto'], content: html, btn: ['关闭'] });
});
}
});
+190
View File
@@ -0,0 +1,190 @@
/**
* need.js - 需求转盘逻辑(列表+筛选+分页 / 新增编辑 / 推送按钮)
*/
$(function () {
renderShell('需求转盘', '业务模块 / 需求转盘');
renderPage();
initPage('need', function (user) {
if (!checkPagePermission('need')) return;
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="描述/联系人/公司名关键词">' +
' <input type="text" data-filter="company_name" placeholder="公司名称">' +
' <input type="text" data-filter="industry" placeholder="行业(精确)">' +
' <input type="text" data-filter="need_category" placeholder="需求大类">' +
' <input type="date" data-filter="start_date" title="开始日期">' +
' <input type="date" data-filter="end_date" title="结束日期">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' <button class="btn btn-warning" id="btn-push-batch">批量推送</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>公司</th><th>行业</th><th>联系人</th><th>需求大类</th><th>意向品类</th><th>描述</th><th>状态</th><th>录入时间</th><th>操作</th></tr></thead>' +
' <tbody id="need-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('need/list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.id + '"></td>' +
'<td>' + r.id + '</td>' +
'<td title="' + escHtml(r.display_name || '') + '">' + escHtml(r.display_name || '-') + '</td>' +
'<td>' + escHtml(r.industry || '-') + '</td>' +
'<td>' + escHtml(r.contact_person || '-') + '</td>' +
'<td>' + escHtml(r.need_category || '-') + '</td>' +
'<td>' + escHtml(r.target_product_category || '-') + '</td>' +
'<td title="' + escHtml(r.description || '') + '">' + escHtml((r.description || '-').substring(0, 30)) + '</td>' +
'<td>' + (r.is_valid ? '<span class="tag tag-green">有效</span>' : '<span class="tag tag-gray">已作废</span>') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' +
' <a onclick="viewNeed(' + r.id + ')">详情</a>' +
' <a onclick="editNeed(' + r.id + ')">编辑</a>' +
' <a onclick="pushNeed(' + r.id + ')">推送</a>' +
(r.is_valid ? ' <a class="danger" onclick="delNeed(' + r.id + ')">作废</a>' : '') +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>';
}
$('#need-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
$('#btn-add').on('click', function () { openForm(null); });
window.editNeed = function (id) {
httpGet('need/detail.php', { id: id }).then(function (d) {
openForm(d.need);
});
};
function openForm(need) {
var isEdit = !!need;
var n = need || {};
var content =
'<form id="need-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item full"><label>所属公司ID<span class="req">*</span></label><input type="number" name="company_id" value="' + escHtml(n.company_id) + '" ' + (isEdit ? 'disabled' : '') + ' placeholder="公司ID(如 1)"></div>' +
' <div class="form-item"><label>需求联系人</label><input type="text" name="contact_person" value="' + escHtml(n.contact_person) + '"></div>' +
' <div class="form-item"><label>需求大类</label><input type="text" name="need_category" value="' + escHtml(n.need_category) + '" placeholder="设备采购/原材料/咨询服务等"></div>' +
' <div class="form-item"><label>意向产品品类</label><input type="text" name="target_product_category" value="' + escHtml(n.target_product_category) + '"></div>' +
' <div class="form-item"><label>业务场景/应用工序</label><input type="text" name="application_scenario" value="' + escHtml(n.application_scenario) + '" placeholder="如:码垛、焊接"></div>' +
' <div class="form-item full"><label>详细需求描述</label><textarea name="description" style="min-height:100px;">' + escHtml(n.description) + '</textarea></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑需求' : '新增需求',
area: ['680px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#need-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, textarea').each(function () {
var name = $(this).attr('name');
if (!name) return;
if ($(this).attr('type') === 'number') {
data[name] = $(this).val() === '' ? '' : parseInt($(this).val(), 10);
} else {
data[name] = $(this).val();
}
});
if (!isEdit && !data.company_id) { Dialog.error('所属公司ID为必填项'); return; }
if (isEdit) data.id = n.id;
httpPost(isEdit ? 'need/update.php' : 'need/add.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
});
});
});
}
window.delNeed = function (id) {
Dialog.confirm('确定作废该需求吗?', function () {
httpPost('need/delete.php', { id: id }).then(function () {
Dialog.success('操作成功', function () { loadList(currentPage); });
});
});
};
window.pushNeed = function (id) {
Dialog.confirm('确定推送该需求吗?(当前为占位实现)', function () {
httpPost('need/push.php', { id: id }).then(function (d) {
Dialog.alert(d.msg || '推送成功');
});
});
};
$('#btn-push-batch').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) { Dialog.error('请先勾选记录'); return; }
Dialog.confirm('确定推送选中的 ' + ids.length + ' 条需求吗?(占位实现)', function () {
httpPost('need/push.php', { ids: ids.join(',') }).then(function (d) {
Dialog.alert(d.msg || '推送成功');
});
});
});
window.viewNeed = function (id) {
httpGet('need/detail.php', { id: id }).then(function (d) {
var n = d.need;
var html =
'<div style="padding:18px 22px;">' +
' <h3 style="margin-bottom:14px;">需求 #' + n.id + '(' + escHtml(n.display_name || '-') + ')</h3>' +
' <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;font-size:13px;">' +
' <div><b>行业:</b>' + escHtml(n.industry || '-') + '</div>' +
' <div><b>国家:</b>' + escHtml(n.country || '-') + '</div>' +
' <div><b>联系人:</b>' + escHtml(n.contact_person || '-') + '</div>' +
' <div><b>需求大类:</b>' + escHtml(n.need_category || '-') + '</div>' +
' <div><b>意向品类:</b>' + escHtml(n.target_product_category || '-') + '</div>' +
' <div><b>应用场景:</b>' + escHtml(n.application_scenario || '-') + '</div>' +
' <div><b>状态:</b>' + (n.is_valid ? '有效' : '已作废') + '</div>' +
' <div><b>录入时间:</b>' + fmtDate(n.created_at) + '</div>' +
' </div>' +
' <h4 style="margin:16px 0 8px;">详细描述</h4>' +
' <div style="font-size:13px;color:#5a6472;white-space:pre-wrap;">' + escHtml(n.description || '-') + '</div>' +
' <div style="margin-top:16px;text-align:right;"><a href="' + escHtml(n.website || '#') + '" target="_blank" class="btn btn-sm" style="' + (n.website ? '' : 'display:none;') + '">访问官网</a></div>' +
'</div>';
Dialog.open({ title: '需求详情', area: ['680px', 'auto'], content: html, btn: ['关闭'] });
});
}
});
+317
View File
@@ -0,0 +1,317 @@
/**
* person.js - 人员数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 导入导出 / 详情)
*/
$(function () {
renderShell('人员数据', '数据管理 / 人员数据');
renderPage();
initPage('person', function (user) {
if (!checkPagePermission('person')) return;
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="姓名/证件号/手机/邮箱关键词">' +
' <select data-filter="gender"><option value="">全部性别</option>' +
' <option value="男">男</option><option value="女">女</option>' +
' <option value="其他">其他</option><option value="保密">保密</option></select>' +
' <input type="text" data-filter="nationality" placeholder="国籍">' +
' <input type="text" data-filter="work_location" placeholder="工作所在地">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' <button class="btn btn-warning" id="btn-import">导入CSV</button>' +
' <button class="btn" id="btn-export">导出</button>' +
' <button class="btn btn-danger" id="btn-del-batch">批量删除</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>姓名</th><th>性别</th><th>国籍</th><th>工作所在地</th><th>家乡</th><th>学历</th><th>联系方式</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="person-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('person/list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td><input type="checkbox" class="row-check" value="' + r.id + '"></td>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.full_name) + '</td>' +
'<td>' + escHtml(r.gender || '-') + '</td>' +
'<td>' + escHtml(r.nationality || '-') + '</td>' +
'<td>' + escHtml(r.work_location || '-') + '</td>' +
'<td>' + escHtml(r.hometown || '-') + '</td>' +
'<td>' + escHtml(r.education || '-') + '</td>' +
'<td title="' + escHtml(r.contacts || '') + '">' + escHtml((r.contacts || '-').substring(0, 40)) + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' +
' <a onclick="viewPerson(' + r.id + ')">详情</a>' +
' <a onclick="viewCompanies(' + r.id + ')">公司</a>' +
' <a onclick="editPerson(' + r.id + ')">编辑</a>' +
' <a class="danger" onclick="delPerson(' + r.id + ')">删除</a>' +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>';
}
$('#person-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
$('#btn-add').on('click', function () { openForm(null); });
window.editPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) {
openForm(d.person, d.accounts);
});
};
function contactsInputHtml(accounts) {
accounts = accounts || [];
var html = '<div id="contacts-box">';
if (!accounts.length) {
accounts = [{ platform: 'email', account_id: '', is_primary: 1 }];
}
accounts.forEach(function (a, i) {
html += '<div class="contact-row" style="display:flex;gap:6px;margin-bottom:6px;">' +
'<select name="c-platform" style="width:110px;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 6px;">' +
platformOpts(a.platform) + '</select>' +
'<input type="text" name="c-account" placeholder="账号/号码" value="' + escHtml(a.account_id) + '" style="flex:1;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 8px;">' +
'<label style="line-height:32px;font-size:12px;"><input type="checkbox" name="c-primary" ' + (a.is_primary ? 'checked' : '') + '> 默认</label>' +
'<button type="button" class="btn btn-sm" onclick="$(this).parent().remove()">删除</button></div>';
});
return html + '</div>' +
'<button type="button" class="btn btn-sm" id="add-contact">+ 添加联系方式</button>';
}
function platformOpts(sel) {
var list = ['phone', 'email', 'wechat', 'douyin', 'linkedin', 'x', 'facebook', 'instagram', 'tiktok', 'other'];
var html = '';
list.forEach(function (p) {
html += '<option value="' + p + '"' + (sel === p ? ' selected' : '') + '>' + p + '</option>';
});
return html;
}
function openForm(person, accounts) {
var isEdit = !!person;
var p = person || {};
var content =
'<form id="person-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>姓名<span class="req">*</span></label><input type="text" name="full_name" value="' + escHtml(p.full_name) + '"></div>' +
' <div class="form-item"><label>性别</label><select name="gender">' +
' <option value="保密"' + (!p.gender || p.gender === '保密' ? ' selected' : '') + '>保密</option>' +
' <option value="男"' + (p.gender === '男' ? ' selected' : '') + '>男</option>' +
' <option value="女"' + (p.gender === '女' ? ' selected' : '') + '>女</option>' +
' <option value="其他"' + (p.gender === '其他' ? ' selected' : '') + '>其他</option></select></div>' +
' <div class="form-item"><label>国籍</label><input type="text" name="nationality" value="' + escHtml(p.nationality) + '"></div>' +
' <div class="form-item"><label>证件类型</label><input type="text" name="id_type" value="' + escHtml(p.id_type) + '"></div>' +
' <div class="form-item"><label>证件号码</label><input type="text" name="id_number" value="' + escHtml(p.id_number) + '"></div>' +
' <div class="form-item"><label>学历</label><input type="text" name="education" value="' + escHtml(p.education) + '"></div>' +
' <div class="form-item"><label>毕业院校</label><input type="text" name="graduated_from" value="' + escHtml(p.graduated_from) + '"></div>' +
' <div class="form-item"><label>家乡</label><input type="text" name="hometown" value="' + escHtml(p.hometown) + '"></div>' +
' <div class="form-item"><label>工作所在地</label><input type="text" name="work_location" value="' + escHtml(p.work_location) + '"></div>' +
' <div class="form-item full"><label>联系方式</label>' + contactsInputHtml(accounts) + '</div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑人员' : '新增人员',
area: ['720px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#add-contact').on('click', function () {
$('#contacts-box').append(
'<div class="contact-row" style="display:flex;gap:6px;margin-bottom:6px;">' +
'<select name="c-platform" style="width:110px;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 6px;">' + platformOpts('') + '</select>' +
'<input type="text" name="c-account" placeholder="账号/号码" style="flex:1;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 8px;">' +
'<label style="line-height:32px;font-size:12px;"><input type="checkbox" name="c-primary"> 默认</label>' +
'<button type="button" class="btn btn-sm" onclick="$(this).parent().remove()">删除</button></div>'
);
});
$('#person-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select').each(function () {
var name = $(this).attr('name');
if (!name) return;
if (name.indexOf('c-') === 0) return;
if ($(this).attr('type') === 'checkbox') {
data[name] = $(this).prop('checked') ? 1 : 0;
} else {
data[name] = $(this).val();
}
});
if (!data.full_name) { Dialog.error('姓名为必填项'); return; }
// 联系方式
var contacts = [];
$('#contacts-box .contact-row').each(function () {
var $row = $(this);
var account = $.trim($row.find('input[name="c-account"]').val());
var platform = $row.find('select[name="c-platform"]').val();
var primary = $row.find('input[name="c-primary"]').prop('checked') ? 1 : 0;
if (account) {
contacts.push({ platform: platform, account_id: account, is_primary: primary });
}
});
data.contacts = JSON.stringify(contacts);
if (isEdit) data.id = p.id;
httpPost(isEdit ? 'person/update.php' : 'person/add.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
});
});
});
}
window.delPerson = function (id) {
Dialog.confirm('确定删除该人员吗?(软删除,可恢复)', function () {
httpPost('person/delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
};
$('#btn-del-batch').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) { Dialog.error('请先勾选记录'); return; }
Dialog.confirm('确定删除选中的 ' + ids.length + ' 条记录吗?', function () {
httpPost('person/delete.php', { ids: ids.join(',') }).then(function () {
Dialog.success('删除成功', function () { loadList(currentPage); });
});
});
});
$('#btn-import').on('click', function () {
triggerFileInput('.csv', function (file) {
var fd = new FormData();
fd.append('file', file);
httpPost('person/import.php', fd, true).then(function (d) {
Dialog.alert('导入结果:成功 ' + d.inserted + ' 条,失败 ' + d.failed + ' 条', function () {
loadList(1);
});
});
});
});
/* ---------- 导出(仅导出勾选的人员,未勾选弹窗提醒) ---------- */
$('#btn-export').on('click', function () {
var ids = [];
$('.row-check:checked').each(function () { ids.push($(this).val()); });
if (!ids.length) {
layer.alert('请先选择需要导出的人员数据', { icon: 2, title: '提示' });
return;
}
download('person/export.php', { ids: ids.join(',') });
});
window.viewPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) {
var p = d.person;
var accountsHtml = '';
(d.accounts || []).forEach(function (a) {
accountsHtml += '<div style="font-size:13px;padding:3px 0;">' + escHtml(a.platform) + ':' + escHtml(a.account_id) +
(a.is_primary ? ' <span class="tag">默认</span>' : '') + '</div>';
});
var expHtml = '';
(d.experiences || []).forEach(function (e) {
expHtml += '<tr><td>' + escHtml(e.display_name || '-') + '</td><td>' + escHtml(e.position || '-') + '</td>' +
'<td>' + escHtml(e.department || '-') + '</td><td>' + escHtml(e.job_level || '-') + '</td>' +
'<td>' + (e.is_current ? '<span class="tag tag-green">在职</span>' : fmtDate(e.start_date) + ' ~ ' + (e.end_date || '至今')) + '</td></tr>';
});
var html =
'<div style="padding:18px 22px;">' +
' <h3 style="margin-bottom:14px;">' + escHtml(p.full_name) + '</h3>' +
' <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px 24px;font-size:13px;">' +
' <div><b>性别:</b>' + escHtml(p.gender || '-') + '</div>' +
' <div><b>国籍:</b>' + escHtml(p.nationality || '-') + '</div>' +
' <div><b>证件:</b>' + escHtml(p.id_type || '-') + ' / ' + escHtml(p.id_number || '-') + '</div>' +
' <div><b>学历:</b>' + escHtml(p.education || '-') + '</div>' +
' <div><b>毕业院校:</b>' + escHtml(p.graduated_from || '-') + '</div>' +
' <div><b>家乡:</b>' + escHtml(p.hometown || '-') + '</div>' +
' <div><b>工作所在地:</b>' + escHtml(p.work_location || '-') + '</div>' +
' <div><b>union_id:</b>' + escHtml(p.union_id) + '</div>' +
' </div>' +
' <h4 style="margin:16px 0 8px;">联系方式</h4>' + (accountsHtml || '<div style="font-size:13px;color:#b6bfcc;">暂无</div>') +
' <h4 style="margin:16px 0 8px;">工作经历</h4>' +
' <table class="grid" style="font-size:12px;"><tr><th>公司</th><th>职位</th><th>部门</th><th>级别</th><th>时间</th></tr>' +
(expHtml || '<tr><td colspan="5" style="color:#b6bfcc;text-align:center;">暂无</td></tr>') + '</table>' +
'</div>';
Dialog.open({ title: '人员详情', area: ['760px', 'auto'], content: html, btn: ['关闭'] });
});
}
/* ---------- 任职公司弹窗(按人员查看任职公司,分页) ---------- */
var compPersonId = 0;
window.viewCompanies = function (personId) {
compPersonId = personId;
var content =
'<div style="padding:16px 18px 6px;">' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>公司名称</th><th>部门</th><th>岗位</th><th>职级</th><th>入职日期</th><th>是否在岗</th></tr></thead>' +
' <tbody id="comp-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="comp-pagination"></div>' +
'</div>';
Dialog.open({ title: '任职公司', area: ['720px', 'auto'], content: content, btn: ['关闭'] });
loadCompanies(1);
};
function loadCompanies(page) {
var params = { person_id: compPersonId, page: page, limit: 5 };
httpGet('person/companies.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + escHtml(r.company_name) + '</td>' +
'<td>' + escHtml(r.department || '-') + '</td>' +
'<td>' + escHtml(r.position || '-') + '</td>' +
'<td>' + escHtml(r.job_level || '-') + '</td>' +
'<td>' + escHtml(r.start_date || '-') + '</td>' +
'<td>' + (r.is_current ? '<span class="tag tag-green">在岗</span>' : '<span class="tag tag-gray">离岗</span>') + '</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="6" class="empty-tip" style="padding:30px 0;">暂无任职记录</td></tr>';
}
$('#comp-tbody').html(html);
renderPagination($('#comp-pagination'), data, loadCompanies);
});
}
});
+96
View File
@@ -0,0 +1,96 @@
/**
* preliminary.js - 碎片处理逻辑(待定逻辑,暂做基础列表 + 状态统计)
*/
$(function () {
renderShell('碎片处理', '数据管理 / 碎片处理(占位)');
renderPage();
initPage('preliminary', function (user) {
if (!checkPagePermission('preliminary')) return;
loadStats();
loadList(1);
});
var currentPage = 1;
/** 渲染页面内容(统计卡+工具栏+表格+分页) */
function renderPage() {
$('#page-content').html(
'<div class="stat-cards" id="stat-cards"></div>' +
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="公司/姓名/电话/邮箱关键词">' +
' <select data-filter="status"><option value="">全部状态</option>' +
' <option value="待处理">待处理</option><option value="待分配">待分配</option>' +
' <option value="处理中">处理中</option><option value="已转换">已转换</option>' +
' <option value="已废弃">已废弃</option><option value="已拒收">已拒收</option></select>' +
' <select data-filter="source_type"><option value="">全部类型</option>' +
' <option value="company">公司</option><option value="person">人员</option>' +
' <option value="product">产品</option><option value="need">需求</option><option value="mixed">混合</option></select>' +
' <input type="date" data-filter="start_date" title="开始日期">' +
' <input type="date" data-filter="end_date" title="结束日期">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>类型</th><th>公司</th><th>姓名</th><th>电话</th><th>邮箱</th><th>意向品类</th><th>描述</th><th>状态</th><th>跟进人</th><th>录入时间</th></tr></thead>' +
' <tbody id="pre-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
}
function loadStats() {
httpGet('preliminary/stats.php').then(function (d) {
var statusMap = d.status || {};
var html = '';
(d.statuses || []).forEach(function (s) {
var cnt = statusMap[s] || 0;
html += '<div class="stat-card c-' + ['blue', 'orange', 'teal', 'green', 'red', 'gray'][(d.statuses || []).indexOf(s) % 6] + '">' +
'<div class="num">' + cnt + '</div><div class="label">' + escHtml(s) + '</div></div>';
});
$('#stat-cards').html(
'<div class="stat-card c-blue"><div class="num">' + d.total + '</div><div class="label">碎片总数</div></div>' +
'<div class="stat-card c-green"><div class="num">' + d.today + '</div><div class="label">今日新增</div></div>' + html
);
});
}
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('preliminary/list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
var statusCls = { '待处理': 'tag-orange', '待分配': 'tag', '处理中': 'tag', '已转换': 'tag-green', '已废弃': 'tag-gray', '已拒收': 'tag-red' }[r.status] || 'tag';
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.source_type || '-') + '</td>' +
'<td>' + escHtml(r.company_name || '-') + '</td>' +
'<td>' + escHtml(r.person_name || '-') + '</td>' +
'<td>' + escHtml(r.contact_phone || '-') + '</td>' +
'<td>' + escHtml(r.contact_email || '-') + '</td>' +
'<td>' + escHtml(r.target_product_category || '-') + '</td>' +
'<td title="' + escHtml(r.description || '') + '">' + escHtml((r.description || '-').substring(0, 26)) + '</td>' +
'<td><span class="tag ' + statusCls + '">' + escHtml(r.status) + '</span></td>' +
'<td>' + escHtml(r.follow_person || '-') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无数据碎片</td></tr>';
}
$('#pre-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
});
}
$('#btn-search').on('click', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadList(1);
});
});
+416
View File
@@ -0,0 +1,416 @@
/**
* system.js - 系统管理逻辑(用户管理 / 角色管理 / 操作日志)
* 页面:user.html / role.html / log.html
*/
$(function () {
var pageFile = location.pathname.split('/').pop();
if (pageFile === 'user.html') initUser();
else if (pageFile === 'role.html') initRole();
else if (pageFile === 'log.html') initLog();
else renderShell('权限管理', '系统管理');
});
/* ================= 用户管理 ================= */
function initUser() {
renderShell('用户管理', '权限管理 / 用户');
renderUserPage();
initPage('system', function (user) {
if (!checkPagePermission('system')) return;
loadUsers(1);
});
var currentPage = 1;
function renderUserPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="账号/姓名关键词">' +
' <select data-filter="role_id" id="filter-role"><option value="">全部角色</option></select>' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增用户</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>账号</th><th>姓名</th><th>角色</th><th>状态</th><th>最后登录</th><th>登录IP</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="user-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
}
function loadUsers(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('system/user_list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.username) + '</td>' +
'<td>' + escHtml(r.real_name || '-') + '</td>' +
'<td>' + escHtml(r.role_name || '-') + '</td>' +
'<td>' + (r.is_active ? '<span class="tag tag-green">启用</span>' : '<span class="tag tag-red">禁用</span>') + '</td>' +
'<td>' + fmtDate(r.last_login_time) + '</td>' +
'<td>' + escHtml(r.last_login_ip || '-') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' +
' <a onclick="editUser(' + r.id + ')">编辑</a>' +
(r.id != 1 ? ' <a class="danger" onclick="delUser(' + r.id + ')">删除</a>' : '') +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="9" class="empty-tip" style="padding:40px 0;">暂无用户</td></tr>';
}
$('#user-tbody').html(html);
// 角色下拉
var roles = data.roles || [];
var optHtml = '<option value="">全部角色</option>';
roles.forEach(function (r) {
optHtml += '<option value="' + r.id + '">' + escHtml(r.role_name) + '</option>';
});
$('#filter-role').html(optHtml);
renderPagination($('#pagination'), data, loadUsers);
});
}
$('#btn-search').on('click', function () { loadUsers(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadUsers(1);
});
$('#btn-add').on('click', function () { openUserForm(null); });
window.editUser = function (id) {
var row = null;
$('#user-tbody tr').each(function () {
var $tds = $(this).find('td');
if ($tds.length && $tds.eq(0).text() == id) {
row = {
id: id,
username: $tds.eq(1).text(),
real_name: $tds.eq(2).text() === '-' ? '' : $tds.eq(2).text(),
role_name: $tds.eq(3).text(),
is_active: $tds.eq(4).find('.tag').hasClass('tag-green') ? 1 : 0
};
}
});
openUserForm(row);
};
function openUserForm(user) {
var isEdit = !!user;
var u = user || {};
var roleOptionsHtml = '';
$('#filter-role option').each(function () {
var $o = $(this);
if (!$o.val()) return;
roleOptionsHtml += '<option value="' + $o.val() + '"' + (u.role_name === $o.text() ? ' selected' : '') + '>' + escHtml($o.text()) + '</option>';
});
var content =
'<form id="user-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>登录账号' + (isEdit ? '' : '<span class="req">*</span>') + '</label>' +
(isEdit ? '<input type="text" value="' + escHtml(u.username) + '" disabled>' : '<input type="text" name="username" placeholder="3-50位字母/数字/下划线">') + '</div>' +
' <div class="form-item"><label>真实姓名</label><input type="text" name="real_name" value="' + escHtml(u.real_name) + '"></div>' +
' <div class="form-item"><label>登录密码' + (isEdit ? '(留空不修改)' : '<span class="req">*</span>') + '</label><input type="password" name="password" placeholder="至少6位"></div>' +
' <div class="form-item"><label>角色<span class="req">*</span></label><select name="role_id">' + roleOptionsHtml + '</select></div>' +
' <div class="form-item"><label>状态</label><select name="is_active">' +
' <option value="1"' + (u.is_active !== 0 ? ' selected' : '') + '>启用</option>' +
' <option value="0"' + (u.is_active === 0 ? ' selected' : '') + '>禁用</option></select></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑用户' : '新增用户',
area: ['560px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#user-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select').each(function () {
var name = $(this).attr('name');
if (!name) return;
data[name] = $(this).val();
});
if (isEdit) data.id = u.id;
httpPost(isEdit ? 'system/user_update.php' : 'system/user_add.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadUsers(currentPage);
});
});
});
}
window.delUser = function (id) {
Dialog.confirm('确定删除该用户吗?', function () {
httpPost('system/user_delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadUsers(currentPage); });
});
});
};
}
/* ================= 角色管理 ================= */
function initRole() {
renderShell('角色管理', '权限管理 / 角色');
renderRolePage();
initPage('system', function (user) {
if (!checkPagePermission('system')) return;
loadRoles(1);
});
var currentPage = 1;
function renderRolePage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="角色名称关键词">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增角色</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>角色名称</th><th>权限</th><th>用户数</th><th>状态</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="role-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
}
function loadRoles(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('system/role_list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
var permTags = (r.permissions || []).map(function (p) {
var m = MENU_MAP[p];
return '<span class="tag" style="margin:2px 4px 2px 0;">' + (m ? escHtml(m.name) : escHtml(p)) + '</span>';
}).join('');
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.role_name) + '</td>' +
'<td>' + (permTags || '<span class="tag tag-gray">无权限</span>') + '</td>' +
'<td>' + r.user_count + '</td>' +
'<td>' + (r.is_active ? '<span class="tag tag-green">启用</span>' : '<span class="tag tag-red">禁用</span>') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' +
(r.id != 1 ? ' <a onclick="editRole(' + r.id + ')">编辑</a>' : '') +
(r.id != 1 ? ' <a class="danger" onclick="delRole(' + r.id + ')">删除</a>' : '<span style="color:#b6bfcc;">内置</span>') +
'</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="7" class="empty-tip" style="padding:40px 0;">暂无角色</td></tr>';
}
$('#role-tbody').html(html);
renderPagination($('#pagination'), data, loadRoles);
});
}
$('#btn-search').on('click', function () { loadRoles(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadRoles(1);
});
$('#btn-add').on('click', function () { openRoleForm(null); });
window.editRole = function (id) {
var row = null;
$('#role-tbody tr').each(function () {
var $tds = $(this).find('td');
if ($tds.length && $tds.eq(0).text() == id) {
var perms = [];
$tds.eq(2).find('.tag').each(function () { perms.push($(this).text()); });
row = {
id: id,
role_name: $tds.eq(1).text(),
permissions: perms,
is_active: $tds.eq(4).find('.tag').hasClass('tag-green') ? 1 : 0
};
}
});
openRoleForm(row);
};
function openRoleForm(role) {
var isEdit = !!role;
var r = role || {};
var permHtml = '';
PERMISSION_OPTIONS.forEach(function (p) {
var checked = (r.permissions || []).indexOf(p.key) >= 0 ? 'checked' : '';
permHtml += '<label style="display:inline-block;min-width:110px;margin:4px 8px 4px 0;font-size:13px;">' +
'<input type="checkbox" name="perm" value="' + p.key + '" ' + checked + '> ' + p.name + '</label>';
});
var content =
'<form id="role-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>角色名称<span class="req">*</span></label><input type="text" name="role_name" value="' + escHtml(r.role_name) + '"></div>' +
' <div class="form-item"><label>状态</label><select name="is_active">' +
' <option value="1"' + (r.is_active !== 0 ? ' selected' : '') + '>启用</option>' +
' <option value="0"' + (r.is_active === 0 ? ' selected' : '') + '>禁用</option></select></div>' +
' <div class="form-item full"><label>权限分配</label><div style="border:1px solid #eef0f4;border-radius:4px;padding:10px;background:#fafbfd;">' + permHtml + '</div></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑角色' : '新增角色',
area: ['620px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#role-form').on('submit', function (e) {
e.preventDefault();
var perms = [];
$('input[name="perm"]:checked').each(function () { perms.push($(this).val()); });
var data = {
role_name: $('input[name="role_name"]').val(),
is_active: $('select[name="is_active"]').val(),
permissions: JSON.stringify(perms)
};
if (isEdit) data.id = r.id;
httpPost(isEdit ? 'system/role_update.php' : 'system/role_add.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadRoles(currentPage);
});
});
});
}
window.delRole = function (id) {
Dialog.confirm('确定删除该角色吗?', function () {
httpPost('system/role_delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () { loadRoles(currentPage); });
});
});
};
}
/* ================= 操作日志 ================= */
function initLog() {
renderShell('操作日志', '权限管理 / 操作日志(仅超级管理员)');
renderLogPage();
initPage('log', function (user) {
var perms = user.permissions || [];
if (perms.indexOf('log') < 0) {
Dialog.alert('仅超级管理员可查看操作日志', function () {
location.href = 'dashboard.html';
});
return;
}
loadLogs(1);
});
var currentPage = 1;
function renderLogPage() {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="账号/内容/模块关键词">' +
' <select data-filter="action" id="filter-action"><option value="">全部操作</option></select>' +
' <select data-filter="module" id="filter-module"><option value="">全部模块</option></select>' +
' <input type="text" data-filter="username" placeholder="操作人账号">' +
' <input type="date" data-filter="start_date" title="开始日期">' +
' <input type="date" data-filter="end_date" title="结束日期">' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>账号</th><th>操作</th><th>模块</th><th>对象表</th><th>对象ID</th><th>内容</th><th>IP</th><th>时间</th></tr></thead>' +
' <tbody id="log-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
'</div>'
);
}
function loadLogs(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
httpGet('system/log_list.php', params).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
var content = '';
try {
var obj = JSON.parse(r.content || 'null');
content = obj ? JSON.stringify(obj) : '';
} catch (e) {}
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.username) + '</td>' +
'<td><span class="tag">' + escHtml(r.action) + '</span></td>' +
'<td>' + escHtml(r.module) + '</td>' +
'<td>' + escHtml(r.target_table || '-') + '</td>' +
'<td>' + (r.target_id || '-') + '</td>' +
'<td title="' + escHtml(content) + '">' + escHtml((content || '-').substring(0, 30)) + '</td>' +
'<td>' + escHtml(r.ip || '-') + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="9" class="empty-tip" style="padding:40px 0;">近1个月暂无操作日志</td></tr>';
}
$('#log-tbody').html(html);
renderPagination($('#pagination'), data, loadLogs);
// 筛选下拉
var dict = data.dict || {};
if (dict.actions && dict.actions.length) {
var h = '<option value="">全部操作</option>';
dict.actions.forEach(function (a) { h += '<option value="' + escHtml(a) + '">' + escHtml(a) + '</option>'; });
$('#filter-action').html(h);
}
if (dict.modules && dict.modules.length) {
var h2 = '<option value="">全部模块</option>';
dict.modules.forEach(function (m) { h2 += '<option value="' + escHtml(m) + '">' + escHtml(m) + '</option>'; });
$('#filter-module').html(h2);
}
});
}
$('#btn-search').on('click', function () { loadLogs(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadLogs(1);
});
}
File diff suppressed because one or more lines are too long
+45
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

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
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+120
View File
@@ -0,0 +1,120 @@
/**
* slider.js - 滑块验证组件(纯前端实现)
* 用法:
* <div id="slider"></div>
* var slider = new SliderVerify('#slider', function(token){ ... }); // 拖动成功回调
* slider.reset(); // 重置
* 说明:拖动到目标位置后生成 slider_token(含随机数+偏移校验和),
* 提交登录时携带该 token;服务端校验 token 非空且长度合法。
* 如需更高安全性,可后续改为服务端签发 token(接口生成背景图+缺口位置)。
*/
(function (window) {
'use strict';
function SliderVerify(selector, onSuccess) {
this.el = typeof selector === 'string' ? document.querySelector(selector) : selector;
this.onSuccess = onSuccess || function () {};
this.passed = false;
this.init();
}
SliderVerify.prototype.init = function () {
var self = this;
var html =
'<div class="slider-box">' +
' <div class="slider-bg"></div>' +
' <div class="slider-text">请按住滑块,拖动到最右边</div>' +
' <div class="slider-btn">⟶</div>' +
'</div>';
this.el.innerHTML = html;
this.box = this.el.querySelector('.slider-box');
this.bg = this.el.querySelector('.slider-bg');
this.text = this.el.querySelector('.slider-text');
this.btn = this.el.querySelector('.slider-btn');
this.boxWidth = this.box.offsetWidth || 300;
this.dragging = false;
this.startX = 0;
this.offset = 0;
this.btn.addEventListener('mousedown', function (e) { self.startDrag(e); });
this.btn.addEventListener('touchstart', function (e) { self.startDrag(e.touches[0]); }, { passive: true });
document.addEventListener('mousemove', function (e) { self.moveDrag(e); });
document.addEventListener('touchmove', function (e) { self.moveDrag(e.touches[0]); }, { passive: false });
document.addEventListener('mouseup', function () { self.endDrag(); });
document.addEventListener('touchend', function () { self.endDrag(); });
};
SliderVerify.prototype.startDrag = function (e) {
if (this.passed) return;
e.preventDefault && e.preventDefault();
this.dragging = true;
this.startX = e.clientX;
};
SliderVerify.prototype.moveDrag = function (e) {
if (!this.dragging) return;
e.preventDefault && e.preventDefault();
var dx = e.clientX - this.startX;
this.offset = Math.max(0, Math.min(dx, this.boxWidth - 50));
this.btn.style.left = this.offset + 'px';
this.bg.style.width = this.offset + 50 + 'px';
if (this.offset >= this.boxWidth - 60) {
this.endDrag(true);
}
};
SliderVerify.prototype.endDrag = function (force) {
if (!this.dragging) return;
this.dragging = false;
if (force || this.offset >= this.boxWidth - 60) {
// 验证通过
this.passed = true;
this.btn.style.left = (this.boxWidth - 50) + 'px';
this.bg.style.width = '100%';
this.text.innerHTML = '✓ 验证通过';
this.text.className = 'slider-text slider-success';
var token = this.makeToken();
if (this.onSuccess) this.onSuccess(token);
} else {
// 复位
var self = this;
this.btn.style.transition = 'left .3s';
this.bg.style.transition = 'width .3s';
this.btn.style.left = '0px';
this.bg.style.width = '50px';
setTimeout(function () {
self.btn.style.transition = '';
self.bg.style.transition = '';
}, 320);
}
};
/** 生成滑块 token:时间戳+随机数+偏移校验和 */
SliderVerify.prototype.makeToken = function () {
var rand = Math.random().toString(36).substr(2, 8);
var ts = Date.now().toString(36);
var offset = this.offset;
var checksum = (offset * 7 + ts.length * 13).toString(36);
return 'sl_' + ts + '_' + rand + '_' + checksum + '_' + offset;
};
SliderVerify.prototype.reset = function () {
this.passed = false;
this.offset = 0;
this.btn.style.left = '0px';
this.bg.style.width = '50px';
this.text.innerHTML = '请按住滑块,拖动到最右边';
this.text.className = 'slider-text';
};
SliderVerify.prototype.isPassed = function () {
return this.passed;
};
window.SliderVerify = SliderVerify;
})(window);
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
# 目录占位文件(上传文件按 年/月 自动分目录存放)