From b8c2a2d5c76b5b27872c465e32f5abd39d928138 Mon Sep 17 00:00:00 2001 From: nanguaboss <602995148@qq.com> Date: Sat, 8 Aug 2026 22:54:56 +0800 Subject: [PATCH] =?UTF-8?q?v1.0.20:=20=E9=9C=80=E6=B1=82=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?(0808=E6=96=87=E6=A1=A3)=20-=20=E6=96=87=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=BF=81=E5=85=A5=E7=AB=9E=E5=93=81=E8=B5=84=E6=96=99?= =?UTF-8?q?+=E4=BC=81=E4=B8=9A=E7=AD=9B=E9=80=89=E5=BC=B9=E7=AA=97(?= =?UTF-8?q?=E5=A4=9A=E6=9D=A1=E4=BB=B6=E4=B8=94/=E6=88=96)+=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=A1=86=E5=86=85=E5=B5=8C=E5=AD=97=E6=AE=B5+?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E8=AE=A1=E5=88=92=E8=B4=B9=E7=94=A8=E5=AD=97?= =?UTF-8?q?=E6=AE=B5/=E7=8A=B6=E6=80=81=E8=A1=8C=E5=86=85=E7=BC=96?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/channel/plan_list.php | 4 +- api/channel/plan_save.php | 79 ++++++++++++++++------ api/common/auth.php | 17 +++++ api/company/list.php | 69 +++++++++++++++++++ api/document/delete.php | 2 +- api/document/list.php | 2 +- api/document/upload.php | 2 +- competitor_data.html | 7 +- sql/migration_v1.0.20.sql | 9 +++ static/css/style.css | 58 ++++++++++++++++ static/js/channel.js | 67 ++++++++++++++++--- static/js/company.js | 125 ++++++++++++++++++++++++++++++++--- static/js/competitor_data.js | 106 +++++++++++++++++++++++++++++ static/js/config.js | 3 +- 14 files changed, 500 insertions(+), 50 deletions(-) create mode 100644 sql/migration_v1.0.20.sql create mode 100644 static/js/competitor_data.js diff --git a/api/channel/plan_list.php b/api/channel/plan_list.php index 9871a7a..161fe6c 100644 --- a/api/channel/plan_list.php +++ b/api/channel/plan_list.php @@ -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); diff --git a/api/channel/plan_save.php b/api/channel/plan_save.php index e32431f..df39fe9 100644 --- a/api/channel/plan_save.php +++ b/api/channel/plan_save.php @@ -1,8 +1,9 @@ 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(); diff --git a/api/common/auth.php b/api/common/auth.php index c35a179..a0d71cc 100644 --- a/api/common/auth.php +++ b/api/common/auth.php @@ -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() { diff --git a/api/company/list.php b/api/company/list.php index 0023d2d..ba26a63 100644 --- a/api/company/list.php +++ b/api/company/list.php @@ -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 ?"; diff --git a/api/document/delete.php b/api/document/delete.php index 28d37a5..80fbb32 100644 --- a/api/document/delete.php +++ b/api/document/delete.php @@ -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'] ?? ''); diff --git a/api/document/list.php b/api/document/list.php index 5e1f559..1bfcac4 100644 --- a/api/document/list.php +++ b/api/document/list.php @@ -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'] ?? ''); diff --git a/api/document/upload.php b/api/document/upload.php index a3dba51..bec9786 100644 --- a/api/document/upload.php +++ b/api/document/upload.php @@ -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); diff --git a/competitor_data.html b/competitor_data.html index 2682138..98dbee8 100644 --- a/competitor_data.html +++ b/competitor_data.html @@ -1,4 +1,4 @@ - +
@@ -12,7 +12,6 @@ - - + - \ No newline at end of file +