v1.0.10
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# SuperLink Web 后台管理系统
|
||||||
|
|
||||||
|
基于 **PHP原生 + MySQL + jQuery** 的 Web 后台管理系统(无框架,纯 HTML+CSS+JS+JQ)。
|
||||||
|
|
||||||
|
## 一、部署环境(文档固定)
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 服务器 | phpstudy_pro |
|
||||||
|
| Web服务器 | Nginx 1.15.11 |
|
||||||
|
| PHP | 7.4.x |
|
||||||
|
| 数据库 | MySQL 5.7.26 |
|
||||||
|
| 数据库名 | mysuperlink(原库已有14张业务表) |
|
||||||
|
| 字符集 | utf8mb4 |
|
||||||
|
| 根目录 | C:/phpstudy_pro/WWW/mysuperlink/ |
|
||||||
|
|
||||||
|
数据库连接信息(`config/database.php` 已按此配置,可按需修改):
|
||||||
|
- 主机 `127.0.0.1` / 端口 `3306` / 库名 `mysuperlink`
|
||||||
|
- 用户 `mysuperlink_root` / 密码 `123456`
|
||||||
|
|
||||||
|
## 二、部署步骤
|
||||||
|
|
||||||
|
1. 将本项目整个 `mysuperlink/` 文件夹复制到 `C:/phpstudy_pro/WWW/mysuperlink/`。
|
||||||
|
2. 在 Navicat/phpMyAdmin 中对 `mysuperlink` 库执行 `sql/system_tables.sql`:
|
||||||
|
- 新建 3 张系统表:`system_users` / `system_roles` / `system_logs`(含默认数据)
|
||||||
|
- 新建 1 张业务表:`channels`(渠道管理模块需要,原14张表中没有)
|
||||||
|
3. 确保 `static/uploads/documents/` 目录可写(PHP上传文件使用)。
|
||||||
|
4. 浏览器访问 `http://localhost/mysuperlink/` 即可。
|
||||||
|
|
||||||
|
**默认账号:admin / admin123**(密码为 sha1 加密存储)。
|
||||||
|
|
||||||
|
## 三、目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mysuperlink/
|
||||||
|
├── index.html / dashboard.html / preliminary.html / company.html / person.html
|
||||||
|
├── media.html / need.html / edm.html / data_custom.html / media_wholesale.html
|
||||||
|
├── channel.html / document.html / user.html / role.html / log.html (共14个页面)
|
||||||
|
├── api/ # 所有PHP接口(严禁HTML混写)
|
||||||
|
│ ├── common/ # db.php(PDO单例) / response.php(统一JSON) / auth.php(Session鉴权) / logger.php(操作日志) / helpers.php
|
||||||
|
│ ├── auth/ # login.php(滑块验证) / logout.php / forgot.php / session.php(会话校验)
|
||||||
|
│ ├── dashboard/ # stats.php(8指标卡+行业柱状图+区域+词云)
|
||||||
|
│ ├── preliminary/ # list.php / stats.php
|
||||||
|
│ ├── company/ person/ media/ need/ # list/detail/add/update/delete/import/export (+push)
|
||||||
|
│ ├── marketing/ # edm.php(行业+区域二级联动) / edm_export.php / data_custom.php / media_wholesale.php
|
||||||
|
│ ├── channel/ # list.php / stats.php / save.php / delete.php
|
||||||
|
│ ├── document/ # list.php / upload.php(存本地) / delete.php(删物理文件)
|
||||||
|
│ └── system/ # user_list/add/update/delete / role_list/add/update/delete / log_list(仅超管)
|
||||||
|
├── static/
|
||||||
|
│ ├── css/ # style.css / login.css / slider.css
|
||||||
|
│ ├── js/ # config.js(含MENU_MAP) / common.js / login.js / dashboard.js / 各模块js
|
||||||
|
│ ├── libs/ # jquery-3.6.0 / echarts / layer / world.js / slider.js(自研)
|
||||||
|
│ └── uploads/documents/ # 文件物理存储(按年/月自动分目录)
|
||||||
|
├── config/database.php # 数据库配置(返回数组)
|
||||||
|
└── sql/
|
||||||
|
├── init.sql # (原库已有14张表,此处不重复提供)
|
||||||
|
└── system_tables.sql # ★ 新增3张系统表 + channels表 + 默认数据(必须执行)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、接口规范
|
||||||
|
|
||||||
|
统一返回 JSON:`{ "code": 0(成功)/非0(失败), "msg": "提示", "data": {} }`
|
||||||
|
|
||||||
|
- 所有写类操作必须携带 `X-Requested-With: XMLHttpRequest` 头(前端 common.js 已自动处理)。
|
||||||
|
- 除登录/找回密码外,所有接口需 Session 鉴权;无权限返回 `403`;未登录返回 `401`。
|
||||||
|
- 所有写类接口(add/update/delete/import/export/login/logout/push/audit/convert)自动调用 `logAction()` 写操作日志。
|
||||||
|
|
||||||
|
### 列表接口通用参数
|
||||||
|
`page`(默认1)、`limit`(默认20)、`keyword`(关键词)、各模块专属筛选:
|
||||||
|
- 企业:`industry` / `business_role` / `country`
|
||||||
|
- 人员:`gender` / `nationality` / `work_location`
|
||||||
|
- 媒体:`platform` / `certification_type` / `account_level` / `owner_type`
|
||||||
|
- 需求:`start_date` / `end_date` / `company_name` / `industry` / `need_category`
|
||||||
|
- 日志:`action` / `module` / `username` / `start_date` / `end_date`
|
||||||
|
|
||||||
|
## 五、权限控制
|
||||||
|
|
||||||
|
- 登录后返回 `permissions` 数组,前端按 `MENU_MAP`(config.js)动态渲染左侧菜单。
|
||||||
|
- 后端每个接口调用 `checkPermission($module)` 二次校验,写操作另有 `checkAjax()` 校验。
|
||||||
|
- 操作日志(`log.html`)仅超级管理员可见(`requireSuperAdmin()`)。
|
||||||
|
- 保护规则:内置管理员(id=1)不可删除/禁用;超级管理员角色(id=1)不可修改/删除;被用户使用的角色不可删除。
|
||||||
|
|
||||||
|
## 六、占位/简化说明(按文档要求)
|
||||||
|
|
||||||
|
- **滑块验证**:纯前端实现(static/libs/slider.js),拖动通过后生成 token 提交登录;服务端校验 token 合法性。如需更安全可改为服务端签发。
|
||||||
|
- **找回密码**:占位实现,校验账号后返回成功提示,未接入SMTP。
|
||||||
|
- **需求推送 push.php**:占位实现,返回成功并写日志,后续接入第三方API/SMTP。
|
||||||
|
- **数据定制 / 媒体批发**:占位页面。
|
||||||
|
- **Dashboard 区域分布**:先用国家/地区柱状图(简化方案),后续可细化世界地图(world.js 已内置,可切换为地图渲染)。
|
||||||
|
- **需求词云**:从 company_needs.description 做简易分词(占位),后续接入专业分词服务。
|
||||||
|
- **渠道模块**:原14张表中无渠道表,已在 system_tables.sql 中新增 `channels` 表。
|
||||||
|
- **日志清理**:每月1日执行 `DELETE FROM system_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 6 MONTH);`(可加cron计划任务)。
|
||||||
|
|
||||||
|
## 七、CSV 导入模板
|
||||||
|
|
||||||
|
- 企业:表头 `name_zh,name_en,display_name,business_role,country,registration_number,address,legal_form,legal_representative,industry,industry_subdivision,website,registered_capital,established_date,is_listed,stock_code`(display_name 必填,UTF-8编码)
|
||||||
|
- 人员:表头 `union_id,full_name,gender,nationality,id_type,id_number,education,graduated_from,hometown,work_location,phone,email`(full_name 必填,union_id 可留空自动生成)
|
||||||
|
- 媒体:表头 `owner_type,owner_id,platform,account_id,profile_url,remark,is_primary,account_level,content_categories,follower_count,avg_read_count,certification_type`(owner_id 可填名称自动解析)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 找回密码接口 POST /api/auth/forgot.php
|
||||||
|
* 入参:username / contact_email
|
||||||
|
* 说明:占位实现 —— 校验账号存在后返回成功(后续可接入 SMTP 发送重置邮件/短信验证码)。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
Response::error('请求方式错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$contactEmail = trim($_POST['contact_email'] ?? '');
|
||||||
|
|
||||||
|
if ($username === '' || $contactEmail === '') {
|
||||||
|
Response::error('请输入账号和联系邮箱');
|
||||||
|
}
|
||||||
|
if (!filter_var($contactEmail, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
Response::error('邮箱格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare("SELECT id, username, real_name FROM system_users WHERE username = ? AND is_active = 1 LIMIT 1");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
Response::error('账号不存在或已禁用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 接入 SMTP 后向 contactEmail 发送重置链接/验证码
|
||||||
|
logAction((int)$user['id'], $user['username'], 'forgot', 'auth', 'system_users', (int)$user['id'], ['contact_email' => $contactEmail]);
|
||||||
|
|
||||||
|
Response::success(null, '重置邮件已发送(占位实现,请接入SMTP后生效)');
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 登录接口 POST /api/auth/login.php
|
||||||
|
* 入参:username / password / slider_token(滑块验证通过后的token)
|
||||||
|
* 出参(成功):{ code:0, msg:'登录成功', data:{ real_name, role_name, permissions } }
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
Response::error('请求方式错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = (string)($_POST['password'] ?? '');
|
||||||
|
$sliderToken = trim($_POST['slider_token'] ?? '');
|
||||||
|
|
||||||
|
// 滑块验证:前端 slider.js 拖动通过后生成的 token,服务端校验非空且格式合法
|
||||||
|
if ($sliderToken === '' || strlen($sliderToken) < 10) {
|
||||||
|
Response::error('请先完成滑块验证');
|
||||||
|
}
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
Response::error('请输入账号和密码');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT u.*, r.role_name, r.permissions
|
||||||
|
FROM system_users u
|
||||||
|
LEFT JOIN system_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.username = ? LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$user || !hash_equals($user['password'], sha1($password))) {
|
||||||
|
Response::error('账号或密码错误');
|
||||||
|
}
|
||||||
|
if ((int)$user['is_active'] !== 1) {
|
||||||
|
Response::error('账号已被禁用,请联系管理员');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后登录时间/IP
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
$upd = $pdo->prepare("UPDATE system_users SET last_login_time = NOW(), last_login_ip = ? WHERE id = ?");
|
||||||
|
$upd->execute([$ip, $user['id']]);
|
||||||
|
|
||||||
|
// 写入 Session
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
$_SESSION['user_id'] = (int)$user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['real_name'] = $user['real_name'] ?? $user['username'];
|
||||||
|
$_SESSION['role_id'] = (int)$user['role_id'];
|
||||||
|
$_SESSION['role_name'] = $user['role_name'] ?? '';
|
||||||
|
$_SESSION['permissions'] = json_decode($user['permissions'] ?? '[]', true) ?: [];
|
||||||
|
|
||||||
|
// 操作日志
|
||||||
|
logAction((int)$user['id'], $user['username'], 'login', 'auth', 'system_users', (int)$user['id'], ['ip' => $ip]);
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'real_name' => $_SESSION['real_name'],
|
||||||
|
'role_name' => $_SESSION['role_name'],
|
||||||
|
'permissions' => $_SESSION['permissions'],
|
||||||
|
], '登录成功');
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 登出接口 POST /api/auth/logout.php
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($_SESSION['user_id'])) {
|
||||||
|
logAction((int)$_SESSION['user_id'], $_SESSION['username'] ?? '', 'logout', 'auth', 'system_users', (int)$_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空并销毁会话
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get('session.use_cookies')) {
|
||||||
|
$p = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
Response::success(null, '已退出登录');
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 会话校验接口 GET /api/auth/session.php
|
||||||
|
* 前端页面加载时调用:未登录返回 401(前端跳转登录页),已登录返回当前用户信息。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare("SELECT last_login_time, last_login_ip FROM system_users WHERE id = ?");
|
||||||
|
$stmt->execute([(int)$_SESSION['user_id']]);
|
||||||
|
$loginInfo = $stmt->fetch() ?: [];
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'user_id' => (int)$_SESSION['user_id'],
|
||||||
|
'username' => $_SESSION['username'],
|
||||||
|
'real_name' => $_SESSION['real_name'],
|
||||||
|
'role_name' => $_SESSION['role_name'],
|
||||||
|
'permissions' => $_SESSION['permissions'],
|
||||||
|
'last_login_time' => $loginInfo['last_login_time'] ?? null,
|
||||||
|
'last_login_ip' => $loginInfo['last_login_ip'] ?? null,
|
||||||
|
]);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道删除接口 POST /api/channel/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('channel');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM channels WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'channel', 'channels', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 个渠道");
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道列表接口 GET/POST /api/channel/list.php
|
||||||
|
* 参数:page / limit / keyword / channel_type / is_active
|
||||||
|
* 数据源:channels 表(见 sql/system_tables.sql 第5节)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('channel');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$type = trim($_REQUEST['channel_type'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($active !== null) {
|
||||||
|
$where = ['is_active = ?'];
|
||||||
|
$params = [$active];
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(channel_name LIKE ? OR contact_person LIKE ? OR contact_phone LIKE ? OR contact_email LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($type !== '') { $where[] = 'channel_type = ?'; $params[] = $type; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM channels WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, channel_name, channel_type, contact_person, contact_phone, contact_email,
|
||||||
|
efficiency_score, remark, is_active, created_at, updated_at
|
||||||
|
FROM channels
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道新增/编辑接口 POST /api/channel/save.php
|
||||||
|
* 入参:id(编辑时必传)/ channel_name / channel_type / contact_person / contact_phone / contact_email / efficiency_score / remark
|
||||||
|
* 数据源:channels 表(见 sql/system_tables.sql 第5节)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('channel');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$channelName = trim($_POST['channel_name'] ?? '');
|
||||||
|
if ($channelName === '') {
|
||||||
|
Response::error('渠道名称为必填项', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'channel_name' => $channelName,
|
||||||
|
'channel_type' => trim($_POST['channel_type'] ?? '') ?: null,
|
||||||
|
'contact_person' => trim($_POST['contact_person'] ?? '') ?: null,
|
||||||
|
'contact_phone' => trim($_POST['contact_phone'] ?? '') ?: null,
|
||||||
|
'contact_email' => trim($_POST['contact_email'] ?? '') ?: null,
|
||||||
|
'remark' => trim($_POST['remark'] ?? '') ?: null,
|
||||||
|
];
|
||||||
|
$score = (float)($_POST['efficiency_score'] ?? 0);
|
||||||
|
$fields['efficiency_score'] = max(0, min(100, $score));
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$check = $pdo->prepare("SELECT * FROM channels WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('渠道不存在');
|
||||||
|
}
|
||||||
|
[$sets, $params] = buildUpdate($fields);
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE channels SET $sets WHERE id = ?")->execute($params);
|
||||||
|
logCurrent('update', 'channel', 'channels', $id, ['before' => $old, 'after' => $fields]);
|
||||||
|
Response::success(['id' => $id], '更新成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sql, $params] = buildInsert($fields);
|
||||||
|
$pdo->prepare("INSERT INTO channels $sql")->execute($params);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
logCurrent('add', 'channel', 'channels', $newId, $fields);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道年度统计接口 GET /api/channel/stats.php?year=2025
|
||||||
|
* 返回:指定年度(默认今年)各渠道效率统计 + 年度汇总
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('channel');
|
||||||
|
|
||||||
|
$year = (int)($_REQUEST['year'] ?? date('Y'));
|
||||||
|
if ($year < 2000 || $year > 2100) {
|
||||||
|
$year = (int)date('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$rows = $pdo->prepare(
|
||||||
|
"SELECT id, channel_name, channel_type, efficiency_score, created_at
|
||||||
|
FROM channels
|
||||||
|
WHERE is_active = 1 AND YEAR(created_at) = ?
|
||||||
|
ORDER BY efficiency_score DESC"
|
||||||
|
);
|
||||||
|
$rows->execute([$year]);
|
||||||
|
$list = $rows->fetchAll();
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'channel_count' => count($list),
|
||||||
|
'avg_score' => 0,
|
||||||
|
'max_score' => 0,
|
||||||
|
'min_score' => 0,
|
||||||
|
];
|
||||||
|
if ($list) {
|
||||||
|
$scores = array_column($list, 'efficiency_score');
|
||||||
|
$summary['avg_score'] = round(array_sum($scores) / count($scores), 2);
|
||||||
|
$summary['max_score'] = (float)max($scores);
|
||||||
|
$summary['min_score'] = (float)min($scores);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可用年度(用于前端下拉)
|
||||||
|
$years = $pdo->query("SELECT DISTINCT YEAR(created_at) AS y FROM channels WHERE is_active = 1 ORDER BY y DESC")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
Response::success(['year' => $year, 'list' => $list, 'summary' => $summary, 'years' => $years]);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Session 鉴权中间件
|
||||||
|
* 所有接口(登录/找回密码除外)均需通过 requireLogin() / checkPermission() 校验。
|
||||||
|
* 写类接口还必须通过 checkAjax()(要求 X-Requested-With: XMLHttpRequest)。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
require_once __DIR__ . '/response.php';
|
||||||
|
|
||||||
|
/** 启动 Session */
|
||||||
|
function startSession()
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验写类接口的 Ajax 头 */
|
||||||
|
function checkAjax()
|
||||||
|
{
|
||||||
|
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
|
||||||
|
Response::error('非法请求', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验是否已登录,未登录返回 401 */
|
||||||
|
function requireLogin()
|
||||||
|
{
|
||||||
|
startSession();
|
||||||
|
if (empty($_SESSION['user_id'])) {
|
||||||
|
Response::error('未登录或会话已过期', 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前用户是否拥有某模块权限(同时校验登录态)
|
||||||
|
* @param string $module 菜单标识:dashboard/company/person/media/need/marketing/channel/document/preliminary/system/log
|
||||||
|
*/
|
||||||
|
function checkPermission($module)
|
||||||
|
{
|
||||||
|
requireLogin();
|
||||||
|
$perms = $_SESSION['permissions'] ?? [];
|
||||||
|
if (!in_array($module, $perms, true)) {
|
||||||
|
Response::error('无权操作', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅超级管理员可操作(如操作日志查询) */
|
||||||
|
function requireSuperAdmin()
|
||||||
|
{
|
||||||
|
requireLogin();
|
||||||
|
if (($_SESSION['role_name'] ?? '') !== '超级管理员') {
|
||||||
|
Response::error('仅超级管理员可操作', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取客户端 IP */
|
||||||
|
function clientIp()
|
||||||
|
{
|
||||||
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页参数解析:page / limit */
|
||||||
|
function pageParams($defaultLimit = 20)
|
||||||
|
{
|
||||||
|
$page = max(1, (int)($_REQUEST['page'] ?? 1));
|
||||||
|
$limit = min(200, max(1, (int)($_REQUEST['limit'] ?? $defaultLimit)));
|
||||||
|
return [$page, $limit];
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PDO 数据库连接类(单例模式)
|
||||||
|
* 用法:$pdo = DB::getInstance()->getPdo();
|
||||||
|
*/
|
||||||
|
class DB
|
||||||
|
{
|
||||||
|
private static $instance = null;
|
||||||
|
private $pdo;
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$cfg = require __DIR__ . '/../../config/database.php';
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||||
|
$cfg['host'],
|
||||||
|
$cfg['port'],
|
||||||
|
$cfg['dbname'],
|
||||||
|
$cfg['charset']
|
||||||
|
);
|
||||||
|
$this->pdo = new PDO($dsn, $cfg['username'], $cfg['password'], [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance()
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPdo()
|
||||||
|
{
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止克隆
|
||||||
|
private function __clone() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 通用辅助函数与字段白名单(供各模块 add/update/import 复用)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
require_once __DIR__ . '/response.php';
|
||||||
|
|
||||||
|
/** 企业表可写入字段白名单(key => 类型标记:s字符串/i整数/d日期/n数字) */
|
||||||
|
const COMPANY_FIELDS = [
|
||||||
|
'name_zh' => 's', 'name_en' => 's', 'display_name' => 's',
|
||||||
|
'business_role' => 's', 'country' => 's', 'registration_number' => 's',
|
||||||
|
'address' => 's', 'legal_form' => 's', 'legal_representative' => 's',
|
||||||
|
'business_scope' => 's', 'established_date' => 'd', 'registered_capital' => 's',
|
||||||
|
'industry' => 's', 'industry_subdivision' => 's',
|
||||||
|
'latest_employee_count' => 'i', 'latest_annual_revenue' => 's',
|
||||||
|
'is_listed' => 'i', 'stock_code' => 's', 'website' => 's',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 人员表可写入字段白名单 */
|
||||||
|
const PERSON_FIELDS = [
|
||||||
|
'union_id' => 's', 'full_name' => 's', 'gender' => 's', 'nationality' => 's',
|
||||||
|
'id_type' => 's', 'id_number' => 's', 'education' => 's', 'graduated_from' => 's',
|
||||||
|
'hometown' => 's', 'work_location' => 's',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 媒体(社交账号)表可写入字段白名单 */
|
||||||
|
const MEDIA_FIELDS = [
|
||||||
|
'owner_type' => 's', 'owner_id' => 'i', 'platform' => 's', 'account_id' => 's',
|
||||||
|
'profile_url' => 's', 'remark' => 's', 'is_primary' => 'i',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 媒体商业属性表可写入字段白名单 */
|
||||||
|
const MEDIA_ATTR_FIELDS = [
|
||||||
|
'account_level' => 's', 'content_categories' => 's', 'follower_count' => 'i',
|
||||||
|
'avg_read_count' => 'i', 'certification_type' => 's',
|
||||||
|
'special_requirements' => 's', 'media_remark' => 's',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 需求表可写入字段白名单 */
|
||||||
|
const NEED_FIELDS = [
|
||||||
|
'company_id' => 'i', 'contact_person' => 's', 'need_category' => 's',
|
||||||
|
'target_product_category' => 's', 'application_scenario' => 's', 'description' => 's',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 POST 中按白名单提取并清洗数据
|
||||||
|
* @param array $allowlist
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function extractFields($allowlist)
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
foreach ($allowlist as $key => $type) {
|
||||||
|
$val = $_POST[$key] ?? null;
|
||||||
|
if ($val === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$val = trim((string)$val);
|
||||||
|
if ($val === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch ($type) {
|
||||||
|
case 'i':
|
||||||
|
$data[$key] = (int)$val;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
$data[$key] = (strtotime($val) !== false) ? date('Y-m-d', strtotime($val)) : null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$data[$key] = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成 INSERT 语句片段:字段名列表 + 占位符 */
|
||||||
|
function buildInsert($data)
|
||||||
|
{
|
||||||
|
$cols = array_keys($data);
|
||||||
|
$sql = '(`' . implode('`,`', $cols) . '`) VALUES (' . rtrim(str_repeat('?,', count($cols)), ',') . ')';
|
||||||
|
return [$sql, array_values($data)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成 UPDATE SET 片段 */
|
||||||
|
function buildUpdate($data)
|
||||||
|
{
|
||||||
|
$sets = [];
|
||||||
|
foreach (array_keys($data) as $col) {
|
||||||
|
$sets[] = "`$col` = ?";
|
||||||
|
}
|
||||||
|
return [implode(',', $sets), array_values($data)];
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 操作日志写入函数
|
||||||
|
* 所有写类接口(add/update/delete/import/export/audit/convert/login/logout)必须调用 logAction()。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入操作日志
|
||||||
|
* @param int|null $userId 操作人ID
|
||||||
|
* @param string|null $username 操作人账号
|
||||||
|
* @param string $action 操作类型
|
||||||
|
* @param string $module 操作模块
|
||||||
|
* @param string|null $targetTable 操作对象表名
|
||||||
|
* @param int|null $targetId 操作对象记录ID
|
||||||
|
* @param mixed $content 操作内容(数组,自动JSON编码)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function logAction($userId, $username, $action, $module, $targetTable = null, $targetId = null, $content = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO system_logs (user_id, username, action, module, target_table, target_id, content, ip)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$contentJson = ($content !== null && $content !== '')
|
||||||
|
? json_encode($content, JSON_UNESCAPED_UNICODE)
|
||||||
|
: null;
|
||||||
|
return $stmt->execute([$userId, $username, $action, $module, $targetTable, $targetId, $contentJson, $ip]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// 日志失败不影响主流程
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于当前 Session 用户写入日志(便捷函数)
|
||||||
|
* @param string $action
|
||||||
|
* @param string $module
|
||||||
|
* @param string|null $targetTable
|
||||||
|
* @param int|null $targetId
|
||||||
|
* @param mixed $content
|
||||||
|
*/
|
||||||
|
function logCurrent($action, $module, $targetTable = null, $targetId = null, $content = null)
|
||||||
|
{
|
||||||
|
return logAction(
|
||||||
|
$_SESSION['user_id'] ?? null,
|
||||||
|
$_SESSION['username'] ?? null,
|
||||||
|
$action,
|
||||||
|
$module,
|
||||||
|
$targetTable,
|
||||||
|
$targetId,
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 统一 JSON 返回类
|
||||||
|
* 用法:Response::success($data, $msg) / Response::error($msg, $code)
|
||||||
|
*/
|
||||||
|
class Response
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 成功返回
|
||||||
|
* @param mixed $data
|
||||||
|
* @param string $msg
|
||||||
|
*/
|
||||||
|
public static function success($data = null, $msg = '操作成功')
|
||||||
|
{
|
||||||
|
self::json(['code' => 0, 'msg' => $msg, 'data' => $data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败返回
|
||||||
|
* @param string $msg
|
||||||
|
* @param int $code 非0错误码(401未登录/403无权限/400参数错误/1业务错误)
|
||||||
|
*/
|
||||||
|
public static function error($msg = '操作失败', $code = 1)
|
||||||
|
{
|
||||||
|
self::json(['code' => $code, 'msg' => $msg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function json($payload)
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业新增接口 POST /api/company/add.php
|
||||||
|
* 必填:display_name;其余字段见 COMPANY_FIELDS 白名单
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
$data = extractFields(COMPANY_FIELDS);
|
||||||
|
// 显示名称不再单独录入:优先取中文名称,其次英文名称(数据库 display_name 为 NOT NULL)
|
||||||
|
if (empty($data['display_name'])) {
|
||||||
|
$data['display_name'] = $data['name_zh'] ?? ($data['name_en'] ?? '');
|
||||||
|
}
|
||||||
|
if (empty($data['display_name'])) {
|
||||||
|
Response::error('企业名称(中文名称或英文名称)为必填项', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
[$sql, $params] = buildInsert($data);
|
||||||
|
$pdo->prepare("INSERT INTO companies $sql")->execute($params);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
logCurrent('add', 'company', 'companies', $newId, $data);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业删除接口(软删除) POST /api/company/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) {
|
||||||
|
$idList[] = $id;
|
||||||
|
}
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) {
|
||||||
|
$idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("UPDATE companies SET is_active = 0 WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'company', 'companies', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 条记录");
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业详情接口 GET /api/company/detail.php?id=1
|
||||||
|
* 返回企业主表信息 + 关联数据(产品品类/需求/文档/财务)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
$id = (int)($_REQUEST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM companies WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$company = $stmt->fetch();
|
||||||
|
if (!$company) {
|
||||||
|
Response::error('企业不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = $pdo->prepare("SELECT id, category_name, category_description, is_core, is_active FROM company_products WHERE company_id = ? AND is_active = 1");
|
||||||
|
$products->execute([$id]);
|
||||||
|
|
||||||
|
$needs = $pdo->prepare("SELECT id, contact_person, need_category, target_product_category, application_scenario, description, is_valid, created_at FROM company_needs WHERE company_id = ? ORDER BY id DESC");
|
||||||
|
$needs->execute([$id]);
|
||||||
|
|
||||||
|
$docs = $pdo->prepare(
|
||||||
|
"SELECT cd.id, cd.doc_name, cd.storage_path, cd.file_type, cd.publish_date, cd.is_active
|
||||||
|
FROM company_documents cd
|
||||||
|
INNER JOIN document_links dl ON dl.document_id = cd.id
|
||||||
|
WHERE dl.owner_type = 'company' AND dl.owner_id = ? AND cd.is_active = 1"
|
||||||
|
);
|
||||||
|
$docs->execute([$id]);
|
||||||
|
|
||||||
|
$financials = $pdo->prepare("SELECT * FROM company_financials WHERE company_id = ? ORDER BY fiscal_year DESC");
|
||||||
|
$financials->execute([$id]);
|
||||||
|
|
||||||
|
$relations = $pdo->prepare(
|
||||||
|
"SELECT cr.id, cr.relation_type, cr.is_direct, cr.ownership_percentage, cr.established_date, c.display_name AS child_name
|
||||||
|
FROM company_relations cr
|
||||||
|
LEFT JOIN companies c ON c.id = cr.child_company_id
|
||||||
|
WHERE cr.parent_company_id = ?"
|
||||||
|
);
|
||||||
|
$relations->execute([$id]);
|
||||||
|
|
||||||
|
// 关联联系人(工作经历 + 联系方式)
|
||||||
|
$contacts = $pdo->prepare(
|
||||||
|
"SELECT p.id, p.full_name,
|
||||||
|
(SELECT GROUP_CONCAT(CONCAT_WS(' ', pwe2.position, pwe2.department) SEPARATOR ' / ')
|
||||||
|
FROM person_work_experiences pwe2
|
||||||
|
WHERE pwe2.company_id = ? AND pwe2.person_id = p.id AND pwe2.is_active = 1) AS position,
|
||||||
|
(SELECT GROUP_CONCAT(CONCAT(sa.platform, ':', sa.account_id) SEPARATOR ' | ')
|
||||||
|
FROM social_accounts sa WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1) AS contacts
|
||||||
|
FROM person_work_experiences pwe
|
||||||
|
LEFT JOIN persons p ON p.id = pwe.person_id
|
||||||
|
WHERE pwe.company_id = ? AND pwe.is_active = 1
|
||||||
|
GROUP BY p.id, p.full_name"
|
||||||
|
);
|
||||||
|
$contacts->execute([$id, $id]);
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'company' => $company,
|
||||||
|
'products' => $products->fetchAll(),
|
||||||
|
'needs' => $needs->fetchAll(),
|
||||||
|
'documents' => $docs->fetchAll(),
|
||||||
|
'financials' => $financials->fetchAll(),
|
||||||
|
'relations' => $relations->fetchAll(),
|
||||||
|
'contacts' => $contacts->fetchAll(),
|
||||||
|
]);
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业员工列表接口 GET /api/company/employees.php
|
||||||
|
* 入参:company_id(必填)/ page / limit / keyword(姓名)/ job_level(职级)
|
||||||
|
* 数据源:persons JOIN person_work_experiences(按公司),联系方式取 social_accounts
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
$companyId = (int)($_REQUEST['company_id'] ?? 0);
|
||||||
|
if ($companyId <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams(10);
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$jobLevel = trim($_REQUEST['job_level'] ?? '');
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// 公司存在性校验
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
|
||||||
|
$chk->execute([$companyId]);
|
||||||
|
if ((int)$chk->fetchColumn() === 0) {
|
||||||
|
Response::error('企业不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = ['pwe.company_id = ?', 'pwe.is_active = 1'];
|
||||||
|
$params = [$companyId];
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = 'p.full_name LIKE ?';
|
||||||
|
$params[] = "%$keyword%";
|
||||||
|
}
|
||||||
|
if ($jobLevel !== '') {
|
||||||
|
$where[] = 'pwe.job_level = ?';
|
||||||
|
$params[] = $jobLevel;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
// 总数
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM person_work_experiences pwe
|
||||||
|
INNER JOIN persons p ON p.id = pwe.person_id
|
||||||
|
WHERE $whereSql"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT p.id AS person_id, p.full_name, pwe.id AS work_id, pwe.position, pwe.department,
|
||||||
|
pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current,
|
||||||
|
(SELECT sa.account_id FROM social_accounts sa
|
||||||
|
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'phone' AND sa.is_active = 1
|
||||||
|
ORDER BY sa.is_primary DESC LIMIT 1) AS phone,
|
||||||
|
(SELECT sa.account_id FROM social_accounts sa
|
||||||
|
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'email' AND sa.is_active = 1
|
||||||
|
ORDER BY sa.is_primary DESC LIMIT 1) AS email
|
||||||
|
FROM person_work_experiences pwe
|
||||||
|
INNER JOIN persons p ON p.id = pwe.person_id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY pwe.is_current DESC, pwe.start_date DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 职级字典(该公司范围内,供筛选下拉)
|
||||||
|
$lv = $pdo->prepare(
|
||||||
|
"SELECT DISTINCT job_level FROM person_work_experiences
|
||||||
|
WHERE company_id = ? AND is_active = 1 AND job_level IS NOT NULL AND job_level <> ''
|
||||||
|
ORDER BY job_level"
|
||||||
|
);
|
||||||
|
$lv->execute([$companyId]);
|
||||||
|
$levels = $lv->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'levels' => $levels]);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业导出接口(CSV) GET/POST /api/company/export.php
|
||||||
|
* 入参:ids(必填,勾选的企业ID,逗号分隔)——只导出勾选的企业
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
// 仅导出勾选的企业
|
||||||
|
$idsRaw = trim($_REQUEST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($idsRaw !== '') {
|
||||||
|
foreach (explode(',', $idsRaw) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) {
|
||||||
|
$idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('请先选择需要导出的企业数据', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, name_zh, name_en, business_role, country, registration_number,
|
||||||
|
address, legal_representative, industry, industry_subdivision, website,
|
||||||
|
registered_capital, established_date, latest_employee_count, latest_annual_revenue,
|
||||||
|
is_listed, stock_code, created_at
|
||||||
|
FROM companies WHERE id IN ($in) AND is_active = 1 ORDER BY id DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
logCurrent('export', 'company', 'companies', null, ['count' => count($list), 'ids' => $idList]);
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="companies_' . date('Ymd_His') . '.csv"');
|
||||||
|
echo "\xEF\xBB\xBF"; // UTF-8 BOM,便于 Excel 直接打开
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
fputcsv($out, ['ID', '公司名称', '英文名称', '业务角色', '国家/地区', '注册号', '地址', '法人', '行业', '行业细分', '官网', '注册资本', '成立日期', '员工数', '年营收', '是否上市', '股票代码', '创建时间']);
|
||||||
|
foreach ($list as $r) {
|
||||||
|
fputcsv($out, [
|
||||||
|
$r['id'], $r['name_zh'], $r['name_en'], $r['business_role'],
|
||||||
|
$r['country'], $r['registration_number'], $r['address'], $r['legal_representative'],
|
||||||
|
$r['industry'], $r['industry_subdivision'], $r['website'], $r['registered_capital'],
|
||||||
|
$r['established_date'], $r['latest_employee_count'], $r['latest_annual_revenue'],
|
||||||
|
$r['is_listed'], $r['stock_code'], date('Y-m-d', strtotime($r['created_at'])),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fclose($out);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业 CSV 导入接口 POST /api/company/import.php (multipart/form-data, 字段名 file)
|
||||||
|
* CSV 表头:name_zh,name_en,display_name,business_role,country,registration_number,address,
|
||||||
|
* legal_form,legal_representative,industry,industry_subdivision,website,
|
||||||
|
* registered_capital,established_date,is_listed,stock_code
|
||||||
|
* 仅 display_name 为必填。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
Response::error('请选择要上传的CSV文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = $_FILES['file']['tmp_name'];
|
||||||
|
$handle = fopen($tmp, 'r');
|
||||||
|
if (!$handle) {
|
||||||
|
Response::error('无法读取文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉 UTF-8 BOM
|
||||||
|
$first = fgets($handle);
|
||||||
|
$first = preg_replace('/^\xEF\xBB\xBF/', '', $first);
|
||||||
|
$header = str_getcsv(trim($first));
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
'name_zh' => 'name_zh', 'name_en' => 'name_en', 'display_name' => 'display_name',
|
||||||
|
'business_role' => 'business_role', 'country' => 'country', 'registration_number' => 'registration_number',
|
||||||
|
'address' => 'address', 'legal_form' => 'legal_form', 'legal_representative' => 'legal_representative',
|
||||||
|
'industry' => 'industry', 'industry_subdivision' => 'industry_subdivision', 'website' => 'website',
|
||||||
|
'registered_capital' => 'registered_capital', 'established_date' => 'established_date',
|
||||||
|
'is_listed' => 'is_listed', 'stock_code' => 'stock_code',
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$inserted = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO companies
|
||||||
|
(name_zh, name_en, display_name, business_role, country, registration_number,
|
||||||
|
address, legal_form, legal_representative, industry, industry_subdivision, website,
|
||||||
|
registered_capital, established_date, is_listed, stock_code)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle)) !== false) {
|
||||||
|
$row = array_map('trim', $row);
|
||||||
|
$rec = [];
|
||||||
|
foreach ($header as $idx => $col) {
|
||||||
|
$col = trim($col);
|
||||||
|
if (isset($map[$col]) && isset($row[$idx])) {
|
||||||
|
$rec[$map[$col]] = $row[$idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($rec['display_name'])) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$rec['established_date'] = ($rec['established_date'] ?? '') !== '' ? date('Y-m-d', strtotime($rec['established_date'])) : null;
|
||||||
|
try {
|
||||||
|
$stmt->execute([
|
||||||
|
$rec['name_zh'] ?? null, $rec['name_en'] ?? null, $rec['display_name'],
|
||||||
|
$rec['business_role'] ?? null, $rec['country'] ?? null, $rec['registration_number'] ?? null,
|
||||||
|
$rec['address'] ?? null, $rec['legal_form'] ?? null, $rec['legal_representative'] ?? null,
|
||||||
|
$rec['industry'] ?? null, $rec['industry_subdivision'] ?? null, $rec['website'] ?? null,
|
||||||
|
$rec['registered_capital'] ?? null, $rec['established_date'],
|
||||||
|
($rec['is_listed'] ?? 0) ? 1 : 0, $rec['stock_code'] ?? null,
|
||||||
|
]);
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
logCurrent('import', 'company', 'companies', null, ['inserted' => $inserted, 'failed' => $failed]);
|
||||||
|
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业列表接口 GET/POST /api/company/list.php
|
||||||
|
* 参数:page / limit / keyword(公司名/注册号)/ industry / business_role / country / is_active
|
||||||
|
* 返回:{ list, total, page, limit }
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$industry = trim($_REQUEST['industry'] ?? '');
|
||||||
|
$role = trim($_REQUEST['business_role'] ?? '');
|
||||||
|
$country = trim($_REQUEST['country'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($active !== null) {
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
$where[] = 'is_active = ?';
|
||||||
|
$params[] = $active;
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(display_name LIKE ? OR name_zh LIKE ? OR name_en LIKE ? OR registration_number LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($industry !== '') {
|
||||||
|
$where[] = 'industry = ?';
|
||||||
|
$params[] = $industry;
|
||||||
|
}
|
||||||
|
if ($role !== '') {
|
||||||
|
$where[] = 'business_role = ?';
|
||||||
|
$params[] = $role;
|
||||||
|
}
|
||||||
|
if ($country !== '') {
|
||||||
|
$where[] = 'country = ?';
|
||||||
|
$params[] = $country;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, name_zh, name_en, display_name, business_role, country, registration_number,
|
||||||
|
address, legal_representative, industry, industry_subdivision, website,
|
||||||
|
latest_employee_count, latest_annual_revenue, is_listed, stock_code,
|
||||||
|
is_active, created_at, updated_at
|
||||||
|
FROM companies
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 企业编辑接口 POST /api/company/update.php
|
||||||
|
* 入参:id + 需要更新的白名单字段
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('company');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = extractFields(COMPANY_FIELDS);
|
||||||
|
unset($data['display_name']); // 显示名称不允许通过编辑接口置空/改名,如需改名请走完整字段
|
||||||
|
if (empty($data)) {
|
||||||
|
Response::error('没有需要更新的字段', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT id, display_name FROM companies WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('企业不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sets, $params] = buildUpdate($data);
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params);
|
||||||
|
|
||||||
|
logCurrent('update', 'company', 'companies', $id, ['before' => $old, 'after' => $data]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard 首页数据接口 GET /api/dashboard/stats.php
|
||||||
|
* 返回:8 个指标卡 + 区域分布(国外=世界地图按国家 / 国内=中国地图按省份,企业/人员双维度)
|
||||||
|
* + 行业分布(企业/人员)+ 需求关键词(简化)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('dashboard');
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// ---------- 8 个指标卡 ----------
|
||||||
|
$sqlMetrics = [
|
||||||
|
'company_total' => "SELECT COUNT(*) AS c FROM companies WHERE is_active = 1",
|
||||||
|
'person_total' => "SELECT COUNT(*) AS c FROM persons WHERE is_active = 1",
|
||||||
|
'company_new' => "SELECT COUNT(*) AS c FROM companies WHERE is_active = 1 AND DATE(created_at) = CURDATE() - INTERVAL 1 DAY",
|
||||||
|
'person_new' => "SELECT COUNT(*) AS c FROM persons WHERE is_active = 1 AND DATE(created_at) = CURDATE() - INTERVAL 1 DAY",
|
||||||
|
'preliminary' => "SELECT COUNT(*) AS c FROM preliminary_data WHERE status = '待处理' AND is_active = 1",
|
||||||
|
'industry_count' => "SELECT COUNT(DISTINCT industry) AS c FROM companies WHERE industry IS NOT NULL AND industry <> ''",
|
||||||
|
'manufacturer' => "SELECT COUNT(*) AS c FROM companies WHERE business_role = 'manufacturer' AND is_active = 1",
|
||||||
|
'integrator' => "SELECT COUNT(*) AS c FROM companies WHERE business_role IN ('integrator','distributor') AND is_active = 1",
|
||||||
|
'media_resource' => "SELECT COUNT(*) AS c FROM media_commercial_attributes",
|
||||||
|
];
|
||||||
|
$metrics = [];
|
||||||
|
foreach ($sqlMetrics as $key => $sql) {
|
||||||
|
$metrics[$key] = (int)$pdo->query($sql)->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 区域分布 ----------
|
||||||
|
// 国外(世界地图):企业按 country,人员按 nationality
|
||||||
|
$regionWorldCompany = $pdo->query(
|
||||||
|
"SELECT country AS name, COUNT(*) AS value
|
||||||
|
FROM companies
|
||||||
|
WHERE country IS NOT NULL AND country <> '' AND is_active = 1
|
||||||
|
GROUP BY country ORDER BY value DESC LIMIT 60"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
$regionWorldPerson = $pdo->query(
|
||||||
|
"SELECT nationality AS name, COUNT(*) AS value
|
||||||
|
FROM persons
|
||||||
|
WHERE nationality IS NOT NULL AND nationality <> '' AND is_active = 1
|
||||||
|
GROUP BY nationality ORDER BY value DESC LIMIT 60"
|
||||||
|
)->fetchAll();
|
||||||
|
if (!$regionWorldPerson) {
|
||||||
|
$regionWorldPerson = $pdo->query(
|
||||||
|
"SELECT work_location AS name, COUNT(*) AS value
|
||||||
|
FROM persons WHERE work_location IS NOT NULL AND work_location <> '' AND is_active = 1
|
||||||
|
GROUP BY work_location ORDER BY value DESC LIMIT 60"
|
||||||
|
)->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 国内(中国地图):按省份统计(企业取注册地址/国家;人员取工作所在地/家乡)
|
||||||
|
$regionChinaCompany = provinceCount(
|
||||||
|
$pdo->query("SELECT country, address FROM companies WHERE is_active = 1")->fetchAll(),
|
||||||
|
['country', 'address']
|
||||||
|
);
|
||||||
|
$regionChinaPerson = provinceCount(
|
||||||
|
$pdo->query("SELECT work_location, hometown FROM persons WHERE is_active = 1")->fetchAll(),
|
||||||
|
['work_location', 'hometown']
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------- 行业分布(企业直接取 industry;人员经工作经历关联到企业行业) ----------
|
||||||
|
$industryCompany = $pdo->query(
|
||||||
|
"SELECT industry AS name, COUNT(*) AS value
|
||||||
|
FROM companies
|
||||||
|
WHERE industry IS NOT NULL AND industry <> '' AND is_active = 1
|
||||||
|
GROUP BY industry ORDER BY value DESC LIMIT 20"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
$industryPerson = $pdo->query(
|
||||||
|
"SELECT c.industry AS name, COUNT(DISTINCT pwe.person_id) AS value
|
||||||
|
FROM person_work_experiences pwe
|
||||||
|
LEFT JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.is_active = 1 AND c.industry IS NOT NULL AND c.industry <> '' AND c.is_active = 1
|
||||||
|
GROUP BY c.industry ORDER BY value DESC LIMIT 20"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
// ---------- 需求关键词(简化:从 company_needs.description 提取高频词,占位) ----------
|
||||||
|
$needRows = $pdo->query(
|
||||||
|
"SELECT description FROM company_needs WHERE is_valid = 1 AND description IS NOT NULL AND description <> '' LIMIT 200"
|
||||||
|
)->fetchAll();
|
||||||
|
$keywordMap = [];
|
||||||
|
foreach ($needRows as $row) {
|
||||||
|
$parts = preg_split('/[,。、,.;;::\s\/\|\-\+\/]+/u', $row['description']);
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$part = trim($part);
|
||||||
|
if (mb_strlen($part, 'utf-8') >= 2 && mb_strlen($part, 'utf-8') <= 12) {
|
||||||
|
$keywordMap[$part] = ($keywordMap[$part] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arsort($keywordMap);
|
||||||
|
$wordCloud = [];
|
||||||
|
$i = 0;
|
||||||
|
foreach ($keywordMap as $word => $count) {
|
||||||
|
if ($i++ >= 50) break;
|
||||||
|
$wordCloud[] = ['name' => $word, 'value' => $count];
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'metrics' => $metrics,
|
||||||
|
'region' => [
|
||||||
|
'company' => ['world' => $regionWorldCompany, 'china' => $regionChinaCompany],
|
||||||
|
'person' => ['world' => $regionWorldPerson, 'china' => $regionChinaPerson],
|
||||||
|
],
|
||||||
|
'industry' => ['company' => $industryCompany, 'person' => $industryPerson],
|
||||||
|
'wordcloud' => $wordCloud,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从一组记录的文本字段中统计中国省份分布
|
||||||
|
* @param array $rows 记录列表(关联数组)
|
||||||
|
* @param array $fields 用于匹配的字段名
|
||||||
|
* @return array [{name:省份, value:数量}]
|
||||||
|
*/
|
||||||
|
function provinceCount($rows, $fields)
|
||||||
|
{
|
||||||
|
// 34 个省级行政区(含直辖市/自治区/特别行政区),匹配顺序:长名称优先
|
||||||
|
$provinces = [
|
||||||
|
'内蒙古自治区', '广西壮族自治区', '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区',
|
||||||
|
'黑龙江省', '河北省', '山西省', '辽宁省', '吉林省', '江苏省', '浙江省', '安徽省',
|
||||||
|
'福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '海南省',
|
||||||
|
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
|
||||||
|
'北京', '天津', '上海', '重庆', '台湾', '香港', '澳门',
|
||||||
|
];
|
||||||
|
// 常见城市 -> 省份(地址/工作地只有城市名时使用)
|
||||||
|
$city2prov = [
|
||||||
|
'深圳' => '广东', '广州' => '广东', '东莞' => '广东', '佛山' => '广东',
|
||||||
|
'珠海' => '广东', '中山' => '广东', '惠州' => '广东', '汕头' => '广东', '江门' => '广东',
|
||||||
|
'杭州' => '浙江', '宁波' => '浙江', '温州' => '浙江', '嘉兴' => '浙江', '绍兴' => '浙江',
|
||||||
|
'南京' => '江苏', '苏州' => '江苏', '无锡' => '江苏', '常州' => '江苏', '南通' => '江苏',
|
||||||
|
'成都' => '四川', '绵阳' => '四川', '武汉' => '湖北', '宜昌' => '湖北', '长沙' => '湖南',
|
||||||
|
'郑州' => '河南', '洛阳' => '河南', '西安' => '陕西', '青岛' => '山东', '济南' => '山东',
|
||||||
|
'烟台' => '山东', '潍坊' => '山东', '大连' => '辽宁', '沈阳' => '辽宁', '哈尔滨' => '黑龙江',
|
||||||
|
'长春' => '吉林', '合肥' => '安徽', '芜湖' => '安徽', '福州' => '福建', '厦门' => '福建',
|
||||||
|
'泉州' => '福建', '昆明' => '云南', '贵阳' => '贵州', '南昌' => '江西', '石家庄' => '河北',
|
||||||
|
'太原' => '山西', '兰州' => '甘肃', '银川' => '宁夏', '西宁' => '青海', '乌鲁木齐' => '新疆',
|
||||||
|
'呼和浩特' => '内蒙古', '南宁' => '广西', '海口' => '海南', '三亚' => '海南',
|
||||||
|
'拉萨' => '西藏', '台北' => '台湾', '高雄' => '台湾', '香港' => '香港', '澳门' => '澳门',
|
||||||
|
];
|
||||||
|
|
||||||
|
$counts = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$text = '';
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
if (!empty($row[$f])) {
|
||||||
|
$text .= $row[$f] . ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$province = null;
|
||||||
|
foreach ($provinces as $p) {
|
||||||
|
if (mb_strpos($text, $p) !== false) {
|
||||||
|
$province = $p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($province === null) {
|
||||||
|
foreach ($city2prov as $city => $p) {
|
||||||
|
if (mb_strpos($text, $city) !== false) {
|
||||||
|
$province = $p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 长名 -> 短名(与中国地图 echarts china.js 的省份名保持一致)
|
||||||
|
if ($province !== null) {
|
||||||
|
$shortMap = [
|
||||||
|
'内蒙古自治区' => '内蒙古', '广西壮族自治区' => '广西', '西藏自治区' => '西藏',
|
||||||
|
'宁夏回族自治区' => '宁夏', '新疆维吾尔自治区' => '新疆',
|
||||||
|
'黑龙江省' => '黑龙江', '河北省' => '河北', '山西省' => '山西', '辽宁省' => '辽宁',
|
||||||
|
'吉林省' => '吉林', '江苏省' => '江苏', '浙江省' => '浙江', '安徽省' => '安徽',
|
||||||
|
'福建省' => '福建', '江西省' => '江西', '山东省' => '山东', '河南省' => '河南',
|
||||||
|
'湖北省' => '湖北', '湖南省' => '湖南', '广东省' => '广东', '海南省' => '海南',
|
||||||
|
'四川省' => '四川', '贵州省' => '贵州', '云南省' => '云南', '陕西省' => '陕西',
|
||||||
|
'甘肃省' => '甘肃', '青海省' => '青海', '台湾省' => '台湾',
|
||||||
|
];
|
||||||
|
if (isset($shortMap[$province])) {
|
||||||
|
$province = $shortMap[$province];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($province !== null) {
|
||||||
|
$counts[$province] = ($counts[$province] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arsort($counts);
|
||||||
|
$result = [];
|
||||||
|
foreach ($counts as $name => $value) {
|
||||||
|
$result[] = ['name' => $name, 'value' => $value];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 文件删除接口(同时删除物理文件) POST /api/document/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('document');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
|
||||||
|
// 取出物理路径
|
||||||
|
$stmt = $pdo->prepare("SELECT id, storage_path FROM company_documents WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$docs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 删除物理文件(仅限本站上传目录内的文件)
|
||||||
|
foreach ($docs as $doc) {
|
||||||
|
$path = $doc['storage_path'];
|
||||||
|
if (strpos($path, 'static/uploads/documents/') === 0) {
|
||||||
|
$full = __DIR__ . '/../../' . $path;
|
||||||
|
if (is_file($full)) {
|
||||||
|
@unlink($full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除记录(document_links 级联删除)
|
||||||
|
$del = $pdo->prepare("DELETE FROM company_documents WHERE id IN ($in)");
|
||||||
|
$del->execute($idList);
|
||||||
|
$affected = $del->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'document', 'company_documents', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 个文件(含物理文件)");
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 文件列表接口 GET/POST /api/document/list.php
|
||||||
|
* 参数:page / limit / keyword / file_type / is_active
|
||||||
|
* 数据源:company_documents 表
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('document');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$fileType = trim($_REQUEST['file_type'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($active !== null) {
|
||||||
|
$where = ['is_active = ?'];
|
||||||
|
$params = [$active];
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(doc_name LIKE ? OR document_source LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like);
|
||||||
|
}
|
||||||
|
if ($fileType !== '') { $where[] = 'file_type = ?'; $params[] = $fileType; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM company_documents WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, doc_name, storage_path, file_type, is_current, publish_date, document_source, tags, is_active, created_at
|
||||||
|
FROM company_documents
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 文件上传接口 POST /api/document/upload.php (multipart/form-data, 字段名 file)
|
||||||
|
* 可选:doc_name(默认取原文件名)/ tags(JSON)
|
||||||
|
* 存储:static/uploads/documents/年/月/随机文件名
|
||||||
|
* 返回:相对路径 storage_path
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('document');
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
Response::error('请选择要上传的文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
$maxSize = 50 * 1024 * 1024; // 50MB
|
||||||
|
if ($file['size'] > $maxSize) {
|
||||||
|
Response::error('文件不能超过50MB', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全扩展名白名单
|
||||||
|
$extMap = [
|
||||||
|
'pdf' => 'application/pdf', 'doc' => 'application/msword',
|
||||||
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'xls' => 'application/vnd.ms-excel',
|
||||||
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'ppt' => 'application/vnd.ms-powerpoint',
|
||||||
|
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'txt' => 'text/plain', 'csv' => 'text/csv', 'png' => 'image/png',
|
||||||
|
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif',
|
||||||
|
'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed',
|
||||||
|
];
|
||||||
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
if (!isset($extMap[$ext])) {
|
||||||
|
Response::error('不允许的文件类型:' . ($ext !== '' ? $ext : '无扩展名'), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目录:static/uploads/documents/年/月
|
||||||
|
$baseDir = __DIR__ . '/../../static/uploads/documents';
|
||||||
|
$subDir = date('Y') . '/' . date('m');
|
||||||
|
$dir = $baseDir . '/' . $subDir;
|
||||||
|
if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
|
||||||
|
Response::error('创建上传目录失败', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newName = date('YmdHis') . '_' . substr(uniqid(), -6) . '.' . $ext;
|
||||||
|
$dest = $dir . '/' . $newName;
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||||
|
Response::error('文件保存失败', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$storagePath = 'static/uploads/documents/' . $subDir . '/' . $newName;
|
||||||
|
$docName = trim($_POST['doc_name'] ?? '') !== '' ? trim($_POST['doc_name']) : $file['name'];
|
||||||
|
$tags = $_POST['tags'] ?? null;
|
||||||
|
$tagsJson = null;
|
||||||
|
if ($tags !== null && $tags !== '') {
|
||||||
|
$decoded = json_decode($tags, true);
|
||||||
|
$tagsJson = is_array($decoded) ? json_encode($decoded, JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO company_documents (doc_name, storage_path, file_type, is_current, document_source, tags)
|
||||||
|
VALUES (?, ?, ?, 1, ?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$docName, $storagePath, $extMap[$ext], '本地上传', $tagsJson]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
logCurrent('upload', 'document', 'company_documents', $newId, ['doc_name' => $docName, 'storage_path' => $storagePath]);
|
||||||
|
Response::success(['id' => $newId, 'storage_path' => $storagePath, 'doc_name' => $docName], '上传成功');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 数据定制接口(占位) GET/POST /api/marketing/data_custom.php
|
||||||
|
* 后续扩展:定制数据需求提交、报价、交付等。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('marketing');
|
||||||
|
|
||||||
|
$action = $_REQUEST['action'] ?? 'info';
|
||||||
|
if ($action === 'submit' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
checkAjax();
|
||||||
|
$content = [
|
||||||
|
'contact_name' => trim($_POST['contact_name'] ?? ''),
|
||||||
|
'contact_phone' => trim($_POST['contact_phone'] ?? ''),
|
||||||
|
'requirements' => trim($_POST['requirements'] ?? ''),
|
||||||
|
];
|
||||||
|
logCurrent('audit', 'marketing', null, null, $content);
|
||||||
|
Response::success(null, '需求已提交(占位实现,后续接入业务流)');
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'title' => '数据定制',
|
||||||
|
'description' => '按行业/区域/规模等维度定制企业数据、人员数据,功能建设中(占位)。',
|
||||||
|
'status' => 'placeholder',
|
||||||
|
]);
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EDM 筛选接口 GET/POST /api/marketing/edm.php
|
||||||
|
* action:
|
||||||
|
* industries 返回行业下拉数据
|
||||||
|
* regions 返回区域二级联动数据(省/州 -> 市/区,来自 persons.work_location / hometown)
|
||||||
|
* filter 按 行业 + 区域 + 关键词 筛选邮箱列表(persons.email + companies 关联联系人邮箱)
|
||||||
|
* 入参(filter):industry / province / city / keyword / page / limit
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('marketing');
|
||||||
|
|
||||||
|
$action = $_REQUEST['action'] ?? 'filter';
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// ---------- 行业下拉 ----------
|
||||||
|
if ($action === 'industries') {
|
||||||
|
$rows = $pdo->query(
|
||||||
|
"SELECT DISTINCT industry FROM companies WHERE industry IS NOT NULL AND industry <> '' AND is_active = 1 ORDER BY industry"
|
||||||
|
)->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
Response::success(['industries' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 区域二级联动 ----------
|
||||||
|
if ($action === 'regions') {
|
||||||
|
// 从 persons.work_location 与 hometown 提取 省/州 与 市/区
|
||||||
|
$rows = $pdo->query(
|
||||||
|
"SELECT work_location AS loc FROM persons WHERE work_location IS NOT NULL AND work_location <> '' AND is_active = 1
|
||||||
|
UNION SELECT hometown AS loc FROM persons WHERE hometown IS NOT NULL AND hometown <> '' AND is_active = 1"
|
||||||
|
)->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$regions = [];
|
||||||
|
foreach ($rows as $loc) {
|
||||||
|
$parts = preg_split('/[\s\-—–\/,,]+/u', trim($loc));
|
||||||
|
$province = $parts[0] ?? $loc;
|
||||||
|
$city = isset($parts[1]) && $parts[1] !== '' ? $parts[1] : '';
|
||||||
|
if ($province === '') continue;
|
||||||
|
if (!isset($regions[$province])) {
|
||||||
|
$regions[$province] = [];
|
||||||
|
}
|
||||||
|
if ($city !== '' && !in_array($city, $regions[$province], true)) {
|
||||||
|
$regions[$province][] = $city;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::success(['regions' => $regions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 筛选邮箱列表 ----------
|
||||||
|
if ($action === 'filter') {
|
||||||
|
$industry = trim($_REQUEST['industry'] ?? '');
|
||||||
|
$province = trim($_REQUEST['province'] ?? '');
|
||||||
|
$city = trim($_REQUEST['city'] ?? '');
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
|
||||||
|
// 邮箱来源:persons 的 social_accounts(platform=email),关联工作经历(company) 做行业过滤
|
||||||
|
$where = ["sa.platform = 'email'", "sa.is_active = 1", 'p.is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($industry !== '') {
|
||||||
|
$where[] = "EXISTS (
|
||||||
|
SELECT 1 FROM person_work_experiences pwe
|
||||||
|
LEFT JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.person_id = p.id AND pwe.is_active = 1 AND c.industry = ?
|
||||||
|
)";
|
||||||
|
$params[] = $industry;
|
||||||
|
}
|
||||||
|
if ($province !== '') {
|
||||||
|
$where[] = "(p.work_location LIKE ? OR p.hometown LIKE ?)";
|
||||||
|
$params[] = "%$province%";
|
||||||
|
$params[] = "%$province%";
|
||||||
|
}
|
||||||
|
if ($city !== '') {
|
||||||
|
$where[] = "(p.work_location LIKE ? OR p.hometown LIKE ?)";
|
||||||
|
$params[] = "%$city%";
|
||||||
|
$params[] = "%$city%";
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(p.full_name LIKE ? OR sa.account_id LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like);
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM social_accounts sa
|
||||||
|
LEFT JOIN persons p ON p.id = sa.owner_id
|
||||||
|
WHERE $whereSql"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams(20);
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT sa.id, sa.account_id AS email, p.id AS person_id, p.full_name, p.work_location, p.hometown,
|
||||||
|
(SELECT c.display_name FROM person_work_experiences pwe
|
||||||
|
LEFT JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.person_id = p.id AND pwe.is_current = 1 LIMIT 1) AS company_name
|
||||||
|
FROM social_accounts sa
|
||||||
|
LEFT JOIN persons p ON p.id = sa.owner_id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY sa.id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::error('未知操作', 400);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EDM 邮箱列表导出接口(CSV) GET/POST /api/marketing/edm_export.php
|
||||||
|
* 入参同 edm.php filter:industry / province / city / keyword
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('marketing');
|
||||||
|
|
||||||
|
// 仅导出勾选的邮箱(emails 逗号分隔)
|
||||||
|
$emailsRaw = trim($_REQUEST['emails'] ?? '');
|
||||||
|
$emailList = [];
|
||||||
|
if ($emailsRaw !== '') {
|
||||||
|
foreach (explode(',', $emailsRaw) as $v) {
|
||||||
|
$v = trim($v);
|
||||||
|
if ($v !== '') {
|
||||||
|
$emailList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$emailList = array_unique($emailList);
|
||||||
|
if (!$emailList) {
|
||||||
|
Response::error('请先选择需要导出的邮箱', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$in = implode(',', array_fill(0, count($emailList), '?'));
|
||||||
|
$where = ["sa.platform = 'email'", 'sa.is_active = 1', 'p.is_active = 1', "sa.account_id IN ($in)"];
|
||||||
|
$params = $emailList;
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT sa.account_id AS email, p.full_name, p.work_location, p.hometown
|
||||||
|
FROM social_accounts sa
|
||||||
|
LEFT JOIN persons p ON p.id = sa.owner_id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY sa.id DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
logCurrent('export', 'marketing', 'social_accounts', null, ['count' => count($list), 'emails' => $emailList]);
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="edm_emails_' . date('Ymd_His') . '.csv"');
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
fputcsv($out, ['邮箱', '姓名', '工作所在地', '家乡']);
|
||||||
|
foreach ($list as $r) {
|
||||||
|
fputcsv($out, [$r['email'], $r['full_name'], $r['work_location'], $r['hometown']]);
|
||||||
|
}
|
||||||
|
fclose($out);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体批发接口(占位) GET/POST /api/marketing/media_wholesale.php
|
||||||
|
* 后续扩展:媒体资源批量询价、下单等。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('marketing');
|
||||||
|
|
||||||
|
$action = $_REQUEST['action'] ?? 'info';
|
||||||
|
if ($action === 'inquiry' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
checkAjax();
|
||||||
|
$content = [
|
||||||
|
'media_ids' => trim($_POST['media_ids'] ?? ''),
|
||||||
|
'remark' => trim($_POST['remark'] ?? ''),
|
||||||
|
];
|
||||||
|
logCurrent('audit', 'marketing', 'social_accounts', null, $content);
|
||||||
|
Response::success(null, '询价已提交(占位实现,后续接入业务流)');
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'title' => '媒体批发',
|
||||||
|
'description' => '媒体账号资源批量采购/询价,功能建设中(占位)。',
|
||||||
|
'status' => 'placeholder',
|
||||||
|
]);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据新增接口 POST /api/media/add.php
|
||||||
|
* 必填:owner_type / owner_id / platform / account_id
|
||||||
|
* 可选:profile_url / remark / is_primary + 商业属性(account_level/certification_type/follower_count 等)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
$data = extractFields(MEDIA_FIELDS);
|
||||||
|
if (!in_array($data['owner_type'] ?? '', ['person', 'company'], true)) {
|
||||||
|
Response::error('owner_type 必须为 person 或 company', 400);
|
||||||
|
}
|
||||||
|
if (empty($data['platform']) || empty($data['account_id'])) {
|
||||||
|
Response::error('平台(platform)和账号(account_id)为必填项', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// owner 存在性校验
|
||||||
|
if ($data['owner_type'] === 'person') {
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE id = ?");
|
||||||
|
} else {
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
|
||||||
|
}
|
||||||
|
$chk->execute([$data['owner_id']]);
|
||||||
|
if ((int)$chk->fetchColumn() === 0) {
|
||||||
|
Response::error('归属对象不存在', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// platform + account_id 唯一
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM social_accounts WHERE platform = ? AND account_id = ?");
|
||||||
|
$chk->execute([$data['platform'], $data['account_id']]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
Response::error('该平台账号已存在', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sql, $params] = buildInsert($data);
|
||||||
|
$pdo->prepare("INSERT INTO social_accounts $sql")->execute($params);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
// 写入商业属性
|
||||||
|
$attr = extractFields(MEDIA_ATTR_FIELDS);
|
||||||
|
if (!empty($attr)) {
|
||||||
|
$attr['social_account_id'] = $newId;
|
||||||
|
[$attrSql, $attrParams] = buildInsert($attr);
|
||||||
|
$pdo->prepare("INSERT INTO media_commercial_attributes $attrSql")->execute($attrParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
logCurrent('add', 'media', 'social_accounts', $newId, ['data' => $data, 'attr' => $attr]);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据删除接口(软删除) POST /api/media/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("UPDATE social_accounts SET is_active = 0 WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'media', 'social_accounts', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 条记录");
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据详情接口 GET /api/media/detail.php?id=1
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
$id = (int)($_REQUEST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT sa.*, mca.account_level, mca.content_categories, mca.follower_count,
|
||||||
|
mca.avg_read_count, mca.certification_type, mca.special_requirements, mca.media_remark
|
||||||
|
FROM social_accounts sa
|
||||||
|
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
|
||||||
|
WHERE sa.id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) {
|
||||||
|
Response::error('媒体账号不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success(['media' => $row]);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据导出接口(CSV) GET/POST /api/media/export.php
|
||||||
|
* 参数同 list.php
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
// 仅导出勾选的媒体账号
|
||||||
|
$idsRaw = trim($_REQUEST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($idsRaw !== '') {
|
||||||
|
foreach (explode(',', $idsRaw) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) {
|
||||||
|
$idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('请先选择需要导出的媒体数据', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = ['sa.id IN (' . implode(',', array_fill(0, count($idList), '?')) . ')', 'sa.is_active = 1'];
|
||||||
|
$params = $idList;
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url, sa.remark,
|
||||||
|
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
|
||||||
|
mca.certification_type, sa.created_at,
|
||||||
|
CASE sa.owner_type
|
||||||
|
WHEN 'person' THEN (SELECT p.full_name FROM persons p WHERE p.id = sa.owner_id)
|
||||||
|
WHEN 'company' THEN (SELECT c.display_name FROM companies c WHERE c.id = sa.owner_id)
|
||||||
|
ELSE NULL END AS owner_name
|
||||||
|
FROM social_accounts sa
|
||||||
|
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
|
||||||
|
WHERE $whereSql ORDER BY sa.id DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
logCurrent('export', 'media', 'social_accounts', null, ['count' => count($list)]);
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="media_' . date('Ymd_His') . '.csv"');
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
fputcsv($out, ['ID', '归属类型', '归属ID', '归属名称', '平台', '账号', '主页链接', '备注', '等级', '内容领域', '粉丝数', '平均阅读', '认证类型', '创建时间']);
|
||||||
|
foreach ($list as $r) {
|
||||||
|
fputcsv($out, [
|
||||||
|
$r['id'], $r['owner_type'], $r['owner_id'], $r['owner_name'], $r['platform'],
|
||||||
|
$r['account_id'], $r['profile_url'], $r['remark'], $r['account_level'],
|
||||||
|
$r['content_categories'], $r['follower_count'], $r['avg_read_count'],
|
||||||
|
$r['certification_type'], $r['created_at'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fclose($out);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据 CSV 导入接口 POST /api/media/import.php (multipart/form-data, 字段名 file)
|
||||||
|
* CSV 表头:owner_type,owner_id,platform,account_id,profile_url,remark,is_primary,
|
||||||
|
* account_level,content_categories,follower_count,avg_read_count,certification_type
|
||||||
|
* owner_type 为 person/company 时 owner_id 也可填对应名称(display_name/full_name),自动解析为ID。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
Response::error('请选择要上传的CSV文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($_FILES['file']['tmp_name'], 'r');
|
||||||
|
if (!$handle) {
|
||||||
|
Response::error('无法读取文件', 400);
|
||||||
|
}
|
||||||
|
$first = preg_replace('/^\xEF\xBB\xBF/', '', fgets($handle));
|
||||||
|
$header = str_getcsv(trim($first));
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$insAccount = $pdo->prepare(
|
||||||
|
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_active)
|
||||||
|
VALUES (?,?,?,?,?,?,?,1)"
|
||||||
|
);
|
||||||
|
$insAttr = $pdo->prepare(
|
||||||
|
"INSERT INTO media_commercial_attributes
|
||||||
|
(social_account_id, account_level, content_categories, follower_count, avg_read_count, certification_type)
|
||||||
|
VALUES (?,?,?,?,?,?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$inserted = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle)) !== false) {
|
||||||
|
$row = array_map('trim', $row);
|
||||||
|
$rec = [];
|
||||||
|
foreach ($header as $idx => $col) {
|
||||||
|
$col = trim($col);
|
||||||
|
if (isset($row[$idx])) {
|
||||||
|
$rec[$col] = $row[$idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($rec['platform']) || empty($rec['account_id']) || empty($rec['owner_type'])) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ownerType = $rec['owner_type'] === 'company' ? 'company' : 'person';
|
||||||
|
$ownerId = (int)($rec['owner_id'] ?? 0);
|
||||||
|
if ($ownerId <= 0) {
|
||||||
|
// 按名称解析
|
||||||
|
$nameCol = $ownerType === 'company' ? 'display_name' : 'full_name';
|
||||||
|
$tbl = $ownerType === 'company' ? 'companies' : 'persons';
|
||||||
|
$find = $pdo->prepare("SELECT id FROM $tbl WHERE $nameCol = ? LIMIT 1");
|
||||||
|
$find->execute([$rec['owner_name'] ?? $rec['owner_id'] ?? '']);
|
||||||
|
$ownerId = (int)$find->fetchColumn();
|
||||||
|
}
|
||||||
|
if ($ownerId <= 0) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM social_accounts WHERE platform = ? AND account_id = ?");
|
||||||
|
$chk->execute([$rec['platform'], $rec['account_id']]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$insAccount->execute([
|
||||||
|
$ownerType, $ownerId, $rec['platform'], $rec['account_id'],
|
||||||
|
$rec['profile_url'] ?? null, $rec['remark'] ?? null, !empty($rec['is_primary']) ? 1 : 0,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
if (!empty($rec['account_level']) || !empty($rec['certification_type'])) {
|
||||||
|
$insAttr->execute([
|
||||||
|
$newId, $rec['account_level'] ?? null, $rec['content_categories'] ?? null,
|
||||||
|
(int)($rec['follower_count'] ?? 0), (int)($rec['avg_read_count'] ?? 0),
|
||||||
|
$rec['certification_type'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
logCurrent('import', 'media', 'social_accounts', null, ['inserted' => $inserted, 'failed' => $failed]);
|
||||||
|
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据列表接口 GET/POST /api/media/list.php
|
||||||
|
* 参数:page / limit / keyword / platform / certification_type / account_level / owner_type / is_active
|
||||||
|
* 数据源:social_accounts JOIN media_commercial_attributes
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$platform = trim($_REQUEST['platform'] ?? '');
|
||||||
|
$certType = trim($_REQUEST['certification_type'] ?? '');
|
||||||
|
$level = trim($_REQUEST['account_level'] ?? '');
|
||||||
|
$ownerType = trim($_REQUEST['owner_type'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['sa.is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($active !== null) {
|
||||||
|
$where = ['sa.is_active = ?'];
|
||||||
|
$params = [$active];
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = "(sa.account_id LIKE ? OR sa.profile_url LIKE ? OR sa.owner_id IN (
|
||||||
|
SELECT p.id FROM persons p WHERE p.full_name LIKE ?
|
||||||
|
UNION SELECT c.id FROM companies c WHERE c.display_name LIKE ?
|
||||||
|
))";
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($platform !== '') { $where[] = 'sa.platform = ?'; $params[] = $platform; }
|
||||||
|
if ($certType !== '') { $where[] = 'mca.certification_type = ?'; $params[] = $certType; }
|
||||||
|
if ($level !== '') { $where[] = 'mca.account_level = ?'; $params[] = $level; }
|
||||||
|
if ($ownerType !== '') { $where[] = 'sa.owner_type = ?'; $params[] = $ownerType; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM social_accounts sa
|
||||||
|
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
|
||||||
|
WHERE $whereSql"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url,
|
||||||
|
sa.remark, sa.is_primary, sa.is_active, sa.created_at,
|
||||||
|
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
|
||||||
|
mca.certification_type, mca.special_requirements, mca.media_remark,
|
||||||
|
CASE sa.owner_type
|
||||||
|
WHEN 'person' THEN (SELECT p.full_name FROM persons p WHERE p.id = sa.owner_id)
|
||||||
|
WHEN 'company' THEN (SELECT c.display_name FROM companies c WHERE c.id = sa.owner_id)
|
||||||
|
ELSE NULL END AS owner_name
|
||||||
|
FROM social_accounts sa
|
||||||
|
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY sa.id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 供前端筛选下拉使用:平台/认证类型/等级 字典
|
||||||
|
$dict = [
|
||||||
|
'platforms' => $pdo->query("SELECT DISTINCT platform FROM social_accounts WHERE is_active = 1 ORDER BY platform")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
|
'certification_types' => $pdo->query("SELECT DISTINCT certification_type FROM media_commercial_attributes WHERE certification_type IS NOT NULL AND certification_type <> '' ORDER BY certification_type")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
|
'account_levels' => $pdo->query("SELECT DISTINCT account_level FROM media_commercial_attributes ORDER BY account_level")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
|
];
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'dict' => $dict]);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 媒体数据编辑接口 POST /api/media/update.php
|
||||||
|
* 入参:id + 白名单字段(含商业属性)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('media');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$check = $pdo->prepare("SELECT * FROM social_accounts WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('媒体账号不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = extractFields(MEDIA_FIELDS);
|
||||||
|
unset($data['owner_type'], $data['owner_id']); // 归属关系不允许改
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
[$sets, $params] = buildUpdate($data);
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE social_accounts SET $sets WHERE id = ?")->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新商业属性(存在则更新,不存在则插入)
|
||||||
|
$attr = extractFields(MEDIA_ATTR_FIELDS);
|
||||||
|
if (!empty($attr)) {
|
||||||
|
$exists = $pdo->prepare("SELECT COUNT(*) FROM media_commercial_attributes WHERE social_account_id = ?");
|
||||||
|
$exists->execute([$id]);
|
||||||
|
if ((int)$exists->fetchColumn() > 0) {
|
||||||
|
[$attrSets, $attrParams] = buildUpdate($attr);
|
||||||
|
$attrParams[] = $id;
|
||||||
|
$pdo->prepare("UPDATE media_commercial_attributes SET $attrSets WHERE social_account_id = ?")->execute($attrParams);
|
||||||
|
} else {
|
||||||
|
$attr['social_account_id'] = $id;
|
||||||
|
[$attrSql, $attrParams] = buildInsert($attr);
|
||||||
|
$pdo->prepare("INSERT INTO media_commercial_attributes $attrSql")->execute($attrParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logCurrent('update', 'media', 'social_accounts', $id, ['before' => $old, 'after' => $data, 'attr' => $attr]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求新增接口 POST /api/need/add.php
|
||||||
|
* 必填:company_id;可选白名单字段见 NEED_FIELDS
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
$data = extractFields(NEED_FIELDS);
|
||||||
|
if (empty($data['company_id'])) {
|
||||||
|
Response::error('所属公司(company_id)为必填项', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
|
||||||
|
$chk->execute([$data['company_id']]);
|
||||||
|
if ((int)$chk->fetchColumn() === 0) {
|
||||||
|
Response::error('所属公司不存在', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sql, $params] = buildInsert($data);
|
||||||
|
$pdo->prepare("INSERT INTO company_needs $sql")->execute($params);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
logCurrent('add', 'need', 'company_needs', $newId, $data);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求删除接口(作废) POST /api/need/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("UPDATE company_needs SET is_valid = 0 WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'need', 'company_needs', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已作废 $affected 条需求");
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求详情接口 GET /api/need/detail.php?id=1
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
$id = (int)($_REQUEST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT cn.*, c.display_name, c.industry, c.country, c.website
|
||||||
|
FROM company_needs cn
|
||||||
|
LEFT JOIN companies c ON c.id = cn.company_id
|
||||||
|
WHERE cn.id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$need = $stmt->fetch();
|
||||||
|
if (!$need) {
|
||||||
|
Response::error('需求不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success(['need' => $need]);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求转盘列表接口 GET/POST /api/need/list.php
|
||||||
|
* 参数:page / limit / keyword / start_date / end_date / company_name / industry / need_category / is_valid
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$startDate = trim($_REQUEST['start_date'] ?? '');
|
||||||
|
$endDate = trim($_REQUEST['end_date'] ?? '');
|
||||||
|
$companyName = trim($_REQUEST['company_name'] ?? '');
|
||||||
|
$industry = trim($_REQUEST['industry'] ?? '');
|
||||||
|
$category = trim($_REQUEST['need_category'] ?? '');
|
||||||
|
$valid = isset($_REQUEST['is_valid']) && $_REQUEST['is_valid'] !== '' ? (int)$_REQUEST['is_valid'] : null;
|
||||||
|
|
||||||
|
$where = ['cn.is_valid = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($valid !== null) {
|
||||||
|
$where = ['cn.is_valid = ?'];
|
||||||
|
$params = [$valid];
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(cn.description LIKE ? OR cn.contact_person LIKE ? OR c.display_name LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($startDate !== '') { $where[] = 'DATE(cn.created_at) >= ?'; $params[] = $startDate; }
|
||||||
|
if ($endDate !== '') { $where[] = 'DATE(cn.created_at) <= ?'; $params[] = $endDate; }
|
||||||
|
if ($companyName !== '') { $where[] = 'c.display_name LIKE ?'; $params[] = "%$companyName%"; }
|
||||||
|
if ($industry !== '') { $where[] = 'c.industry = ?'; $params[] = $industry; }
|
||||||
|
if ($category !== '') { $where[] = 'cn.need_category = ?'; $params[] = $category; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM company_needs cn LEFT JOIN companies c ON c.id = cn.company_id WHERE $whereSql"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT cn.id, cn.company_id, c.display_name, c.industry, cn.contact_person,
|
||||||
|
cn.need_category, cn.target_product_category, cn.application_scenario,
|
||||||
|
cn.description, cn.is_valid, cn.created_at
|
||||||
|
FROM company_needs cn
|
||||||
|
LEFT JOIN companies c ON c.id = cn.company_id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY cn.id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求推送接口(占位) POST /api/need/push.php
|
||||||
|
* 入参:id(或 ids 逗号分隔)
|
||||||
|
* 说明:当前为占位实现,仅写日志并返回成功;
|
||||||
|
* 后续可扩展为调用第三方API推送JSON数据 / 通过SMTP发送邮件。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("SELECT id, company_id FROM company_needs WHERE id IN ($in) AND is_valid = 1");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$needs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if (!$needs) {
|
||||||
|
Response::error('没有可推送的有效需求');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 在此接入第三方API推送 / SMTP邮件发送
|
||||||
|
// 示例:
|
||||||
|
// foreach ($needs as $n) { sendToThirdParty($n); sendEmail($n); }
|
||||||
|
|
||||||
|
logCurrent('push', 'need', 'company_needs', null, ['ids' => $idList, 'count' => count($needs)]);
|
||||||
|
Response::success(['count' => count($needs)], "已推送 " . count($needs) . " 条需求(占位实现,后续接入第三方API/邮件)");
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 需求编辑接口 POST /api/need/update.php
|
||||||
|
* 入参:id + 白名单字段(可含 is_valid 作废/恢复)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('need');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is_valid 允许单独传
|
||||||
|
$data = extractFields(NEED_FIELDS);
|
||||||
|
if (isset($_POST['is_valid']) && $_POST['is_valid'] !== '') {
|
||||||
|
$data['is_valid'] = (int)$_POST['is_valid'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (empty($data)) {
|
||||||
|
Response::error('没有需要更新的字段', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$check = $pdo->prepare("SELECT * FROM company_needs WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('需求不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sets, $params] = buildUpdate($data);
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE company_needs SET $sets WHERE id = ?")->execute($params);
|
||||||
|
|
||||||
|
logCurrent('update', 'need', 'company_needs', $id, ['before' => $old, 'after' => $data]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员新增接口 POST /api/person/add.php
|
||||||
|
* 必填:full_name;union_id 为空时自动生成。
|
||||||
|
* 可选:contacts(JSON 数组,如 [{"platform":"email","account_id":"a@b.com","is_primary":1}])
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
$data = extractFields(PERSON_FIELDS);
|
||||||
|
if (empty($data['full_name'])) {
|
||||||
|
Response::error('姓名(full_name)为必填项', 400);
|
||||||
|
}
|
||||||
|
if (empty($data['union_id'])) {
|
||||||
|
$data['union_id'] = 'P' . date('YmdHis') . substr(uniqid(), -6);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// union_id 唯一性
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE union_id = ?");
|
||||||
|
$chk->execute([$data['union_id']]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
Response::error('union_id 已存在,请更换');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$sql, $params] = buildInsert($data);
|
||||||
|
$pdo->prepare("INSERT INTO persons $sql")->execute($params);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
// 写入联系方式
|
||||||
|
$contacts = json_decode($_POST['contacts'] ?? '[]', true);
|
||||||
|
if (is_array($contacts)) {
|
||||||
|
$ins = $pdo->prepare(
|
||||||
|
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary)
|
||||||
|
VALUES ('person', ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
foreach ($contacts as $c) {
|
||||||
|
$platform = trim($c['platform'] ?? '');
|
||||||
|
$accountId = trim($c['account_id'] ?? '');
|
||||||
|
if ($platform === '' || $accountId === '') continue;
|
||||||
|
$ins->execute([$newId, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logCurrent('add', 'person', 'persons', $newId, ['data' => $data, 'contacts' => $contacts]);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员任职公司列表接口 GET /api/person/companies.php
|
||||||
|
* 入参:person_id(必填)/ page / limit
|
||||||
|
* 返回:该人员任职的公司列表(公司名称/部门/岗位/职级/入职日期/是否在岗)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
$personId = (int)($_REQUEST['person_id'] ?? 0);
|
||||||
|
if ($personId <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams(5);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE id = ?");
|
||||||
|
$chk->execute([$personId]);
|
||||||
|
if ((int)$chk->fetchColumn() === 0) {
|
||||||
|
Response::error('人员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*) FROM person_work_experiences pwe
|
||||||
|
INNER JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.person_id = ? AND pwe.is_active = 1"
|
||||||
|
);
|
||||||
|
$stmt->execute([$personId]);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT pwe.id AS work_id, pwe.company_id,
|
||||||
|
CASE WHEN c.name_zh IS NOT NULL AND c.name_zh <> '' THEN c.name_zh
|
||||||
|
ELSE COALESCE(c.name_en, c.display_name) END AS company_name,
|
||||||
|
c.industry, pwe.department, pwe.position, pwe.job_level,
|
||||||
|
pwe.start_date, pwe.end_date, pwe.is_current
|
||||||
|
FROM person_work_experiences pwe
|
||||||
|
INNER JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.person_id = ? AND pwe.is_active = 1
|
||||||
|
ORDER BY pwe.is_current DESC, pwe.start_date DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute([$personId]);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员删除接口(软删除) POST /api/person/delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("UPDATE persons SET is_active = 0 WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'person', 'persons', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 条记录");
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员详情接口 GET /api/person/detail.php?id=1
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
$id = (int)($_REQUEST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM persons WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$person = $stmt->fetch();
|
||||||
|
if (!$person) {
|
||||||
|
Response::error('人员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accounts = $pdo->prepare("SELECT id, platform, account_id, profile_url, remark, is_primary, is_active FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? ORDER BY is_primary DESC, id DESC");
|
||||||
|
$accounts->execute([$id]);
|
||||||
|
|
||||||
|
$experiences = $pdo->prepare(
|
||||||
|
"SELECT pwe.id, pwe.company_id, c.display_name, c.industry, pwe.position, pwe.department,
|
||||||
|
pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current
|
||||||
|
FROM person_work_experiences pwe
|
||||||
|
LEFT JOIN companies c ON c.id = pwe.company_id
|
||||||
|
WHERE pwe.person_id = ? AND pwe.is_active = 1
|
||||||
|
ORDER BY pwe.is_current DESC, pwe.start_date DESC"
|
||||||
|
);
|
||||||
|
$experiences->execute([$id]);
|
||||||
|
|
||||||
|
Response::success(['person' => $person, 'accounts' => $accounts->fetchAll(), 'experiences' => $experiences->fetchAll()]);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员导出接口(CSV) GET/POST /api/person/export.php
|
||||||
|
* 参数同 list.php
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
// 仅导出勾选的人员
|
||||||
|
$idsRaw = trim($_REQUEST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($idsRaw !== '') {
|
||||||
|
foreach (explode(',', $idsRaw) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) {
|
||||||
|
$idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('请先选择需要导出的人员数据', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = ['p.id IN (' . implode(',', array_fill(0, count($idList), '?')) . ')', 'p.is_active = 1'];
|
||||||
|
$params = $idList;
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
|
||||||
|
p.education, p.graduated_from, p.hometown, p.work_location, p.created_at,
|
||||||
|
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='phone' AND sa.is_active=1 LIMIT 1) AS phone,
|
||||||
|
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='email' AND sa.is_active=1 LIMIT 1) AS email
|
||||||
|
FROM persons p WHERE $whereSql ORDER BY p.id DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
logCurrent('export', 'person', 'persons', null, ['count' => count($list)]);
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="persons_' . date('Ymd_His') . '.csv"');
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
fputcsv($out, ['ID', 'union_id', '姓名', '性别', '国籍', '证件类型', '证件号', '学历', '毕业院校', '家乡', '工作所在地', '手机', '邮箱', '创建时间']);
|
||||||
|
foreach ($list as $r) {
|
||||||
|
fputcsv($out, [
|
||||||
|
$r['id'], $r['union_id'], $r['full_name'], $r['gender'], $r['nationality'],
|
||||||
|
$r['id_type'], $r['id_number'], $r['education'], $r['graduated_from'],
|
||||||
|
$r['hometown'], $r['work_location'], $r['phone'], $r['email'], $r['created_at'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fclose($out);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员 CSV 导入接口 POST /api/person/import.php (multipart/form-data, 字段名 file)
|
||||||
|
* CSV 表头:union_id,full_name,gender,nationality,id_type,id_number,education,
|
||||||
|
* graduated_from,hometown,work_location,phone,email
|
||||||
|
* 仅 full_name 必填;union_id 留空自动生成;phone/email 写入 social_accounts。
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
Response::error('请选择要上传的CSV文件', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($_FILES['file']['tmp_name'], 'r');
|
||||||
|
if (!$handle) {
|
||||||
|
Response::error('无法读取文件', 400);
|
||||||
|
}
|
||||||
|
$first = preg_replace('/^\xEF\xBB\xBF/', '', fgets($handle));
|
||||||
|
$header = str_getcsv(trim($first));
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$insPerson = $pdo->prepare(
|
||||||
|
"INSERT INTO persons (union_id, full_name, gender, nationality, id_type, id_number, education, graduated_from, hometown, work_location)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
);
|
||||||
|
$insAccount = $pdo->prepare(
|
||||||
|
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary)
|
||||||
|
VALUES ('person', ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$inserted = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle)) !== false) {
|
||||||
|
$row = array_map('trim', $row);
|
||||||
|
$rec = [];
|
||||||
|
foreach ($header as $idx => $col) {
|
||||||
|
$col = trim($col);
|
||||||
|
if (isset($row[$idx])) {
|
||||||
|
$rec[$col] = $row[$idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($rec['full_name'])) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$unionId = !empty($rec['union_id']) ? $rec['union_id'] : ('P' . date('YmdHis') . substr(uniqid(), -6));
|
||||||
|
// 跳过重复 union_id
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE union_id = ?");
|
||||||
|
$chk->execute([$unionId]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$insPerson->execute([
|
||||||
|
$unionId, $rec['full_name'], $rec['gender'] ?? '保密', $rec['nationality'] ?? null,
|
||||||
|
$rec['id_type'] ?? null, $rec['id_number'] ?? null, $rec['education'] ?? null,
|
||||||
|
$rec['graduated_from'] ?? null, $rec['hometown'] ?? null, $rec['work_location'] ?? null,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
if (!empty($rec['phone'])) {
|
||||||
|
$insAccount->execute([$newId, 'phone', $rec['phone'], 1]);
|
||||||
|
}
|
||||||
|
if (!empty($rec['email'])) {
|
||||||
|
$insAccount->execute([$newId, 'email', $rec['email'], 0]);
|
||||||
|
}
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
logCurrent('import', 'person', 'persons', null, ['inserted' => $inserted, 'failed' => $failed]);
|
||||||
|
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员列表接口 GET/POST /api/person/list.php
|
||||||
|
* 参数:page / limit / keyword(姓名/证件号/手机/邮箱)/ gender / nationality / work_location / is_active
|
||||||
|
* 返回:{ list, total, page, limit }
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$gender = trim($_REQUEST['gender'] ?? '');
|
||||||
|
$nationality = trim($_REQUEST['nationality'] ?? '');
|
||||||
|
$location = trim($_REQUEST['work_location'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['p.is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($active !== null) {
|
||||||
|
$where = ['p.is_active = ?'];
|
||||||
|
$params = [$active];
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
// 姓名/证件号直查;手机/邮箱通过 social_accounts 关联
|
||||||
|
$where[] = "(p.full_name LIKE ? OR p.id_number LIKE ? OR p.union_id LIKE ? OR EXISTS (
|
||||||
|
SELECT 1 FROM social_accounts sa
|
||||||
|
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1
|
||||||
|
AND sa.account_id LIKE ?
|
||||||
|
))";
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($gender !== '') { $where[] = 'p.gender = ?'; $params[] = $gender; }
|
||||||
|
if ($nationality !== '') { $where[] = 'p.nationality = ?'; $params[] = $nationality; }
|
||||||
|
if ($location !== '') { $where[] = 'p.work_location LIKE ?'; $params[] = "%$location%"; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM persons p WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
|
||||||
|
p.education, p.graduated_from, p.hometown, p.work_location, p.is_active, p.created_at,
|
||||||
|
(SELECT GROUP_CONCAT(CONCAT(sa.platform, ':', sa.account_id) SEPARATOR ' | ')
|
||||||
|
FROM social_accounts sa WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1) AS contacts
|
||||||
|
FROM persons p
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY p.id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 人员编辑接口 POST /api/person/update.php
|
||||||
|
* 入参:id + 白名单字段;可选 contacts(JSON 数组,整体替换联系方式)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
require_once __DIR__ . '/../common/helpers.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('person');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = extractFields(PERSON_FIELDS);
|
||||||
|
unset($data['union_id']); // 不允许修改 union_id
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$check = $pdo->prepare("SELECT * FROM persons WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('人员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$contactsChanged = false;
|
||||||
|
if (isset($_POST['contacts'])) {
|
||||||
|
$contactsChanged = true;
|
||||||
|
// 整体替换联系方式:先删旧(保留非联系人性质?此处直接删除全部后重建)
|
||||||
|
$pdo->prepare("DELETE FROM social_accounts WHERE owner_type = 'person' AND owner_id = ?")->execute([$id]);
|
||||||
|
$contacts = json_decode($_POST['contacts'], true);
|
||||||
|
if (is_array($contacts)) {
|
||||||
|
$ins = $pdo->prepare(
|
||||||
|
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary)
|
||||||
|
VALUES ('person', ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
foreach ($contacts as $c) {
|
||||||
|
$platform = trim($c['platform'] ?? '');
|
||||||
|
$accountId = trim($c['account_id'] ?? '');
|
||||||
|
if ($platform === '' || $accountId === '') continue;
|
||||||
|
$ins->execute([$id, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
[$sets, $params] = buildUpdate($data);
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE persons SET $sets WHERE id = ?")->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
logCurrent('update', 'person', 'persons', $id, ['before' => $old, 'after' => $data, 'contacts_replaced' => $contactsChanged]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 碎片处理列表接口(待定逻辑,暂做基础列表) GET/POST /api/preliminary/list.php
|
||||||
|
* 参数:page / limit / keyword / status / source_type / start_date / end_date
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('preliminary');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$status = trim($_REQUEST['status'] ?? '');
|
||||||
|
$sourceType = trim($_REQUEST['source_type'] ?? '');
|
||||||
|
$startDate = trim($_REQUEST['start_date'] ?? '');
|
||||||
|
$endDate = trim($_REQUEST['end_date'] ?? '');
|
||||||
|
|
||||||
|
$where = ['is_active = 1'];
|
||||||
|
$params = [];
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(company_name LIKE ? OR person_name LIKE ? OR contact_phone LIKE ? OR contact_email LIKE ? OR description LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($status !== '') { $where[] = 'status = ?'; $params[] = $status; }
|
||||||
|
if ($sourceType !== '') { $where[] = 'source_type = ?'; $params[] = $sourceType; }
|
||||||
|
if ($startDate !== '') { $where[] = 'DATE(created_at) >= ?'; $params[] = $startDate; }
|
||||||
|
if ($endDate !== '') { $where[] = 'DATE(created_at) <= ?'; $params[] = $endDate; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM preliminary_data WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, source_type, company_name, person_name, contact_phone, contact_email,
|
||||||
|
target_product_category, description, status, source_channel, recorded_by,
|
||||||
|
follow_person, converted_type, converted_id, created_at, updated_at
|
||||||
|
FROM preliminary_data
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 碎片处理统计接口 GET /api/preliminary/stats.php
|
||||||
|
* 返回:总数 / 今日新增 / 各状态数量
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('preliminary');
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$total = (int)$pdo->query("SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1")->fetchColumn();
|
||||||
|
$today = (int)$pdo->query("SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1 AND DATE(created_at) = CURDATE()")->fetchColumn();
|
||||||
|
|
||||||
|
$statusRows = $pdo->query(
|
||||||
|
"SELECT status, COUNT(*) AS cnt FROM preliminary_data WHERE is_active = 1 GROUP BY status"
|
||||||
|
)->fetchAll();
|
||||||
|
$statusMap = [];
|
||||||
|
foreach ($statusRows as $r) {
|
||||||
|
$statusMap[$r['status']] = (int)$r['cnt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success([
|
||||||
|
'total' => $total,
|
||||||
|
'today' => $today,
|
||||||
|
'status' => $statusMap,
|
||||||
|
'statuses' => ['待处理', '待分配', '处理中', '已转换', '已废弃', '已拒收'],
|
||||||
|
]);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 操作日志查询接口 GET/POST /api/system/log_list.php
|
||||||
|
* 仅超级管理员可见,默认查询近1个月
|
||||||
|
* 参数:page / limit / keyword / action / module / username / start_date / end_date
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
requireSuperAdmin();
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$action = trim($_REQUEST['action'] ?? '');
|
||||||
|
$module = trim($_REQUEST['module'] ?? '');
|
||||||
|
$username = trim($_REQUEST['username'] ?? '');
|
||||||
|
$startDate = trim($_REQUEST['start_date'] ?? '');
|
||||||
|
$endDate = trim($_REQUEST['end_date'] ?? '');
|
||||||
|
|
||||||
|
$where = ['created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)'];
|
||||||
|
$params = [];
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(username LIKE ? OR content LIKE ? OR module LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($action !== '') { $where[] = 'action = ?'; $params[] = $action; }
|
||||||
|
if ($module !== '') { $where[] = 'module = ?'; $params[] = $module; }
|
||||||
|
if ($username !== '') { $where[] = 'username = ?'; $params[] = $username; }
|
||||||
|
if ($startDate !== '') { $where[] = 'DATE(created_at) >= ?'; $params[] = $startDate; }
|
||||||
|
if ($endDate !== '') { $where[] = 'DATE(created_at) <= ?'; $params[] = $endDate; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_logs WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, user_id, username, action, module, target_table, target_id, content, ip, created_at
|
||||||
|
FROM system_logs
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 筛选下拉字典
|
||||||
|
$dict = [
|
||||||
|
'actions' => $pdo->query("SELECT DISTINCT action FROM system_logs ORDER BY action")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
|
'modules' => $pdo->query("SELECT DISTINCT module FROM system_logs ORDER BY module")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
|
];
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'dict' => $dict]);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 新增角色接口(含权限分配) POST /api/system/role_add.php
|
||||||
|
* 入参:role_name / permissions(JSON数组,菜单标识)/ is_active
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$roleName = trim($_POST['role_name'] ?? '');
|
||||||
|
$perms = json_decode($_POST['permissions'] ?? '[]', true);
|
||||||
|
$isActive = isset($_POST['is_active']) ? ((int)$_POST['is_active'] ? 1 : 0) : 1;
|
||||||
|
|
||||||
|
if ($roleName === '') {
|
||||||
|
Response::error('角色名称不能为空', 400);
|
||||||
|
}
|
||||||
|
if (!is_array($perms)) {
|
||||||
|
Response::error('权限格式错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE role_name = ?");
|
||||||
|
$chk->execute([$roleName]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
Response::error('角色名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO system_roles (role_name, permissions, is_active) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$roleName, json_encode(array_values($perms), JSON_UNESCAPED_UNICODE), $isActive]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
logCurrent('add', 'system', 'system_roles', $newId, ['role_name' => $roleName, 'permissions' => $perms]);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 删除角色接口 POST /api/system/role_delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
* 保护:不允许删除内置超级管理员角色(id=1)及仍被用户使用的角色
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
if (in_array(1, $idList, true)) {
|
||||||
|
Response::error('不允许删除内置超级管理员角色', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
// 检查角色是否被使用
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("SELECT role_id, COUNT(*) AS cnt FROM system_users WHERE role_id IN ($in) GROUP BY role_id");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$inUse = $stmt->fetchAll();
|
||||||
|
if ($inUse) {
|
||||||
|
$names = [];
|
||||||
|
foreach ($inUse as $r) {
|
||||||
|
$names[] = "角色ID {$r['role_id']}({$r['cnt']}个用户)";
|
||||||
|
}
|
||||||
|
Response::error('以下角色仍被用户使用,无法删除:' . implode('、', $names), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$del = $pdo->prepare("DELETE FROM system_roles WHERE id IN ($in)");
|
||||||
|
$del->execute($idList);
|
||||||
|
$affected = $del->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'system', 'system_roles', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 个角色");
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 角色列表接口 GET/POST /api/system/role_list.php
|
||||||
|
* 参数:page / limit / keyword / is_active
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['1=1'];
|
||||||
|
$params = [];
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = 'role_name LIKE ?';
|
||||||
|
$params[] = "%$keyword%";
|
||||||
|
}
|
||||||
|
if ($active !== null) { $where[] = 'is_active = ?'; $params[] = $active; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, role_name, permissions, is_active, created_at,
|
||||||
|
(SELECT COUNT(*) FROM system_users u WHERE u.role_id = system_roles.id) AS user_count
|
||||||
|
FROM system_roles
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
foreach ($list as &$r) {
|
||||||
|
$r['permissions'] = json_decode($r['permissions'], true) ?: [];
|
||||||
|
}
|
||||||
|
unset($r);
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 编辑角色接口(含权限分配) POST /api/system/role_update.php
|
||||||
|
* 入参:id / role_name / permissions(JSON数组)/ is_active
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$check = $pdo->prepare("SELECT * FROM system_roles WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('角色不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员角色保护:不允许移除其自身
|
||||||
|
if ($id === 1) {
|
||||||
|
Response::error('内置超级管理员角色不允许修改', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sets = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (isset($_POST['role_name'])) {
|
||||||
|
$roleName = trim($_POST['role_name']);
|
||||||
|
if ($roleName === '') {
|
||||||
|
Response::error('角色名称不能为空', 400);
|
||||||
|
}
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE role_name = ? AND id <> ?");
|
||||||
|
$chk->execute([$roleName, $id]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
Response::error('角色名称已存在');
|
||||||
|
}
|
||||||
|
$sets[] = 'role_name = ?';
|
||||||
|
$params[] = $roleName;
|
||||||
|
}
|
||||||
|
if (isset($_POST['permissions'])) {
|
||||||
|
$perms = json_decode($_POST['permissions'], true);
|
||||||
|
if (!is_array($perms)) {
|
||||||
|
Response::error('权限格式错误', 400);
|
||||||
|
}
|
||||||
|
$sets[] = 'permissions = ?';
|
||||||
|
$params[] = json_encode(array_values($perms), JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
if (isset($_POST['is_active'])) {
|
||||||
|
$sets[] = 'is_active = ?';
|
||||||
|
$params[] = (int)$_POST['is_active'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sets) {
|
||||||
|
Response::error('没有需要更新的字段', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE system_roles SET " . implode(', ', $sets) . " WHERE id = ?")->execute($params);
|
||||||
|
|
||||||
|
logCurrent('update', 'system', 'system_roles', $id, ['before' => $old, 'after' => $sets]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 新增用户接口 POST /api/system/user_add.php
|
||||||
|
* 入参:username / password / real_name / role_id / is_active
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = (string)($_POST['password'] ?? '');
|
||||||
|
$realName = trim($_POST['real_name'] ?? '');
|
||||||
|
$roleId = (int)($_POST['role_id'] ?? 0);
|
||||||
|
$isActive = isset($_POST['is_active']) ? ((int)$_POST['is_active'] ? 1 : 0) : 1;
|
||||||
|
|
||||||
|
if ($username === '' || !preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) {
|
||||||
|
Response::error('账号需为3-50位字母/数字/下划线', 400);
|
||||||
|
}
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
Response::error('密码长度不能少于6位', 400);
|
||||||
|
}
|
||||||
|
if ($roleId <= 0) {
|
||||||
|
Response::error('请选择角色', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_users WHERE username = ?");
|
||||||
|
$chk->execute([$username]);
|
||||||
|
if ((int)$chk->fetchColumn() > 0) {
|
||||||
|
Response::error('账号已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$chkRole = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE id = ?");
|
||||||
|
$chkRole->execute([$roleId]);
|
||||||
|
if ((int)$chkRole->fetchColumn() === 0) {
|
||||||
|
Response::error('角色不存在', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO system_users (username, password, real_name, role_id, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$username, sha1($password), $realName !== '' ? $realName : null, $roleId, $isActive]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
logCurrent('add', 'system', 'system_users', $newId, ['username' => $username, 'role_id' => $roleId, 'is_active' => $isActive]);
|
||||||
|
Response::success(['id' => $newId], '新增成功');
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 删除用户接口 POST /api/system/user_delete.php
|
||||||
|
* 入参:id(或 ids 逗号分隔批量)
|
||||||
|
* 保护:不允许删除内置管理员(id=1)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$ids = trim($_POST['ids'] ?? '');
|
||||||
|
$idList = [];
|
||||||
|
if ($id > 0) $idList[] = $id;
|
||||||
|
if ($ids !== '') {
|
||||||
|
foreach (explode(',', $ids) as $v) {
|
||||||
|
$v = (int)trim($v);
|
||||||
|
if ($v > 0) $idList[] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idList = array_unique($idList);
|
||||||
|
if (!$idList) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
if (in_array(1, $idList, true)) {
|
||||||
|
Response::error('不允许删除内置管理员账号', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$in = implode(',', array_fill(0, count($idList), '?'));
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM system_users WHERE id IN ($in)");
|
||||||
|
$stmt->execute($idList);
|
||||||
|
$affected = $stmt->rowCount();
|
||||||
|
|
||||||
|
logCurrent('delete', 'system', 'system_users', null, ['ids' => $idList]);
|
||||||
|
Response::success(['affected' => $affected], "已删除 $affected 个用户");
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 用户列表接口 GET/POST /api/system/user_list.php
|
||||||
|
* 参数:page / limit / keyword / role_id / is_active
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
[$page, $limit] = pageParams();
|
||||||
|
$keyword = trim($_REQUEST['keyword'] ?? '');
|
||||||
|
$roleId = (int)($_REQUEST['role_id'] ?? 0);
|
||||||
|
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
|
||||||
|
|
||||||
|
$where = ['1=1'];
|
||||||
|
$params = [];
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$where[] = '(u.username LIKE ? OR u.real_name LIKE ?)';
|
||||||
|
$like = "%$keyword%";
|
||||||
|
array_push($params, $like, $like);
|
||||||
|
}
|
||||||
|
if ($roleId > 0) { $where[] = 'u.role_id = ?'; $params[] = $roleId; }
|
||||||
|
if ($active !== null) { $where[] = 'u.is_active = ?'; $params[] = $active; }
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_users u WHERE $whereSql");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT u.id, u.username, u.real_name, u.role_id, r.role_name, u.is_active,
|
||||||
|
u.last_login_time, u.last_login_ip, u.created_at
|
||||||
|
FROM system_users u
|
||||||
|
LEFT JOIN system_roles r ON r.id = u.role_id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY u.id ASC
|
||||||
|
LIMIT $limit OFFSET $offset"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$list = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 角色下拉
|
||||||
|
$roles = $pdo->query("SELECT id, role_name FROM system_roles WHERE is_active = 1 ORDER BY id")->fetchAll();
|
||||||
|
|
||||||
|
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'roles' => $roles]);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 编辑用户接口(含角色分配) POST /api/system/user_update.php
|
||||||
|
* 入参:id / real_name / role_id / is_active / password(可选,留空不改密码)
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../common/db.php';
|
||||||
|
require_once __DIR__ . '/../common/response.php';
|
||||||
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
|
|
||||||
|
checkAjax();
|
||||||
|
checkPermission('system');
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
Response::error('参数错误', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$check = $pdo->prepare("SELECT * FROM system_users WHERE id = ?");
|
||||||
|
$check->execute([$id]);
|
||||||
|
$old = $check->fetch();
|
||||||
|
if (!$old) {
|
||||||
|
Response::error('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内置管理员保护:不允许禁用 id=1(admin)
|
||||||
|
if ($id === 1 && isset($_POST['is_active']) && (int)$_POST['is_active'] === 0) {
|
||||||
|
Response::error('不允许禁用内置管理员账号', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sets = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (isset($_POST['real_name'])) {
|
||||||
|
$sets[] = 'real_name = ?';
|
||||||
|
$params[] = trim($_POST['real_name']) !== '' ? trim($_POST['real_name']) : null;
|
||||||
|
}
|
||||||
|
if (isset($_POST['role_id'])) {
|
||||||
|
$roleId = (int)$_POST['role_id'];
|
||||||
|
$chkRole = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE id = ?");
|
||||||
|
$chkRole->execute([$roleId]);
|
||||||
|
if ((int)$chkRole->fetchColumn() === 0) {
|
||||||
|
Response::error('角色不存在', 400);
|
||||||
|
}
|
||||||
|
$sets[] = 'role_id = ?';
|
||||||
|
$params[] = $roleId;
|
||||||
|
}
|
||||||
|
if (isset($_POST['is_active'])) {
|
||||||
|
$sets[] = 'is_active = ?';
|
||||||
|
$params[] = (int)$_POST['is_active'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
$password = (string)($_POST['password'] ?? '');
|
||||||
|
if ($password !== '') {
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
Response::error('密码长度不能少于6位', 400);
|
||||||
|
}
|
||||||
|
$sets[] = 'password = ?';
|
||||||
|
$params[] = sha1($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sets) {
|
||||||
|
Response::error('没有需要更新的字段', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $id;
|
||||||
|
$pdo->prepare("UPDATE system_users SET " . implode(', ', $sets) . " WHERE id = ?")->execute($params);
|
||||||
|
|
||||||
|
logCurrent('update', 'system', 'system_users', $id, ['before' => ['username' => $old['username'], 'role_id' => $old['role_id'], 'is_active' => $old['is_active']]]);
|
||||||
|
Response::success(null, '更新成功');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@echo off
|
||||||
|
rem ============================================================
|
||||||
|
rem 版本号递增(Windows 双击运行)
|
||||||
|
rem 每次修改代码后双击本文件,版本号 v1.0.01 -> v1.0.02 ...
|
||||||
|
rem ============================================================
|
||||||
|
setlocal
|
||||||
|
set SCRIPT=%~dp0tools\bump_version.php
|
||||||
|
|
||||||
|
rem 1) 优先使用 PATH 中的 php
|
||||||
|
where php >nul 2>nul
|
||||||
|
if %errorlevel%==0 (
|
||||||
|
php "%SCRIPT%"
|
||||||
|
goto :done
|
||||||
|
)
|
||||||
|
|
||||||
|
rem 2) 自动查找 phpstudy 内置 PHP
|
||||||
|
if exist "C:\phpstudy_pro\Extensions\php" (
|
||||||
|
for /r "C:\phpstudy_pro\Extensions\php" %%f in (php.exe) do (
|
||||||
|
"%%f" "%SCRIPT%"
|
||||||
|
goto :done
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [错误] 未找到 php.exe,请安装 phpstudy 或将 PHP 加入 PATH。
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:done
|
||||||
|
pause
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>渠道管理 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/channel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>企业数据 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/company.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 数据库配置文件
|
||||||
|
* 返回数组,供 api/common/db.php 读取
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'host' => '127.0.0.1',
|
||||||
|
'port' => 3306,
|
||||||
|
'dbname' => 'mysuperlink',
|
||||||
|
'username' => 'mysuperlink_root',
|
||||||
|
'password' => '123456',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
];
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>综合概览 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="static/libs/layer.js"></script>
|
||||||
|
<script src="static/libs/echarts.min.js"></script>
|
||||||
|
<script src="static/libs/world.js"></script>
|
||||||
|
<script src="static/libs/china.js"></script>
|
||||||
|
<script src="static/js/config.js"></script>
|
||||||
|
<script src="static/js/common.js"></script>
|
||||||
|
<script src="static/js/dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>数据定制 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/marketing.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>文件管理 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/document.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EDM筛选 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/marketing.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>登录 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/login.css">
|
||||||
|
<link rel="stylesheet" href="static/css/slider.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-wrap">
|
||||||
|
<div class="login-title">Super<span style="color:#4fa3ff;">Link</span> 管理系统</div>
|
||||||
|
<div class="login-sub">企业数据 · 营销服务 · 渠道管理</div>
|
||||||
|
<div id="login-msg" class="login-msg"></div>
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>账号</label>
|
||||||
|
<input type="text" id="username" placeholder="请输入账号" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" id="password" placeholder="请输入密码" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>安全验证</label>
|
||||||
|
<div id="slider"></div>
|
||||||
|
</div>
|
||||||
|
<button class="login-btn" id="login-btn" disabled>登 录</button>
|
||||||
|
<div class="login-links">
|
||||||
|
<a href="javascript:;" id="btn-forgot">找回密码</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="login-copyright">© SuperLink Web 后台管理系统</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="static/libs/layer.js"></script>
|
||||||
|
<script src="static/js/config.js"></script>
|
||||||
|
<script src="static/libs/slider.js"></script>
|
||||||
|
<script src="static/js/login.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>操作日志 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/system.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>媒体数据 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/media.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>媒体批发 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/marketing.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>需求转盘 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/need.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>人员数据 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/person.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>碎片处理 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/preliminary.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>角色管理 - SuperLink</title>
|
||||||
|
<link rel="stylesheet" href="static/libs/layer.css">
|
||||||
|
<link rel="stylesheet" href="static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||||
|
<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/system.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- SuperLink Web 后台管理系统 - 新增系统表
|
||||||
|
-- 数据库:mysuperlink (utf8mb4 / utf8mb4_unicode_ci)
|
||||||
|
-- 说明:
|
||||||
|
-- 1) 原库已有14张业务表(companies/persons/social_accounts/company_needs 等),本文件不改动它们。
|
||||||
|
-- 2) 本文件新增文档要求的 3 张系统表:system_users / system_roles / system_logs(含默认数据)。
|
||||||
|
-- 3) 另新增 1 张业务表 channels(渠道管理模块需要,原14张表中没有渠道表),
|
||||||
|
-- 如不需要渠道模块可直接跳过该段。
|
||||||
|
-- 执行方式:Navicat / phpMyAdmin / mysql 命令行执行本文件即可(可重复执行前请先删除旧表)。
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 1. 系统用户表
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `system_users`;
|
||||||
|
CREATE TABLE `system_users` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||||
|
`password` VARCHAR(64) NOT NULL COMMENT '密码(sha1加密)',
|
||||||
|
`real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
|
||||||
|
`role_id` INT UNSIGNED NOT NULL COMMENT '角色ID',
|
||||||
|
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态(0禁用,1启用)',
|
||||||
|
`last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
|
||||||
|
`last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_username` (`username`),
|
||||||
|
KEY `idx_role_id` (`role_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表';
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 2. 系统角色表
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `system_roles`;
|
||||||
|
CREATE TABLE `system_roles` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
|
||||||
|
`permissions` JSON NOT NULL COMMENT '权限列表(菜单标识数组)',
|
||||||
|
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_role_name` (`role_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统角色表';
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 3. 操作日志表
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `system_logs`;
|
||||||
|
CREATE TABLE `system_logs` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`user_id` INT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||||
|
`username` VARCHAR(50) NOT NULL COMMENT '操作人账号',
|
||||||
|
`action` VARCHAR(50) NOT NULL COMMENT '操作类型(login/logout/add/update/delete/import/export/audit/convert)',
|
||||||
|
`module` VARCHAR(50) NOT NULL COMMENT '操作模块(company/person/media/need/preliminary/system等)',
|
||||||
|
`target_table` VARCHAR(50) DEFAULT NULL COMMENT '操作对象表名',
|
||||||
|
`target_id` INT UNSIGNED DEFAULT NULL COMMENT '操作对象记录ID',
|
||||||
|
`content` JSON DEFAULT NULL COMMENT '操作内容(变更前后数据)',
|
||||||
|
`ip` VARCHAR(50) DEFAULT NULL COMMENT '客户端IP',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_action` (`action`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 4. 默认数据(必须插入)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 超级管理员角色
|
||||||
|
INSERT INTO `system_roles` (`role_name`, `permissions`) VALUES
|
||||||
|
('超级管理员', '["dashboard","company","person","media","need","marketing","channel","document","preliminary","system","log"]');
|
||||||
|
|
||||||
|
-- 默认管理员账号(密码:admin123,sha1加密)
|
||||||
|
INSERT INTO `system_users` (`username`, `password`, `real_name`, `role_id`) VALUES
|
||||||
|
('admin', SHA1('admin123'), '系统管理员', 1);
|
||||||
|
|
||||||
|
-- 基础角色(可选,建议一并创建)
|
||||||
|
INSERT INTO `system_roles` (`role_name`, `permissions`) VALUES
|
||||||
|
('数据管理员', '["dashboard","company","person","media","document","preliminary"]'),
|
||||||
|
('业务专员', '["dashboard","need","marketing"]'),
|
||||||
|
('只读访客', '["dashboard"]');
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 5. 渠道管理业务表(原14张表中无渠道表,渠道模块所需,按需创建)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `channels`;
|
||||||
|
CREATE TABLE `channels` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`channel_name` VARCHAR(100) NOT NULL COMMENT '渠道名称',
|
||||||
|
`channel_type` VARCHAR(50) DEFAULT NULL COMMENT '渠道类型(线上/线下/展会/转介绍等)',
|
||||||
|
`contact_person` VARCHAR(50) DEFAULT NULL COMMENT '联系人',
|
||||||
|
`contact_phone` VARCHAR(50) DEFAULT NULL COMMENT '联系电话',
|
||||||
|
`contact_email` VARCHAR(100) DEFAULT NULL COMMENT '联系邮箱',
|
||||||
|
`efficiency_score` DECIMAL(5,2) DEFAULT 0 COMMENT '效率评分(0-100)',
|
||||||
|
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||||
|
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_channel_type` (`channel_type`),
|
||||||
|
KEY `idx_is_active` (`is_active`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道管理表';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 会话与权限 ============ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前页面权限,无权限则提示并跳回首页
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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: '操作日志' }
|
||||||
|
];
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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('网络错误,请重试');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 || '已提交');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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: ['关闭'] });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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: ['关闭'] });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
Vendored
+45
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user