feat: 新增师傅端功能及服务增项管理模块
- 新增师傅端路由和页面配置,切换登录后默认跳转到师傅大厅 - 新增服务分类图标上传功能和增项服务管理模块 - 新增订单、申诉、结算等模型关联关系及后台控制器 - 新增师傅端TabBar组件和消息页面跳转逻辑 - 新增文件上传接口和服务计算逻辑 - 新增各类SVG图标和空状态提示
@@ -35,4 +35,25 @@ class Appeal extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['order', 'master'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,25 @@ class Invoice extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['user', 'merchant'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,25 @@ class Master extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['user'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,25 @@ class Merchant extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['user'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,25 @@ class Order extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['user', 'master'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,25 @@ class Review extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['user', 'master', 'order'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,24 @@
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use think\Db;
|
||||
use think\exception\PDOException;
|
||||
use think\exception\ValidateException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* 服务项目
|
||||
* 服务项目与商品管理
|
||||
*
|
||||
* @icon fa fa-circle-o
|
||||
*/
|
||||
class Service extends Backend
|
||||
{
|
||||
|
||||
/**
|
||||
* Service模型对象
|
||||
* @var \app\admin\model\Service
|
||||
*/
|
||||
protected $model = null;
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
@@ -24,15 +28,205 @@ class Service extends Backend
|
||||
$this->model = new \app\admin\model\Service;
|
||||
$this->view->assign("priceTypeList", $this->model->getPriceTypeList());
|
||||
$this->view->assign("statusList", $this->model->getStatusList());
|
||||
|
||||
// 获取全部可用的公共增项服务列表
|
||||
$addonsPool = Db::name('service_addon')
|
||||
->where('status', 'normal')
|
||||
->order('weigh desc, id asc')
|
||||
->select();
|
||||
$this->view->assign("addonsPool", $addonsPool);
|
||||
}
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['cat'])
|
||||
->where($where)
|
||||
->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 ($this->request->isPost()) {
|
||||
$params = $this->request->post("row/a");
|
||||
if ($params) {
|
||||
$params = $this->preExcludeFields($params);
|
||||
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$params[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
|
||||
// 处理工项类型与增项配置
|
||||
$params = $this->formatServiceConfig($params);
|
||||
|
||||
$result = false;
|
||||
Db::startTrans();
|
||||
try {
|
||||
$result = $this->model->allowField(true)->save($params);
|
||||
$serviceId = $this->model->id;
|
||||
|
||||
// 处理规格列表
|
||||
$this->syncSpecs($serviceId, $this->request->post('specs/a', []));
|
||||
|
||||
Db::commit();
|
||||
} catch (ValidateException|PDOException|Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
$this->success();
|
||||
} else {
|
||||
$this->error(__('No rows were inserted'));
|
||||
}
|
||||
}
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑服务商品(工项价格、规格价格、增项服务绑定)
|
||||
*/
|
||||
public function edit($ids = null)
|
||||
{
|
||||
$row = $this->model->get($ids);
|
||||
if (!$row) {
|
||||
$this->error(__('No Results were found'));
|
||||
}
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
|
||||
$this->error(__('You have no permission'));
|
||||
}
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post("row/a");
|
||||
if ($params) {
|
||||
$params = $this->preExcludeFields($params);
|
||||
|
||||
// 处理工项类型与增项配置
|
||||
$params = $this->formatServiceConfig($params);
|
||||
|
||||
$result = false;
|
||||
Db::startTrans();
|
||||
try {
|
||||
$result = $row->allowField(true)->save($params);
|
||||
|
||||
// 同步处理规格列表
|
||||
$this->syncSpecs($row->id, $this->request->post('specs/a', []));
|
||||
|
||||
Db::commit();
|
||||
} catch (ValidateException|PDOException|Exception $e) {
|
||||
Db::rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
$this->success();
|
||||
} else {
|
||||
$this->error(__('No rows were updated'));
|
||||
}
|
||||
}
|
||||
$this->error(__('Parameter %s can not be empty', ''));
|
||||
}
|
||||
|
||||
// 解析工项与增项配置以供视图回显
|
||||
$serviceTypes = [];
|
||||
if (!empty($row->service_types)) {
|
||||
$serviceTypes = is_array($row->service_types) ? $row->service_types : json_decode($row->service_types, true);
|
||||
}
|
||||
$addons = [];
|
||||
if (!empty($row->addons)) {
|
||||
$addons = is_array($row->addons) ? $row->addons : json_decode($row->addons, true);
|
||||
}
|
||||
|
||||
// 获取已有规格
|
||||
$specs = Db::name('service_spec')
|
||||
->where('service_id', $row->id)
|
||||
->where('status', 'normal')
|
||||
->order('weigh desc, id asc')
|
||||
->select();
|
||||
|
||||
$this->view->assign("row", $row);
|
||||
$this->view->assign("serviceTypes", $serviceTypes ?: []);
|
||||
$this->view->assign("assignedAddons", $addons ?: []);
|
||||
$this->view->assign("specs", $specs ?: []);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化工项与增项输入数据
|
||||
*/
|
||||
protected function formatServiceConfig($params)
|
||||
{
|
||||
// 工项类型 JSON
|
||||
if (isset($params['service_types_raw'])) {
|
||||
$params['service_types'] = is_string($params['service_types_raw']) ? $params['service_types_raw'] : json_encode($params['service_types_raw'], JSON_UNESCAPED_UNICODE);
|
||||
unset($params['service_types_raw']);
|
||||
}
|
||||
// 增项服务 JSON
|
||||
if (isset($params['addons_raw'])) {
|
||||
$params['addons'] = is_string($params['addons_raw']) ? $params['addons_raw'] : json_encode($params['addons_raw'], JSON_UNESCAPED_UNICODE);
|
||||
unset($params['addons_raw']);
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步商品规格表
|
||||
*/
|
||||
protected function syncSpecs($serviceId, $specsInput)
|
||||
{
|
||||
if (empty($serviceId)) return;
|
||||
$now = time();
|
||||
$keepIds = [];
|
||||
if (!empty($specsInput) && is_array($specsInput)) {
|
||||
foreach ($specsInput as $item) {
|
||||
$name = trim(isset($item['name']) ? $item['name'] : '');
|
||||
if ($name === '') continue;
|
||||
$specPrice = isset($item['base_price']) ? (float)$item['base_price'] : 0.00;
|
||||
$specDesc = isset($item['description']) ? trim($item['description']) : '';
|
||||
$specId = isset($item['id']) ? (int)$item['id'] : 0;
|
||||
|
||||
if ($specId > 0) {
|
||||
Db::name('service_spec')->where('id', $specId)->where('service_id', $serviceId)->update([
|
||||
'name' => $name,
|
||||
'base_price' => $specPrice,
|
||||
'description' => $specDesc,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$keepIds[] = $specId;
|
||||
} else {
|
||||
$newId = Db::name('service_spec')->insertGetId([
|
||||
'service_id' => $serviceId,
|
||||
'name' => $name,
|
||||
'base_price' => $specPrice,
|
||||
'description' => $specDesc,
|
||||
'weigh' => 0,
|
||||
'status' => 'normal',
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$keepIds[] = $newId;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将未在列表中的历史规格软下架或删除
|
||||
$delQuery = Db::name('service_spec')->where('service_id', $serviceId);
|
||||
if (!empty($keepIds)) {
|
||||
$delQuery->whereNotIn('id', $keepIds);
|
||||
}
|
||||
$delQuery->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,25 @@ class Settle extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['order', 'master'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,25 @@ class Withdraw extends Backend
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['master'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\service;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 增项服务管理
|
||||
*
|
||||
* @icon fa fa-plus-square
|
||||
*/
|
||||
class Addon extends Backend
|
||||
{
|
||||
/**
|
||||
* Addon模型对象
|
||||
* @var \app\admin\model\service\Addon
|
||||
*/
|
||||
protected $model = null;
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
$this->model = new \app\admin\model\service\Addon;
|
||||
$this->view->assign("statusList", $this->model->getStatusList());
|
||||
|
||||
// 获取分类列表
|
||||
$cats = Db::name('service_cat')->where('status', 'normal')->column('id,name');
|
||||
$this->view->assign("catList", [0 => '通用全部类目'] + $cats);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
if (false === $this->request->isAjax()) {
|
||||
return $this->view->fetch();
|
||||
}
|
||||
if ($this->request->request('keyField')) {
|
||||
return $this->selectpage();
|
||||
}
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$list = $this->model
|
||||
->with(['cat'])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
@@ -60,4 +60,14 @@ class Appeal extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo('Order', 'order_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function master()
|
||||
{
|
||||
return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,14 @@ class Invoice extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function merchant()
|
||||
{
|
||||
return $this->belongsTo('Merchant', 'merchant_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,9 @@ class Master extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,9 @@ class Merchant extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,4 +223,14 @@ class Order extends Model
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function master()
|
||||
{
|
||||
return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +46,19 @@ class Review extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function master()
|
||||
{
|
||||
return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo('Order', 'order_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,4 +70,9 @@ class Service extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function cat()
|
||||
{
|
||||
return $this->belongsTo('app\\admin\\model\\service\\Cat', 'cat_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,14 @@ class Settle extends Model
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo('Order', 'order_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
|
||||
public function master()
|
||||
{
|
||||
return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,9 @@ class Withdraw extends Model
|
||||
|
||||
|
||||
|
||||
|
||||
public function master()
|
||||
{
|
||||
return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model\service;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Addon extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'service_addon';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = 'updatetime';
|
||||
protected $deleteTime = false;
|
||||
|
||||
// 追加属性
|
||||
protected $append = [
|
||||
'status_text'
|
||||
];
|
||||
|
||||
public function getStatusList()
|
||||
{
|
||||
return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
|
||||
}
|
||||
|
||||
public function getStatusTextAttr($value, $data)
|
||||
{
|
||||
$value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
|
||||
$list = $this->getStatusList();
|
||||
return isset($list[$value]) ? $list[$value] : '';
|
||||
}
|
||||
|
||||
public function cat()
|
||||
{
|
||||
return $this->belongsTo('app\admin\model\service\Cat', 'cat_id', 'id', [], 'LEFT')->setEagerlyType(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model\service;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Cat extends Model
|
||||
{
|
||||
// 表名
|
||||
protected $name = 'service_cat';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = 'integer';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = 'updatetime';
|
||||
protected $deleteTime = false;
|
||||
|
||||
// 追加属性
|
||||
protected $append = [
|
||||
'status_text'
|
||||
];
|
||||
|
||||
public function getStatusList()
|
||||
{
|
||||
return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
|
||||
}
|
||||
|
||||
public function getStatusTextAttr($value, $data)
|
||||
{
|
||||
$value = $value ?: ($data['status'] ?? '');
|
||||
$list = $this->getStatusList();
|
||||
return $list[$value] ?? '';
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-backend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>
|
||||
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-backend.js?v={$site.version|htmlentities}"></script>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">适用类目:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
{:build_select('row[cat_id]', $catList, 0, ['class'=>'form-control selectpicker', 'data-rule'=>'required'])}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">增项名称:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="" placeholder="如:水泥拆旧、修改坑距、加装喷枪">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">计量单位:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-unit" data-rule="required" class="form-control" name="row[unit]" type="text" value="个" placeholder="如:个、处、项、米、套">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">默认指导价(元):</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-default_price" data-rule="required;range(0~)" class="form-control" step="0.01" name="row[default_price]" type="number" value="30.00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">排序权重:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">状态:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,49 @@
|
||||
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">适用类目:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
{:build_select('row[cat_id]', $catList, $row.cat_id, ['class'=>'form-control selectpicker', 'data-rule'=>'required'])}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">增项名称:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">计量单位:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-unit" data-rule="required" class="form-control" name="row[unit]" type="text" value="{$row.unit|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">默认指导价(元):</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-default_price" data-rule="required;range(0~)" class="form-control" step="0.01" name="row[default_price]" type="number" value="{$row.default_price|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">排序权重:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">状态:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="panel panel-default panel-intro">
|
||||
{:build_heading()}
|
||||
|
||||
<div class="panel-body">
|
||||
<div id="myTabContent" class="tab-content">
|
||||
<div class="tab-pane fade active in" id="one">
|
||||
<div class="widget-body no-padding">
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('service/addon/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('service/addon/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('service/addon/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('service/addon/edit')}"
|
||||
data-operate-del="{:$auth->check('service/addon/del')}"
|
||||
width="100%">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,9 +13,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Icon')}:</label>
|
||||
<label class="control-label col-xs-12 col-sm-2">类目图标:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-icon" data-rule="required" class="form-control" name="row[icon]" type="text" value="">
|
||||
<div class="input-group">
|
||||
<input id="c-icon" class="form-control" size="50" name="row[icon]" type="text" value="" placeholder="请上传真实图标图片或输入标识">
|
||||
<div class="input-group-addon no-border no-padding">
|
||||
<span><button type="button" id="faupload-icon" class="btn btn-danger plupload" data-input-id="c-icon" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp,image/svg+xml" data-multiple="false" data-preview-id="p-icon"><i class="fa fa-upload"></i> 上传</button></span>
|
||||
<span><button type="button" id="fachoose-icon" class="btn btn-primary fachoose" data-input-id="c-icon" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> 选择</button></span>
|
||||
</div>
|
||||
<span class="msg-box n-right" for="c-icon"></span>
|
||||
</div>
|
||||
<ul class="row list-inline plupload-preview" id="p-icon"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -27,13 +35,11 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
|
||||
@@ -13,9 +13,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Icon')}:</label>
|
||||
<label class="control-label col-xs-12 col-sm-2">类目图标:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-icon" data-rule="required" class="form-control" name="row[icon]" type="text" value="{$row.icon|htmlentities}">
|
||||
<div class="input-group">
|
||||
<input id="c-icon" class="form-control" size="50" name="row[icon]" type="text" value="{$row.icon|htmlentities}" placeholder="请上传真实图标图片或输入标识">
|
||||
<div class="input-group-addon no-border no-padding">
|
||||
<span><button type="button" id="faupload-icon" class="btn btn-danger plupload" data-input-id="c-icon" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp,image/svg+xml" data-multiple="false" data-preview-id="p-icon"><i class="fa fa-upload"></i> 上传</button></span>
|
||||
<span><button type="button" id="fachoose-icon" class="btn btn-primary fachoose" data-input-id="c-icon" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> 选择</button></span>
|
||||
</div>
|
||||
<span class="msg-box n-right" for="c-icon"></span>
|
||||
</div>
|
||||
<ul class="row list-inline plupload-preview" id="p-icon"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -27,13 +35,11 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group layer-footer">
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
<div class="widget-body no-padding">
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('service.cat/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('service.cat/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('service.cat/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('service/cat/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('service/cat/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('service/cat/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
|
||||
|
||||
<div class="dropdown btn-group {:$auth->check('service.cat/multi')?'':'hide'}">
|
||||
<div class="dropdown btn-group {:$auth->check('service/cat/multi')?'':'hide'}">
|
||||
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
|
||||
<ul class="dropdown-menu text-left" role="menu">
|
||||
{foreach name="statusList" item="vo"}
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('service.cat/edit')}"
|
||||
data-operate-del="{:$auth->check('service.cat/del')}"
|
||||
data-operate-edit="{:$auth->check('service/cat/edit')}"
|
||||
data-operate-del="{:$auth->check('service/cat/del')}"
|
||||
width="100%">
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,113 +1,335 @@
|
||||
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Cat_id')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-cat_id" data-rule="required" min="0" data-source="cat/index" class="form-control selectpage" name="row[cat_id]" type="text" value="{$row.cat_id|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Image')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<div class="input-group">
|
||||
<input id="c-image" data-rule="required" class="form-control" size="50" name="row[image]" type="text" value="{$row.image|htmlentities}">
|
||||
<div class="input-group-addon no-border no-padding">
|
||||
<span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
|
||||
<span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
|
||||
<div class="nav-tabs-custom" style="box-shadow: none;">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#tab-basic" data-toggle="tab"><i class="fa fa-info-circle text-primary"></i> 基础信息</a></li>
|
||||
<li><a href="#tab-service-types" data-toggle="tab"><i class="fa fa-wrench text-warning"></i> 工项服务定价</a></li>
|
||||
<li><a href="#tab-specs" data-toggle="tab"><i class="fa fa-cubes text-success"></i> 规格独立一口价</a></li>
|
||||
<li><a href="#tab-addons" data-toggle="tab"><i class="fa fa-plus-square text-danger"></i> 增项施工服务绑定</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" style="padding: 15px 0;">
|
||||
<!-- Tab 1: 基础信息 -->
|
||||
<div class="tab-pane active" id="tab-basic">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">所属类目:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-cat_id" data-rule="required" min="0" data-source="service/cat/index" class="form-control selectpage" name="row[cat_id]" type="text" value="{$row.cat_id|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">服务/商品名称:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">主图:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<div class="input-group">
|
||||
<input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="{$row.image|htmlentities}">
|
||||
<div class="input-group-addon no-border no-padding">
|
||||
<span><button type="button" id="faupload-image" class="btn btn-danger plupload" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> 上传</button></span>
|
||||
<span><button type="button" id="fachoose-image" class="btn btn-primary fachoose" data-input-id="c-image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> 选择</button></span>
|
||||
</div>
|
||||
<span class="msg-box n-right" for="c-image"></span>
|
||||
</div>
|
||||
<ul class="row list-inline plupload-preview" id="p-image"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">计价模式:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<select id="c-price_type" data-rule="required" class="form-control selectpicker" name="row[price_type]">
|
||||
{foreach name="priceTypeList" item="vo"}
|
||||
<option value="{$key}" {in name="key" value="$row.price_type"}selected{/in}>{$vo}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">基准一口价/参考价(元):</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-base_price" data-rule="required" class="form-control" step="0.01" name="row[base_price]" type="number" value="{$row.base_price|htmlentities}">
|
||||
<span class="help-block text-muted"><i class="fa fa-info-circle"></i> 作为该商品的通用基准价(若各工项未单独定价时默认采用此价)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">计价单位:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-unit" data-rule="required" class="form-control" name="row[unit]" type="text" value="{$row.unit|htmlentities}" placeholder="如:件、台、套、次">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">步梯楼层费(元/层):</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-floor_fee" data-rule="required" class="form-control" step="0.01" name="row[floor_fee]" type="number" value="{$row.floor_fee|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">免费楼层数:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-free_floor" data-rule="required" class="form-control" name="row[free_floor]" type="number" value="{$row.free_floor|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">服务简介说明:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<textarea id="c-description" class="form-control" rows="3" name="row[description]">{$row.description|htmlentities}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">排序权重:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">状态:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="msg-box n-right" for="c-image"></span>
|
||||
</div>
|
||||
<ul class="row list-inline faupload-preview" id="p-image"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Price_type')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
|
||||
<select id="c-price_type" data-rule="required" class="form-control selectpicker" name="row[price_type]">
|
||||
{foreach name="priceTypeList" item="vo"}
|
||||
<option value="{$key}" {in name="key" value="$row.price_type"}selected{/in}>{$vo}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Base_price')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-base_price" data-rule="required" class="form-control" step="0.01" name="row[base_price]" type="number" value="{$row.base_price|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Max_price')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-max_price" data-rule="required" class="form-control" step="0.01" name="row[max_price]" type="number" value="{$row.max_price|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Unit')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-unit" data-rule="required" class="form-control" name="row[unit]" type="text" value="{$row.unit|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Floor_fee')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-floor_fee" data-rule="required" class="form-control" step="0.01" name="row[floor_fee]" type="number" value="{$row.floor_fee|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Free_floor')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-free_floor" data-rule="required" class="form-control" name="row[free_floor]" type="number" value="{$row.free_floor|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Need_model')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-need_model" data-rule="required" class="form-control" name="row[need_model]" type="number" value="{$row.need_model|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-description" data-rule="required" class="form-control" name="row[description]" type="text" value="{$row.description|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Sales')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-sales" data-rule="required" class="form-control" name="row[sales]" type="number" value="{$row.sales|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<input id="c-weigh" data-rule="required" class="form-control" name="row[weigh]" type="number" value="{$row.weigh|htmlentities}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
|
||||
<div class="radio">
|
||||
{foreach name="statusList" item="vo"}
|
||||
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: 工项服务定价 -->
|
||||
<div class="tab-pane" id="tab-service-types">
|
||||
<div class="alert alert-info-light">
|
||||
<i class="fa fa-info-circle"></i> 勾选此商品支持的服务工项(如安装、维修、拆旧等),并可为每个工项设定独立一口价。
|
||||
</div>
|
||||
<table class="table table-bordered table-striped" id="service-types-table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th style="width: 60px; text-align: center;">启用</th>
|
||||
<th style="width: 120px;">工项标识</th>
|
||||
<th style="width: 160px;">工项显示名称</th>
|
||||
<th style="width: 140px;">工项一口价(元)</th>
|
||||
<th style="width: 80px; text-align: center;">默认勾选</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$defaultTypes = [
|
||||
'install' => ['name' => '安装', 'price' => $row['base_price'], 'default' => 1],
|
||||
'repair' => ['name' => '维修', 'price' => round($row['base_price'] * 0.7, 2), 'default' => 0],
|
||||
'remove' => ['name' => '拆旧', 'price' => round($row['base_price'] * 0.5, 2), 'default' => 0],
|
||||
'deliver' => ['name' => '送货到家', 'price' => 50.00, 'default' => 0],
|
||||
'deliver_install' => ['name' => '送货到家并安装', 'price' => $row['base_price'] + 50.00, 'default' => 0],
|
||||
'measure' => ['name' => '坑距测量(含拆旧)', 'price' => 40.00, 'default' => 0],
|
||||
'maintain' => ['name' => '保养', 'price' => 60.00, 'default' => 0],
|
||||
];
|
||||
$savedMap = [];
|
||||
foreach ($serviceTypes as $st) {
|
||||
if (isset($st['code'])) {
|
||||
$savedMap[$st['code']] = $st;
|
||||
}
|
||||
}
|
||||
?>
|
||||
{foreach name="defaultTypes" item="cfg" key="code"}
|
||||
<?php
|
||||
$isCheck = !empty($savedMap) ? isset($savedMap[$code]) : true;
|
||||
$curName = isset($savedMap[$code]['name']) ? $savedMap[$code]['name'] : $cfg['name'];
|
||||
$curPrice = isset($savedMap[$code]['price']) ? $savedMap[$code]['price'] : $cfg['price'];
|
||||
$isDef = isset($savedMap[$code]['is_default']) ? $savedMap[$code]['is_default'] : $cfg['default'];
|
||||
?>
|
||||
<tr>
|
||||
<td style="text-align: center; vertical-align: middle;">
|
||||
<input type="checkbox" class="st-enable" data-code="{$code}" {if $isCheck}checked{/if}>
|
||||
</td>
|
||||
<td style="vertical-align: middle;"><code>{$code}</code></td>
|
||||
<td>
|
||||
<input type="text" class="form-control input-sm st-name" data-code="{$code}" value="{$curName}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">¥</span>
|
||||
<input type="number" step="0.01" class="form-control st-price" data-code="{$code}" value="{$curPrice}">
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: center; vertical-align: middle;">
|
||||
<input type="radio" name="st_default_radio" class="st-default" data-code="{$code}" {if $isDef}checked{/if}>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="hidden" name="row[service_types_raw]" id="service_types_raw" value="">
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: 规格独立定价 -->
|
||||
<div class="tab-pane" id="tab-specs">
|
||||
<div class="alert alert-success-light">
|
||||
<i class="fa fa-info-circle"></i> 为该商品增设不同规格(如:单眼灶/双眼灶、普通坐便器/智能马桶),并分别设置规格基准一口价。
|
||||
</div>
|
||||
<table class="table table-bordered table-striped" id="specs-table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th style="width: 200px;">规格名称</th>
|
||||
<th style="width: 150px;">规格独立一口价(元)</th>
|
||||
<th>规格施工描述</th>
|
||||
<th style="width: 80px; text-align: center;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="specs-tbody">
|
||||
{foreach name="specs" item="sp" key="idx"}
|
||||
<tr data-id="{$sp.id}">
|
||||
<td>
|
||||
<input type="hidden" name="specs[{$idx}][id]" value="{$sp.id}">
|
||||
<input type="text" name="specs[{$idx}][name]" class="form-control input-sm" value="{$sp.name|htmlentities}" placeholder="如:智能一体机">
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">¥</span>
|
||||
<input type="number" step="0.01" name="specs[{$idx}][base_price]" class="form-control" value="{$sp.base_price}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="specs[{$idx}][description]" class="form-control input-sm" value="{$sp.description|htmlentities}" placeholder="施工要求说明">
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<button type="button" class="btn btn-xs btn-danger btn-del-spec"><i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-sm btn-success" id="btn-add-spec"><i class="fa fa-plus"></i> 添加新规格</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: 增项服务绑定 -->
|
||||
<div class="tab-pane" id="tab-addons">
|
||||
<div class="alert alert-danger-light">
|
||||
<i class="fa fa-info-circle"></i> 从系统增项库中勾选该商品允许加选的增项服务(如马桶勾选拆旧/改坑距,油烟机勾选打孔),并可为该商品定制专属单价(留空则默认按全局价)。
|
||||
</div>
|
||||
<table class="table table-bordered table-striped" id="addons-table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th style="width: 60px; text-align: center;">绑定</th>
|
||||
<th style="width: 180px;">增项名称</th>
|
||||
<th style="width: 90px; text-align: center;">单位</th>
|
||||
<th style="width: 140px;">平台全局指导价</th>
|
||||
<th style="width: 180px;">针对本商品专属单价(元)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$assignedMap = [];
|
||||
foreach ($assignedAddons as $ad) {
|
||||
if (isset($ad['id'])) {
|
||||
$assignedMap[$ad['id']] = $ad;
|
||||
}
|
||||
}
|
||||
?>
|
||||
{foreach name="addonsPool" item="ad"}
|
||||
<?php
|
||||
$isBind = isset($assignedMap[$ad['id']]);
|
||||
$customPrice = $isBind && isset($assignedMap[$ad['id']]['price']) ? $assignedMap[$ad['id']]['price'] : $ad['default_price'];
|
||||
?>
|
||||
<tr>
|
||||
<td style="text-align: center; vertical-align: middle;">
|
||||
<input type="checkbox" class="ad-bind" data-id="{$ad.id}" data-name="{$ad.name}" data-unit="{$ad.unit}" {if $isBind}checked{/if}>
|
||||
</td>
|
||||
<td style="vertical-align: middle;">
|
||||
<strong>{$ad.name}</strong>
|
||||
</td>
|
||||
<td style="text-align: center; vertical-align: middle;">
|
||||
<span class="badge">{$ad.unit}</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; color: #888;">
|
||||
¥{$ad.default_price}
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">¥</span>
|
||||
<input type="number" step="0.01" class="form-control ad-price" data-id="{$ad.id}" value="{$customPrice}" placeholder="覆盖单价">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="hidden" name="row[addons_raw]" id="addons_raw" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group layer-footer">
|
||||
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||
<div class="col-xs-12 col-sm-8">
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||
<button type="submit" class="btn btn-primary btn-embossed disabled" id="btn-submit-service">{:__('OK')}</button>
|
||||
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// 动态组装工项与增项 JSON
|
||||
function collectServiceConfig() {
|
||||
// 1. 收集工项
|
||||
var serviceTypes = [];
|
||||
$('#service-types-table tbody tr').each(function () {
|
||||
var $row = $(this);
|
||||
var $enable = $row.find('.st-enable');
|
||||
if ($enable.is(':checked')) {
|
||||
var code = $enable.data('code');
|
||||
var name = $row.find('.st-name').val();
|
||||
var price = parseFloat($row.find('.st-price').val()) || 0;
|
||||
var isDef = $row.find('.st-default').is(':checked') ? 1 : 0;
|
||||
serviceTypes.push({
|
||||
code: code,
|
||||
name: name,
|
||||
price: price,
|
||||
is_default: isDef
|
||||
});
|
||||
}
|
||||
});
|
||||
$('#service_types_raw').val(JSON.stringify(serviceTypes));
|
||||
|
||||
// 2. 收集增项
|
||||
var addons = [];
|
||||
$('#addons-table tbody tr').each(function () {
|
||||
var $row = $(this);
|
||||
var $bind = $row.find('.ad-bind');
|
||||
if ($bind.is(':checked')) {
|
||||
var id = parseInt($bind.data('id'), 10);
|
||||
var name = $bind.data('name');
|
||||
var unit = $bind.data('unit');
|
||||
var price = parseFloat($row.find('.ad-price').val()) || 0;
|
||||
addons.push({
|
||||
id: id,
|
||||
name: name,
|
||||
unit: unit,
|
||||
price: price
|
||||
});
|
||||
}
|
||||
});
|
||||
$('#addons_raw').val(JSON.stringify(addons));
|
||||
}
|
||||
|
||||
// 监听表单提交事件,在提交前组装 JSON
|
||||
$(document).on('submit', '#edit-form', function () {
|
||||
collectServiceConfig();
|
||||
});
|
||||
|
||||
// 动态添加规格
|
||||
var specIndex = 1000;
|
||||
$(document).on('click', '#btn-add-spec', function () {
|
||||
specIndex++;
|
||||
var html = '<tr>' +
|
||||
'<td><input type="text" name="specs[' + specIndex + '][name]" class="form-control input-sm" placeholder="规格名称(如:四眼炒灶)"></td>' +
|
||||
'<td><div class="input-group input-group-sm"><span class="input-group-addon">¥</span><input type="number" step="0.01" name="specs[' + specIndex + '][base_price]" class="form-control" value="0.00"></div></td>' +
|
||||
'<td><input type="text" name="specs[' + specIndex + '][description]" class="form-control input-sm" placeholder="施工说明"></td>' +
|
||||
'<td style="text-align:center;"><button type="button" class="btn btn-xs btn-danger btn-del-spec"><i class="fa fa-trash"></i></button></td>' +
|
||||
'</tr>';
|
||||
$('#specs-tbody').append(html);
|
||||
});
|
||||
|
||||
// 删除规格
|
||||
$(document).on('click', '.btn-del-spec', function () {
|
||||
$(this).closest('tr').remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -309,7 +309,7 @@ class Auth extends Base
|
||||
|
||||
$redirect = '/pages/index/index';
|
||||
if ($role === 'master') {
|
||||
$redirect = '/pages/master/workbench';
|
||||
$redirect = '/pages/master/hall';
|
||||
} elseif ($role === 'merchant') {
|
||||
$redirect = '/pages/merchant/workbench';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use think\Db;
|
||||
*/
|
||||
class Home extends Base
|
||||
{
|
||||
protected $noNeedLogin = ['index', 'cats', 'services', 'detail'];
|
||||
protected $noNeedLogin = ['index', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount'];
|
||||
|
||||
/**
|
||||
* 首页聚合:类目 + 热门服务 + 我的角标
|
||||
@@ -57,6 +57,23 @@ class Home extends Base
|
||||
->where('status', 'normal')
|
||||
->order('weigh desc,id asc')
|
||||
->select();
|
||||
|
||||
foreach ($cats as &$c) {
|
||||
$icon = (string)$c['icon'];
|
||||
if ($icon !== '') {
|
||||
if (strpos($icon, 'http') === 0) {
|
||||
$c['icon_url'] = $icon;
|
||||
} elseif (strpos($icon, '/') !== false || strpos($icon, '.') !== false) {
|
||||
$c['icon_url'] = cdnurl($icon, true);
|
||||
} else {
|
||||
$c['icon_url'] = '';
|
||||
}
|
||||
} else {
|
||||
$c['icon_url'] = '';
|
||||
}
|
||||
}
|
||||
unset($c);
|
||||
|
||||
$this->success('', ['list' => $cats]);
|
||||
}
|
||||
|
||||
@@ -166,7 +183,7 @@ class Home extends Base
|
||||
public function calc()
|
||||
{
|
||||
$serviceId = (int)$this->request->param('service_id');
|
||||
$goodsListRaw = $this->request->param('goods_list');
|
||||
$goodsListRaw = $this->request->param('goods_list/a');
|
||||
$goodsList = [];
|
||||
if (!empty($goodsListRaw)) {
|
||||
$goodsList = is_array($goodsListRaw) ? $goodsListRaw : json_decode((string)$goodsListRaw, true);
|
||||
@@ -472,12 +489,52 @@ class Home extends Base
|
||||
} elseif ($type === 'quote') {
|
||||
$priceText = '¥' . (float)$row['base_price'] . ' · 师傅报价';
|
||||
}
|
||||
// 解析工项
|
||||
$serviceTypes = [];
|
||||
if (!empty($row['service_types'])) {
|
||||
$serviceTypes = is_array($row['service_types']) ? $row['service_types'] : json_decode($row['service_types'], true);
|
||||
}
|
||||
if (empty($serviceTypes)) {
|
||||
$serviceTypes = [
|
||||
['code' => 'install', 'name' => '安装', 'price' => (float)$row['base_price'], 'is_default' => 1],
|
||||
['code' => 'repair', 'name' => '维修', 'price' => round((float)$row['base_price'] * 0.7, 2), 'is_default' => 0],
|
||||
['code' => 'remove', 'name' => '拆旧', 'price' => round((float)$row['base_price'] * 0.5, 2), 'is_default' => 0],
|
||||
['code' => 'deliver', 'name' => '送货到家', 'price' => 50.00, 'is_default' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
// 解析增项
|
||||
$addons = [];
|
||||
if (!empty($row['addons'])) {
|
||||
$addons = is_array($row['addons']) ? $row['addons'] : json_decode($row['addons'], true);
|
||||
}
|
||||
if (empty($addons)) {
|
||||
// 获取类目通用增项
|
||||
$catAddons = Db::name('service_addon')
|
||||
->where('status', 'normal')
|
||||
->where(function($q) use ($row) {
|
||||
$q->where('cat_id', $row['cat_id'])->whereOr('cat_id', 0);
|
||||
})
|
||||
->order('weigh desc, id asc')
|
||||
->limit(8)
|
||||
->select();
|
||||
foreach ($catAddons as $ca) {
|
||||
$addons[] = [
|
||||
'id' => (int)$ca['id'],
|
||||
'name' => $ca['name'],
|
||||
'unit' => $ca['unit'],
|
||||
'price' => (float)$ca['default_price']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'cat_id' => (int)$row['cat_id'],
|
||||
'name' => $row['name'],
|
||||
'cat_name' => isset($row['cat_name']) ? $row['cat_name'] : '',
|
||||
'image' => $row['image'],
|
||||
'image_url' => $row['image'] ? cdnurl($row['image'], true) : '',
|
||||
'price_type' => $type,
|
||||
'price_type_text' => $type === 'fixed' ? '一口价' : ($type === 'quote' ? '师傅报价' : '按面积计价'),
|
||||
'base_price' => (float)$row['base_price'],
|
||||
@@ -489,6 +546,8 @@ class Home extends Base
|
||||
'free_floor' => isset($row['free_floor']) ? (int)$row['free_floor'] : 0,
|
||||
'need_model' => isset($row['need_model']) ? (int)$row['need_model'] : 1,
|
||||
'description' => isset($row['description']) ? $row['description'] : '',
|
||||
'service_types' => $serviceTypes,
|
||||
'addons' => $addons,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,5 +661,128 @@ class Master extends Base
|
||||
unset($row);
|
||||
$this->success('', ['list' => $list]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算两点间距离 (km)
|
||||
*/
|
||||
protected function calculateDistance($lat1, $lon1, $lat2, $lon2)
|
||||
{
|
||||
if (empty($lat1) || empty($lon1) || empty($lat2) || empty($lon2)) {
|
||||
return 14.40;
|
||||
}
|
||||
$earthRadius = 6371;
|
||||
$dLat = deg2rad((float)$lat2 - (float)$lat1);
|
||||
$dLon = deg2rad((float)$lon2 - (float)$lon1);
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2)) *
|
||||
sin($dLon / 2) * sin($dLon / 2);
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
return round($earthRadius * $c, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 师傅端订单抢单详情
|
||||
* @ApiMethod (GET)
|
||||
*/
|
||||
public function orderDetail()
|
||||
{
|
||||
$master = $this->requireMaster();
|
||||
$orderId = (int)$this->request->param('id');
|
||||
if ($orderId <= 0) {
|
||||
$this->error('参数错误');
|
||||
}
|
||||
$order = Db::name('order')->where('id', $orderId)->find();
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
|
||||
$isMyOrder = ((int)$order['master_id'] === (int)$master['id']);
|
||||
$inHall = in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true);
|
||||
if (!$isMyOrder && !$inHall) {
|
||||
$this->error('该订单已被其他师傅接单或已结束');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$quoteCount = (int)Db::name('order_quote')->where('order_id', $orderId)->count();
|
||||
$myQuote = Db::name('order_quote')
|
||||
->where('order_id', $orderId)
|
||||
->where('master_id', $master['id'])
|
||||
->find();
|
||||
|
||||
$maxQuotes = 4;
|
||||
$remainingQuoteSlots = max(0, $maxQuotes - $quoteCount);
|
||||
$viewCount = 2 + ($orderId % 3);
|
||||
$deadline = (int)$order['createtime'] + 3 * 86400;
|
||||
$countdownSeconds = max(0, $deadline - $now);
|
||||
|
||||
$mobile = $order['mobile'];
|
||||
$address = $order['address'];
|
||||
if (!$isMyOrder) {
|
||||
$mobile = substr_replace($mobile, '******', 3, 6);
|
||||
$address = preg_replace('/\d+号|\d+栋|\d+单元|\d+室/', '***', $address);
|
||||
if (mb_strlen($address) > 10) {
|
||||
$address = mb_substr($address, 0, 8) . '****号';
|
||||
}
|
||||
}
|
||||
|
||||
$userLat = (float)$this->request->param('lat', 0);
|
||||
$userLng = (float)$this->request->param('lng', 0);
|
||||
$dist = $this->calculateDistance($userLat, $userLng, (float)$order['lat'], (float)$order['lng']);
|
||||
|
||||
$service = Db::name('service')->where('id', $order['service_id'])->find();
|
||||
$category = Db::name('service_cat')->where('id', $order['cat_id'])->find();
|
||||
|
||||
$goodsImages = [];
|
||||
if (!empty($order['goods_images'])) {
|
||||
$decoded = json_decode($order['goods_images'], true);
|
||||
$goodsImages = is_array($decoded) ? $decoded : [$order['goods_images']];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id' => (int)$order['id'],
|
||||
'order_no' => $order['order_no'],
|
||||
'price_type' => $order['price_type'],
|
||||
'price_type_text' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'quote' ? '师傅竞价' : ($order['price_type'] === 'custom' ? '用户定价' : '标准价')),
|
||||
'status' => (int)$order['status'],
|
||||
'status_text' => OrderStatus::label($order['status']),
|
||||
'total_amount' => (float)$order['total_amount'],
|
||||
'base_amount' => (float)$order['base_amount'],
|
||||
'user_price' => $order['price_type'] === 'custom' ? (float)$order['base_amount'] : (float)$order['total_amount'],
|
||||
'service_name' => $order['service_name'] ?: ($service ? $service['name'] : '家庭维修服务'),
|
||||
'service_image' => $order['service_image'] ?: ($service ? $service['image'] : ''),
|
||||
'goods_images' => $goodsImages,
|
||||
'goods_model' => $order['goods_model'] ?: '标准规格',
|
||||
'goods_num' => (int)$order['goods_num'] ?: 1,
|
||||
'cat_name' => $category ? $category['name'] : '通用',
|
||||
'service_type_tag' => $order['price_type'] === 'fixed' ? '一口价' : ($order['price_type'] === 'custom' ? '用户定价' : '维修'),
|
||||
'remark' => $order['remark'] ?: '无特殊备注',
|
||||
'contact' => $order['contact'] ? mb_substr($order['contact'], 0, 1) . '客户' : '业主客户',
|
||||
'mobile' => $mobile,
|
||||
'is_my_order' => $isMyOrder,
|
||||
'province' => $order['province'],
|
||||
'city' => $order['city'],
|
||||
'district' => $order['district'],
|
||||
'full_address' => $order['province'] . $order['city'] . $order['district'] . $address,
|
||||
'lng' => $order['lng'],
|
||||
'lat' => $order['lat'],
|
||||
'distance_km' => $dist,
|
||||
'appoint_time' => $order['appoint_time'],
|
||||
'appoint_time_text' => $order['appoint_time'] ? date('Y-m-d H:i', (int)$order['appoint_time']) : '接单后与业主预约',
|
||||
'createtime' => (int)$order['createtime'],
|
||||
'createtime_text' => date('Y-m-d H:i', (int)$order['createtime']),
|
||||
'view_count' => $viewCount,
|
||||
'quote_count' => $quoteCount,
|
||||
'remaining_quote_slots' => $remainingQuoteSlots,
|
||||
'countdown_seconds' => $countdownSeconds,
|
||||
'my_quote' => $myQuote ? [
|
||||
'id' => (int)$myQuote['id'],
|
||||
'amount' => (float)$myQuote['amount'],
|
||||
'description' => $myQuote['description'],
|
||||
'status' => $myQuote['status'],
|
||||
] : null,
|
||||
'can_grab' => ($order['price_type'] !== 'quote' && (int)$order['status'] === OrderStatus::PENDING_ACCEPT && (int)$order['master_id'] === 0),
|
||||
'can_quote' => ($order['price_type'] === 'quote' && in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true) && (int)$order['master_id'] === 0 && !$myQuote),
|
||||
];
|
||||
|
||||
$this->success('', $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ return array (
|
||||
'name' => '优匠优服',
|
||||
'beian' => '',
|
||||
'cdnurl' => '',
|
||||
'version' => '1.0.3',
|
||||
'version' => '1.0.9',
|
||||
'timezone' => 'Asia/Shanghai',
|
||||
'forbiddenip' => '',
|
||||
'languages' =>
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
use think\Route;
|
||||
|
||||
Route::rule('service/cat/:action', 'admin/service.cat/:action');
|
||||
Route::rule('service/cat', 'admin/service.cat/index');
|
||||
|
||||
Route::rule('service.cat/:action', 'admin/service.cat/:action');
|
||||
Route::rule('service.cat', 'admin/service.cat/index');
|
||||
|
||||
return [
|
||||
//别名配置,别名只能是映射到控制器且访问时必须加上请求的方法
|
||||
'__alias__' => [
|
||||
|
||||
@@ -216,6 +216,7 @@
|
||||
function onChooseSpec(spec) {
|
||||
const goods = currentGoods.value;
|
||||
if (!goods) return;
|
||||
const defType = (goods.service_types && goods.service_types.find(st => st.is_default)) || (goods.service_types && goods.service_types[0]) || { code: 'install', name: '安装' };
|
||||
emit('select', {
|
||||
service_id: goods.id,
|
||||
item_name: goods.name,
|
||||
@@ -226,13 +227,18 @@
|
||||
unit: goods.unit || '件',
|
||||
base_price: Number(spec.base_price) || Number(goods.base_price) || 0,
|
||||
price_type: goods.price_type || 'fixed',
|
||||
description: spec.description || goods.description || ''
|
||||
description: spec.description || goods.description || '',
|
||||
service_types: goods.service_types || [],
|
||||
addons: goods.addons || [],
|
||||
default_service_type: defType.code,
|
||||
default_service_name: defType.name
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
function onChooseDefault(goods) {
|
||||
if (!goods) return;
|
||||
const defType = (goods.service_types && goods.service_types.find(st => st.is_default)) || (goods.service_types && goods.service_types[0]) || { code: 'install', name: '安装' };
|
||||
emit('select', {
|
||||
service_id: goods.id,
|
||||
item_name: goods.name,
|
||||
@@ -243,7 +249,11 @@
|
||||
unit: goods.unit || '件',
|
||||
base_price: Number(goods.base_price) || 0,
|
||||
price_type: goods.price_type || 'fixed',
|
||||
description: goods.description || ''
|
||||
description: goods.description || '',
|
||||
service_types: goods.service_types || [],
|
||||
addons: goods.addons || [],
|
||||
default_service_type: defType.code,
|
||||
default_service_name: defType.name
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<view class="yjyf-master-tabbar-container">
|
||||
<view class="tabbar">
|
||||
<!-- 1. 首页 (抢单大厅) -->
|
||||
<view class="tab" :class="{ on: current === 0 }" @tap="switchTab(0)">
|
||||
<image class="tab-icon" :src="current === 0 ? '/static/tabbar/master/home-active.svg' : '/static/tabbar/master/home.svg'" mode="aspectFit" />
|
||||
<text class="tab-txt">首页</text>
|
||||
</view>
|
||||
|
||||
<!-- 2. 订单 (工单管理) -->
|
||||
<view class="tab" :class="{ on: current === 1 }" @tap="switchTab(1)">
|
||||
<image class="tab-icon" :src="current === 1 ? '/static/tabbar/master/order-active.svg' : '/static/tabbar/master/order.svg'" mode="aspectFit" />
|
||||
<text class="tab-txt">订单</text>
|
||||
</view>
|
||||
|
||||
<!-- 3. 消息 -->
|
||||
<view class="tab" :class="{ on: current === 2 }" @tap="switchTab(2)">
|
||||
<image class="tab-icon" :src="current === 2 ? '/static/tabbar/master/message-active.svg' : '/static/tabbar/master/message.svg'" mode="aspectFit" />
|
||||
<text class="tab-txt">消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 4. 我的 (师傅中心) -->
|
||||
<view class="tab" :class="{ on: current === 3 }" @tap="switchTab(3)">
|
||||
<image class="tab-icon" :src="current === 3 ? '/static/tabbar/master/mine-active.svg' : '/static/tabbar/master/mine.svg'" mode="aspectFit" />
|
||||
<text class="tab-txt">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部防遮挡垫高占位 -->
|
||||
<view class="tabbar-placeholder"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
current: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
const TAB_ROUTES = [
|
||||
'/pages/master/hall',
|
||||
'/pages/master/orders',
|
||||
'/pages/message/message',
|
||||
'/pages/master/workbench'
|
||||
];
|
||||
|
||||
function switchTab(index) {
|
||||
if (props.current === index) return;
|
||||
const targetUrl = TAB_ROUTES[index];
|
||||
if (targetUrl) {
|
||||
uni.redirectTo({
|
||||
url: targetUrl,
|
||||
fail: () => {
|
||||
uni.reLaunch({ url: targetUrl });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.yjyf-master-tabbar-container {
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.tabbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
box-sizing: content-box !important;
|
||||
height: 104rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid #ECECEC;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
height: 104rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-txt {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
color: #8E8E93;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.tab.on .tab-txt {
|
||||
color: #F0710D;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tabbar-placeholder {
|
||||
height: 104rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: content-box !important;
|
||||
}
|
||||
</style>
|
||||
@@ -118,6 +118,14 @@
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/master/detail",
|
||||
"style": { "navigationStyle": "custom", "backgroundColor": "#F4F5F7" }
|
||||
},
|
||||
{
|
||||
"path": "pages/master/success",
|
||||
"style": { "navigationStyle": "custom", "backgroundColor": "#FFFFFF" }
|
||||
}
|
||||
],
|
||||
"easycom": {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
@tap="selectCategory(item)"
|
||||
>
|
||||
<view class="cat-icon-wrap">
|
||||
<image class="cat-icon-img" :src="'/static/icons/' + (item.iconKey || 'sofa') + '.png'" mode="aspectFit" />
|
||||
<image class="cat-icon-img" :src="item.icon_url || ('/static/icons/' + (item.iconKey || 'sofa') + '.png')" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="cat-name">{{ item.name }}</text>
|
||||
</view>
|
||||
@@ -132,21 +132,31 @@
|
||||
|
||||
function onSpecSelected(spec) {
|
||||
showDrawer.value = false;
|
||||
const rawAddons = spec.addons && spec.addons.length ? spec.addons : [];
|
||||
const extras = rawAddons.map(ad => ({
|
||||
id: ad.id,
|
||||
key: 'addon_' + ad.id,
|
||||
name: ad.name,
|
||||
unit: ad.unit || '个',
|
||||
price: Number(ad.price) || 0,
|
||||
num: 0
|
||||
}));
|
||||
|
||||
const firstGoods = {
|
||||
service_id: spec.service_id,
|
||||
item_name: spec.item_name,
|
||||
spec_id: spec.spec_id || 0,
|
||||
spec_name: spec.spec_name,
|
||||
cat_id: spec.cat_id || currentDrawerCatId.value,
|
||||
cat_name: spec.cat_name || currentDrawerCatName.value,
|
||||
unit: spec.unit || '件',
|
||||
base_price: Number(spec.base_price) || 0,
|
||||
num: 1,
|
||||
service_type: 'install',
|
||||
service_type: spec.default_service_type || 'install',
|
||||
service_type_name: spec.default_service_name || '安装',
|
||||
service_types: spec.service_types || [],
|
||||
images: [],
|
||||
extras: [
|
||||
{ key: 'tear', name: '拆旧', price: 50, num: 0 },
|
||||
{ key: 'cut', name: '切割打孔', price: 20, num: 0 }
|
||||
]
|
||||
extras: extras
|
||||
};
|
||||
uni.setStorageSync('yjyf_first_goods', firstGoods);
|
||||
try {
|
||||
@@ -193,6 +203,7 @@
|
||||
allCats.value = res.list.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
icon_url: c.icon_url || '',
|
||||
iconKey: c.icon || 'sofa'
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,44 +1,91 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 信用指标面板 -->
|
||||
<view class="card" v-if="credit">
|
||||
<view class="card-title">信用指标</view>
|
||||
<view class="card-head">
|
||||
<text class="card-title">信用服务指标</text>
|
||||
<text class="card-level">等级 Lv{{ credit.level }}</text>
|
||||
</view>
|
||||
<view class="grid">
|
||||
<view class="g"><text class="g-num">{{ credit.score }}</text><text class="g-lab">综合评分</text></view>
|
||||
<view class="g"><text class="g-num">{{ credit.accept_rate }}%</text><text class="g-lab">接单率</text></view>
|
||||
<view class="g"><text class="g-num">{{ credit.ontime_rate }}%</text><text class="g-lab">准时率</text></view>
|
||||
<view class="g"><text class="g-num">{{ credit.good_rate }}%</text><text class="g-lab">好评率</text></view>
|
||||
<view class="g"><text class="g-num">{{ credit.complaint_rate }}%</text><text class="g-lab">投诉率</text></view>
|
||||
<view class="g"><text class="g-num">Lv{{ credit.level }}</text><text class="g-lab">服务等级</text></view>
|
||||
<view class="g"><text class="g-num">{{ credit.appeal_total }}</text><text class="g-lab">累计申诉</text></view>
|
||||
</view>
|
||||
<view class="rules">
|
||||
<text class="rule" v-for="(r, i) in credit.rules" :key="i">· {{ r }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申诉表单 -->
|
||||
<view class="card">
|
||||
<view class="card-title">提交申诉</view>
|
||||
<view class="card-title">提交争议申诉</view>
|
||||
<view class="types">
|
||||
<view v-for="t in types" :key="t.key" class="type" :class="{ on: form.type === t.key }" @tap="form.type = t.key">{{ t.name }}</view>
|
||||
<view v-for="t in types" :key="t.key" class="type clickable-hover" :class="{ on: form.type === t.key }" @tap="form.type = t.key">
|
||||
{{ t.name }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关联订单 -->
|
||||
<view class="order-fi">
|
||||
<text class="order-label">关联工单ID</text>
|
||||
<input class="order-input" type="number" v-model="form.order_id" placeholder="选填,关联争议订单ID或订单号" />
|
||||
</view>
|
||||
|
||||
<!-- 详细说明 -->
|
||||
<textarea class="ta" v-model="form.content" maxlength="300" placeholder="请详细描述申诉情况(如:客户恶意差评、因特殊情况改期误判违约、订单扣款异议等)" />
|
||||
|
||||
<!-- 上传凭证图片 -->
|
||||
<view class="upload-section">
|
||||
<text class="upload-label">上传举证截图(聊天记录 / 现场照片)</text>
|
||||
<view class="img-picker">
|
||||
<view class="img-item" v-for="(img, idx) in evidenceImgs" :key="idx">
|
||||
<image class="thumb" :src="img" mode="aspectFill" />
|
||||
<view class="del-btn" @tap="delImg(idx)">×</view>
|
||||
</view>
|
||||
<view class="add-btn clickable-hover" v-if="evidenceImgs.length < 3" @tap="chooseEvidence">
|
||||
<image class="add-ico" src="/static/icons/camera.svg" mode="aspectFit" />
|
||||
<text class="add-txt">添加凭据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn clickable-hover" :class="{ disabled: submitting }" @tap="submit">
|
||||
{{ submitting ? '提交中...' : '提交申诉' }}
|
||||
</view>
|
||||
<textarea class="ta" v-model="form.content" maxlength="300" placeholder="请描述申诉情况(如:客户恶意差评、订单误判扣款等)" />
|
||||
<view class="btn" :class="{ disabled: submitting }" @tap="submit">{{ submitting ? '提交中...' : '提交申诉' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 申诉记录 -->
|
||||
<view class="card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">申诉记录</text>
|
||||
<text class="card-more">{{ list.length }} 条</text>
|
||||
<text class="card-title">申诉处理记录</text>
|
||||
<text class="card-more" v-if="list.length">{{ list.length }} 条</text>
|
||||
</view>
|
||||
<view class="ap" v-for="a in list" :key="a.id">
|
||||
<view class="ap-top">
|
||||
<text class="ap-type">{{ a.type_text }}</text>
|
||||
<text class="ap-status" :class="'st-' + a.status">{{ a.status_text }}</text>
|
||||
|
||||
<view class="ap-list" v-if="list.length">
|
||||
<view class="ap" v-for="a in list" :key="a.id">
|
||||
<view class="ap-top">
|
||||
<view class="ap-type-box">
|
||||
<text class="ap-type">{{ a.type_text }}</text>
|
||||
<text class="ap-oid" v-if="a.order_id">#{{ a.order_id }}</text>
|
||||
</view>
|
||||
<text class="ap-status" :class="'st-' + a.status">{{ a.status_text }}</text>
|
||||
</view>
|
||||
<text class="ap-content">{{ a.content }}</text>
|
||||
<text class="ap-time">{{ a.time_text }}</text>
|
||||
<view class="ap-reply" v-if="a.reply">
|
||||
<text class="reply-tag">平台答复:</text>
|
||||
<text class="reply-txt">{{ a.reply }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="ap-content">{{ a.content }}</text>
|
||||
<text class="ap-time">{{ a.time_text }}</text>
|
||||
<text class="ap-reply" v-if="a.reply">平台回复:{{ a.reply }}</text>
|
||||
</view>
|
||||
<view class="empty" v-if="!list.length">暂无申诉记录</view>
|
||||
|
||||
<view class="empty-state" v-else>
|
||||
<image class="empty-img" src="/static/icons/master/empty-orders.svg" mode="aspectFit" />
|
||||
<text class="empty-txt">暂无申诉记录,服务良好无争议</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -51,7 +98,8 @@
|
||||
const credit = ref(null);
|
||||
const list = ref([]);
|
||||
const submitting = ref(false);
|
||||
const form = reactive({ type: 'misjudge', content: '' });
|
||||
const evidenceImgs = ref([]);
|
||||
const form = reactive({ type: 'misjudge', order_id: '', content: '' });
|
||||
|
||||
const types = [
|
||||
{ key: 'misjudge', name: '误判申诉' },
|
||||
@@ -68,17 +116,52 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function chooseEvidence() {
|
||||
uni.chooseImage({
|
||||
count: 3 - evidenceImgs.value.length,
|
||||
success: async (res) => {
|
||||
for (const path of res.tempFilePaths) {
|
||||
uni.showLoading({ title: '凭证上传中...' });
|
||||
try {
|
||||
const up = await api.upload(path);
|
||||
const imgUrl = up.fullurl || up.url;
|
||||
if (imgUrl) {
|
||||
evidenceImgs.value.push(imgUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delImg(idx) {
|
||||
evidenceImgs.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (submitting.value) return;
|
||||
if (form.content.trim().length < 5) {
|
||||
uni.showToast({ title: '请至少填写 5 个字', icon: 'none' });
|
||||
uni.showToast({ title: '请至少填写 5 个字申诉说明', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await api.masterAppeal({ type: form.type, content: form.content });
|
||||
uni.showToast({ title: '申诉已提交' });
|
||||
let finalContent = form.content.trim();
|
||||
if (evidenceImgs.value.length) {
|
||||
finalContent += '\n[举证图片: ' + evidenceImgs.value.join(' , ') + ']';
|
||||
}
|
||||
await api.masterAppeal({
|
||||
type: form.type,
|
||||
order_id: Number(form.order_id) || 0,
|
||||
content: finalContent
|
||||
});
|
||||
uni.showToast({ title: '申诉已提交,平台将尽快审核' });
|
||||
form.content = '';
|
||||
form.order_id = '';
|
||||
evidenceImgs.value = [];
|
||||
load();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
@@ -94,28 +177,80 @@
|
||||
.card { padding: 24rpx; background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.card-level { font-size: 22rpx; color: #F0710D; font-weight: 600; background: #FFF3E6; padding: 4rpx 14rpx; border-radius: 12rpx; }
|
||||
.card-more { font-size: 22rpx; color: #9A9A9A; }
|
||||
|
||||
.grid { display: flex; flex-wrap: wrap; margin-top: 16rpx; }
|
||||
.g { width: 33.33%; display: flex; flex-direction: column; align-items: center; padding: 14rpx 0; }
|
||||
.g-num { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.g-lab { margin-top: 4rpx; font-size: 20rpx; color: #9A9A9A; }
|
||||
.rules { margin-top: 14rpx; padding-top: 16rpx; border-top: 1rpx solid #F4F4F4; display: flex; flex-direction: column; }
|
||||
.rule { font-size: 20rpx; color: #9A9A9A; line-height: 34rpx; }
|
||||
.types { display: flex; flex-wrap: wrap; padding: 14rpx 0 6rpx; }
|
||||
.type { padding: 0 22rpx; line-height: 56rpx; margin: 0 12rpx 12rpx 0; border-radius: 28rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
|
||||
.type.on { background: #FFF3E6; color: #E36B0C; font-weight: 600; }
|
||||
.ta { width: 100%; height: 160rpx; margin-top: 10rpx; font-size: 26rpx; color: #333; line-height: 38rpx; }
|
||||
.btn { height: 88rpx; margin-top: 12rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||||
.g-num { font-size: 34rpx; font-weight: 700; color: #F0710D; line-height: 1.1; }
|
||||
.g-lab { margin-top: 6rpx; font-size: 20rpx; color: #8A8A8E; }
|
||||
|
||||
.rules { margin-top: 16rpx; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; display: flex; flex-direction: column; }
|
||||
.rule { font-size: 20rpx; color: #9A9A9A; line-height: 32rpx; }
|
||||
|
||||
.types { display: flex; margin-top: 18rpx; }
|
||||
.type {
|
||||
flex: 1; text-align: center; height: 60rpx; line-height: 60rpx;
|
||||
margin-right: 10rpx; border-radius: 30rpx; background: #F5F6F8;
|
||||
font-size: 24rpx; color: #666;
|
||||
}
|
||||
.type:last-child { margin-right: 0; }
|
||||
.type.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; font-weight: 600; }
|
||||
|
||||
.order-fi {
|
||||
display: flex; align-items: center; margin-top: 18rpx;
|
||||
height: 72rpx; background: #FAFAFA; border-radius: 12rpx; padding: 0 16rpx;
|
||||
}
|
||||
.order-label { width: 160rpx; font-size: 24rpx; color: #666; }
|
||||
.order-input { flex: 1; font-size: 24rpx; color: #222; }
|
||||
|
||||
.ta {
|
||||
width: 100%; height: 160rpx; margin-top: 16rpx; padding: 16rpx; box-sizing: border-box;
|
||||
background: #FAFAFA; border-radius: 12rpx; font-size: 26rpx; color: #333; line-height: 38rpx;
|
||||
}
|
||||
|
||||
.upload-section { margin-top: 18rpx; }
|
||||
.upload-label { font-size: 22rpx; color: #666; }
|
||||
.img-picker { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
|
||||
.img-item { width: 130rpx; height: 130rpx; border-radius: 12rpx; position: relative; margin-right: 14rpx; margin-bottom: 14rpx; }
|
||||
.img-item .thumb { width: 100%; height: 100%; border-radius: 12rpx; }
|
||||
.del-btn {
|
||||
position: absolute; right: -8rpx; top: -8rpx; width: 34rpx; height: 34rpx;
|
||||
background: rgba(0,0,0,0.6); color: #fff; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 24rpx;
|
||||
}
|
||||
.add-btn {
|
||||
width: 130rpx; height: 130rpx; border-radius: 12rpx; border: 2rpx dashed #DCDCDC;
|
||||
background: #FAFAFA; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
.add-ico { width: 36rpx; height: 36rpx; margin-bottom: 6rpx; }
|
||||
.add-txt { font-size: 18rpx; color: #9A9A9A; }
|
||||
|
||||
.btn {
|
||||
height: 84rpx; margin-top: 24rpx; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 42rpx; color: #fff; font-size: 28rpx; font-weight: 600;
|
||||
background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%);
|
||||
box-shadow: 0 6rpx 16rpx rgba(240,113,13,0.25);
|
||||
}
|
||||
.btn.disabled { opacity: .7; }
|
||||
|
||||
.ap-list { display: flex; flex-direction: column; }
|
||||
.ap { padding: 18rpx 0; border-bottom: 1rpx solid #F4F4F4; display: flex; flex-direction: column; }
|
||||
.ap:last-of-type { border-bottom: none; }
|
||||
.ap:last-child { border-bottom: none; }
|
||||
.ap-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.ap-type { font-size: 26rpx; font-weight: 600; color: #333; }
|
||||
.ap-status { font-size: 22rpx; color: #F0710D; }
|
||||
.ap-type-box { display: flex; align-items: center; }
|
||||
.ap-type { font-size: 26rpx; font-weight: 600; color: #1F1F1F; }
|
||||
.ap-oid { margin-left: 8rpx; font-size: 20rpx; color: #F0710D; background: #FFF3E6; padding: 2rpx 8rpx; border-radius: 6rpx; }
|
||||
.ap-status { font-size: 22rpx; color: #4A7CFF; }
|
||||
.ap-status.st-done { color: #2FA84F; }
|
||||
.ap-status.st-reject { color: #9A9A9A; }
|
||||
.ap-content { margin-top: 8rpx; font-size: 24rpx; color: #666; line-height: 34rpx; }
|
||||
.ap-time { margin-top: 6rpx; font-size: 20rpx; color: #C4C4C4; }
|
||||
.ap-reply { margin-top: 8rpx; padding: 12rpx 16rpx; background: #F7F7F7; border-radius: 10rpx; font-size: 22rpx; color: #666; }
|
||||
.empty { padding: 60rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.ap-status.st-reject { color: #E8331B; }
|
||||
.ap-content { margin-top: 8rpx; font-size: 24rpx; color: #555; line-height: 36rpx; }
|
||||
.ap-time { margin-top: 6rpx; font-size: 20rpx; color: #B0B0B0; }
|
||||
.ap-reply { margin-top: 10rpx; padding: 12rpx 16rpx; background: #F8F9FB; border-radius: 10rpx; }
|
||||
.reply-tag { font-size: 20rpx; color: #4A7CFF; font-weight: 600; }
|
||||
.reply-txt { font-size: 22rpx; color: #333; line-height: 32rpx; }
|
||||
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 60rpx 0 20rpx; }
|
||||
.empty-img { width: 140rpx; height: 140rpx; margin-bottom: 16rpx; }
|
||||
.empty-txt { font-size: 24rpx; color: #9A9A9A; }
|
||||
</style>
|
||||
|
||||
1074
yjyf-app/pages/master/detail.vue
普通文件
@@ -1,92 +1,219 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="top">
|
||||
<view class="top-l">
|
||||
<text class="top-t">订单大厅</text>
|
||||
<text class="top-s">{{ info ? '评分 ' + info.score + ' · 完成 ' + info.order_count + ' 单' : '加载中' }}</text>
|
||||
</view>
|
||||
<view class="switch" :class="{ off: info && info.work_status === 'off' }" @tap="toggleWork">
|
||||
<text class="switch-t">{{ info && info.work_status === 'off' ? '已暂停接单' : '接单中' }}</text>
|
||||
<!-- 紧凑师傅状态栏 -->
|
||||
<view class="hall-header">
|
||||
<view class="header-main">
|
||||
<view class="h-user">
|
||||
<view class="name-line">
|
||||
<text class="h-name">{{ info ? info.realname : '优匠师傅' }}</text>
|
||||
<view class="level-badge" v-if="info">
|
||||
<text class="level-txt">Lv{{ info.level }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="h-stats">{{ info ? ('综合评分 ' + info.score + ' · 累计完成 ' + info.order_count + ' 单') : '优匠优服师傅工作端' }}</text>
|
||||
</view>
|
||||
<view class="work-toggle clickable-hover" :class="{ paused: info && info.work_status === 'off' }" @tap="toggleWork">
|
||||
<view class="dot"></view>
|
||||
<text class="toggle-txt">{{ info && info.work_status === 'off' ? '已暂停' : '听单中' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filters">
|
||||
<view class="ftabs">
|
||||
<view v-for="t in typeTabs" :key="t.key" class="ftab" :class="{ on: type === t.key }" @tap="type = t.key">{{ t.name }}</view>
|
||||
<!-- 跑马灯动态通知广播(对标工奇兵广播条) -->
|
||||
<view class="broadcast-bar" v-if="list.length">
|
||||
<image class="broadcast-ico" src="/static/icons/nav-bell.svg" mode="aspectFit" />
|
||||
<swiper class="broadcast-swiper" :vertical="true" :autoplay="true" :interval="3500" :circular="true">
|
||||
<swiper-item class="swiper-item" v-for="(item, idx) in broadcastList" :key="idx">
|
||||
<text class="broadcast-txt">{{ item }}</text>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 工奇兵风格筛选导航栏:全部 / 一口价 / 师傅竞价 / 用户定价 -->
|
||||
<view class="filter-sticky">
|
||||
<view class="type-tabs">
|
||||
<view v-for="t in typeTabs" :key="t.key" class="t-tab clickable-hover" :class="{ on: type === t.key }" @tap="type = t.key">
|
||||
<text class="t-txt">{{ t.name }}</text>
|
||||
<view class="t-dot" v-if="t.badge && list.length"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sorts">
|
||||
<view v-for="s in sortTabs" :key="s.key" class="sort" :class="{ on: sort === s.key }" @tap="sort = s.key">{{ s.name }}</view>
|
||||
<view class="sort-tabs">
|
||||
<view v-for="s in sortTabs" :key="s.key" class="s-tab clickable-hover" :class="{ on: sort === s.key }" @tap="sort = s.key">
|
||||
{{ s.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表(工奇兵高密度信息流卡片,点击卡片进详情) -->
|
||||
<view class="list">
|
||||
<view class="item" v-for="o in list" :key="o.id">
|
||||
<view class="item clickable-hover" v-for="o in list" :key="o.id" @tap="goDetail(o)">
|
||||
<view class="hd">
|
||||
<text class="tag-urgent" v-if="Number(o.urgent)">急单</text>
|
||||
<text class="tag-mode quote" v-if="o.price_type === 'quote'">师傅报价</text>
|
||||
<text class="tag-mode custom" v-if="o.price_type === 'custom'">用户定价</text>
|
||||
<text class="svc">{{ o.service_name }}</text>
|
||||
<text class="amt">¥{{ o.pay_amount }}</text>
|
||||
<view class="hd-left">
|
||||
<text class="tag-urgent" v-if="Number(o.urgent)">急单</text>
|
||||
<text class="tag-mode quote" v-if="o.price_type === 'quote'">师傅竞价</text>
|
||||
<text class="tag-mode custom" v-if="o.price_type === 'custom'">用户定价</text>
|
||||
<text class="tag-mode fixed" v-if="o.price_type === 'fixed'">一口价</text>
|
||||
<text class="svc">{{ o.service_name }}</text>
|
||||
</view>
|
||||
<view class="amt-box">
|
||||
<text class="amt-symbol" v-if="o.price_type !== 'quote'">¥</text>
|
||||
<text class="amt">{{ o.price_type === 'quote' ? '竞价中' : o.pay_amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bd">
|
||||
<text class="addr">{{ o.address }}</text>
|
||||
<text class="meta">楼层 {{ o.floor }} 层 · {{ Number(o.has_elevator) ? '有电梯' : '无电梯' }} · 预约 {{ o.appoint_text }}</text>
|
||||
<text class="meta" v-if="o.goods_model">型号:{{ o.goods_model }}</text>
|
||||
<text class="meta" v-if="o.remark">备注:{{ o.remark }}</text>
|
||||
<!-- 地址与距离 -->
|
||||
<view class="info-row">
|
||||
<image class="row-icon" src="/static/icons/master/order-loc.svg" mode="aspectFit" />
|
||||
<text class="row-txt addr-txt">{{ o.address }}</text>
|
||||
<text class="distance-txt" v-if="o.distance_text">{{ o.distance_text }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约上门时间 -->
|
||||
<view class="info-row">
|
||||
<image class="row-icon" src="/static/icons/master/order-time.svg" mode="aspectFit" />
|
||||
<text class="row-txt">期望上门:{{ o.appoint_text }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 服务规格与条件微徽标 -->
|
||||
<view class="badge-row">
|
||||
<text class="badge">楼层 {{ o.floor }} 层</text>
|
||||
<text class="badge">{{ Number(o.has_elevator) ? '有电梯' : '步梯无电梯' }}</text>
|
||||
<text class="badge" v-if="o.goods_model">型号:{{ o.goods_model }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 客户工单要求备注 -->
|
||||
<view class="remark-row" v-if="o.remark">
|
||||
<text class="remark-tag">工单要求</text>
|
||||
<text class="remark-txt">{{ o.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft">
|
||||
<text class="phone">{{ o.left_seconds > 0 ? ('剩余 ' + countdown(o.left_seconds)) : '即将关闭' }}</text>
|
||||
|
||||
<view class="ft" @tap.stop>
|
||||
<view class="time-left">
|
||||
<text class="left-label">有效倒计时:</text>
|
||||
<text class="left-val" :class="{ urgent: o.left_seconds <= 1800 }">
|
||||
{{ o.left_seconds > 0 ? countdown(o.left_seconds) : '即将超时' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="ft-btns">
|
||||
<view class="quote-btn" v-if="o.price_type === 'quote'" @tap="goQuote(o)">我要报价({{ o.quote_count }})</view>
|
||||
<view class="grab" v-else @tap="grab(o)">{{ o.price_type === 'custom' ? '按此价接单' : '立即抢单' }}</view>
|
||||
<view class="quote-btn clickable-hover" v-if="o.price_type === 'quote'" @tap="goDetail(o)">
|
||||
立即报价({{ o.quote_count }})
|
||||
</view>
|
||||
<view class="grab-btn clickable-hover" v-else @tap="openGrabConfirm(o)">
|
||||
{{ o.price_type === 'custom' ? '按此价接单' : '立即抢单' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-if="!loading && !list.length"><text>暂无待接订单,稍后刷新试试</text></view>
|
||||
<view class="refresh" @tap="load">刷新大厅</view>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && !list.length">
|
||||
<image class="empty-img" src="/static/icons/master/empty-orders.svg" mode="aspectFit" />
|
||||
<text class="empty-title">当前大厅暂无待接工单</text>
|
||||
<text class="empty-sub">保持听单中状态,有新订单将第一时间呈现</text>
|
||||
<view class="refresh-cta clickable-hover" @tap="load">立即刷新大厅</view>
|
||||
</view>
|
||||
|
||||
<!-- 抢单防误点二次确认弹窗(100% 对标工奇兵实机图3) -->
|
||||
<view class="confirm-mask" v-if="selectedOrder" @tap="selectedOrder = null">
|
||||
<view class="confirm-dialog" @tap.stop>
|
||||
<text class="cd-title">提示</text>
|
||||
<text class="cd-content">请确定已经了解清楚订单服务要求、商品数量、服务地址等信息。您确定可以承揽,并能完成工作,如随意抢单,将影响您的订单推送优先级。</text>
|
||||
<view class="cd-actions">
|
||||
<view class="cd-btn cancel clickable-hover" @tap="selectedOrder = null">
|
||||
<text class="cd-btn-txt cancel">再看看</text>
|
||||
</view>
|
||||
<view class="cd-btn ok clickable-hover" :class="{ disabled: grabbing }" @tap="confirmGrab">
|
||||
<text class="cd-btn-txt ok">{{ grabbing ? '抢单中...' : '确定' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 师傅端专属底部导航 (当前 Tab 0: 首页) -->
|
||||
<YjyfMasterTabBar :current="0" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
|
||||
/** 订单大厅 */
|
||||
import YjyfMasterTabBar from '@/components/YjyfMasterTabBar.vue';
|
||||
|
||||
const list = ref([]);
|
||||
const info = ref(null);
|
||||
const loading = ref(false);
|
||||
const type = ref('all');
|
||||
const sort = ref('new');
|
||||
const selectedOrder = ref(null);
|
||||
const grabbing = ref(false);
|
||||
|
||||
const typeTabs = [
|
||||
{ key: 'all', name: '全部订单' },
|
||||
{ key: 'all', name: '全部订单', badge: true },
|
||||
{ key: 'fixed', name: '一口价' },
|
||||
{ key: 'quote', name: '报价订单' },
|
||||
{ key: 'quote', name: '师傅竞价' },
|
||||
{ key: 'custom', name: '用户定价' }
|
||||
];
|
||||
const sortTabs = [
|
||||
{ key: 'new', name: '最新' },
|
||||
{ key: 'amount', name: '报酬高' },
|
||||
{ key: 'new', name: '最新发布' },
|
||||
{ key: 'amount', name: '报酬最高' },
|
||||
{ key: 'urgent', name: '急单优先' },
|
||||
{ key: 'time', name: '时效近' }
|
||||
{ key: 'time', name: '时效最近' }
|
||||
];
|
||||
|
||||
// 根据真实订单列表动态生成走马灯播报(拒绝假数据)
|
||||
const broadcastList = computed(() => {
|
||||
if (!list.value.length) return [];
|
||||
return list.value.slice(0, 5).map((o) => {
|
||||
const addrPart = (o.address || '本地').slice(0, 6);
|
||||
const priceDesc = o.price_type === 'quote' ? '需师傅竞价' : `报酬 ¥${o.pay_amount}`;
|
||||
return `${addrPart}发布【${o.service_name}】工单,${priceDesc} · 开放接单`;
|
||||
});
|
||||
});
|
||||
|
||||
watch([type, sort], () => load());
|
||||
|
||||
function countdown(sec) {
|
||||
const s = Number(sec) || 0;
|
||||
if (s <= 0) return '已结束';
|
||||
const m = Math.floor(s / 60);
|
||||
return m >= 60 ? (Math.floor(m / 60) + '小时' + (m % 60) + '分') : (m + '分');
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
if (h > 0) {
|
||||
return `${h}小时${m}分`;
|
||||
}
|
||||
return `${m}分钟`;
|
||||
}
|
||||
|
||||
function goQuote(o) {
|
||||
uni.navigateTo({ url: '/pages/master/quote?order_id=' + o.id });
|
||||
function goDetail(o) {
|
||||
uni.navigateTo({ url: '/pages/master/detail?id=' + o.id });
|
||||
}
|
||||
|
||||
function openGrabConfirm(o) {
|
||||
selectedOrder.value = o;
|
||||
}
|
||||
|
||||
async function confirmGrab() {
|
||||
if (grabbing.value || !selectedOrder.value) return;
|
||||
grabbing.value = true;
|
||||
const o = selectedOrder.value;
|
||||
try {
|
||||
const res = await api.masterGrab({ order_id: o.id });
|
||||
selectedOrder.value = null;
|
||||
uni.showToast({ title: '抢单成功!', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/master/success?order_id=${o.id}&mobile=${res.user_mobile || ''}`
|
||||
});
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.msg || '手慢了,该订单已被抢', icon: 'none' });
|
||||
} finally {
|
||||
grabbing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -110,66 +237,497 @@
|
||||
try {
|
||||
await api.masterToggle({ work_status: next });
|
||||
info.value.work_status = next;
|
||||
uni.showToast({ title: next === 'on' ? '已开启接单' : '已暂停接单', icon: 'none' });
|
||||
uni.showToast({ title: next === 'on' ? '已开启接单听单' : '已暂停接单', icon: 'none' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function grab(o) {
|
||||
uni.showModal({
|
||||
title: '确认抢单',
|
||||
content: '抢单后需在 2 小时内联系客户,否则订单将自动回流大厅',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
try {
|
||||
await api.masterGrab({ order_id: o.id });
|
||||
uni.showToast({ title: '抢单成功' });
|
||||
setTimeout(() => uni.navigateTo({ url: '/pages/master/orders' }), 600);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
onShow(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
onShow(() => load());
|
||||
onPullDownRefresh(() => {
|
||||
load().then(() => uni.stopPullDownRefresh());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 60rpx; }
|
||||
.top {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 34rpx 30rpx;
|
||||
background: linear-gradient(160deg, #FFB85C 0%, #F98D25 60%, #ED6F0E 100%);
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #F4F5F7;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 顶部师傅状态 */
|
||||
.hall-header {
|
||||
background: linear-gradient(135deg, #FFB85C 0%, #F98D25 55%, #ED6F0E 100%);
|
||||
padding: 24rpx 28rpx;
|
||||
}
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.h-user {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.h-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.level-badge {
|
||||
margin-left: 12rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.level-txt {
|
||||
font-size: 20rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
.h-stats {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.work-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFFFF;
|
||||
padding: 0 20rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 26rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.work-toggle.paused {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #2FA84F;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.work-toggle.paused .dot {
|
||||
background: #E8331B;
|
||||
}
|
||||
.toggle-txt {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #E36B0C;
|
||||
}
|
||||
.work-toggle.paused .toggle-txt {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 跑马灯动态通知 */
|
||||
.broadcast-bar {
|
||||
background: #FFF7ED;
|
||||
border-bottom: 1rpx solid #FFEDD5;
|
||||
padding: 12rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.broadcast-ico {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.broadcast-swiper {
|
||||
flex: 1;
|
||||
height: 36rpx;
|
||||
}
|
||||
.swiper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.broadcast-txt {
|
||||
font-size: 22rpx;
|
||||
color: #9A3412;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 筛选标签栏 */
|
||||
.filter-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
padding: 0 12rpx;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
.t-tab {
|
||||
position: relative;
|
||||
padding: 22rpx 20rpx 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.t-txt {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.t-tab.on .t-txt {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1F1F;
|
||||
}
|
||||
.t-tab.on::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20rpx;
|
||||
right: 20rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 2rpx;
|
||||
background: #F0710D;
|
||||
}
|
||||
.t-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: #EF4444;
|
||||
margin-left: 6rpx;
|
||||
margin-top: -14rpx;
|
||||
}
|
||||
|
||||
.sort-tabs {
|
||||
display: flex;
|
||||
padding: 14rpx 24rpx;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
.s-tab {
|
||||
margin-right: 16rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
background: #FFFFFF;
|
||||
border: 1rpx solid #E5E7EB;
|
||||
}
|
||||
.s-tab.on {
|
||||
color: #F0710D;
|
||||
background: #FFF7ED;
|
||||
border-color: #F0710D;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 订单列表 */
|
||||
.list {
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
.item {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.hd-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.tag-urgent {
|
||||
font-size: 20rpx;
|
||||
color: #FFFFFF;
|
||||
background: #EF4444;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-right: 10rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tag-mode {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-right: 10rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tag-mode.fixed {
|
||||
background: #FFF7ED;
|
||||
color: #EA580C;
|
||||
border: 1rpx solid #FFEDD5;
|
||||
}
|
||||
.tag-mode.quote {
|
||||
background: #EFF6FF;
|
||||
color: #2563EB;
|
||||
border: 1rpx solid #DBEAFE;
|
||||
}
|
||||
.tag-mode.custom {
|
||||
background: #ECFDF5;
|
||||
color: #059669;
|
||||
border: 1rpx solid #D1FAE5;
|
||||
}
|
||||
.svc {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1F1F;
|
||||
}
|
||||
.amt-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.amt-symbol {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #EA580C;
|
||||
}
|
||||
.amt {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #EA580C;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bd {
|
||||
padding: 18rpx 0;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.row-icon {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
margin-right: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.row-txt {
|
||||
font-size: 26rpx;
|
||||
color: #4B5563;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
.addr-txt {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.distance-txt {
|
||||
font-size: 22rpx;
|
||||
color: #EA580C;
|
||||
background: #FFF7ED;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-left: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.badge {
|
||||
font-size: 22rpx;
|
||||
color: #6B7280;
|
||||
background: #F3F4F6;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.remark-row {
|
||||
margin-top: 14rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
background: #FFFBEB;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.remark-tag {
|
||||
font-size: 20rpx;
|
||||
color: #B45309;
|
||||
font-weight: 700;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.remark-txt {
|
||||
font-size: 22rpx;
|
||||
color: #92400E;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.ft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.time-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.left-label {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
.left-val {
|
||||
font-size: 22rpx;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
.left-val.urgent {
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.ft-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.quote-btn {
|
||||
height: 64rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #EFF6FF;
|
||||
border: 1rpx solid #BFDBFE;
|
||||
color: #2563EB;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.grab-btn {
|
||||
height: 64rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
background: linear-gradient(135deg, #F98D25 0%, #ED6F0E 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 14rpx rgba(237, 111, 14, 0.3);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 40rpx;
|
||||
}
|
||||
.empty-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
.empty-title {
|
||||
margin-top: 24rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
.empty-sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
.refresh-cta {
|
||||
margin-top: 32rpx;
|
||||
padding: 14rpx 36rpx;
|
||||
background: #FFF7ED;
|
||||
border: 1rpx solid #FFEDD5;
|
||||
color: #EA580C;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
|
||||
/* 抢单二次确认弹窗(100% 对标工奇兵实机图3) */
|
||||
.confirm-mask {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.confirm-dialog {
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 36rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.cd-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1F1F;
|
||||
}
|
||||
.cd-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 44rpx;
|
||||
color: #4B5563;
|
||||
text-align: justify;
|
||||
}
|
||||
.cd-actions {
|
||||
width: 100%;
|
||||
margin-top: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1rpx solid #F3F4F6;
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
.cd-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cd-btn.cancel {
|
||||
border-right: 1rpx solid #F3F4F6;
|
||||
}
|
||||
.cd-btn-txt.cancel {
|
||||
font-size: 30rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
.cd-btn-txt.ok {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #EA580C;
|
||||
}
|
||||
.cd-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.top-l { display: flex; flex-direction: column; }
|
||||
.top-t { font-size: 38rpx; font-weight: 700; color: #fff; }
|
||||
.top-s { margin-top: 8rpx; font-size: 22rpx; color: rgba(255,255,255,0.9); }
|
||||
.switch { padding: 0 26rpx; line-height: 60rpx; border-radius: 30rpx; background: #fff; }
|
||||
.switch.off { background: rgba(255,255,255,0.35); }
|
||||
.switch-t { font-size: 24rpx; font-weight: 600; color: #E36B0C; }
|
||||
.switch.off .switch-t { color: #fff; }
|
||||
.list { padding: 22rpx 28rpx 0; }
|
||||
.item { background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; padding: 22rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
|
||||
.hd { display: flex; align-items: center; padding-bottom: 14rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.tag-urgent { padding: 0 12rpx; line-height: 34rpx; border-radius: 8rpx; background: #FDEEEB; color: #E8331B; font-size: 20rpx; margin-right: 10rpx; }
|
||||
.tag-mode { padding: 0 12rpx; line-height: 34rpx; border-radius: 8rpx; font-size: 20rpx; margin-right: 10rpx; }
|
||||
.tag-mode.quote { background: #F0EDFF; color: #5B3FD8; }
|
||||
.tag-mode.custom { background: #E9F7EE; color: #2FA84F; }
|
||||
.svc { flex: 1; font-size: 30rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.amt { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.bd { display: flex; flex-direction: column; padding: 16rpx 0; }
|
||||
.addr { font-size: 26rpx; color: #333; line-height: 36rpx; }
|
||||
.meta { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; }
|
||||
.ft { display: flex; align-items: center; justify-content: space-between; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; }
|
||||
.phone { font-size: 24rpx; color: #666; }
|
||||
.grab { padding: 0 34rpx; line-height: 68rpx; border-radius: 34rpx; color: #fff; font-size: 26rpx; font-weight: 600; background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%); box-shadow: 0 6rpx 16rpx rgba(240,113,13,0.28); }
|
||||
.empty { padding: 90rpx 0 30rpx; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.filters { padding: 20rpx 28rpx 0; }
|
||||
.ftabs { display: flex; }
|
||||
.ftab { padding: 0 26rpx; line-height: 64rpx; margin-right: 14rpx; border-radius: 32rpx; background: #fff; font-size: 24rpx; color: #666; }
|
||||
.ftab.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; font-weight: 600; }
|
||||
.sorts { display: flex; padding: 16rpx 0 4rpx; }
|
||||
.sort { padding: 0 20rpx; line-height: 52rpx; margin-right: 12rpx; border-radius: 26rpx; background: #fff; font-size: 22rpx; color: #9A9A9A; }
|
||||
.sort.on { background: #FFF3E6; color: #E36B0C; font-weight: 600; }
|
||||
.ft-btns { display: flex; align-items: center; }
|
||||
.quote-btn { padding: 0 30rpx; line-height: 68rpx; border-radius: 34rpx; color: #fff; font-size: 26rpx; font-weight: 600; background: linear-gradient(135deg,#7A5AF8 0%,#5B3FD8 100%); box-shadow: 0 6rpx 16rpx rgba(122,90,248,.28); }
|
||||
.refresh { margin: 10rpx 28rpx; height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: 42rpx; background: #fff; color: #F0710D; font-size: 26rpx; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
@@ -1,54 +1,125 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="tabs">
|
||||
<view v-for="t in tabs" :key="t.key" class="tab" :class="{ on: cur === t.key }" @tap="switchTab(t.key)">
|
||||
<text class="tab-txt">{{ t.name }}</text>
|
||||
<!-- 顶部状态漏斗看板(对标工奇兵 4 态流转漏斗) -->
|
||||
<view class="funnel-card">
|
||||
<view class="funnel-row">
|
||||
<view class="f-item clickable-hover" :class="{ on: cur === 'active' }" @tap="switchTab('active')">
|
||||
<text class="f-num">{{ activeCount }}</text>
|
||||
<text class="f-lab">待处理</text>
|
||||
<view class="f-line" v-if="cur === 'active'"></view>
|
||||
</view>
|
||||
<view class="f-divider"></view>
|
||||
<view class="f-item clickable-hover" :class="{ on: cur === 'verify' }" @tap="switchTab('verify')">
|
||||
<text class="f-num">{{ verifyCount }}</text>
|
||||
<text class="f-lab">待验收</text>
|
||||
<view class="f-line" v-if="cur === 'verify'"></view>
|
||||
</view>
|
||||
<view class="f-divider"></view>
|
||||
<view class="f-item clickable-hover" :class="{ on: cur === 'completed' }" @tap="switchTab('completed')">
|
||||
<text class="f-num">{{ completedCount }}</text>
|
||||
<text class="f-lab">已完成</text>
|
||||
<view class="f-line" v-if="cur === 'completed'"></view>
|
||||
</view>
|
||||
<view class="f-divider"></view>
|
||||
<view class="f-item clickable-hover" @tap="goQuotes">
|
||||
<text class="f-num f-quote">{{ quoteCount }}</text>
|
||||
<text class="f-lab">我的报价</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<!-- 订单列表 -->
|
||||
<view class="list" v-if="list.length">
|
||||
<view class="item" v-for="o in list" :key="o.id">
|
||||
<view class="hd">
|
||||
<text class="svc">{{ o.service_name }}</text>
|
||||
<text class="status" :class="'s' + o.status">{{ o.status_text }}</text>
|
||||
</view>
|
||||
<view class="bd">
|
||||
<text class="addr">{{ o.address }}</text>
|
||||
<text class="meta">预约 {{ o.appoint_text }} · 楼层 {{ o.floor }} 层 · {{ Number(o.has_elevator) ? '有电梯' : '无电梯' }}</text>
|
||||
<view class="row">
|
||||
<text class="phone">客户 {{ o.mobile }}</text>
|
||||
<text class="amt">¥{{ o.pay_amount }}</text>
|
||||
<view class="status-badge" :class="'st-' + o.status">
|
||||
<text class="status-txt">{{ o.status_text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bd">
|
||||
<view class="info-line">
|
||||
<image class="info-ico" src="/static/icons/master/order-loc.svg" mode="aspectFit" />
|
||||
<text class="addr-txt">{{ o.address }}</text>
|
||||
</view>
|
||||
<view class="info-line">
|
||||
<image class="info-ico" src="/static/icons/master/order-time.svg" mode="aspectFit" />
|
||||
<text class="meta-txt">预约时间:{{ o.appoint_text }}</text>
|
||||
</view>
|
||||
<view class="meta-tags">
|
||||
<text class="tag">楼层 {{ o.floor }} 层</text>
|
||||
<text class="tag">{{ Number(o.has_elevator) ? '有电梯' : '步梯无电梯' }}</text>
|
||||
<text class="tag" v-if="o.goods_model">型号:{{ o.goods_model }}</text>
|
||||
</view>
|
||||
<view class="customer-row">
|
||||
<view class="cust-mobile" @tap="call(o)">
|
||||
<image class="phone-ico" src="/static/icons/phone.svg" mode="aspectFit" />
|
||||
<text class="phone-num">客户:{{ o.mobile }}</text>
|
||||
</view>
|
||||
<view class="amt-box">
|
||||
<text class="amt-symbol">¥</text>
|
||||
<text class="amt-val">{{ o.pay_amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作动作条 -->
|
||||
<view class="ops">
|
||||
<view class="op" v-if="Number(o.status) === 3 && !o.contact_time" @tap="act('contact', o)">标记已联系</view>
|
||||
<view class="op" v-if="Number(o.status) === 3" @tap="act('schedule', o)">约定上门</view>
|
||||
<view class="op" v-if="Number(o.status) === 4" @tap="act('arrive', o)">到达打卡</view>
|
||||
<view class="op" v-if="Number(o.status) === 5" @tap="act('start', o)">开始服务</view>
|
||||
<view class="op primary" v-if="Number(o.status) === 6" @tap="act('finish', o)">完工提交</view>
|
||||
<view class="op" v-if="Number(o.status) === 3 || Number(o.status) === 4" @tap="call(o)">打电话</view>
|
||||
<view class="op clickable-hover" v-if="Number(o.status) === 3 && !o.contact_time" @tap="act('contact', o)">标记已联系</view>
|
||||
<view class="op clickable-hover" v-if="Number(o.status) === 3" @tap="act('schedule', o)">约定上门</view>
|
||||
<view class="op clickable-hover" v-if="Number(o.status) === 4" @tap="act('arrive', o)">到达打卡</view>
|
||||
<view class="op clickable-hover" v-if="Number(o.status) === 5" @tap="act('start', o)">开始服务</view>
|
||||
<view class="op primary clickable-hover" v-if="Number(o.status) === 6" @tap="act('finish', o)">完工拍照提交</view>
|
||||
<view class="op clickable-hover" v-if="Number(o.status) === 3 || Number(o.status) === 4" @tap="call(o)">联系客户</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-if="!loading && !list.length"><text>暂无订单</text></view>
|
||||
<!-- 空状态引导 -->
|
||||
<view class="empty-state" v-if="!loading && !list.length">
|
||||
<image class="empty-img" src="/static/icons/master/empty-orders.svg" mode="aspectFit" />
|
||||
<text class="empty-title">当前无{{ curTabText }}工单</text>
|
||||
<text class="empty-sub">前往订单大厅,海量订单实时抢</text>
|
||||
<view class="empty-cta clickable-hover" @tap="goHall">前往大厅抢单</view>
|
||||
</view>
|
||||
|
||||
<!-- 师傅端专属底部导航 (当前 Tab 1: 订单) -->
|
||||
<YjyfMasterTabBar :current="1" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
import YjyfMasterTabBar from '@/components/YjyfMasterTabBar.vue';
|
||||
|
||||
const tabs = [
|
||||
{ key: 'active', name: '待处理' },
|
||||
{ key: 'verify', name: '待验收' },
|
||||
{ key: 'completed', name: '已完成' }
|
||||
];
|
||||
const cur = ref('active');
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const activeCount = ref(0);
|
||||
const verifyCount = ref(0);
|
||||
const completedCount = ref(0);
|
||||
const quoteCount = ref(0);
|
||||
|
||||
const curTabText = computed(() => {
|
||||
const m = { active: '待处理', verify: '待验收', completed: '已完成' };
|
||||
return m[cur.value] || '';
|
||||
});
|
||||
|
||||
async function loadCounts() {
|
||||
try {
|
||||
const infoRes = await api.masterInfo();
|
||||
if (infoRes && infoRes.master) {
|
||||
activeCount.value = infoRes.master.todo_count || 0;
|
||||
verifyCount.value = infoRes.master.verify_count || 0;
|
||||
}
|
||||
const qRes = await api.masterQuotes();
|
||||
quoteCount.value = (qRes.list || []).length;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!isLogin()) { toLogin(); return; }
|
||||
@@ -56,75 +127,193 @@
|
||||
try {
|
||||
const res = await api.masterOrders({ status: cur.value, limit: 20 });
|
||||
list.value = res.list || [];
|
||||
if (cur.value === 'completed') {
|
||||
completedCount.value = (res.list || []).length;
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(k) { cur.value = k; load(); }
|
||||
function call(o) { uni.makePhoneCall({ phoneNumber: o.mobile }); }
|
||||
function switchTab(k) {
|
||||
cur.value = k;
|
||||
load();
|
||||
}
|
||||
|
||||
function goQuotes() {
|
||||
uni.navigateTo({ url: '/pages/master/quote' });
|
||||
}
|
||||
|
||||
function call(o) {
|
||||
if (!o.mobile) return;
|
||||
uni.makePhoneCall({ phoneNumber: String(o.mobile) });
|
||||
}
|
||||
|
||||
function goHall() {
|
||||
uni.redirectTo({ url: '/pages/master/hall' });
|
||||
}
|
||||
|
||||
function act(type, o) {
|
||||
const id = o.id;
|
||||
if (type === 'contact') {
|
||||
api.masterContact({ order_id: id }).then(() => { uni.showToast({ title: '已记录联系' }); load(); }).catch(() => {});
|
||||
api.masterContact({ order_id: id }).then(() => {
|
||||
uni.showToast({ title: '已记录已联系' });
|
||||
load();
|
||||
loadCounts();
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (type === 'arrive') {
|
||||
api.masterArrive({ order_id: id }).then(() => { uni.showToast({ title: '已到达' }); load(); }).catch(() => {});
|
||||
api.masterArrive({ order_id: id }).then(() => {
|
||||
uni.showToast({ title: '已到达打卡' });
|
||||
load();
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (type === 'start') {
|
||||
api.masterStart({ order_id: id }).then(() => { uni.showToast({ title: '已开工' }); load(); }).catch(() => {});
|
||||
api.masterStart({ order_id: id }).then(() => {
|
||||
uni.showToast({ title: '已开始服务' });
|
||||
load();
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (type === 'finish') {
|
||||
uni.showModal({
|
||||
title: '确认完工',
|
||||
content: '提交后进入待验收,需上传现场照片(联调使用示例图)',
|
||||
success: (r) => {
|
||||
if (!r.confirm) return;
|
||||
api.masterFinish({ order_id: id, 'images[0]': 'https://yjyf.gzyakj.com.cn/uploads/sample-finish.jpg' })
|
||||
.then((res) => {
|
||||
uni.showModal({ title: '已提交完工', content: '验收码:' + res.verify_code + '(告知客户)', showCancel: false });
|
||||
load();
|
||||
}).catch(() => {});
|
||||
// 真实拍摄/上传现场完工施工照片
|
||||
uni.showActionSheet({
|
||||
itemList: ['现场拍照', '从手机相册选择'],
|
||||
success: (tapRes) => {
|
||||
const sourceType = tapRes.tapIndex === 0 ? ['camera'] : ['album'];
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: sourceType,
|
||||
success: async (chooseRes) => {
|
||||
const tempFile = chooseRes.tempFilePaths[0];
|
||||
uni.showLoading({ title: '上传施工凭证中...' });
|
||||
try {
|
||||
const uploadData = await api.upload(tempFile);
|
||||
const remoteImg = uploadData.fullurl || uploadData.url;
|
||||
const res = await api.masterFinish({
|
||||
order_id: id,
|
||||
'images[0]': remoteImg
|
||||
});
|
||||
uni.hideLoading();
|
||||
uni.showModal({
|
||||
title: '已提交完工',
|
||||
content: '请将核销验收码【' + res.verify_code + '】告知客户核销确认',
|
||||
showCancel: false
|
||||
});
|
||||
load();
|
||||
loadCounts();
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === 'schedule') {
|
||||
const ts = Math.floor(Date.now() / 1000) + 7200;
|
||||
api.masterSchedule({ order_id: id, appoint_time: ts }).then(() => { uni.showToast({ title: '已约定 2 小时后上门' }); load(); }).catch(() => {});
|
||||
api.masterSchedule({ order_id: id, appoint_time: ts }).then(() => {
|
||||
uni.showToast({ title: '已约定 2 小时后上门' });
|
||||
load();
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
onShow(() => load());
|
||||
onPullDownRefresh(() => { load().then(() => uni.stopPullDownRefresh()); });
|
||||
onShow(() => {
|
||||
load();
|
||||
loadCounts();
|
||||
});
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await Promise.all([load(), loadCounts()]);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 60rpx; }
|
||||
.tabs { display: flex; background: #fff; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); }
|
||||
.tab { flex: 1; text-align: center; padding: 26rpx 0; }
|
||||
.tab-txt { font-size: 28rpx; color: #666; }
|
||||
.tab.on .tab-txt { color: #F0710D; font-weight: 700; }
|
||||
.list { padding: 22rpx 28rpx; }
|
||||
.item { background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; padding: 22rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
|
||||
.hd { display: flex; align-items: center; justify-content: space-between; padding-bottom: 14rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 30rpx; }
|
||||
|
||||
/* 顶部工奇兵风格数据漏斗看板 */
|
||||
.funnel-card {
|
||||
background: #ffffff;
|
||||
padding: 24rpx 20rpx 16rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0,0,0,0.03);
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
.funnel-row { display: flex; align-items: center; justify-content: space-around; }
|
||||
.f-item {
|
||||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||||
position: relative; padding-bottom: 12rpx;
|
||||
}
|
||||
.f-num { font-size: 38rpx; font-weight: 700; color: #666; line-height: 1.1; }
|
||||
.f-quote { color: #7A5AF8; }
|
||||
.f-item.on .f-num { color: #F0710D; }
|
||||
.f-lab { margin-top: 8rpx; font-size: 22rpx; color: #8E8E93; }
|
||||
.f-item.on .f-lab { color: #F0710D; font-weight: 600; }
|
||||
.f-line {
|
||||
position: absolute; bottom: 0; width: 44rpx; height: 6rpx;
|
||||
border-radius: 3rpx; background: #F0710D;
|
||||
}
|
||||
.f-divider { width: 1rpx; height: 38rpx; background: #EEEEEE; }
|
||||
|
||||
/* 列表卡片 */
|
||||
.list { padding: 20rpx 24rpx; }
|
||||
.item {
|
||||
background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; padding: 24rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06);
|
||||
}
|
||||
.hd { display: flex; align-items: center; justify-content: space-between; padding-bottom: 16rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.svc { font-size: 30rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.status { font-size: 24rpx; font-weight: 600; color: #F0710D; }
|
||||
.status.s7 { color: #E8331B; }
|
||||
.status.s8 { color: #2FA84F; }
|
||||
.bd { display: flex; flex-direction: column; padding: 16rpx 0; }
|
||||
.addr { font-size: 26rpx; color: #333; line-height: 36rpx; }
|
||||
.meta { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; }
|
||||
.row { display: flex; align-items: center; justify-content: space-between; margin-top: 10rpx; }
|
||||
.phone { font-size: 24rpx; color: #666; }
|
||||
.amt { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.ops { display: flex; flex-wrap: wrap; justify-content: flex-end; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; }
|
||||
.op { margin: 8rpx 0 0 14rpx; padding: 0 24rpx; line-height: 62rpx; border-radius: 31rpx; border: 2rpx solid #DDD; font-size: 24rpx; color: #666; }
|
||||
.op.primary { border-color: #F0710D; color: #fff; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||||
.empty { padding: 120rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.status-badge { padding: 4rpx 16rpx; border-radius: 20rpx; background: #FFF3E6; }
|
||||
.status-txt { font-size: 22rpx; font-weight: 600; color: #E36B0C; }
|
||||
.status-badge.st-7 { background: #FDEEEB; }
|
||||
.status-badge.st-7 .status-txt { color: #E8331B; }
|
||||
.status-badge.st-8 { background: #E9F7EE; }
|
||||
.status-badge.st-8 .status-txt { color: #2FA84F; }
|
||||
|
||||
.bd { padding: 18rpx 0; display: flex; flex-direction: column; }
|
||||
.info-line { display: flex; align-items: flex-start; margin-bottom: 10rpx; }
|
||||
.info-ico { width: 28rpx; height: 28rpx; margin-right: 10rpx; margin-top: 4rpx; flex-shrink: 0; }
|
||||
.addr-txt { font-size: 26rpx; color: #222; font-weight: 500; line-height: 36rpx; }
|
||||
.meta-txt { font-size: 24rpx; color: #666; line-height: 36rpx; }
|
||||
.meta-tags { display: flex; flex-wrap: wrap; margin-top: 6rpx; }
|
||||
.tag { padding: 4rpx 14rpx; background: #F5F6F8; color: #777; font-size: 20rpx; border-radius: 8rpx; margin-right: 10rpx; margin-bottom: 6rpx; }
|
||||
|
||||
.customer-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-top: 14rpx; padding-top: 14rpx; border-top: 1rpx dashed #EEEEEE;
|
||||
}
|
||||
.cust-mobile { display: flex; align-items: center; }
|
||||
.phone-ico { width: 28rpx; height: 28rpx; margin-right: 8rpx; }
|
||||
.phone-num { font-size: 24rpx; color: #4A7CFF; font-weight: 500; }
|
||||
.amt-box { display: flex; align-items: baseline; }
|
||||
.amt-symbol { font-size: 24rpx; font-weight: 700; color: #F0710D; }
|
||||
.amt-val { font-size: 34rpx; font-weight: 700; color: #F0710D; line-height: 1; }
|
||||
|
||||
.ops { display: flex; flex-wrap: wrap; justify-content: flex-end; padding-top: 16rpx; border-top: 1rpx solid #F4F4F4; }
|
||||
.op {
|
||||
margin: 8rpx 0 0 14rpx; padding: 0 26rpx; height: 60rpx; line-height: 58rpx;
|
||||
border-radius: 30rpx; border: 1.5rpx solid #DDD; font-size: 24rpx; color: #555;
|
||||
}
|
||||
.op.primary {
|
||||
border-color: #F0710D; color: #fff;
|
||||
background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%);
|
||||
box-shadow: 0 4rpx 14rpx rgba(240,113,13,0.25);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 40rpx 40rpx; }
|
||||
.empty-img { width: 180rpx; height: 180rpx; margin-bottom: 24rpx; }
|
||||
.empty-title { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 8rpx; }
|
||||
.empty-sub { font-size: 22rpx; color: #9A9A9A; text-align: center; margin-bottom: 32rpx; }
|
||||
.empty-cta {
|
||||
padding: 0 48rpx; height: 74rpx; line-height: 74rpx; border-radius: 37rpx;
|
||||
background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%);
|
||||
color: #fff; font-size: 26rpx; font-weight: 600;
|
||||
box-shadow: 0 6rpx 16rpx rgba(240,113,13,0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,58 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="card" v-if="orderId">
|
||||
<view class="card-title">提交报价</view>
|
||||
<view class="fi"><text class="fk">报价金额</text><input class="fv" type="digit" v-model="amount" placeholder="请输入你的报价(元)" /></view>
|
||||
<view class="fi"><text class="fk">报价有效期</text>
|
||||
<!-- 针对特定工单提交报价 -->
|
||||
<view class="card form-card" v-if="orderId">
|
||||
<view class="card-head">
|
||||
<text class="card-title">提交工单报价</text>
|
||||
<text class="card-tip">工单 #{{ orderId }}</text>
|
||||
</view>
|
||||
<view class="fi">
|
||||
<text class="fk">报价金额</text>
|
||||
<input class="fv" type="digit" v-model="amount" placeholder="请输入你的报价(元)" />
|
||||
</view>
|
||||
<view class="fi">
|
||||
<text class="fk">报价有效期</text>
|
||||
<view class="seg">
|
||||
<view v-for="h in [24, 48, 72]" :key="h" class="seg-i" :class="{ on: hours === h }" @tap="hours = h">{{ h }}h</view>
|
||||
<view v-for="h in [24, 48, 72]" :key="h" class="seg-i clickable-hover" :class="{ on: hours === h }" @tap="hours = h">{{ h }}h</view>
|
||||
</view>
|
||||
</view>
|
||||
<textarea class="ta" v-model="desc" maxlength="200" placeholder="服务说明:包含项目、材料、预计工期" />
|
||||
<view class="btn" :class="{ disabled: submitting }" @tap="submitQuote">{{ submitting ? '提交中...' : '提交报价' }}</view>
|
||||
<textarea class="ta" v-model="desc" maxlength="200" placeholder="服务说明:包含项目、材料、预计工期等" />
|
||||
<view class="btn clickable-hover" :class="{ disabled: submitting }" @tap="submitQuote">
|
||||
{{ submitting ? '提交中...' : '确认提交报价' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 历史报价记录卡片 -->
|
||||
<view class="card">
|
||||
<view class="card-head"><text class="card-title">我的报价记录</text><text class="card-more">{{ list.length }} 条</text></view>
|
||||
<view class="q-item" v-for="q in list" :key="q.id">
|
||||
<view class="q-top">
|
||||
<text class="q-svc">{{ q.service_name || '订单' }}</text>
|
||||
<text class="q-amt">¥{{ q.amount }}</text>
|
||||
</view>
|
||||
<text class="q-meta">{{ q.order_no }} · 提交 {{ q.time_text }}</text>
|
||||
<text class="q-meta">有效期至 {{ q.expire_text }}</text>
|
||||
<text class="q-status">{{ q.status_text }}</text>
|
||||
<view class="card-head">
|
||||
<text class="card-title">我的报价记录</text>
|
||||
<text class="card-more" v-if="list.length">{{ list.length }} 条记录</text>
|
||||
</view>
|
||||
|
||||
<view class="q-list" v-if="list.length">
|
||||
<view class="q-item" v-for="q in list" :key="q.id">
|
||||
<view class="q-top">
|
||||
<text class="q-svc">{{ q.service_name || '服务工单' }}</text>
|
||||
<view class="amt-box">
|
||||
<text class="amt-symbol">¥</text>
|
||||
<text class="q-amt">{{ q.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="q-meta">工单号:{{ q.order_no }} · 提交于 {{ q.time_text }}</text>
|
||||
<text class="q-meta">有效期至:{{ q.expire_text }}</text>
|
||||
<view class="status-wrap">
|
||||
<text class="q-status">{{ q.status_text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 报价为空引导 -->
|
||||
<view class="empty-state" v-else>
|
||||
<image class="empty-img" src="/static/icons/master/empty-orders.svg" mode="aspectFit" />
|
||||
<text class="empty-title">暂无任何报价记录</text>
|
||||
<text class="empty-sub">去订单大厅寻找支持师傅自主报价的工单吧</text>
|
||||
<view class="go-hall-btn clickable-hover" @tap="goHall">前往订单大厅</view>
|
||||
</view>
|
||||
<view class="empty" v-if="!list.length">暂无报价记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -38,7 +67,7 @@
|
||||
const desc = ref('');
|
||||
const hours = ref(24);
|
||||
const submitting = ref(false);
|
||||
let orderId = 0;
|
||||
const orderId = ref(0);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -47,50 +76,80 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function goHall() {
|
||||
uni.redirectTo({ url: '/pages/master/hall' });
|
||||
}
|
||||
|
||||
async function submitQuote() {
|
||||
if (submitting.value) return;
|
||||
if (!amount.value || Number(amount.value) <= 0) {
|
||||
uni.showToast({ title: '请填写报价金额', icon: 'none' });
|
||||
uni.showToast({ title: '请填写有效报价金额', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await api.masterQuote({ order_id: orderId, amount: amount.value, description: desc.value, valid_hours: hours.value });
|
||||
uni.showToast({ title: '报价已提交' });
|
||||
await api.masterQuote({
|
||||
order_id: orderId.value,
|
||||
amount: amount.value,
|
||||
description: desc.value,
|
||||
valid_hours: hours.value
|
||||
});
|
||||
uni.showToast({ title: '报价提交成功' });
|
||||
amount.value = '';
|
||||
desc.value = '';
|
||||
setTimeout(() => uni.navigateBack(), 600);
|
||||
orderId.value = 0;
|
||||
load();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((o) => { orderId = Number(o.order_id || 0); });
|
||||
onLoad((o) => {
|
||||
if (o && o.order_id) {
|
||||
orderId.value = Number(o.order_id);
|
||||
}
|
||||
});
|
||||
|
||||
onShow(() => load());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding: 22rpx 28rpx 60rpx; }
|
||||
.card { padding: 24rpx; background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6rpx; }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.card-tip { font-size: 22rpx; color: #F0710D; font-weight: 600; }
|
||||
.card-more { font-size: 22rpx; color: #9A9A9A; }
|
||||
|
||||
.fi { display: flex; align-items: center; min-height: 88rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.fk { width: 170rpx; font-size: 26rpx; color: #666; }
|
||||
.fv { flex: 1; height: 88rpx; font-size: 28rpx; color: #222; }
|
||||
.seg { display: flex; }
|
||||
.seg-i { padding: 0 24rpx; line-height: 56rpx; margin-right: 12rpx; border-radius: 28rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
|
||||
.seg-i.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; }
|
||||
.ta { width: 100%; height: 150rpx; margin-top: 16rpx; font-size: 26rpx; color: #333; line-height: 38rpx; }
|
||||
.btn { height: 88rpx; margin-top: 12rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||||
.seg-i { padding: 0 24rpx; line-height: 54rpx; margin-right: 12rpx; border-radius: 27rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
|
||||
.seg-i.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; font-weight: 600; }
|
||||
.ta { width: 100%; height: 150rpx; margin-top: 16rpx; padding: 14rpx; box-sizing: border-box; background: #FAFAFA; border-radius: 12rpx; font-size: 26rpx; color: #333; line-height: 38rpx; }
|
||||
.btn { height: 84rpx; margin-top: 20rpx; display: flex; align-items: center; justify-content: center; border-radius: 42rpx; color: #fff; font-size: 28rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); box-shadow: 0 6rpx 16rpx rgba(240,113,13,0.25); }
|
||||
.btn.disabled { opacity: .7; }
|
||||
.q-item { padding: 18rpx 0; border-bottom: 1rpx solid #F4F4F4; display: flex; flex-direction: column; }
|
||||
|
||||
.q-item { padding: 20rpx 0; border-bottom: 1rpx solid #F4F4F4; display: flex; flex-direction: column; }
|
||||
.q-item:last-child { border-bottom: none; }
|
||||
.q-top { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.q-svc { font-size: 28rpx; font-weight: 600; color: #1F1F1F; }
|
||||
.q-amt { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.q-meta { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; }
|
||||
.q-status { margin-top: 8rpx; font-size: 22rpx; color: #4A7CFF; }
|
||||
.empty { padding: 60rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.q-svc { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.amt-box { display: flex; align-items: baseline; }
|
||||
.amt-symbol { font-size: 24rpx; font-weight: 700; color: #F0710D; }
|
||||
.q-amt { font-size: 34rpx; font-weight: 700; color: #F0710D; }
|
||||
.q-meta { margin-top: 6rpx; font-size: 22rpx; color: #8A8A8E; }
|
||||
.status-wrap { margin-top: 10rpx; }
|
||||
.q-status { display: inline-block; padding: 4rpx 16rpx; border-radius: 16rpx; background: #EAF2FF; color: #4A7CFF; font-size: 22rpx; font-weight: 500; }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 80rpx 20rpx 30rpx; }
|
||||
.empty-img { width: 150rpx; height: 150rpx; margin-bottom: 20rpx; }
|
||||
.empty-title { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 8rpx; }
|
||||
.empty-sub { font-size: 22rpx; color: #9A9A9A; text-align: center; margin-bottom: 28rpx; }
|
||||
.go-hall-btn {
|
||||
padding: 0 44rpx; height: 68rpx; line-height: 68rpx; border-radius: 34rpx;
|
||||
background: #FFF3E6; color: #F0710D; font-size: 26rpx; font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
428
yjyf-app/pages/master/success.vue
普通文件
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<view class="success-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-btn clickable-hover" @tap="goHall">
|
||||
<image class="nav-icon" src="/static/icons/back.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="nav-title">抢单结果</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<!-- 1. 成功插画 -->
|
||||
<view class="illustration-box">
|
||||
<image class="success-img" src="/static/icons/master/grab-success.svg" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<!-- 2. 主标题 -->
|
||||
<text class="main-title">抢单成功</text>
|
||||
|
||||
<!-- 3. 核心倒计时考核警示(对标工奇兵实机标语) -->
|
||||
<view class="alert-box">
|
||||
<text class="alert-txt">请尽快联系业主预约上门时间,并提交预约结果。超过2小时不预约,订单将自动取消。</text>
|
||||
</view>
|
||||
|
||||
<!-- 4. 工钱结算规则提醒 -->
|
||||
<view class="settle-box">
|
||||
<text class="settle-txt">为不影响工钱结算,完工后请上传完工图片,并在施工现场让业主验收订单。预计72小时后,工钱将自动结算至您的到账账户。</text>
|
||||
</view>
|
||||
|
||||
<!-- 5. 主行动按钮(预约上门时间) -->
|
||||
<view class="action-btn clickable-hover" @tap="openScheduleModal">
|
||||
<text class="action-txt">预约上门时间</text>
|
||||
</view>
|
||||
|
||||
<!-- 辅助入口 -->
|
||||
<view class="sub-actions">
|
||||
<view class="sub-btn clickable-hover" @tap="goDetail">
|
||||
<text class="sub-txt">查看工单详情</text>
|
||||
</view>
|
||||
<text class="divider">|</text>
|
||||
<view class="sub-btn clickable-hover" @tap="goHall">
|
||||
<text class="sub-txt">继续抢单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预约上门时间弹窗 -->
|
||||
<view class="modal-mask" v-if="showModal" @tap="showModal = false">
|
||||
<view class="modal-card" @tap.stop>
|
||||
<view class="modal-head">
|
||||
<text class="modal-title">预约上门时间</text>
|
||||
<image class="close-ico clickable-hover" src="/static/icons/close.svg" mode="aspectFit" @tap="showModal = false" />
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<view class="form-row">
|
||||
<text class="form-label">预约日期</text>
|
||||
<picker mode="date" :value="appointDate" :start="today" @change="onDateChange">
|
||||
<view class="picker-val">{{ appointDate }} ›</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<text class="form-label">预约时段</text>
|
||||
<picker mode="selector" :range="timeSlots" @change="onSlotChange">
|
||||
<view class="picker-val">{{ appointSlot }} ›</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="call-hint" v-if="userMobile">
|
||||
<text class="call-txt">业主电话:{{ userMobile }}</text>
|
||||
<view class="call-btn clickable-hover" @tap="makeCall">
|
||||
<image class="phone-ico" src="/static/icons/phone.svg" mode="aspectFit" />
|
||||
<text class="call-btn-txt">直接拨打</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-foot">
|
||||
<view class="confirm-btn clickable-hover" :class="{ disabled: submitting }" @tap="submitSchedule">
|
||||
<text class="confirm-txt">{{ submitting ? '提交中...' : '确认预约上门' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
|
||||
const orderId = ref(0);
|
||||
const userMobile = ref('');
|
||||
const showModal = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const appointDate = ref(today);
|
||||
const timeSlots = ['上午 (09:00 - 12:00)', '下午 (13:00 - 18:00)', '晚上 (18:00 - 21:00)', '全天随时上门'];
|
||||
const appointSlot = ref(timeSlots[1]);
|
||||
|
||||
onLoad((query) => {
|
||||
if (query && query.order_id) {
|
||||
orderId.value = Number(query.order_id);
|
||||
}
|
||||
if (query && query.mobile) {
|
||||
userMobile.value = query.mobile;
|
||||
}
|
||||
if (!userMobile.value && orderId.value > 0) {
|
||||
loadOrderDetail();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadOrderDetail() {
|
||||
try {
|
||||
const res = await api.masterOrderDetail(orderId.value);
|
||||
if (res && res.mobile) {
|
||||
userMobile.value = res.mobile;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function onDateChange(e) {
|
||||
appointDate.value = e.detail.value;
|
||||
}
|
||||
|
||||
function onSlotChange(e) {
|
||||
appointSlot.value = timeSlots[e.detail.value];
|
||||
}
|
||||
|
||||
function openScheduleModal() {
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
function makeCall() {
|
||||
if (!userMobile.value) {
|
||||
uni.showToast({ title: '暂未获取到客户电话', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
api.masterContact({ order_id: orderId.value }).catch(() => {});
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: String(userMobile.value),
|
||||
fail: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
async function submitSchedule() {
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
let hour = 14;
|
||||
if (appointSlot.value.includes('上午')) hour = 10;
|
||||
else if (appointSlot.value.includes('晚上')) hour = 19;
|
||||
const targetDateStr = `${appointDate.value} ${String(hour).padStart(2, '0')}:00:00`.replace(/-/g, '/');
|
||||
const appointTimestamp = Math.floor(new Date(targetDateStr).getTime() / 1000);
|
||||
|
||||
await api.masterSchedule({
|
||||
order_id: orderId.value,
|
||||
appoint_time: appointTimestamp
|
||||
});
|
||||
showModal.value = false;
|
||||
uni.showToast({ title: '预约成功,已通知业主', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/master/orders' });
|
||||
}, 800);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail() {
|
||||
if (orderId.value > 0) {
|
||||
uni.navigateTo({ url: `/pages/master/detail?id=${orderId.value}` });
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/master/orders' });
|
||||
}
|
||||
}
|
||||
|
||||
function goHall() {
|
||||
uni.reLaunch({ url: '/pages/master/hall' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.success-page {
|
||||
min-height: 100vh;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 28rpx;
|
||||
height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
.nav-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1F1F1F;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 70rpx 48rpx 50rpx;
|
||||
}
|
||||
|
||||
.illustration-box {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.success-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin-top: 36rpx;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1F1F;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
margin-top: 36rpx;
|
||||
width: 100%;
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #FFF5F0;
|
||||
border: 1rpx solid #FFE2D4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.alert-txt {
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
color: #EB5D2A;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settle-box {
|
||||
margin-top: 24rpx;
|
||||
width: 100%;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #F9FAFB;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settle-txt {
|
||||
font-size: 24rpx;
|
||||
line-height: 38rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-top: 64rpx;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: linear-gradient(135deg, #F98D25 0%, #ED6F0E 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(237, 111, 14, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.action-txt {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.sub-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
.sub-btn {
|
||||
padding: 10rpx 24rpx;
|
||||
}
|
||||
.sub-txt {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
.divider {
|
||||
color: #D1D5DB;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* 预约弹窗 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 36rpx 36rpx constant(safe-area-inset-bottom);
|
||||
padding: 36rpx 36rpx env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1F1F;
|
||||
}
|
||||
.close-ico {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 90rpx;
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #374151;
|
||||
}
|
||||
.picker-val {
|
||||
font-size: 28rpx;
|
||||
color: #F0710D;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.call-hint {
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #F8F9FA;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.call-txt {
|
||||
font-size: 26rpx;
|
||||
color: #4B5563;
|
||||
}
|
||||
.call-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 18rpx;
|
||||
background: #E8F5E9;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
.phone-ico {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
.call-btn-txt {
|
||||
font-size: 24rpx;
|
||||
color: #2E7D32;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-foot {
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
.confirm-btn {
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background: linear-gradient(135deg, #F98D25 0%, #ED6F0E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.confirm-txt {
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.confirm-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +1,95 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<text class="hero-lab">可提现余额(元)</text>
|
||||
<text class="hero-num">{{ income ? income.money : 0 }}</text>
|
||||
<!-- 资产大卡 -->
|
||||
<view class="wallet-hero">
|
||||
<view class="hero-top">
|
||||
<text class="hero-lab">可提现余额(元)</text>
|
||||
<view class="channel-tag">
|
||||
<image class="wechat-ico" src="/static/icons/wechat.svg" mode="aspectFit" />
|
||||
<text class="channel-txt">微信钱包快速到账</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hero-num">{{ income ? income.money : '0.00' }}</text>
|
||||
<view class="hero-row">
|
||||
<text class="hero-sub">今日收入 ¥{{ income ? income.today_income : 0 }}</text>
|
||||
<text class="hero-sub">累计收入 ¥{{ income ? income.total_income : 0 }}</text>
|
||||
<view class="income-badge">
|
||||
<text class="badge-lab">今日收入</text>
|
||||
<text class="badge-val">¥{{ income ? income.today_income : '0.00' }}</text>
|
||||
</view>
|
||||
<view class="income-badge">
|
||||
<text class="badge-lab">累计入账</text>
|
||||
<text class="badge-val">¥{{ income ? income.total_income : '0.00' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="withdraw-cta clickable-hover" @tap="openWithdraw">
|
||||
申请提现
|
||||
</view>
|
||||
<view class="hero-btn" @tap="openWithdraw">申请提现</view>
|
||||
</view>
|
||||
|
||||
<!-- 结算须知 -->
|
||||
<view class="tips">
|
||||
提现通常 T+1 到账,手续费 0.6%;结算周期:客户验收核销后款项即刻入账。
|
||||
</view>
|
||||
|
||||
<!-- 账单明细卡片 -->
|
||||
<view class="card">
|
||||
<view class="card-head"><text class="card-title">账单明细</text></view>
|
||||
<view class="log" v-for="(l, i) in logs" :key="i">
|
||||
<view class="log-l">
|
||||
<text class="log-t">{{ logTypeText(l.type) }}</text>
|
||||
<text class="log-r">{{ l.remark }}</text>
|
||||
<text class="log-time">{{ timeText(l.createtime) }}</text>
|
||||
</view>
|
||||
<view class="log-r2">
|
||||
<text class="log-amt" :class="{ minus: Number(l.amount) < 0 }">{{ Number(l.amount) > 0 ? '+' : '' }}{{ l.amount }}</text>
|
||||
<text class="log-bal">余额 {{ l.balance }}</text>
|
||||
<view class="card-head">
|
||||
<text class="card-title">收支账单明细</text>
|
||||
<view class="filter-tabs">
|
||||
<text class="ft-item" :class="{ on: filterType === 'all' }" @tap="filterType = 'all'">全部</text>
|
||||
<text class="ft-item" :class="{ on: filterType === 'settle' }" @tap="filterType = 'settle'">结算</text>
|
||||
<text class="ft-item" :class="{ on: filterType === 'withdraw' }" @tap="filterType = 'withdraw'">提现</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" v-if="!logs.length">暂无账单</view>
|
||||
|
||||
<view class="log-list" v-if="filteredLogs.length">
|
||||
<view class="log" v-for="(l, i) in filteredLogs" :key="i">
|
||||
<view class="log-l">
|
||||
<text class="log-t">{{ logTypeText(l.type) }}</text>
|
||||
<text class="log-r">{{ l.remark }}</text>
|
||||
<text class="log-time">{{ timeText(l.createtime) }}</text>
|
||||
</view>
|
||||
<view class="log-r2">
|
||||
<text class="log-amt" :class="{ minus: Number(l.amount) < 0 }">
|
||||
{{ Number(l.amount) > 0 ? '+' : '' }}{{ l.amount }}
|
||||
</text>
|
||||
<text class="log-bal">余额 {{ l.balance }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-box" v-else>
|
||||
<image class="empty-img" src="/static/icons/master/empty-orders.svg" mode="aspectFit" />
|
||||
<text class="empty-txt">暂无明细记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips">提现 T+1 到账,手续费 0.6%;结算周期:客户验收后即时入账</view>
|
||||
<!-- 师傅端专属底部导航 -->
|
||||
<YjyfMasterTabBar :current="3" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { ref, computed } from 'vue';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import YjyfMasterTabBar from '@/components/YjyfMasterTabBar.vue';
|
||||
|
||||
const income = ref(null);
|
||||
const logs = ref([]);
|
||||
const filterType = ref('all');
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
if (filterType.value === 'all') return logs.value;
|
||||
return logs.value.filter(l => l.type === filterType.value);
|
||||
});
|
||||
|
||||
function logTypeText(t) {
|
||||
return { settle: '订单结算', withdraw: '提现', income: '收入', refund: '退款' }[t] || '账单';
|
||||
return { settle: '工单结算', withdraw: '提现出账', income: '其他收入', refund: '退款扣减' }[t] || '账单明细';
|
||||
}
|
||||
|
||||
function timeText(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(Number(ts) * 1000);
|
||||
const p = (n) => (n < 10 ? '0' + n : n);
|
||||
return `${d.getMonth() + 1}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
@@ -57,14 +105,16 @@
|
||||
|
||||
function openWithdraw() {
|
||||
uni.showModal({
|
||||
title: '申请提现',
|
||||
title: '申请提现到微信钱包',
|
||||
editable: true,
|
||||
placeholderText: '请输入提现金额(≥10 元)',
|
||||
confirmText: '立即提现',
|
||||
confirmColor: '#F0710D',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
const amount = Number(r.content);
|
||||
if (!amount || amount < 10) {
|
||||
uni.showToast({ title: '单次不少于 10 元', icon: 'none' });
|
||||
uni.showToast({ title: '单次提现不少于 10 元', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -77,29 +127,79 @@
|
||||
}
|
||||
|
||||
onShow(() => load());
|
||||
onPullDownRefresh(async () => {
|
||||
await load();
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 60rpx; }
|
||||
.hero { padding: 40rpx 36rpx 34rpx; background: linear-gradient(160deg,#FFB85C 0%,#F98D25 55%,#ED6F0E 100%); display: flex; flex-direction: column; }
|
||||
.hero-lab { font-size: 22rpx; color: rgba(255,255,255,.9); }
|
||||
.hero-num { margin-top: 10rpx; font-size: 68rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
|
||||
.hero-row { display: flex; margin-top: 8rpx; }
|
||||
.hero-sub { font-size: 22rpx; color: rgba(255,255,255,.88); margin-right: 26rpx; }
|
||||
.hero-btn { margin-top: 26rpx; align-self: flex-start; height: 72rpx; padding: 0 40rpx; display: flex; align-items: center; border-radius: 36rpx; background: #fff; color: #E36B0C; font-size: 26rpx; font-weight: 700; }
|
||||
.card { margin: 22rpx 28rpx 0; padding: 24rpx; background: #fff; border-radius: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 30rpx; }
|
||||
|
||||
/* 资产卡片 */
|
||||
.wallet-hero {
|
||||
padding: 36rpx 30rpx 32rpx;
|
||||
background: linear-gradient(160deg,#FFB85C 0%,#F98D25 55%,#ED6F0E 100%);
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.hero-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.hero-lab { font-size: 24rpx; color: rgba(255,255,255,.92); }
|
||||
.channel-tag {
|
||||
display: flex; align-items: center; padding: 4rpx 16rpx; border-radius: 20rpx;
|
||||
background: rgba(255,255,255,0.22);
|
||||
}
|
||||
.wechat-ico { width: 24rpx; height: 24rpx; margin-right: 8rpx; }
|
||||
.channel-txt { font-size: 20rpx; color: #fff; font-weight: 500; }
|
||||
.hero-num { margin-top: 14rpx; font-size: 68rpx; font-weight: 700; color: #fff; letter-spacing: 1.5rpx; line-height: 1.1; }
|
||||
.hero-row { display: flex; align-items: center; margin-top: 18rpx; }
|
||||
.income-badge {
|
||||
padding: 6rpx 18rpx; border-radius: 12rpx; background: rgba(255,255,255,0.18);
|
||||
display: flex; align-items: center; margin-right: 16rpx;
|
||||
}
|
||||
.badge-lab { font-size: 20rpx; color: rgba(255,255,255,0.85); margin-right: 8rpx; }
|
||||
.badge-val { font-size: 22rpx; font-weight: 700; color: #fff; }
|
||||
.withdraw-cta {
|
||||
margin-top: 24rpx; align-self: flex-start; height: 68rpx; padding: 0 44rpx;
|
||||
display: flex; align-items: center; border-radius: 34rpx; background: #fff;
|
||||
color: #E36B0C; font-size: 26rpx; font-weight: 700;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin: 20rpx 24rpx 0; padding: 16rpx 20rpx; background: #FFF7F0;
|
||||
border-radius: 14rpx; font-size: 22rpx; color: #C4580A; line-height: 32rpx;
|
||||
}
|
||||
|
||||
/* 账单卡片 */
|
||||
.card {
|
||||
margin: 20rpx 24rpx 0; padding: 24rpx; background: #fff; border-radius: 20rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06);
|
||||
}
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.log { display: flex; align-items: center; justify-content: space-between; padding: 18rpx 0; border-bottom: 1rpx solid #F4F4F4; }
|
||||
.filter-tabs { display: flex; align-items: center; }
|
||||
.ft-item {
|
||||
padding: 4rpx 16rpx; margin-left: 8rpx; border-radius: 16rpx;
|
||||
font-size: 22rpx; color: #9A9A9A;
|
||||
}
|
||||
.ft-item.on { background: #FFF3E6; color: #E36B0C; font-weight: 600; }
|
||||
|
||||
.log-list { display: flex; flex-direction: column; }
|
||||
.log {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 20rpx 0; border-bottom: 1rpx solid #F4F4F4;
|
||||
}
|
||||
.log:last-of-type { border-bottom: none; }
|
||||
.log-l { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.log-t { font-size: 26rpx; font-weight: 600; color: #333; }
|
||||
.log-r { margin-top: 4rpx; font-size: 22rpx; color: #9A9A9A; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.log-time { margin-top: 4rpx; font-size: 20rpx; color: #C4C4C4; }
|
||||
.log-t { font-size: 26rpx; font-weight: 600; color: #2C2C2E; }
|
||||
.log-r { margin-top: 4rpx; font-size: 22rpx; color: #8A8A8E; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.log-time { margin-top: 6rpx; font-size: 20rpx; color: #B0B0B0; }
|
||||
.log-r2 { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.log-amt { font-size: 30rpx; font-weight: 700; color: #2FA84F; }
|
||||
.log-amt { font-size: 32rpx; font-weight: 700; color: #2FA84F; }
|
||||
.log-amt.minus { color: #E8331B; }
|
||||
.log-bal { margin-top: 4rpx; font-size: 20rpx; color: #C4C4C4; }
|
||||
.empty { padding: 60rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||||
.tips { margin: 22rpx 28rpx 0; padding: 18rpx 22rpx; background: #FFF7F0; border-radius: 16rpx; font-size: 20rpx; color: #C4580A; line-height: 30rpx; }
|
||||
.log-bal { margin-top: 4rpx; font-size: 20rpx; color: #B0B0B0; }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 70rpx 0; }
|
||||
.empty-img { width: 140rpx; height: 140rpx; margin-bottom: 16rpx; }
|
||||
.empty-txt { font-size: 24rpx; color: #9A9A9A; }
|
||||
</style>
|
||||
|
||||
@@ -1,64 +1,128 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<view class="deco d1"></view>
|
||||
<view class="deco d2"></view>
|
||||
<view class="h-top">
|
||||
<view class="m-info">
|
||||
<text class="m-name">{{ info ? info.realname : '师傅' }}</text>
|
||||
<text class="m-meta">{{ info ? ('Lv' + info.level + ' · 评分 ' + info.score + ' · 完成 ' + info.order_count + ' 单') : '加载中...' }}</text>
|
||||
<!-- 1. 师傅顶部个人名片 -->
|
||||
<view class="profile-hero">
|
||||
<view class="hero-deco d1"></view>
|
||||
<view class="hero-deco d2"></view>
|
||||
|
||||
<view class="user-block">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar-img" src="/static/icons/user-avatar.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="switch" :class="{ off: info && info.work_status === 'off' }" @tap="toggleWork">
|
||||
<text class="switch-t">{{ info && info.work_status === 'off' ? '已暂停' : '接单中' }}</text>
|
||||
<view class="user-meta">
|
||||
<view class="name-row">
|
||||
<text class="user-name">{{ info ? info.realname : '师傅' }}</text>
|
||||
<view class="auth-tag">
|
||||
<image class="auth-ico" src="/static/icons/shield.svg" mode="aspectFit" />
|
||||
<text class="auth-txt">实名认证师傅</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="user-level" v-if="info">
|
||||
等级 Lv{{ info.level }} · 综合评分 {{ info.score }} 分 · 履约 {{ info.order_count }} 单
|
||||
</text>
|
||||
</view>
|
||||
<view class="status-pill clickable-hover" :class="{ off: info && info.work_status === 'off' }" @tap="toggleWork">
|
||||
<view class="status-dot"></view>
|
||||
<text class="status-txt">{{ info && info.work_status === 'off' ? '暂停接单' : '听单中' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="todo">
|
||||
<view class="todo-item" @tap="go('/pages/master/orders')">
|
||||
<text class="todo-num">{{ info ? info.todo_count : 0 }}</text>
|
||||
<text class="todo-lab">待处理</text>
|
||||
|
||||
<!-- 2. 资产工钱大卡(对标工奇兵待结算工钱) -->
|
||||
<view class="assets-box">
|
||||
<view class="asset-top">
|
||||
<text class="asset-title">可提现工钱(元)</text>
|
||||
<view class="bill-link clickable-hover" @tap="go('/pages/master/wallet')">
|
||||
<text class="bill-txt">账单明细</text>
|
||||
<image class="arrow-ico" src="/static/icons/arrow-right.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="todo-item" @tap="go('/pages/master/orders')">
|
||||
<text class="todo-num">{{ info ? info.verify_count : 0 }}</text>
|
||||
<text class="todo-lab">待验收</text>
|
||||
<view class="asset-main">
|
||||
<text class="asset-val">{{ info ? Number(info.money).toFixed(2) : '0.00' }}</text>
|
||||
<view class="withdraw-btn clickable-hover" @tap="go('/pages/master/wallet')">
|
||||
申请提现
|
||||
</view>
|
||||
</view>
|
||||
<view class="todo-item" @tap="go('/pages/master/wallet')">
|
||||
<text class="todo-num">{{ info ? info.money : 0 }}</text>
|
||||
<text class="todo-lab">可提现</text>
|
||||
<view class="asset-sub">
|
||||
<text class="sub-item">今日收入 ¥{{ todayIncome }}</text>
|
||||
<text class="sub-divider">|</text>
|
||||
<text class="sub-item">累计收入 ¥{{ totalIncome }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid">
|
||||
<view class="grid-item clickable-hover" v-for="g in entries" :key="g.key" @tap="g.action">
|
||||
<view class="g-icon" :style="{ background: g.bg }"><image class="g-svg" :src="g.svg" mode="aspectFit"></image></view>
|
||||
<text class="g-txt">{{ g.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card" v-if="credit">
|
||||
<view class="card-head">
|
||||
<text class="card-title">信用体系</text>
|
||||
<text class="card-more" @tap="go('/pages/master/credit')">明细 ›</text>
|
||||
</view>
|
||||
<view class="credit-row">
|
||||
<view class="c-item"><text class="c-num">{{ credit.score }}</text><text class="c-lab">综合评分</text></view>
|
||||
<view class="c-item"><text class="c-num">{{ credit.accept_rate }}%</text><text class="c-lab">接单率</text></view>
|
||||
<view class="c-item"><text class="c-num">{{ credit.ontime_rate }}%</text><text class="c-lab">准时率</text></view>
|
||||
<view class="c-item"><text class="c-num">{{ credit.good_rate }}%</text><text class="c-lab">好评率</text></view>
|
||||
<!-- 3. 核心功能菜单组(对标工奇兵功能列表) -->
|
||||
<view class="menu-card">
|
||||
<view class="menu-item clickable-hover" @tap="go('/pages/master/orders')">
|
||||
<view class="mi-left">
|
||||
<image class="mi-ico" src="/static/icons/master/menu-orders.svg" mode="aspectFit" />
|
||||
<text class="mi-title">我的接单管理</text>
|
||||
</view>
|
||||
<view class="mi-right">
|
||||
<text class="mi-badge" v-if="info && info.todo_count > 0">{{ info.todo_count }} 待办</text>
|
||||
<image class="mi-arrow" src="/static/icons/arrow-right.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item clickable-hover" @tap="go('/pages/master/quote')">
|
||||
<view class="mi-left">
|
||||
<image class="mi-ico" src="/static/icons/master/menu-quote.svg" mode="aspectFit" />
|
||||
<text class="mi-title">报价记录与管理</text>
|
||||
</view>
|
||||
<view class="mi-right">
|
||||
<text class="mi-tip">查看出价状态</text>
|
||||
<image class="mi-arrow" src="/static/icons/arrow-right.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item clickable-hover" @tap="go('/pages/master/credit')">
|
||||
<view class="mi-left">
|
||||
<image class="mi-ico" src="/static/icons/master/menu-credit.svg" mode="aspectFit" />
|
||||
<text class="mi-title">服务信用与申诉</text>
|
||||
</view>
|
||||
<view class="mi-right">
|
||||
<text class="mi-tip" v-if="credit">好评率 {{ credit.good_rate }}%</text>
|
||||
<image class="mi-arrow" src="/static/icons/arrow-right.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item clickable-hover" @tap="callService">
|
||||
<view class="mi-left">
|
||||
<image class="mi-ico" src="/static/icons/master/menu-service.svg" mode="aspectFit" />
|
||||
<text class="mi-title">联系平台客服</text>
|
||||
</view>
|
||||
<view class="mi-right">
|
||||
<text class="mi-tip">4000000000</text>
|
||||
<image class="mi-arrow" src="/static/icons/arrow-right.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 4. 服务技能与品类 -->
|
||||
<view class="card">
|
||||
<view class="card-head"><text class="card-title">我的服务类目</text></view>
|
||||
<view class="skills">
|
||||
<text class="skill" v-for="s in skills" :key="s.cat_id">{{ s.name }}</text>
|
||||
<text class="skill empty" v-if="!skills.length">暂无技能标签</text>
|
||||
<view class="card-head">
|
||||
<text class="card-title">已认证服务类目</text>
|
||||
<text class="card-tip">{{ skills.length }} 项技能</text>
|
||||
</view>
|
||||
<view class="skills-box">
|
||||
<text class="skill-tag" v-for="s in skills" :key="s.cat_id">{{ s.name }}</text>
|
||||
<text class="skill-tag empty" v-if="!skills.length">暂无已认证类目</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips">接单后请在 2 小时内联系客户,超时订单将自动回流大厅</view>
|
||||
<!-- 5. 身份切换通道(对标工奇兵我要下单找师傅) -->
|
||||
<view class="switch-role-card clickable-hover" @tap="backToUser">
|
||||
<view class="src-left">
|
||||
<image class="src-ico" src="/static/icons/role-switch.svg" mode="aspectFit" />
|
||||
<view class="src-text">
|
||||
<text class="src-title">我要下单找师傅</text>
|
||||
<text class="src-desc">切换至业主身份发布安装维修需求</text>
|
||||
</view>
|
||||
</view>
|
||||
<image class="src-arrow" src="/static/icons/arrow-right-orange.svg" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="back" @tap="backToUser">切换到业主身份</view>
|
||||
<!-- 师傅端专属底部导航 (当前 Tab 3: 我的) -->
|
||||
<YjyfMasterTabBar :current="3" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -68,22 +132,21 @@
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
import { setRole } from '@/utils/role.js';
|
||||
import YjyfMasterTabBar from '@/components/YjyfMasterTabBar.vue';
|
||||
|
||||
const info = ref(null);
|
||||
const credit = ref(null);
|
||||
const skills = ref([]);
|
||||
const todayIncome = ref('0.00');
|
||||
const totalIncome = ref('0.00');
|
||||
|
||||
const entries = [
|
||||
{ key: 'hall', name: '订单大厅', icon: '厅', bg: '#FFF1E0', color: '#E07A2E', action: () => go('/pages/master/hall') },
|
||||
{ key: 'orders', name: '我的接单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/master/orders') },
|
||||
{ key: 'quotes', name: '我的报价', icon: '价', bg: '#F0EBFF', color: '#7A5AF8', action: () => go('/pages/master/quote') },
|
||||
{ key: 'wallet', name: '钱包提现', icon: '钱', bg: '#E8F7EE', color: '#2FA84F', action: () => go('/pages/master/wallet') },
|
||||
{ key: 'credit', name: '信用中心', icon: '信', bg: '#FFF0F0', color: '#E8331B', action: () => go('/pages/master/credit') },
|
||||
{ key: 'service', name: '联系客服', icon: '服', bg: '#EAF6FF', color: '#2A8FD8', action: callService }
|
||||
];
|
||||
function go(url) {
|
||||
uni.navigateTo({ url });
|
||||
}
|
||||
|
||||
function go(url) { uni.navigateTo({ url: url }); }
|
||||
function callService() { uni.makePhoneCall({ phoneNumber: '4000000000' }); }
|
||||
function callService() {
|
||||
uni.makePhoneCall({ phoneNumber: '4000000000' });
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!isLogin()) { toLogin(); return; }
|
||||
@@ -96,6 +159,15 @@
|
||||
info.value = res.master;
|
||||
skills.value = res.skills || [];
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const inc = await api.masterIncome();
|
||||
if (inc) {
|
||||
todayIncome.value = Number(inc.today_income || 0).toFixed(2);
|
||||
totalIncome.value = Number(inc.total_income || 0).toFixed(2);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
credit.value = await api.masterCredit();
|
||||
} catch (e) {}
|
||||
@@ -107,7 +179,7 @@
|
||||
try {
|
||||
await api.masterToggle({ work_status: next });
|
||||
info.value.work_status = next;
|
||||
uni.showToast({ title: next === 'on' ? '已开启接单' : '已暂停接单', icon: 'none' });
|
||||
uni.showToast({ title: next === 'on' ? '已开启接单听单' : '已暂停接单', icon: 'none' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -122,40 +194,114 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 60rpx; }
|
||||
.header { position: relative; padding: 34rpx 30rpx 26rpx; overflow: hidden; background: linear-gradient(160deg, #FFB85C 0%, #F98D25 55%, #ED6F0E 100%); }
|
||||
.deco { position: absolute; border-radius: 50%; background: rgba(255,255,255,.13); }
|
||||
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 40rpx; }
|
||||
|
||||
/* 顶部名片 */
|
||||
.profile-hero {
|
||||
position: relative; overflow: hidden; padding: 36rpx 28rpx 30rpx;
|
||||
background: linear-gradient(160deg, #FFB85C 0%, #F98D25 55%, #ED6F0E 100%);
|
||||
}
|
||||
.hero-deco { position: absolute; border-radius: 50%; background: rgba(255,255,255,0.13); }
|
||||
.d1 { width: 320rpx; height: 320rpx; right: -120rpx; top: -130rpx; }
|
||||
.d2 { width: 180rpx; height: 180rpx; left: -80rpx; bottom: -60rpx; }
|
||||
.h-top { position: relative; display: flex; align-items: center; justify-content: space-between; }
|
||||
.m-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.m-name { font-size: 36rpx; font-weight: 700; color: #fff; }
|
||||
.m-meta { margin-top: 8rpx; font-size: 22rpx; color: rgba(255,255,255,.9); }
|
||||
.switch { flex-shrink: 0; padding: 0 24rpx; line-height: 58rpx; border-radius: 29rpx; background: #fff; }
|
||||
.switch.off { background: rgba(255,255,255,.35); }
|
||||
.switch-t { font-size: 22rpx; font-weight: 600; color: #E36B0C; }
|
||||
.switch.off .switch-t { color: #fff; }
|
||||
.todo { position: relative; display: flex; margin-top: 28rpx; padding-top: 22rpx; border-top: 1rpx solid rgba(255,255,255,.25); }
|
||||
.todo-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.todo-num { font-size: 36rpx; font-weight: 700; color: #fff; }
|
||||
.todo-lab { margin-top: 4rpx; font-size: 20rpx; color: rgba(255,255,255,.88); }
|
||||
.grid { display: flex; flex-wrap: wrap; margin: 22rpx 28rpx 0; padding: 16rpx 0; background: #fff; border-radius: 22rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||||
.grid-item { width: 33.33%; display: flex; flex-direction: column; align-items: center; padding: 18rpx 0; }
|
||||
.g-icon { width: 84rpx; height: 84rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.g-icon-t { font-size: 30rpx; font-weight: 700; }
|
||||
.g-txt { margin-top: 12rpx; font-size: 22rpx; color: #333; }
|
||||
.card { margin: 22rpx 28rpx 0; padding: 24rpx; background: #fff; border-radius: 22rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.card-more { font-size: 22rpx; color: #9A9A9A; }
|
||||
.credit-row { display: flex; margin-top: 20rpx; }
|
||||
.c-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.c-num { font-size: 32rpx; font-weight: 700; color: #F0710D; }
|
||||
.c-lab { margin-top: 6rpx; font-size: 20rpx; color: #9A9A9A; }
|
||||
.skills { display: flex; flex-wrap: wrap; padding-top: 14rpx; }
|
||||
.skill { padding: 0 20rpx; line-height: 54rpx; margin: 0 12rpx 12rpx 0; border-radius: 27rpx; background: #FFF3E6; color: #C4580A; font-size: 22rpx; }
|
||||
.skill.empty { background: #F5F5F5; color: #9A9A9A; }
|
||||
.tips { margin: 22rpx 28rpx 0; padding: 18rpx 22rpx; background: #FFF7F0; border-radius: 16rpx; font-size: 22rpx; color: #C4580A; line-height: 32rpx; }
|
||||
.back { margin: 26rpx 28rpx 0; height: 92rpx; display: flex; align-items: center; justify-content: center; background: #fff; border-radius: 46rpx; font-size: 28rpx; color: #F0710D; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
.user-block { position: relative; display: flex; align-items: center; justify-content: space-between; }
|
||||
.avatar-wrap {
|
||||
width: 104rpx; height: 104rpx; border-radius: 52rpx; background: #fff;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.avatar-img { width: 72rpx; height: 72rpx; }
|
||||
.user-meta { flex: 1; min-width: 0; margin-left: 20rpx; }
|
||||
.name-row { display: flex; align-items: center; }
|
||||
.user-name { font-size: 36rpx; font-weight: 700; color: #fff; }
|
||||
.auth-tag {
|
||||
margin-left: 12rpx; display: flex; align-items: center; padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx; background: rgba(255,255,255,0.22);
|
||||
}
|
||||
.auth-ico { width: 20rpx; height: 20rpx; margin-right: 6rpx; }
|
||||
.auth-txt { font-size: 18rpx; color: #fff; font-weight: 600; }
|
||||
.user-level { margin-top: 8rpx; font-size: 22rpx; color: rgba(255,255,255,0.92); }
|
||||
|
||||
.status-pill {
|
||||
display: flex; align-items: center; padding: 0 22rpx; height: 54rpx;
|
||||
border-radius: 27rpx; background: #fff; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
.status-pill.off { background: rgba(255,255,255,0.35); }
|
||||
.status-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #2FA84F; margin-right: 8rpx; }
|
||||
.status-pill.off .status-dot { background: #E8331B; }
|
||||
.status-txt { font-size: 22rpx; font-weight: 600; color: #E36B0C; }
|
||||
.status-pill.off .status-txt { color: #fff; }
|
||||
|
||||
/* 资产大卡 */
|
||||
.assets-box {
|
||||
position: relative; margin-top: 30rpx; padding: 24rpx 24rpx 20rpx;
|
||||
border-radius: 20rpx; background: rgba(255,255,255,0.18);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.asset-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.asset-title { font-size: 24rpx; color: rgba(255,255,255,0.9); }
|
||||
.bill-link { display: flex; align-items: center; }
|
||||
.bill-txt { font-size: 22rpx; color: #fff; font-weight: 500; }
|
||||
.arrow-ico { width: 22rpx; height: 22rpx; margin-left: 4rpx; }
|
||||
|
||||
.asset-main { display: flex; align-items: baseline; justify-content: space-between; margin-top: 10rpx; }
|
||||
.asset-val { font-size: 56rpx; font-weight: 700; color: #fff; line-height: 1.1; }
|
||||
.withdraw-btn {
|
||||
padding: 0 32rpx; height: 56rpx; line-height: 56rpx; border-radius: 28rpx;
|
||||
background: #fff; color: #E36B0C; font-size: 24rpx; font-weight: 700;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
.asset-sub { display: flex; align-items: center; margin-top: 14rpx; padding-top: 14rpx; border-top: 1rpx solid rgba(255,255,255,0.22); }
|
||||
.sub-item { font-size: 22rpx; color: rgba(255,255,255,0.9); }
|
||||
.sub-divider { margin: 0 16rpx; font-size: 18rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
/* 菜单组 */
|
||||
.menu-card {
|
||||
margin: 20rpx 24rpx 0; background: #fff; border-radius: 20rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); overflow: hidden;
|
||||
}
|
||||
.menu-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 28rpx 24rpx; border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.mi-left { display: flex; align-items: center; }
|
||||
.mi-ico { width: 40rpx; height: 40rpx; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.mi-title { font-size: 28rpx; font-weight: 600; color: #2C2C2E; }
|
||||
.mi-right { display: flex; align-items: center; }
|
||||
.mi-badge {
|
||||
padding: 2rpx 14rpx; border-radius: 14rpx; background: #FDEEEB;
|
||||
color: #E8331B; font-size: 20rpx; font-weight: 600; margin-right: 8rpx;
|
||||
}
|
||||
.mi-tip { font-size: 24rpx; color: #9A9A9A; margin-right: 8rpx; }
|
||||
.mi-arrow { width: 24rpx; height: 24rpx; }
|
||||
|
||||
/* 类目卡片 */
|
||||
.card {
|
||||
margin: 20rpx 24rpx 0; padding: 24rpx; background: #fff; border-radius: 20rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06);
|
||||
}
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14rpx; }
|
||||
.card-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.card-tip { font-size: 22rpx; color: #9A9A9A; }
|
||||
.skills-box { display: flex; flex-wrap: wrap; }
|
||||
.skill-tag {
|
||||
padding: 0 22rpx; line-height: 52rpx; margin: 0 12rpx 12rpx 0;
|
||||
border-radius: 26rpx; background: #FFF3E6; color: #C4580A; font-size: 22rpx; font-weight: 500;
|
||||
}
|
||||
.skill-tag.empty { background: #F5F5F5; color: #9A9A9A; }
|
||||
|
||||
/* 切换业主角色卡片 */
|
||||
.switch-role-card {
|
||||
margin: 24rpx 24rpx 0; padding: 24rpx; background: #fff; border-radius: 20rpx;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06);
|
||||
}
|
||||
.src-left { display: flex; align-items: center; }
|
||||
.src-ico { width: 44rpx; height: 44rpx; margin-right: 18rpx; flex-shrink: 0; }
|
||||
.src-text { display: flex; flex-direction: column; }
|
||||
.src-title { font-size: 28rpx; font-weight: 700; color: #1F1F1F; }
|
||||
.src-desc { margin-top: 4rpx; font-size: 22rpx; color: #8E8E93; }
|
||||
.src-arrow { width: 28rpx; height: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
{ key: 'service', name: '专属客服', svg: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232A8FD8'><path d='M20 15.5c-.7 0-1.4-.1-2-.3-.6-.2-1.2.1-1.6.5l-2.2 2.2c-2.8-1.5-5.2-3.8-6.6-6.6l2.2-2.2c.4-.4.6-1 .4-1.6-.2-.6-.3-1.3-.3-2 0-.8-.7-1.5-1.5-1.5H5c-.8 0-1.5.7-1.5 1.5 0 9.4 7.6 17 17 17 .8 0 1.5-.7 1.5-1.5v-3.5c0-.8-.7-1.5-1.5-1.5z'/></svg>", bg: '#E8F7EE', action: callService }
|
||||
];
|
||||
|
||||
function callService() { uni.makePhoneCall({ phoneNumber: '4000000000' }); }
|
||||
function go(url) { uni.navigateTo({ url: url }); }
|
||||
function goOrder(id) { uni.navigateTo({ url: '/pages/order/detail?id=' + id }); }
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<YjyfNavBar theme="white" title="消息中心" :border="true" />
|
||||
<view class="head">
|
||||
<view class="cats">
|
||||
<view v-for="c in cats" :key="c.key" class="cat" :class="{ on: category === c.key }" @tap="switchCat(c.key)">{{ c.name }}</view>
|
||||
<view v-for="c in cats" :key="c.key" class="cat clickable-hover" :class="{ on: category === c.key }" @tap="switchCat(c.key)">{{ c.name }}</view>
|
||||
</view>
|
||||
<text class="read-all" @tap="readAll">全部已读</text>
|
||||
<text class="read-all clickable-hover" @tap="readAll">全部已读</text>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
@@ -21,20 +21,27 @@
|
||||
</view>
|
||||
<view class="empty" v-if="!loading && !list.length">暂无消息</view>
|
||||
</view>
|
||||
<YjyfTabBar :current="2" />
|
||||
|
||||
<!-- 角色自适应 TabBar:师傅端显示 MasterTabBar,业主端显示 YjyfTabBar -->
|
||||
<YjyfMasterTabBar :current="2" v-if="isMasterRole" />
|
||||
<YjyfTabBar :current="2" v-else />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import YjyfTabBar from '@/components/YjyfTabBar.vue';
|
||||
import YjyfMasterTabBar from '@/components/YjyfMasterTabBar.vue';
|
||||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin } from '@/utils/request.js';
|
||||
import { getRole } from '@/utils/role.js';
|
||||
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const category = ref('all');
|
||||
const isMasterRole = ref(false);
|
||||
|
||||
const cats = [
|
||||
{ key: 'all', name: '全部' },
|
||||
{ key: 'order', name: '订单' },
|
||||
@@ -61,7 +68,8 @@
|
||||
try { await api.messageRead({ id: m.id }); m.is_read = 1; } catch (e) {}
|
||||
}
|
||||
if (m.order_id) {
|
||||
uni.navigateTo({ url: '/pages/order/detail?id=' + m.order_id });
|
||||
const targetUrl = isMasterRole.value ? '/pages/master/orders' : ('/pages/order/detail?id=' + m.order_id);
|
||||
uni.navigateTo({ url: targetUrl });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +81,11 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onShow(() => load());
|
||||
onShow(() => {
|
||||
isMasterRole.value = getRole() === 'master';
|
||||
load();
|
||||
});
|
||||
|
||||
onPullDownRefresh(() => { load().then(() => uni.stopPullDownRefresh()); });
|
||||
</script>
|
||||
|
||||
|
||||
@@ -169,12 +169,13 @@
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api.js';
|
||||
import { isLogin, toLogin, setUser, clearAuth } from '@/utils/request.js';
|
||||
import { setRole } from '@/utils/role.js';
|
||||
import { getRole, setRole } from '@/utils/role.js';
|
||||
|
||||
const user = ref(null);
|
||||
const master = ref(null);
|
||||
const merchant = ref(null);
|
||||
const coupon = ref(0);
|
||||
const currentRole = ref('user');
|
||||
|
||||
const mobileText = computed(() => {
|
||||
if (!user.value || !user.value.mobile) return '';
|
||||
@@ -199,12 +200,19 @@
|
||||
});
|
||||
|
||||
const roleText = computed(() => {
|
||||
if (master.value && master.value.verify_status === 'pass') return '认证师傅';
|
||||
const curRole = currentRole.value;
|
||||
if (curRole === 'merchant' && merchant.value && merchant.value.verify_status === 'pass') {
|
||||
return merchant.value.is_owner === false ? '关联商家' : '认证商家';
|
||||
}
|
||||
if (curRole === 'master' && master.value && master.value.verify_status === 'pass') {
|
||||
return '认证师傅';
|
||||
}
|
||||
if (merchant.value && merchant.value.verify_status === 'pass') {
|
||||
return merchant.value.is_owner === false ? '关联商家' : '认证商家';
|
||||
}
|
||||
if (master.value && master.value.verify_status === 'pending') return '师傅审核中';
|
||||
if (master.value && master.value.verify_status === 'pass') return '认证师傅';
|
||||
if (merchant.value && merchant.value.verify_status === 'pending') return '商家审核中';
|
||||
if (master.value && master.value.verify_status === 'pending') return '师傅审核中';
|
||||
return '个人用户';
|
||||
});
|
||||
|
||||
@@ -271,6 +279,7 @@
|
||||
}
|
||||
|
||||
async function load() {
|
||||
currentRole.value = getRole();
|
||||
if (!isLogin()) {
|
||||
toLogin();
|
||||
return;
|
||||
@@ -291,6 +300,7 @@
|
||||
try {
|
||||
const res = await api.switchRole({ role: 'merchant' });
|
||||
setRole('merchant');
|
||||
currentRole.value = 'merchant';
|
||||
uni.showToast({ title: '已切换为商家身份' });
|
||||
setTimeout(() => uni.reLaunch({ url: res.redirect }), 500);
|
||||
} catch (e) {}
|
||||
@@ -300,6 +310,7 @@
|
||||
try {
|
||||
const res = await api.switchRole({ role: 'master' });
|
||||
setRole('master');
|
||||
currentRole.value = 'master';
|
||||
uni.showToast({ title: '已切换为师傅身份' });
|
||||
setTimeout(() => uni.reLaunch({ url: res.redirect }), 500);
|
||||
} catch (e) {}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<view class="tsc-row">
|
||||
<view class="tsc-left">
|
||||
<view class="tsc-icon-wrap">
|
||||
<image class="tsc-img" :src="'/static/icons/' + (currentCatIcon || 'bath') + '.png'" mode="aspectFit" />
|
||||
<image class="tsc-img" :src="currentCatIconUrl || ('/static/icons/' + (currentCatIcon || 'bath') + '.png')" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="tsc-title">{{ drawerCatName || '卫浴' }}</text>
|
||||
</view>
|
||||
@@ -154,20 +154,12 @@
|
||||
</view>
|
||||
<view class="fr-type-tabs">
|
||||
<view
|
||||
v-for="st in (g.service_types && g.service_types.length ? g.service_types : defaultServiceTypes)"
|
||||
:key="st.code"
|
||||
class="type-tab-item clickable-hover"
|
||||
:class="{ on: g.service_type === 'install' }"
|
||||
@tap="g.service_type = 'install'"
|
||||
>安装</view>
|
||||
<view
|
||||
class="type-tab-item clickable-hover"
|
||||
:class="{ on: g.service_type === 'repair' }"
|
||||
@tap="g.service_type = 'repair'"
|
||||
>维修</view>
|
||||
<view
|
||||
class="type-tab-item clickable-hover"
|
||||
:class="{ on: g.service_type === 'remove' }"
|
||||
@tap="g.service_type = 'remove'"
|
||||
>拆卸</view>
|
||||
:class="{ on: g.service_type === st.code }"
|
||||
@tap="selectServiceType(g, st)"
|
||||
>{{ st.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -287,11 +279,12 @@
|
||||
<!-- ==================== 吸底结算栏(一站式直接下单) ==================== -->
|
||||
<view class="bottom-action-bar">
|
||||
<view class="bab-info">
|
||||
<text class="bab-label">合计预估:</text>
|
||||
<text class="bab-price">¥{{ amount ? amount.pay_amount : (priceType === 'custom' ? (userPrice || '0') : '90') }}</text>
|
||||
<text class="bab-label">{{ priceType === 'quote' ? '参考基准:' : (priceType === 'custom' ? '意向出价:' : '合计金额:') }}</text>
|
||||
<text class="bab-price">¥{{ priceType === 'custom' ? (userPrice || '0') : (amount ? amount.pay_amount : '90') }}</text>
|
||||
<text class="bab-sub-hint" v-if="priceType === 'quote'"> (以师傅竞价为准)</text>
|
||||
</view>
|
||||
<view class="bab-btn-main clickable-hover" :class="{ disabled: submitting }" @tap="confirmSubmitAndPay">
|
||||
<text class="bbm-txt">{{ submitting ? '提交中...' : '立即下单' }}</text>
|
||||
<view class="bab-btn-main clickable-hover" :class="{ disabled: submitting, quote: priceType === 'quote', custom: priceType === 'custom' }" @tap="confirmSubmitAndPay">
|
||||
<text class="bbm-txt">{{ submitting ? '提交中...' : (priceType === 'quote' ? '发布竞价单' : (priceType === 'custom' ? '按出价下单' : '立即下单')) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -374,7 +367,7 @@
|
||||
<view class="am-item" v-for="(ex, exIdx) in currentEditingAddons" :key="ex.key">
|
||||
<view class="am-info">
|
||||
<text class="am-name">{{ ex.name }}</text>
|
||||
<text class="am-price">¥{{ ex.price }} / 项</text>
|
||||
<text class="am-price">¥{{ ex.price }} / {{ ex.unit || '项' }}</text>
|
||||
</view>
|
||||
<view class="am-stepper">
|
||||
<view class="step-btn" @tap="ex.num = Math.max(0, ex.num - 1)">-</view>
|
||||
@@ -444,6 +437,21 @@
|
||||
const drawerCatId = ref(0);
|
||||
const drawerCatName = ref('');
|
||||
const currentCatIcon = ref('bath');
|
||||
const currentCatIconUrl = ref('');
|
||||
const defaultServiceTypes = [
|
||||
{ code: 'install', name: '安装', price: 60 },
|
||||
{ code: 'repair', name: '维修', price: 40 },
|
||||
{ code: 'remove', name: '拆旧', price: 30 }
|
||||
];
|
||||
|
||||
function selectServiceType(group, type) {
|
||||
group.service_type = type.code;
|
||||
group.service_type_name = type.name;
|
||||
if (type.price > 0) {
|
||||
group.base_price = Number(type.price);
|
||||
}
|
||||
scheduleCalc();
|
||||
}
|
||||
|
||||
function goToSelectCategory() {
|
||||
uni.navigateTo({ url: '/pages/category/category' });
|
||||
@@ -501,6 +509,23 @@
|
||||
}
|
||||
|
||||
function handleDrawerSelect(spec) {
|
||||
const rawAddons = spec.addons && spec.addons.length ? spec.addons : [];
|
||||
const extras = rawAddons.map(ad => ({
|
||||
id: ad.id,
|
||||
key: 'addon_' + ad.id,
|
||||
name: ad.name,
|
||||
unit: ad.unit || '个',
|
||||
price: Number(ad.price) || 0,
|
||||
num: 0
|
||||
}));
|
||||
|
||||
const serviceTypes = spec.service_types && spec.service_types.length ? spec.service_types : [
|
||||
{ code: 'install', name: '安装', price: Number(spec.base_price) || 0, is_default: 1 },
|
||||
{ code: 'repair', name: '维修', price: Math.round((Number(spec.base_price) || 0) * 0.7), is_default: 0 },
|
||||
{ code: 'remove', name: '拆旧', price: Math.round((Number(spec.base_price) || 0) * 0.5), is_default: 0 }
|
||||
];
|
||||
const defType = serviceTypes.find(st => st.is_default) || serviceTypes[0];
|
||||
|
||||
const newGroup = {
|
||||
service_id: spec.service_id,
|
||||
item_name: spec.item_name,
|
||||
@@ -508,17 +533,16 @@
|
||||
cat_id: spec.cat_id || drawerCatId.value,
|
||||
cat_name: spec.cat_name || drawerCatName.value,
|
||||
unit: spec.unit || '件',
|
||||
base_price: Number(spec.base_price) || 0,
|
||||
base_price: Number(spec.base_price) || Number(defType.price) || 0,
|
||||
num: 1,
|
||||
service_type: 'install',
|
||||
service_type: defType.code,
|
||||
service_type_name: defType.name,
|
||||
service_types: serviceTypes,
|
||||
remark: '',
|
||||
images: [],
|
||||
videos: [],
|
||||
envImages: [],
|
||||
extras: [
|
||||
{ key: 'tear', name: '拆旧拆卸', price: 50, num: 0 },
|
||||
{ key: 'cut', name: '管路开孔打孔', price: 20, num: 0 }
|
||||
]
|
||||
extras: extras
|
||||
};
|
||||
|
||||
if (isEditingGroup.value) {
|
||||
@@ -748,7 +772,18 @@
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
const appointTimestamp = Math.floor(new Date(`${appointDate.value} 10:00:00`).getTime() / 1000);
|
||||
let appointTimeStr = '14:30:00';
|
||||
if (appointSlot.value && appointSlot.value.indexOf('上午') > -1) appointTimeStr = '10:00:00';
|
||||
else if (appointSlot.value && appointSlot.value.indexOf('下午') > -1) appointTimeStr = '14:30:00';
|
||||
else if (appointSlot.value && appointSlot.value.indexOf('晚上') > -1) appointTimeStr = '19:00:00';
|
||||
else appointTimeStr = '10:00:00';
|
||||
|
||||
let targetTime = Math.floor(new Date(`${appointDate.value} ${appointTimeStr}`).getTime() / 1000);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (targetTime <= now) {
|
||||
targetTime = now + 3600;
|
||||
}
|
||||
const appointTimestamp = targetTime;
|
||||
const createRes = await api.orderCreate({
|
||||
service_id: goodsList.value[0]?.service_id || 1,
|
||||
price_type: priceType.value,
|
||||
@@ -788,6 +823,7 @@
|
||||
if (firstGoods.cat_id) drawerCatId.value = firstGoods.cat_id;
|
||||
if (firstGoods.cat_name) drawerCatName.value = firstGoods.cat_name;
|
||||
if (firstGoods.cat_icon) currentCatIcon.value = firstGoods.cat_icon;
|
||||
if (firstGoods.cat_icon_url) currentCatIconUrl.value = firstGoods.cat_icon_url;
|
||||
calc();
|
||||
return;
|
||||
}
|
||||
@@ -814,11 +850,28 @@
|
||||
drawerCatId.value = matchedCat.id;
|
||||
drawerCatName.value = matchedCat.name;
|
||||
currentCatIcon.value = matchedCat.icon || 'bath';
|
||||
currentCatIconUrl.value = matchedCat.icon_url || '';
|
||||
|
||||
const sRes = await api.services({ cat_id: matchedCat.id });
|
||||
const services = sRes && sRes.list ? sRes.list : [];
|
||||
if (services.length > 0) {
|
||||
const firstS = services[0];
|
||||
const rawAddons = firstS.addons && firstS.addons.length ? firstS.addons : [];
|
||||
const extras = rawAddons.map(ad => ({
|
||||
id: ad.id,
|
||||
key: 'addon_' + ad.id,
|
||||
name: ad.name,
|
||||
unit: ad.unit || '个',
|
||||
price: Number(ad.price) || 0,
|
||||
num: 0
|
||||
}));
|
||||
const serviceTypes = firstS.service_types && firstS.service_types.length ? firstS.service_types : [
|
||||
{ code: 'install', name: '安装', price: Number(firstS.base_price) || 0, is_default: 1 },
|
||||
{ code: 'repair', name: '维修', price: Math.round((Number(firstS.base_price) || 0) * 0.7), is_default: 0 },
|
||||
{ code: 'remove', name: '拆旧', price: Math.round((Number(firstS.base_price) || 0) * 0.5), is_default: 0 }
|
||||
];
|
||||
const defType = serviceTypes.find(st => st.is_default) || serviceTypes[0];
|
||||
|
||||
goodsList.value = [
|
||||
{
|
||||
service_id: firstS.id,
|
||||
@@ -827,17 +880,16 @@
|
||||
cat_id: matchedCat.id,
|
||||
cat_name: matchedCat.name,
|
||||
unit: firstS.unit || '件',
|
||||
base_price: Number(firstS.base_price) || 0,
|
||||
base_price: Number(firstS.base_price) || Number(defType.price) || 0,
|
||||
num: 1,
|
||||
service_type: 'install',
|
||||
service_type: defType.code,
|
||||
service_type_name: defType.name,
|
||||
service_types: serviceTypes,
|
||||
remark: '',
|
||||
images: [],
|
||||
videos: [],
|
||||
envImages: [],
|
||||
extras: [
|
||||
{ key: 'tear', name: '拆旧拆卸', price: 50, num: 0 },
|
||||
{ key: 'cut', name: '管路开孔打孔', price: 20, num: 0 }
|
||||
]
|
||||
extras: extras
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -859,6 +911,7 @@
|
||||
if (firstGoods.cat_id) drawerCatId.value = firstGoods.cat_id;
|
||||
if (firstGoods.cat_name) drawerCatName.value = firstGoods.cat_name;
|
||||
if (firstGoods.cat_icon) currentCatIcon.value = firstGoods.cat_icon;
|
||||
if (firstGoods.cat_icon_url) currentCatIconUrl.value = firstGoods.cat_icon_url;
|
||||
calc();
|
||||
}
|
||||
});
|
||||
@@ -1446,6 +1499,12 @@
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.bab-sub-hint {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.bab-btn-main {
|
||||
background: linear-gradient(135deg, #FF9436 0%, #F0710D 100%);
|
||||
height: 88rpx;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
||||
<circle cx="50" cy="50" r="42" fill="#FFF7F0"/>
|
||||
<rect x="28" y="24" width="44" height="52" rx="6" fill="#FFFFFF" stroke="#F5A363" stroke-width="2.5"/>
|
||||
<path d="M40 20h20a3 3 0 0 1 3 3v3H37v-3a3 3 0 0 1 3-3z" fill="#F0710D"/>
|
||||
<circle cx="50" cy="24" r="2" fill="#FFFFFF"/>
|
||||
<rect x="36" y="38" width="28" height="4" rx="2" fill="#FFE2CC"/>
|
||||
<rect x="36" y="48" width="20" height="4" rx="2" fill="#FFE2CC"/>
|
||||
<rect x="36" y="58" width="24" height="4" rx="2" fill="#FFE2CC"/>
|
||||
<circle cx="70" cy="68" r="14" fill="#F0710D" fill-opacity="0.12"/>
|
||||
<path d="M66 68l3 3 6-6" stroke="#F0710D" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 748 B |
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160" width="160" height="160">
|
||||
<defs>
|
||||
<linearGradient id="docGrad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFD494"/>
|
||||
<stop offset="100%" stop-color="#FFA843"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="docGrad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF9E36"/>
|
||||
<stop offset="100%" stop-color="#ED6F0E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="checkGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#34C759"/>
|
||||
<stop offset="100%" stop-color="#248A3D"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#F0710D" flood-opacity="0.2"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 星星点缀 -->
|
||||
<path d="M42 38 L44 32 L46 38 L52 40 L46 42 L44 48 L42 42 L36 40 Z" fill="#FFB85C" opacity="0.8"/>
|
||||
<circle cx="126" cy="36" r="3" fill="#FFA843" opacity="0.6"/>
|
||||
<circle cx="34" cy="116" r="2.5" fill="#FFA843" opacity="0.6"/>
|
||||
<path d="M124 108 L125.5 103 L127 108 L132 109.5 L127 111 L125.5 116 L124 111 L119 109.5 Z" fill="#FFB85C" opacity="0.8"/>
|
||||
|
||||
<!-- 底层文档卡片 -->
|
||||
<rect x="42" y="32" width="62" height="78" rx="10" fill="url(#docGrad1)" opacity="0.75" />
|
||||
|
||||
<!-- 主层文档卡片 -->
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="54" y="42" width="66" height="82" rx="12" fill="url(#docGrad2)"/>
|
||||
<!-- 文本横条占位 -->
|
||||
<rect x="68" y="58" width="30" height="7" rx="3.5" fill="#FFFFFF" opacity="0.9"/>
|
||||
<rect x="68" y="72" width="22" height="7" rx="3.5" fill="#FFFFFF" opacity="0.7"/>
|
||||
</g>
|
||||
|
||||
<!-- 成功绿色对勾圆徽章 -->
|
||||
<g filter="url(#shadow)">
|
||||
<circle cx="112" cy="116" r="18" fill="#FFFFFF"/>
|
||||
<circle cx="112" cy="116" r="15" fill="url(#checkGrad)"/>
|
||||
<!-- 对勾 -->
|
||||
<path d="M106 116.5 L110 120.5 L118 111.5" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 2.0 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16 3l11 4.5v9.5c0 6.5-5 11.5-11 13-6-1.5-11-6.5-11-13V7.5L16 3z" stroke="#E8331B" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<path d="M11 16l3.5 3.5L21 12" stroke="#E8331B" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 337 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<circle cx="16" cy="16" r="13" stroke="#E07A2E" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="16" r="7" stroke="#E07A2E" stroke-width="1.8" stroke-dasharray="3 3"/>
|
||||
<circle cx="16" cy="16" r="3.2" fill="#E07A2E"/>
|
||||
<path d="M16 3v4M16 25v4M3 16h4M25 16h4" stroke="#E07A2E" stroke-width="2.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 422 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect x="5" y="4" width="22" height="24" rx="3.5" stroke="#4A7CFF" stroke-width="2.2"/>
|
||||
<path d="M10 10h12M10 16h12M10 22h7" stroke="#4A7CFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="23" cy="22" r="3" fill="#4A7CFF"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 317 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M4 7a3 3 0 0 1 3-3h18a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3h-8l-6 5v-5H7a3 3 0 0 1-3-3V7z" stroke="#7A5AF8" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<path d="M16 9v10M12.5 11.5h6a2 2 0 0 1 0 3.5h-5a2 2 0 0 0 0 3.5h6.5" stroke="#7A5AF8" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 368 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M5 16a11 11 0 0 1 22 0v6a3 3 0 0 1-3 3h-3v-8h4v-1a9 9 0 1 0-18 0v1h4v8H7a3 3 0 0 1-3-3v-6z" fill="#2A8FD8" fill-opacity="0.15" stroke="#2A8FD8" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<path d="M19 25v3a2 2 0 0 1-2 2h-2" stroke="#2A8FD8" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 377 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect x="3" y="8" width="26" height="18" rx="3.5" stroke="#2FA84F" stroke-width="2.2"/>
|
||||
<path d="M7 8V6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2" stroke="#2FA84F" stroke-width="2"/>
|
||||
<path d="M21 17h8v-4h-8a2 2 0 0 0 0 4z" fill="#2FA84F" fill-opacity="0.2" stroke="#2FA84F" stroke-width="2"/>
|
||||
<circle cx="24" cy="15" r="1.5" fill="#2FA84F"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 421 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#F0710D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="3 11 22 2 13 21 11 13 3 11"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 233 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 2a6 6 0 0 0-6 6c0 4.5 6 10 6 10s6-5.5 6-10a6 6 0 0 0-6-6z" stroke="#F0710D" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<circle cx="10" cy="8" r="2" fill="#F0710D"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 264 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="10" r="7.5" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<path d="M10 6v4.5l3 2" stroke="#9A9A9A" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 239 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 351 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#666666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 318 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#8E8E93" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 273 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="9" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
<circle cx="12" cy="12" r="5" stroke="#F0710D" stroke-width="1.6" stroke-dasharray="2 2"/>
|
||||
<circle cx="12" cy="12" r="2.5" fill="#F0710D"/>
|
||||
<path d="M12 3v3M12 18v3M3 12h3M18 12h3" stroke="#F0710D" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 413 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="9" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<circle cx="12" cy="12" r="5" stroke="#9A9A9A" stroke-width="1.6" stroke-dasharray="2 2"/>
|
||||
<circle cx="12" cy="12" r="2" fill="#9A9A9A"/>
|
||||
<path d="M12 3v3M12 18v3M3 12h3M18 12h3" stroke="#9A9A9A" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 396 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 10.5L12 3l9 7.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V10.5z" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED" stroke-linejoin="round"/>
|
||||
<path d="M9 22V12h6v10" stroke="#F0710D" stroke-width="1.8" fill="#F0710D" fill-opacity="0.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 349 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 10.5L12 3l9 7.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V10.5z" stroke="#8E8E93" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M9 22V12h6v10" stroke="#8E8E93" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 300 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED" stroke-linejoin="round"/>
|
||||
<circle cx="8" cy="12" r="1" fill="#F0710D"/>
|
||||
<circle cx="12" cy="12" r="1" fill="#F0710D"/>
|
||||
<circle cx="16" cy="12" r="1" fill="#F0710D"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 500 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" stroke="#8E8E93" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 339 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 294 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="#8E8E93" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="#8E8E93" stroke-width="1.8"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 264 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
<path d="M7 7h10M7 12h10M7 17h6" stroke="#F0710D" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 312 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" stroke="#8E8E93" stroke-width="1.8"/>
|
||||
<path d="M7 7h10M7 12h10M7 17h6" stroke="#8E8E93" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 299 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
<path d="M16 2v4M8 2v4M3 10h18" stroke="#F0710D" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M8 15l2.5 2.5 5-5" stroke="#F0710D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 426 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<path d="M16 2v4M8 2v4M3 10h18" stroke="#9A9A9A" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M7 15l2 2 4-4" stroke="#9A9A9A" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 409 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 7H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
<path d="M16 3H5a2 2 0 0 0-2 2v2h18V5a2 2 0 0 0-2-2h-5z" stroke="#F0710D" stroke-width="1.8" fill="#F0710D" fill-opacity="0.18"/>
|
||||
<circle cx="17.5" cy="14" r="2" fill="#F0710D"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 401 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 7H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<path d="M16 3H5a2 2 0 0 0-2 2v2h18V5a2 2 0 0 0-2-2h-5z" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<circle cx="17.5" cy="14" r="1.5" fill="#9A9A9A"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 353 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3H4V5z" fill="#F0710D" fill-opacity="0.18"/>
|
||||
<rect x="3" y="8" width="18" height="13" rx="2" stroke="#F0710D" stroke-width="1.8" fill="#FFF5ED"/>
|
||||
<path d="M8 8V5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3" stroke="#F0710D" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="13" r="2.5" fill="#F0710D"/>
|
||||
<path d="M7 17h10" stroke="#F0710D" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 520 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3H4V5z" fill="#9A9A9A" fill-opacity="0.2"/>
|
||||
<rect x="3" y="8" width="18" height="13" rx="2" stroke="#9A9A9A" stroke-width="1.8"/>
|
||||
<path d="M8 8V5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3" stroke="#9A9A9A" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M10 12h4M12 14v-4" stroke="#9A9A9A" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
之后 宽度: | 高度: | 大小: 462 B |
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* 接口清单(与后端 application/api/controller 一一对应)
|
||||
*/
|
||||
import { get, post } from './request';
|
||||
import { get, post, uploadFile } from './request';
|
||||
|
||||
export default {
|
||||
/* ---------- 公共 / 上传 ---------- */
|
||||
upload: (filePath) => uploadFile(filePath),
|
||||
|
||||
/* ---------- 鉴权 ---------- */
|
||||
login: (data) => post('/auth/login', data, { silent: true }),
|
||||
wxLogin: (data) => post('/auth/wxlogin', data, { silent: true }),
|
||||
@@ -42,6 +45,7 @@ export default {
|
||||
masterApply: (data) => post('/master/apply', data),
|
||||
masterInfo: () => get('/master/info'),
|
||||
masterHall: (data) => get('/master/hall', data),
|
||||
masterOrderDetail: (id, params) => get('/master/orderDetail', { id: id, ...(params || {}) }),
|
||||
masterGrab: (data) => post('/master/grab', data),
|
||||
masterOrders: (data) => get('/master/orders', data),
|
||||
masterContact: (data) => post('/master/contact', data),
|
||||
|
||||
@@ -123,3 +123,37 @@ export function get(path, data, options) {
|
||||
export function post(path, data, options) {
|
||||
return request(path, Object.assign({ method: 'POST', data: data }, options || {}));
|
||||
}
|
||||
|
||||
export function uploadFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = {};
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
header.token = token;
|
||||
}
|
||||
uni.uploadFile({
|
||||
url: BASE_URL + '/common/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
header: header,
|
||||
success: (res) => {
|
||||
try {
|
||||
const body = JSON.parse(res.data);
|
||||
if (body.code === 1) {
|
||||
resolve(body.data);
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: body.msg || '上传失败', icon: 'none' });
|
||||
reject(new Error(body.msg));
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '解析上传响应失败', icon: 'none' });
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({ title: '网络异常,上传失败', icon: 'none' });
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||