From 1ab27a7bb825c058e60a581adb8b04596bd25e1b Mon Sep 17 00:00:00 2001 From: hant Date: Tue, 27 May 2025 00:03:42 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=80=E6=AC=BE=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/statistics/Aftersale.php | 195 +++++++++- .../admin/controller/statistics/Item.php | 2 - application/admin/view/order/add.html | 353 +++++++++--------- .../view/statistics/aftersale/index.html | 43 ++- .../assets/js/backend/statistics/aftersale.js | 102 ++++- public/assets/js/cascader.js | 196 ++++++++-- 6 files changed, 655 insertions(+), 236 deletions(-) diff --git a/application/admin/controller/statistics/Aftersale.php b/application/admin/controller/statistics/Aftersale.php index e643a25..9163f45 100644 --- a/application/admin/controller/statistics/Aftersale.php +++ b/application/admin/controller/statistics/Aftersale.php @@ -6,6 +6,7 @@ namespace app\admin\controller\statistics; use app\admin\model\Admin; use app\admin\model\Order; use app\common\controller\Backend; +use think\Db; use think\Model; use function Symfony\Component\Clock\now; @@ -17,10 +18,23 @@ use function Symfony\Component\Clock\now; class Aftersale extends Backend { - protected $relationSearch = true; + protected $relationSearch = true,$items,$itemsformattedTree; public function _initialize() { + + $items = Db::name('item') + ->where('status', 1) + ->field(['id', 'title', 'key_word', 'pid']) + ->order('pid', 'asc') + ->order('sort', 'desc') + ->select(); + $tree = $this->buildTree($items); + $formattedTree = $this->formatTree($tree); + + $this->items = $items; + $this->itemsformattedTree = $formattedTree; + $this->view->assign("items", $formattedTree); parent::_initialize(); } @@ -39,15 +53,45 @@ class Aftersale extends Backend { $build = new Admin(); $build->alias('a') - ->join('order b','a.id = b.admin_id','right') - ->join('aftersale c','b.id = c.admin_id','left') - ->where('b.status',Order::STATUS_FINISHED); + ->join('order b', 'a.id = b.admin_id', 'right') + ->join('aftersale c', 'b.id = c.admin_id', 'left') + ->where('b.status', Order::STATUS_FINISHED); + + + $start = now()->modify('-30 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + + + $filter = request()->get('range', ''); + if (!empty($filter)) { + $arr = explode(' - ', $filter); + if (trim($arr[0])) { + $start = trim($arr[0]); + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + $keyword = request()->get('keyword'); + + + $build->where('audit_time', '>=', $start); + $build->where('audit_time', '<=', $end_at); + + if (!is_null($keyword)) { + $build->where(function ($q) use ($keyword) { + $q->where('nickname', 'like', '%' . $keyword . '%') + ->whereor('mobile', 'like', '%' . $keyword . '%'); + }); + } + $build->group('a.id'); $build->field( [ 'a.id', 'a.nickname', + 'a.mobile', 'count(b.id) order_total', 'count(c.id) after_total', ] @@ -57,31 +101,146 @@ class Aftersale extends Backend $total = $res->total(); $ress = $res->items(); // dd(Admin::getLastSql()); + $data = []; + foreach ($ress as $res) { + $item = $res->toArray(); + $item['rate'] = $this->mydiv($item['after_total'],$item['order_total']) . '%'; + $data [] = $item; + } return [ - 'rows' => $ress, - 'total' => $total - ]; + 'rows' => $data, + 'total' => $total + ]; + + + } + public function city() + { + $build = new Order(); + $build->alias('a') + ->join('aftersale c', 'a.id = c.order_id', 'left') + ->where('a.status', Order::STATUS_FINISHED); + + $start = now()->modify('-30 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + $area_code = request()->get('area_id'); + $filter = request()->get('range', ''); + if (!empty($filter)) { + $arr = explode(' - ', $filter); + if (trim($arr[0])) { + $start = trim($arr[0]); + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + $build->where('audit_time', '>=', $start); + $build->where('audit_time', '<=', $end_at); + $build->group('a.area_id'); + + + if ($area_code) { + $build->where('area_id', 'like', $this->getSelectAreaCode($area_code) . '%'); + } + + $build->field( + [ + 'a.area_id', + 'count(a.id) order_total', + 'count(c.id) after_total', + ] + )->with([ + 'area' => function ($q) { + $q->field('id,area_code,merge_name'); + } + ]); + + $res = $build->paginate(); + $total = $res->total(); + $ress = $res->items(); +// dd(Admin::getLastSql()); + $data = []; + foreach ($ress as $res) { + $item = $res->toArray(); + $item['rate'] = $this->mydiv($item['after_total'],$item['order_total']) . '%'; + $data [] = $item; + } + return [ + 'rows' => $data, + 'total' => $total + ]; + } + + public function item() + { + $build = new Order(); + $build->alias('a') + ->join('aftersale c', 'a.id = c.order_id', 'left') + ->where('a.status', Order::STATUS_FINISHED); + + $start = now()->modify('-30 days')->format('Y-m-d'); + $end_at = now()->format('Y-m-d 23:29:59'); + $area_code = request()->get('area_id'); + $filter = request()->get('range', ''); + if (!empty($filter)) { + $arr = explode(' - ', $filter); + if (trim($arr[0])) { + $start = trim($arr[0]); + } + if (trim($arr[1])) { + $end_at = trim($arr[1]) . ' 23:29:59'; + } + } + $build->where('audit_time', '>=', $start); + $build->where('audit_time', '<=', $end_at); + $build->group('a.item_title'); + + + if ($area_code) { + $build->where('area_id', 'like', $this->getSelectAreaCode($area_code) . '%'); + } + + $build->field( + [ + 'a.item_title', + 'count(a.id) order_total', + 'count(c.id) after_total', + ] + ); + + $res = $build->paginate(); + $total = $res->total(); + $ress = $res->items(); +// dd(Admin::getLastSql()); + $data = []; + foreach ($ress as $res) { + $item = $res->toArray(); + $item['rate'] = $this->mydiv($item['after_total'],$item['order_total']) . '%'; + $data [] = $item; + } + return [ + 'rows' => $data, + 'total' => $total + ]; + } + 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/controller/statistics/Item.php b/application/admin/controller/statistics/Item.php index bb266f6..641a550 100644 --- a/application/admin/controller/statistics/Item.php +++ b/application/admin/controller/statistics/Item.php @@ -206,8 +206,6 @@ class Item extends Backend ]; } - - return $result; } diff --git a/application/admin/view/order/add.html b/application/admin/view/order/add.html index cc297f1..7da2239 100644 --- a/application/admin/view/order/add.html +++ b/application/admin/view/order/add.html @@ -7,189 +7,193 @@
-
- -
- +
+
+ +
+ +
-
-
- -
- +
+ +
+ +
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
- - -
- -
- -
-
- -
- -
- -
-
- - -
- -
+
+ +
-
-
-
- -
- -
地图查找
- - - -
-
- - - - - - - - - - - - - -
- -
- -
-
- -
- -
- -
-
-
- -
- - - -
-
- -
- -
- -
-
-
- -
- - -
-
-
- -
-
- -
- - -
-
-
    +
    +
    + +
    +
    + +
    地图查找
    +
    + + + +
    +
    + +
    + +
    + + + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + + +
    + +
    +
      +
      -
      @@ -202,6 +206,9 @@ width: 100vw; background: #fff; } + .myform-group{ + display: flex; + } .overlay { position: absolute; top: 0; @@ -227,6 +234,8 @@ display: flex; justify-content: center; align-items: flex-start; + height: 100vh; + overflow-y: auto; } .function-area { flex: 1; @@ -254,7 +263,7 @@ background-color: #f0f0f0; } .control-label{ - text-align: right; + text-align: left; } .col-xs-12{ margin: 5px 0; diff --git a/application/admin/view/statistics/aftersale/index.html b/application/admin/view/statistics/aftersale/index.html index 6ea7e51..d436f76 100644 --- a/application/admin/view/statistics/aftersale/index.html +++ b/application/admin/view/statistics/aftersale/index.html @@ -175,26 +175,57 @@
      -
      -
      +
      + +
      - -
      +
      + +
      +
      + +
      + + +
      + + + +
      + + +
      +
      + +
      +
      + + + + + +
      + + +
      +
      @@ -210,3 +241,7 @@
      + + \ No newline at end of file diff --git a/public/assets/js/backend/statistics/aftersale.js b/public/assets/js/backend/statistics/aftersale.js index 1d9ccca..a2b8ae8 100644 --- a/public/assets/js/backend/statistics/aftersale.js +++ b/public/assets/js/backend/statistics/aftersale.js @@ -1,4 +1,5 @@ -define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-theme', 'template', 'addtabs', 'moment','citypicker'], function ($, undefined, Backend, Table, Form, echarts, undefined, Template, Datatable, Moment) { +define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template', 'addtabs', 'moment','citypicker','cascader'], function + ($, undefined, Backend, Table, Form,Template,undefined, Moment) { var Controller = { @@ -10,7 +11,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t if (tabVal === 'first') { Controller.api.first(); }else if (tabVal === 'second') { + console.log(3333); Controller.api.second(); + }else if (tabVal === 'third'){ + Controller.api.third(); } }); Controller.api.first(); @@ -37,11 +41,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t searchFormVisible: true, columns: [ [ - {field: 'nickname', title: '项目类型', operate: false}, + {field: 'nickname', title: '名称', operate: false}, + {field: 'mobile', title: '电话', operate: false}, {field: 'order_total', title: '订单总数', operate: false}, {field: 'after_total', title: '退款订单数', operate: false}, - {field: 'after_total', title: '退款率', operate: false}, - + {field: 'rate', title: '退款率', operate: false}, ] ] }); @@ -51,10 +55,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t $('#first-search').on('click', function () { const range = $('#daterange-table').val(); + const keyword = $('#keyword').val(); let data = ''; if (range !== ''){ data += 'range=' + range; } + if (keyword !== ''){ + data += '&keyword=' + keyword; + } // data = encodeURIComponent(data); $("#table1").bootstrapTable('refresh',{ url:'statistics/aftersale/dispatch?' + data, @@ -67,7 +75,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t // 表格2 var table = $("#table2"); table.bootstrapTable({ - url: 'statistics/aftersale/dispatch', + url: 'statistics/aftersale/city', sortName: 'id', search: false, commonSearch: false, @@ -78,10 +86,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t searchFormVisible: true, columns: [ [ - {field: 'nickname', title: '项目类型', operate: false}, + {field: 'area.merge_name', title: '城市', operate: false}, {field: 'order_total', title: '订单总数', operate: false}, {field: 'after_total', title: '退款订单数', operate: false}, - {field: 'after_total', title: '退款率', operate: false}, + {field: 'rate', title: '退款率', operate: false}, ] ] @@ -89,19 +97,70 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t // 为表格2绑定事件 Table.api.bindevent(table); - $('#first-search').on('click', function () { + $('#first-search2').on('click', function () { - const range = $('#daterange-table').val(); + const range = $('#daterange-table2').val(); + const area_id = $('#area_id').val(); let data = ''; if (range !== ''){ data += 'range=' + range; } + if (area_id !== ''){ + data += 'area_id=' + area_id; + } // data = encodeURIComponent(data); - $("#table1").bootstrapTable('refresh',{ - url:'statistics/aftersale/dispatch?' + data, + $("#table2").bootstrapTable('refresh',{ + url:'statistics/aftersale/city?' + data, }); }); + Controller.api.areapicker(); + + }, + + third: function (){ + Table.api.init(); + // 表格2 + var table = $("#table3"); + table.bootstrapTable({ + url: 'statistics/aftersale/item', + sortName: 'id', + search: false, + commonSearch: false, + visible: false, + showToggle: false, + showColumns: false, + showExport: true, + searchFormVisible: true, + columns: [ + [ + {field: 'item_title', title: '服务项目', operate: false}, + {field: 'order_total', title: '订单总数', operate: false}, + {field: 'after_total', title: '退款订单数', operate: false}, + {field: 'rate', title: '退款率', operate: false}, + ] + ] + }); + // 为表格2绑定事件 + Table.api.bindevent(table); + + $('#first-search3').on('click', function () { + + const range = $('#daterange-table3').val(); + const item_id = $('#item_id_value').val(); + let data = ''; + if (range !== ''){ + data += 'range=' + range; + } + if (item_id !== ''){ + data += 'item_id=' + item_id; + } + // data = encodeURIComponent(data); + $("#table3").bootstrapTable('refresh',{ + url:'statistics/aftersale/item?' + data, + }); + }); + Controller.api.itemspicker(); }, datepicker: function () { var ranges = {}; @@ -143,6 +202,27 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t }); }); }, + + areapicker: function () { + $("#city-search").citypicker(); + + $("#city-search").on("cp:updated", function() { + var citypicker = $(this).data("citypicker"); + var code = citypicker.getCode("district") || citypicker.getCode("city") || citypicker.getCode("province"); + // table.bootstrapTable('refresh',{query: {area_code: code}}); + $('#area_id').val(code); + }); + }, + itemspicker: function () { + var _data = items; + $('#item_id').zdCascader({ + data: _data, + onChange: function ($this, data, allPathData) { + // console.log(data,allPathData); + $('#item_id_value').val(data.value); + } + }); + }, } }; return Controller; diff --git a/public/assets/js/cascader.js b/public/assets/js/cascader.js index 6a496a0..dc09116 100644 --- a/public/assets/js/cascader.js +++ b/public/assets/js/cascader.js @@ -19,6 +19,7 @@ function ZdCascader(el, options) { this.options = options; + if (options.search) this.getLabelList(); this.CLASS = ZdCascader.CLASS; this.$el = $(el); //input this.$el_ = this.$el.clone(); @@ -49,8 +50,8 @@ ZdCascader.DEFAULTS = { data: null, //支持格式[{value:"",label:"",children:[{value:"",label:""}]}] range: ' / ', //分割符 - onChange: function (data) {}, - defaultValue: null // 新增 defaultValue 属性 + search: true, //搜索 + onChange: function (data) {} } ZdCascader.METHODS = ['reload', 'destroy']; @@ -68,8 +69,7 @@ var self = this; //最外层容器 this.$container = this.$el.wrap(`
      `) - .wrap(`
      `).addClass(this.CLASS.input).prop('readonly', true) - .closest('.' + this.CLASS.wrap); + .wrap(`
      `).addClass(this.CLASS.input).prop('readonly', !this.options.search).closest('.' + this.CLASS.wrap); //文本框右侧图标 this.$arrow = $(` @@ -85,7 +85,6 @@ `).insertAfter(this.$el); - //下拉列表 this.$dropdownWrap = $(`
      `).appendTo(this.$container).wrap(`
      `); @@ -107,9 +106,14 @@ this.$container.removeClass(this.CLASS.checkClass.wrapFocus); }, this)); - this.$container.on('click.item', '.' + this.CLASS.menuNode, $.proxy(this._nodeClick, this)); + this.$container.on('click.item', '.' + this.CLASS.menuNode, $.proxy(this._nodeClick, this)) + .on('dblclick.item', '.' + this.CLASS.menuNode, $.proxy(this._nodeDoubleClick, this)); this.$el.on('keyup.wrap', $.proxy(this._keyup, this)); + + this.$el.on('input', $.proxy(function (event) { + this.search(this.$el.val()) + }, this)); } ZdCascader.prototype._wrapClick = function () { event.stopPropagation(); @@ -147,13 +151,38 @@ $that.prepend($(``)); this.$el.data('bindData', data); this.$el.data('bindPathData', allPathData); - console.log(allPathData); if (this.options.onChange && typeof this.options.onChange === "function") this.options.onChange(this, data, allPathData); event.stopPropagation(); } else this._loadChildren($that); } + ZdCascader.prototype._nodeDoubleClick = function (event) { + var $that = event.currentTarget ? $(event.currentTarget) : $(event); // li + var $wrap = $that.closest('.' + this.CLASS.menuWrap); + $that.addClass(this.CLASS.checkClass.menuNodeSelected).siblings().removeClass(this.CLASS.checkClass.menuNodeSelected); + var data = $that.data('bindData'); + $wrap.nextAll().remove(); // 移除所有后续级别 + var prevWrap = $wrap.prevAll(); + var value = data.label; + var allPathData = [data]; + $.each(prevWrap, (i, m) => { + var selectedData = $(m).find('li.' + this.CLASS.checkClass.menuNodeSelected).data('bindData'); + value = selectedData.label + this.options.range + value; + allPathData.push(selectedData); + }); + this.$el.val(value).focus(); + this.$container.removeClass(this.CLASS.checkClass.wrapFocus); + this.$dropdownWrap.find('.' + this.CLASS.checkClass.nodeSelectedIcon).remove(); + $that.prepend($(``)); + this.$el.data('bindData', data); + this.$el.data('bindPathData', allPathData); + if (this.options.onChange && typeof this.options.onChange === "function") { + this.options.onChange(this, data, allPathData); + } + event.stopPropagation(); + }; + ZdCascader.prototype._loadChildren = function ($parentNode) { this.$el.focus(); $parentNode.addClass(this.CLASS.checkClass.menuNodeSelected).siblings().removeClass(this.CLASS.checkClass.menuNodeSelected); @@ -214,12 +243,105 @@ $(this.$el).insertAfter(this.$el_); this.$el.remove(); } + //获取名称列表 + ZdCascader.prototype.getLabelList = function () { + var datas = []; + this.options.data.forEach(function(prov) { + if (prov.children) { + prov.children.forEach(function(city) { + if (city.children) { + city.children.forEach(function(area) { + datas.push({ + label: `${prov.label} / ${city.label} / ${area.label}`, + labels: [prov.label, city.label, area.label], + value: [prov.value, city.value, area.value], + }) + }) + } else { + datas.push({ + label: `${prov.label} / ${city.label}`, + labels: [prov.label, city.label], + value: [prov.value, city.value], + }) + } + }) + } else { + datas.push({ + label: prov.label, + labels: [prov.label], + value: prov.value, + }) + } + }) + this.labelList = datas + } + //搜索 + ZdCascader.prototype.search = function (keyword) { + if (keyword) keyword = keyword.trim(); + if (!keyword) { + this.options.keyword = keyword; + this.reload(null, true) + return + }; + var keywords = keyword.replace(' ','').replace('/','').split('') + var data = this.labelList.filter(function(item) { + item.num = 0 + keywords.forEach(function(key) { + if (item.label.includes(key)) item.num++ + }) + if (item.label.includes(keyword)) item.num += 2 + return item.num>(keywords.length==1?0:1) + }).sort(function(a, b) { + return b.num - a.num + }).slice(0, 10) + this.reload(data, true) + } + //关键词筛选数据(暂不用) + ZdCascader.prototype.searchData = function (keyword) { + var data = []; + this.options.data.forEach(function(prov){ + if(prov.label.indexOf(keyword) >= 0){ + data.push(prov) + } else { + if(prov.children){ + var citys = [] + prov.children.forEach(function(city){ + if(city.label.indexOf(keyword) >= 0){ + citys.push(city) + } else { + if(city.children){ + var areas = [] + city.children.forEach(function(area){ + if(area.label.indexOf(keyword) >= 0){ + areas.push(area) + } + }) + if(areas.length > 0){ + citys.push({ + value: city.value, + label: city.label, + children: areas + }) + } + } + } + }) + if (citys.length > 0) { + data.push({ + label: prov.label, + value: prov.value, + children: citys + }) + } + } + } + }); + } //重新加载下拉数据 - ZdCascader.prototype.reload = function (data,clear = false) { + ZdCascader.prototype.reload = function (data, search) { data = data || this.options.data; - if (clear){ - this.$el.val('').removeData('bindData').removeData('bindPathData'); - } + this.$el.removeData('bindData').removeData('bindPathData'); + if (!search) this.$el.val(''); this.$dropdownWrap.empty(); var selectedData = this.$el.data('bindData'); var $firstWrap = $(`
      @@ -245,16 +367,16 @@ `); $li.append($label).data('bindData', m); if (m.children && m.children.length > 0) $li.append($icon); - else if (this.options.defaultValue && m.value == this.options.defaultValue) { + else if (selectedData && m.value == selectedData.value) { this.$dropdownWrap.find('.' + this.CLASS.checkClass.nodeSelectedIcon).remove(); $li.prepend($(``)); - this.$el.val(m.label); } $ul.append($li); }); - this.$dropdownWrap.append($firstWrap); + this.$dropdownWrap.find('li.' + this.CLASS.checkClass.nodeAnchor).removeClass(this.CLASS.checkClass.nodeAnchor); + this.$dropdownWrap.append($firstWrap).find(this.CLASS.menuNode).eq(0).focus().addClass(this.CLASS.checkClass + .nodeAnchor); } - ZdCascader.prototype._keyup = function (event) { var keycode = event.which; switch (keycode) { @@ -364,22 +486,38 @@ this.$el.focus(); } - $.fn.zdCascader = function (options) { - options = $.extend({}, ZdCascader.DEFAULTS, options); - return this.each(function () { - var $this = $(this); - var data = $this.data('zdCascader'); - if (!data) { - data = new ZdCascader(this, options); - $this.data('zdCascader', data); - } - if (typeof options === 'string') { - if (ZdCascader.METHODS.indexOf(options) > -1) { - data[options](); + $.fn.zdCascader = function (option) { + var value, + args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var $this = $(this), + data = $this.data('zdCascader'), + options = $.extend({}, ZdCascader.DEFAULTS, $this.data(), + typeof option === 'object' && option); + + if (typeof option === 'string') { + if ($.inArray(option, ZdCascader.METHODS) < 0) { + throw new Error("Unknown method: " + option); + } + + if (!data) { + return; + } + + value = data[option].apply(data, args); + + if (option === 'destroy') { + $this.removeData('zdCascader'); } } - }); - } + if (!data) { + $this.data('zdCascader', (data = new ZdCascader(this, options))); + } + }); + + return typeof value === 'undefined' ? this : value; + }; })(jQuery); \ No newline at end of file