diff --git a/db/schema_phase13_master_skill_menu.sql b/db/schema_phase13_master_skill_menu.sql new file mode 100644 index 0000000..1568a31 --- /dev/null +++ b/db/schema_phase13_master_skill_menu.sql @@ -0,0 +1,99 @@ +-- ============================================================================ +-- 优匠优服 · Phase 13 师傅技能与分类管理规范体系 +-- 1. 创建师傅技能分类表 fa_skill_category +-- 2. 创建师傅技能列表表 fa_skill_dict +-- 3. 扩充师傅技能关联表 fa_master_skill (支持技能名、分类与自定义来源) +-- 4. 规范后台【师傅管理】子菜单: +-- - 师傅列表 (master): 内置师傅技能查看与弹窗配置 +-- - 技能分类 (skill_category): 技能大类管理 +-- - 技能列表 (skill_dict): 技能明细与师傅自定义技能管理 +-- ============================================================================ +SET NAMES utf8mb4; + +-- 1. 创建师傅技能分类表 +CREATE TABLE IF NOT EXISTS `fa_skill_category` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '分类名称', + `icon` varchar(255) NOT NULL DEFAULT '' COMMENT '分类图标', + `weigh` int(11) NOT NULL DEFAULT '0' COMMENT '权重', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + KEY `weigh` (`weigh`), + KEY `status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='师傅技能分类表'; + +-- 2. 创建师傅技能列表表 +CREATE TABLE IF NOT EXISTS `fa_skill_dict` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `category_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '技能分类ID', + `category` varchar(50) NOT NULL DEFAULT '' COMMENT '技能大类', + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '技能名称', + `source` enum('system','custom') NOT NULL DEFAULT 'system' COMMENT '来源:system=系统预置,custom=师傅自定义', + `master_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '自定义师傅ID', + `weigh` int(11) NOT NULL DEFAULT '0' COMMENT '权重', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `category_id` (`category_id`), + KEY `category` (`category`), + KEY `source` (`source`), + KEY `status` (`status`), + KEY `weigh` (`weigh`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='师傅技能字典表'; + +-- 3. 扩充师傅技能关联表 +ALTER TABLE `fa_master_skill` + ADD COLUMN IF NOT EXISTS `skill_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '技能字典ID' AFTER `cat_id`, + ADD COLUMN IF NOT EXISTS `skill_name` varchar(50) NOT NULL DEFAULT '' COMMENT '技能名称' AFTER `skill_id`, + ADD COLUMN IF NOT EXISTS `category` varchar(50) NOT NULL DEFAULT '' COMMENT '所属大类' AFTER `skill_name`, + ADD COLUMN IF NOT EXISTS `is_custom` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否自定义添加:0=字典选择,1=自定义输入' AFTER `category`; + +-- 4. 调整师傅列表菜单 +UPDATE `fa_auth_rule` SET `title` = '师傅列表', `weigh` = 175 WHERE `name` = 'master'; + +-- 隐藏原单独的师傅技能菜单(已全面合并进师傅列表) +UPDATE `fa_auth_rule` SET `ismenu` = 0 WHERE `name` = 'master_skill'; + +-- 5. 注册【技能分类 (skill_category)】菜单 +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category', '技能分类', 'fa fa-th-large', '', '师傅服务技能大类管理', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 173, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_manage' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category/index', '查看', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'skill_category' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category/index'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category/add', '添加', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'skill_category' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category/add'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category/edit', '编辑', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'skill_category' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category/edit'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category/del', '删除', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'skill_category' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category/del'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'skill_category/multi', '批量更新', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'skill_category' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'skill_category/multi'); + +-- 6. 注册/更新【技能列表 (skill_dict)】菜单 +UPDATE `fa_auth_rule` SET `title` = '技能列表', `icon` = 'fa fa-tags', `weigh` = 172 WHERE `name` = 'skill_dict'; diff --git a/server/addons/address/.addonrc b/server/addons/address/.addonrc new file mode 100644 index 0000000..6bab05b --- /dev/null +++ b/server/addons/address/.addonrc @@ -0,0 +1 @@ +{"files":["public\\assets\\addons\\address\\js\\gcoord.min.js","public\\assets\\addons\\address\\js\\jquery.autocomplete.js"],"license":"regular","licenseto":"26425","licensekey":"3DruY9BJMFlc5v7U qTT2DqvsmTkJ8a4TGK6OUA==","domains":[],"licensecodes":[],"validations":[]} \ No newline at end of file diff --git a/server/addons/address/Address.php b/server/addons/address/Address.php new file mode 100644 index 0000000..4bc36ea --- /dev/null +++ b/server/addons/address/Address.php @@ -0,0 +1,32 @@ + + */ +class Address extends Addons +{ + + /** + * 插件安装方法 + * @return bool + */ + public function install() + { + return true; + } + + /** + * 插件卸载方法 + * @return bool + */ + public function uninstall() + { + return true; + } + +} diff --git a/server/addons/address/bootstrap.js b/server/addons/address/bootstrap.js new file mode 100644 index 0000000..655162d --- /dev/null +++ b/server/addons/address/bootstrap.js @@ -0,0 +1,34 @@ +require([], function () { + //绑定data-toggle=addresspicker属性点击事件 + + $(document).on('click', "[data-toggle='addresspicker']", function () { + var that = this; + var callback = $(that).data('callback'); + var input_id = $(that).data("input-id") ? $(that).data("input-id") : ""; + var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : ""; + var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : ""; + var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : ""; + var lat = lat_id ? $("#" + lat_id).val() : ''; + var lng = lng_id ? $("#" + lng_id).val() : ''; + var zoom = zoom_id ? $("#" + zoom_id).val() : ''; + var url = "/addons/address/index/select"; + url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : ''; + Fast.api.open(url, '位置选择', { + callback: function (res) { + input_id && $("#" + input_id).val(res.address).trigger("change"); + lat_id && $("#" + lat_id).val(res.lat).trigger("change"); + lng_id && $("#" + lng_id).val(res.lng).trigger("change"); + zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change"); + + try { + //执行回调函数 + if (typeof callback === 'function') { + callback.call(that, res); + } + } catch (e) { + + } + } + }); + }); +}); diff --git a/server/addons/address/config.php b/server/addons/address/config.php new file mode 100644 index 0000000..962b36e --- /dev/null +++ b/server/addons/address/config.php @@ -0,0 +1,146 @@ + 'maptype', + 'title' => '默认地图类型', + 'type' => 'radio', + 'content' => [ + 'tianditu' => '天地图', + 'baidu' => '百度地图', + 'amap' => '高德地图', + 'tencent' => '腾讯地图', + ], + 'value' => 'tianditu', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'zoom', + 'title' => '默认缩放级别', + 'type' => 'string', + 'content' => [], + 'value' => '11', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'lat', + 'title' => '默认Lat', + 'type' => 'string', + 'content' => [], + 'value' => '39.919990', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'lng', + 'title' => '默认Lng', + 'type' => 'string', + 'content' => [], + 'value' => '116.456270', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'baidukey', + 'title' => '百度地图KEY', + 'type' => 'string', + 'content' => [], + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'amapkey', + 'title' => '高德地图KEY', + 'type' => 'string', + 'content' => [], + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'amapsecurityjscode', + 'title' => '高德地图安全密钥', + 'type' => 'string', + 'content' => [], + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'tencentkey', + 'title' => '腾讯地图KEY', + 'type' => 'string', + 'content' => [], + 'value' => '', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'tianditukey', + 'title' => '天地图KEY', + 'type' => 'string', + 'content' => [], + 'value' => '369ea1edc677b9e3746093f8d9dc4465', + 'rule' => '', + 'msg' => '', + 'tip' => '天地图开发密钥TK', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'coordtype', + 'title' => '坐标系类型', + 'type' => 'select', + 'content' => [ + 'DEFAULT' => '默认(使用所选地图默认坐标系)', + 'WGS84' => 'WGS-84 / CGCS2000 (天地图原生)', + 'GCJ02' => 'GCJ-02 (高德/腾讯)', + 'BD09' => 'BD-09 (百度)', + ], + 'value' => 'DEFAULT', + 'rule' => 'required', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => '__tips__', + 'title' => '温馨提示', + 'type' => '', + 'content' => [], + 'value' => '请先申请对应地图的Key,配置后再使用', + 'rule' => '', + 'msg' => '', + 'tip' => '', + 'ok' => '', + 'extend' => 'alert-danger-light', + ], +]; diff --git a/server/addons/address/controller/Index.php b/server/addons/address/controller/Index.php new file mode 100644 index 0000000..5cd31e3 --- /dev/null +++ b/server/addons/address/controller/Index.php @@ -0,0 +1,64 @@ +request->langset(); + $lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn'; + + $site = Config::get("site"); + + // 配置信息 + $config = [ + 'site' => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])), + 'upload' => null, + 'modulename' => 'addons', + 'controllername' => 'index', + 'actionname' => 'index', + 'jsname' => 'addons/address', + 'moduleurl' => '', + 'language' => $lang + ]; + $config = array_merge($config, Config::get("view_replace_str")); + + // 配置信息后 + Hook::listen("config_init", $config); + // 加载当前控制器语言包 + $this->view->assign('site', $site); + $this->view->assign('config', $config); + + return $this->view->fetch(); + } + + // 选择地址 + public function select() + { + $config = get_addon_config('address'); + $zoom = (int)$this->request->get('zoom', $config['zoom']); + $lng = (float)$this->request->get('lng'); + $lat = (float)$this->request->get('lat'); + $address = $this->request->get('address'); + $lng = $lng ?: $config['lng']; + $lat = $lat ?: $config['lat']; + $this->view->assign('zoom', $zoom); + $this->view->assign('lng', $lng); + $this->view->assign('lat', $lat); + $this->view->assign('address', $address); + $maptype = $config['maptype']; + if (!isset($config[$maptype . 'key']) || !$config[$maptype . 'key']) { + $this->error("请在配置中配置对应类型地图的密钥"); + } + return $this->view->fetch('index/' . $maptype); + } + +} diff --git a/server/addons/address/info.ini b/server/addons/address/info.ini new file mode 100644 index 0000000..8f60fe3 --- /dev/null +++ b/server/addons/address/info.ini @@ -0,0 +1,10 @@ +name = address +title = 地址位置选择插件 +intro = 地图位置选择插件,可返回地址和经纬度 +author = FastAdmin +website = +version = 1.1.8 +state = 1 +url = /addons/address.html +license = regular +licenseto = 26425 diff --git a/server/addons/address/view/index/amap.html b/server/addons/address/view/index/amap.html new file mode 100644 index 0000000..62972fc --- /dev/null +++ b/server/addons/address/view/index/amap.html @@ -0,0 +1,258 @@ + + + + + 地址选择器 + + + + + + +
确定
+
+ + + + + + + + + diff --git a/server/addons/address/view/index/baidu.html b/server/addons/address/view/index/baidu.html new file mode 100644 index 0000000..86aa74b --- /dev/null +++ b/server/addons/address/view/index/baidu.html @@ -0,0 +1,243 @@ + + + + + 地址选择器 + + + + + + +
确定
+
+ + + + + + diff --git a/server/addons/address/view/index/index.html b/server/addons/address/view/index/index.html new file mode 100644 index 0000000..07f9712 --- /dev/null +++ b/server/addons/address/view/index/index.html @@ -0,0 +1,132 @@ + + + + + 地图位置(经纬度)选择插件 + + + + +
+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
参数释义
data-input-id填充地址的文本框ID
data-lng-id填充经度的文本框ID
data-lat-id填充纬度的文本框ID
data-zoom-id填充缩放的文本框ID
+
+ +
+
+
+ +
+ + + + + + + + + diff --git a/server/addons/address/view/index/tencent.html b/server/addons/address/view/index/tencent.html new file mode 100644 index 0000000..89df386 --- /dev/null +++ b/server/addons/address/view/index/tencent.html @@ -0,0 +1,291 @@ + + + + + 地址选择器 + + + + + + +
确定
+
+ + + + + + + + + diff --git a/server/addons/address/view/index/tianditu.html b/server/addons/address/view/index/tianditu.html new file mode 100644 index 0000000..9fa65b8 --- /dev/null +++ b/server/addons/address/view/index/tianditu.html @@ -0,0 +1,404 @@ + + + + + 地址选择器 - 天地图 + + + + + + + + +
+
+ {$address|htmlentities|default='点击或拖动标注选点'} +
+
+ 坐标:{$lng}, {$lat} +
+
+ +
确定
+
+ + + + + + + + diff --git a/server/application/admin/controller/Master.php b/server/application/admin/controller/Master.php index 21beeb0..5c5e058 100644 --- a/server/application/admin/controller/Master.php +++ b/server/application/admin/controller/Master.php @@ -43,10 +43,153 @@ class Master extends Backend ->where($where) ->order($sort, $order) ->paginate($limit); - $result = ['total' => $list->total(), 'rows' => $list->items()]; + + // 合并查出当前页每个师傅拥有的技能 + $items = $list->items(); + $masterIds = array_column($items, 'id'); + $skillsGroup = []; + if (!empty($masterIds)) { + $allSkills = \think\Db::name('master_skill') + ->where('master_id', 'in', $masterIds) + ->where('status', 'normal') + ->select(); + foreach ($allSkills as $sk) { + $name = !empty($sk['skill_name']) ? $sk['skill_name'] : ''; + if ($name) { + $skillsGroup[$sk['master_id']][] = $name; + } + } + } + foreach ($items as &$it) { + $it['skills_list'] = $skillsGroup[$it['id']] ?? []; + } + + $result = ['total' => $list->total(), 'rows' => $items]; return json($result); } + /** + * 师傅服务技能管理(内置于师傅列表) + */ + public function skills($ids = null) + { + $row = $this->model->find($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + + if ($this->request->isPost()) { + $selectedSkills = (array)$this->request->post('skills/a', []); + $customText = trim((string)$this->request->post('custom_text', '')); + + // 1. 处理自定义输入的技能 + $customNames = []; + if (!empty($customText)) { + $parts = preg_split('/[,,\s]+/u', $customText); + foreach ($parts as $p) { + $p = trim($p); + if (!empty($p) && !in_array($p, $customNames) && !in_array($p, $selectedSkills)) { + $customNames[] = $p; + } + } + } + + // 2. 清理当前师傅原有的技能 + \think\Db::name('master_skill')->where('master_id', $row['id'])->delete(); + + // 3. 写入选中的技能 + $dictRows = \think\Db::name('skill_dict')->where('name', 'in', $selectedSkills)->select(); + $dictMap = []; + foreach ($dictRows as $dr) { + $dictMap[$dr['name']] = $dr; + } + + $now = time(); + foreach ($selectedSkills as $sName) { + $dict = $dictMap[$sName] ?? null; + \think\Db::name('master_skill')->insert([ + 'master_id' => $row['id'], + 'cat_id' => 0, + 'skill_id' => $dict ? $dict['id'] : 0, + 'skill_name' => $sName, + 'category' => $dict ? $dict['category'] : '服务技能', + 'is_custom' => 0, + 'cert_image' => '', + 'status' => 'normal', + 'createtime' => $now + ]); + } + + // 4. 处理自定义输入的技能(沉淀入库) + foreach ($customNames as $cName) { + $exist = \think\Db::name('skill_dict')->where('name', $cName)->find(); + if (!$exist) { + $skillDictId = \think\Db::name('skill_dict')->insertGetId([ + 'category_id'=> 0, + 'category' => '其他技能', + 'name' => $cName, + 'source' => 'custom', + 'master_id' => $row['id'], + 'weigh' => 0, + 'status' => 'normal', + 'createtime' => $now, + 'updatetime' => $now + ]); + } else { + $skillDictId = $exist['id']; + } + + \think\Db::name('master_skill')->insert([ + 'master_id' => $row['id'], + 'cat_id' => 0, + 'skill_id' => $skillDictId, + 'skill_name' => $cName, + 'category' => '其他技能', + 'is_custom' => 1, + 'cert_image' => '', + 'status' => 'normal', + 'createtime' => $now + ]); + } + + $this->success('技能配置更新成功'); + } + + // 读取技能字典全部分类与技能 + $categories = \think\Db::name('skill_category')->where('status', 'normal')->order('weigh desc')->select(); + $allDictSkills = \think\Db::name('skill_dict')->where('status', 'normal')->order('weigh desc')->select(); + + $catTree = []; + foreach ($categories as $cat) { + $catTree[$cat['name']] = []; + } + foreach ($allDictSkills as $item) { + $cName = $item['category']; + if (!isset($catTree[$cName])) { + $catTree[$cName] = []; + } + $catTree[$cName][] = $item; + } + + // 读取当前师傅已拥有的技能 + $mySkills = \think\Db::name('master_skill')->where('master_id', $row['id'])->select(); + $selectedSkills = []; + $customArr = []; + foreach ($mySkills as $ms) { + if ($ms['is_custom'] == 1) { + $customArr[] = $ms['skill_name']; + } else { + $selectedSkills[] = $ms['skill_name']; + } + } + + $this->view->assign('row', $row); + $this->view->assign('catTree', $catTree); + $this->view->assign('selectedSkills', $selectedSkills); + $this->view->assign('customText', implode(' ', $customArr)); + return $this->view->fetch(); + } + /** * 师傅全景档案画像 */ @@ -78,10 +221,19 @@ class Master extends Backend ->limit(5) ->select(); + // 4. 服务技能与认证证书 + $skills = \think\Db::name('master_skill') + ->alias('s') + ->join('service_cat c', 'c.id = s.cat_id', 'LEFT') + ->where('s.master_id', $row['id']) + ->field('s.*, c.name as cat_name') + ->select(); + $this->view->assign('row', $row); $this->view->assign('recentOrders', $recentOrders); $this->view->assign('recentWithdraws', $recentWithdraws); $this->view->assign('recentReviews', $recentReviews); + $this->view->assign('skills', $skills); return $this->view->fetch(); } diff --git a/server/application/admin/controller/MasterSkill.php b/server/application/admin/controller/MasterSkill.php new file mode 100644 index 0000000..a977336 --- /dev/null +++ b/server/application/admin/controller/MasterSkill.php @@ -0,0 +1,51 @@ +model = new \app\admin\model\MasterSkill; + $this->view->assign("statusList", $this->model->getStatusList()); + } + + /** + * 查看 + */ + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + return $this->view->fetch(); + } + if ($this->request->request('keyField')) { + return $this->selectpage(); + } + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + $list = $this->model + ->with(['master', 'cat']) + ->where($where) + ->order($sort, $order) + ->paginate($limit); + + $result = ['total' => $list->total(), 'rows' => $list->items()]; + return json($result); + } +} diff --git a/server/application/admin/controller/Service.php b/server/application/admin/controller/Service.php index 9f8f71b..4f4740e 100644 --- a/server/application/admin/controller/Service.php +++ b/server/application/admin/controller/Service.php @@ -29,10 +29,13 @@ class Service extends Backend $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') + ->alias('a') + ->leftJoin('service_cat c', 'a.cat_id = c.id') + ->field('a.*, COALESCE(c.name, "全平台通用") as cat_name') + ->where('a.status', 'normal') + ->order('a.cat_id asc, a.weigh desc, a.id asc') ->select(); $this->view->assign("addonsPool", $addonsPool); } diff --git a/server/application/admin/controller/SkillCategory.php b/server/application/admin/controller/SkillCategory.php new file mode 100644 index 0000000..db60a29 --- /dev/null +++ b/server/application/admin/controller/SkillCategory.php @@ -0,0 +1,50 @@ +model = new \app\admin\model\SkillCategory; + $this->view->assign("statusList", $this->model->getStatusList()); + } + + /** + * 查看分类列表 + */ + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + return $this->view->fetch(); + } + if ($this->request->request('keyField')) { + return $this->selectpage(); + } + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + $list = $this->model + ->withCount('skills') + ->where($where) + ->order($sort, $order) + ->paginate($limit); + + $result = ['total' => $list->total(), 'rows' => $list->items()]; + return json($result); + } +} diff --git a/server/application/admin/controller/SkillDict.php b/server/application/admin/controller/SkillDict.php new file mode 100644 index 0000000..9ed673c --- /dev/null +++ b/server/application/admin/controller/SkillDict.php @@ -0,0 +1,117 @@ +model = new \app\admin\model\SkillDict; + + // 从真实技能分类表中拉取分类列表 + $categoryList = Db::name('skill_category') + ->where('status', 'normal') + ->order('weigh desc, id asc') + ->column('name', 'id'); + + $this->view->assign("categoryList", $categoryList); + $this->view->assign("sourceList", $this->model->getSourceList()); + $this->view->assign("statusList", $this->model->getStatusList()); + } + + /** + * 查看技能列表 + */ + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + return $this->view->fetch(); + } + 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); + } + + /** + * 添加技能 + */ + public function add() + { + if ($this->request->isPost()) { + $params = $this->request->post("row/a"); + if ($params) { + if (!empty($params['category_id'])) { + $cat = Db::name('skill_category')->where('id', $params['category_id'])->find(); + if ($cat) { + $params['category'] = $cat['name']; + } + } + $result = $this->model->allowField(true)->save($params); + if ($result !== false) { + $this->success(); + } else { + $this->error($this->model->getError()); + } + } + $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')); + } + if ($this->request->isPost()) { + $params = $this->request->post("row/a"); + if ($params) { + if (!empty($params['category_id'])) { + $cat = Db::name('skill_category')->where('id', $params['category_id'])->find(); + if ($cat) { + $params['category'] = $cat['name']; + } + } + $result = $row->allowField(true)->save($params); + if ($result !== false) { + $this->success(); + } else { + $this->error($row->getError()); + } + } + $this->error(__('Parameter %s can not be empty', '')); + } + $this->view->assign("row", $row); + return $this->view->fetch(); + } +} diff --git a/server/application/admin/lang/zh-cn/master_skill.php b/server/application/admin/lang/zh-cn/master_skill.php new file mode 100644 index 0000000..e5ad26e --- /dev/null +++ b/server/application/admin/lang/zh-cn/master_skill.php @@ -0,0 +1,15 @@ + 'ID', + 'Master_id' => '师傅ID', + 'Cat_id' => '服务技能/类目ID', + 'Cert_image' => '资质证书', + 'Status' => '状态', + 'Normal' => '正常', + 'Hidden' => '隐藏', + 'Createtime' => '认证时间', + 'Master.realname'=> '师傅姓名', + 'Master.mobile' => '师傅电话', + 'Cat.name' => '服务类目' +]; diff --git a/server/application/admin/lang/zh-cn/service.php b/server/application/admin/lang/zh-cn/service.php index b08adc6..3f2cbbe 100644 --- a/server/application/admin/lang/zh-cn/service.php +++ b/server/application/admin/lang/zh-cn/service.php @@ -2,7 +2,7 @@ return [ 'Cat_id' => '所属服务类目', - 'Name' => '服务项目名称', + 'Name' => '服务商品名称', 'Image' => '服务展示主图', 'Price_type' => '计价方式', 'Base_price' => '基础基准价(元)', diff --git a/server/application/admin/lang/zh-cn/service/addon.php b/server/application/admin/lang/zh-cn/service/addon.php new file mode 100644 index 0000000..49dafce --- /dev/null +++ b/server/application/admin/lang/zh-cn/service/addon.php @@ -0,0 +1,12 @@ + '所属服务类目', + 'Name' => '增值服务名称', + 'Unit' => '计价单位', + 'Default_price' => '平台指导价(元)', + 'Weigh' => '排序权重', + 'Status' => '服务状态', + 'Normal' => '正常启用', + 'Hidden' => '隐藏下架', +]; diff --git a/server/application/admin/lang/zh-cn/skill_category.php b/server/application/admin/lang/zh-cn/skill_category.php new file mode 100644 index 0000000..0cef63a --- /dev/null +++ b/server/application/admin/lang/zh-cn/skill_category.php @@ -0,0 +1,13 @@ + 'ID', + 'Name' => '分类名称', + 'Icon' => '图标', + 'Weigh' => '权重', + 'Status' => '状态', + 'Normal' => '正常启用', + 'Hidden' => '隐藏停用', + 'Createtime' => '创建时间', + 'Updatetime' => '更新时间' +]; diff --git a/server/application/admin/lang/zh-cn/skill_dict.php b/server/application/admin/lang/zh-cn/skill_dict.php new file mode 100644 index 0000000..1b917c6 --- /dev/null +++ b/server/application/admin/lang/zh-cn/skill_dict.php @@ -0,0 +1,16 @@ + 'ID', + 'Category' => '技能大类', + 'Name' => '技能名称', + 'Source' => '字典来源', + 'Master_id' => '师傅ID', + 'Weigh' => '权重', + 'Status' => '状态', + 'Normal' => '正常启用', + 'Hidden' => '隐藏停用', + 'Createtime' => '创建时间', + 'Updatetime' => '更新时间', + 'Master.realname'=> '提交师傅' +]; diff --git a/server/application/admin/model/MasterSkill.php b/server/application/admin/model/MasterSkill.php new file mode 100644 index 0000000..ba16e9d --- /dev/null +++ b/server/application/admin/model/MasterSkill.php @@ -0,0 +1,46 @@ + __('Normal'), 'hidden' => __('Hidden')]; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + public function master() + { + return $this->belongsTo('app\admin\model\Master', 'master_id', 'id')->setEagerlyType(0); + } + + public function cat() + { + return $this->belongsTo('app\admin\model\service\Cat', 'cat_id', 'id')->setEagerlyType(0); + } +} diff --git a/server/application/admin/model/SkillCategory.php b/server/application/admin/model/SkillCategory.php new file mode 100644 index 0000000..35cd78d --- /dev/null +++ b/server/application/admin/model/SkillCategory.php @@ -0,0 +1,47 @@ + '正常启用', + 'hidden' => '隐藏停用' + ]; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + /** + * 关联技能列表 + */ + public function skills() + { + return $this->hasMany('app\admin\model\SkillDict', 'category_id', 'id'); + } +} diff --git a/server/application/admin/model/SkillDict.php b/server/application/admin/model/SkillDict.php new file mode 100644 index 0000000..412ffb9 --- /dev/null +++ b/server/application/admin/model/SkillDict.php @@ -0,0 +1,73 @@ + '安装维修', + '建筑装修' => '建筑装修', + '物流运输' => '物流运输', + '汽车服务' => '汽车服务', + '家政服务' => '家政服务', + '便民服务' => '便民服务', + '其他技能' => '其他技能' + ]; + } + + public function getSourceList() + { + return [ + 'system' => '系统预置', + 'custom' => '师傅自定义' + ]; + } + + public function getStatusList() + { + return [ + 'normal' => '正常启用', + 'hidden' => '隐藏停用' + ]; + } + + public function getSourceTextAttr($value, $data) + { + $value = $value ?: ($data['source'] ?? ''); + $list = $this->getSourceList(); + return $list[$value] ?? ''; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + public function master() + { + return $this->belongsTo('app\admin\model\Master', 'master_id', 'id'); + } +} diff --git a/server/application/admin/view/master/detail.html b/server/application/admin/view/master/detail.html index 0c0b74e..1d38217 100644 --- a/server/application/admin/view/master/detail.html +++ b/server/application/admin/view/master/detail.html @@ -175,6 +175,29 @@ + + +
+
+

