Accept Merge Request #47: (feature/hant -> develop)
Merge Request: 面板 Created By: @todayswind Accepted By: @todayswind URL: https://g-bcrc3009.coding.net/p/allocatr/d/allocatr/git/merge/47?initial=true
This commit is contained in:
commit
7320830616
|
|
@ -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',
|
||||
|
|
|
|||
189
application/admin/controller/statistics/Item.php
Normal file
189
application/admin/controller/statistics/Item.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace app\admin\controller\statistics;
|
||||
|
||||
use app\admin\model\Admin;
|
||||
use app\admin\model\Aftersale;
|
||||
use app\admin\model\Order;
|
||||
use app\admin\model\OrderDispatch;
|
||||
use app\admin\model\OrderReview;
|
||||
use app\common\controller\Backend;
|
||||
use PDOStatement;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 服务项目统计
|
||||
*
|
||||
* @icon fa fa-circle-o
|
||||
*/
|
||||
class Item extends Backend
|
||||
{
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->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 = json_decode(request()->get('filter','[]'),true);
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
$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
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
51
application/admin/view/statistics/item/index.html
Normal file
51
application/admin/view/statistics/item/index.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<div class="panel panel-default panel-intro">
|
||||
<div class="panel-heading">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a data-val="first" href="#first" data-toggle="tab">统计图表</a></li>
|
||||
<li><a href="#second" data-val="second" data-toggle="tab">统计列表</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel-body">
|
||||
<div id="myTabContent" class="tab-content">
|
||||
<div class="tab-pane fade active in" id="first">
|
||||
<section class="connectedSortable">
|
||||
|
||||
<div class="nav-tabs-custom charts-custom">
|
||||
<!-- Tabs within a box -->
|
||||
<ul class="nav nav-tabs pull-right">
|
||||
<li class="active"><a href="#bar-chart" data-toggle="tab">柱状图</a></li>
|
||||
<!-- <li><a href="#pie-chart" data-toggle="tab">饼图</a></li>-->
|
||||
|
||||
<li class="pull-left header"><i class="fa fa-inbox"></i> 服务项目</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<div class="form-inline" id="chart-filter" style="margin-top:20px;margin-bottom: 30px;">
|
||||
<!-- 时间范围 -->
|
||||
<div class="form-group" style="margin-left: 15px;">
|
||||
<input type="text" class="form-control datetimerange" data-locale='{"format":"YYYY-MM-DD"}' placeholder="指定日期" name="filter[daterange]" id="daterange" autocomplete="off" style="width: 200px;">
|
||||
</div>
|
||||
|
||||
<!-- 查询按钮 -->
|
||||
<button class="btn btn-default" id="filter-btn" style="margin-left: 15px;">查询</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-content no-padding">
|
||||
<div class="chart tab-pane active" id="bar-chart" style="width: 100%; height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="second">
|
||||
<table id="table2" class="table table-striped table-bordered table-hover">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
218
public/assets/js/backend/statistics/item.js
Normal file
218
public/assets/js/backend/statistics/item.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
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'),visible:true,operate: false},
|
||||
{field: 'name', title: '项目类型',operate: false},
|
||||
{field: 'total', title: __('成效额(¥)'), operate: false},
|
||||
{field: 'performance', 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: '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",
|
||||
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 daterange = $('#daterange').val();
|
||||
|
||||
// 构建查询参数
|
||||
var params = {
|
||||
'daterange': daterange,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: "statistics/item/chartData", //
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data:params,
|
||||
success: function (response) {
|
||||
Controller.api.chart(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]"));
|
||||
},
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return Controller;
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user