From 685e8472089f17e9847c2d6ad8e0bf6d01c885ec Mon Sep 17 00:00:00 2001 From: hant Date: Mon, 12 May 2025 17:53:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/admin/controller/Orderplan.php | 3 - .../admin/controller/statistics/Item.php | 72 +++++++ .../admin/view/statistics/item/index.html | 61 ++++++ public/assets/js/addons.js | 8 +- public/assets/js/backend/statistics/item.js | 194 ++++++++++++++++++ 5 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 application/admin/controller/statistics/Item.php create mode 100644 application/admin/view/statistics/item/index.html create mode 100644 public/assets/js/backend/statistics/item.js diff --git a/application/admin/controller/Orderplan.php b/application/admin/controller/Orderplan.php index eb1f378..8e34def 100644 --- a/application/admin/controller/Orderplan.php +++ b/application/admin/controller/Orderplan.php @@ -52,9 +52,7 @@ class Orderplan extends Backend public function dashboard() { - return $this->fetch('orderplan/index'); - } public function data() @@ -127,7 +125,6 @@ class Orderplan extends Backend } } - $this->buildDate($build); $build->field([ 'DATE(create_time) day', 'sum(total) total', diff --git a/application/admin/controller/statistics/Item.php b/application/admin/controller/statistics/Item.php new file mode 100644 index 0000000..103475d --- /dev/null +++ b/application/admin/controller/statistics/Item.php @@ -0,0 +1,72 @@ +fetch('index'); + } + + public function list(){ + + $build = new Order(); + + $start = now()->modify('-14 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + + $filter ['daterange'] = request()->get('daterange'); + if (!empty($filter['daterange'])) { + $arr = explode(' - ', $filter['daterange']); + if (trim($arr[0])) { + $start = trim($arr[0]); + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + $build->field([ + 'item_title', + 'sum(total) total', + 'count(id) count', + 'sum(performance) performance' + ])->group('item_title'); + + return []; + } + + public function chartData(){ + return []; + } + +} diff --git a/application/admin/view/statistics/item/index.html b/application/admin/view/statistics/item/index.html new file mode 100644 index 0000000..b74f52d --- /dev/null +++ b/application/admin/view/statistics/item/index.html @@ -0,0 +1,61 @@ +
+ + + +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
diff --git a/public/assets/js/addons.js b/public/assets/js/addons.js index 640ed38..aeba863 100755 --- a/public/assets/js/addons.js +++ b/public/assets/js/addons.js @@ -1,6 +1,8 @@ define([], function () { require([], function () { //绑定data-toggle=addresspicker属性点击事件 + console.log('111'); + $(document).on('click', "[data-toggle='addresspicker']", function () { var that = this; var callback = $(that).data('callback'); @@ -10,9 +12,9 @@ define([], function () { var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : ""; var lat = lat_id ? $("#" + lat_id).val() : ''; var lng = lng_id ? $("#" + lng_id).val() : ''; - var city_code = $("#c-city").val(); + var city_code = $("#area_id").val(); var zoom = zoom_id ? $("#" + zoom_id).val() : ''; - var url = "/addons/address/index/select?a=1"; + var url = "/addons/address/index/select?1=1"; url += (lat && lng) ? 'lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") +(zoom ? "&zoom=" + zoom : "") : '' @@ -20,7 +22,7 @@ define([], function () { if (city_code){ url += city_code ? "&city_code=" + city_code : ""; } - // console.log(url); + console.log(url); Fast.api.open(url, '位置选择', { callback: function (res) { input_id && $("#" + input_id).val(res.address).trigger("change"); diff --git a/public/assets/js/backend/statistics/item.js b/public/assets/js/backend/statistics/item.js new file mode 100644 index 0000000..409a941 --- /dev/null +++ b/public/assets/js/backend/statistics/item.js @@ -0,0 +1,194 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-theme', 'template','addtabs','moment'], function ($, undefined, Backend, Table, Form,Echarts,undefined,Template,Datatable,Moment) { + + var Controller = { + + index: function () { + //绑定事件 + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + var $targetPanel = $($(this).attr("href")); + var tabVal = $(this).data('val'); + + if (tabVal === 'second') { + // 当切换到“统计列表”时,自动刷新表格 + //$targetPanel.find(".btn-refresh").trigger("click"); + // 初始化表格参数配置 + Table.api.init(); + // 表格2 + var table2 = $("#table2"); + table2.bootstrapTable({ + url: 'statistics/item/list' + location.search, + toolbar: '#toolbar1', + sortName: 'id', + search: false, + commonSearch:true, + visible: false, + showToggle: false, + showColumns: false, + showExport: true, + searchFormVisible:true, + columns: [ + [ + //{field: 'id', title: __('Id')}, + {field: 'id', title: __('ID'),visible:true,operate: false}, + {field: 'admin_user', title: __('派单员'),operate: "LIKE"}, + {field: 'count_num', title: __('总订单数'),operate: false}, + {field: 'finish_num', title: __('完单数'),operate: false}, + {field: 'total', title: __('成效额(¥)'), operate: false}, + {field: 'performance', title: __('总业绩(¥)'), operate: false}, + {field: 'cost_total', title: __('总成本(¥)'), operate: false}, + {field: 'refund_total', title: __('退款金额(¥)'), operate: false}, + {field: 'refund_count', title: __('退款单数'), operate: false}, + + {field: 'performance_rate', title: __('利润率(%)'), operate: false}, + {field: 'trans_rate', title: __('转化率(%)'), operate: false}, + {field: 'cash_value', title: __('变现值'), operate: false}, + {field: 'performance_avg', title: __('客单利润(¥)'), operate: false}, + {field: 'total_avg', title: __('客单价(¥)'), operate: false}, + + {field: 'avg_time_diff', title: __('派单时效(小时)'), operate: false}, + + //{field: 'admin_user', title: __('派单员'),operate: "LIKE",visible:false}, + //{field: 'city_name', title: __('城市'),operate: "LIKE",visible:false}, + //{field: 'city_name', title: __('城市'),operate: "LIKE",visible:false}, + {field: 'time_by', title: __('时间维度'), visible:false,searchList: {"1":__('录单时间'),"2":__('派单时间')},defaultValue:1, formatter: Table.api.formatter.normal}, + {field: 'daterange', title: __('时间筛选'), addclass:'datetimerange', + autocomplete:false, + operate: "RANGE", + datetimeFormat: "YYYY-MM-DD", + //defaultValue:today()+' - '+today(), + data:'autocomplete="off" data-local={"format":"YYYY-MM-DD"}', + visible:false, + defaultValue: Config.default_daterange + }, + {field: 'operate', title: __('Operate'), table: table2, events: Table.api.events.operate, formatter: Table.api.formatter.operate, + buttons: [ + { + name: 'aftersales', + text:"售后列表", + title:"售后列表", + icon: 'fa fa-list', + url: function(row){ + return 'aftersales/aftersale/index?from=2&man_id='+row.id; + }, + extend: 'data-toggle="tooltip" data-container="body"', + classname: 'btn btn-xs btn-default btn-dialog', + visible:function(row){ + return true; + } + }, + ] + } + ] + ] + }); + // 为表格2绑定事件 + Table.api.bindevent(table2); + } + + }); + + + + // 触发 tab 后发起 ajax 获取图表数据 + $('ul.nav-tabs li.active a[data-toggle="tab"]').on("shown.bs.tab", function () { + getChartData(); + }); + + + function getChartData(){ + // 获取单选框选中的值 + var timeBy = $('input[name="filter[time_by]"]:checked').val(); + + // 获取日期范围值 + var daterange = $('#daterange').val(); + + // 构建查询参数 + var params = { + 'time_by': timeBy, + 'daterange': daterange + }; + + $.ajax({ + url: "statistics/item/chartData", // + type: "POST", + dataType: "json", + data:params, + success: function (response) { + + }, + error: function () { + console.error("图表数据加载失败"); + } + }); + } + + + + + var form = $("#chart-filter"); + var ranges = {}; + ranges[__('Today')] = [Moment().startOf('day'), Moment().endOf('day')]; + ranges[__('Yesterday')] = [Moment().subtract(1, 'days').startOf('day'), Moment().subtract(1, 'days').endOf('day')]; + ranges[__('Last 7 Days')] = [Moment().subtract(6, 'days').startOf('day'), Moment().endOf('day')]; + ranges[__('Last 30 Days')] = [Moment().subtract(29, 'days').startOf('day'), Moment().endOf('day')]; + ranges[__('This Month')] = [Moment().startOf('month'), Moment().endOf('month')]; + ranges[__('Last Month')] = [Moment().subtract(1, 'month').startOf('month'), Moment().subtract(1, 'month').endOf('month')]; + ranges[__('今年')] = [Moment().startOf('year'), Moment().endOf('year')]; + var options = { + timePicker: false, + autoUpdateInput: false, + timePickerSeconds: true, + timePicker24Hour: true, + autoApply: true, + locale: { + format: 'YYYY-MM-DD', + customRangeLabel: __("Custom Range"), + applyLabel: __("Apply"), + cancelLabel: __("Clear"), + }, + ranges: ranges, + }; + var callback = function (start, end) { + $(this.element).val(start.format(options.locale.format) + " - " + end.format(options.locale.format)); + }; + require(['bootstrap-daterangepicker'], function () { + $(".datetimerange", form).each(function () { + $(this).on('apply.daterangepicker', function (ev, picker) { + callback.call(picker, picker.startDate, picker.endDate); + var label = picker.chosenLabel; + $(picker.element).data('label', label).trigger("change"); + }); + $(this).on('cancel.daterangepicker', function (ev, picker) { + $(this).val(''); + }); + $(this).daterangepicker($.extend({}, options), callback); + }); + }); + + // 手动触发一次激活 tab 的 shown.bs.tab + getChartData(); + // 绑定查询按钮的点击事件 + $('#filter-btn').on('click', function() { + getChartData(); + }); + }, + + add: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + + aftersales: function () { + Controller.api.bindevent(); + }, + + api: { + bindevent: function () { + Form.api.bindevent($("form[role=form]")); + } + } + }; + return Controller; +}); From 83dba34c3eb895b1017d02ceec03f43c35d62785 Mon Sep 17 00:00:00 2001 From: hant Date: Mon, 12 May 2025 22:10:13 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/statistics/Item.php | 143 ++++++++++++++++-- .../admin/view/statistics/item/index.html | 12 +- public/assets/js/backend/statistics/item.js | 102 ++++++++----- 3 files changed, 194 insertions(+), 63 deletions(-) diff --git a/application/admin/controller/statistics/Item.php b/application/admin/controller/statistics/Item.php index 103475d..437cb35 100644 --- a/application/admin/controller/statistics/Item.php +++ b/application/admin/controller/statistics/Item.php @@ -13,6 +13,7 @@ use think\Collection; use think\Exception; use think\exception\DbException; use think\Loader; +use think\Model; use think\response\Json; use function Symfony\Component\Clock\now; @@ -25,7 +26,6 @@ class Item extends Backend { - protected $relationSearch = true; public function _initialize() @@ -38,14 +38,15 @@ class Item extends Backend return $this->fetch('index'); } - public function list(){ - + public function list() + { $build = new Order(); $start = now()->modify('-14 days')->format('Y-m-d'); $end_at = now()->format('Y-m-d 23:29:59'); - $filter ['daterange'] = request()->get('daterange'); + $filter = json_decode(request()->get('filter','[]'),true); + if (!empty($filter['daterange'])) { $arr = explode(' - ', $filter['daterange']); if (trim($arr[0])) { @@ -55,18 +56,134 @@ class Item extends Backend $end_at = trim($arr[1]) . ' 23:29:59'; } } - $build->field([ - 'item_title', - 'sum(total) total', - 'count(id) count', - 'sum(performance) performance' - ])->group('item_title'); + $offset = request()->get('offset',0); + $limit = request()->get('limit',15); + $build->whereBetween('create_time', [$start, $end_at]) + ->field([ + 'item_title name', // 类型 + 'sum(total) total', // 营业额 + 'count(id) count_num', // 单量 + 'count(if(status=60,1,null)) finish_num', // 单量 + 'sum(performance) performance', // 收益 + 'sum(cost) cost', // 收益 + 'sum(material_cost) material_cost', // 收益 + 'sum(refund_amount) refund_amount', // 公司退款 + 'sum(worker_refund_amount) worker_refund_amount', // 工人退款 + ])->group('item_title') + ->order('count_num', 'desc'); +// dd($total); + $res = $build->paginate(); + $total = $res->total(); + $ress = $res->items(); + $data = []; +// dd($res); + foreach ($ress as $re) { + $re = $re->toArray(); +// dd($re); + $cost_total = $re['cost'] + $re['material_cost']; + $name = explode('/', $re['name']); + $re['name'] = str_replace(' ', '', array_pop($name)); + $re['performance_rate'] = $this->mydiv($re['performance'],$re['total']); + $re['trans_rate'] = $this->mydiv($re['finish_num'],$re['count_num']); + $re['cash_value'] = $this->mydiv($re['performance'],$re['count_num']); + $re['total_avg'] = $this->mydiv($re['total'],$re['count_num'],2,false); + $re['performance_avg'] = $this->mydiv($re['performance'],$re['finish_num'],2,false); + + $re['cost_total'] = $cost_total; +// $re['id'] = $re['item_id']; + $data [] = $re; + } + + return [ + 'rows' => $data, + 'total' => $total + ]; - return []; } - public function chartData(){ - return []; + public function chartData() + { + $build = new Order(); + + $start = now()->modify('-14 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + + $filter ['daterange'] = request()->post('daterange'); + + if (!empty($filter['daterange'])) { + $arr = explode(' - ', $filter['daterange']); + if (trim($arr[0])) { + $start = trim($arr[0]); + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + + $res = $build->whereBetween('create_time', [$start, $end_at]) + ->where('status', Order::STATUS_FINISHED) + ->field([ + 'item_title name', // 类型 + 'sum(total) total', // 营业额 + 'count(id) count', // 单量 + 'sum(performance) performance', // 收益 + 'sum(refund_amount) refund_amount', // 公司退款 + 'sum(worker_refund_amount) worker_refund_amount', // 工人退款 + ])->group('item_title') + ->order('count', 'desc')->select(); + $data = []; + foreach ($res as $re) { + $re = $re->getData(); + $name = explode('/', $re['name']); + $re['name'] = str_replace(' ', '', array_pop($name)); + $data [] = $re; + } + + $xAxis = []; + $totalPerformance = []; + $conversionRate = []; // 假设转化率 = performance / total(或自定义逻辑) + $profitRate = []; // 假设利润率 = (performance - worker_refund_amount) / performance + $refundRate = []; // 假设退款率 = refund_amount / total + $monetizedValue = []; // 假设变现值 = performance - refund_amount + + foreach ($data as $item) { + $name = $item['name']; + $total = (float)$item['total']; + $performance = (float)$item['performance']; + $refund = (float)($item['refund_amount'] + $item['worker_refund_amount']); + $workerRefund = (float)$item['worker_refund_amount']; + $count = max((int)$item['count'], 1); // 避免除以0 + + $xAxis[] = $name; + $totalPerformance[] = $total; + $conversionRate[] = $total > 0 ? round($performance / $total * 100, 2) : 0; + $profitRate[] = $total > 0 ? round(($total - $workerRefund) / $total * 100, 2) : 0; + $refundRate[] = $total > 0 ? round($refund / $total * 100, 2) : 0; + $monetizedValue[] = $total / $count; + } + $result = [ + 'xAxis' => $xAxis, + 'series' => [ + ['name' => '总业绩(¥)', 'type' => 'bar', 'yAxisIndex' => 0, 'data' => $totalPerformance], + ['name' => '转化率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $conversionRate], + ['name' => '利润率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $profitRate], + ['name' => '退款率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => $refundRate], + ['name' => '变现值(¥)', 'type' => 'line', 'yAxisIndex' => 0, 'data' => $monetizedValue], + ] + ]; + + return $result; + } + + private function mydiv($a, $b, int $scale = 4, $is_percent = true): int|string + { + $val = $b > 0 ? bcdiv($a, $b, $scale) : 0; + + if ($is_percent) { + + return bcmul($val, 100, 2); + } + return $val; } } diff --git a/application/admin/view/statistics/item/index.html b/application/admin/view/statistics/item/index.html index b74f52d..749ac6e 100644 --- a/application/admin/view/statistics/item/index.html +++ b/application/admin/view/statistics/item/index.html @@ -24,19 +24,9 @@
- -
- - -
-
- +
diff --git a/public/assets/js/backend/statistics/item.js b/public/assets/js/backend/statistics/item.js index 409a941..97349e2 100644 --- a/public/assets/js/backend/statistics/item.js +++ b/public/assets/js/backend/statistics/item.js @@ -1,4 +1,4 @@ -define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-theme', 'template','addtabs','moment'], function ($, undefined, Backend, Table, Form,Echarts,undefined,Template,Datatable,Moment) { +define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-theme', 'template','addtabs','moment'], function ($, undefined, Backend, Table, Form,echarts,undefined,Template,Datatable,Moment) { var Controller = { @@ -28,29 +28,28 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t searchFormVisible:true, columns: [ [ - //{field: 'id', title: __('Id')}, - {field: 'id', title: __('ID'),visible:true,operate: false}, - {field: 'admin_user', title: __('派单员'),operate: "LIKE"}, - {field: 'count_num', title: __('总订单数'),operate: false}, - {field: 'finish_num', title: __('完单数'),operate: false}, + // {field: 'id', title: __('ID'),visible:true,operate: false}, + {field: 'name', title: '项目类型',operate: false}, {field: 'total', title: __('成效额(¥)'), operate: false}, {field: 'performance', title: __('总业绩(¥)'), operate: false}, - {field: 'cost_total', title: __('总成本(¥)'), operate: false}, - {field: 'refund_total', title: __('退款金额(¥)'), operate: false}, - {field: 'refund_count', title: __('退款单数'), operate: false}, + {field: 'count_num', title: __('总订单数'),operate: false}, {field: 'performance_rate', title: __('利润率(%)'), operate: false}, + {field: 'trans_rate', title: __('转化率(%)'), operate: false}, {field: 'cash_value', title: __('变现值'), operate: false}, + {field: 'performance_avg', title: __('客单利润(¥)'), operate: false}, {field: 'total_avg', title: __('客单价(¥)'), operate: false}, {field: 'avg_time_diff', title: __('派单时效(小时)'), operate: false}, - //{field: 'admin_user', title: __('派单员'),operate: "LIKE",visible:false}, - //{field: 'city_name', title: __('城市'),operate: "LIKE",visible:false}, - //{field: 'city_name', title: __('城市'),operate: "LIKE",visible:false}, - {field: 'time_by', title: __('时间维度'), visible:false,searchList: {"1":__('录单时间'),"2":__('派单时间')},defaultValue:1, formatter: Table.api.formatter.normal}, + {field: 'cost_total', title: __('总成本(¥)'), operate: false}, + {field: 'finish_num', title: __('完单数'),operate: false}, + + {field: 'refund_count', title: __('退款单数'), operate: false}, + {field: 'refund_total', title: __('退款金额(¥)'), operate: false}, + {field: 'daterange', title: __('时间筛选'), addclass:'datetimerange', autocomplete:false, operate: "RANGE", @@ -60,24 +59,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t visible:false, defaultValue: Config.default_daterange }, - {field: 'operate', title: __('Operate'), table: table2, events: Table.api.events.operate, formatter: Table.api.formatter.operate, - buttons: [ - { - name: 'aftersales', - text:"售后列表", - title:"售后列表", - icon: 'fa fa-list', - url: function(row){ - return 'aftersales/aftersale/index?from=2&man_id='+row.id; - }, - extend: 'data-toggle="tooltip" data-container="body"', - classname: 'btn btn-xs btn-default btn-dialog', - visible:function(row){ - return true; - } - }, - ] - } + // {field: 'operate', title: __('Operate'), table: table2, events: + // Table.api.events.operate, + // formatter: Table.api.formatter.operate, + // buttons: [ + // { + // name: 'aftersales', + // text:"售后列表", + // title:"售后列表", + // icon: 'fa fa-list', + // url: function(row){ + // return 'aftersales/aftersale/index?from=2&man_id='+row.id; + // }, + // extend: 'data-toggle="tooltip" data-container="body"', + // classname: 'btn btn-xs btn-default btn-dialog', + // visible:function(row){ + // return true; + // } + // }, + // ] + // } ] ] }); @@ -87,8 +88,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t }); - - // 触发 tab 后发起 ajax 获取图表数据 $('ul.nav-tabs li.active a[data-toggle="tab"]').on("shown.bs.tab", function () { getChartData(); @@ -96,16 +95,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t function getChartData(){ - // 获取单选框选中的值 - var timeBy = $('input[name="filter[time_by]"]:checked').val(); - // 获取日期范围值 var daterange = $('#daterange').val(); // 构建查询参数 var params = { - 'time_by': timeBy, - 'daterange': daterange + 'daterange': daterange, }; $.ajax({ @@ -114,7 +109,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t dataType: "json", data:params, success: function (response) { - + Controller.api.chart(response); }, error: function () { console.error("图表数据加载失败"); @@ -187,6 +182,35 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t api: { bindevent: function () { Form.api.bindevent($("form[role=form]")); + }, + chart(chartData){ + const option = { + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' } + }, + legend: { + data: chartData.series.map(s => s.name) + }, + xAxis: { + type: 'category', + data: chartData.xAxis + }, + yAxis: [ + { type: 'value', name: '金额 (¥)' }, + { type: 'value', name: '比率 (%)' } + ], + series: chartData.series + }; + +// 初始化图表 + const myChart = echarts.init(document.getElementById('bar-chart')); + myChart.setOption(option); + + // 监听窗口大小变化,自动重新绘制图表 + window.addEventListener('resize', function() { + myChart.resize(); + }); } } };