diff --git a/application/admin/command/Test.php b/application/admin/command/Test.php index 3356e58..dabed0b 100644 --- a/application/admin/command/Test.php +++ b/application/admin/command/Test.php @@ -3,12 +3,26 @@ namespace app\admin\command; use app\admin\addresmart\Address; +use app\admin\controller\orders\DispatchLogic; +use app\admin\model\Order; +use app\admin\model\OrderDispatch; +use app\admin\model\OrderReview; +use app\admin\model\Worker; +use app\admin\model\WorkerItem; +use think\Collection; use think\console\Command; use think\console\Input; use think\console\Output; +use think\Db; +use think\exception\DbException; +use think\Hook; +use think\Model; +use function Symfony\Component\Clock\now; class Test extends Command { + + protected function configure() { $this->setName('test') @@ -17,10 +31,12 @@ class Test extends Command protected function execute(Input $input, Output $output) { - $string = '张三 13800138000 120113196808214821深圳市龙华区龙华街道1980科技文化产业园3栋317 地板'; - $r = Address::smart($string); - dd($r); + $order = (new Order())->find(110); + $id = (new DispatchLogic())->getMaxScoreWorker($order); + dd($id); } + + } diff --git a/application/admin/controller/Order.php b/application/admin/controller/Order.php index 8211365..978ac81 100644 --- a/application/admin/controller/Order.php +++ b/application/admin/controller/Order.php @@ -3,7 +3,7 @@ namespace app\admin\controller; use app\admin\addresmart\Address; -use app\admin\model\AuthGroupAccess; +use app\admin\controller\orders\DispatchLogic; use app\admin\model\order\Invoice; use app\admin\model\OrderDispatch; use app\admin\model\Worker; @@ -27,7 +27,6 @@ use function Symfony\Component\Clock\now; */ class Order extends Backend { - /** * Order模型对象 * @var \app\admin\model\Order @@ -347,24 +346,13 @@ class Order extends Backend private function autoDispatch($order) { - if ($order->dispatch_type != 2) { - return false; - } +// +// if ($order->dispatch_type != 2) { +// return false; +// } - $worker_ids = (new Worker())->where('area_id', $order->area_id) - ->where('status', 1) - ->field(['id', 'area_id', 'lng'], 'lat') - ->column('id'); - $worker_items_ids = (new WorkerItem()) - ->where('item_id', $order->item_id) - ->whereIn('worker_id', $worker_ids) - ->field(['worker_id'], 'lat') - ->column('worker_id'); - - $out_workers = array_intersect($worker_ids, $worker_items_ids); - - $worker_id = $out_workers[0] ?? false; + $worker_id = (new DispatchLogic())->getMaxScoreWorker($order); if (!$worker_id) { $order->dispatch_type = 1; @@ -389,7 +377,7 @@ class Order extends Backend $orderDispatch->allowField(true)->save($insert); $order->status = \app\admin\model\Order::STATUS_DISPATCHED; $order->dispatch_time = date('Y-m-d H:i:s'); - // $order->dispatch_admin_id = $this->auth->id; + // $order->dispatch_admin_id = $this->auth->id; $order->worker_id = $worker_id; $order->save(); diff --git a/application/admin/controller/orders/DispatchLogic.php b/application/admin/controller/orders/DispatchLogic.php new file mode 100644 index 0000000..a65dcd0 --- /dev/null +++ b/application/admin/controller/orders/DispatchLogic.php @@ -0,0 +1,310 @@ +lng,$order->lat]); + + $worker_ids = array_column($worker_info,'id'); + + + $worker_items_ids = (new WorkerItem()) + ->where('item_id', $order->item_id) + ->whereIn('worker_id', $worker_ids) + ->field(['worker_id']) + ->column('worker_id'); + + $out_worker_ids = array_intersect($worker_ids, $worker_items_ids); + + $worker_doings = (new OrderDispatch()) + ->whereIn('worker_id', $out_worker_ids) + ->group('worker_id') + ->field(['worker_id','count(if(status > 0 & status < 60,1,null)) count']) + ->select(); + + $worker_doing_map = []; + foreach ($worker_doings as $item){ + $worker_doing_map[$item->worker_id] = $item->toArray(); + } + + // 获取工人信息; + $worker_rate = $this->getWorkerRate($out_worker_ids); + + $worker_scores = []; + $worker_rate_map = array_column($worker_rate,null,'worker_id'); + + foreach ($worker_info as $item){ + if (in_array($item['id'],$out_worker_ids)){ + $worker_scores [] = [ + 'id' => $item['id'], + 'distance' => $item['distance'], +// 'star' => $item['star'], + 'doing' => $worker_doing_map[$item['id']]['count'] ?? 0, + 'status' => $worker_rate_map[$item['id']] ?? [ + 'arrive_rate' => 0, + 'refuse_rate' => 0, + 'trans_rate' => 0, + 'good_rate' => 0, + 'avg_time_diff' => 0, + ], + ]; + } + } + $worker_score_map = []; + + foreach ($worker_scores as $worker_score){ + $worker_score_map [$worker_score['id']] = $this->scoreWorker($worker_score); + } + + // 根据得分排序(从高到低) + arsort($worker_score_map); // 保持键名不变 + + // 取出第一个 key(就是得分最高的 ID) + return array_key_first($worker_score_map); + } + + + function scoreWorker($worker, $maxDistance = 10000, $requiredSkills = []) { + // 距离得分(越近分数越高) + $geoScore = max(0, 1 - ($worker['distance'] / $maxDistance)); // 范围 [0,1] + + // 技能得分(假设这个人不匹配) + $skillScore = count($requiredSkills) ? 0 : 1; + + // 当前状态得分(未完成订单越少越好) + $doing = $worker['doing']; + $statusScore = $doing >= 10 ? 0 : (10 - $doing) / 10; + + // 历史表现得分(各项 0~1) + $s = $worker['status']; + + $historyScore = ( + 0.4 * (floatval($s['good_rate']) / 100) + + 0.25 * (floatval($s['trans_rate']) / 100) + + 0.2 * (floatval($s['arrive_rate']) / 100) + + 0.1 * (1 - min(floatval($s['avg_time_diff']) / 10, 1)) + // 联系时间越短越好 + 0.05 * (1 - floatval($s['refuse_rate']) / 100) // 拒单率越低越好 + ); + + // 综合得分(加权) + $totalScore = + 0.3 * $geoScore + + 0.2 * $skillScore + + 0.2 * $statusScore + + 0.3 * $historyScore; + + return round($totalScore, 4); + } + + + + + private function getWorkerRate($worker_ids){ + + $filter = [ + 'start_time' => now()->modify('-31 days')->format('Y-m-d H:i:s'), + 'end_timne'=> now()->format('Y-m-d H:i:s') + ]; + + //派单表 + $dispatchSubsql = $this->dispatchSubsql($filter); + //订单表 + $orderSubsql = $this->oderSubsql($filter); + //评分表 + $viewSubsql = $this->viewSubsql($filter); + + $list = (new Worker())->alias('fa_worker') + ->whereIn('id',$worker_ids) + ->field([ + 'fa_worker.*', + 'IFNULL(a.dispatch_count, 0) AS dispatch_count', + 'IFNULL(a.get_count, 0) AS get_count', + 'IFNULL(a.refuse_count, 0) AS refuse_count', + 'IFNULL(a.arrive_count, 0) AS arrive_count', + 'IFNULL(a.avg_time_diff, 0) AS avg_time_diff', + 'IFNULL(b.finish_num, 0) AS finish_num', + 'IFNULL(b.total, 0) AS total', + 'IFNULL(b.performance, 0) AS performance', + 'IFNULL(b.refund_total, 0) AS refund_total', + 'IFNULL(b.refund_count, 0) AS refund_count', + 'IFNULL(b.cost, 0) AS cost', + 'IFNULL(c.good_count, 0) AS good_count' + ]) + ->join([$dispatchSubsql => 'a'], 'fa_worker.id = a.worker_id', 'LEFT') + ->join([$orderSubsql => 'b'], 'fa_worker.id = b.worker_id', 'LEFT') + ->join([$viewSubsql => 'c'], 'fa_worker.id = c.worker_id', 'LEFT') + ->select(); + + + $this->_toList($list); + $out = []; + foreach ($list as $item){ + $out [] = [ + 'worker_id' => $item->id, + 'arrive_rate' => $item->arrive_rate, + 'refuse_rate' => $item->refuse_rate, + 'trans_rate' => $item->trans_rate, + 'good_rate' => $item->good_rate, + 'avg_time_diff' => $item->avg_time_diff, + + + ]; + } + return $out; + + } + + + /** + * @return bool|Collection|PDOStatement|string + * @throws DbException + */ + public function viewSubsql($filter=[]){ + //评分表 + $viewSubsql = OrderReview::where('worker_star',5)->field(['worker_id','count(*) as good_count']); + + if(!empty($filter['start_time'])){ + $viewSubsql->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $viewSubsql->where('create_time','<=',$filter['start_time']); + } + return $viewSubsql->group('worker_id')->buildSql(); + } + + /** + * @return bool|PDOStatement|string|Collection + * @throws DbException + */ + public function dispatchSubsql($filter): Collection|bool|string|PDOStatement + { + $builder = new OrderDispatch(); + $fields = [ + 'worker_id', + // 使用 IFNULL 确保结果为 null 时返回 0 + "IFNULL(COUNT(*), 0) AS dispatch_count", //分配数 + "IFNULL(COUNT(CASE WHEN status NOT IN (0, -10) THEN 1 END), 0) AS get_count", //接单数 + //"COUNT(CASE WHEN status IN (60) THEN 1 END) AS finish_count", //完成数 + "IFNULL(COUNT(CASE WHEN status NOT IN (-10) THEN 1 END), 0) AS refuse_count", //拒绝数 + "IFNULL(COUNT(arrive_time), 0) AS arrive_count", //上门数 + "IFNULL(AVG(CASE WHEN status = 60 AND arrive_time IS NOT NULL THEN UNIX_TIMESTAMP(arrive_time) - UNIX_TIMESTAMP(create_time) END), 0) AS avg_time_diff", //联系时效 + ]; + $builder->field($fields); + + if(!empty($filter['start_time'])){ + $builder->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $builder->where('create_time','<=',$filter['start_time']); + } + + $builder->group('worker_id'); + return $builder->buildSql(); + } + + + + //图表统计 + + /** + * @throws DbException + */ + public function oderSubsql($filter = []): Collection|bool|string|PDOStatement + { + + $orderValid = implode(',',(new Order())->tabStatus(Order::TAB_VALID)); + + //"COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END) AS ing_num", + $fields = [ + 'worker_id', + // 使用 IFNULL 确保结果为 null 时返回 0 + "IFNULL(COUNT(CASE WHEN status = 60 THEN 1 END), 0) AS finish_num", //完成数 + //"COUNT(CASE WHEN status IN (".$orderValid.") THEN 1 END) AS count_num", //总订单数 (排除取消 和草稿) + "IFNULL(SUM(CASE WHEN status = 60 THEN total END), 0) AS total", //成效额 + "IFNULL(SUM(CASE WHEN status = 60 THEN performance END), 0) AS performance", //业绩 + "IFNULL(SUM(CASE WHEN status = 60 THEN cost END), 0) AS cost", //成效额 + + // "SUM(CASE WHEN status = 60 THEN (cost + material_cost) END) AS cost_total", //总成本 + + "IFNULL(SUM(CASE WHEN status = 60 THEN (refund_amount + worker_refund_amount) END), 0) AS refund_total", //退款总数 + "IFNULL(COUNT(CASE WHEN refund_amount > 0 OR worker_refund_amount > 0 THEN 1 END), 0) AS refund_count", //退款订单数量 + //"AVG(CASE WHEN status > 10 THEN UNIX_TIMESTAMP(dispatch_time) - UNIX_TIMESTAMP(create_time) END) AS avg_time_diff", //派单时效 + // "SUM(CASE WHEN status = 60 THEN (field1 + field2) END) AS performance", + ]; + + $builder = (new Order())->field($fields); + + if(!empty($filter['start_time'])){ + $builder->where('create_time','>=',$filter['start_time']); + } + if(!empty($filter['end_time'])){ + $builder->where('create_time','<=',$filter['start_time']); + } + + //->where('dispatch_admin_id','>',0); + return $builder->group('worker_id')->buildSql(); + + } + + private function _toList($list) + { + foreach ($list as &$datum){ + //利润率 = 总业绩/总成效额 + $datum->performance_rate = $this->_calc($datum->performance,$datum->total,4,true); + //转化率 = 完单数 / 总接单数 + $datum->trans_rate = $this->_calc($datum->finish_num,$datum->get_count,4,true); + //变现值 = 总业绩 / 总接单数 + $datum->cash_value = $this->_calc($datum->performance,$datum->get_count,2); + //拒单率 = 拒绝数 / 派单数 + $datum->refuse_rate = $this->_calc($datum->refuse_count,$datum->dispatch_count,4,true); + //上门率 = 打卡数 / 接单数 + $datum->arrive_rate = $this->_calc($datum->arrive_count,$datum->get_count,4,true); + //好评率 + $datum->good_rate = $this->_calc($datum->good_count,$datum->finish_num,4,true); + + $datum->avg_time_diff = $this->_calc($datum->avg_time_diff,3600,2); + } + } + + + private function _calc($a, $b, int $scale=4, bool $is_percent=false): int|string + { + $a = $a??0; + $b = $b??0; + $val = $b > 0 ? bcdiv($a,$b,$scale) : '0.00'; + + 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 437cb35..bb266f6 100644 --- a/application/admin/controller/statistics/Item.php +++ b/application/admin/controller/statistics/Item.php @@ -42,13 +42,12 @@ class Item extends Backend { $build = new Order(); - $start = now()->modify('-14 days')->format('Y-m-d'); + $start = now()->modify('-7 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']); + $filter = request()->get('range',''); + if (!empty($filter)){ + $arr = explode(' - ', $filter); if (trim($arr[0])) { $start = trim($arr[0]); } @@ -56,8 +55,14 @@ class Item extends Backend $end_at = trim($arr[1]) . ' 23:29:59'; } } - $offset = request()->get('offset',0); - $limit = request()->get('limit',15); + $area_id = request()->get('area_id'); + + if ($area_id) { + $area_id = $this->trimSpecialZeros($area_id); + $build->where('area_id', 'like', $area_id . '%'); + } + + $build->whereBetween('create_time', [$start, $end_at]) ->field([ 'item_title name', // 类型 @@ -101,6 +106,21 @@ class Item extends Backend } + function trimSpecialZeros($str) { + if (strlen($str) !== 6) { + return $str; // 非6位字符串直接返回 + } + + if (substr($str, -4) === "0000") { + return substr($str, 0, 2); // 去掉后4位 + } elseif (substr($str, -2) === "00") { + return substr($str, 0, 4); // 去掉后2位 + } + + return $str; // 不符合条件则原样返回 + } + + public function chartData() { $build = new Order(); @@ -161,16 +181,32 @@ class Item extends Backend $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], - ] - ]; + + if ($totalPerformance){ + $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], + ] + ]; + }else{ + $result = [ + 'xAxis' => ['无'], + 'series' => [ + ['name' => '总业绩(¥)', 'type' => 'bar', 'yAxisIndex' => 0, 'data' => [0]], + ['name' => '转化率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '利润率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '退款率(%)', 'type' => 'bar', 'yAxisIndex' => 1, 'data' => [0]], + ['name' => '变现值(¥)', 'type' => 'line', 'yAxisIndex' => 0, 'data' => [0]], + ] + ]; + } + + return $result; } diff --git a/application/admin/controller/supplement/Orders.php b/application/admin/controller/supplement/Orders.php new file mode 100644 index 0000000..2ce30b1 --- /dev/null +++ b/application/admin/controller/supplement/Orders.php @@ -0,0 +1,241 @@ +model = new \app\admin\model\supplement\Orders; + + + $sources = Db::name('source') + ->where('status', 1) + ->field(['id', 'title', 'key_word', 'pid']) + ->order('pid', 'asc') + ->order('sort', 'desc') + ->select(); + $this->sources = $sources; + $filtered = array_filter($sources, function ($item) { + return $item['pid'] == 0; + }); + + $pid_map = array_column($filtered, null, 'id'); + $res = []; + foreach ($sources as $item) { + if ($item['pid'] != 0 && isset($pid_map[$item['pid']])) { + $res [] = [ + ...$item, 'ptitle' => $pid_map[$item['pid']]['title'] + ]; + } + } + + $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("sources", $res); + $this->view->assign("items", $formattedTree); + + + $this->view->assign("statusList", $this->model->getStatusList()); + } + + public function index() + { + //设置过滤方法 + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + return $this->view->fetch(); + } + //如果发送的来源是 Selectpage,则转发到 Selectpage + if ($this->request->request('keyField')) { + return $this->selectpage(); + } + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + + + $list = $this->model + ->where($where) + ->with(['user' => function ($q) { + $q->field('id,nickname'); + }, + 'area' => function ($q) { + $q->field('id,area_code,merge_name'); + }, + ]) + ->order($sort, $order) + ->paginate($limit); + $result = ['total' => $list->total(), 'rows' => $list->items()]; + return json($result); + } + + + /** + * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法 + * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑 + * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改 + */ + + + public function add() + { + if (false === $this->request->isPost()) { + return $this->view->fetch(); + } + $params = $this->request->post('row/a'); + if (empty($params)) { + $this->error(__('Parameter %s can not be empty', '')); + } + $params = $this->preExcludeFields($params); + + if ($this->dataLimit && $this->dataLimitFieldAutoFill) { + $params[$this->dataLimitField] = $this->auth->id; + } + $result = false; + Db::startTrans(); + try { + //是否采用模型验证 + if ($this->modelValidate) { + $name = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate; + $this->model->validateFailException()->validate($validate); + } + + $sources = $this->sources; + $sources = array_column($sources, 'title', 'id'); + $params['source_name'] = $sources[$params['source']] ?? null; + $params['source_id'] = $params['source']; + + $params['item_name'] = $this->findElementByValue($this->itemsformattedTree, $params['item_id'] ?? null); + + + $params['admin_id'] = ($params['admin_id'] ?? -1) == -1 ? $this->auth->id : $params['admin_id']; + if (empty($params['admin_id'])) { + $params['admin_id'] = $this->auth->id; + } + $params['status'] = 0; + $params['created_at'] = date('Y-m-d H:i:s'); + $params['updated_at'] = date('Y-m-d H:i:s'); +// dd($params); + $result = $this->model->allowField(true)->save($params); + Db::commit(); + } catch (ValidateException | PDOException | Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + if ($result === false) { + $this->error(__('No rows were inserted')); + } + $this->success(); + } + + + function findElementByValue($data, $targetValue, $path = []) + { + foreach ($data as $item) { + // 将当前节点的 label 添加到路径中 + $newPath = array_merge($path, [$item['label']]); + + // 如果找到目标值,返回路径 + if ($item['value'] == $targetValue) { + return implode(' / ', $newPath); + } + + // 如果当前节点有 children,递归搜索 + if (isset($item['children']) && is_array($item['children'])) { + $result = $this->findElementByValue($item['children'], $targetValue, $newPath); + if ($result) { + return $result; + } + } + } + return null; // 如果找不到返回 null + } + + public function edit($ids = null) + { + if (!$ids) { + if (request()->isPost()) { + $ids = input('id'); + if (!$ids) { + $this->error('缺少订单ID'); + } + } else { + $this->error('缺少订单ID'); + } + } + + // 获取当前ID对应的订单信息 + $order = $this->model->get($ids); + if (!$order) { + $this->error('订单不存在'); + } + + // 判断是否为POST请求,进行更新操作 + if (request()->isPost()) { + // 获取表单提交的数据 + $params = input('post.row/a'); + + $sources = $this->sources; + $sources = array_column($sources, 'title', 'id'); + $params['source_name'] = $sources[$params['source']] ?? null; + $params['source_id'] = $params['source']; + + $params['item_name'] = $this->findElementByValue($this->itemsformattedTree, $params['item_id'] ?? null); + + unset($params['source']); + + $params['admin_id'] = ($params['admin_id'] ?? -1) == -1 ? $this->auth->id : $params['admin_id']; + if (empty($params['admin_id'])) { + $params['admin_id'] = $this->auth->id; + } + $params['updated_at'] = date('Y-m-d H:i:s'); + // 更新订单信息 + $order->save($params); + + // 返回成功信息 + $this->success('更新成功', 'index'); + } + $area = new \app\admin\model\Area(); + $area_name = $area->getNameByCode($order->area_id); + $order->area_name = str_replace(',', '/', $area_name); + + // 将订单数据传递到视图 + $this->assign('row', $order); + // 渲染编辑页面 + return $this->fetch(); + } + + +} diff --git a/application/admin/lang/zh-cn/supplement/orders.php b/application/admin/lang/zh-cn/supplement/orders.php new file mode 100644 index 0000000..5e4d602 --- /dev/null +++ b/application/admin/lang/zh-cn/supplement/orders.php @@ -0,0 +1,27 @@ + 'ID', + 'Area_id' => '区域 ID', + 'Area_name' => '地址', + 'Source_id' => '所属店铺 ID', + 'Source_name' => '所属店铺 ID', + 'Platform_order_no' => '平台订单编号', + 'Item_id' => '服务类型 ID', + 'Item_name' => '服务类型 ID', + 'Buyer_account' => '刷手账号', + 'Amount' => '支付金额', + 'Commission' => '刷单佣金', + 'Screenshots' => '截图 JSON(下单图、评价图等)', + 'Status' => '订单状态', + 'Status 0' => '待发货', + 'Set status to 0' => '设为待发货', + 'Status 1' => '待评价', + 'Set status to 1' => '设为待评价', + 'Status 2' => '评价超时', + 'Set status to 2' => '设为评价超时', + 'Admin_id' => '运营人员 ID', + 'Remark' => '备注信息', + 'Created_at' => '创建时间', + 'Updated_at' => '最后更新时间' +]; diff --git a/application/admin/model/supplement/Orders.php b/application/admin/model/supplement/Orders.php new file mode 100644 index 0000000..0a026df --- /dev/null +++ b/application/admin/model/supplement/Orders.php @@ -0,0 +1,51 @@ + __('Status 0'), '1' => __('Status 1'), '2' => __('Status 2'), '3' => '被驳回']; + } + + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + public function area(){ + return $this->belongsTo(Area::class,'area_id','area_code'); + } + public function user(){ + return $this->belongsTo(Admin::class,'admin_id'); + } + +} diff --git a/application/admin/validate/supplement/Orders.php b/application/admin/validate/supplement/Orders.php new file mode 100644 index 0000000..d233386 --- /dev/null +++ b/application/admin/validate/supplement/Orders.php @@ -0,0 +1,27 @@ + [], + 'edit' => [], + ]; + +} diff --git a/application/admin/view/statistics/item/index.html b/application/admin/view/statistics/item/index.html index 749ac6e..8a0c7a7 100644 --- a/application/admin/view/statistics/item/index.html +++ b/application/admin/view/statistics/item/index.html @@ -43,6 +43,18 @@
+
+
+ + +
+
+ +
+ + +
diff --git a/application/admin/view/supplement/orders/add.html b/application/admin/view/supplement/orders/add.html new file mode 100644 index 0000000..91573b3 --- /dev/null +++ b/application/admin/view/supplement/orders/add.html @@ -0,0 +1,98 @@ +
+ + +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+ + +
+ +
+
    +
    +
    + +
    + +
    + +
    + {foreach name="statusList" item="vo"} + + {/foreach} +
    + +
    +
    +
    + +
    + +
    +
    + +
    + + + \ No newline at end of file diff --git a/application/admin/view/supplement/orders/edit.html b/application/admin/view/supplement/orders/edit.html new file mode 100644 index 0000000..db21061 --- /dev/null +++ b/application/admin/view/supplement/orders/edit.html @@ -0,0 +1,98 @@ +
    + + +
    + +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + + +
    + +
    +
      +
      +
      + +
      + +
      + +
      + {foreach name="statusList" item="vo"} + + {/foreach} +
      + +
      +
      +
      + +
      + +
      +
      + +
      + + + \ No newline at end of file diff --git a/application/admin/view/supplement/orders/index.html b/application/admin/view/supplement/orders/index.html new file mode 100644 index 0000000..d2440c1 --- /dev/null +++ b/application/admin/view/supplement/orders/index.html @@ -0,0 +1,46 @@ +
      + +
      + {:build_heading(null,FALSE)} + +
      + + +
      +
      +
      +
      +
      + + {:__('Add')} + {:__('Edit')} + {:__('Delete')} + + + + + +
      + +
      +
      +
      + +
      +
      +
      diff --git a/public/assets/js/backend/order.js b/public/assets/js/backend/order.js index 3fb9310..d1a9594 100644 --- a/public/assets/js/backend/order.js +++ b/public/assets/js/backend/order.js @@ -110,7 +110,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'cascader'], function {field: 'order_no', title: __('Order_no'), operate: 'LIKE'}, {field: 'customer', title: __('Customer'), operate: 'LIKE'}, {field: 'tel', title: __('Tel'), operate: 'LIKE'}, - {field: 'area.merge_name', title: __('Area_id')}, + {field: 'area.merge_name', title: __('Area_id'),searchable:false}, { field: 'address', title: __('Address'), diff --git a/public/assets/js/backend/statistics/item.js b/public/assets/js/backend/statistics/item.js index 97349e2..dbba0ef 100644 --- a/public/assets/js/backend/statistics/item.js +++ b/public/assets/js/backend/statistics/item.js @@ -1,9 +1,11 @@ -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','citypicker'], function ($, undefined, Backend, Table, Form, echarts, undefined, Template, Datatable, Moment) { var Controller = { index: function () { //绑定事件 + Controller.api.datepicker(); + Controller.api.areapicker(); $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { var $targetPanel = $($(this).attr("href")); var tabVal = $(this).data('val'); @@ -16,23 +18,23 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t // 表格2 var table2 = $("#table2"); table2.bootstrapTable({ - url: 'statistics/item/list' + location.search, + url: 'statistics/item/list', toolbar: '#toolbar1', sortName: 'id', search: false, - commonSearch:true, + commonSearch: false, visible: false, showToggle: false, showColumns: false, showExport: true, - searchFormVisible:true, + searchFormVisible: true, columns: [ [ // {field: 'id', title: __('ID'),visible:true,operate: false}, - {field: 'name', title: '项目类型',operate: false}, + {field: 'name', title: '项目类型', operate: false}, {field: 'total', title: __('成效额(¥)'), operate: false}, {field: 'performance', title: __('总业绩(¥)'), operate: false}, - {field: 'count_num', title: __('总订单数'),operate: false}, + {field: 'count_num', title: __('总订单数'), operate: false}, {field: 'performance_rate', title: __('利润率(%)'), operate: false}, @@ -45,40 +47,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t {field: 'avg_time_diff', title: __('派单时效(小时)'), operate: false}, {field: 'cost_total', title: __('总成本(¥)'), operate: false}, - {field: 'finish_num', 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; - // } - // }, - // ] - // } ] ] }); @@ -87,84 +59,34 @@ 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(); + Controller.api.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(); + Controller.api.getChartData(); // 绑定查询按钮的点击事件 - $('#filter-btn').on('click', function() { - getChartData(); + $('#filter-btn').on('click', function () { + Controller.api.getChartData(); + }); + + $('#filter-btn-table').on('click', function () { + + const area_id = $('#area_id').val(); + const range = $('#daterange-table').val(); + let data = ''; + if (area_id !== ''){ + data += 'area_id=' + area_id+'&'; + } + if (area_id !== ''){ + data += 'range=' + range; + } + // data = encodeURIComponent(data); + $("#table2").bootstrapTable('refresh',{ + url:'statistics/item/list?' + data, + }); }); }, @@ -183,11 +105,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t bindevent: function () { Form.api.bindevent($("form[role=form]")); }, - chart(chartData){ + chart(chartData) { const option = { tooltip: { trigger: 'axis', - axisPointer: { type: 'shadow' } + axisPointer: {type: 'shadow'} }, legend: { data: chartData.series.map(s => s.name) @@ -197,20 +119,92 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'echarts', 'echarts-t data: chartData.xAxis }, yAxis: [ - { type: 'value', name: '金额 (¥)' }, - { type: 'value', name: '比率 (%)' } + {type: 'value', name: '金额 (¥)'}, + {type: 'value', name: '比率 (%)'} ], series: chartData.series }; - -// 初始化图表 + // 初始化图表 const myChart = echarts.init(document.getElementById('bar-chart')); myChart.setOption(option); // 监听窗口大小变化,自动重新绘制图表 - window.addEventListener('resize', function() { + window.addEventListener('resize', function () { myChart.resize(); }); + }, + datepicker: function () { + 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").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); + }); + }); + }, + areapicker: function () { + $("#area-table").citypicker(); + + $("#area-table").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); + }); + + }, + getChartData: function () { + // 获取日期范围值 + 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("图表数据加载失败"); + } + }); } } }; diff --git a/public/assets/js/backend/supplement/orders.js b/public/assets/js/backend/supplement/orders.js new file mode 100644 index 0000000..bb9290d --- /dev/null +++ b/public/assets/js/backend/supplement/orders.js @@ -0,0 +1,134 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form','cascader'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + // 初始化表格参数配置 + Table.api.init({ + extend: { + index_url: 'supplement/orders/index' + location.search, + add_url: 'supplement/orders/add', + edit_url: 'supplement/orders/edit', + del_url: 'supplement/orders/del', + multi_url: 'supplement/orders/multi', + import_url: 'supplement/orders/import', + table: 'supplement_orders', + } + }); + + var table = $("#table"); + + // 初始化表格 + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + pk: 'id', + sortName: 'id', + fixedColumns: true, + fixedRightNumber: 1, + columns: [ + [ + {checkbox: true}, + {field: 'id', title: __('Id')}, + {field: 'user.nickname', title: '创建人'}, + {field: 'area.merge_name', title: '地区',searchable:false}, + {field: 'item_name', title: __('Item_name')}, + {field: 'source_name', title: __('Source_name')}, + {field: 'platform_order_no', title: __('Platform_order_no'), operate: 'LIKE'}, + {field: 'buyer_account', title: __('Buyer_account'), operate: 'LIKE'}, + {field: 'amount', title: __('Amount'), operate:'BETWEEN'}, + {field: 'commission', title: __('Commission'), operate:'BETWEEN'}, + {field: 'screenshots', title: '图片', operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content}, + {field: 'status', title: __('Status'), searchList: {"0":__('Status 0'),"1":__('Status 1'),"2":__('Status 2')}, formatter: Table.api.formatter.status}, + {field: 'created_at', title: __('Created_at'), operate:'RANGE', addclass:'datetimerange', autocomplete:false}, + {field: 'updated_at', title: __('Updated_at'), operate:'RANGE', addclass:'datetimerange', autocomplete:false}, + {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate} + ] + ] + }); + + // 为表格绑定事件 + Table.api.bindevent(table); + }, + add: function () { + Controller.api.bindevent(); + Controller.api.map(); + }, + edit: function () { + Controller.api.bindevent(); + Controller.api.map(); + }, + api: { + bindevent: function () { + Form.api.bindevent($("form[role=form]")); + }, + map:function () { + $("#c-city").on("cp:updated", function () { + var citypicker = $(this).data("citypicker"); + var code = citypicker.getCode("district") || citypicker.getCode("city") || citypicker.getCode("province"); + + $("#area_id").val(code); + $("#area_name").val(citypicker.getVal()); + }); + // $("#area_map").data("callback", function (res) { + // Form.api.target($('#c-address')); + // }); + $(document).on('click', "#area_map", function (e) { + const data = $("#c-city").val(); + if (!data){ + Toastr.error('请先选择区域'); + return false; + } + + var that = this; + var callback = $(that).data('callback'); + var input_id = $(that).data("input-id") ? $(that).data("input-id") : ""; + var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : ""; + var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : ""; + 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 zoom = zoom_id ? $("#" + zoom_id).val() : ''; + var url = "/addons/address/index/select?a=1"; + url += (lat && lng) ? 'lat=' + lat + '&lng=' + lng + + (input_id ? "&address=" + $("#" + input_id).val() : "") + +(zoom ? "&zoom=" + zoom : "") : '' + ; + if (city_code){ + url += city_code ? "&city_code=" + city_code : ""; + } + // console.log(url); + Fast.api.open(url, '位置选择', { + callback: function (res) { + input_id && $("#" + input_id).val(res.address).trigger("change"); + lat_id && $("#" + lat_id).val(res.lat).trigger("change"); + lng_id && $("#" + lng_id).val(res.lng).trigger("change"); + zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change"); + + try { + //执行回调函数 + if (typeof callback === 'function') { + callback.call(that, res); + } + } catch (e) { + + } + } + }); + + }); + + var _data = items; + + $('#item_id').zdCascader({ + data: _data, + onChange: function ($this, data, allPathData) { + // console.log(data,allPathData); + $('#item_id_value').val(data.value); + } + }); + $('#item_id').val($('#item_id').data('value')).focus(); + } + } + }; + return Controller; +});