feat: 新增师傅端功能及服务增项管理模块

- 新增师傅端路由和页面配置,切换登录后默认跳转到师傅大厅
- 新增服务分类图标上传功能和增项服务管理模块
- 新增订单、申诉、结算等模型关联关系及后台控制器
- 新增师傅端TabBar组件和消息页面跳转逻辑
- 新增文件上传接口和服务计算逻辑
- 新增各类SVG图标和空状态提示
这个提交包含在:
YuanWen
2026-11-03 15:45:21 +08:00
父节点 c01e8fc7d9
当前提交 361dffc7d8
共有 82 个文件被更改,包括 4787 次插入 和 531 次删除

查看文件

@@ -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>

文件差异内容过多而无法显示 加载差异

查看文件

@@ -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>

查看文件

@@ -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);
}
});
});
}