v1.0.20: 需求迭代(0808文档) - 文件管理迁入竞品资料+企业筛选弹窗(多条件且/或)+搜索框内嵌字段+渠道计划费用字段/状态行内编辑

This commit is contained in:
nanguaboss
2026-08-08 22:54:56 +08:00
parent 1c8b374a72
commit b8c2a2d5c7
14 changed files with 500 additions and 50 deletions
+3 -1
View File
@@ -41,7 +41,7 @@ $total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, channel_type, source_detail, industry, start_date, end_date, remark, status, created_at, updated_at
"SELECT id, channel_type, source_detail, industry, start_date, end_date, remark, status, cost, created_at, updated_at
FROM channel_plans
WHERE $whereSql
ORDER BY id DESC
@@ -58,6 +58,8 @@ foreach ($list as &$row) {
$diff = (int)ceil((strtotime($row['end_date']) - $today) / 86400);
$row['remaining_days'] = max(0, $diff);
}
// cost 转数字(空为 null)
$row['cost'] = ($row['cost'] === null || $row['cost'] === '') ? null : (float)$row['cost'];
}
unset($row);
+58 -21
View File
@@ -1,8 +1,9 @@
<?php
/**
* 渠道新增计划保存接口 POST /api/channel/plan_save.php
* 入参:id(可选,编辑时传)/ channel_type(渠道类别=source_channel 枚举,必填)/
* source_detail / industry / start_date / end_date / remark / status(待启动|已执行|错过)
* 入参:id(可选,编辑时传)/ channel_type(渠道类别=source_channel 枚举,新增时必填)/
* source_detail / industry / start_date / end_date / remark / status(待启动|已执行|错过)/ cost(费用(元))
* 说明:编辑(传 id)时仅更新提交的字段,便于列表行内实时改状态/费用。
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
@@ -13,34 +14,55 @@ require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('channel');
$channelType = trim($_POST['channel_type'] ?? '');
if ($channelType === '') {
Response::error('渠道类别(channel_type)为必填项', 400);
}
$status = trim($_POST['status'] ?? '待启动');
if (!in_array($status, ['待启动', '已执行', '错过'], true)) {
$status = '待启动';
$id = (int)($_POST['id'] ?? 0);
// 新增时渠道类别必填;编辑时不强制(可能只改状态/费用)
if ($id <= 0) {
$channelType = trim($_POST['channel_type'] ?? '');
if ($channelType === '') {
Response::error('渠道类别(channel_type)为必填项', 400);
}
}
$status = trim($_POST['status'] ?? '');
if ($status !== '' && !in_array($status, ['待启动', '已执行', '错过'], true)) {
Response::error('状态反馈取值不合法', 400);
}
$cost = trim($_POST['cost'] ?? '');
if ($cost !== '' && !is_numeric($cost)) {
Response::error('费用(元)必须为数字', 400);
}
$id = (int)($_POST['id'] ?? 0);
$pdo = DB::getInstance()->getPdo();
$fields = [
'channel_type' => $channelType,
'source_detail' => trim($_POST['source_detail'] ?? '') !== '' ? trim($_POST['source_detail']) : null,
'industry' => trim($_POST['industry'] ?? '') !== '' ? trim($_POST['industry']) : null,
'start_date' => trim($_POST['start_date'] ?? '') !== '' ? $_POST['start_date'] : null,
'end_date' => trim($_POST['end_date'] ?? '') !== '' ? $_POST['end_date'] : null,
'remark' => trim($_POST['remark'] ?? '') !== '' ? trim($_POST['remark']) : null,
'status' => $status,
];
/** 从 POST 收集需要写入的字段(空串转 null) */
$fields = [];
$collect = function ($key) use (&$fields) {
if (isset($_POST[$key])) {
$v = trim((string)$_POST[$key]);
$fields[$key] = $v !== '' ? $v : null;
}
};
if ($id > 0) {
$check = $pdo->prepare("SELECT id FROM channel_plans WHERE id = ? AND is_active = 1");
$check = $pdo->prepare("SELECT id, status FROM channel_plans WHERE id = ? AND is_active = 1");
$check->execute([$id]);
if (!$check->fetch()) {
$cur = $check->fetch();
if (!$cur) {
Response::error('计划不存在');
}
// 只更新提交的字段
foreach (['channel_type', 'source_detail', 'industry', 'start_date', 'end_date', 'remark', 'status'] as $k) {
$collect($k);
}
if (isset($_POST['cost'])) {
$c = trim((string)$_POST['cost']);
$fields['cost'] = $c !== '' ? round((float)$c, 2) : null;
}
// 若只改了状态为「已执行」而原费用为空,前端会单独提交 cost;此处不自动置值
if (!$fields) {
Response::error('没有需要更新的字段', 400);
}
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE channel_plans SET $sets WHERE id = ?")->execute($params);
@@ -48,6 +70,21 @@ if ($id > 0) {
Response::success(null, '更新成功');
}
// 新增:渠道类别必填
$channelType = trim($_POST['channel_type'] ?? '');
if ($channelType === '') {
Response::error('渠道类别(channel_type)为必填项', 400);
}
$fields = [
'channel_type' => $channelType,
'source_detail' => strOrNull($_POST['source_detail'] ?? null),
'industry' => strOrNull($_POST['industry'] ?? null),
'start_date' => strOrNull($_POST['start_date'] ?? null),
'end_date' => strOrNull($_POST['end_date'] ?? null),
'remark' => strOrNull($_POST['remark'] ?? null),
'status' => $status !== '' ? $status : '待启动',
'cost' => $cost !== '' ? round((float)$cost, 2) : null,
];
[$sql, $params] = buildInsert($fields);
$pdo->prepare("INSERT INTO channel_plans $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
+17
View File
@@ -45,6 +45,23 @@ function checkPermission($module)
}
}
/**
* 校验当前用户是否拥有多个模块权限中的任意一个(同时校验登录态)
* 用途:功能迁移场景(如文件管理内容迁入竞品资料,document 与 competitor_data 任一权限可访问)
* @param array $modules 菜单标识数组
*/
function checkAnyPermission($modules)
{
requireLogin();
$perms = $_SESSION['permissions'] ?? [];
foreach ((array)$modules as $m) {
if (in_array($m, $perms, true)) {
return;
}
}
Response::error('无权操作', 403);
}
/** 仅超级管理员可操作(如操作日志查询) */
function requireSuperAdmin()
{
+69
View File
@@ -34,6 +34,75 @@ if ($active !== null) {
$where = ['is_active = ?'];
$params = [$active];
}
// 筛选弹窗多字段条件:filters=JSON数组 [{field,op,value}],filter_logic=and|or
// 注意:必须在此处($where/$params 初始化后)追加,否则会被上面的赋值覆盖
$filtersJson = trim($_REQUEST['filters'] ?? '');
$filterLogic = strtoupper(trim($_REQUEST['filter_logic'] ?? 'and')) === 'OR' ? 'OR' : 'AND';
$filterConds = [];
if ($filtersJson !== '') {
$decoded = json_decode($filtersJson, true);
if (is_array($decoded)) {
foreach ($decoded as $c) {
$fField = trim($c['field'] ?? '');
$fOp = trim($c['op'] ?? '');
$fVal = trim((string)($c['value'] ?? ''));
if ($fField === '' || !in_array($fField, $searchableFields, true)) {
continue;
}
switch ($fOp) {
case 'contains':
$filterConds[] = "$fField LIKE ?";
$params[] = "%$fVal%";
break;
case 'eq':
$filterConds[] = "$fField = ?";
$params[] = $fVal;
break;
case 'neq':
$filterConds[] = "$fField <> ?";
$params[] = $fVal;
break;
case 'starts_with':
$filterConds[] = "$fField LIKE ?";
$params[] = "$fVal%";
break;
case 'ends_with':
$filterConds[] = "$fField LIKE ?";
$params[] = "%$fVal";
break;
case 'gt':
$filterConds[] = "$fField > ?";
$params[] = $fVal;
break;
case 'lt':
$filterConds[] = "$fField < ?";
$params[] = $fVal;
break;
case 'gte':
$filterConds[] = "$fField >= ?";
$params[] = $fVal;
break;
case 'lte':
$filterConds[] = "$fField <= ?";
$params[] = $fVal;
break;
case 'is_empty':
$filterConds[] = "($fField IS NULL OR $fField = '')";
break;
case 'is_not_empty':
$filterConds[] = "($fField IS NOT NULL AND $fField <> '')";
break;
default:
break;
}
}
}
}
if ($filterConds) {
$where[] = '(' . implode(" $filterLogic ", $filterConds) . ')';
}
if ($keyword !== '') {
if ($field !== '' && in_array($field, $searchableFields, true)) {
$where[] = "$field LIKE ?";
+1 -1
View File
@@ -9,7 +9,7 @@ require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('document');
checkAnyPermission(['document', 'competitor_data']);
$id = (int)($_POST['id'] ?? 0);
$ids = trim($_POST['ids'] ?? '');
+1 -1
View File
@@ -8,7 +8,7 @@ require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('document');
checkAnyPermission(['document', 'competitor_data']);
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
+1 -1
View File
@@ -11,7 +11,7 @@ require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('document');
checkAnyPermission(['document', 'competitor_data']);
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('请选择要上传的文件', 400);
+2 -3
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
@@ -12,7 +12,6 @@
<script src="static/libs/layer.js"></script>
<script src="static/js/config.js"></script>
<script src="static/js/common.js"></script>
<script src="static/js/placeholder.js"></script>
<script>renderPlaceholder('competitor_data');</script>
<script src="static/js/competitor_data.js"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
-- ============================================================
-- SuperLink Web - v1.0.20 数据库结构变更
-- 需求文档:0808---62b20256-32fc-48ce-9164-4d05b6e83f36.docx
-- 渠道新增计划增加「费用(元)」字段(仅已执行状态可填写)
-- ============================================================
SET NAMES utf8mb4;
ALTER TABLE `channel_plans`
ADD COLUMN `cost` DECIMAL(12,2) NULL DEFAULT NULL COMMENT '费用(元),仅已执行状态可填写' AFTER `status`;
+58
View File
@@ -287,6 +287,64 @@ body {
.toolbar input:focus, .toolbar select:focus { border-color: #4fa3ff; }
.toolbar .spacer { flex: 1; }
/* 搜索框:框内左侧字段选择 + 框内右侧搜索图标 */
.toolbar .search-wrap {
display: inline-flex;
align-items: center;
height: 34px;
border: 1px solid #d9dee8;
border-radius: 4px;
background: #fff;
overflow: hidden;
}
.toolbar .search-wrap:focus-within { border-color: #4fa3ff; }
.toolbar .search-wrap select {
border: none;
height: 100%;
min-width: 108px;
max-width: 150px;
border-right: 1px solid #eef0f4;
border-radius: 0;
background: #f7f9fc;
font-size: 12px;
padding: 0 8px;
}
.toolbar .search-wrap input[type="text"] {
border: none;
height: 100%;
min-width: 220px;
border-radius: 0;
box-shadow: none;
outline: none;
}
.toolbar .search-wrap .search-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 100%;
color: #8a94a6;
cursor: pointer;
background: #fff;
}
.toolbar .search-wrap .search-icon:hover { color: #2a5298; }
/* 筛选弹窗条件行 */
.filter-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
/* 渠道效能分析-来源明细:单行显示,过长用…占位(不分行) */
.ana-detail .src-cell {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ---------- 按钮 ---------- */
.btn {
display: inline-block;
+59 -8
View File
@@ -58,7 +58,6 @@ $(function () {
' <span style="font-size:12px;color:#8a94a6;">根据历史数据有计划地拓展高效渠道</span>' +
' </div>' +
' <div class="toolbar">' +
' <input type="text" data-filter="keyword" placeholder="来源详情/行业/备注关键词">' +
' <select data-filter="channel_type"><option value="">全部渠道类别</option></select>' +
' <select data-filter="status"><option value="">全部状态</option>' +
' <option value="待启动">待启动</option><option value="已执行">已执行</option><option value="错过">错过</option></select>' +
@@ -68,7 +67,7 @@ $(function () {
' <button class="btn btn-success" id="btn-plan-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>' +
' <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="plan-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="plan-pagination"></div>' +
@@ -168,8 +167,7 @@ $(function () {
var html = '';
(r.details || []).forEach(function (item) {
var src = item.source || '';
var short = src.length > 8 ? src.substring(0, 8) + '…' : src;
html += '<tr><td title="' + escHtml(src) + '">' + escHtml(short) + '</td><td>' + (item.count !== undefined ? item.count : item.cnt) + '</td></tr>';
html += '<tr><td class="src-cell" title="' + escHtml(src) + '">' + escHtml(src) + '</td><td>' + (item.count !== undefined ? item.count : item.cnt) + '</td></tr>';
});
if (!(r.details || []).length) {
html = '<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无明细</td></tr>';
@@ -217,6 +215,12 @@ $(function () {
var win = r.start_date && r.end_date
? fmtWin(r.start_date) + ' 至 ' + fmtWin(r.end_date)
: (r.start_date || r.end_date || '-');
var statusSel = '<select class="plan-status" data-id="' + r.id + '" style="height:28px;padding:0 6px;font-size:12px;border:1px solid #d9dee8;border-radius:4px;">' +
'<option value="待启动"' + (r.status === '待启动' ? ' selected' : '') + '>待启动</option>' +
'<option value="已执行"' + (r.status === '已执行' ? ' selected' : '') + '>已执行</option>' +
'<option value="错过"' + (r.status === '错过' ? ' selected' : '') + '>错过</option></select>';
var costInput = '<input type="text" class="plan-cost" data-id="' + r.id + '" value="' + escHtml(r.cost !== null && r.cost !== undefined ? r.cost : '') + '" placeholder="0.00"' +
(r.status === '已执行' ? '' : ' disabled style="background:#f2f4f8;color:#8a94a6;"') + ' style="width:90px;height:28px;padding:0 6px;font-size:12px;border:1px solid #d9dee8;border-radius:4px;text-align:right;">';
html += '<tr>' +
'<td>' + r.id + '</td>' +
'<td>' + escHtml(r.channel_type) + '</td>' +
@@ -225,19 +229,55 @@ $(function () {
'<td>' + escHtml(win) + '</td>' +
'<td>' + (r.remaining_days > 0 ? '<span class="tag' + (r.remaining_days <= 7 ? ' tag-red' : '') + '">' + r.remaining_days + ' 天</span>' : '<span class="tag tag-gray">已到期</span>') + '</td>' +
'<td title="' + escHtml(r.remark || '') + '">' + escHtml((r.remark || '-').substring(0, 20)) + '</td>' +
'<td>' + statusTag(r.status) + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops"><a onclick="editPlan(' + r.id + ')">编辑</a>' +
'<a class="danger" onclick="delPlan(' + r.id + ')">删除</a></td></tr>';
'<td>' + statusSel + '</td>' +
'<td>' + costInput + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td></tr>';
});
if (!rows.length) {
html = '<tr><td colspan="10" class="empty-tip" style="padding:40px 0;">暂无渠道新增计划</td></tr>';
}
$('#plan-tbody').html(html);
bindPlanInlineEdit();
renderPagination($('#plan-pagination'), data, loadPlans);
});
}
/** 行内实时编辑:状态下拉随时改;费用(元)仅在「已执行」时激活可输入 */
function bindPlanInlineEdit() {
$('.plan-status').off('change.plan').on('change.plan', function () {
var id = $(this).data('id');
var status = $(this).val();
var $row = $(this).closest('tr');
var $cost = $row.find('.plan-cost');
if (status === '已执行') {
$cost.prop('disabled', false).css({ background: '#fff', color: '#1e2a3a' });
} else {
$cost.prop('disabled', true).css({ background: '#f2f4f8', color: '#8a94a6' });
}
httpPost('channel/plan_save.php', { id: id, status: status }).then(function () {
Dialog.toast('状态已更新为「' + status + '」');
}).catch(function () {
loadPlans(currentPage);
});
});
$('.plan-cost').off('change.plan').on('change.plan', function () {
var id = $(this).data('id');
var val = $.trim($(this).val());
if (val === '') return;
if (isNaN(parseFloat(val))) {
Dialog.error('费用(元)请输入数字');
return;
}
var cost = Math.round(parseFloat(val) * 100) / 100;
$(this).val(cost);
httpPost('channel/plan_save.php', { id: id, cost: cost }).then(function () {
Dialog.toast('费用已更新:¥' + cost.toFixed(2));
}).catch(function () {
loadPlans(currentPage);
});
});
}
/** 时间窗口显示格式:2025-4-1 */
function fmtWin(d) {
if (!d) return '';
@@ -295,6 +335,8 @@ $(function () {
' <option value="待启动"' + (!r.status || r.status === '待启动' ? ' selected' : '') + '>待启动</option>' +
' <option value="已执行"' + (r.status === '已执行' ? ' selected' : '') + '>已执行</option>' +
' <option value="错过"' + (r.status === '错过' ? ' selected' : '') + '>错过</option></select></div>' +
' <div class="form-item"><label>费用(元)</label><input type="text" name="cost" value="' + escHtml(r.cost !== null && r.cost !== undefined ? r.cost : '') + '" placeholder="仅已执行时可填写"' +
(r.status === '已执行' ? '' : ' disabled style="background:#f2f4f8;color:#8a94a6;"') + '></div>' +
' <div class="form-item"><label>开始日期</label><input type="date" name="start_date" value="' + escHtml(r.start_date || '') + '"></div>' +
' <div class="form-item"><label>结束日期</label><input type="date" name="end_date" value="' + escHtml(r.end_date || '') + '"></div>' +
' <div class="form-item full"><label>备注说明</label><input type="text" name="remark" value="' + escHtml(r.remark || '') + '"></div>' +
@@ -312,6 +354,15 @@ $(function () {
$('#plan-form-cancel').on('click', function () { Dialog.close(idx); });
// 状态反馈 → 费用(元) 联动:仅「已执行」可填写
$('#plan-form select[name="status"]').on('change', function () {
var enabled = $(this).val() === '已执行';
var $cost = $('#plan-form input[name="cost"]');
$cost.prop('disabled', !enabled);
$cost.css({ background: enabled ? '#fff' : '#f2f4f8', color: enabled ? '#1e2a3a' : '#8a94a6' });
if (!enabled) $cost.val('');
});
$('#plan-form').on('submit', function (e) {
e.preventDefault();
var data = {};
+114 -11
View File
@@ -28,17 +28,20 @@ $(function () {
$('#page-content').html(
'<div class="card">' +
' <div class="toolbar">' +
' <select id="search-field"><option value="">全部字段</option>' +
' <option value="name_zh">企业名称</option><option value="name_en">英文名称</option>' +
' <option value="registration_number">注册号</option><option value="legal_representative">法定代表人</option>' +
' <option value="address">注册地址</option><option value="industry">行业</option>' +
' <option value="industry_subdivision">行业细分</option><option value="website">官网</option>' +
' <option value="source_channel">来源渠道</option><option value="source_detail">来源详情</option>' +
' <option value="business_scope">经营范围</option><option value="stock_code">股票代码</option></select>' +
' <input type="text" id="search-keyword" placeholder="输入要检索的内容">' +
' <button class="btn" id="btn-filter" title="按字段设置多个筛选条件(支持 且/或 逻辑)">筛选</button>' +
' <div class="search-wrap">' +
' <select id="search-field"><option value="">全字段</option>' +
' <option value="name_zh">企业名称</option><option value="name_en">英文名称</option>' +
' <option value="registration_number">注册号</option><option value="legal_representative">法定代表人</option>' +
' <option value="address">注册地址</option><option value="industry">行业</option>' +
' <option value="industry_subdivision">行业细分</option><option value="website">官网</option>' +
' <option value="source_channel">来源渠道</option><option value="source_detail">来源详情</option>' +
' <option value="business_scope">经营范围</option><option value="stock_code">股票代码</option></select>' +
' <input type="text" id="search-keyword" placeholder="输入关键词,回车搜索">' +
' <span class="search-icon" id="btn-search" title="搜索"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>' +
' </div>' +
' <select id="search-industry" data-filter="industry"><option value="">全部行业</option></select>' +
' <select id="search-company-type" data-filter="company_type"><option value="">全部企业类型</option></select>' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <select id="search-company-type" data-filter="company_type"><option value="">全部业务类型</option></select>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
@@ -71,6 +74,8 @@ $(function () {
}
/* ---------- 列表 ---------- */
var companyFilters = { logic: 'and', conditions: [] }; // 筛选弹窗条件(多字段 + 且/或)
function loadList(page) {
currentPage = page;
var params = {
@@ -81,6 +86,11 @@ $(function () {
page: page,
limit: PAGE_SIZE
};
// 筛选弹窗条件(多字段 + 逻辑关系)
if (companyFilters.conditions.length) {
params.filters = JSON.stringify(companyFilters.conditions);
params.filter_logic = companyFilters.logic;
}
// 空值不传
Object.keys(params).forEach(function (k) {
if (params[k] === '') delete params[k];
@@ -118,17 +128,110 @@ $(function () {
});
}
/* ---------- 筛选 ---------- */
/* ---------- 检索:搜索(字段范围+关键词) / 筛选弹窗 / 行业+业务类型快速筛选 ---------- */
$('#btn-search').on('click', function () { loadList(1); });
$('#search-keyword').on('keydown', function (e) { if (e.keyCode === 13) loadList(1); });
$('#search-industry, #search-company-type').on('change', function () { loadList(1); });
$('#btn-reset').on('click', function () {
$('#search-field').val('');
$('#search-keyword').val('');
$('#search-industry').val('');
$('#search-company-type').val('');
companyFilters = { logic: 'and', conditions: [] };
loadList(1);
});
/* ---------- 筛选弹窗:多字段条件 + 且/或 逻辑关系 ---------- */
var FILTER_FIELDS = [
['name_zh', '企业名称'], ['name_en', '英文名称'], ['registration_number', '注册号'],
['legal_representative', '法定代表人'], ['address', '注册地址'], ['country', '国家/地区'],
['industry', '行业'], ['industry_subdivision', '行业细分'], ['legal_form', '企业类型'],
['business_role', '业务角色'], ['website', '官网'], ['source_channel', '来源渠道'],
['source_detail', '来源详情'], ['business_scope', '经营范围'], ['stock_code', '股票代码'],
['established_date', '成立日期'], ['latest_employee_count', '最新员工数'],
['latest_annual_revenue', '最新年营收']
];
var FILTER_OPS = [
['contains', '包含'], ['eq', '等于'], ['neq', '不等于'],
['starts_with', '开头是'], ['ends_with', '结尾是'],
['gt', '大于'], ['lt', '小于'], ['gte', '大于等于'], ['lte', '小于等于'],
['is_empty', '为空'], ['is_not_empty', '不为空']
];
function filterFieldOptions(cur) {
var h = '';
FILTER_FIELDS.forEach(function (f) {
h += '<option value="' + f[0] + '"' + (f[0] === cur ? ' selected' : '') + '>' + f[1] + '</option>';
});
return h;
}
function filterOpOptions(cur) {
var h = '';
FILTER_OPS.forEach(function (o) {
h += '<option value="' + o[0] + '"' + (o[0] === cur ? ' selected' : '') + '>' + o[1] + '</option>';
});
return h;
}
function filterRowHtml(c) {
c = c || {};
var valInput = (c.op === 'is_empty' || c.op === 'is_not_empty')
? '<input type="text" value="" disabled placeholder="(无需填值)" style="background:#f2f4f8;color:#8a94a6;width:180px;">'
: '<input type="text" value="' + escHtml(c.value || '') + '" placeholder="条件值" style="width:180px;">';
return '<div class="filter-row">' +
' <select class="f-field" style="width:150px;">' + filterFieldOptions(c.field) + '</select>' +
' <select class="f-op" style="width:110px;">' + filterOpOptions(c.op) + '</select>' +
valInput +
' <button type="button" class="btn btn-sm btn-danger" onclick="$(this).closest(\'.filter-row\').remove()">删除</button>' +
'</div>';
}
window.openFilterDialog = function () {
var logicHtml =
'<label style="margin-right:14px;font-size:13px;"><input type="radio" name="f-logic" value="and"' +
(companyFilters.logic !== 'or' ? ' checked' : '') + '> 且(同时满足)</label>' +
'<label style="font-size:13px;"><input type="radio" name="f-logic" value="or"' +
(companyFilters.logic === 'or' ? ' checked' : '') + '> 或(任一满足)</label>';
var rowsHtml = '';
if (companyFilters.conditions.length) {
companyFilters.conditions.forEach(function (c) { rowsHtml += filterRowHtml(c); });
} else {
rowsHtml = filterRowHtml(null);
}
var content =
'<div style="padding:16px 20px 6px;">' +
' <div style="margin-bottom:10px;font-size:13px;color:#5a6472;">每个字段是一个筛选条件,不同条件之间可为「且」「或」关系:</div>' +
' <div style="margin-bottom:10px;">' + logicHtml + '</div>' +
' <div id="filter-rows">' + rowsHtml + '</div>' +
' <button type="button" class="btn btn-sm" id="btn-add-filter" style="margin-top:8px;">+ 添加条件</button>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="filter-cancel">取消</button>' +
'<button type="button" class="btn btn-primary" id="filter-apply">应用筛选</button></div>';
var idx = Dialog.open({
title: '设置筛选条件',
area: ['620px', 'auto'],
content: content,
btn: false
});
$('#btn-add-filter').on('click', function () { $('#filter-rows').append(filterRowHtml(null)); });
$('#filter-cancel').on('click', function () { Dialog.close(idx); });
$('#filter-apply').on('click', function () {
var logic = $('input[name="f-logic"]:checked').val() || 'and';
var conds = [];
$('#filter-rows .filter-row').each(function () {
var field = $(this).find('.f-field').val();
var op = $(this).find('.f-op').val();
var value = $.trim($(this).find('input[type="text"]').val());
if (!field || !op) return;
if (op !== 'is_empty' && op !== 'is_not_empty' && value === '') return; // 无值且需要值的条件跳过
conds.push({ field: field, op: op, value: value });
});
companyFilters = { logic: logic, conditions: conds };
Dialog.close(idx);
loadList(1);
});
};
$('#btn-filter').on('click', function () { openFilterDialog(); });
/* ---------- 新增/编辑 ---------- */
$('#btn-add').on('click', function () {
httpGet('company/cert_types.php').then(function (d) {
+106
View File
@@ -0,0 +1,106 @@
/**
* competitor_data.js - 竞品资料页(v1.0.20 起承载原「文件管理」内容)
* 需求:左侧导航删掉「文件管理」,其呈现内容迁移到「竞品管理 > 竞品资料」,
* 即「竞品资料」=「文件管理」(表格展示 / 上传 / 删除)。
*/
$(function () {
renderShell('竞品资料', '竞品管理 / 竞品资料');
renderPage();
initPage('competitor_data', function (user) {
if (!checkPagePermission('competitor_data')) 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); });
});
});
});
});
+1 -2
View File
@@ -5,7 +5,7 @@ var BASE_URL = '/api/';
var PAGE_SIZE = 20;
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
var APP_VERSION = 'v1.0.19';
var APP_VERSION = 'v1.0.20';
/** 页脚版权/备案信息(在 config.js 中修改) */
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
@@ -86,7 +86,6 @@ var MENU_GROUPS = [
]
},
{ 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' },