已认证服务技能 ({:count($skills)})

+
+
+ {if !empty($skills)} +
+ {foreach name="skills" item="sk"} + + {$sk.cat_name|default='未归类技能'} + {if !empty($sk.cert_image)} + + {/if} + + {/foreach} +
+ {else} +
暂未认证任何服务技能
+ {/if} +
+
diff --git a/server/application/admin/view/master/skills.html b/server/application/admin/view/master/skills.html new file mode 100644 index 0000000..55fd467 --- /dev/null +++ b/server/application/admin/view/master/skills.html @@ -0,0 +1,58 @@ + + +
+
+
+ 当前正在为师傅 {$row.realname} ({$row.mobile}) 配置已认证服务技能。 +
+ + {foreach name="catTree" key="catName" item="items"} +
+
+ {$catName} + {:count($items)} 项技能 +
+
+ {if !empty($items)} + {foreach name="items" item="it"} + + {/foreach} + {else} + 该分类下暂无技能字典项 + {/if} +
+
+ {/foreach} + + +
+
+ 其他自定义技能 +
+
+ +
保存后将自动归入师傅名下并沉淀进全平台技能列表库中。
+
+
+
+ + +
diff --git a/server/application/admin/view/master_skill/add.html b/server/application/admin/view/master_skill/add.html new file mode 100644 index 0000000..3c3baac --- /dev/null +++ b/server/application/admin/view/master_skill/add.html @@ -0,0 +1,54 @@ +
+ +
+
+

