feat: 重构运营看板,新增多维度数据展示与交互功能

新增同城便民生活服务平台标识,重构看板为业务总览与运营看板
新增待办督办中心、营收构成分析、热门分类与区域排行等模块
新增近7/14/30天多时段趋势切换,支持发布量/订单额/用户/曝光量指标切换
新增平台商业与生态资产统计模块,新增待办预警与空状态提示
新增趋势数据汇总与明细表联动,优化响应式布局与交互样式
新增商家管理、举报投诉等语言翻译项,新增登录推荐图
这个提交包含在:
2026-10-30 17:39:15 +08:00
父节点 d812e54f8e
当前提交 5b616634ab
共有 7 个文件被更改,包括 2723 次插入 和 802 次删除

查看文件

@@ -6,124 +6,334 @@ use app\common\controller\Backend;
use think\Db;
/**
* 同城-数据总览
* 同城分类信息 - 数据总览与运营看板
*
* 后台核心看板:今日/累计发布量、待审核数、今日/累计订单额、会员数、商家数、
* 分销佣金(待结算/已结算)、提现待审数,以及近 14 天趋势(发布量 / 订单额 / 新增注册用户)。
* 数据一律由本控制器直接查库(fa_fc_* / fa_user),不在视图里写 SQL,也不走 /api/v1/admin/**。
* 后台核心运营仪表盘:
* 1. 今日核心指标与昨日对比(发布量 / 订单额 / 新增用户 / 浏览曝光量)
* 2. 待办督办中心(帖子待审 / 商家待审 / 提现待审 / 举报待处理)
* 3. 多时段多维趋势分析(近 7 天 / 14 天 / 30 天,支持发布量、订单额、新增用户、浏览量平滑折线图)
* 4. 营收来源构成分析(会员购买 / 发帖付费 / 置顶推广 / 付费查看等占比环形图)
* 5. 热门分类发帖分布与同城区域活跃排行
* 6. 用户、会员、商户与分销生态资产全景
*
* @icon fa fa-dashboard
*/
class Dashboard extends Backend
{
/** 趋势统计天数 */
const TREND_DAYS = 14;
/**
* 查看
* 数据总览首页入口与数据下发
*
* @return string|\think\response\Json
*/
public function index()
{
$todayStart = strtotime('today');
$todayEnd = $todayStart + 86400 - 1;
$yesterdayStart = $todayStart - 86400;
$yesterdayEnd = $todayStart - 1;
/* =========================================================
* 1. 核心 KPI(今日、较昨日对比、累计总量)
* ========================================================= */
// 发布量
$todayPost = (int)Db::name('fc_post')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayPost = (int)Db::name('fc_post')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalPost = (int)Db::name('fc_post')->count();
// 订单额与笔数(仅统计已支付订单)
$todayOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$todayStart, $todayEnd])->sum('amount'), 2);
$yesterdayOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$yesterdayStart, $yesterdayEnd])->sum('amount'), 2);
$totalOrderAmount = round((float)Db::name('fc_order')->where('status', 'paid')->sum('amount'), 2);
$todayOrderCount = (int)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$todayStart, $todayEnd])->count();
$totalOrderCount = (int)Db::name('fc_order')->where('status', 'paid')->count();
// 用户与会员
$todayUser = (int)Db::name('user')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayUser = (int)Db::name('user')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalUser = (int)Db::name('user')->count();
$memberCount = (int)Db::name('fc_user_ext')->where('is_member', 1)->count();
$merchantCount = (int)Db::name('fc_merchant')->count();
$recommendMerchantCount = (int)Db::name('fc_merchant')->where('recommend', 1)->count();
// 浏览量 PV
$todayViews = (int)Db::name('fc_view_log')->where('createtime', 'between', [$todayStart, $todayEnd])->count();
$yesterdayViews = (int)Db::name('fc_view_log')->where('createtime', 'between', [$yesterdayStart, $yesterdayEnd])->count();
$totalViews = (int)Db::name('fc_post')->sum('views');
/* ---------- 核心指标 ---------- */
$metrics = [
// 发布量
'todayPost' => (int)Db::name('fc_post')->where('createtime', 'between', [$todayStart, $todayEnd])->count(),
'totalPost' => (int)Db::name('fc_post')->count(),
'pendingPost' => (int)Db::name('fc_post')->where('audit_status', 0)->count(),
// 订单额(仅统计已支付订单;paytime 缺失时回落 createtime)
'todayOrderAmount' => round((float)Db::name('fc_order')->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$todayStart, $todayEnd])->sum('amount'), 2),
'totalOrderAmount' => round((float)Db::name('fc_order')->where('status', 'paid')->sum('amount'), 2),
// 用户 / 商家
'memberCount' => (int)Db::name('fc_user_ext')->where('is_member', 1)->count(),
'merchantCount' => (int)Db::name('fc_merchant')->count(),
'userCount' => (int)Db::name('user')->count(),
// 分销佣金
'commissionPending' => round((float)Db::name('fc_commission')->where('status', 0)->sum('amount'), 2),
'commissionSettled' => round((float)Db::name('fc_commission')->where('status', 1)->sum('amount'), 2),
// 提现待审核
'withdrawAuditing' => (int)Db::name('fc_withdraw')->where('status', 'auditing')->count(),
'todayPost' => $todayPost,
'yesterdayPost' => $yesterdayPost,
'postDelta' => $todayPost - $yesterdayPost,
'totalPost' => $totalPost,
'todayOrderAmount' => $todayOrderAmount,
'yesterdayOrderAmount' => $yesterdayOrderAmount,
'orderDelta' => round($todayOrderAmount - $yesterdayOrderAmount, 2),
'totalOrderAmount' => $totalOrderAmount,
'todayOrderCount' => $todayOrderCount,
'totalOrderCount' => $totalOrderCount,
'todayUser' => $todayUser,
'yesterdayUser' => $yesterdayUser,
'userDelta' => $todayUser - $yesterdayUser,
'totalUser' => $totalUser,
'memberCount' => $memberCount,
'merchantCount' => $merchantCount,
'recommendMerchant' => $recommendMerchantCount,
'todayViews' => $todayViews,
'yesterdayViews' => $yesterdayViews,
'viewsDelta' => $todayViews - $yesterdayViews,
'totalViews' => $totalViews,
];
/* ---------- 近 14 天趋势 ---------- */
$dateStart = $todayStart - (self::TREND_DAYS - 1) * 86400;
$days = [];
for ($i = 0; $i < self::TREND_DAYS; $i++) {
$days[] = date('Y-m-d', $dateStart + $i * 86400);
}
/* =========================================================
* 2. 待办督办中心(待审核事项与预警)
* ========================================================= */
$pendingPost = (int)Db::name('fc_post')->where('audit_status', 0)->count();
$pendingMerchant = (int)Db::name('fc_merchant')->where('audit_status', 0)->count();
$pendingWithdraw = (int)Db::name('fc_withdraw')->where('status', 'auditing')->count();
$pendingWithdrawAmount = round((float)Db::name('fc_withdraw')->where('status', 'auditing')->sum('amount'), 2);
$pendingReport = (int)Db::name('fc_report')->where('status', 'pending')->count();
$pendingTotal = $pendingPost + $pendingMerchant + $pendingWithdraw + $pendingReport;
// 每天发布量
$postsMap = [];
$posts = Db::name('fc_post')
->where('createtime', 'between time', [$dateStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($posts as $row) {
$postsMap[$row['day']] = (int)$row['num'];
}
$pending = [
'total' => $pendingTotal,
'post' => $pendingPost,
'merchant' => $pendingMerchant,
'withdraw' => $pendingWithdraw,
'withdrawAmount' => $pendingWithdrawAmount,
'report' => $pendingReport,
];
// 每天订单额(已支付;paytime 为空时按 createtime 归属)
$ordersMap = [];
$orders = Db::name('fc_order')
/* =========================================================
* 3. 平台生态与分销资产
* ========================================================= */
$commissionPending = round((float)Db::name('fc_commission')->where('status', 0)->sum('amount'), 2);
$commissionSettled = round((float)Db::name('fc_commission')->where('status', 1)->sum('amount'), 2);
$distributorCount = (int)Db::name('fc_dist_relation')->distinct(true)->field('parent_id')->count();
$ecosystem = [
'commissionPending' => $commissionPending,
'commissionSettled' => $commissionSettled,
'distributorCount' => $distributorCount,
'memberRate' => $totalUser > 0 ? round(($memberCount / $totalUser) * 100, 1) : 0,
];
/* =========================================================
* 4. 营收构成分析(按订单类型统计已支付实收)
* ========================================================= */
$orderTypeLabels = [
'member' => 'VIP会员购买',
'post' => '发布信息付费',
'payview' => '付费查看电话',
'recharge' => '钱包充值',
'top' => '置顶推广套餐',
'refresh' => '信息刷新',
];
$rawRevenue = Db::name('fc_order')
->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$dateStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(IFNULL(`paytime`,`createtime`),'%Y-%m-%d') AS day, SUM(`amount`) AS num")
->group('day')
->fieldRaw("type, count(*) as count, sum(amount) as amount")
->group('type')
->order('amount', 'desc')
->select();
foreach ($orders as $row) {
$ordersMap[$row['day']] = round((float)$row['num'], 2);
}
// 每天新增注册用户(fa_user.createtime)
$membersMap = [];
$members = Db::name('user')
->where('createtime', 'between time', [$dateStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($members as $row) {
$membersMap[$row['day']] = (int)$row['num'];
}
/* ---------- 今日较昨日的变化(用于首页涨跌标识) ---------- */
$yesterday = date('Y-m-d', $todayStart - 86400);
$metrics['yesterdayPost'] = isset($postsMap[$yesterday]) ? $postsMap[$yesterday] : 0;
$metrics['yesterdayOrder'] = isset($ordersMap[$yesterday]) ? $ordersMap[$yesterday] : 0;
$metrics['postDelta'] = $metrics['todayPost'] - $metrics['yesterdayPost'];
$metrics['orderDelta'] = round($metrics['todayOrderAmount'] - $metrics['yesterdayOrder'], 2);
$trend = [
'dates' => $days,
'posts' => [],
'orders' => [],
'members' => [],
];
$trendRows = [];
foreach ($days as $day) {
$postNum = isset($postsMap[$day]) ? $postsMap[$day] : 0;
$orderNum = isset($ordersMap[$day]) ? $ordersMap[$day] : 0;
$memberNum = isset($membersMap[$day]) ? $membersMap[$day] : 0;
$trend['posts'][] = $postNum;
$trend['orders'][] = $orderNum;
$trend['members'][] = $memberNum;
$trendRows[] = [
'date' => $day,
'posts' => $postNum,
'orders' => $orderNum,
'members' => $memberNum,
$revenueTotalSum = 0;
$revenueTypes = [];
foreach ($rawRevenue as $r) {
$amt = round((float)$r['amount'], 2);
$cnt = (int)$r['count'];
$label = isset($orderTypeLabels[$r['type']]) ? $orderTypeLabels[$r['type']] : ($r['type'] ? $r['type'] : '其他服务');
$revenueTotalSum += $amt;
$revenueTypes[] = [
'type' => $r['type'],
'label' => $label,
'count' => $cnt,
'amount' => $amt,
];
}
foreach ($revenueTypes as &$item) {
$item['percent'] = $revenueTotalSum > 0 ? round(($item['amount'] / $revenueTotalSum) * 100, 1) : 0;
}
unset($item);
/* =========================================================
* 5. 热门分类发帖分布与同城区域排行
* ========================================================= */
// 一级分类发帖分布
$rawCategories = Db::name('fc_category')
->alias('c')
->join('__FC_POST__ p', '(p.category_id = c.id OR p.category_id IN (SELECT sub.id FROM ' . config('database.prefix') . 'fc_category sub WHERE sub.pid = c.id))', 'LEFT')
->where('c.pid', 0)
->field('c.id, c.name, count(p.id) as post_count')
->group('c.id')
->order('post_count', 'desc')
->select();
$categoryTotal = 0;
$categoryRanks = [];
foreach ($rawCategories as $c) {
$cnt = (int)$c['post_count'];
$categoryTotal += $cnt;
$categoryRanks[] = [
'id' => $c['id'],
'name' => $c['name'],
'count' => $cnt,
];
}
foreach ($categoryRanks as &$cItem) {
$cItem['percent'] = $categoryTotal > 0 ? round(($cItem['count'] / $categoryTotal) * 100, 1) : 0;
}
unset($cItem);
// 同城热门地区 Top 5
$rawAreas = Db::name('fc_post')
->fieldRaw("IF(area != '', area, city) as region_name, count(*) as count")
->where("area != '' OR city != ''")
->group('region_name')
->order('count', 'desc')
->limit(5)
->select();
$areaRanks = [];
$areaMaxCount = 1;
foreach ($rawAreas as $ar) {
$cnt = (int)$ar['count'];
if ($cnt > $areaMaxCount) {
$areaMaxCount = $cnt;
}
$areaRanks[] = [
'region_name' => $ar['region_name'] ? $ar['region_name'] : '未知区域',
'count' => $cnt,
];
}
foreach ($areaRanks as &$arItem) {
$arItem['percent'] = round(($arItem['count'] / $areaMaxCount) * 100, 1);
}
unset($arItem);
/* =========================================================
* 6. 多时段趋势数据计算(近 7 天 / 14 天 / 30 天)
* ========================================================= */
$maxTrendDays = 30;
$trendStart = $todayStart - ($maxTrendDays - 1) * 86400;
// 发帖趋势
$postsMap = [];
$postsQuery = Db::name('fc_post')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($postsQuery as $row) {
$postsMap[$row['day']] = (int)$row['num'];
}
// 订单额趋势
$ordersMap = [];
$ordersQuery = Db::name('fc_order')
->where('status', 'paid')
->whereTime('paytime|createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(IFNULL(`paytime`,`createtime`),'%Y-%m-%d') AS day, SUM(`amount`) AS num")
->group('day')
->select();
foreach ($ordersQuery as $row) {
$ordersMap[$row['day']] = round((float)$row['num'], 2);
}
// 新增用户趋势
$usersMap = [];
$usersQuery = Db::name('user')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($usersQuery as $row) {
$usersMap[$row['day']] = (int)$row['num'];
}
// 浏览量趋势
$viewsMap = [];
$viewsQuery = Db::name('fc_view_log')
->where('createtime', 'between', [$trendStart, $todayEnd])
->fieldRaw("FROM_UNIXTIME(`createtime`,'%Y-%m-%d') AS day, COUNT(*) AS num")
->group('day')
->select();
foreach ($viewsQuery as $row) {
$viewsMap[$row['day']] = (int)$row['num'];
}
// 计算 7/14/30 天数据包
$ranges = [7, 14, 30];
$allTrends = [];
foreach ($ranges as $span) {
$startTs = $todayStart - ($span - 1) * 86400;
$dates = [];
$posts = [];
$orders = [];
$users = [];
$views = [];
$rows = [];
for ($i = 0; $i < $span; $i++) {
$day = date('Y-m-d', $startTs + $i * 86400);
$p = isset($postsMap[$day]) ? $postsMap[$day] : 0;
$o = isset($ordersMap[$day]) ? $ordersMap[$day] : 0;
$u = isset($usersMap[$day]) ? $usersMap[$day] : 0;
$v = isset($viewsMap[$day]) ? $viewsMap[$day] : 0;
$dates[] = $day;
$posts[] = $p;
$orders[] = $o;
$users[] = $u;
$views[] = $v;
$rows[] = [
'date' => $day,
'posts' => $p,
'orders' => $o,
'users' => $u,
'views' => $v,
];
}
$allTrends[$span] = [
'dates' => $dates,
'posts' => $posts,
'orders' => $orders,
'users' => $users,
'views' => $views,
'rows' => array_reverse($rows),
];
}
// 如果是 AJAX 请求局部刷新趋势,可直接返回对应时段数据
if ($this->request->isAjax() && $this->request->get('action') === 'trend') {
$days = (int)$this->request->get('days', 14);
$trendData = isset($allTrends[$days]) ? $allTrends[$days] : $allTrends[14];
return json(['code' => 1, 'msg' => 'ok', 'data' => $trendData]);
}
// 默认 14 天用于模板首屏初始化
$defaultTrend = $allTrends[14];
/* =========================================================
* 7. 视图变量绑定与前端配置传递
* ========================================================= */
$this->view->assign('metrics', $metrics);
$this->view->assign('trendRows', $trendRows);
$this->view->assign('pending', $pending);
$this->view->assign('ecosystem', $ecosystem);
$this->view->assign('revenueTypes', $revenueTypes);
$this->view->assign('categoryRanks', $categoryRanks);
$this->view->assign('areaRanks', $areaRanks);
$this->view->assign('trendRows', $defaultTrend['rows']);
$this->view->assign('updatedAt', date('Y-m-d H:i'));
$this->assignconfig('trend', $trend);
// 向前端 RequireJS 传入完整时段数据包及分类分布,供平滑渲染交互
$this->assignconfig('allTrends', $allTrends);
$this->assignconfig('revenueTypes', $revenueTypes);
$this->assignconfig('categoryRanks', $categoryRanks);
return $this->view->fetch();
}

查看文件

@@ -2,49 +2,92 @@
return [
'核心看板' => '核心看板',
'全部指标由后台直接统计;订单额仅统计「已支付」订单,佣金不含已驳回部分,提现为「待审核」笔数。'
=> '全部指标由后台直接统计;订单额仅统计「已支付」订单,佣金不含已驳回部分,提现为「待审核」笔数。',
'业务总览' => '业务总览',
'业务总览与运营看板' => '业务总览与运营看板',
'同城便民生活服务平台' => '同城便民生活服务平台',
'数据更新于' => '数据更新于',
'订单额仅统计已支付订单' => '订单额仅统计已支付订单',
'订单额仅统计已支付实收口径' => '订单额仅统计已支付实收口径',
'内容管理' => '内容管理',
'商家管理' => '商家管理',
'提现审核' => '提现审核',
'刷新数据' => '刷新数据',
'举报投诉' => '举报投诉',
'刷新看板' => '刷新看板',
'刷新' => '刷新',
'运营待办' => '运营待办',
'当前有' => '当前有',
'项待处理事项需要审核跟进:' => '项待处理事项需要审核跟进:',
'待审核信息' => '待审核信息',
'信息待审核' => '信息待审核',
'商家入驻待审' => '商家入驻待审',
'提现申请待审' => '提现申请待审',
'提现待审核' => '提现待审核',
'共' => '共',
'违规举报待处理' => '违规举报待处理',
'举报待处理' => '举报待处理',
'当前所有待审核帖子、商家、提现与举报均已处理完毕,平台健康运行中'
=> '当前所有待审核帖子、商家、提现与举报均已处理完毕,平台健康运行中',
'今日核心指标' => '今日核心指标',
'较昨日' => '较昨日',
'与昨日持平' => '与昨日持平',
'去审核' => '去审核',
'去处理' => '去处理',
'暂无待审核' => '暂无待审核',
'今日发布量' => '今日发布量',
'累计发布量' => '累计发布量',
'待审核信息数' => '待审核信息数',
'今日订单额' => '今日订单额',
'累计订单额' => '累计订单额',
'会员数' => '会员数',
'商家数' => '商家数',
'注册用户数' => '注册用户数',
'平台累计' => '平台累计',
'分销佣金' => '分销佣金',
'待结算佣金' => '待结算佣金',
'已结算佣金' => '已结算佣金',
'待审核提现数' => '待审核提现数',
'近 14 天趋势' => '近 14 天趋势',
'篇' => '篇',
'较昨日' => '较昨日',
'持平' => '持平',
'累计' => '累计',
'今日订单实收' => '今日订单实收',
'今日新增用户' => '今日新增用户',
'人' => '人',
'累计注册' => '累计注册',
'今日信息曝光量' => '今日信息曝光量',
'次' => '次',
'累计曝光' => '累计曝光',
'业务多维趋势透视' => '业务多维趋势透视',
'发布量按发布时间归属,订单额按实际支付时间归属'
=> '发布量按发布时间归属,订单额按实际支付时间归属',
'选择时段' => '选择时段',
'近 7 天' => '近 7 天',
'近 14 天' => '近 14 天',
'趋势图' => '趋势图',
'近 14 天合计' => '近 14 天合计',
'近 30 天' => '近 30 天',
'切换趋势指标' => '切换趋势指标',
'发布量按发布时间统计,订单额按支付时间统计'
=> '发布量按发布时间统计,订单额按支付时间统计',
'近 14 天明细' => '近 14 天明细',
'展开查看每日数据' => '展开查看每日数据',
'收起明细' => '收起明细',
'近 14 天每日发布量、订单额与新增注册用户'
=> '近 14 天每日发布量、订单额与新增注册用户',
'日期' => '日期',
'发布量' => '发布量',
'订单额' => '订单额',
'订单额(元)' => '订单额(元)',
'新增用户' => '新增用户',
'新增注册用户' => '新增注册用户',
'信息曝光' => '信息曝光',
'时段累计总量' => '时段累计总量',
'日均水平' => '日均水平',
'单日最高峰值' => '单日最高峰值',
'业务趋势图' => '业务趋势图',
'营收来源构成' => '营收来源构成',
'实收合计' => '实收合计',
'营收构成环形图' => '营收构成环形图',
'笔' => '笔',
'暂无已支付订单数据' => '暂无已支付订单数据',
'热门分类发帖分布' => '热门分类发帖分布',
'累计发帖' => '累计发帖',
'暂无分类数据' => '暂无分类数据',
'同城活跃区域 Top 5' => '同城活跃区域 Top 5',
'基于信息发布所处行政区/县的热度分布' => '基于信息发布所处行政区/县的热度分布',
'查看帖子列表' => '查看帖子列表',
'全部帖子' => '全部帖子',
'暂无地区发布数据' => '暂无地区发布数据',
'平台商业与生态资产' => '平台商业与生态资产',
'会员沉淀、商户入驻及分销裂变体系' => '会员沉淀、商户入驻及分销裂变体系',
'VIP会员体系' => 'VIP会员体系',
'有效VIP会员' => '有效VIP会员',
'会员转化率' => '会员转化率',
'商户入驻生态' => '商户入驻生态',
'入驻商户数' => '入驻商户数',
'推荐商家数' => '推荐商家数',
'二级分销裂变' => '二级分销裂变',
'推广分销员' => '推广分销员',
'已结佣金' => '已结佣金',
'待结佣金' => '待结佣金',
'每日数据明细 (近 14 天)' => '每日数据明细 (近 14 天)',
'每日数据明细 (近 %s 天)' => '每日数据明细 (近 %s 天)',
'展开明细' => '展开明细',
'收起明细' => '收起明细',
'日期' => '日期',
'发布量 (篇)' => '发布量 (篇)',
'订单实收 (元)' => '订单实收 (元)',
'新增注册用户 (人)' => '新增注册用户 (人)',
'信息曝光量 (次)' => '信息曝光量 (次)',
'暂无明细数据' => '暂无明细数据',
'元' => '元',
];

查看文件

@@ -1,188 +1,424 @@
<div class="fc-dash">
<!-- 页头 -->
<header class="fc-dash__head">
<div>
<h1 class="fc-dash__title">{:__('业务总览')}</h1>
<div class="fc-dash__head-info">
<h1 class="fc-dash__title">{:__('业务总览与运营看板')}</h1>
<p class="fc-dash__desc">
{:__('数据更新于')} {$updatedAt} · {:__('订单额仅统计已支付订单')}
{:__('同城便民生活服务平台')} · {:__('数据更新于')} {$updatedAt} · {:__('订单额仅统计已支付实收口径')}
</p>
</div>
<div class="fc-dash__actions">
<a href="fc/post" title="内容管理" class="fc-btn addtabsit {:$auth->check('fc/post')?'':'hide'}">
内容
</a>
<a href="fc/withdraw" title="提现审核" class="fc-btn addtabsit {:$auth->check('fc/withdraw')?'':'hide'}">
提现
{if $metrics.withdrawAuditing > 0}
<span class="fc-btn__badge">{$metrics.withdrawAuditing}</span>
<a href="fc/post" title="{:__('内容管理')}" class="fc-btn addtabsit {:$auth->check('fc/post')?'':'hide'}">
<i class="fa fa-file-text-o" aria-hidden="true"></i> {:__('内容管理')}
{if $pending.post > 0}
<span class="fc-btn__badge">{$pending.post}</span>
{/if}
</a>
<button type="button" class="fc-btn fc-btn--primary fc-dash__refresh" title="刷新数据">
刷新
<a href="fc/merchant" title="{:__('商家管理')}" class="fc-btn addtabsit {:$auth->check('fc/merchant')?'':'hide'}">
<i class="fa fa-shopping-bag" aria-hidden="true"></i> {:__('商家管理')}
{if $pending.merchant > 0}
<span class="fc-btn__badge">{$pending.merchant}</span>
{/if}
</a>
<a href="fc/withdraw" title="{:__('提现审核')}" class="fc-btn addtabsit {:$auth->check('fc/withdraw')?'':'hide'}">
<i class="fa fa-credit-card" aria-hidden="true"></i> {:__('提现审核')}
{if $pending.withdraw > 0}
<span class="fc-btn__badge">{$pending.withdraw}</span>
{/if}
</a>
<a href="fc/report" title="{:__('举报投诉')}" class="fc-btn addtabsit {:$auth->check('fc/report')?'':'hide'}">
<i class="fa fa-shield" aria-hidden="true"></i> {:__('举报投诉')}
{if $pending.report > 0}
<span class="fc-btn__badge">{$pending.report}</span>
{/if}
</a>
<button type="button" class="fc-btn fc-btn--primary fc-dash__refresh" title="{:__('刷新看板')}">
<i class="fa fa-refresh" aria-hidden="true"></i> {:__('刷新')}
</button>
</div>
</header>
<!-- 待办督办预警专区 -->
{if $pending.total > 0}
<div class="fc-alert-banner">
<div class="fc-alert-banner__lead">
<span class="fc-alert-banner__badge"><i class="fa fa-bell-o" aria-hidden="true"></i> {:__('运营待办')}</span>
<span class="fc-alert-banner__text">{:__('当前有')} <strong>{$pending.total}</strong> {:__('项待处理事项需要审核跟进:')}</span>
</div>
<div class="fc-alert-banner__items">
{if $pending.post > 0}
<a href="fc/post" class="fc-alert-pill addtabsit" title="{:__('待审核信息')}">
<i class="fa fa-clock-o text-warning" aria-hidden="true"></i>
<span>{:__('信息待审核')} <b>{$pending.post}</b></span>
</a>
{/if}
{if $pending.merchant > 0}
<a href="fc/merchant" class="fc-alert-pill addtabsit" title="{:__('商家入驻待审')}">
<i class="fa fa-user-circle-o text-info" aria-hidden="true"></i>
<span>{:__('商家入驻待审')} <b>{$pending.merchant}</b></span>
</a>
{/if}
{if $pending.withdraw > 0}
<a href="fc/withdraw" class="fc-alert-pill addtabsit" title="{:__('提现申请待审')}">
<i class="fa fa-money text-danger" aria-hidden="true"></i>
<span>{:__('提现待审核')} <b>{$pending.withdraw}</b> ({:__('共')} ¥{$pending.withdrawAmount|number_format=2})</span>
</a>
{/if}
{if $pending.report > 0}
<a href="fc/report" class="fc-alert-pill addtabsit" title="{:__('违规举报待处理')}">
<i class="fa fa-exclamation-triangle text-warning" aria-hidden="true"></i>
<span>{:__('举报待处理')} <b>{$pending.report}</b></span>
</a>
{/if}
</div>
</div>
{else}
<div class="fc-alert-banner fc-alert-banner--ok">
<i class="fa fa-check-circle fc-alert-banner__ok-icon" aria-hidden="true"></i>
<span>{:__('当前所有待审核帖子、商家、提现与举报均已处理完毕,平台健康运行中')}</span>
</div>
{/if}
<!-- 4 大核心 KPI 指标卡 -->
<section class="fc-kpi-grid" aria-label="{:__('今日核心指标')}">
<!-- 1. 今日发布量 -->
<article class="fc-kpi">
<div class="fc-kpi__head">
<span class="fc-kpi__label">{:__('今日发布量')}</span>
<span class="fc-kpi__icon"><i class="fa fa-paper-plane-o" aria-hidden="true"></i></span>
<span class="fc-kpi__icon fc-kpi__icon--green"><i class="fa fa-paper-plane-o" aria-hidden="true"></i></span>
</div>
<p class="fc-kpi__value">{$metrics.todayPost} <small class="fc-kpi__unit">{:__('篇')}</small></p>
<div class="fc-kpi__foot">
<div class="fc-kpi__compare">
{if $metrics.postDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>{$metrics.postDelta}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{elseif $metrics.postDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>{:abs($metrics['postDelta'])}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('持平')}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{/if}
</div>
<div class="fc-kpi__total">
{:__('累计')} <span>{$metrics.totalPost}</span> {:__('篇')}
</div>
</div>
<p class="fc-kpi__value">{$metrics.todayPost}</p>
<p class="fc-kpi__foot">
{if $metrics.postDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>{$metrics.postDelta}</span>
<span>{:__('较昨日')}</span>
{elseif $metrics.postDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>{:abs($metrics['postDelta'])}</span>
<span>{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('与昨日持平')}</span>
{/if}
</p>
</article>
<!-- 2. 今日订单额 -->
<article class="fc-kpi">
<div class="fc-kpi__head">
<span class="fc-kpi__label">{:__('今日订单额')}</span>
<span class="fc-kpi__label">{:__('今日订单实收')}</span>
<span class="fc-kpi__icon fc-kpi__icon--gold"><i class="fa fa-jpy" aria-hidden="true"></i></span>
</div>
<p class="fc-kpi__value fc-kpi__value--money">¥{$metrics.todayOrderAmount|number_format=2}</p>
<p class="fc-kpi__foot">
{if $metrics.orderDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>{$metrics.orderDelta|number_format=2}</span>
<span>{:__('较昨日')}</span>
{elseif $metrics.orderDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>{:number_format(abs($metrics['orderDelta']), 2)}</span>
<span>{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('与昨日持平')}</span>
{/if}
</p>
<div class="fc-kpi__foot">
<div class="fc-kpi__compare">
{if $metrics.orderDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>¥{$metrics.orderDelta|number_format=2}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{elseif $metrics.orderDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>¥{:number_format(abs($metrics['orderDelta']), 2)}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('持平')}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{/if}
</div>
<div class="fc-kpi__total">
{:__('累计')} <span>¥{$metrics.totalOrderAmount|number_format=2}</span>
</div>
</div>
</article>
<!-- 3. 今日新增用户 -->
<article class="fc-kpi">
<div class="fc-kpi__head">
<span class="fc-kpi__label">{:__('待审核信息数')}</span>
<span class="fc-kpi__icon fc-kpi__icon--red"><i class="fa fa-clock-o" aria-hidden="true"></i></span>
<span class="fc-kpi__label">{:__('今日新增用户')}</span>
<span class="fc-kpi__icon fc-kpi__icon--blue"><i class="fa fa-user-plus" aria-hidden="true"></i></span>
</div>
<p class="fc-kpi__value">{$metrics.todayUser} <small class="fc-kpi__unit">{:__('人')}</small></p>
<div class="fc-kpi__foot">
<div class="fc-kpi__compare">
{if $metrics.userDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>{$metrics.userDelta}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{elseif $metrics.userDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>{:abs($metrics['userDelta'])}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('持平')}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{/if}
</div>
<div class="fc-kpi__total">
{:__('累计注册')} <span>{$metrics.totalUser}</span> {:__('人')}
</div>
</div>
<p class="fc-kpi__value">{$metrics.pendingPost}</p>
<p class="fc-kpi__foot">
{if $metrics.pendingPost > 0 && $auth->check('fc/post')}
<a href="fc/post" title="{:__('内容管理')}" class="fc-kpi__link addtabsit">{:__('去审核')}<i class="fa fa-angle-right" aria-hidden="true"></i></a>
{else}
<span>{:__('暂无待审核')}</span>
{/if}
</p>
</article>
<!-- 4. 今日信息浏览曝光 -->
<article class="fc-kpi">
<div class="fc-kpi__head">
<span class="fc-kpi__label">{:__('待审核提现数')}</span>
<span class="fc-kpi__icon fc-kpi__icon--blue"><i class="fa fa-credit-card" aria-hidden="true"></i></span>
<span class="fc-kpi__label">{:__('今日信息曝光量')}</span>
<span class="fc-kpi__icon fc-kpi__icon--purple"><i class="fa fa-eye" aria-hidden="true"></i></span>
</div>
<p class="fc-kpi__value">{$metrics.todayViews} <small class="fc-kpi__unit">{:__('次')}</small></p>
<div class="fc-kpi__foot">
<div class="fc-kpi__compare">
{if $metrics.viewsDelta > 0}
<span class="fc-trend fc-trend--up"><i class="fa fa-long-arrow-up" aria-hidden="true"></i>{$metrics.viewsDelta}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{elseif $metrics.viewsDelta < 0}
<span class="fc-trend fc-trend--down"><i class="fa fa-long-arrow-down" aria-hidden="true"></i>{:abs($metrics['viewsDelta'])}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{else}
<span class="fc-trend fc-trend--flat">{:__('持平')}</span>
<span class="fc-kpi__subtext">{:__('较昨日')}</span>
{/if}
</div>
<div class="fc-kpi__total">
{:__('累计曝光')} <span>{$metrics.totalViews}</span> {:__('次')}
</div>
</div>
<p class="fc-kpi__value">{$metrics.withdrawAuditing}</p>
<p class="fc-kpi__foot">
{if $metrics.withdrawAuditing > 0 && $auth->check('fc/withdraw')}
<a href="fc/withdraw" title="{:__('提现审核')}" class="fc-kpi__link addtabsit">{:__('去处理')}<i class="fa fa-angle-right" aria-hidden="true"></i></a>
{else}
<span>{:__('暂无待审核')}</span>
{/if}
</p>
</article>
</section>
<div class="fc-dash__main">
<!-- 中区:多维趋势与业务构成 -->
<div class="fc-dash__middle-grid">
<!-- 左侧:多维趋势分析图 -->
<section class="fc-card fc-card--trend">
<header class="fc-card__head">
<div>
<h2 class="fc-card__title">{:__('业务多维趋势透视')}</h2>
<p class="fc-card__desc">{:__('发布量按发布时间归属,订单额按实际支付时间归属')}</p>
</div>
<div class="fc-card__controls">
<!-- 时段切换 -->
<div class="fc-seg fc-seg--range" role="group" aria-label="{:__('选择时段')}">
<button type="button" class="fc-seg__btn fc-range-btn" data-days="7">{:__('近 7 天')}</button>
<button type="button" class="fc-seg__btn fc-range-btn is-active" data-days="14">{:__('近 14 天')}</button>
<button type="button" class="fc-seg__btn fc-range-btn" data-days="30">{:__('近 30 天')}</button>
</div>
<!-- 指标切换 -->
<div class="fc-seg fc-seg--metric" role="group" aria-label="{:__('切换趋势指标')}">
<button type="button" class="fc-seg__btn fc-metric-btn is-active" data-metric="posts">{:__('发布量')}</button>
<button type="button" class="fc-seg__btn fc-metric-btn" data-metric="orders">{:__('订单额')}</button>
<button type="button" class="fc-seg__btn fc-metric-btn" data-metric="users">{:__('新增用户')}</button>
<button type="button" class="fc-seg__btn fc-metric-btn" data-metric="views">{:__('信息曝光')}</button>
</div>
</div>
</header>
<!-- 趋势小计看板 -->
<div class="fc-trend-summary">
<div class="fc-trend-summary__item">
<span class="fc-trend-summary__label">{:__('时段累计总量')}</span>
<strong id="fc-trend-sum" class="fc-trend-summary__num">—</strong>
</div>
<div class="fc-trend-summary__item">
<span class="fc-trend-summary__label">{:__('日均水平')}</span>
<strong id="fc-trend-avg" class="fc-trend-summary__num">—</strong>
</div>
<div class="fc-trend-summary__item">
<span class="fc-trend-summary__label">{:__('单日最高峰值')}</span>
<strong id="fc-trend-peak" class="fc-trend-summary__num">—</strong>
</div>
</div>
<!-- Echarts 折线走势容器 -->
<div id="fc-trend-chart" class="fc-chart" role="img" aria-label="{:__('业务趋势图')}"></div>
</section>
<!-- 右侧:营收构成分析 & 热门分类分布 -->
<div class="fc-side-stack">
<!-- 营收构成分析 -->
<section class="fc-card fc-card--compact">
<header class="fc-card__head">
<h2 class="fc-card__title">{:__('营收来源构成')}</h2>
<span class="fc-card__tag">{:__('实收合计')} ¥{$metrics.totalOrderAmount|number_format=2}</span>
</header>
<div class="fc-card__body">
<div class="fc-pie-wrap">
<div id="fc-revenue-chart" class="fc-pie-chart" role="img" aria-label="{:__('营收构成环形图')}"></div>
</div>
<div class="fc-revenue-list">
{if count($revenueTypes) > 0}
{foreach $revenueTypes as $rev}
<div class="fc-rev-item">
<div class="fc-rev-item__info">
<span class="fc-rev-item__name">{$rev.label}</span>
<span class="fc-rev-item__cnt">({$rev.count}{:__('笔')})</span>
</div>
<div class="fc-rev-item__num">
<strong>¥{$rev.amount|number_format=2}</strong>
<small>{$rev.percent}%</small>
</div>
</div>
{/foreach}
{else}
<div class="fc-empty-hint">{:__('暂无已支付订单数据')}</div>
{/if}
</div>
</div>
</section>
<!-- 热门分类发帖分布 -->
<section class="fc-card fc-card--compact">
<header class="fc-card__head">
<h2 class="fc-card__title">{:__('热门分类发帖分布')}</h2>
<span class="fc-card__tag">{:__('累计发帖')} {$metrics.totalPost} {:__('篇')}</span>
</header>
<div class="fc-card__body fc-cat-ranks">
{if count($categoryRanks) > 0}
{foreach $categoryRanks as $cat}
<div class="fc-rank-row">
<div class="fc-rank-row__head">
<span class="fc-rank-row__title">{$cat.name}</span>
<span class="fc-rank-row__count"><b>{$cat.count}</b> {:__('篇')} ({$cat.percent}%)</span>
</div>
<div class="fc-rank-bar">
<div class="fc-rank-bar__inner" style="width: {$cat.percent}%;"></div>
</div>
</div>
{/foreach}
{else}
<div class="fc-empty-hint">{:__('暂无分类数据')}</div>
{/if}
</div>
</section>
</div>
</div>
<!-- 下区:同城活跃区域排行与生态资产概览 -->
<div class="fc-dash__bottom-grid">
<!-- 1. 同城活跃区域排行 -->
<section class="fc-card">
<header class="fc-card__head">
<div>
<h2 class="fc-card__title">{:__('近 14 天趋势')}</h2>
<p class="fc-card__desc">{:__('发布量按发布时间统计,订单额按支付时间统计')}</p>
</div>
<div class="fc-seg" role="group" aria-label="{:__('切换趋势指标')}">
<button type="button" class="fc-seg__btn is-active" data-metric="posts" aria-pressed="true">{:__('发布量')}</button>
<button type="button" class="fc-seg__btn" data-metric="orders" aria-pressed="false">{:__('订单额')}</button>
<button type="button" class="fc-seg__btn" data-metric="members" aria-pressed="false">{:__('新增用户')}</button>
<h2 class="fc-card__title">{:__('同城活跃区域 Top 5')}</h2>
<p class="fc-card__desc">{:__('基于信息发布所处行政区/县的热度分布')}</p>
</div>
<a href="fc/post" title="{:__('查看帖子列表')}" class="fc-card__more addtabsit">{:__('全部帖子')} <i class="fa fa-angle-right" aria-hidden="true"></i></a>
</header>
<div id="fc-trend-chart" class="fc-chart" role="img" aria-label="{:__('近 14 天发布量趋势图')}"></div>
<p class="fc-chart__total">
<span>{:__('近 14 天合计')}</span>
<strong id="fc-trend-total">—</strong>
</p>
<div class="fc-card__body">
{if count($areaRanks) > 0}
<div class="fc-area-list">
{foreach $areaRanks as $idx => $area}
<div class="fc-area-item">
<span class="fc-area-item__badge {if $idx < 3}is-top{/if}">{$idx + 1}</span>
<span class="fc-area-item__name">{$area.region_name}</span>
<div class="fc-area-item__bar-wrap">
<div class="fc-area-item__bar" style="width: {$area.percent}%;"></div>
</div>
<span class="fc-area-item__num"><b>{$area.count}</b> {:__('篇')}</span>
</div>
{/foreach}
</div>
{else}
<div class="fc-empty-hint">{:__('暂无地区发布数据')}</div>
{/if}
</div>
</section>
<!-- 2. 会员、商家与分销裂变生态 -->
<section class="fc-card">
<header class="fc-card__head">
<h2 class="fc-card__title">{:__('平台累计')}</h2>
<div>
<h2 class="fc-card__title">{:__('平台商业与生态资产')}</h2>
<p class="fc-card__desc">{:__('会员沉淀、商户入驻及分销裂变体系')}</p>
</div>
</header>
<dl class="fc-stats">
<div class="fc-stat">
<dt>{:__('累计发布量')}</dt>
<dd>{$metrics.totalPost}</dd>
</div>
<div class="fc-stat">
<dt>{:__('累计订单额')}</dt>
<dd class="is-money">¥{$metrics.totalOrderAmount|number_format=2}</dd>
</div>
<div class="fc-stat">
<dt>{:__('注册用户数')}</dt>
<dd>{$metrics.userCount}</dd>
</div>
<div class="fc-stat">
<dt>{:__('会员数')}</dt>
<dd>{$metrics.memberCount}</dd>
</div>
<div class="fc-stat">
<dt>{:__('商家数')}</dt>
<dd>{$metrics.merchantCount}</dd>
</div>
</dl>
<div class="fc-card__body">
<div class="fc-eco-grid">
<!-- 会员体系 -->
<div class="fc-eco-block">
<div class="fc-eco-block__title"><i class="fa fa-diamond text-warning" aria-hidden="true"></i> {:__('VIP会员体系')}</div>
<div class="fc-eco-block__stat">
<div>
<span class="fc-eco-block__val">{$metrics.memberCount}</span>
<span class="fc-eco-block__desc">{:__('有效VIP会员')}</span>
</div>
<div>
<span class="fc-eco-block__val">{$ecosystem.memberRate}%</span>
<span class="fc-eco-block__desc">{:__('会员转化率')}</span>
</div>
</div>
</div>
<header class="fc-card__head fc-card__head--sub">
<h2 class="fc-card__title">{:__('分销佣金')}</h2>
</header>
<dl class="fc-stats">
<div class="fc-stat">
<dt>{:__('待结算佣金')}</dt>
<dd class="is-money">¥{$metrics.commissionPending|number_format=2}</dd>
<!-- 商户生态 -->
<div class="fc-eco-block">
<div class="fc-eco-block__title"><i class="fa fa-shopping-bag text-success" aria-hidden="true"></i> {:__('商户入驻生态')}</div>
<div class="fc-eco-block__stat">
<div>
<span class="fc-eco-block__val">{$metrics.merchantCount}</span>
<span class="fc-eco-block__desc">{:__('入驻商户数')}</span>
</div>
<div>
<span class="fc-eco-block__val">{$metrics.recommendMerchant}</span>
<span class="fc-eco-block__desc">{:__('推荐商家数')}</span>
</div>
</div>
</div>
<!-- 分销体系 -->
<div class="fc-eco-block">
<div class="fc-eco-block__title"><i class="fa fa-sitemap text-primary" aria-hidden="true"></i> {:__('二级分销裂变')}</div>
<div class="fc-eco-block__stat">
<div>
<span class="fc-eco-block__val">{$ecosystem.distributorCount}</span>
<span class="fc-eco-block__desc">{:__('推广分销员')}</span>
</div>
<div>
<span class="fc-eco-block__val is-gold">¥{$ecosystem.commissionSettled|number_format=2}</span>
<span class="fc-eco-block__desc">{:__('已结佣金')}</span>
</div>
<div>
<span class="fc-eco-block__val is-muted">¥{$ecosystem.commissionPending|number_format=2}</span>
<span class="fc-eco-block__desc">{:__('待结佣金')}</span>
</div>
</div>
</div>
</div>
<div class="fc-stat">
<dt>{:__('已结算佣金')}</dt>
<dd class="is-muted">¥{$metrics.commissionSettled|number_format=2}</dd>
</div>
</dl>
</div>
</section>
</div>
<details class="fc-card fc-detail">
<!-- 底区:详细数据明细表(支持折叠和多时段联动) -->
<details class="fc-card fc-detail" open>
<summary class="fc-detail__summary">
<span class="fc-card__title">{:__('近 14 天明细')}</span>
<div class="fc-detail__title-wrap">
<i class="fa fa-table" aria-hidden="true"></i>
<span class="fc-card__title" id="fc-table-title">{:__('每日数据明细 (近 14 天)')}</span>
</div>
<span class="fc-detail__hint">
<span class="fc-detail__hint-open">{:__('展开查看每日数据')}</span>
<span class="fc-detail__hint-open">{:__('展开明细')}</span>
<span class="fc-detail__hint-close">{:__('收起明细')}</span>
</span>
<i class="fa fa-angle-down fc-detail__chevron" aria-hidden="true"></i>
</summary>
<div class="fc-detail__body">
<div class="fc-table-wrap">
<table class="fc-table">
<caption class="sr-only">{:__('近 14 天每日发布量、订单额与新增注册用户')}</caption>
<table class="fc-table" id="fc-trend-table">
<thead>
<tr>
<th scope="col">{:__('日期')}</th>
<th scope="col" class="is-num">{:__('发布量')}</th>
<th scope="col" class="is-num">{:__('订单额')}</th>
<th scope="col" class="is-num">{:__('新增注册用户')}</th>
<th scope="col" class="is-num">{:__('发布量 (篇)')}</th>
<th scope="col" class="is-num">{:__('订单实收 (元)')}</th>
<th scope="col" class="is-num">{:__('新增注册用户 (人)')}</th>
<th scope="col" class="is-num">{:__('信息曝光量 (次)')}</th>
</tr>
</thead>
<tbody>
<tbody id="fc-table-body">
{foreach $trendRows as $item}
<tr>
<td>{$item.date}</td>
<td><strong>{$item.date}</strong></td>
<td class="is-num">{$item.posts}</td>
<td class="is-num">¥{$item.orders|number_format=2}</td>
<td class="is-num">{$item.members}</td>
<td class="is-num fc-td-money">¥{$item.orders|number_format=2}</td>
<td class="is-num">{$item.users}</td>
<td class="is-num">{$item.views}</td>
</tr>
{/foreach}
</tbody>

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

二进制
public/assets/img/login-recommend.jpg 普通文件

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 62 KiB

查看文件

@@ -1,164 +1,374 @@
define(['jquery', 'bootstrap', 'backend', 'echarts', 'echarts-theme'], function ($, undefined, Backend, Echarts) {
/**
* 同城分类信息 - 数据总览与运营看板
*/
var Controller = {
/**
* 看板首页初始化与图表联动逻辑
*/
index: function () {
var trend = (typeof Config !== 'undefined' && Config.trend) ? Config.trend : null;
var allTrends = (typeof Config !== 'undefined' && Config.allTrends) ? Config.allTrends : null;
var revenueTypes = (typeof Config !== 'undefined' && Config.revenueTypes) ? Config.revenueTypes : [];
var categoryRanks = (typeof Config !== 'undefined' && Config.categoryRanks) ? Config.categoryRanks : [];
// 刷新当前看板
// 页面刷新按钮
$(document).on('click', '.fc-dash__refresh', function () {
$(this).find('.fa').addClass('fa-spin');
var $btn = $(this);
$btn.find('.fa').addClass('fa-spin');
window.location.reload();
});
// 没有趋势数据(或 echarts 未加载)时静默降级:下方明细表已展示同样数据
if (!trend || !trend.dates || !trend.dates.length || !Echarts) {
return;
}
// 当前状态
var currentDays = 14;
var currentMetric = 'posts';
var dates = $.map(trend.dates, function (day) {
return day.slice(5); // 2026-10-09 => 10-09
});
// 工具:格式化千分位数值
var formatNum = function (n, isMoney) {
var val = Number(n) || 0;
if (isMoney) {
return '¥' + val.toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
return val.toLocaleString('zh-CN');
};
var total = function (list) {
// 工具:数组求和
var sumArray = function (arr) {
var sum = 0;
$.each(list || [], function (i, n) {
sum += Number(n) || 0;
$.each(arr || [], function (i, v) {
sum += (Number(v) || 0);
});
return sum;
};
var formatNum = function (n) {
var value = Number(n) || 0;
var options = value % 1 === 0 ? {} : {minimumFractionDigits: 2, maximumFractionDigits: 2};
try {
return value.toLocaleString('zh-CN', options);
} catch (e) {
return value.toFixed(value % 1 === 0 ? 0 : 2);
}
// 工具:数组求峰值
var peakArray = function (arr) {
var max = 0;
$.each(arr || [], function (i, v) {
var n = Number(v) || 0;
if (n > max) {
max = n;
}
});
return max;
};
var METRICS = {
// 指标元数据配置(符合轻氧浅绿与品牌规范)
var METRIC_CONFIG = {
posts: {
label: __('发布量'),
color: '#44507A',
fill: 'rgba(68, 80, 122, .18)',
axis: '{value}',
money: false,
data: trend.posts || []
unit: __('篇'),
color: '#00D26A',
fillStart: 'rgba(0, 210, 106, 0.28)',
fillEnd: 'rgba(0, 210, 106, 0.01)',
isMoney: false
},
orders: {
label: __('订单额'),
color: '#B7793A',
fill: 'rgba(183, 121, 58, .18)',
axis: '¥{value}',
money: true,
data: trend.orders || []
unit: __('元'),
color: '#F5A623',
fillStart: 'rgba(245, 166, 35, 0.28)',
fillEnd: 'rgba(245, 166, 35, 0.01)',
isMoney: true
},
members: {
label: __('新增注册用户'),
color: '#2E8A66',
fill: 'rgba(46, 138, 102, .16)',
axis: '{value}',
money: false,
data: trend.members || []
users: {
label: __('新增用户'),
unit: __('人'),
color: '#3B82F6',
fillStart: 'rgba(59, 130, 246, 0.25)',
fillEnd: 'rgba(59, 130, 246, 0.01)',
isMoney: false
},
views: {
label: __('信息曝光'),
unit: __('次'),
color: '#8B5CF6',
fillStart: 'rgba(139, 92, 246, 0.25)',
fillEnd: 'rgba(139, 92, 246, 0.01)',
isMoney: false
}
};
var reduceMotion = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
var $chart = $('#fc-trend-chart');
var chart = null;
var trendChartInstance = null;
var revenueChartInstance = null;
var $trendChartEl = $('#fc-trend-chart');
var $revenueChartEl = $('#fc-revenue-chart');
/**
* 构建趋势折线/面积图配置项
*
* @param {Object} trendData
* @param {string} metricKey
* @return {Object}
*/
var buildTrendOption = function (trendData, metricKey) {
var meta = METRIC_CONFIG[metricKey];
var rawDates = trendData.dates || [];
var dates = $.map(rawDates, function (d) {
return d.slice(5); // 截取 MM-DD
});
var data = trendData[metricKey] || [];
var buildOption = function (metric) {
return {
color: [metric.color],
grid: {left: 6, right: 16, top: 18, bottom: 4, containLabel: true},
color: [meta.color],
grid: {
left: 12,
right: 20,
top: 24,
bottom: 8,
containLabel: true
},
tooltip: {
trigger: 'axis',
backgroundColor: '#FFFFFF',
borderColor: '#E7EBF1',
borderColor: '#E2E8F0',
borderWidth: 1,
padding: [8, 12],
textStyle: {color: '#232A38', fontSize: 12},
extraCssText: 'border-radius:8px;box-shadow:0 6px 18px rgba(23,35,61,.10);',
axisPointer: {type: 'line', lineStyle: {color: '#C9D2DF', type: 'dashed'}},
padding: [10, 14],
textStyle: {color: '#1E293B', fontSize: 13},
extraCssText: 'border-radius:10px;box-shadow:0 8px 24px rgba(15,23,42,0.08);',
axisPointer: {
type: 'line',
lineStyle: {color: meta.color, width: 1.5, type: 'dashed'}
},
formatter: function (params) {
var item = params[0];
var prefix = metric.money ? '¥' : '';
var dot = '<span style="display:inline-block;width:7px;height:7px;border-radius:50%;'
+ 'background:' + metric.color + ';margin-right:6px;"></span>';
return item.axisValue + '<br/>' + dot + metric.label + ':<b>'
+ prefix + formatNum(item.data) + '</b>';
var fullDate = rawDates[item.dataIndex] || item.axisValue;
var valStr = meta.isMoney ? ('¥' + Number(item.data).toFixed(2)) : (item.data + ' ' + meta.unit);
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;'
+ 'background:' + meta.color + ';margin-right:6px;"></span>';
return '<div style="font-size:12px;color:#64748B;margin-bottom:4px;">' + fullDate + '</div>'
+ dot + meta.label + ':<b style="font-size:14px;color:#1E293B;">' + valStr + '</b>';
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
axisLine: {lineStyle: {color: '#E7EBF1'}},
axisLine: {lineStyle: {color: '#E2E8F0'}},
axisTick: {show: false},
axisLabel: {color: '#6B7684', fontSize: 11, margin: 10}
axisLabel: {color: '#64748B', fontSize: 12, margin: 12}
},
yAxis: {
type: 'value',
minInterval: metric.money ? 0 : 1,
minInterval: meta.isMoney ? 0 : 1,
axisLine: {show: false},
axisTick: {show: false},
axisLabel: {color: '#6B7684', fontSize: 11, formatter: metric.axis},
splitLine: {lineStyle: {color: '#EFF2F6', type: 'dashed'}}
axisLabel: {
color: '#64748B',
fontSize: 12,
formatter: function (v) {
return meta.isMoney ? ('¥' + v) : v;
}
},
splitLine: {lineStyle: {color: '#F1F5F9', type: 'dashed'}}
},
series: [{
name: metric.label,
name: meta.label,
type: 'line',
smooth: true,
smooth: 0.35,
symbol: 'circle',
symbolSize: 6,
showSymbol: false,
lineStyle: {width: 2},
itemStyle: {color: metric.color, borderColor: '#FFFFFF', borderWidth: 2},
emphasis: {focus: 'series'},
lineStyle: {width: 2.5, color: meta.color},
itemStyle: {color: meta.color, borderColor: '#FFFFFF', borderWidth: 2},
emphasis: {
focus: 'series',
scale: true,
itemStyle: {color: meta.color, borderColor: '#FFFFFF', borderWidth: 3}
},
areaStyle: {
color: new Echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: metric.fill},
{offset: 1, color: 'rgba(255, 255, 255, 0)'}
{offset: 0, color: meta.fillStart},
{offset: 1, color: meta.fillEnd}
])
},
data: metric.data
data: data
}],
animation: !reduceMotion,
animationDuration: 500
animationDuration: 400
};
};
var applyMetric = function (key) {
var metric = METRICS[key];
$('.fc-seg__btn')
.removeClass('is-active')
.attr('aria-pressed', 'false')
.filter('[data-metric="' + key + '"]')
.addClass('is-active')
.attr('aria-pressed', 'true');
$chart.attr('aria-label', __('近 14 天') + metric.label + __('趋势图'));
$('#fc-trend-total').text((metric.money ? '¥' : '') + formatNum(total(metric.data)));
if (chart) {
chart.setOption(buildOption(metric), true);
/**
* 更新图表及上方统计卡
*/
var renderTrend = function () {
if (!allTrends || !allTrends[currentDays]) {
return;
}
var trendData = allTrends[currentDays];
var meta = METRIC_CONFIG[currentMetric];
var data = trendData[currentMetric] || [];
// 1. 计算时段总计、日均、峰值
var total = sumArray(data);
var avg = data.length > 0 ? (total / data.length) : 0;
var peak = peakArray(data);
if (meta.isMoney) {
$('#fc-trend-sum').text(formatNum(total, true));
$('#fc-trend-avg').text(formatNum(avg, true));
$('#fc-trend-peak').text(formatNum(peak, true));
} else {
$('#fc-trend-sum').text(formatNum(total, false) + ' ' + meta.unit);
$('#fc-trend-avg').text(avg.toFixed(1) + ' ' + meta.unit);
$('#fc-trend-peak').text(formatNum(peak, false) + ' ' + meta.unit);
}
// 2. 更新 Echarts
if (trendChartInstance) {
trendChartInstance.setOption(buildTrendOption(trendData, currentMetric), true);
}
// 3. 同步更新底表明细
updateDetailTable(trendData.rows || []);
};
if ($chart.length) {
chart = Echarts.init($chart[0], 'walden');
applyMetric('posts');
/**
* 同步更新底部每日明细表内容
*
* @param {Array} rows
*/
var updateDetailTable = function (rows) {
$('#fc-table-title').text(__('每日数据明细 (近 %s 天)', currentDays));
var $tbody = $('#fc-table-body');
$tbody.empty();
$(window).on('resize', function () {
chart.resize();
if (!rows || rows.length === 0) {
$tbody.append('<tr><td colspan="5" class="text-center text-muted" style="padding: 24px;">' + __('暂无明细数据') + '</td></tr>');
return;
}
$.each(rows, function (idx, item) {
var orderStr = '¥' + Number(item.orders || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
var tr = '<tr>'
+ '<td><strong>' + item.date + '</strong></td>'
+ '<td class="is-num">' + (item.posts || 0) + '</td>'
+ '<td class="is-num fc-td-money">' + orderStr + '</td>'
+ '<td class="is-num">' + (item.users || 0) + '</td>'
+ '<td class="is-num">' + (item.views || 0) + '</td>'
+ '</tr>';
$tbody.append(tr);
});
};
/**
* 初始化营收构成环形饼图
*/
var initRevenueChart = function () {
if (!$revenueChartEl.length || !Echarts) {
return;
}
revenueChartInstance = Echarts.init($revenueChartEl[0]);
if (!revenueTypes || revenueTypes.length === 0) {
return;
}
var chartData = $.map(revenueTypes, function (item) {
return {
name: item.label,
value: item.amount
};
});
$(document).on('click', '.fc-seg__btn', function () {
applyMetric($(this).data('metric'));
});
var palette = ['#00D26A', '#F5A623', '#3B82F6', '#8B5CF6', '#EC4899', '#14B8A6'];
var option = {
color: palette,
tooltip: {
trigger: 'item',
backgroundColor: '#FFFFFF',
borderColor: '#E2E8F0',
borderWidth: 1,
padding: [8, 12],
textStyle: {color: '#1E293B', fontSize: 12},
extraCssText: 'border-radius:8px;box-shadow:0 6px 18px rgba(15,23,42,0.08);',
formatter: function (p) {
return p.name + '<br/><b>¥' + Number(p.value).toFixed(2) + '</b> (' + p.percent + '%)';
}
},
legend: {
show: false
},
series: [{
type: 'pie',
radius: ['52%', '76%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#FFFFFF',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
scale: true,
scaleSize: 6,
label: {
show: true,
fontSize: 13,
fontWeight: 'bold',
formatter: '{b}\n{d}%'
}
},
labelLine: {show: false},
data: chartData
}]
};
revenueChartInstance.setOption(option);
};
// 初始化趋势主图
if ($trendChartEl.length && Echarts) {
trendChartInstance = Echarts.init($trendChartEl[0]);
renderTrend();
}
// 初始化营收环形图
initRevenueChart();
// 监听窗口尺寸变化自适应
$(window).on('resize', function () {
if (trendChartInstance) {
trendChartInstance.resize();
}
if (revenueChartInstance) {
revenueChartInstance.resize();
}
});
// 时段切换点击(近 7 / 14 / 30 天)
$(document).on('click', '.fc-range-btn', function () {
var $btn = $(this);
var days = parseInt($btn.data('days'), 10) || 14;
if (days === currentDays) {
return;
}
currentDays = days;
$('.fc-range-btn').removeClass('is-active');
$btn.addClass('is-active');
renderTrend();
});
// 指标切换点击(发布量 / 订单额 / 新增用户 / 信息曝光)
$(document).on('click', '.fc-metric-btn', function () {
var $btn = $(this);
var metric = $btn.data('metric');
if (metric === currentMetric) {
return;
}
currentMetric = metric;
$('.fc-metric-btn').removeClass('is-active');
$btn.addClass('is-active');
renderTrend();
});
}
};