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