师傅服务技能认证信息

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+ + +
+ +
+
    +
    +
    +
    + +
    +
    + {foreach name="statusList" item="vo"} + + {/foreach} +
    +
    +
    +
    +
    + + +
    diff --git a/server/application/admin/view/master_skill/edit.html b/server/application/admin/view/master_skill/edit.html new file mode 100644 index 0000000..50addb2 --- /dev/null +++ b/server/application/admin/view/master_skill/edit.html @@ -0,0 +1,54 @@ +
    + +
    +
    +

    修改师傅技能与资质

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + + +
    + +
    +
      +
      +
      +
      + +
      +
      + {foreach name="statusList" item="vo"} + + {/foreach} +
      +
      +
      +
      +
      + + +
      diff --git a/server/application/admin/view/master_skill/index.html b/server/application/admin/view/master_skill/index.html new file mode 100644 index 0000000..c8f7a9d --- /dev/null +++ b/server/application/admin/view/master_skill/index.html @@ -0,0 +1,23 @@ +
      + {:build_heading()} + +
      + +
      +
      diff --git a/server/application/admin/view/service/add.html b/server/application/admin/view/service/add.html index 4a51f57..a6d225b 100644 --- a/server/application/admin/view/service/add.html +++ b/server/application/admin/view/service/add.html @@ -1,154 +1,284 @@
      - -
      -
      -

      服务项目基础资料

      -
      -
      -
      - -
      - -
      -
      -
      - -
      - -
      -
      -
      - -
      -
      - -
      - - -
      - -
      -
        -
        -
        -
        - -
        - -
        -
        -
        -
        + + + +
        +
        + 勾选该商品所支持的具体施工工项并分别设定基准价。用户下单时可按需在这些工项间切换。 +
        + + + + + + + + + + + + ['name' => '安装', 'price' => 80.00, 'enabled' => true, 'is_default' => true], + 'repair' => ['name' => '维修', 'price' => 60.00, 'enabled' => true, 'is_default' => false], + 'remove' => ['name' => '拆旧', 'price' => 40.00, 'enabled' => true, 'is_default' => false], + 'deliver' => ['name' => '送货到家', 'price' => 50.00, 'enabled' => false, 'is_default' => false], + 'deliver_install' => ['name' => '送货到家并安装', 'price' => 120.00, 'enabled' => false, 'is_default' => false], + 'measure' => ['name' => '上门测量', 'price' => 30.00, 'enabled' => false, 'is_default' => false], + ]; + ?> + {foreach name="defaultTypes" item="st" key="code"} + + + + + + + {/foreach} - - + +
        启用工项标识前端显示名称一口基准工价(元)设为默认
        + + + {$code} + + + +
        + ¥ + +
        +
        + +
        +
        -
        - -
        -
        -
        -
        - 基础 ¥ - -
        -
        -
        -
        - 上限 ¥ - -
        -
        -
        -
        - 计价单位 - -
        -
        -
        -
        -
        -
        - -
        -
        -
        -
        - 单价 ¥ - - /层 -
        -
        -
        -
        - 免楼层数 - - 层起算 -
        -
        -
        -
        -
        -
        - -
        -
        - - -
        -
        -
        - - - -
        -
        -

        上架与运营权重

        -
        -
        -
        - -
        -
        -
        -
        - 初始销量 - -
        -
        -
        -
        - 排序权重 - -
        -
        -
        + +
        +
        + 为该商品增设不同规格(如:智能一体机 / 普通坐便器),并分别设置规格基准一口价。
        + + + + + + + + + + + +
        规格名称规格独立一口价(元)规格施工描述操作
        +
        -
        - -
        -
        - {foreach name="statusList" item="vo"} - - {/foreach} + + +
        +
        + 增值服务按类目精准隔离与绑定: + 系统根据上方所选的服务类目动态过滤可用增值服务。不同品类拥有独立的工法增项标准,支持在此单独设置专属单价。 +
        + +
        +
        + 视图筛选: +
        + + +
        +
        +
        + + +
        + + + + + + + + + + + + + + {foreach name="addonsPool" item="ad"} + + + + + + + + + + {/foreach} + +
        绑定所属品类增值服务名称单位平台指导价针对本商品专属单价(元)
        + + + {if $isCommon} + 全平台通用 + {else} + {$ad.cat_name} + {/if} + + {$ad.name} + + {$ad.unit} + + ¥{$ad.default_price} + +
        + ¥ + +
        +
        +
        @@ -156,8 +286,133 @@ + + diff --git a/server/application/admin/view/service/edit.html b/server/application/admin/view/service/edit.html index 66af075..d037f1b 100644 --- a/server/application/admin/view/service/edit.html +++ b/server/application/admin/view/service/edit.html @@ -241,18 +241,37 @@
        - +
        -
        - 从系统增项库中勾选该商品允许加选的增项服务(如马桶勾选拆旧/改坑距,油烟机勾选打孔),并可为该商品定制专属单价(留空则默认按全局价)。 +
        + 增值服务按类目精准隔离与绑定: + 系统已自动为您匹配该商品所属类目的专属增项池。不同品类拥有独立的工法与增项标准,支持为本商品单独设置专属覆写单价(留空则采用平台指导价)。
        - + + +
        +
        + 视图筛选: +
        + + +
        +
        +
        + + + +
        +
        + +
        - - - - + + + + + @@ -269,16 +288,24 @@ - + +
        绑定增项名称单位平台全局指导价绑定所属品类增值服务名称单位平台指导价 针对本商品专属单价(元)
        - + + + {if $isCommon} + 全平台通用 + {else} + {$ad.cat_name} + {/if} {$ad.name} - {$ad.unit} + {$ad.unit} ¥{$ad.default_price} @@ -351,6 +378,75 @@ $('#addons_raw').val(JSON.stringify(addons)); } + // 增值服务按所属类目筛选 + function applyAddonsFilter(onlyCategory) { + var currentCatId = parseInt($('#c-cat_id').val(), 10) || 0; + $('#addons-table tbody tr.addon-row').each(function () { + var rowCatId = parseInt($(this).data('cat-id'), 10) || 0; + if (onlyCategory) { + // 仅显示属于当前商品类目或者通用的增值服务 + if (rowCatId === 0 || rowCatId === currentCatId) { + $(this).show(); + } else { + $(this).hide(); + } + } else { + $(this).show(); + } + }); + } + + // 初始化时仅显示本类目与通用增项 + $(function () { + applyAddonsFilter(true); + }); + + // 切换筛选视图 + $(document).on('click', '#btn-filter-category', function () { + $('#btn-filter-group .btn').removeClass('active btn-primary').addClass('btn-default'); + $(this).removeClass('btn-default').addClass('active btn-primary'); + applyAddonsFilter(true); + }); + + $(document).on('click', '#btn-filter-all', function () { + $('#btn-filter-group .btn').removeClass('active btn-primary').addClass('btn-default'); + $(this).removeClass('btn-default').addClass('active btn-primary'); + applyAddonsFilter(false); + }); + + // 一键勾选本类目专属增项 + $(document).on('click', '#btn-check-cat-addons', function () { + var currentCatId = parseInt($('#c-cat_id').val(), 10) || 0; + $('#addons-table tbody tr.addon-row').each(function () { + var rowCatId = parseInt($(this).data('cat-id'), 10) || 0; + if (rowCatId > 0 && rowCatId === currentCatId) { + $(this).find('.ad-bind').prop('checked', true); + } + }); + }); + + // 一键勾选全平台通用增项 + $(document).on('click', '#btn-check-common-addons', function () { + $('#addons-table tbody tr.addon-row').each(function () { + var rowCatId = parseInt($(this).data('cat-id'), 10) || 0; + if (rowCatId === 0) { + $(this).find('.ad-bind').prop('checked', true); + } + }); + }); + + // 全部取消勾选 + $(document).on('click', '#btn-uncheck-all-addons', function () { + $('#addons-table tbody tr.addon-row .ad-bind').prop('checked', false); + }); + + // 当所属类目变更时动态联动过滤 + $(document).on('change', '#c-cat_id', function () { + if ($('#btn-filter-category').hasClass('active')) { + applyAddonsFilter(true); + } + }); + // 监听表单提交事件,在提交前组装 JSON $(document).on('submit', '#edit-form', function () { collectServiceConfig(); diff --git a/server/application/admin/view/skill_category/add.html b/server/application/admin/view/skill_category/add.html new file mode 100644 index 0000000..864d2ee --- /dev/null +++ b/server/application/admin/view/skill_category/add.html @@ -0,0 +1,54 @@ +
        + +
        +
        +

        技能分类定义

        +
        +
        +
        + +
        + +
        +
        +
        + +
        +
        + +
        + + +
        + +
        +
          +
          +
          +
          + +
          + +
          +
          +
          + +
          +
          + {foreach name="statusList" item="vo"} + + {/foreach} +
          +
          +
          +
          +
          + + +
          diff --git a/server/application/admin/view/skill_category/edit.html b/server/application/admin/view/skill_category/edit.html new file mode 100644 index 0000000..cfe10cf --- /dev/null +++ b/server/application/admin/view/skill_category/edit.html @@ -0,0 +1,54 @@ +
          + +
          +
          +

          修改技能分类

          +
          +
          +
          + +
          + +
          +
          +
          + +
          +
          + +
          + + +
          + +
          +
            +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            + {foreach name="statusList" item="vo"} + + {/foreach} +
            +
            +
            +
            +
            + + +
            diff --git a/server/application/admin/view/skill_category/index.html b/server/application/admin/view/skill_category/index.html new file mode 100644 index 0000000..16f1163 --- /dev/null +++ b/server/application/admin/view/skill_category/index.html @@ -0,0 +1,23 @@ +
            + {:build_heading()} + +
            + +
            +
            diff --git a/server/application/admin/view/skill_dict/add.html b/server/application/admin/view/skill_dict/add.html new file mode 100644 index 0000000..8f5df45 --- /dev/null +++ b/server/application/admin/view/skill_dict/add.html @@ -0,0 +1,56 @@ +
            + +
            +
            +

            技能信息定义

            +
            +
            +
            + +
            + {:build_select('row[category_id]', $categoryList, '', ['class'=>'form-control selectpicker', 'data-rule'=>'required'])} +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            + {foreach name="sourceList" item="vo"} + + {/foreach} +
            +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            + {foreach name="statusList" item="vo"} + + {/foreach} +
            +
            +
            +
            +
            + + +
            diff --git a/server/application/admin/view/skill_dict/edit.html b/server/application/admin/view/skill_dict/edit.html new file mode 100644 index 0000000..27abdfc --- /dev/null +++ b/server/application/admin/view/skill_dict/edit.html @@ -0,0 +1,59 @@ +
            + +
            +
            +

            修改技能标签

            +
            +
            +
            + +
            + {:build_select('row[category_id]', $categoryList, $row.category_id, ['class'=>'form-control selectpicker', 'data-rule'=>'required'])} +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            + {foreach name="sourceList" item="vo"} + + {/foreach} +
            + {if $row.source == 'custom'} + 该技能由师傅自定义输入提交,改为【系统预置】即可纳入平台标准公共字典供所有师傅勾选。 + {/if} +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            + {foreach name="statusList" item="vo"} + + {/foreach} +
            +
            +
            +
            +
            + + +
            diff --git a/server/application/admin/view/skill_dict/index.html b/server/application/admin/view/skill_dict/index.html new file mode 100644 index 0000000..23c95c1 --- /dev/null +++ b/server/application/admin/view/skill_dict/index.html @@ -0,0 +1,23 @@ +
            + {:build_heading()} + +
            + +
            +
            diff --git a/server/application/api/controller/Master.php b/server/application/api/controller/Master.php index 4578ae5..95bdbfd 100644 --- a/server/application/api/controller/Master.php +++ b/server/application/api/controller/Master.php @@ -92,11 +92,25 @@ class Master extends Base if (!$master) { $this->success('未入驻', ['applied' => false]); } - $skills = Db::name('master_skill')->alias('s') + $skillsRaw = Db::name('master_skill')->alias('s') ->join('service_cat c', 'c.id = s.cat_id', 'LEFT') - ->field('s.cat_id,c.name,c.icon') + ->field('s.cat_id,s.skill_id,s.skill_name,s.category,s.is_custom,c.name as cat_name,c.icon') ->where('s.master_id', $master['id']) ->select(); + $skills = []; + foreach ($skillsRaw as $sr) { + $name = !empty($sr['skill_name']) ? $sr['skill_name'] : ($sr['cat_name'] ?? ''); + if ($name) { + $skills[] = [ + 'cat_id' => (int)$sr['cat_id'], + 'skill_id' => (int)$sr['skill_id'], + 'name' => $name, + 'category' => $sr['category'] ?: '服务技能', + 'is_custom' => (int)$sr['is_custom'], + 'icon' => $sr['icon'] ?? '' + ]; + } + } $todo = Db::name('order')->where('master_id', $master['id']) ->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]) @@ -915,4 +929,160 @@ class Master extends Base $this->success('', $data); } + + /** + * 获取技能字典树与师傅当前选定状态 + * @ApiMethod (GET) + */ + public function skillDict() + { + $master = $this->masterInfo(); + $masterId = $master ? (int)$master['id'] : 0; + + // 1. 获取所有正常启用的字典项 + $dictList = Db::name('skill_dict') + ->where('status', 'normal') + ->order('weigh desc, id asc') + ->select(); + + // 2. 按大类分组 + $groups = []; + $categoryOrder = ['安装维修', '建筑装修', '物流运输', '汽车服务', '家政服务', '便民服务']; + foreach ($categoryOrder as $cat) { + $groups[$cat] = []; + } + + foreach ($dictList as $item) { + $cat = $item['category']; + if (!isset($groups[$cat])) { + $groups[$cat] = []; + } + $groups[$cat][] = [ + 'id' => (int)$item['id'], + 'name' => $item['name'], + 'category' => $item['category'], + 'source' => $item['source'], + ]; + } + + $tree = []; + foreach ($groups as $catName => $items) { + if (!empty($items)) { + $tree[] = [ + 'category' => $catName, + 'items' => $items + ]; + } + } + + // 3. 当前师傅已勾选的技能 + $selectedNames = []; + $customText = ''; + if ($masterId > 0) { + $mySkills = Db::name('master_skill')->where('master_id', $masterId)->select(); + $customArr = []; + foreach ($mySkills as $ms) { + $sName = !empty($ms['skill_name']) ? $ms['skill_name'] : ''; + if ($ms['is_custom'] == 1) { + $customArr[] = $sName; + } else if ($sName) { + $selectedNames[] = $sName; + } + } + $customText = implode(' ', $customArr); + } + + $this->success('ok', [ + 'tree' => $tree, + 'selected_skills' => $selectedNames, + 'custom_text' => $customText, + ]); + } + + /** + * 保存师傅服务技能与自定义输入技能 + * @ApiMethod (POST) + */ + public function saveSkills() + { + $master = $this->requireMaster(); + $selectedSkills = (array)$this->request->post('selected_skills/a', []); + $customText = trim((string)$this->request->post('custom_text', '')); + + // 1. 处理自定义输入的技能 + $customNames = []; + if (!empty($customText)) { + $parts = preg_split('/[,,\s]+/u', $customText); + foreach ($parts as $p) { + $p = trim($p); + if (!empty($p) && !in_array($p, $customNames) && !in_array($p, $selectedSkills)) { + $customNames[] = $p; + } + } + } + + if (empty($selectedSkills) && empty($customNames)) { + $this->error('请至少选择或输入一项服务技能'); + } + + // 2. 清理当前师傅原有的技能 + Db::name('master_skill')->where('master_id', $master['id'])->delete(); + + // 3. 写入字典勾选的技能 + $dictRows = Db::name('skill_dict')->where('name', 'in', $selectedSkills)->select(); + $dictMap = []; + foreach ($dictRows as $dr) { + $dictMap[$dr['name']] = $dr; + } + + $now = time(); + foreach ($selectedSkills as $sName) { + $dict = $dictMap[$sName] ?? null; + Db::name('master_skill')->insert([ + 'master_id' => $master['id'], + 'cat_id' => 0, + 'skill_id' => $dict ? $dict['id'] : 0, + 'skill_name' => $sName, + 'category' => $dict ? $dict['category'] : '服务技能', + 'is_custom' => 0, + 'cert_image' => '', + 'status' => 'normal', + 'createtime' => $now + ]); + } + + // 4. 处理自定义输入的技能(沉淀入库到 skill_dict 且归属当前师傅) + foreach ($customNames as $cName) { + $exist = Db::name('skill_dict')->where('name', $cName)->find(); + if (!$exist) { + $skillDictId = Db::name('skill_dict')->insertGetId([ + 'category' => '其他技能', + 'name' => $cName, + 'source' => 'custom', + 'master_id' => $master['id'], + 'weigh' => 0, + 'status' => 'normal', + 'createtime' => $now, + 'updatetime' => $now + ]); + } else { + $skillDictId = $exist['id']; + } + + Db::name('master_skill')->insert([ + 'master_id' => $master['id'], + 'cat_id' => 0, + 'skill_id' => $skillDictId, + 'skill_name' => $cName, + 'category' => '其他技能', + 'is_custom' => 1, + 'cert_image' => '', + 'status' => 'normal', + 'createtime' => $now + ]); + } + + $this->success('服务技能保存成功'); + } } + diff --git a/server/application/extra/site.php b/server/application/extra/site.php index 3d04c5b..5fafa00 100755 --- a/server/application/extra/site.php +++ b/server/application/extra/site.php @@ -4,7 +4,7 @@ return array ( 'name' => '优匠优服', 'beian' => '', 'cdnurl' => '', - 'version' => '1.0.18', + 'version' => '1.0.21', 'timezone' => 'Asia/Shanghai', 'forbiddenip' => '', 'languages' => diff --git a/yjyf-app/components/GoodsPickerDrawer.vue b/yjyf-app/components/GoodsPickerDrawer.vue index 09b6bc3..30dac2b 100644 --- a/yjyf-app/components/GoodsPickerDrawer.vue +++ b/yjyf-app/components/GoodsPickerDrawer.vue @@ -73,6 +73,12 @@ 暂无规格选项 + + + + 找不到商品? + 点这里下单 > + @@ -257,6 +263,27 @@ }); close(); } + + // 奇兵到家同款:找不到商品兜底通道(类目自动转为其它,商品转为手动输入) + function onCustomOrder() { + emit('custom', { + cat_id: 15, + cat_name: '其它', + service_id: 32, + item_name: '', + is_custom: true, + unit: '次', + price_type: 'quote', + service_types: [ + { code: 'install', name: '安装', price: 0, is_default: 1 }, + { code: 'repair', name: '维修', price: 0, is_default: 0 }, + { code: 'remove', name: '拆卸/拆旧', price: 0, is_default: 0 }, + { code: 'measure', name: '现场测量', price: 0, is_default: 0 }, + { code: 'clean', name: '深度清洗保养', price: 0, is_default: 0 } + ] + }); + close(); + } diff --git a/yjyf-app/pages/master/workbench.vue b/yjyf-app/pages/master/workbench.vue index 4a63704..dd7530e 100644 --- a/yjyf-app/pages/master/workbench.vue +++ b/yjyf-app/pages/master/workbench.vue @@ -99,13 +99,16 @@ - - 已认证服务类目 - {{ skills.length }} 项技能 + + 已认证服务技能 + + {{ skills.length }} 项技能 + 修改 › + - - {{ s.name }} - 暂无已认证类目 + + {{ s.name }} + 暂无已认证技能,点击去选择 @@ -144,6 +147,10 @@ uni.navigateTo({ url }); } + function goSkills() { + uni.navigateTo({ url: '/pages/master/skills' }); + } + function callService() { uni.makePhoneCall({ phoneNumber: '4000000000' }); } diff --git a/yjyf-app/pages/order/create.vue b/yjyf-app/pages/order/create.vue index c3f3d6c..3c5470a 100644 --- a/yjyf-app/pages/order/create.vue +++ b/yjyf-app/pages/order/create.vue @@ -69,11 +69,11 @@ - 增项服务 - + 增值服务 + {{ ad.name }} (+¥{{ ad.price }}) + 该商品暂无额外增值服务 本组单项预估 @@ -207,12 +208,13 @@ const submitting = ref(false); const autoQuote = ref(true); - const availableAddons = [ - { name: '拆旧服务', price: 30 }, - { name: '现场开孔', price: 20 }, - { name: '台面切割', price: 25 }, - { name: '墙体加固', price: 35 } - ]; + // 真实增值服务列表:从当前所选商品的真实 addons 动态派生(无 mock,按商品品类精准区分) + const availableAddons = computed(() => { + if (svc.value && svc.value.addons && Array.isArray(svc.value.addons) && svc.value.addons.length > 0) { + return svc.value.addons; + } + return []; + }); // 多商品组合容器 (对标奇兵到家 frame_010) const goodsGroups = ref([ @@ -221,7 +223,7 @@ function addGroup() { goodsGroups.value.push({ - goods_name: '', + goods_name: svc.value ? svc.value.name : '', goods_model: '', goods_num: 1, image: '', @@ -252,11 +254,11 @@ } function calcGroupPrice(item) { - const unitBase = svc.value && Number(svc.value.price) > 0 ? Number(svc.value.price) : 80; + const unitBase = svc.value && Number(svc.value.base_price) > 0 ? Number(svc.value.base_price) : 80; let addonSum = 0; for (const adName of item.addons) { - const hit = availableAddons.find(a => a.name === adName); - if (hit) addonSum += hit.price; + const hit = availableAddons.value.find(a => a.name === adName); + if (hit) addonSum += Number(hit.price) || 0; } return (unitBase * item.goods_num) + addonSum; } @@ -518,6 +520,7 @@ .group-tag { margin-left: 12rpx; padding: 2rpx 12rpx; border-radius: 8rpx; font-size: 20rpx; background: #FFF3E6; color: #F0710D; font-weight: 600; } .del-txt { font-size: 24rpx; color: #E8331B; } .addons-row { display: flex; flex-wrap: wrap; padding: 8rpx 0; } + .addon-empty-hint { font-size: 24rpx; color: #999; padding: 12rpx 0; } .addon-pill { padding: 6rpx 18rpx; margin-right: 12rpx; margin-bottom: 10rpx; border-radius: 20rpx; background: #F5F5F7; border: 1.5rpx solid transparent; font-size: 22rpx; color: #555; diff --git a/yjyf-app/pages/order/quick.vue b/yjyf-app/pages/order/quick.vue index d9248fe..4ddfc8a 100644 --- a/yjyf-app/pages/order/quick.vue +++ b/yjyf-app/pages/order/quick.vue @@ -51,9 +51,12 @@ - + + + + {{ currentModeName }} + 下单送券·慢必赔 - {{ currentModeName }} 重新选择下单模式 @@ -150,8 +153,24 @@ - - + + + + * + 输入商品 + + + + {{ (g.custom_name || '').length }}/20 + + + * 选择商品 @@ -534,19 +553,25 @@ - - + + 已使用优惠券,共减 ¥{{ (amount && amount.discount_amount) || 0 }}(原价 ¥{{ (amount && amount.base_amount) || 0 }}) - - + + + + 共{{ goodsList.length }}组商品 + + + + 共{{ goodsList.length }}组商品合计 券 ¥{{ amount ? amount.pay_amount : '0.00' }} - - + + + 继续添加商品 @@ -591,7 +616,7 @@ :cat-name="drawerCatName" @update:visible="showGoodsDrawer = $event" @select="handleGoodsDrawerSelect" - @custom="showGoodsDrawer = false" + @custom="handleGoodsDrawerCustom" /> @@ -730,8 +755,8 @@ const showModeDrawer = ref(false); const currentModeName = computed(() => { - if (priceType.value === 'quote') return '师傅报价下单'; - if (priceType.value === 'custom') return '自己定价下单'; + if (priceType.value === 'quote') return '报价下单'; + if (priceType.value === 'custom') return '自主定价'; return '一口价下单'; }); @@ -745,6 +770,10 @@ const drawerCatName = ref(''); const currentCatIconUrl = ref(''); + const isOtherCategory = computed(() => { + return Number(drawerCatId.value) === 15 || drawerCatName.value === '其它' || drawerCatName.value === '更多'; + }); + function goToSelectCategory() { uni.navigateTo({ url: '/pages/category/category' }); } @@ -869,6 +898,84 @@ scheduleCalc(); } + // 奇兵到家同款:找不到商品去下单兜底(自填商品) + function handleGoodsDrawerCustom() { + showGoodsDrawer.value = false; + drawerCatId.value = 15; + drawerCatName.value = '其它'; + currentCatIconUrl.value = '/uploads/icons/cat-more.svg'; + priceType.value = 'quote'; // 报价下单 + + const customGroup = { + service_id: 32, + item_name: '', + custom_name: '', + is_custom: true, + spec_name: '', + cat_id: 15, + cat_name: '其它', + unit: '次', + base_price: 0, + num: 1, + service_type: 'install', + service_type_name: '安装', + service_types: [ + { code: 'install', name: '安装', price: 0, is_default: 1 }, + { code: 'repair', name: '维修', price: 0, is_default: 0 }, + { code: 'remove', name: '拆卸/拆旧', price: 0, is_default: 0 }, + { code: 'measure', name: '现场测量', price: 0, is_default: 0 }, + { code: 'clean', name: '深度清洗保养', price: 0, is_default: 0 } + ], + remark: '', + images: [], + videos: [], + envImages: [], + extras: [] + }; + + if (isEditingGroup.value) { + goodsList.value[editingGroupIndex.value] = { ...goodsList.value[editingGroupIndex.value], ...customGroup }; + } else { + goodsList.value.push(customGroup); + } + scheduleCalc(); + } + + function handleAddMoreGoods() { + if (isOtherCategory.value) { + // 在其它类目下,继续添加商品直接增加一组自填商品 + goodsList.value.push({ + service_id: 32, + item_name: '', + custom_name: '', + is_custom: true, + spec_name: '', + cat_id: 15, + cat_name: '其它', + unit: '次', + base_price: 0, + num: 1, + service_type: 'install', + service_type_name: '安装', + service_types: [ + { code: 'install', name: '安装', price: 0, is_default: 1 }, + { code: 'repair', name: '维修', price: 0, is_default: 0 }, + { code: 'remove', name: '拆卸/拆旧', price: 0, is_default: 0 }, + { code: 'measure', name: '现场测量', price: 0, is_default: 0 }, + { code: 'clean', name: '深度清洗保养', price: 0, is_default: 0 } + ], + remark: '', + images: [], + videos: [], + envImages: [], + extras: [] + }); + scheduleCalc(); + } else { + openGoodsDrawer(false); + } + } + // 2. 服务项目选择抽屉 const showServiceTypeDrawer = ref(false); const currentEditingServiceTypes = ref([]); @@ -1247,7 +1354,14 @@ // 分步导航验证 function goStep2() { for (let i = 0; i < goodsList.value.length; i++) { - if (!goodsList.value[i].item_name) { + const g = goodsList.value[i]; + if (g.is_custom || isOtherCategory.value) { + if (!g.custom_name || !g.custom_name.trim()) { + uni.showToast({ title: `请输入第 ${i + 1} 组商品名称`, icon: 'none' }); + return; + } + g.item_name = g.custom_name.trim(); + } else if (!g.item_name) { uni.showToast({ title: `第 ${i + 1} 组尚未选择商品`, icon: 'none' }); return; } @@ -1344,6 +1458,7 @@ let targetCatId = options && options.cat_id ? Number(options.cat_id) : 0; let targetCatName = options && options.cat_name ? decodeURIComponent(options.cat_name) : ''; if (options && options.mode === 'custom') priceType.value = 'custom'; + if (options && (options.mode === 'quote' || options.custom == 1)) priceType.value = 'quote'; try { const resCats = await api.cats(); @@ -1360,6 +1475,47 @@ matchedCat = cats[0]; } + // 奇兵到家同款:找不到商品进入自填兜底模式(类目:其它,模式:报价下单) + if (options?.custom == 1 || targetCatId === 15 || targetCatName === '其它') { + matchedCat = cats.find(c => c.id === 15 || c.name === '其它') || { id: 15, name: '其它', icon_url: '/uploads/icons/cat-more.svg' }; + drawerCatId.value = matchedCat.id; + drawerCatName.value = '其它'; + currentCatIconUrl.value = matchedCat.icon_url || '/uploads/icons/cat-more.svg'; + priceType.value = 'quote'; + + goodsList.value = [ + { + service_id: 32, + item_name: '', + custom_name: '', + is_custom: true, + spec_name: '', + cat_id: 15, + cat_name: '其它', + unit: '次', + base_price: 0, + num: 1, + service_type: 'install', + service_type_name: '安装', + service_types: [ + { code: 'install', name: '安装', price: 0, is_default: 1 }, + { code: 'repair', name: '维修', price: 0, is_default: 0 }, + { code: 'remove', name: '拆卸/拆旧', price: 0, is_default: 0 }, + { code: 'measure', name: '现场测量', price: 0, is_default: 0 }, + { code: 'clean', name: '深度清洗保养', price: 0, is_default: 0 } + ], + remark: '', + images: [], + videos: [], + envImages: [], + extras: [] + } + ]; + scheduleCalc(); + fetchMasterCount(); + return; + } + drawerCatId.value = matchedCat.id; drawerCatName.value = matchedCat.name; currentCatIconUrl.value = matchedCat.icon_url || ''; @@ -1590,6 +1746,23 @@ color: #1F2937; } + .tsc-mode-info { + display: flex; + align-items: center; + gap: 12rpx; + } + + .tsc-benefit-tag { + background: #FFF2E8; + border: 1rpx solid #FFA940; + color: #D4380D; + font-size: 20rpx; + font-weight: 600; + padding: 2rpx 12rpx; + border-radius: 6rpx; + line-height: 28rpx; + } + .tsc-btn { padding: 10rpx 24rpx; background: #F9FAFB; @@ -1930,6 +2103,27 @@ text-align: right; } + .fr-input-box { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12rpx; + } + + .fr-custom-input { + flex: 1; + font-size: 28rpx; + color: #1F2937; + text-align: right; + } + + .fr-char-count { + font-size: 24rpx; + color: #9CA3AF; + flex-shrink: 0; + } + .fr-input.right-align { text-align: right; } @@ -2550,6 +2744,39 @@ padding: 20rpx 24rpx; } + .step1-group-count-bar { + display: flex; + justify-content: flex-end; + padding: 16rpx 28rpx 0; + } + + .sgc-txt { + font-size: 24rpx; + color: #4B5563; + font-weight: 500; + } + + .quote-action-row { + padding-top: 12rpx; + } + + .quote-btns-full { + width: 100% !important; + display: flex; + align-items: center; + gap: 20rpx; + } + + .quote-btns-full .btn-add-more { + flex: 1; + border-radius: 12rpx; + } + + .quote-btns-full .btn-step-next { + flex: 1; + border-radius: 12rpx; + } + .step1-summary { display: flex; align-items: baseline; diff --git a/yjyf-app/static/icons/mode-hammer.svg b/yjyf-app/static/icons/mode-hammer.svg new file mode 100644 index 0000000..96b5953 --- /dev/null +++ b/yjyf-app/static/icons/mode-hammer.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/yjyf-app/utils/api.js b/yjyf-app/utils/api.js index 3a101ba..ab6664e 100644 --- a/yjyf-app/utils/api.js +++ b/yjyf-app/utils/api.js @@ -64,6 +64,8 @@ export default { masterQuotes: () => get('/master/quotes'), masterQuote: (data) => post('/master/quote', data), masterWithdraw: (data) => post('/master/withdraw', data), + masterSkillDict: () => get('/master/skillDict'), + masterSaveSkills: (data) => post('/master/saveSkills', data), /* ---------- 评价 ---------- */ reviewCreate: (data) => post('/review/create', data),