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);