v1.0.19: 修复人员编辑入口缺失(person.js addPerson/editPerson) + 渠道明细数量字段名(analysis.php cnt) + CDP全量验证43项PASS

This commit is contained in:
nanguaboss
2026-08-08 19:29:14 +08:00
parent 2cb688ed46
commit 1c8b374a72
40 changed files with 2313 additions and 658 deletions
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* 渠道效能分析接口 GET /api/channel/analysis.php?year=2025
* v1.0.18 渠道管理 - 渠道效能分析:
* - 企业/人员两个维度,分别按 source_channel(渠道字段)分组计数、按数量降序、计算百分比
* - 每个渠道附带 source_detail TOP10 排名(来源/数量)
* - year 为空 = 全部年份;否则按创建年份过滤
* 返回:{ years, company: [{channel, count, percent, details:[{source,count}]}], person: [...] }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
$year = trim($_REQUEST['year'] ?? '');
if ($year !== '' && (!ctype_digit($year) || (int)$year < 2000 || (int)$year > 2100)) {
$year = '';
}
$pdo = DB::getInstance()->getPdo();
// 可选年份(企业/人员创建年份并集)
$years = $pdo->query(
"SELECT DISTINCT y FROM (
SELECT YEAR(created_at) AS y FROM companies WHERE is_active = 1 AND created_at IS NOT NULL
UNION
SELECT YEAR(created_at) AS y FROM persons WHERE is_active = 1 AND created_at IS NOT NULL
) t ORDER BY y DESC"
)->fetchAll(PDO::FETCH_COLUMN);
/** 单维度渠道分析 */
function channelAnalysis($pdo, $table, $year)
{
$where = 'is_active = 1 AND source_channel IS NOT NULL AND source_channel <> \'\'';
$params = [];
if ($year !== '') {
$where .= ' AND YEAR(created_at) = ?';
$params[] = (int)$year;
}
$stmt = $pdo->prepare(
"SELECT source_channel AS channel, COUNT(*) AS cnt
FROM $table WHERE $where
GROUP BY source_channel ORDER BY cnt DESC"
);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$total = 0;
foreach ($rows as $r) {
$total += (int)$r['cnt'];
}
$result = [];
foreach ($rows as $r) {
$channel = $r['channel'];
// 该渠道 source_detail TOP10
$detailStmt = $pdo->prepare(
"SELECT COALESCE(NULLIF(TRIM(source_detail), ''), '(未填写)') AS source, COUNT(*) AS cnt
FROM $table
WHERE is_active = 1 AND source_channel = ?" . ($year !== '' ? ' AND YEAR(created_at) = ?' : '') . "
GROUP BY source ORDER BY cnt DESC LIMIT 10"
);
$dParams = [$channel];
if ($year !== '') {
$dParams[] = (int)$year;
}
$detailStmt->execute($dParams);
$result[] = [
'channel' => $channel,
'count' => (int)$r['cnt'],
'percent' => $total > 0 ? round(((int)$r['cnt'] / $total) * 100, 1) : 0,
'details' => $detailStmt->fetchAll(),
];
}
return $result;
}
Response::success([
'years' => $years,
'company' => channelAnalysis($pdo, 'companies', $year),
'person' => channelAnalysis($pdo, 'persons', $year),
]);
-36
View File
@@ -1,36 +0,0 @@
<?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 个渠道");
-50
View File
@@ -1,50 +0,0 @@
<?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]);
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* 渠道新增计划删除接口 POST /api/channel/plan_delete.php
* 入参:id(必填);软删除(is_active = 0)
*/
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);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("UPDATE channel_plans SET is_active = 0 WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) {
Response::error('计划不存在');
}
logCurrent('delete', 'channel_plan', 'channel_plans', $id, ['soft_delete' => true]);
Response::success(null, '删除成功');
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* 渠道新增计划列表接口 GET /api/channel/plan_list.php
* v1.0.18 渠道管理 - 渠道新增计划
* 参数:page / limit / channel_type / status / keyword
* 返回:{ list, total, page, limit }(list 含 remaining_days 剩余天数)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
[$page, $limit] = pageParams();
$channelType = trim($_REQUEST['channel_type'] ?? '');
$status = trim($_REQUEST['status'] ?? '');
$keyword = trim($_REQUEST['keyword'] ?? '');
$where = ['is_active = 1'];
$params = [];
if ($channelType !== '') {
$where[] = 'channel_type = ?';
$params[] = $channelType;
}
if ($status !== '') {
$where[] = 'status = ?';
$params[] = $status;
}
if ($keyword !== '') {
$where[] = '(source_detail LIKE ? OR industry LIKE ? OR remark LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like);
}
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM channel_plans WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, channel_type, source_detail, industry, start_date, end_date, remark, status, created_at, updated_at
FROM channel_plans
WHERE $whereSql
ORDER BY id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 剩余天数:距时间窗口结束日(end_date)的天数,已过期为 0
$today = strtotime(date('Y-m-d'));
foreach ($list as &$row) {
$row['remaining_days'] = 0;
if (!empty($row['end_date']) && strtotime($row['end_date']) !== false) {
$diff = (int)ceil((strtotime($row['end_date']) - $today) / 86400);
$row['remaining_days'] = max(0, $diff);
}
}
unset($row);
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* 渠道新增计划保存接口 POST /api/channel/plan_save.php
* 入参:id(可选,编辑时传)/ channel_type(渠道类别=source_channel 枚举,必填)/
* source_detail / industry / start_date / end_date / remark / status(待启动|已执行|错过)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/helpers.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('channel');
$channelType = trim($_POST['channel_type'] ?? '');
if ($channelType === '') {
Response::error('渠道类别(channel_type)为必填项', 400);
}
$status = trim($_POST['status'] ?? '待启动');
if (!in_array($status, ['待启动', '已执行', '错过'], true)) {
$status = '待启动';
}
$id = (int)($_POST['id'] ?? 0);
$pdo = DB::getInstance()->getPdo();
$fields = [
'channel_type' => $channelType,
'source_detail' => trim($_POST['source_detail'] ?? '') !== '' ? trim($_POST['source_detail']) : null,
'industry' => trim($_POST['industry'] ?? '') !== '' ? trim($_POST['industry']) : null,
'start_date' => trim($_POST['start_date'] ?? '') !== '' ? $_POST['start_date'] : null,
'end_date' => trim($_POST['end_date'] ?? '') !== '' ? $_POST['end_date'] : null,
'remark' => trim($_POST['remark'] ?? '') !== '' ? trim($_POST['remark']) : null,
'status' => $status,
];
if ($id > 0) {
$check = $pdo->prepare("SELECT id FROM channel_plans WHERE id = ? AND is_active = 1");
$check->execute([$id]);
if (!$check->fetch()) {
Response::error('计划不存在');
}
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE channel_plans SET $sets WHERE id = ?")->execute($params);
logCurrent('update', 'channel_plan', 'channel_plans', $id, $fields);
Response::success(null, '更新成功');
}
[$sql, $params] = buildInsert($fields);
$pdo->prepare("INSERT INTO channel_plans $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'channel_plan', 'channel_plans', $newId, $fields);
Response::success(['id' => $newId], '新增成功');
-53
View File
@@ -1,53 +0,0 @@
<?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], '新增成功');
-44
View File
@@ -1,44 +0,0 @@
<?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]);
+9 -4
View File
@@ -18,22 +18,22 @@ $data = [];
if ($type === 'all' || $type === 'country') {
$data['countries'] = $pdo->query(
"SELECT id, code, name FROM date_dict_country WHERE is_active = 1 ORDER BY sort_order, id"
"SELECT id, code, name FROM data_dict_country WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
if ($type === 'all' || $type === 'area') {
$data['areas'] = $pdo->query(
"SELECT id, code, name, parent_code FROM date_dict_area WHERE is_active = 1 ORDER BY sort_order, id"
"SELECT id, code, name, parent_code FROM data_dict_area WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
if ($type === 'all' || $type === 'industry') {
$data['industries'] = $pdo->query(
"SELECT id, code, name, parent_code FROM date_dict_industry WHERE is_active = 1 ORDER BY sort_order, id"
"SELECT id, code, name, parent_code FROM data_dict_industry WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
if ($type === 'all' || $type === 'certificate') {
$data['certificates'] = $pdo->query(
"SELECT id, code, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id"
"SELECT id, code, name FROM data_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
if ($type === 'all' || $type === 'source_channel') {
@@ -41,5 +41,10 @@ if ($type === 'all' || $type === 'source_channel') {
"SELECT id, name FROM source_channels WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
if ($type === 'all' || $type === 'company_type') {
$data['company_types'] = $pdo->query(
"SELECT id, code, name FROM data_dict_company_type WHERE is_active = 1 ORDER BY sort_order, id"
)->fetchAll();
}
Response::success($data);
+86 -3
View File
@@ -110,6 +110,87 @@ function strOrNull($v)
return ($v === '') ? null : $v;
}
/**
* 保存人员工作履历(整表替换:先删后插)
* @param PDO $pdo
* @param int $personId
* @param array $experiences JSON 解码后的数组 [{company_id,position,department,job_level,start_date,end_date,is_current}]
*/
function savePersonExperiences($pdo, $personId, $experiences)
{
$del = $pdo->prepare("DELETE FROM person_work_experiences WHERE person_id = ?");
$del->execute([$personId]);
if (empty($experiences)) {
return;
}
$ins = $pdo->prepare(
"INSERT INTO person_work_experiences
(person_id, company_id, position, department, job_level, start_date, end_date, is_current)
VALUES (?,?,?,?,?,?,?,?)"
);
foreach ($experiences as $e) {
if (!is_array($e)) {
continue;
}
$companyId = (int)($e['company_id'] ?? 0);
if ($companyId <= 0) {
continue;
}
$start = strOrNull($e['start_date'] ?? null);
$end = strOrNull($e['end_date'] ?? null);
$ins->execute([
$personId,
$companyId,
strOrNull($e['position'] ?? null),
strOrNull($e['department'] ?? null),
strOrNull($e['job_level'] ?? null),
$start !== null && strtotime($start) !== false ? date('Y-m-d', strtotime($start)) : null,
$end !== null && strtotime($end) !== false ? date('Y-m-d', strtotime($end)) : null,
!empty($e['is_current']) ? 1 : 0,
]);
}
}
/**
* 保存企业联系方式(social_accounts,整表替换:先删后插)
* 只操作 owner_type = 'company' 的记录
* @param PDO $pdo
* @param int $companyId
* @param array $accounts JSON 解码后的数组 [{platform,account_id,profile_url,remark,is_active,is_defult}]
*/
function saveCompanyAccounts($pdo, $companyId, $accounts)
{
$del = $pdo->prepare("DELETE FROM social_accounts WHERE owner_type = 'company' AND owner_id = ?");
$del->execute([$companyId]);
if (empty($accounts)) {
return;
}
$ins = $pdo->prepare(
"INSERT INTO social_accounts
(owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active)
VALUES ('company', ?, ?, ?, ?, ?, 0, ?, ?)"
);
foreach ($accounts as $a) {
if (!is_array($a)) {
continue;
}
$platform = trim($a['platform'] ?? '');
$accountId = trim($a['account_id'] ?? '');
if ($platform === '' || $accountId === '') {
continue;
}
$ins->execute([
$companyId,
$platform,
$accountId,
strOrNull($a['profile_url'] ?? null),
strOrNull($a['remark'] ?? null),
!empty($a['is_defult']) ? 1 : 0,
!empty($a['is_active']) ? 1 : 0,
]);
}
}
/**
* 保存企业年度财务明细(整表替换:先删后插)
* @param PDO $pdo
@@ -175,11 +256,11 @@ function saveCompanyCertifications($pdo, $companyId, $certifications)
return;
}
$valid = $pdo->prepare("SELECT id FROM date_dict_certificate WHERE id = ?");
$valid = $pdo->prepare("SELECT id FROM data_dict_certificate WHERE id = ?");
$ins = $pdo->prepare(
"INSERT INTO company_certifications
(company_id, certification_type_id, certificate_number, issue_date, expiry_date)
VALUES (?,?,?,?,?)"
(company_id, certification_type_id, certificate_number, level, issuing_authority, issue_date, expiry_date)
VALUES (?,?,?,?,?,?,?)"
);
$seen = [];
foreach ($certifications as $ct) {
@@ -199,6 +280,8 @@ function saveCompanyCertifications($pdo, $companyId, $certifications)
$companyId,
$tid,
strOrNull($ct['certificate_number'] ?? null),
strOrNull($ct['level'] ?? null),
strOrNull($ct['issuing_authority'] ?? null),
strOrNull($ct['issue_date'] ?? null),
strOrNull($ct['expiry_date'] ?? null),
]);
+10 -2
View File
@@ -26,8 +26,8 @@ $pdo = DB::getInstance()->getPdo();
$pdo->prepare("INSERT INTO companies $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
// 财务信息 / 资质认证(可选,整表替换)
if (array_key_exists('financials', $_POST) || array_key_exists('certifications', $_POST)) {
// 财务信息 / 资质认证 / 联系方式(可选,整表替换)
if (array_key_exists('financials', $_POST) || array_key_exists('certifications', $_POST) || array_key_exists('accounts', $_POST)) {
$pdo->beginTransaction();
try {
if (array_key_exists('financials', $_POST)) {
@@ -46,6 +46,14 @@ if (array_key_exists('financials', $_POST) || array_key_exists('certifications',
}
saveCompanyCertifications($pdo, $newId, $certs);
}
if (array_key_exists('accounts', $_POST)) {
$accounts = json_decode($_POST['accounts'], true);
if (!is_array($accounts)) {
$pdo->rollBack();
Response::error('联系方式格式错误', 400);
}
saveCompanyAccounts($pdo, $newId, $accounts);
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
+2 -2
View File
@@ -2,7 +2,7 @@
/**
* 认证类型字典接口 GET /api/company/cert_types.php
* 返回启用中的认证类型(如:瞪羚企业、国家高新技术企业等),供编辑弹窗下拉使用
* 数据源:date_dict_certificate(原 certification_types,v1.0.16 改名)
* 数据源:data_dict_certificate(原 certification_types,v1.0.16 改名,v1.0.18 再改名 data_dict_certificate)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
@@ -11,6 +11,6 @@ require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
$pdo = DB::getInstance()->getPdo();
$types = $pdo->query("SELECT id, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
$types = $pdo->query("SELECT id, name FROM data_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
Response::success(['types' => $types]);
+5 -5
View File
@@ -23,7 +23,7 @@ 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 = $pdo->prepare("SELECT id, category_name, category_description, positioning, series, is_core, is_active, created_at, updated_at FROM company_products WHERE company_id = ? AND is_active = 1");
$products->execute([$id]);
$docs = $pdo->prepare(
@@ -45,18 +45,18 @@ $relations = $pdo->prepare(
);
$relations->execute([$id]);
// 资质认证(瞪羚企业等,JOIN 字典表取名称)
// 资质认证(瞪羚企业等,JOIN 字典表取名称;含级别/颁证机构)
$certs = $pdo->prepare(
"SELECT cc.certification_type_id, ct.name AS certification_name, cc.certificate_number,
cc.issue_date, cc.expiry_date
cc.level, cc.issuing_authority, cc.issue_date, cc.expiry_date
FROM company_certifications cc
INNER JOIN date_dict_certificate ct ON ct.id = cc.certification_type_id
INNER JOIN data_dict_certificate ct ON ct.id = cc.certification_type_id
WHERE cc.company_id = ?
ORDER BY ct.sort_order, ct.id"
);
$certs->execute([$id]);
$certTypes = $pdo->query("SELECT id, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
$certTypes = $pdo->query("SELECT id, name FROM data_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
// 联系方式:social_accounts 中属于该公司的记录(仅展示,无修改/删除入口)
$accounts = $pdo->prepare(
+31 -20
View File
@@ -1,7 +1,11 @@
<?php
/**
* 企业列表接口 GET/POST /api/company/list.php
* 参数:page / limit / keyword(公司名/注册号)/ industry / business_role / country / is_active
* v1.0.18 检索改造:
* - field + keyword 联动:field=具体字段(如 name_zh/registration_number/…),keyword 只匹配该字段;field 为空=全部字段
* - industry:行业下拉(精确匹配)
* - company_type:企业类型(companies.legal_form,取自 data_dict_company_type)
* 参数:page / limit / keyword / field / industry / company_type / is_active
* 返回:{ list, total, page, limit }
*/
require_once __DIR__ . '/../common/db.php';
@@ -11,36 +15,43 @@ require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$keyword = trim($_REQUEST['keyword'] ?? '');
$field = trim($_REQUEST['field'] ?? '');
$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;
$companyType = trim($_REQUEST['company_type'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
// 可指定筛选的字段白名单(下拉选项来自前端「筛选」)
$searchableFields = [
'name_zh', 'name_en', 'registration_number', 'legal_representative',
'address', 'industry', 'industry_subdivision', 'website',
'source_channel', 'source_detail', 'business_scope', 'stock_code',
];
$where = ['is_active = 1'];
$params = [];
if ($active !== null) {
$where = [];
$params = [];
$where[] = 'is_active = ?';
$params[] = $active;
$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 ($field !== '' && in_array($field, $searchableFields, true)) {
$where[] = "$field LIKE ?";
$params[] = "%$keyword%";
} else {
// 全部字段:匹配常用字段
$where[] = '(name_zh LIKE ? OR name_en LIKE ? OR display_name LIKE ? OR registration_number LIKE ? OR legal_representative LIKE ? OR industry LIKE ? OR industry_subdivision LIKE ? OR website LIKE ? OR source_detail LIKE ? OR stock_code LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like, $like, $like, $like, $like, $like, $like);
}
}
if ($industry !== '') {
$where[] = 'industry = ?';
$params[] = $industry;
}
if ($role !== '') {
$where[] = 'business_role = ?';
$params[] = $role;
}
if ($country !== '') {
$where[] = 'country = ?';
$params[] = $country;
if ($companyType !== '') {
$where[] = 'legal_form = ?';
$params[] = $companyType;
}
$whereSql = implode(' AND ', $where);
@@ -52,7 +63,7 @@ $total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, name_zh, name_en, display_name, business_role, country, registration_number,
"SELECT id, name_zh, name_en, display_name, business_role, legal_form, country, registration_number,
address, legal_representative, industry, industry_subdivision, website,
latest_employee_count, latest_annual_revenue, is_listed, stock_code,
source_channel, source_detail, is_active, created_at, updated_at
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* 企业官媒新增接口 POST /api/company/official_media_add.php
* 为指定公司快速新增一条官媒记录(social_accounts owner_type=company)
* 入参:company_id(必填)/ platform(平台)/ account_id(账号名称)/ profile_url(主页链接)/
* remark / is_active(1启用 0停用)/ is_defult(1常用)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/helpers.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('company');
$companyId = (int)($_POST['company_id'] ?? 0);
$platform = trim($_POST['platform'] ?? '');
$accountId = trim($_POST['account_id'] ?? '');
if ($companyId <= 0 || $platform === '' || $accountId === '') {
Response::error('平台与账号名称为必填项', 400);
}
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
$chk->execute([$companyId]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('企业不存在');
}
$ins = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active)
VALUES ('company', ?, ?, ?, ?, ?, 0, ?, ?)"
);
$ins->execute([
$companyId,
$platform,
$accountId,
strOrNull($_POST['profile_url'] ?? null),
strOrNull($_POST['remark'] ?? null),
!empty($_POST['is_defult']) ? 1 : 0,
isset($_POST['is_active']) && (int)$_POST['is_active'] === 0 ? 0 : 1,
]);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'official_media', 'social_accounts', $newId, ['company_id' => $companyId, 'platform' => $platform, 'account_id' => $accountId]);
Response::success(['id' => $newId], '新增成功');
+43
View File
@@ -0,0 +1,43 @@
<?php
/**
* 企业官媒列表接口 GET /api/company/official_media_list.php
* 归属该公司的媒体清单(social_accounts owner_type=company)
* 字段:平台/账号名称/ID/主页链接/状态/更新日期
* 入参:company_id(必填)/ page / limit
*/
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(5);
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
$chk->execute([$companyId]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('企业不存在');
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM social_accounts WHERE owner_type = 'company' AND owner_id = ? AND is_active = 1");
$stmt->execute([$companyId]);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, platform, account_id, profile_url, remark, is_defult, is_active, created_at
FROM social_accounts
WHERE owner_type = 'company' AND owner_id = ? AND is_active = 1
ORDER BY is_defult DESC, id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute([$companyId]);
Response::success(['list' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'limit' => $limit]);
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* 企业产品新增接口 POST /api/company/product_add.php
* 入参:company_id(必填)/ category_name(产品品类)/ positioning(产品基本定位)/ series(包含系列)/
* is_active(1在产 0停产)/ attrs(可选 JSON:[{attr_id, value}],EAV 参数值)
* 说明:attr_id 来自 company_products_attr(按企业所属行业定义的参数属性)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/helpers.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('company');
$companyId = (int)($_POST['company_id'] ?? 0);
$categoryName = trim($_POST['category_name'] ?? '');
if ($companyId <= 0 || $categoryName === '') {
Response::error('产品品类(category_name)为必填项', 400);
}
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT id, industry FROM companies WHERE id = ?");
$chk->execute([$companyId]);
$company = $chk->fetch();
if (!$company) {
Response::error('企业不存在');
}
$pdo->beginTransaction();
try {
$ins = $pdo->prepare(
"INSERT INTO company_products (company_id, category_name, category_description, positioning, series, is_core, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$ins->execute([
$companyId,
$categoryName,
strOrNull($_POST['category_description'] ?? null),
strOrNull($_POST['positioning'] ?? null),
strOrNull($_POST['series'] ?? null),
!empty($_POST['is_core']) ? 1 : 0,
isset($_POST['is_active']) && (int)$_POST['is_active'] === 0 ? 0 : 1,
]);
$productId = (int)$pdo->lastInsertId();
// EAV 参数值
$attrs = json_decode($_POST['attrs'] ?? '[]', true);
if (is_array($attrs) && $attrs) {
// 校验 attr 属于该公司行业
$valid = $pdo->prepare("SELECT id, attr_type FROM company_products_attr WHERE id = ? AND industry = ? AND is_active = 1");
$insV = $pdo->prepare(
"INSERT INTO company_products_attr_value (product_id, attr_id, value_string, value_number, value_boolean, value_date)
VALUES (?, ?, ?, ?, ?, ?)"
);
foreach ($attrs as $a) {
$attrId = (int)($a['attr_id'] ?? 0);
if ($attrId <= 0) {
continue;
}
$valid->execute([$attrId, $company['industry']]);
$def = $valid->fetch();
if (!$def) {
continue;
}
$val = trim((string)($a['value'] ?? ''));
if ($val === '') {
continue;
}
$vStr = $vNum = $vBool = $vDate = null;
switch ($def['attr_type']) {
case 'number':
$vNum = is_numeric($val) ? $val : null;
if ($vNum === null) {
$vStr = $val;
}
break;
case 'boolean':
$vBool = (in_array($val, ['1', 'true', '是', 'yes', 'Y', 'y', '支持'], true)) ? 1 : 0;
break;
case 'date':
$vDate = (strtotime($val) !== false) ? date('Y-m-d', strtotime($val)) : null;
if ($vDate === null) {
$vStr = $val;
}
break;
default:
$vStr = $val;
}
$insV->execute([$productId, $attrId, $vStr, $vNum, $vBool, $vDate]);
}
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
Response::error('产品保存失败:' . $e->getMessage());
}
logCurrent('add', 'product', 'company_products', $productId, ['company_id' => $companyId, 'category_name' => $categoryName]);
Response::success(['id' => $productId], '新增成功');
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* 产品参数属性定义接口 GET /api/company/product_attrs.php?industry=汽车制造
* 返回指定行业的参数属性定义(EAV),供新增产品弹窗动态生成参数输入项
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
$industry = trim($_REQUEST['industry'] ?? '');
if ($industry === '') {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$attrs = $pdo->prepare(
"SELECT id, attr_name, attr_type, unit FROM company_products_attr
WHERE industry = ? AND is_active = 1 ORDER BY sort_order, id"
);
$attrs->execute([$industry]);
Response::success(['attrs' => $attrs->fetchAll()]);
+35 -3
View File
@@ -1,6 +1,7 @@
<?php
/**
* 企业产品品类列表接口 GET /api/company/products.php
* 企业产品列表接口 GET /api/company/products.php
* v1.0.18:返回产品基础字段(产品品类/基本定位/包含系列/状态/更新日期)+ EAV 参数值(JSON)
* 入参:company_id(必填)/ page / limit
*/
require_once __DIR__ . '/../common/db.php';
@@ -30,12 +31,43 @@ $total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, category_name, category_description, is_core, is_active
"SELECT id, category_name, category_description, positioning, series, is_core, is_active, created_at, updated_at
FROM company_products
WHERE company_id = ? AND is_active = 1
ORDER BY is_core DESC, id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute([$companyId]);
$list = $stmt->fetchAll();
Response::success(['list' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'limit' => $limit]);
// 批量取 EAV 参数值(每个产品一个 JSON:attr_id => 值)
if ($list) {
$ids = array_column($list, 'id');
$in = implode(',', array_fill(0, count($ids), '?'));
$valStmt = $pdo->prepare(
"SELECT v.product_id, a.attr_name, a.attr_type, a.unit,
v.value_string, v.value_number, v.value_boolean, v.value_date
FROM company_products_attr_value v
INNER JOIN company_products_attr a ON a.id = v.attr_id
WHERE v.product_id IN ($in)"
);
$valStmt->execute($ids);
$values = [];
foreach ($valStmt as $v) {
$values[$v['product_id']][] = [
'attr_name' => $v['attr_name'],
'attr_type' => $v['attr_type'],
'unit' => $v['unit'],
'value' => $v['value_string'] !== null ? $v['value_string']
: ($v['value_number'] !== null ? rtrim(rtrim(sprintf('%.4f', $v['value_number']), '0'), '.')
: ($v['value_boolean'] !== null ? ($v['value_boolean'] ? '是' : '否')
: ($v['value_date'] ?? ''))),
];
}
foreach ($list as &$row) {
$row['attrs'] = $values[$row['id']] ?? [];
}
unset($row);
}
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+12 -3
View File
@@ -21,7 +21,8 @@ $data = extractFields(COMPANY_FIELDS);
unset($data['display_name']); // 显示名称不允许通过编辑接口置空/改名,如需改名请走完整字段
$hasFinancials = array_key_exists('financials', $_POST);
$hasCertifications = array_key_exists('certifications', $_POST);
if (empty($data) && !$hasFinancials && !$hasCertifications) {
$hasAccounts = array_key_exists('accounts', $_POST);
if (empty($data) && !$hasFinancials && !$hasCertifications && !$hasAccounts) {
Response::error('没有需要更新的字段', 400);
}
@@ -40,8 +41,8 @@ if (!empty($data)) {
$pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params);
}
// 财务信息 / 资质认证(可选,整表替换)
if ($hasFinancials || $hasCertifications) {
// 财务信息 / 资质认证 / 联系方式(可选,整表替换)
if ($hasFinancials || $hasCertifications || $hasAccounts) {
$pdo->beginTransaction();
try {
if ($hasFinancials) {
@@ -60,6 +61,14 @@ if ($hasFinancials || $hasCertifications) {
}
saveCompanyCertifications($pdo, $id, $certs);
}
if ($hasAccounts) {
$accounts = json_decode($_POST['accounts'], true);
if (!is_array($accounts)) {
$pdo->rollBack();
Response::error('联系方式格式错误', 400);
}
saveCompanyAccounts($pdo, $id, $accounts);
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
+6
View File
@@ -49,5 +49,11 @@ if (is_array($contacts)) {
}
}
// 工作履历(v1.0.18:整表替换)
$experiences = json_decode($_POST['experiences'] ?? '[]', true);
if (is_array($experiences)) {
savePersonExperiences($pdo, $newId, $experiences);
}
logCurrent('add', 'person', 'persons', $newId, ['data' => $data, 'contacts' => $contacts]);
Response::success(['id' => $newId], '新增成功');
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* 任职公司选项接口 GET /api/person/company_options.php
* 返回启用中的企业(id + 显示名称),供人员「工作履历」行内公司下拉使用
* 入参:keyword(可选,模糊匹配企业名称)/ limit(默认 500)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('person');
$keyword = trim($_REQUEST['keyword'] ?? '');
$limit = min(1000, max(1, (int)($_REQUEST['limit'] ?? 500)));
$pdo = DB::getInstance()->getPdo();
$where = 'is_active = 1';
$params = [];
if ($keyword !== '') {
$where .= ' AND (name_zh LIKE ? OR name_en LIKE ? OR display_name LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like);
}
$stmt = $pdo->prepare(
"SELECT id, COALESCE(NULLIF(display_name,''), name_zh, name_en) AS name
FROM companies WHERE $where ORDER BY id ASC LIMIT $limit"
);
$stmt->execute($params);
Response::success(['list' => $stmt->fetchAll()]);
+185
View File
@@ -0,0 +1,185 @@
<?php
/**
* 人员人脉接口 GET /api/person/connections.php?person_id=X
* 列出与该人员相关的其他人员(交集:工作履历/家乡/毕业院校)
* - 同事:在 person_work_experiences 中任职过同一家公司
* - 老乡:家乡市级一致(如 湖南衡阳 vs 湖南衡阳;仅为湖南省不算)
* - 校友:毕业院校相同
* 亲密度(百分比):
* 老乡/校友/同事 各 +20;同一家公司任职次数≥2 +10;同事且入职年份一致 +10;
* 工作地点一致 +10;性别一致(均为男或均为女)+5;剩余 0~5 随机;
* 任何两人亲密度不得超过 99%(不能达到 100%)
* 入参:person_id(必填)/ limit(默认 50)
* 返回:{ list: [{id, full_name, phone, tags:[老乡,校友,同事], intimacy}] }
*/
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);
}
$limit = (int)($_REQUEST['limit'] ?? 50);
if ($limit <= 0 || $limit > 200) {
$limit = 50;
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT id, full_name, hometown, graduated_from, gender, work_location FROM persons WHERE id = ?");
$stmt->execute([$personId]);
$target = $stmt->fetch();
if (!$target) {
Response::error('人员不存在');
}
/** 省-市名称归一化:湖南省衡阳市 -> 湖南衡阳;新疆维吾尔自治区乌鲁木齐市 -> 新疆乌鲁木齐;北京市 -> 北京 */
function normalizeAreaName($s)
{
$s = trim((string)$s);
if ($s === '') {
return '';
}
$muni = ['北京市' => '北京', '天津市' => '天津', '上海市' => '上海', '重庆市' => '重庆'];
if (isset($muni[$s])) {
return $muni[$s];
}
// 自治区后缀(维吾尔/壮族/回族)及省后缀
$s = preg_replace('/(维吾尔|壮族|回族)?自治区$/', '', $s);
$s = str_replace('省', '', $s);
// 去掉末尾「市」
$s = preg_replace('/市$/', '', $s);
$s = str_replace('特别行政区', '', $s);
return $s;
}
$tHometown = normalizeAreaName($target['hometown']);
$tWorkLoc = normalizeAreaName($target['work_location']);
$tSchool = trim((string)$target['graduated_from']);
$tGender = $target['gender'];
$isGenderPair = ($tGender === '男' || $tGender === '女');
// 目标人员的任职公司集合:company_id => 入职年份
$expMap = []; // person_id => [ ['company_id'=>x,'sy'=>yyyy|null], ... ]
$targetComps = []; // company_id => sy
$stmt = $pdo->prepare(
"SELECT person_id, company_id, YEAR(start_date) AS sy
FROM person_work_experiences WHERE is_active = 1 AND person_id != ?"
);
$stmt->execute([$personId]);
$candIds = [];
foreach ($stmt as $row) {
$pid = (int)$row['person_id'];
$expMap[$pid][] = ['company_id' => (int)$row['company_id'], 'sy' => $row['sy'] !== null ? (int)$row['sy'] : null];
$candIds[$pid] = true;
}
$stmt = $pdo->prepare(
"SELECT company_id, YEAR(start_date) AS sy
FROM person_work_experiences WHERE is_active = 1 AND person_id = ?"
);
$stmt->execute([$personId]);
foreach ($stmt as $row) {
$targetComps[(int)$row['company_id']] = $row['sy'] !== null ? (int)$row['sy'] : null;
}
// 候选人员基本信息
$candList = $pdo->query(
"SELECT id, full_name, hometown, graduated_from, gender, work_location FROM persons WHERE id != $personId AND is_active = 1"
)->fetchAll();
// 手机号(常用优先)
$phones = [];
foreach ($pdo->query(
"SELECT owner_id, account_id FROM social_accounts
WHERE owner_type = 'person' AND platform = 'phone' AND is_active = 1
ORDER BY is_defult DESC, is_primary DESC, id DESC"
) as $row) {
if (!isset($phones[$row['owner_id']])) {
$phones[$row['owner_id']] = $row['account_id'];
}
}
$result = [];
foreach ($candList as $c) {
$tags = [];
$score = 0;
// 老乡:市级一致(归一化后整体相等即省市都一致)
$cHometown = normalizeAreaName($c['hometown']);
if ($tHometown !== '' && $cHometown !== '' && $tHometown === $cHometown) {
$tags[] = '老乡';
$score += 20;
}
// 校友
$cSchool = trim((string)$c['graduated_from']);
if ($tSchool !== '' && $cSchool !== '' && $tSchool === $cSchool) {
$tags[] = '校友';
$score += 20;
}
// 同事:任职公司交集
$shared = []; // company_id => [targetSy, candSy]
foreach (($expMap[$c['id']] ?? []) as $e) {
if (isset($targetComps[$e['company_id']])) {
$shared[$e['company_id']] = [$targetComps[$e['company_id']], $e['sy']];
}
}
if ($shared) {
$tags[] = '同事';
$score += 20;
// 同一家公司任职次数≥2(两家及以上共同任职公司)
if (count($shared) >= 2) {
$score += 10;
}
// 同事且入职年份一致
foreach ($shared as $pair) {
if ($pair[0] !== null && $pair[1] !== null && $pair[0] === $pair[1]) {
$score += 10;
break;
}
}
}
// 工作地点一致
$cWorkLoc = normalizeAreaName($c['work_location']);
if ($tWorkLoc !== '' && $cWorkLoc !== '' && $tWorkLoc === $cWorkLoc) {
$score += 10;
}
// 性别一致(均为男或均为女)
if ($isGenderPair && ($c['gender'] === '男' || $c['gender'] === '女') && $c['gender'] === $tGender) {
$score += 5;
}
// 至少有一种交集才进入人脉
if (!$tags) {
continue;
}
// 随机 0~5(剩余百分比),总亲密上限 99(不能达到 100%)
$score += mt_rand(0, 5);
$score = min(99, $score);
$result[] = [
'id' => (int)$c['id'],
'full_name' => $c['full_name'],
'phone' => $phones[$c['id']] ?? '',
'tags' => $tags,
'intimacy' => $score,
];
}
// 亲密度降序,其次按 id 升序
usort($result, function ($a, $b) {
if ($b['intimacy'] !== $a['intimacy']) {
return $b['intimacy'] - $a['intimacy'];
}
return $a['id'] - $b['id'];
});
$result = array_slice($result, 0, $limit);
Response::success(['list' => $result]);
+9
View File
@@ -54,5 +54,14 @@ if (!empty($data)) {
$pdo->prepare("UPDATE persons SET $sets WHERE id = ?")->execute($params);
}
// 工作履历(v1.0.18:可选,整表替换)
if (array_key_exists('experiences', $_POST)) {
$experiences = json_decode($_POST['experiences'], true);
if (!is_array($experiences)) {
Response::error('工作履历格式错误', 400);
}
savePersonExperiences($pdo, $id, $experiences);
}
logCurrent('update', 'person', 'persons', $id, ['before' => $old, 'after' => $data, 'contacts_replaced' => $contactsChanged]);
Response::success(null, '更新成功');
+1
View File
@@ -10,6 +10,7 @@
<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/js/config.js"></script>
<script src="static/js/common.js"></script>
<script src="static/js/channel.js"></script>
+18
View File
@@ -0,0 +1,18 @@
<!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/placeholder.js"></script>
<script>renderPlaceholder('competitor_analysis');</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!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/placeholder.js"></script>
<script>renderPlaceholder('competitor_data');</script>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
# mysuperlink v1.0.18 迭代需求(2026-08-08 文档)
## 来源文档:0807---29877584-0c3b-41d8-b102-e9aa9c1cea62.docx(13 条)
### 1. 企业新增弹窗
- "工商信息" tab → "基本信息"
- 删"英文名称";"公司名称" → "企业名称";"法律形式" → "企业类型"
- "资质认证"列表增加"级别""颁证机构"字段
### 2. 字典表 + 表单导航
- 新建字典表 `data_dict_company_type`(对应 companies 表"企业类型",后续按需加类型)
- 基本信息下方左侧增加文字锚文本导航:工商信息 / 联系方式 / 行业归属 / 业务类型 / 来源
- 删除"行业信息" tab 卡
### 3. 企业数据顶部检索
- ① "筛选"下拉:所有字段为选项,第一个"全部字段",指定字段后检索词只匹配该字段(①和②联动)
- ② 文本输入框
- ③ "行业"下拉筛选,初始"全部行业"
- ④ "全部业务角色" → "企业类型"
- ⑤ 删掉国家/地区文本框
### 4. 数据库改名(其它无变化)
- date_dict_area → data_dict_area
- date_dict_certificate → data_dict_certificate
- date_dict_country → data_dict_country
- date_dict_industry → data_dict_industry
### 5. 新建数据表(EAV 模式,参考 https://chat.deepseek.com/share/jqeygekewi0qlle1dl)
- company_products(已存在)增加"产品基本定位"字段(商业定位介绍)
- 新建 `company_products_attr`(参数属性定义表)
- 新建 `company_products_attr_value`(产品参数值表)
- 字段按链接 EAV 方案自由拟定
### 6. 企业数据"产品"详情页
- 展示:产品品类、基本定位、包含系列、状态、更新日期
- "新增产品"按钮:快速新增产品记录
### 7. 企业数据新增"官媒"入口
- 该公司媒体清单:平台、账号名称、ID、主页链接、状态、更新日期
- "新增官媒"按钮
### 8. 人员数据新增"人脉"入口
- 列出相关人员:工作履历/家乡/毕业院校有交集(家乡须市级一致,如湖南衡阳 vs 湖南长沙不算)
- 字段:姓名、联系方式、交集(老乡/校友/同事)、亲密度(%)
- 亲密度规则:老乡/校友/同事各 20%;同公司次数≥2 +10%;同事且入职年份一致 +10%;工作地点一致 +10%;性别一致 +5%;随机 +5%;任何两人不得达到 100%
- 点击姓名可反查该人员详情
### 9. 人员编辑页
- 国籍为中国/中国香港/中国澳门/中国台湾时,家乡输入框置灰不可填
- 工作所在地/家乡都只显示"省-市"两级(不细化区县)
- 存储与显示均为"省-市"结构:如"湖南省衡阳市"→"湖南衡阳"、"新疆维吾尔自治区乌鲁木齐市"→"新疆乌鲁木齐"
### 10. 人员新增
- 分两个 tab:基本信息 / 工作履历
### 11. 左侧导航
- 数据管理下删掉"媒体数据"
- "营销服务" → "精准营销",删掉其下"媒体批发"
- 新增一级导航"媒体管理":媒体资源 / 舆情监测 / 推广服务(占位)
- "需求转盘"改为"精准营销"下的二级导航
### 12. 新增一级导航"竞品管理"
- 二级菜单:竞品分析 / 竞品资料
### 13. 渠道管理
- 目标:记录企业/人员数据来源渠道 + 拓展高效渠道(数据源 source_channel + source_detail)
- 两部分:渠道效能分析 / 渠道新增计划
- 渠道效能分析:企业(左)/人员(右)两个板块,右上角"年份"下拉(默认全部)
- 按"渠道"字段饼状图按百分比排序;鼠标悬停饼图区域显示该渠道 source_detail TOP10 表格
- 表格字段:来源(只显示前 8 字,超出用"…")、数量;默认显示渠道第一名
- 渠道新增计划:列表:渠道类别(=source_channel)、来源详情、所属行业、时间窗口("2025-4-1至2025-4-5")、剩余天数、备注说明、状态反馈(下拉:待启动/已执行/错过)
### 14. 删除 sql/system_tables.sql
---
## DeepSeek 分享链接 EAV 方案要点
1. 产品基础表 products:product_id, industry, company_name, product_name, series, release_date, created_at
2. 参数属性定义表 attributes:attr_id, industry, attr_name, attr_type ENUM('string','number','boolean','date'), unit, sort_order, UNIQUE(industry, attr_name)
3. 产品参数值表 product_attributes:id, product_id, attr_id, value_string TEXT, value_number DECIMAL(20,4), value_boolean TINYINT(1), value_date DATE, created_at, UNIQUE(product_id, attr_id)
- 行转列 Pivot 查询做行业对比;本项目按公司维度(company_products)
+18
View File
@@ -0,0 +1,18 @@
<!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/placeholder.js"></script>
<script>renderPlaceholder('media_opinion');</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!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/placeholder.js"></script>
<script>renderPlaceholder('media_promo');</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!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/placeholder.js"></script>
<script>renderPlaceholder('media_resources');</script>
</body>
</html>
+9 -9
View File
@@ -2,7 +2,7 @@
-- 数据字典种子数据(由官方 Excel 文件生成,勿手工修改)
-- 来源:国家地区编码.xls / 行政区划编码.xls / 招标行业分类.xls / 证书类型枚举.xls
--
-- ⚠️ 重要:date_dict_certificate 被 company_certifications.certification_type_id
-- ⚠️ 重要:data_dict_certificate 被 company_certifications.certification_type_id
-- 外键 ON DELETE CASCADE 引用,本文件会对证书表 DELETE 后重建!
-- 执行前必须先备份 company_certifications(及相关业务表)!!
-- 安全做法:
@@ -16,8 +16,8 @@ SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 1) 国家地区编码(62 条,另补充 'CN 中国' 1 条供国内企业/人员使用)
DELETE FROM `date_dict_country`;
INSERT INTO `date_dict_country` (`code`, `name`, `sort_order`) VALUES
DELETE FROM `data_dict_country`;
INSERT INTO `data_dict_country` (`code`, `name`, `sort_order`) VALUES
('CN', '中国', 0),
('HK', '中国香港', 1),
('MO', '中国澳门', 2),
@@ -83,8 +83,8 @@ INSERT INTO `date_dict_country` (`code`, `name`, `sort_order`) VALUES
('BR', '巴西', 62);
-- 2) 行政区划编码(省/市/区县三级,共 3632 条)
DELETE FROM `date_dict_area`;
INSERT INTO `date_dict_area` (`code`, `name`, `parent_code`, `sort_order`) VALUES
DELETE FROM `data_dict_area`;
INSERT INTO `data_dict_area` (`code`, `name`, `parent_code`, `sort_order`) VALUES
('110000', '北京市', NULL, 1),
('110100', '北京市', '110000', 2),
('110101', '东城区', '110100', 3),
@@ -3719,8 +3719,8 @@ INSERT INTO `date_dict_area` (`code`, `name`, `parent_code`, `sort_order`) VALUE
('820000', '澳门特别行政区', NULL, 3632);
-- 3) 招标行业分类(24 条)
DELETE FROM `date_dict_industry`;
INSERT INTO `date_dict_industry` (`code`, `name`, `parent_code`, `sort_order`) VALUES
DELETE FROM `data_dict_industry`;
INSERT INTO `data_dict_industry` (`code`, `name`, `parent_code`, `sort_order`) VALUES
('1', '工程建筑', NULL, 1),
('2', '办公文教', NULL, 2),
('3', '医疗卫生', NULL, 3),
@@ -3747,8 +3747,8 @@ INSERT INTO `date_dict_industry` (`code`, `name`, `parent_code`, `sort_order`) V
('15', '其他', NULL, 24);
-- 4) 证书类型枚举(263 条)
DELETE FROM `date_dict_certificate`;
INSERT INTO `date_dict_certificate` (`code`, `name`, `sort_order`) VALUES
DELETE FROM `data_dict_certificate`;
INSERT INTO `data_dict_certificate` (`code`, `name`, `sort_order`) VALUES
('000001', '强制性产品认证', 1),
('000002001', '质量管理体系认证', 2),
('000002002', '环境管理体系认证', 3),
+118
View File
@@ -0,0 +1,118 @@
-- ============================================================
-- SuperLink Web 后台管理系统 - v1.0.18 数据库结构变更脚本
-- 对应需求文档:0807---29877584-0c3b-41d8-b102-e9aa9c1cea62.docx
-- 1) 数据字典表改名:date_dict_* -> data_dict_*(其它无变化)
-- 2) 新增企业类型字典表 data_dict_company_type(对应 companies.legal_form 企业类型)
-- 3) company_certifications 增加「级别」「颁证机构」
-- 4) company_products 增加「产品基本定位」「包含系列」
-- 5) 新增 EAV 产品参数表:company_products_attr / company_products_attr_value
-- 6) 新增渠道新增计划表 channel_plans
-- 执行方式:直接执行本文件(先备份数据库)
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- 1) 数据字典表改名(其它无变化)
-- ------------------------------------------------------------
RENAME TABLE `date_dict_area` TO `data_dict_area`;
RENAME TABLE `date_dict_certificate` TO `data_dict_certificate`;
RENAME TABLE `date_dict_country` TO `data_dict_country`;
RENAME TABLE `date_dict_industry` TO `data_dict_industry`;
-- ------------------------------------------------------------
-- 2) 企业类型字典表(对应 companies 表「企业类型」,后续按需增加具体类型)
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `data_dict_company_type`;
CREATE TABLE `data_dict_company_type` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NULL DEFAULT NULL COMMENT '企业类型编码',
`name` VARCHAR(100) NOT NULL COMMENT '企业类型名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='企业类型字典(对应 companies.legal_form 企业类型)';
INSERT INTO `data_dict_company_type` (`code`, `name`, `sort_order`) VALUES
('llc', '有限责任公司', 1),
('joint_stock', '股份有限公司', 2),
('partnership', '合伙企业', 3),
('sole_proprietorship', '个人独资企业', 4),
('state_owned', '国有企业', 5),
('collective', '集体所有制企业', 6),
('foreign_invested', '外商投资企业', 7),
('joint_venture', '中外合资企业', 8),
('other', '其他', 99);
-- ------------------------------------------------------------
-- 3) 资质认证表增加「级别」「颁证机构」
-- ------------------------------------------------------------
ALTER TABLE `company_certifications`
ADD COLUMN `level` VARCHAR(50) NULL DEFAULT NULL COMMENT '级别' AFTER `certificate_number`,
ADD COLUMN `issuing_authority` VARCHAR(200) NULL DEFAULT NULL COMMENT '颁证机构' AFTER `level`;
-- ------------------------------------------------------------
-- 4) 产品基础表(company_products)增加「产品基本定位」「包含系列」
-- 说明:产品基本定位 = 介绍这一款产品的商业定位
-- ------------------------------------------------------------
ALTER TABLE `company_products`
ADD COLUMN `positioning` TEXT NULL COMMENT '产品基本定位(商业定位介绍)' AFTER `category_description`,
ADD COLUMN `series` VARCHAR(200) NULL COMMENT '包含系列' AFTER `positioning`;
-- ------------------------------------------------------------
-- 5) EAV 模式产品参数表(参考 DeepSeek 分享方案)
-- company_products_attr 参数属性定义表:按行业定义「有哪些参数」(元数据)
-- company_products_attr_value 产品参数值表:产品 + 参数属性 -> 具体值
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `company_products_attr`;
CREATE TABLE `company_products_attr` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '参数属性ID',
`industry` VARCHAR(100) NOT NULL COMMENT '所属行业(对应 companies.industry)',
`attr_name` VARCHAR(50) NOT NULL COMMENT '参数名称',
`attr_type` ENUM('string','number','boolean','date') NOT NULL DEFAULT 'string' COMMENT '参数类型',
`unit` VARCHAR(20) NULL DEFAULT NULL COMMENT '单位',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '展示排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_industry_attr` (`industry`, `attr_name`),
KEY `idx_industry` (`industry`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品参数属性定义表(EAV,按行业定义参数)';
DROP TABLE IF EXISTS `company_products_attr_value`;
CREATE TABLE `company_products_attr_value` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` INT UNSIGNED NOT NULL COMMENT '产品ID(关联 company_products.id)',
`attr_id` INT UNSIGNED NOT NULL COMMENT '参数属性ID(关联 company_products_attr.id)',
`value_string` TEXT NULL COMMENT '字符串值',
`value_number` DECIMAL(20,4) NULL COMMENT '数值',
`value_boolean` TINYINT(1) NULL COMMENT '布尔值',
`value_date` DATE NULL COMMENT '日期值',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_attr` (`product_id`, `attr_id`),
KEY `idx_product` (`product_id`),
KEY `idx_attr` (`attr_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品参数值表(EAV,产品与参数属性关联存储具体值)';
-- ------------------------------------------------------------
-- 6) 渠道新增计划表(渠道管理 - 渠道新增计划)
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `channel_plans`;
CREATE TABLE `channel_plans` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`channel_type` VARCHAR(50) NOT NULL COMMENT '渠道类别(= source_channel 枚举)',
`source_detail` VARCHAR(255) NULL DEFAULT NULL COMMENT '来源详情',
`industry` VARCHAR(100) NULL DEFAULT NULL COMMENT '所属行业',
`start_date` DATE NULL DEFAULT NULL COMMENT '时间窗口开始',
`end_date` DATE NULL DEFAULT NULL COMMENT '时间窗口结束',
`remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注说明',
`status` ENUM('待启动','已执行','错过') NOT NULL DEFAULT '待启动' COMMENT '状态反馈',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_channel_type` (`channel_type`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道新增计划表';
-188
View File
@@ -1,188 +0,0 @@
-- ============================================================
-- SuperLink Web 后台管理系统 - 系统表(含 v1.0.16 数据字典表)
-- 数据库:mysuperlink (utf8mb4 / utf8mb4_unicode_ci)
-- 说明:
-- 1) 原库已有14张业务表(companies/persons/social_accounts/company_needs 等),本文件不改动它们。
-- 2) 本文件定义系统表:system_users / system_roles / system_logs / channels,
-- 以及 v1.0.16 新增的数据字典表:date_dict_country / date_dict_area /
-- date_dict_industry / date_dict_certificate / source_channels。
-- 3) 按需求文档:所有时间字段调整为"年-月-日"(DATE 类型,不保留时分秒)。
-- 注意:DATE 默认值使用表达式 DEFAULT (CURRENT_DATE),需 MySQL 8.0.13+;
-- 若为 MySQL 5.7,请将时间字段保留为 DATETIME 或由应用层写入日期。
-- 4) 数据字典的完整数据由官方 Excel 生成,见 sql/dict_seed.sql(建表后执行)。
-- 执行方式: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` DATE DEFAULT NULL COMMENT '最后登录时间(年-月-日)',
`last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
`created_at` DATE DEFAULT (CURRENT_DATE) COMMENT '创建时间(年-月-日)',
`updated_at` DATE DEFAULT (CURRENT_DATE) 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` DATE DEFAULT (CURRENT_DATE) 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` DATE DEFAULT (CURRENT_DATE) 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` DATE DEFAULT (CURRENT_DATE) COMMENT '创建时间(年-月-日)',
`updated_at` DATE DEFAULT (CURRENT_DATE) 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='渠道管理表';
-- ------------------------------------------------------------
-- 6. v1.0.16 数据字典表
-- ------------------------------------------------------------
-- 6.1 国家地区编码字典
DROP TABLE IF EXISTS `date_dict_country`;
CREATE TABLE `date_dict_country` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '国家/地区编码',
`name` VARCHAR(100) NOT NULL COMMENT '国家/地区中文名',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家地区编码字典';
-- 6.2 行政区划编码字典(省/市/区县三级,parent_code 关联)
DROP TABLE IF EXISTS `date_dict_area`;
CREATE TABLE `date_dict_area` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行政区划编码',
`name` VARCHAR(100) NOT NULL COMMENT '行政区划名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行政区划编码(省级为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行政区划编码字典';
-- 6.3 招标行业分类字典(parent_code 预留大类/细分层级)
DROP TABLE IF EXISTS `date_dict_industry`;
CREATE TABLE `date_dict_industry` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行业编码',
`name` VARCHAR(100) NOT NULL COMMENT '行业名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行业编码(大类为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='招标行业分类字典';
-- 6.4 证书类型枚举字典(原 certification_types 表改名而来;新装环境在此创建)
DROP TABLE IF EXISTS `date_dict_certificate`;
CREATE TABLE `date_dict_certificate` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) DEFAULT NULL COMMENT '证书类型枚举编码',
`name` VARCHAR(100) NOT NULL COMMENT '证书类型名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='证书类型枚举字典';
-- 6.5 来源渠道字典(source_channel 枚举)
DROP TABLE IF EXISTS `source_channels`;
CREATE TABLE `source_channels` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '渠道名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='来源渠道字典';
-- 6.6 来源渠道默认数据
INSERT INTO `source_channels` (`name`, `sort_order`) VALUES
('展会', 1), ('SNS/社交媒体', 2), ('竞价广告', 3), ('搜索引擎自然流量', 4),
('行业媒体/门户', 5), ('转介绍', 6), ('电话销售', 7), ('邮件营销', 8),
('地推', 9), ('代理商/渠道', 10), ('官网咨询', 11), ('其他', 99);
-- 6.7 数据字典完整数据(国家/行政区划/行业/证书)由官方 Excel 生成:
-- 请执行 sql/dict_seed.sql
+135
View File
@@ -519,6 +519,141 @@ table.grid .ops a:hover { text-decoration: underline; }
.cert-row input[type="text"] { width: 170px; }
.cert-row input[type="date"] { width: 140px; }
/* ---------- 基本信息锚点导航布局(左侧文字锚 + 右侧分区) ---------- */
.form-anchor-layout {
display: flex;
gap: 14px;
align-items: flex-start;
}
.form-anchor-nav {
flex: 0 0 86px;
display: flex;
flex-direction: column;
gap: 2px;
position: sticky;
top: 0;
border-right: 1px solid #eef0f4;
padding-right: 10px;
padding-top: 4px;
}
.form-anchor-nav .anchor-link {
display: block;
padding: 7px 10px;
font-size: 13px;
color: #5a6472;
text-decoration: none;
border-radius: 4px;
white-space: nowrap;
}
.form-anchor-nav .anchor-link:hover { color: #2a5298; background: #f2f6fd; }
.form-anchor-nav .anchor-link.active {
color: #2a5298;
background: #e8f0fe;
font-weight: bold;
}
.form-anchor-body { flex: 1; min-width: 0; }
.form-sec { padding: 4px 2px 14px; border-bottom: 1px dashed #eef0f4; }
.form-sec:last-child { border-bottom: none; }
.form-sec-title {
font-size: 13px;
font-weight: bold;
color: #2a5298;
margin-bottom: 10px;
padding-left: 8px;
border-left: 3px solid #2a5298;
line-height: 1.4;
}
/* 联系方式行(企业 social_accounts) */
.account-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
flex-wrap: wrap;
}
.account-row select, .account-row input {
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 7px 8px;
font-size: 13px;
background: #fff;
}
.account-row select[name="acc-platform"] { width: 110px; }
.account-row input[name="acc-account"] { width: 150px; }
.account-row input[name="acc-url"] { width: 210px; }
.account-row input[name="acc-remark"] { width: 130px; }
.account-row select[name="acc-active"] { width: 76px; }
/* ---------- 人员工作履历行 ---------- */
.exp-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
flex-wrap: wrap;
}
.exp-row select, .exp-row input {
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 7px 8px;
font-size: 13px;
background: #fff;
}
.exp-row input[type="text"] { width: 110px; }
.exp-row input[type="date"] { width: 130px; }
/* 家乡置灰(中国/港澳台国籍时不可填) */
.area-disabled select, .area-disabled input {
background: #f2f3f5 !important;
color: #b6bfcc !important;
cursor: not-allowed;
}
/* 人脉弹窗姓名链接 */
.link { color: #2a5298; text-decoration: underline; cursor: pointer; }
.link:hover { color: #4fa3ff; }
/* ---------- 渠道管理:效能分析面板 ---------- */
.ana-panel {
flex: 1 1 46%;
min-width: 340px;
border: 1px solid #e5e9f0;
border-radius: 6px;
overflow: hidden;
}
.ana-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: #f7f9fc;
border-bottom: 1px solid #e5e9f0;
font-size: 13px;
font-weight: bold;
color: #1e2a3a;
}
.ana-year {
border: 1px solid #d9dee8;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
background: #fff;
}
.ana-body { padding: 12px 14px; }
.ana-chart { width: 100%; height: 260px; }
.ana-detail {
margin-top: 10px;
border-top: 1px dashed #eef0f4;
padding-top: 10px;
}
.ana-detail-title {
font-size: 12px;
color: #2a5298;
font-weight: bold;
margin-bottom: 6px;
}
/* ---------- 页脚(版权/备案) ---------- */
.footer {
padding: 14px 22px;
+275 -89
View File
@@ -1,173 +1,359 @@
/**
* channel.js - 渠道管理逻辑(渠道列表 + 年度统计 + 效率数据)
* channel.js - 渠道管理逻辑(v1.0.18 重构)
* 目标:记录企业/人员数据的来源渠道,并根据以往数据有计划地拓展高效渠道
* 两部分:
* 1) 渠道效能分析:企业(左)/人员(右)两个板块,右上角年份下拉(默认全部)
* - 按「渠道」字段饼状图按百分比排序;鼠标悬停饼图区域显示该渠道 source_detail TOP10 表格
* - 表格字段:来源(只显示前 8 字,超出用…)、数量;默认显示渠道第一名
* 2) 渠道新增计划:列表(渠道类别/来源详情/所属行业/时间窗口/剩余天数/备注说明/状态反馈)
*/
$(function () {
renderShell('渠道管理', '业务模块 / 渠道管理');
renderPage();
initPage('channel', function (user) {
if (!checkPagePermission('channel')) return;
loadList(1);
loadStats();
loadAnalysis();
loadPlans(1);
});
var currentPage = 1;
var pageRows = [];
var analysisData = null;
var charts = { company: null, person: null };
/** 渲染页面内容(统计卡+工具栏+表格+分页) */
/** 渲染页面内容:渠道效能分析(左右两板块)+ 渠道新增计划 */
function renderPage() {
$('#page-content').html(
'<div class="stat-cards" id="stat-summary"></div>' +
'<div class="card">' +
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">' +
' <b style="font-size:15px;color:#1e2a3a;">渠道效能分析</b>' +
' <span style="font-size:12px;color:#8a94a6;">按「渠道」字段统计,鼠标悬停饼图查看该渠道来源详情 TOP10</span>' +
' </div>' +
' <div style="display:flex;gap:14px;flex-wrap:wrap;">' +
' <div class="ana-panel">' +
' <div class="ana-head"><span>企业数据</span>' +
' <select id="ana-year-company" class="ana-year"><option value="">全部年份</option></select></div>' +
' <div class="ana-body">' +
' <div id="ana-chart-company" class="ana-chart"></div>' +
' <div class="ana-detail"><div class="ana-detail-title" id="ana-detail-title-c">渠道明细</div>' +
' <table class="grid" style="font-size:12px;"><thead><tr><th>来源</th><th>数量</th></tr></thead>' +
' <tbody id="ana-detail-c"></tbody></table></div>' +
' </div>' +
' </div>' +
' <div class="ana-panel">' +
' <div class="ana-head"><span>人员数据</span>' +
' <select id="ana-year-person" class="ana-year"><option value="">全部年份</option></select></div>' +
' <div class="ana-body">' +
' <div id="ana-chart-person" class="ana-chart"></div>' +
' <div class="ana-detail"><div class="ana-detail-title" id="ana-detail-title-p">渠道明细</div>' +
' <table class="grid" style="font-size:12px;"><thead><tr><th>来源</th><th>数量</th></tr></thead>' +
' <tbody id="ana-detail-p"></tbody></table></div>' +
' </div>' +
' </div>' +
' </div>' +
'</div>' +
'<div class="card" style="margin-top:14px;">' +
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">' +
' <b style="font-size:15px;color:#1e2a3a;">渠道新增计划</b>' +
' <span style="font-size:12px;color:#8a94a6;">根据历史数据有计划地拓展高效渠道</span>' +
' </div>' +
' <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>' +
' <input type="text" data-filter="keyword" placeholder="来源详情/行业/备注关键词">' +
' <select data-filter="channel_type"><option value="">全部渠道类别</option></select>' +
' <select data-filter="status"><option value="">全部状态</option>' +
' <option value="待启动">待启动</option><option value="已执行">已执行</option><option value="错过">错过</option></select>' +
' <button class="btn btn-primary" id="btn-plan-search">搜索</button>' +
' <button class="btn" id="btn-plan-reset">重置</button>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success" id="btn-add">+ 新增</button>' +
' <button class="btn btn-success" id="btn-plan-add">+ 新增计划</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th>ID</th><th>渠道名称</th><th>类型</th><th>联系人</th><th>电话</th><th>邮箱</th><th>效率评分</th><th>备注</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="channel-tbody"></tbody>' +
' <thead><tr><th>ID</th><th>渠道类别</th><th>来源详情</th><th>所属行业</th><th>时间窗口</th><th>剩余天数</th><th>备注说明</th><th>状态反馈</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="plan-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pagination"></div>' +
' <div class="pagination" id="plan-pagination"></div>' +
'</div>'
);
}
/* ---------- 列表 ---------- */
function loadList(page) {
/* ============================================================
* 1) 渠道效能分析
* ============================================================ */
function loadAnalysis() {
var year = null;
var $c = $('#ana-year-company');
var $p = $('#ana-year-person');
if ($c.length && $p.length) {
var yc = $c.val();
var yp = $p.val();
year = (yc === yp) ? yc : ''; // 两板块年份不同时,分别请求
}
if (year !== null && year === '' && $c.length && $p.length && $c.val() !== $p.val()) {
// 两板块各自请求
httpGet('channel/analysis.php', { year: $c.val() || '' }).then(function (d) {
analysisData = d;
renderAnalysis(d, 'company');
fillYears(d, 'company');
});
httpGet('channel/analysis.php', { year: $p.val() || '' }).then(function (d) {
renderAnalysis(d, 'person');
fillYears(d, 'person');
});
return;
}
httpGet('channel/analysis.php', { year: year || '' }).then(function (d) {
analysisData = d;
fillYears(d, 'company');
fillYears(d, 'person');
renderAnalysis(d, 'company');
renderAnalysis(d, 'person');
});
}
function fillYears(d, side) {
var $sel = side === 'company' ? $('#ana-year-company') : $('#ana-year-person');
var cur = $sel.val();
var html = '<option value="">全部年份</option>';
(d.years || []).forEach(function (y) {
html += '<option value="' + y + '"' + (String(y) === String(cur) ? ' selected' : '') + '>' + y + '年</option>';
});
$sel.html(html);
}
function renderAnalysis(d, side) {
var rows = side === 'company' ? (d.company || []) : (d.person || []);
var chartId = side === 'company' ? 'ana-chart-company' : 'ana-chart-person';
var detailBodyId = side === 'company' ? 'ana-detail-c' : 'ana-detail-p';
var detailTitleId = side === 'company' ? 'ana-detail-title-c' : 'ana-detail-title-p';
// 饼图数据(按百分比排序)
var pieData = rows.map(function (r) {
return { name: r.channel, value: r.count, percent: r.percent };
}).sort(function (a, b) { return b.value - a.value; });
if (!charts[side]) {
charts[side] = echarts.init(document.getElementById(chartId));
}
var chart = charts[side];
chart.clear();
chart.setOption({
tooltip: {
trigger: 'item',
formatter: function (p) {
if (p.data && p.data.percent !== undefined) {
return p.data.name + '<br/>数量:' + p.data.value + '(' + p.data.percent + '%)';
}
return p.name + '<br/>数量:' + p.value;
}
},
legend: { bottom: 0, type: 'scroll', textStyle: { fontSize: 11 } },
series: [{
type: 'pie',
radius: ['38%', '62%'],
center: ['50%', '44%'],
data: pieData,
label: { show: true, formatter: '{b}\n{d}%', fontSize: 11 },
emphasis: { itemStyle: { shadowBlur: 8, shadowColor: 'rgba(0,0,0,0.2)' } }
}]
});
// 默认显示第一名渠道的 source_detail TOP10
function renderDetail(index) {
var r = rows[index];
if (!r) {
$('#' + detailBodyId).html('<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无数据</td></tr>');
return;
}
$('#' + detailTitleId).text(r.channel + ' 来源明细(TOP' + Math.min(10, (r.details || []).length) + ',共 ' + r.count + ' 条)');
var html = '';
(r.details || []).forEach(function (item) {
var src = item.source || '';
var short = src.length > 8 ? src.substring(0, 8) + '…' : src;
html += '<tr><td title="' + escHtml(src) + '">' + escHtml(short) + '</td><td>' + (item.count !== undefined ? item.count : item.cnt) + '</td></tr>';
});
if (!(r.details || []).length) {
html = '<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无明细</td></tr>';
}
$('#' + detailBodyId).html(html);
}
renderDetail(0);
// 悬停饼图区域 -> 显示该渠道 TOP10 表格
chart.off('mouseover');
chart.on('mouseover', function (p) {
if (p.dataIndex !== undefined) {
renderDetail(p.dataIndex);
}
});
chart.off('mouseout');
chart.on('mouseout', function () {
renderDetail(0);
});
// 空数据提示
if (!rows.length) {
$('#' + detailBodyId).html('<tr><td colspan="2" style="color:#b6bfcc;text-align:center;padding:16px;">暂无渠道数据(企业/人员需填写来源渠道)</td></tr>');
}
}
$('#ana-year-company, #ana-year-person').on('change', function () {
loadAnalysis();
});
/* ============================================================
* 2) 渠道新增计划
* ============================================================ */
function loadPlans(page) {
currentPage = page;
var params = buildFilterParam();
delete params.year; // 年度仅用于统计
params.page = page;
params.limit = PAGE_SIZE;
httpGet('channel/list.php', params).then(function (data) {
httpGet('channel/plan_list.php', params).then(function (data) {
var rows = data.list || [];
pageRows = rows;
var html = '';
rows.forEach(function (r) {
var win = r.start_date && r.end_date
? fmtWin(r.start_date) + ' 至 ' + fmtWin(r.end_date)
: (r.start_date || r.end_date || '-');
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>' + escHtml(r.channel_type) + '</td>' +
'<td title="' + escHtml(r.source_detail || '') + '">' + escHtml((r.source_detail || '-').substring(0, 16)) + '</td>' +
'<td>' + escHtml(r.industry || '-') + '</td>' +
'<td>' + escHtml(win) + '</td>' +
'<td>' + (r.remaining_days > 0 ? '<span class="tag' + (r.remaining_days <= 7 ? ' tag-red' : '') + '">' + r.remaining_days + ' 天</span>' : '<span class="tag tag-gray">已到期</span>') + '</td>' +
'<td title="' + escHtml(r.remark || '') + '">' + escHtml((r.remark || '-').substring(0, 20)) + '</td>' +
'<td>' + statusTag(r.status) + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops"><a onclick="editChannel(' + r.id + ')">编辑</a>' +
'<a class="danger" onclick="delChannel(' + r.id + ')">删除</a></td></tr>';
'<td class="ops"><a onclick="editPlan(' + r.id + ')">编辑</a>' +
'<a class="danger" onclick="delPlan(' + 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>';
html = '<tr><td colspan="10" class="empty-tip" style="padding:40px 0;">暂无渠道新增计划</td></tr>';
}
$('#channel-tbody').html(html);
renderPagination($('#pagination'), data, loadList);
$('#plan-tbody').html(html);
renderPagination($('#plan-pagination'), data, loadPlans);
});
}
/* ---------- 年度统计 ---------- */
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;
});
/** 时间窗口显示格式:2025-4-1 */
function fmtWin(d) {
if (!d) return '';
var parts = String(d).split('-');
if (parts.length === 3) {
return parseInt(parts[0], 10) + '-' + parseInt(parts[1], 10) + '-' + parseInt(parts[2], 10);
}
return d;
}
$('#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;
});
function statusTag(s) {
if (s === '已执行') return '<span class="tag tag-green">已执行</span>';
if (s === '错过') return '<span class="tag tag-red">错过</span>';
return '<span class="tag">待启动</span>';
}
$('#btn-plan-search').on('click', function () { loadPlans(1); });
$('#btn-plan-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
loadPlans(1);
});
/* ---------- 新增/编辑 ---------- */
$('#btn-add').on('click', function () { openForm(null); });
window.editChannel = function (id) {
/* ---------- 新增/编辑计划 ---------- */
$('#btn-plan-add').on('click', function () { openPlanForm(null); });
window.editPlan = function (id) {
var row = null;
pageRows.forEach(function (r) { if (r.id == id) row = r; });
openForm(row);
openPlanForm(row);
};
function openForm(ch) {
var isEdit = !!ch;
var c = ch || {};
function openPlanForm(pl) {
var isEdit = !!pl;
var r = pl || {};
// 渠道类别下拉(来自 source_channels 字典)
var chHtml = '<option value="">请选择渠道类别</option>';
var hasCur = false;
if (window._channelTypes && window._channelTypes.length) {
window._channelTypes.forEach(function (c) {
var sel = (c === r.channel_type) ? ' selected' : '';
if (sel) hasCur = true;
chHtml += '<option value="' + escHtml(c) + '"' + sel + '>' + escHtml(c) + '</option>';
});
}
if (r.channel_type && !hasCur) {
chHtml += '<option value="' + escHtml(r.channel_type) + '" selected>' + escHtml(r.channel_type) + '</option>';
}
var content =
'<form id="channel-form" style="padding:18px 22px 4px;">' +
'<form id="plan-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 class="form-item"><label>渠道类别<span class="req">*</span></label><select name="channel_type">' + chHtml + '</select></div>' +
' <div class="form-item"><label>所属行业</label><input type="text" name="industry" value="' + escHtml(r.industry || '') + '"></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(r.source_detail || '') + '" placeholder="如:华南工博会"></div>' +
' <div class="form-item"><label>状态反馈</label><select name="status">' +
' <option value="待启动"' + (!r.status || r.status === '待启动' ? ' selected' : '') + '>待启动</option>' +
' <option value="已执行"' + (r.status === '已执行' ? ' selected' : '') + '>已执行</option>' +
' <option value="错过"' + (r.status === '错过' ? ' selected' : '') + '>错过</option></select></div>' +
' <div class="form-item"><label>开始日期</label><input type="date" name="start_date" value="' + escHtml(r.start_date || '') + '"></div>' +
' <div class="form-item"><label>结束日期</label><input type="date" name="end_date" value="' + escHtml(r.end_date || '') + '"></div>' +
' <div class="form-item full"><label>备注说明</label><input type="text" name="remark" value="' + escHtml(r.remark || '') + '"></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<div class="dialog-footer"><button type="button" class="btn" id="plan-form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: isEdit ? '编辑渠道' : '新增渠道',
title: isEdit ? '编辑计划' : '新增计划',
area: ['680px', 'auto'],
content: content,
btn: false
});
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#plan-form-cancel').on('click', function () { Dialog.close(idx); });
$('#channel-form').on('submit', function (e) {
$('#plan-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, textarea').each(function () {
$(this).find('input, select').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;
if (!data.channel_type) { Dialog.error('渠道类别为必填项'); return; }
if (data.start_date && data.end_date && data.start_date > data.end_date) {
Dialog.error('开始日期不能晚于结束日期');
return;
}
if (isEdit) data.id = r.id;
httpPost(isEdit ? 'channel/save.php' : 'channel/save.php', data).then(function () {
httpPost('channel/plan_save.php', data).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
loadList(currentPage);
loadStats();
loadPlans(currentPage);
});
});
});
}
window.delChannel = function (id) {
Dialog.confirm('确定删除该渠道吗?', function () {
httpPost('channel/delete.php', { id: id }).then(function () {
window.delPlan = function (id) {
Dialog.confirm('确定删除该计划吗?', function () {
httpPost('channel/plan_delete.php', { id: id }).then(function () {
Dialog.success('删除成功', function () {
loadList(currentPage);
loadStats();
loadPlans(currentPage);
});
});
});
};
/* ---------- 初始化渠道类别下拉(新增计划用 + 列表筛选) ---------- */
httpGet('common/dicts.php', { type: 'source_channel' }).then(function (d) {
var types = (d.source_channels || []).map(function (s) { return s.name; });
window._channelTypes = types;
var html = '<option value="">全部渠道类别</option>';
types.forEach(function (t) {
html += '<option value="' + escHtml(t) + '">' + escHtml(t) + '</option>';
});
$('.toolbar select[data-filter="channel_type"]').html(html);
}).catch(function () {});
});
+419 -66
View File
@@ -1,12 +1,24 @@
/**
* company.js - 企业数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 导入导出 / 详情)
* company.js - 企业数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 产品 / 官媒 / 导入导出 / 详情)
* v1.0.18:
* - 检索区:筛选字段下拉(全部字段/指定字段,与检索词联动)+ 行业下拉 + 企业类型下拉(去掉国家/地区框)
* - 新增/编辑:Tab 卡改为 基本信息|财务信息|资质认证;基本信息内左侧文字锚导航(工商信息/联系方式/行业归属/业务类型/来源)
* - 表单字段:公司名称→企业名称、法律形式→企业类型(字典下拉)、删除英文名称;资质认证增加 级别/颁证机构
* - 产品弹窗:展示 产品品类/基本定位/包含系列/状态/更新日期 + 新增产品按钮(EAV 参数)
* - 新增「官媒」入口:公司媒体清单 + 新增官媒按钮
*/
$(function () {
renderShell('企业数据', '数据管理 / 企业数据');
renderPage();
initPage('company', function (user) {
if (!checkPagePermission('company')) return;
loadList(1);
loadDicts().then(function (dicts) {
window._companyDicts = dicts;
fillSearchDicts(dicts);
loadList(1);
}).catch(function () {
loadList(1);
});
});
var currentPage = 1;
@@ -16,13 +28,16 @@ $(function () {
$('#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="国家/地区">' +
' <select id="search-field"><option value="">全部字段</option>' +
' <option value="name_zh">企业名称</option><option value="name_en">英文名称</option>' +
' <option value="registration_number">注册号</option><option value="legal_representative">法定代表人</option>' +
' <option value="address">注册地址</option><option value="industry">行业</option>' +
' <option value="industry_subdivision">行业细分</option><option value="website">官网</option>' +
' <option value="source_channel">来源渠道</option><option value="source_detail">来源详情</option>' +
' <option value="business_scope">经营范围</option><option value="stock_code">股票代码</option></select>' +
' <input type="text" id="search-keyword" placeholder="输入要检索的内容">' +
' <select id="search-industry" data-filter="industry"><option value="">全部行业</option></select>' +
' <select id="search-company-type" data-filter="company_type"><option value="">全部企业类型</option></select>' +
' <button class="btn btn-primary" id="btn-search">搜索</button>' +
' <button class="btn" id="btn-reset">重置</button>' +
' <span class="spacer"></span>' +
@@ -31,7 +46,7 @@ $(function () {
' <button class="btn" id="btn-export">导出</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>' +
' <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>' +
@@ -40,12 +55,36 @@ $(function () {
$('#check-all').on('change', function () { $('.row-check').prop('checked', this.checked); });
}
/** 填充检索区字典下拉(行业/企业类型) */
function fillSearchDicts(dicts) {
var indHtml = '<option value="">全部行业</option>';
(dicts.industries || []).forEach(function (i) {
indHtml += '<option value="' + escHtml(i.name) + '">' + escHtml(i.name) + '</option>';
});
$('#search-industry').html(indHtml);
var ctHtml = '<option value="">全部企业类型</option>';
(dicts.company_types || []).forEach(function (t) {
ctHtml += '<option value="' + escHtml(t.name) + '">' + escHtml(t.name) + '</option>';
});
$('#search-company-type').html(ctHtml);
}
/* ---------- 列表 ---------- */
function loadList(page) {
currentPage = page;
var params = buildFilterParam();
params.page = page;
params.limit = PAGE_SIZE;
var params = {
field: $('#search-field').val(),
keyword: $.trim($('#search-keyword').val()),
industry: $('#search-industry').val(),
company_type: $('#search-company-type').val(),
page: page,
limit: PAGE_SIZE
};
// 空值不传
Object.keys(params).forEach(function (k) {
if (params[k] === '') delete params[k];
});
httpGet('company/list.php', params).then(function (data) {
var rows = data.list || [];
@@ -67,6 +106,7 @@ $(function () {
' <a onclick="viewCompany(' + r.id + ')">详情</a>' +
' <a onclick="viewEmployees(' + r.id + ')">员工</a>' +
' <a onclick="viewProducts(' + r.id + ')">产品</a>' +
' <a onclick="viewOfficialMedia(' + r.id + ')">官媒</a>' +
' <a onclick="editCompany(' + r.id + ')">编辑</a>' +
'</td></tr>';
});
@@ -80,8 +120,12 @@ $(function () {
/* ---------- 筛选 ---------- */
$('#btn-search').on('click', function () { loadList(1); });
$('#search-keyword').on('keydown', function (e) { if (e.keyCode === 13) loadList(1); });
$('#btn-reset').on('click', function () {
$('.toolbar [data-filter]').val('');
$('#search-field').val('');
$('#search-keyword').val('');
$('#search-industry').val('');
$('#search-company-type').val('');
loadList(1);
});
@@ -89,22 +133,24 @@ $(function () {
$('#btn-add').on('click', function () {
httpGet('company/cert_types.php').then(function (d) {
loadDicts().then(function (dicts) {
openForm(null, [], [], d.types || [], dicts);
window._companyDicts = dicts;
openForm(null, [], [], [], d.types || [], dicts);
});
});
});
window.editCompany = function (id) {
httpGet('company/detail.php', { id: id }).then(function (d) {
loadDicts().then(function (dicts) {
openForm(d.company, d.financials, d.certifications, d.certification_types, dicts);
window._companyDicts = dicts;
openForm(d.company, d.financials, d.certifications, d.accounts || [], d.certification_types, dicts);
});
});
};
/** 拉取数据字典(国家/行业/来源渠道),供表单下拉使用 */
/** 拉取数据字典(国家/行业/来源渠道/企业类型),供表单与检索使用 */
function loadDicts() {
return httpGet('common/dicts.php', { type: 'all' }).catch(function () {
return { countries: [], industries: [], source_channels: [] };
return { countries: [], industries: [], source_channels: [], company_types: [] };
});
}
@@ -124,45 +170,91 @@ $(function () {
return html;
}
function openForm(company, financials, certifications, certTypes, dicts) {
/* ============================================================
* 新增/编辑弹窗:Tab = 基本信息 | 财务信息 | 资质认证
* 基本信息内左侧文字锚导航:工商信息 / 联系方式 / 行业归属 / 业务类型 / 来源
* ============================================================ */
function openForm(company, financials, certifications, accounts, certTypes, dicts) {
var isEdit = !!company;
var c = company || {};
financials = financials || [];
certifications = certifications || [];
accounts = accounts || [];
certTypes = certTypes || [];
dicts = dicts || {};
// Tab1 工商信息
// ---- 基本信息 Tab(含左侧锚导航 + 5 个分区) ----
var secBiz =
'<div class="form-sec" id="sec-biz">' +
' <div class="form-sec-title">工商信息</div>' +
' <div class="form-grid">' +
' <div class="form-item"><label>企业名称<span class="req">*</span></label>' +
' <div style="display:flex;gap:8px;align-items:center;">' +
' <input type="text" name="name_zh" value="' + escHtml(c.name_zh) + '" style="width:60%;flex:0 0 auto;">' +
' <button type="button" class="btn btn-sm" id="btn-gongshang-query" title="预留:后续接入工商查询API,自动填写企业名称/注册号/注册地址等">工商查询</button>' +
' </div></div>' +
' <div class="form-item"><label>国家/地区</label><select name="country">' + dictOptions(dicts.countries, c.country) + '</select></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><select name="legal_form">' + dictOptions(dicts.company_types, c.legal_form) + '</select></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="date" name="established_date" value="' + escHtml(c.established_date) + '"></div>' +
' <div class="form-item"><label>注册资本(万元)</label><input type="text" name="registered_capital" value="' + escHtml(c.registered_capital) + '"></div>' +
' <div class="form-item full"><label>经营范围</label><textarea name="business_scope">' + escHtml(c.business_scope) + '</textarea></div>' +
' </div></div>';
// 联系方式(social_accounts,整表替换)
var accountRowsHtml = '';
if (!accounts.length) {
accountRowsHtml = accountRowHtml(null);
} else {
accounts.forEach(function (a) { accountRowsHtml += accountRowHtml(a); });
}
var secContact =
'<div class="form-sec" id="sec-contact">' +
' <div class="form-sec-title">联系方式' +
' <button type="button" class="btn btn-sm" id="add-account-row" style="margin-left:10px;">+ 添加</button></div>' +
' <div id="account-rows">' + accountRowsHtml + '</div>' +
' <div style="font-size:12px;color:#8a94a6;margin-top:6px;">如:官网、公众号、微博、抖音、领英等平台账号</div>' +
'</div>';
var secIndustry =
'<div class="form-sec" id="sec-industry">' +
' <div class="form-sec-title">行业归属</div>' +
' <div class="form-grid">' +
' <div class="form-item"><label>行业</label><select name="industry">' + dictOptions(dicts.industries, c.industry) + '</select></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></div>';
var secBizType =
'<div class="form-sec" id="sec-biztype">' +
' <div class="form-sec-title">业务类型</div>' +
' <div class="form-grid">' +
' <div class="form-item"><label>业务角色</label><select name="business_role">' + roleOptions(c.business_role) + '</select></div>' +
' </div></div>';
var secSource =
'<div class="form-sec" id="sec-source">' +
' <div class="form-sec-title">来源</div>' +
' <div class="form-grid">' +
' <div class="form-item"><label>来源渠道</label><select name="source_channel">' + dictOptions(dicts.source_channels, c.source_channel) + '</select></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(c.source_detail) + '" placeholder="如:华南工博会"></div>' +
' </div></div>';
var bizHtml =
'<div class="form-grid">' +
' <div class="form-item"><label>公司名称<span class="req">*</span></label>' +
' <div style="display:flex;gap:8px;align-items:center;">' +
' <input type="text" name="name_zh" value="' + escHtml(c.name_zh) + '" style="width:60%;flex:0 0 auto;">' +
' <button type="button" class="btn btn-sm" id="btn-gongshang-query" title="预留:后续接入工商查询API,自动填写公司名称/注册号/注册地址等">工商查询</button>' +
' </div></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="country">' + dictOptions(dicts.countries, c.country) + '</select></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_form" value="' + escHtml(c.legal_form) + '"></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="date" name="established_date" value="' + escHtml(c.established_date) + '"></div>' +
' <div class="form-item"><label>注册资本(万元)</label><input type="text" name="registered_capital" value="' + escHtml(c.registered_capital) + '"></div>' +
' <div class="form-item full"><label>经营范围</label><textarea name="business_scope">' + escHtml(c.business_scope) + '</textarea></div>' +
'<div class="form-anchor-layout">' +
' <div class="form-anchor-nav">' +
' <a href="#sec-biz" class="anchor-link active">工商信息</a>' +
' <a href="#sec-contact" class="anchor-link">联系方式</a>' +
' <a href="#sec-industry" class="anchor-link">行业归属</a>' +
' <a href="#sec-biztype" class="anchor-link">业务类型</a>' +
' <a href="#sec-source" class="anchor-link">来源</a>' +
' </div>' +
' <div class="form-anchor-body">' + secBiz + secContact + secIndustry + secBizType + secSource + '</div>' +
'</div>';
// Tab2 行业信息(业务角色/行业/行业细分/官网)
var indHtml =
'<div class="form-grid">' +
' <div class="form-item"><label>业务角色</label><select name="business_role">' + roleOptions(c.business_role) + '</select></div>' +
' <div class="form-item"><label>行业</label><select name="industry">' + dictOptions(dicts.industries, c.industry) + '</select></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><select name="source_channel">' + dictOptions(dicts.source_channels, c.source_channel) + '</select></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(c.source_detail) + '" placeholder="如:华南工博会"></div>' +
'</div>';
// Tab3 财务信息:顶部独立字段(是否上市/股票代码/最新员工/最新营收)+ 年度财务明细列表
// ---- 财务信息 Tab(保持不变) ----
var finRowsHtml = '';
if (!financials.length) {
finRowsHtml = finRowHtml(null);
@@ -187,7 +279,7 @@ $(function () {
' <tbody id="fin-tbody">' + finRowsHtml + '</tbody>' +
'</table></div>';
// Tab3 资质认证:列表形式(瞪羚企业等)
// ---- 资质认证 Tab(含级别/颁证机构) ----
var certRowsHtml = '';
if (!certifications.length) {
certRowsHtml = certRowHtml(null, certTypes);
@@ -206,14 +298,12 @@ $(function () {
'<form id="company-form" style="padding:14px 22px 4px;">' +
' <div class="edit-tabs">' +
' <div class="edit-tabs-head">' +
' <span class="edit-tab active" data-tab="biz" onclick="switchEditTab(this)">工商信息</span>' +
' <span class="edit-tab" data-tab="ind" onclick="switchEditTab(this)">行业信息</span>' +
' <span class="edit-tab active" data-tab="biz" onclick="switchEditTab(this)">基本信息</span>' +
' <span class="edit-tab" data-tab="fin" onclick="switchEditTab(this)">财务信息</span>' +
' <span class="edit-tab" data-tab="cert" onclick="switchEditTab(this)">资质认证</span>' +
' </div>' +
' <div class="edit-tabs-body">' +
' <div class="edit-pane" id="edit-pane-biz">' + bizHtml + '</div>' +
' <div class="edit-pane" id="edit-pane-ind" style="display:none;">' + indHtml + '</div>' +
' <div class="edit-pane" id="edit-pane-fin" style="display:none;">' + finHtml + '</div>' +
' <div class="edit-pane" id="edit-pane-cert" style="display:none;">' + certHtml + '</div>' +
' </div>' +
@@ -224,27 +314,29 @@ $(function () {
var idx = Dialog.open({
title: isEdit ? '编辑企业' : '新增企业',
area: ['820px', 'auto'],
area: ['860px', 'auto'],
content: content,
btn: false
});
bindAnchorNav();
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#add-fin-row').on('click', function () { $('#fin-tbody').append(finRowHtml(null)); });
$('#add-cert-row').on('click', function () { $('#cert-rows').append(certRowHtml(null, certTypes)); });
$('#add-account-row').on('click', function () { $('#account-rows').append(accountRowHtml(null)); });
// 工商查询按钮(预留:后续接入工商查询API自动填写)
$('#btn-gongshang-query').on('click', function () {
Dialog.alert('工商查询API预留中,接入后可通过企业名称自动查询并填写公司名称、注册号、注册地址等信息');
Dialog.alert('工商查询API预留中,接入后可通过企业名称自动查询并填写企业名称、注册号、注册地址等信息');
});
$('#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('公司名称或英文名称至少填写一项');
if (!nameZh) {
Dialog.error('企业名称为必填项');
return;
}
@@ -271,7 +363,7 @@ $(function () {
});
fd.append('financials', JSON.stringify(financials));
// 资质认证(整表替换)
// 资质认证(整表替换,含级别/颁证机构)
var certs = [];
$('#cert-rows .cert-row').each(function () {
var $r = $(this);
@@ -280,12 +372,32 @@ $(function () {
certs.push({
certification_type_id: tid,
certificate_number: $r.find('[name="cert-number"]').val(),
level: $r.find('[name="cert-level"]').val(),
issuing_authority: $r.find('[name="cert-issuer"]').val(),
issue_date: $r.find('[name="cert-issue"]').val(),
expiry_date: $r.find('[name="cert-expiry"]').val()
});
});
fd.append('certifications', JSON.stringify(certs));
// 联系方式(整表替换)
var accounts = [];
$('#account-rows .account-row').each(function () {
var $r = $(this);
var platform = $.trim($r.find('[name="acc-platform"]').val());
var accountId = $.trim($r.find('[name="acc-account"]').val());
if (!platform || !accountId) return;
accounts.push({
platform: platform,
account_id: accountId,
profile_url: $r.find('[name="acc-url"]').val(),
remark: $r.find('[name="acc-remark"]').val(),
is_active: $r.find('[name="acc-active"]').val(),
is_defult: $r.find('[name="acc-defult"]').prop('checked') ? 1 : 0
});
});
fd.append('accounts', JSON.stringify(accounts));
httpPost(isEdit ? 'company/update.php' : 'company/add.php', fd, true).then(function () {
Dialog.success('保存成功', function () {
Dialog.close(idx);
@@ -304,6 +416,40 @@ $(function () {
$('#edit-pane-' + $(el).data('tab')).show();
};
/** 基本信息内左侧锚点导航:点击平滑滚动 + 高亮 */
function bindAnchorNav() {
$('.form-anchor-nav .anchor-link').off('click').on('click', function (e) {
e.preventDefault();
var $target = $($(this).attr('href'));
var $body = $target.closest('.edit-pane');
// 若锚点所在分区不在当前可见 Tab,先切到对应 Tab
if (!$body.is(':visible')) {
var tab = $body.attr('id').replace('edit-pane-', '');
var $tabEl = $('.edit-tabs-head .edit-tab[data-tab="' + tab + '"]');
if ($tabEl.length) switchEditTab($tabEl[0]);
}
var offset = $target.offset();
if (offset) {
$('.edit-tabs-body').animate({ scrollTop: $target.offset().top - $('.edit-tabs-body').offset().top + $('.edit-tabs-body').scrollTop() }, 200);
}
$('.form-anchor-nav .anchor-link').removeClass('active');
$(this).addClass('active');
});
// 滚动时高亮当前分区
var $secs = $('.form-anchor-body .form-sec');
$('.edit-tabs-body').off('scroll.anchor').on('scroll.anchor', function () {
var top = $(this).scrollTop() + 60;
var currentId = null;
$secs.each(function () {
if ($(this).position().top <= top) currentId = $(this).attr('id');
});
if (currentId) {
$('.form-anchor-nav .anchor-link').removeClass('active');
$('.form-anchor-nav .anchor-link[href="#' + currentId + '"]').addClass('active');
}
});
}
/** 年度财务明细行模板(列表形式,一行一个财年) */
function finRowHtml(f) {
f = f || {};
@@ -325,7 +471,7 @@ $(function () {
'</tr>';
}
/** 资质认证行模板 */
/** 资质认证行模板(含级别/颁证机构) */
function certRowHtml(ct, certTypes) {
ct = ct || {};
var opts = '<option value="">请选择认证类型</option>';
@@ -335,12 +481,35 @@ $(function () {
return '<div class="cert-row">' +
'<select name="cert-type">' + opts + '</select>' +
'<input type="text" name="cert-number" placeholder="证书编号" value="' + escHtml(ct.certificate_number || '') + '">' +
'<input type="text" name="cert-level" placeholder="级别" value="' + escHtml(ct.level || '') + '">' +
'<input type="text" name="cert-issuer" placeholder="颁证机构" value="' + escHtml(ct.issuing_authority || '') + '">' +
'<input type="date" name="cert-issue" value="' + escHtml(ct.issue_date || '') + '">' +
'<input type="date" name="cert-expiry" value="' + escHtml(ct.expiry_date || '') + '">' +
'<button type="button" class="btn btn-sm btn-danger" onclick="$(this).closest(\'.cert-row\').remove()">删除</button>' +
'</div>';
}
/** 联系方式行模板(企业 social_accounts) */
function accountRowHtml(a) {
a = a || {};
var empty = function (v) { return (v === null || v === undefined) ? '' : v; };
var platOpts = ['', '官网', '微信公众号', '微信', '微博', '抖音', '快手', 'B站', '小红书', '领英', 'Facebook', 'Twitter/X', 'YouTube', 'Instagram', '其他'];
var platHtml = '';
platOpts.forEach(function (p) {
platHtml += '<option value="' + escHtml(p) + '"' + (String(p) === String(a.platform || '') ? ' selected' : '') + '>' + (p || '平台') + '</option>';
});
return '<div class="account-row">' +
'<select name="acc-platform">' + platHtml + '</select>' +
'<input type="text" name="acc-account" placeholder="账号名称/ID" value="' + escHtml(empty(a.account_id)) + '">' +
'<input type="text" name="acc-url" placeholder="主页链接" value="' + escHtml(empty(a.profile_url)) + '">' +
'<input type="text" name="acc-remark" placeholder="备注" value="' + escHtml(empty(a.remark)) + '">' +
'<select name="acc-active" title="状态"><option value="1"' + (a.is_active !== 0 ? ' selected' : '') + '>启用</option>' +
'<option value="0"' + (a.is_active === 0 ? ' selected' : '') + '>停用</option></select>' +
'<label title="常用" style="font-size:12px;color:#5a6472;white-space:nowrap;"><input type="checkbox" name="acc-defult"' + (a.is_defult ? ' checked' : '') + '> 常用</label>' +
'<button type="button" class="btn btn-sm btn-danger" onclick="$(this).closest(\'.account-row\').remove()">删除</button>' +
'</div>';
}
function roleOptions(sel) {
var opts = ['', 'manufacturer', 'integrator', 'distributor', 'brand_operator', 'others'];
var labels = { 'manufacturer': '生产型', 'integrator': '集成商', 'distributor': '代理商', 'brand_operator': '品牌运营', 'others': '其他' };
@@ -402,7 +571,7 @@ $(function () {
'<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.name_zh) + detailRow('企业类型', c.legal_form) +
detailRow('业务角色', c.business_role) + detailRow('国家/地区', c.country) +
detailRow('注册号', c.registration_number) + detailRow('地址', c.address) +
detailRow('法定代表人', c.legal_representative) + detailRow('行业', c.industry) +
@@ -509,40 +678,224 @@ $(function () {
});
}
/* ---------- 产品品类弹窗(按企业查看产品品类,分页) ---------- */
/* ---------- 产品弹窗(产品品类/基本定位/包含系列/状态/更新日期 + 新增产品) ---------- */
var prodCompanyId = 0;
window.viewProducts = function (companyId) {
prodCompanyId = companyId;
var content =
'<div style="padding:14px 18px 6px;overflow:hidden;height:330px;">' +
'<div style="padding:14px 18px 6px;overflow:hidden;height:360px;">' +
' <div class="toolbar" style="margin-bottom:10px;">' +
' <span style="font-size:13px;color:#5a6472;">共 <b id="prod-total">0</b> 条产品记录</span>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success btn-sm" id="btn-add-product">+ 新增产品</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid" style="font-size:13px;">' +
' <thead><tr><th>品类名称</th><th>品类描述</th><th>是否核心</th><th>状态</th></tr></thead>' +
' <thead><tr><th>产品品类</th><th>基本定位</th><th>包含系列</th><th>状态</th><th>更新日期</th><th>参数</th></tr></thead>' +
' <tbody id="prod-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="prod-pagination"></div>' +
'</div>';
Dialog.open({ title: '产品品类', area: ['720px', 'auto'], content: content, btn: ['关闭'] });
Dialog.open({ title: '产品列表', area: ['860px', 'auto'], content: content, btn: ['关闭'] });
loadProducts(1);
$('#btn-add-product').on('click', function () { openProductForm(null); });
};
function loadProducts(page) {
httpGet('company/products.php', { company_id: prodCompanyId, page: page, limit: 5 }).then(function (data) {
$('#prod-total').text(data.total);
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
var attrHtml = (r.attrs && r.attrs.length)
? r.attrs.map(function (a) { return '<div>' + escHtml(a.attr_name) + (a.unit ? '(' + escHtml(a.unit) + ')' : '') + ':' + escHtml(a.value) + '</div>'; }).join('')
: '<span style="color:#b6bfcc;">-</span>';
html += '<tr>' +
'<td>' + escHtml(r.category_name) + '</td>' +
'<td>' + escHtml(r.category_description || '-') + '</td>' +
'<td>' + (r.is_core ? '<span class="tag">核心</span>' : '-') + '</td>' +
'<td title="' + escHtml(r.positioning || '') + '">' + escHtml((r.positioning || '-').substring(0, 24)) + '</td>' +
'<td>' + escHtml(r.series || '-') + '</td>' +
'<td>' + (r.is_active ? '<span class="tag tag-green">在产</span>' : '<span class="tag tag-gray">停产</span>') + '</td>' +
'<td>' + fmtDate(r.updated_at || r.created_at) + '</td>' +
'<td style="font-size:12px;">' + attrHtml + '</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="4" class="empty-tip" style="padding:30px 0;">暂无产品品类</td></tr>';
html = '<tr><td colspan="6" class="empty-tip" style="padding:30px 0;">暂无产品,点击右上角「+ 新增产品」添加</td></tr>';
}
$('#prod-tbody').html(html);
renderPagination($('#prod-pagination'), data, loadProducts);
});
}
/** 新增产品弹窗(含 EAV 参数,按企业所属行业加载参数属性定义) */
function openProductForm() {
// 获取企业行业
httpGet('company/detail.php', { id: prodCompanyId }).then(function (d) {
var industry = (d.company && d.company.industry) || '';
var content =
'<form id="product-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' +
' <div class="form-item"><label>产品品类<span class="req">*</span></label><input type="text" name="category_name" placeholder="如:工业机器人、手机"></div>' +
' <div class="form-item"><label>包含系列</label><input type="text" name="series" placeholder="如:SUV、Gen 13"></div>' +
' <div class="form-item"><label>状态</label><select name="is_active"><option value="1" selected>在产</option><option value="0">停产</option></select></div>' +
' <div class="form-item"><label>是否核心</label><select name="is_core"><option value="0" selected>否</option><option value="1">是</option></select></div>' +
' <div class="form-item full"><label>产品基本定位</label><textarea name="positioning" placeholder="介绍这一款产品的商业定位"></textarea></div>' +
' <div class="form-item full"><label>品类描述</label><textarea name="category_description" placeholder="选填"></textarea></div>' +
'</div>' +
'<div id="product-attr-box" style="margin-top:6px;' + (industry ? '' : 'display:none;') + '">' +
' <b style="font-size:13px;color:#1e2a3a;">产品参数' + (industry ? '(' + escHtml(industry) + ')' : '') + '</b>' +
' <div id="product-attr-rows" style="margin-top:8px;"></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="prod-form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: '新增产品',
area: ['640px', 'auto'],
content: content,
btn: false
});
$('#prod-form-cancel').on('click', function () { Dialog.close(idx); });
// 加载该行业参数属性定义(EAV)
if (industry) {
httpGet('company/product_attrs.php', { industry: industry }).then(function (rd) {
var attrs = rd.attrs || [];
var html = '';
attrs.forEach(function (a) {
html += '<div class="form-item" style="width:calc(50% - 10px);display:inline-block;margin-right:10px;">' +
'<label>' + escHtml(a.attr_name) + (a.unit ? '(' + escHtml(a.unit) + ')' : '') + '</label>' +
'<input type="text" data-attr-id="' + a.id + '" data-attr-type="' + escHtml(a.attr_type) + '" placeholder="' +
(a.attr_type === 'boolean' ? '是/否' : a.attr_type === 'date' ? 'YYYY-MM-DD' : '') + '"></div>';
});
if (!attrs.length) {
html = '<div style="font-size:12px;color:#b6bfcc;">该行业暂无参数属性定义(可在 company_products_attr 表中维护)</div>';
}
$('#product-attr-rows').html(html);
}).catch(function () {});
}
$('#product-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select, textarea').each(function () {
var name = $(this).attr('name');
if (name) data[name] = $(this).val();
});
if (!data.category_name) { Dialog.error('产品品类为必填项'); return; }
// 收集 EAV 参数值
var attrs = [];
$('#product-attr-rows input[data-attr-id]').each(function () {
var $this = $(this);
var val = $.trim($this.val());
if (!val) return;
attrs.push({ attr_id: $this.data('attr-id'), value: val });
});
data.company_id = prodCompanyId;
data.attrs = JSON.stringify(attrs);
httpPost('company/product_add.php', data).then(function () {
Dialog.success('新增成功', function () {
Dialog.close(idx);
loadProducts(1);
});
});
});
});
}
/* ---------- 官媒弹窗(该公司媒体清单 + 新增官媒) ---------- */
var mediaCompanyId = 0;
window.viewOfficialMedia = function (companyId) {
mediaCompanyId = companyId;
var content =
'<div style="padding:14px 18px 6px;overflow:hidden;height:360px;">' +
' <div class="toolbar" style="margin-bottom:10px;">' +
' <span style="font-size:13px;color:#5a6472;">共 <b id="media-total">0</b> 条官媒记录</span>' +
' <span class="spacer"></span>' +
' <button class="btn btn-success btn-sm" id="btn-add-media">+ 新增官媒</button>' +
' </div>' +
' <div class="table-wrap"><table class="grid" style="font-size:13px;">' +
' <thead><tr><th>平台</th><th>账号名称</th><th>ID</th><th>主页链接</th><th>状态</th><th>更新日期</th></tr></thead>' +
' <tbody id="media-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="media-pagination"></div>' +
'</div>';
Dialog.open({ title: '官媒列表', area: ['820px', 'auto'], content: content, btn: ['关闭'] });
loadOfficialMedia(1);
$('#btn-add-media').on('click', function () { openOfficialMediaForm(); });
};
function loadOfficialMedia(page) {
httpGet('company/official_media_list.php', { company_id: mediaCompanyId, page: page, limit: 5 }).then(function (data) {
$('#media-total').text(data.total);
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + escHtml(r.platform || '-') + '</td>' +
'<td>' + escHtml(r.account_id || '-') + '</td>' +
'<td>' + r.id + '</td>' +
'<td>' + (r.profile_url ? '<a href="' + escHtml(r.profile_url) + '" target="_blank">打开</a>' : '-') + '</td>' +
'<td>' + (r.is_active ? '<span class="tag tag-green">启用</span>' : '<span class="tag tag-gray">停用</span>') + '</td>' +
'<td>' + fmtDate(r.updated_at || r.created_at) + '</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="6" class="empty-tip" style="padding:30px 0;">暂无官媒,点击右上角「+ 新增官媒」添加</td></tr>';
}
$('#media-tbody').html(html);
renderPagination($('#media-pagination'), data, loadOfficialMedia);
});
}
/** 新增官媒弹窗 */
function openOfficialMediaForm() {
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><input type="text" name="platform" placeholder="如:微博、微信公众号、抖音"></div>' +
' <div class="form-item"><label>账号名称<span class="req">*</span></label><input type="text" name="account_id" placeholder="账号名称/ID"></div>' +
' <div class="form-item full"><label>主页链接</label><input type="text" name="profile_url" placeholder="https://..."></div>' +
' <div class="form-item full"><label>备注</label><input type="text" name="remark"></div>' +
' <div class="form-item"><label>状态</label><select name="is_active"><option value="1" selected>启用</option><option value="0">停用</option></select></div>' +
' <div class="form-item"><label>是否常用</label><select name="is_defult"><option value="0" selected>否</option><option value="1">是</option></select></div>' +
'</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="media-form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' +
'</form>';
var idx = Dialog.open({
title: '新增官媒',
area: ['620px', 'auto'],
content: content,
btn: false
});
$('#media-form-cancel').on('click', function () { Dialog.close(idx); });
$('#media-form').on('submit', function (e) {
e.preventDefault();
var data = {};
$(this).find('input, select').each(function () {
var name = $(this).attr('name');
if (name) data[name] = $(this).val();
});
if (!data.platform || !data.account_id) { Dialog.error('平台与账号名称为必填项'); return; }
data.company_id = mediaCompanyId;
httpPost('company/official_media_add.php', data).then(function () {
Dialog.success('新增成功', function () {
Dialog.close(idx);
loadOfficialMedia(1);
});
});
});
}
});
+29 -10
View File
@@ -5,7 +5,7 @@ var BASE_URL = '/api/';
var PAGE_SIZE = 20;
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
var APP_VERSION = 'v1.0.17';
var APP_VERSION = 'v1.0.19';
/** 页脚版权/备案信息(在 config.js 中修改) */
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
@@ -38,9 +38,13 @@ var MENU_MAP = {
'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' },
'marketing': { name: '精准营销', icon: 'marketing', url: 'edm.html' },
'media_resources': { name: '媒体资源', icon: 'media', url: 'media_resources.html' },
'media_opinion': { name: '舆情监测', icon: 'media', url: 'media_opinion.html' },
'media_promo': { name: '推广服务', icon: 'media', url: 'media_promo.html' },
'competitor_analysis': { name: '竞品分析', icon: 'role', url: 'competitor_analysis.html' },
'competitor_data': { name: '竞品资料', icon: 'role', url: 'competitor_data.html' },
'channel': { name: '渠道管理', icon: 'channel', url: 'channel.html' },
'document': { name: '文件管理', icon: 'document', url: 'document.html' },
'system': { name: '权限管理', icon: 'system', url: 'user.html' },
@@ -58,16 +62,27 @@ var MENU_GROUPS = [
{
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: 'person', name: '人员数据', icon: 'person', url: 'person.html' }
]
},
{ key: 'need', name: '需求转盘', icon: 'need', url: 'need.html' },
{
key: 'marketing', name: '营销服务', icon: 'marketing', children: [
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: 'need', name: '需求转盘', icon: 'need', url: 'need.html' }
]
},
{
key: 'media_mgmt', name: '媒体管理', icon: 'media', children: [
{ key: 'media_resources', name: '媒体资源', icon: 'media', url: 'media_resources.html' },
{ key: 'media_opinion', name: '舆情监测', icon: 'media', url: 'media_opinion.html' },
{ key: 'media_promo', name: '推广服务', icon: 'media', url: 'media_promo.html' }
]
},
{
key: 'competitor', name: '竞品管理', icon: 'role', children: [
{ key: 'competitor_analysis', name: '竞品分析', icon: 'role', url: 'competitor_analysis.html' },
{ key: 'competitor_data', name: '竞品资料', icon: 'role', url: 'competitor_data.html' }
]
},
{ key: 'channel', name: '渠道管理', icon: 'channel', url: 'channel.html' },
@@ -113,9 +128,13 @@ var PERMISSION_OPTIONS = [
{ key: 'preliminary', name: '碎片处理' },
{ key: 'company', name: '企业数据' },
{ key: 'person', name: '人员数据' },
{ key: 'media', name: '媒体数据' },
{ key: 'need', name: '需求转盘' },
{ key: 'marketing', name: '营销服务' },
{ key: 'marketing', name: '精准营销' },
{ key: 'media_resources', name: '媒体资源' },
{ key: 'media_opinion', name: '舆情监测' },
{ key: 'media_promo', name: '推广服务' },
{ key: 'competitor_analysis', name: '竞品分析' },
{ key: 'competitor_data', name: '竞品资料' },
{ key: 'channel', name: '渠道管理' },
{ key: 'document', name: '文件管理' },
{ key: 'system', name: '权限管理' },
+266 -71
View File
@@ -1,5 +1,10 @@
/**
* person.js - 人员数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 导入导出 / 详情)
* person.js - 人员数据管理逻辑(列表+筛选+分页 / 新增编辑弹窗 / 人脉 / 导入导出 / 详情)
* v1.0.18:
* - 新增「人脉」入口:与本人有交集(同事/老乡/校友)的人员列表,含亲密度百分比;点姓名反查详情
* - 新增/编辑表单分两个 Tab:基本信息 | 工作履历
* - 家乡/工作所在地:省-市两级联动(不细化区县),存储为「省-市」短结构(如 湖南衡阳 / 新疆乌鲁木齐)
* - 国籍为中国/中国香港/中国澳门/中国台湾时,家乡置灰不可填写
*/
$(function () {
renderShell('人员数据', '数据管理 / 人员数据');
@@ -64,6 +69,7 @@ $(function () {
'<td class="ops">' +
' <a onclick="viewPerson(' + r.id + ')">详情</a>' +
' <a onclick="viewResume(' + r.id + ')">履历</a>' +
' <a onclick="viewConnections(' + r.id + ')">人脉</a>' +
' <a onclick="editPerson(' + r.id + ')">编辑</a>' +
'</td></tr>';
});
@@ -81,14 +87,20 @@ $(function () {
loadList(1);
});
$('#btn-add').on('click', function () { openForm(null, null, null, null, null); });
window.editPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) {
openForm(d.person, d.accounts, d.phone, d.email, d.experiences);
});
};
/* ============================================================
* 新增/编辑弹窗:Tab = 基本信息 | 工作履历
* ============================================================ */
var companyOptions = []; // 任职公司下拉选项
/** 手机号输入后自动识别归属地(录入手机号码时自动识别:国内-国家省-市;港澳台-中国香港/澳门/台湾;海外-国籍) */
function loadCompanyOptions() {
if (companyOptions.length) return $.Deferred().resolve(companyOptions).promise();
return httpGet('person/company_options.php', { limit: 500 }).then(function (d) {
companyOptions = d.list || [];
return companyOptions;
}).catch(function () { companyOptions = []; return companyOptions; });
}
/** 手机号输入后自动识别归属地(录入手机号码时自动识别:国内-省-市;港澳台-中国香港/澳门/台湾;海外-国籍) */
function bindPhoneGeo($phoneInput, $geoHint) {
var timer = null;
$phoneInput.on('blur', function () {
@@ -107,18 +119,25 @@ $(function () {
});
}
/** 省市区三级联动下拉(数据来自 date_dict_area 字典)
* value 格式:"省-市-区县"(直辖市省略市,如 "北京市-东城区") */
/** 名称去行政区后缀:湖南省->湖南,新疆维吾尔自治区->新疆,北京市->北京,石家庄市->石家庄 */
function stripAreaSuffix(name) {
return String(name || '')
.replace(/(维吾尔|壮族|回族)?自治区$/, '')
.replace(/省$/, '')
.replace(/特别行政区$/, '')
.replace(/市$/, '');
}
/** 省-市两级联动下拉(不细化区县;值存储为短结构,如 湖南衡阳 / 新疆乌鲁木齐 / 北京) */
function areaSelectsHtml(target, value) {
return '<div class="area-cascade" data-target="' + target + '" style="display:flex;gap:6px;">' +
'<select class="area-prov" style="flex:1;min-width:0;"><option value="">省</option></select>' +
'<select class="area-city" style="flex:1;min-width:0;"><option value="">市</option></select>' +
'<select class="area-dist" style="flex:1;min-width:0;"><option value="">区/县</option></select>' +
'</div>' +
'<input type="hidden" name="' + target + '" value="' + escHtml(value || '') + '">';
}
/** 构建省/市/区三级联动:填充选项并按已有值回选 */
/** 构建省/市两级联动:填充选项并按已有值回选(兼容旧格式:湖南衡阳 / 湖南省衡阳市 / 北京市-东城区) */
function initAreaCascade(areas) {
var provMap = {}; // code -> name
var childMap = {}; // parent_code -> [{code,name}]
@@ -144,69 +163,77 @@ $(function () {
html += '<option value="' + c.code + '">' + escHtml(c.name) + '</option>';
});
$box.find('.area-city').html(html);
$box.find('.area-dist').html('<option value="">区/县</option>');
}
function fillDist($box, cityCode) {
var html = '<option value="">区/县</option>';
(childMap[cityCode] || []).forEach(function (c) {
html += '<option value="' + c.code + '">' + escHtml(c.name) + '</option>';
});
$box.find('.area-dist').html(html);
}
function combine($box) {
var target = $box.data('target');
var provName = $box.find('.area-prov option:selected').text();
var cityName = $box.find('.area-city option:selected').text();
var distName = $box.find('.area-dist option:selected').text();
var parts = [];
if ($box.find('.area-prov').val()) parts.push(provName);
if ($box.find('.area-city').val() && cityName !== provName) parts.push(cityName);
if ($box.find('.area-dist').val()) parts.push(distName);
$box.closest('.form-item').find('input[name="' + target + '"]').val(parts.join('-'));
var provShort = provName ? stripAreaSuffix(provName) : '';
var cityShort = cityName ? stripAreaSuffix(cityName) : '';
var val = provShort;
// 直辖市(省/市同名)只存省
if (cityShort && cityShort !== provShort) val += cityShort;
$box.closest('.form-item').find('input[name="' + target + '"]').val(val);
}
/** 按短名匹配省(如 湖南 / 新疆 / 北京) */
function matchProv(shortName) {
var sorted = provCodes.slice().sort(function (a, b) { return b.length - a.length; });
for (var i = 0; i < sorted.length; i++) {
if (stripAreaSuffix(provMap[sorted[i]]) === shortName) return sorted[i];
}
return '';
}
/** 按短名匹配市 */
function matchCity(provCode, shortName) {
var list = childMap[provCode] || [];
for (var i = 0; i < list.length; i++) {
if (stripAreaSuffix(list[i].name) === shortName) return list[i].code;
}
return '';
}
/** 解析已有值 -> [省短名, 市短名](兼容 湖南衡阳 / 湖南省衡阳市 / 北京市-东城区 等格式) */
function parseValue(value) {
var v = String(value || '').trim();
if (!v) return ['', ''];
if (v.indexOf('-') >= 0) {
var parts = v.split('-');
return [stripAreaSuffix(parts[0]), parts[1] ? stripAreaSuffix(parts[1]) : ''];
}
// 无分隔符:最长省名前缀匹配
var sorted = provCodes.slice().sort(function (a, b) { return b.length - a.length; });
for (var i = 0; i < sorted.length; i++) {
var pShort = stripAreaSuffix(provMap[sorted[i]]);
if (pShort && v.indexOf(pShort) === 0) {
var rest = v.substring(pShort.length);
return [pShort, rest];
}
}
return ['', v];
}
$('.area-cascade').each(function () {
var $box = $(this);
var target = $box.data('target');
var current = $box.closest('.form-item').find('input[name="' + target + '"]').val() || '';
var parts = current.split('-');
var provName = parts[0] || '';
var cityName = parts[1] || '';
var distName = parts[2] || '';
var parsed = parseValue(current);
var provShort = parsed[0];
var cityShort = parsed[1];
fillProv($box);
// 回选省
var provCode = '';
provCodes.forEach(function (code) {
if (provMap[code] === provName) {
provCode = code;
$box.find('.area-prov').val(code);
}
});
var provCode = provShort ? matchProv(provShort) : '';
if (!provCode) return;
$box.find('.area-prov').val(provCode);
fillCity($box, provCode);
var cityList = childMap[provCode] || [];
var cityCode = '';
// 直接按市名匹配;直辖市(省/市同名,如"北京市-东城区")时 parts[1] 实为区县
var direct = cityList.filter(function (c) { return c.name === cityName; });
if (direct.length) {
cityCode = direct[0].code;
$box.find('.area-city').val(cityCode);
} else if (cityList.length === 1 && cityList[0].name === provName) {
cityCode = cityList[0].code;
$box.find('.area-city').val(cityCode);
if (parts[1] && !distName) {
distName = parts[1]; // 直辖市:第二段实为区县
}
}
if (!cityCode) return;
fillDist($box, cityCode);
(childMap[cityCode] || []).forEach(function (c) {
if (c.name === distName) $box.find('.area-dist').val(c.code);
});
// 直辖市:市=省本身
if (cityShort === provShort) {
var muniCode = matchCity(provCode, cityShort);
if (muniCode) $box.find('.area-city').val(muniCode);
return;
}
var cityCode = cityShort ? matchCity(provCode, cityShort) : '';
if (cityCode) $box.find('.area-city').val(cityCode);
});
// 联动事件
@@ -216,22 +243,44 @@ $(function () {
combine($box);
});
$('.area-cascade').off('change.areaCity').on('change.areaCity', '.area-city', function () {
var $box = $(this).closest('.area-cascade');
fillDist($box, $(this).val());
combine($box);
});
$('.area-cascade').off('change.areaDist').on('change.areaDist', '.area-dist', function () {
combine($(this).closest('.area-cascade'));
});
}
/** 国籍是否为中国大陆/港澳台(此时家乡置灰不可填) */
function isCnNationality(n) {
var v = String(n || '').trim();
return v === '中国' || v === '中国香港' || v === '中国澳门' || v === '中国台湾';
}
/** 工作履历行模板 */
function expRowHtml(e) {
e = e || {};
var empty = function (v) { return (v === null || v === undefined) ? '' : v; };
var opts = '<option value="">请选择公司</option>';
companyOptions.forEach(function (c) {
opts += '<option value="' + c.id + '"' + (String(c.id) === String(e.company_id) ? ' selected' : '') + '>' + escHtml(c.name) + '</option>';
});
return '<div class="exp-row">' +
'<select name="exp-company" style="width:180px;">' + opts + '</select>' +
'<input type="text" name="exp-position" placeholder="职位" value="' + escHtml(empty(e.position)) + '">' +
'<input type="text" name="exp-department" placeholder="部门" value="' + escHtml(empty(e.department)) + '">' +
'<input type="text" name="exp-level" placeholder="职级" value="' + escHtml(empty(e.job_level)) + '">' +
'<input type="date" name="exp-start" title="入职日期" value="' + escHtml(empty(e.start_date)) + '">' +
'<input type="date" name="exp-end" title="离职日期" value="' + escHtml(empty(e.end_date)) + '">' +
'<label title="当前在职" style="font-size:12px;color:#5a6472;white-space:nowrap;"><input type="checkbox" name="exp-current"' + (e.is_current ? ' checked' : '') + '> 在职</label>' +
'<button type="button" class="btn btn-sm btn-danger" onclick="$(this).closest(\'.exp-row\').remove()">删除</button>' +
'</div>';
}
function openForm(person, accounts, phone, email, experiences) {
var isEdit = !!person;
var p = person || {};
var pPhone = phone || '';
var pEmail = email || '';
var content =
'<form id="person-form" style="padding:18px 22px 4px;">' +
// Tab1 基本信息
var baseHtml =
'<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">' +
@@ -239,28 +288,55 @@ $(function () {
' <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) + '" list="person-country-list" placeholder="选择或输入">' +
' <div class="form-item"><label>国籍</label><input type="text" name="nationality" id="person-nationality" value="' + escHtml(p.nationality) + '" list="person-country-list" placeholder="选择或输入">' +
' <datalist id="person-country-list"></datalist></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>' + areaSelectsHtml('hometown', p.hometown) + '</div>' +
' <div class="form-item"><label>工作所在地</label>' + areaSelectsHtml('work_location', p.work_location) + '</div>' +
' <div class="form-item"><label>家乡</label><div class="hometown-box">' + areaSelectsHtml('hometown', p.hometown) + '</div></div>' +
' <div class="form-item"><label>工作所在地</label><div class="workloc-box">' + areaSelectsHtml('work_location', p.work_location) + '</div></div>' +
' <div class="form-item"><label>手机</label>' +
' <div><input type="text" name="phone" id="person-phone" value="' + escHtml(pPhone) + '" placeholder="常用手机号">' +
' <div class="geo-hint" id="person-phone-geo" style="font-size:12px;color:#2f8f46;margin-top:4px;display:none;"></div></div></div>' +
' <div class="form-item"><label>邮箱</label><input type="text" name="email" value="' + escHtml(pEmail) + '" placeholder="常用邮箱"></div>' +
' <div class="form-item"><label>来源渠道</label><select name="source_channel" id="person-source-channel"><option value="">-</option></select></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(p.source_detail) + '" placeholder="如:华南工博会"></div>' +
'</div>';
// Tab2 工作履历
var expRowsHtml = '';
if (!experiences || !experiences.length) {
expRowsHtml = expRowHtml(null);
} else {
experiences.forEach(function (e) { expRowsHtml += expRowHtml(e); });
}
var expHtml =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
' <b style="font-size:13px;color:#1e2a3a;">工作履历</b>' +
' <button type="button" class="btn btn-sm" id="add-exp-row">+ 添加履历</button>' +
'</div>' +
'<div id="exp-rows">' + expRowsHtml + '</div>';
var content =
'<form id="person-form" style="padding:14px 22px 4px;">' +
' <div class="edit-tabs">' +
' <div class="edit-tabs-head">' +
' <span class="edit-tab active" data-tab="base" onclick="switchPersonTab(this)">基本信息</span>' +
' <span class="edit-tab" data-tab="exp" onclick="switchPersonTab(this)">工作履历</span>' +
' </div>' +
' <div class="edit-tabs-body">' +
' <div class="edit-pane" id="edit-pane-base">' + baseHtml + '</div>' +
' <div class="edit-pane" id="edit-pane-exp" style="display:none;">' + expHtml + '</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: ['720px', 'auto'],
area: ['760px', 'auto'],
content: content,
btn: false
});
@@ -268,13 +344,13 @@ $(function () {
// 手机号归属地自动识别
bindPhoneGeo($('#person-phone'), $('#person-phone-geo'));
// 加载国籍字典 + 行政区划字典(三级联动) + 来源渠道字典
// 加载国籍字典 + 行政区划字典(省-市两级) + 来源渠道字典 + 任职公司
httpGet('common/dicts.php', { type: 'all' }).then(function (d) {
var $dl = $('#person-country-list');
var html = '';
(d.countries || []).forEach(function (c) { html += '<option value="' + escHtml(c.name) + '"></option>'; });
$dl.html(html);
// 省市区三级联动
// 省-市两级联动
initAreaCascade(d.areas || []);
var $sc = $('#person-source-channel');
var scHtml = '<option value="">-</option>';
@@ -288,8 +364,39 @@ $(function () {
scHtml += '<option value="' + escHtml(p.source_channel) + '" selected>' + escHtml(p.source_channel) + '(自定义)</option>';
}
$sc.html(scHtml);
// 国籍变化 -> 家乡置灰/可用
function syncHometownDisabled() {
var n = $('#person-nationality').val();
var disabled = isCnNationality(n);
var $box = $('.hometown-box');
$box.find('select, input').prop('disabled', disabled);
$box.toggleClass('area-disabled', disabled);
if (disabled) {
$box.closest('.form-item').find('input[name="hometown"]').val('');
}
}
$('#person-nationality').off('input.home').on('input.home', syncHometownDisabled);
syncHometownDisabled();
}).catch(function () {});
// 任职公司选项
loadCompanyOptions().then(function () {
// 已有行重填公司下拉(若首次加载后追加的选项)
$('#exp-rows .exp-row select[name="exp-company"]').each(function () {
if ($(this).find('option').length <= 1) {
var cur = $(this).data('cur') || '';
var opts = '<option value="">请选择公司</option>';
companyOptions.forEach(function (c) {
opts += '<option value="' + c.id + '"' + (String(c.id) === String(cur) ? ' selected' : '') + '>' + escHtml(c.name) + '</option>';
});
$(this).html(opts);
}
});
});
$('#add-exp-row').on('click', function () { $('#exp-rows').append(expRowHtml(null)); });
$('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#person-form').on('submit', function (e) {
@@ -313,6 +420,25 @@ $(function () {
data.contacts = JSON.stringify(contacts);
delete data.phone;
delete data.email;
// 工作履历(整表替换)
var exps = [];
$('#exp-rows .exp-row').each(function () {
var $r = $(this);
var companyId = $r.find('[name="exp-company"]').val();
if (!companyId) return;
exps.push({
company_id: companyId,
position: $r.find('[name="exp-position"]').val(),
department: $r.find('[name="exp-department"]').val(),
job_level: $r.find('[name="exp-level"]').val(),
start_date: $r.find('[name="exp-start"]').val(),
end_date: $r.find('[name="exp-end"]').val(),
is_current: $r.find('[name="exp-current"]').prop('checked') ? 1 : 0
});
});
data.experiences = JSON.stringify(exps);
if (isEdit) data.id = p.id;
httpPost(isEdit ? 'person/update.php' : 'person/add.php', data).then(function () {
@@ -324,6 +450,30 @@ $(function () {
});
}
/* ---------- 新增 / 编辑入口 ---------- */
window.addPerson = function () {
openForm(null, [], '', '', []);
};
window.editPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) {
openForm(d.person, d.accounts || [], d.phone || '', d.email || '', d.experiences || []);
}).catch(function () {
Dialog.alert('加载人员详情失败');
});
};
$('#btn-add').on('click', function () { addPerson(); });
/* ---------- 人员弹窗 Tab 切换 ---------- */
window.switchPersonTab = function (el) {
var $tabs = $(el).closest('.edit-tabs');
$tabs.find('.edit-tab').removeClass('active');
$(el).addClass('active');
$tabs.find('.edit-pane').hide();
$('#edit-pane-' + $(el).data('tab')).show();
};
/* ---------- 导入 ---------- */
$('#btn-import').on('click', function () {
triggerFileInput('.csv', function (file) {
@@ -383,6 +533,51 @@ $(function () {
});
}
/* ---------- 人脉弹窗(同事/老乡/校友 + 亲密度) ---------- */
var connPersonId = 0;
window.viewConnections = function (personId) {
connPersonId = personId;
var content =
'<div style="padding:14px 18px 6px;overflow:hidden;height:420px;">' +
' <div style="margin-bottom:10px;font-size:13px;color:#5a6472;">共 <b id="conn-total">0</b> 条人脉(交集:工作履历/家乡/毕业院校)</div>' +
' <div class="table-wrap"><table class="grid" style="font-size:13px;">' +
' <thead><tr><th>姓名</th><th>联系方式</th><th>交集</th><th>亲密度</th></tr></thead>' +
' <tbody id="conn-tbody"></tbody>' +
' </table></div>' +
'</div>';
Dialog.open({ title: '人脉', area: ['760px', 'auto'], content: content, btn: ['关闭'] });
loadConnections();
};
function loadConnections() {
httpGet('person/connections.php', { person_id: connPersonId, limit: 50 }).then(function (data) {
var rows = data.list || [];
$('#conn-total').text(rows.length);
var html = '';
rows.forEach(function (r) {
var tagHtml = (r.tags || []).map(function (t) {
return '<span class="tag">' + escHtml(t) + '</span>';
}).join(' ');
html += '<tr>' +
'<td><a href="javascript:void(0)" class="link" onclick="viewPerson(' + r.id + ')">' + escHtml(r.full_name) + '</a></td>' +
'<td>' + escHtml(r.phone || '-') + '</td>' +
'<td>' + (tagHtml || '-') + '</td>' +
'<td style="white-space:nowrap;">' +
' <span style="color:#2a5298;font-weight:bold;">' + r.intimacy + '%</span>' +
' <span style="display:inline-block;width:70px;height:6px;background:#eef0f4;border-radius:3px;vertical-align:middle;margin-left:6px;overflow:hidden;">' +
' <span style="display:block;height:100%;width:' + r.intimacy + '%;background:#4fa3ff;border-radius:3px;"></span>' +
' </span>' +
'</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="4" class="empty-tip" style="padding:40px 0;">暂无相关人脉</td></tr>';
}
$('#conn-tbody').html(html);
});
}
/* ---------- 任职履历弹窗(按人员查看任职公司/履历,分页) ---------- */
var compPersonId = 0;
+20
View File
@@ -0,0 +1,20 @@
/**
* placeholder.js - 占位页面通用逻辑(媒体管理/竞品管理等未开发模块)
* 用法:renderPlaceholder('media_resources'); —— key 需在 MENU_MAP 中存在
*/
function renderPlaceholder(key) {
var m = MENU_MAP[key] || { name: key, icon: 'document' };
renderShell(m.name, '业务模块 / ' + m.name);
var html =
'<div class="card" style="padding:60px 20px;text-align:center;">' +
' <div style="width:64px;height:64px;margin:0 auto 16px;color:#c3cbd8;">' + (ICONS[m.icon] || '') + '</div>' +
' <div style="font-size:18px;color:#1e2a3a;font-weight:600;">' + escHtml(m.name) + '</div>' +
' <div style="font-size:13px;color:#8a94a6;margin-top:8px;">该模块正在建设中,敬请期待</div>' +
'</div>';
$('#page-content').html(html);
initPage(key, function (user) {
if (!checkPagePermission(key)) return;
});
}