lizhi 3 месяцев назад
Родитель
Сommit
eb31f0a1c5
44 измененных файлов с 2937 добавлено и 388 удалено
  1. 2 0
      protected/components/DB.php
  2. 214 0
      protected/controllers/CompanyController.php
  3. 139 0
      protected/controllers/CompanyRelationController.php
  4. 2 1
      protected/controllers/FollowController.php
  5. 15 0
      protected/controllers/SchoolController.php
  6. 33 0
      protected/controllers/SiteController.php
  7. 6 0
      protected/controllers/UseradminController.php
  8. 7 0
      protected/include/DbCriteria.php
  9. 24 29
      protected/include/LewaimaiAdminPingtaiAuth.php
  10. 8 7
      script/upgrade/1.0.0.sql
  11. 2 2
      web/src/api/canteenApi.ts
  12. 64 0
      web/src/api/companyApi.ts
  13. 64 0
      web/src/api/companyRelationApi.ts
  14. 2 2
      web/src/api/roleApi.ts
  15. 2 2
      web/src/api/schoolApi.ts
  16. 2 2
      web/src/api/schoolRelationApi.ts
  17. 8 0
      web/src/api/usersApi.ts
  18. 1 2
      web/src/components/custom/FollowDrawer.vue
  19. 9 0
      web/src/locales/langs/en.json
  20. 9 0
      web/src/locales/langs/zh.json
  21. 371 285
      web/src/router/routes/asyncRoutes.ts
  22. 6 0
      web/src/router/routesAlias.ts
  23. 0 1
      web/src/types/router/index.ts
  24. 71 0
      web/src/typings/api.d.ts
  25. 23 0
      web/src/typings/form.d.ts
  26. 3 2
      web/src/views/canteen/edit.vue
  27. 4 3
      web/src/views/canteen/follow/index.vue
  28. 2 2
      web/src/views/canteen/follow/info.vue
  29. 7 10
      web/src/views/canteen/list/index.vue
  30. 0 1
      web/src/views/canteen/list/user-search.vue
  31. 191 0
      web/src/views/company/edit.vue
  32. 215 0
      web/src/views/company/follow/index.vue
  33. 87 0
      web/src/views/company/follow/info.vue
  34. 87 0
      web/src/views/company/follow/modules/user-search.vue
  35. 123 0
      web/src/views/company/info.vue
  36. 406 0
      web/src/views/company/list/index.vue
  37. 92 0
      web/src/views/company/list/user-search.vue
  38. 336 0
      web/src/views/company/relation/index.vue
  39. 157 0
      web/src/views/company/relation/modules/user-dialog.vue
  40. 77 0
      web/src/views/company/relation/modules/user-search.vue
  41. 3 2
      web/src/views/school/follow/index.vue
  42. 55 30
      web/src/views/school/list/index.vue
  43. 6 3
      web/src/views/school/relation/index.vue
  44. 2 2
      web/src/views/system/role/index.vue

+ 2 - 0
protected/components/DB.php

@@ -19,6 +19,7 @@ class DB
         if (!$header) {
             $header = array_keys($data[0]);
         }
+        $table = self::formTableName($table);
         $headerStr = implode(',', $header);
         $sql = "INSERT INTO {$table} ({$headerStr}) values ";
         foreach ($data as $k => $datum) {
@@ -359,6 +360,7 @@ class DB
      */
     public static function deleteByCondition($tableName, $filters, $dbName = 'db')
     {
+        $tableName = self::formTableName($tableName);
         if (empty($filters)) {
             return false;
         }

+ 214 - 0
protected/controllers/CompanyController.php

@@ -0,0 +1,214 @@
+<?php
+
+class CompanyController extends Controller
+{
+    public static string $table = 'company';
+    public function actionInfo()
+    {
+        $id = Helper::getPostInt('id');
+        if ($id <= 0) {
+            Helper::error('参数错误');
+        }
+        $data = DB::getInfoById(self::$table, $id);
+        if (!$data) {
+            Helper::error('数据不存在');
+        }
+        $cri = DbCriteria::simpleCompare(['id' => [$data['last_user_id'], $data['bind_user_id']]])->setSelect('id, username');
+        $users = Helper::arrayColumn(DB::getListWithCriteria('useradmin', $cri), 'username', 'id');
+        $data['last_user_name'] = $users[$data['last_user_id']] ?? '';
+        $data['bind_user_name'] = $users[$data['bind_user_id']] ?? '';
+        $data['distinct'] = [
+            $data['province'],
+            $data['city'],
+            $data['area'],
+        ];
+
+        // 关联食堂
+        $data['canteen_names'] = [];
+        $data['canteens'] = [];
+        $cri = DbCriteria::simpleCompare(['t.company_id' => $id])
+            ->setAlias('t')
+            ->setSelect('t.school_id, t.canteen_id, s.name as school_name, c.name as canteen_name')
+            ->setJoin('left join wx_school s on t.school_id = s.id')
+            ->addJoin('left join wx_canteen c on t.canteen_id = c.id');
+        $canteens = DB::getListWithCriteria('company_canteen_relation', $cri);
+        foreach ($canteens['records'] as $item) {
+            $data['canteens'][] = [(int)$item['school_id'], (int)$item['canteen_id']];
+            $data['canteen_names'][] = "{$item['canteen_name']}({$item['school_name']})";
+        }
+
+        // 关系人
+        $relations = DB::getListWithCriteria(
+            'company_contact',
+            DbCriteria::simpleCompare(['company_id' => $id])->setSelect('id, name, phone, position, weixin'),
+        );
+        $data['relations'] = $relations['records'];
+
+        Helper::ok($data);
+    }
+
+    public function actionList()
+    {
+        $filter = [
+            'is_del' => 0,
+            'phone' => Helper::getPostString('phone')
+        ];
+        if ($name = Helper::getPostString('name')) {
+            $filter['name'] = '%' . $name;
+        }
+        $address = Helper::getArrParam($_POST, 'address', Helper::PARAM_KEY_TYPE['array_string']);
+        $filter['province'] = $address[0]?? null;
+        $filter['city'] = $address[1]?? null;
+        $filter['area'] = $address[2]?? null;
+        if ($school_id = Helper::getPostInt('school_id')) {
+            $cri = DbCriteria::simpleCompare(['school_id' => $school_id])->setSelect('company_id');
+            $ids = Helper::arrayColumn(
+                DB::getListWithCriteria('company_canteen_relation', $cri),
+                'company_id'
+            );
+            $filter['id'] = $ids?: -1;
+        }
+        $cri = DbCriteria::simpleCompareWithPage($filter);
+        $data = DB::getListWithCriteria(self::$table, $cri);
+        if (!empty($data['records'])) {
+            $users = Helper::arrayColumn(
+                DB::getListWithCriteria('useradmin', DbCriteria::simpleCompare([])->setSelect('id, username')),
+                'username',
+                'id'
+            );
+            $data['records'] = array_map(function ($item) use ($users) {
+                $item['last_user_name'] = $users[$item['last_user_id']] ?? '-';
+                $item['bind_user_name'] = $users[$item['bind_user_id']] ?? '-';
+                return $item;
+            }, $data['records']);
+        }
+        Helper::ok($data);
+    }
+
+    /**
+     * 下拉列表获取
+     * @return void
+     */
+    public function actionGetSelectList()
+    {
+        $cri = DbCriteria::simpleCompare([])->setSelect('id, name');
+        $data = DB::getListWithCriteria(self::$table, $cri);
+        Helper::ok($data['records']??[]);
+    }
+
+    public function actionDelete()
+    {
+        $id = Helper::getPostInt('id');
+        if ($id < 1) {
+            Helper::error('参数错误');
+        }
+        Db::updateById(self::$table, ['is_del' => 1], $id);
+        Helper::ok();
+    }
+
+    public function actionAdd()
+    {
+        $this->_save();
+    }
+
+    public function actionEdit()
+    {
+        $id = Helper::getPostInt('id');
+        if (!$id) {
+            Helper::error('参数错误');
+        }
+        $this->_save($id);
+    }
+
+    private function _save($id = 0)
+    {
+        $data = [
+            'name' => Helper::getPostString('name'),
+            'address' => Helper::getPostString('address'),
+            'memo' => Helper::getPostString('memo'),
+        ];
+
+        // 空字段检测
+        if (!Helper::checkEmptyKey($data, ['name','address', 'memo'], ['memo'])) {
+            Helper::error('参数错误');
+        }
+
+        // 处理地区
+        $district = Helper::getArrParam($_POST, 'distinct', Helper::PARAM_KEY_TYPE['array_string']);
+        $district = array_filter($district);
+        if (count($district) != 3) {
+            Helper::error('地区参数错误');
+        }
+        $data['province'] = $district[0];
+        $data['city'] = $district[1];
+        $data['area'] = $district[2];
+
+        // 关联食堂
+        $canteens = $_POST['canteens']?? [];
+        if (!$canteens) {
+            Helper::error('请选择关联的食堂');
+        }
+        foreach ($canteens as $k => $canteen) {
+            $canteens[$k] = array_filter(explode(',', $canteen));
+            if (count($canteens[$k]) != 2 || empty($canteens[$k][0]) || empty($canteens[$k][1])) {
+                Helper::error('选择的食堂参数有误 ' . json_encode($canteens));
+            }
+        }
+        $name = $data['name'];
+        // 检测名称重复
+        $cri = DbCriteria::simpleCompare(['name' => $name])->setSelect('id');
+        if ($id > 0) {
+            $cri->addCondition('id!=' . $id);
+        }
+        if ($fid = DB::getScalerWithCriteria(self::$table, $cri)) {
+            Helper::error('公司名称已存在 ' . $fid);
+        }
+
+        $trans = \Yii::app()->db->beginTransaction();
+        try {
+            if ($id) {
+                DB::updateById(self::$table, $data, $id);
+                DB::deleteByCondition('company_canteen_relation', ['company_id' => $id]);
+            } else {
+                $id = DB::addData(self::$table, $data);
+                if (!$id) {
+                    throw new \Exception('添加失败');
+                }
+            }
+            $batchArr = [];
+            foreach ($canteens as $canteen) {
+                $batchArr[] = [
+                    'company_id' => $id,
+                    'school_id' => $canteen[0],
+                    'canteen_id' => $canteen[1],
+                ];
+            }
+            DB::safeBatchInsert('company_canteen_relation', $batchArr);
+            $trans->commit();
+        } catch (\Exception $e) {
+            $trans->rollback();
+            Helper::error($e->getMessage());
+        }
+        Helper::ok();
+    }
+
+    public function actionUpdateAttr()
+    {
+        $id = Helper::getPostInt('id');
+        $attr = Helper::getPostString('attr');
+        $value = Helper::getPostString('value');
+        if ($id <= 0 || !$attr) {
+            Helper::error('参数错误');
+        }
+        if (!in_array($attr, ['is_eleme_in_school', 'person_num'])) {
+            Helper::error('参数错误2');
+        }
+        if ($attr == 'is_eleme_in_school' && !in_array($value, [1, 0])) {
+            Helper::error('参数错误3');
+        }
+        if (DB::updateById(self::$table, [$attr => $value], $id) === false) {
+            Helper::error('更新失败');
+        }
+        Helper::ok();
+    }
+}

+ 139 - 0
protected/controllers/CompanyRelationController.php

@@ -0,0 +1,139 @@
+<?php
+
+class CompanyRelationController extends Controller
+{
+    public string $table = 'company_contact';
+
+    public function actionInfo()
+    {
+        $id = Helper::getPostInt('id');
+        if ($id <= 0) {
+            Helper::error('参数错误');
+        }
+        $data = DB::getInfoById($this->table, $id);
+        if (!$data) {
+            Helper::error('数据不存在');
+        }
+        Helper::ok($data);
+    }
+
+    public function actionList()
+    {
+        $filter = [
+            'r.is_del'    => 0,
+            'r.phone'     => Helper::getPostString('phone'),
+            'r.company_id' => Helper::getPostInt('company_id') ?: null,
+        ];
+        if ($name = Helper::getPostString('name')) {
+            $filter['r.name'] = '%'.$name;
+        }
+        $cri = DbCriteria::simpleCompareWithPage($filter)
+            ->setAlias('r')
+            ->setSelect('r.*, s.name as company_name')
+            ->setJoin('LEFT JOIN wx_company s ON s.id=r.company_id');
+        $data = DB::getListWithCriteria($this->table, $cri);
+        if (!empty($data['records'])) {
+            $data['records'] = array_map(function ($item) {
+                return $item;
+            }, $data['records']);
+        }
+        Helper::ok($data);
+    }
+
+    /**
+     * 下拉列表获取
+     * @return void
+     */
+    public function actionGetSelectList()
+    {
+        $cri = DbCriteria::simpleCompare(['is_del' => 0])->setSelect('id, name');
+        $companys = Helper::arrayColumn(DB::getListWithCriteria('company', $cri), null, 'id');
+        if (empty($companys)) {
+            Helper::ok();
+        }
+        $cri1 = DbCriteria::simpleCompare(['is_del' => 0])->setSelect('id, name, company_id');
+        $relations = DB::getListWithCriteria($this->table, $cri1);
+        foreach ($relations['records'] as $relation) {
+            $sid = $relation['company_id'];
+            if (!isset($companys[$sid])) {
+                continue;
+            }
+            if (!isset($companys[$sid]['children'])) {
+                $companys[$sid]['children'] = [];
+            }
+            $companys[$sid]['children'][] = [
+                'id'   => $relation['id'],
+                'name' => $relation['name'],
+            ];
+        }
+        Helper::ok(array_values($companys));
+    }
+
+    public function actionDelete()
+    {
+        $id = Helper::getPostInt('id');
+        if ($id < 1) {
+            Helper::error('参数错误');
+        }
+        Db::updateById($this->table, ['is_del' => 1], $id);
+        Helper::ok();
+    }
+
+    public function actionAdd()
+    {
+        $this->_save();
+    }
+
+    public function actionEdit()
+    {
+        $id = Helper::getPostInt('id');
+        if (!$id) {
+            Helper::error('参数错误');
+        }
+        $this->_save($id);
+    }
+
+    private function _save($id = 0)
+    {
+        $data = [
+            'name'      => Helper::getPostString('name'),
+            'company_id' => Helper::getPostInt('company_id'),
+            'phone'     => Helper::getPostString('phone'),
+            'weixin'    => Helper::getPostString('weixin'),
+            'position'  => Helper::getPostString('position'),
+            'memo'      => Helper::getPostString('memo'),
+        ];
+
+        $notNullField = ["name", "company_id", "phone", "weixin", "position"];
+        $allowEmptyField = [];
+
+        // 空字段检测
+        if (!Helper::checkEmptyKey($data, $notNullField, $allowEmptyField)) {
+            Helper::error('参数错误');
+        }
+
+        if ($id) {
+            DB::updateById($this->table, $data, $id);
+        } else {
+            DB::addData($this->table, $data);
+        }
+        Helper::ok();
+    }
+
+    public function actionUpdateAttr()
+    {
+        $id = Helper::getPostInt('id');
+        $attr = Helper::getPostString('attr');
+        $value = Helper::getPostString('value');
+        if ($id <= 0 || !$attr) {
+            Helper::error('参数错误');
+        }
+        if (!in_array($attr, [])) {
+            Helper::error('参数错误2');
+        }
+        if (DB::updateById($this->table, [$attr => $value], $id) === false) {
+            Helper::error('更新失败');
+        }
+        Helper::ok();
+    }
+}

+ 2 - 1
protected/controllers/FollowController.php

@@ -71,7 +71,8 @@ class FollowController extends Controller
                 DB::updateById('school', $upInfo, $firstId);
                 DB::updateById('school_contact', $upInfo, $secondId);
             } elseif ($this->type == 'canteen') {
-                DB::updateById('canteen', $upInfo, $firstId);
+                DB::updateById('school', $upInfo, $firstId);
+                DB::updateById('canteen', $upInfo, $secondId);
             } elseif ($this->type == 'company') {
                 DB::updateById('company', $upInfo, $firstId);
                 DB::updateById('company_contact', $upInfo, $secondId);

+ 15 - 0
protected/controllers/SchoolController.php

@@ -18,6 +18,21 @@ class SchoolController extends Controller
             $data['city'],
             $data['area'],
         ];
+
+        // 关系人
+        $relations = DB::getListWithCriteria(
+            'school_contact',
+            DbCriteria::simpleCompare(['school_id' => $id])->setSelect('id, name, phone, position, weixin'),
+        );
+        $data['relations'] = $relations['records'];
+
+        // 关联食堂
+        $canteens = DB::getListWithCriteria(
+            'canteen',
+            DbCriteria::simpleCompare(['school_id' => $id])->setSelect('id, name, username, weixin, phone'),
+        );
+        $data['canteens'] = $canteens['records'];
+
         Helper::ok($data);
     }
 

+ 33 - 0
protected/controllers/SiteController.php

@@ -103,4 +103,37 @@ class SiteController extends Controller
     {
         echo (new DBTable(Helper::getGetString('t1')))->getDetailHtml();
     }
+
+    public function actionDefault()
+    {
+        $data = 'id: number
+    name: string // 名称
+    distinct: string[] // 地区,
+    canteens: number[] // 地区,
+    province: string // 省
+    city: string // 市
+    area: string // 区
+    address: string // 详细地址
+    bind_user_id?: number // 负责人
+    memo: string // 备注';
+        $data = explode("\n", $data);
+        $data = array_map(function ($item) {
+            $item = trim($item);
+            $item = explode(':', $item);
+            $name = trim($item[0], '? ');
+            $type = trim(explode('//', $item[1])[0]);
+            $value = "''";
+            switch ($type) {
+                case '0|1':
+                case 'number':
+                    $value = 0;
+                    break;
+                case 'string[]':
+                case 'number[]':
+                    $value = '[]';
+                    break;
+            }
+            echo "{$name}: {$value}, <br/>";
+        }, $data);
+    }
 }

+ 6 - 0
protected/controllers/UseradminController.php

@@ -91,6 +91,12 @@ class UseradminController extends Controller
         Helper::ok($data);
     }
 
+    public function actionGetSelectList()
+    {
+       $data = DB::getListWithCriteria('useradmin', DbCriteria::simpleCompare(['id' => '!=1', 'status' => 1])->setSelect('id, username as name'));
+       Helper::ok($data['records']);
+    }
+
     public function actionSaveRoleAuth()
     {
         $id = Helper::getPostInt('id');

+ 7 - 0
protected/include/DbCriteria.php

@@ -163,6 +163,13 @@ class DbCriteria extends \CDbCriteria
         return $this;
     }
 
+    public function addJoin($join): static
+    {
+        $this->join.= ' ' . $join;
+
+        return $this;
+    }
+
     public function setAlias($alias): static
     {
         $this->alias = $alias;

+ 24 - 29
protected/include/LewaimaiAdminPingtaiAuth.php

@@ -91,35 +91,30 @@ class LewaimaiAdminPingtaiAuth
             'school/updateattr' => 120102,
             'school/delete' => 120103,
 
-            // ===================   学校关系  =======================
-            'schoolrelation/list' => 120200,
-            'schoolrelation/getselectlist' => 120200,
-            'schoolrelation/info' => 120200,
-            'schoolrelation/add' => 120201,
-            'schoolrelation/edit' => 120202,
-            'schoolrelation/updateattr' => 120202,
-            'schoolrelation/delete' => 120203,
-
-            // ===================   学校跟进 =======================
-            'follow/schoollist' => 120300,
-            'follow/schoolall' => 120300,
-            'follow/schoolinfo' => 120300,
-            'follow/schooladd' => 120301,
-
-            // ===================   食堂  =======================
-            'canteen/list' => 130100,
-            'canteen/getselectlist' => 130100,
-            'canteen/info' => 130100,
-            'canteen/add' => 130101,
-            'canteen/edit' => 130102,
-            'canteen/updateattr' => 130102,
-            'canteen/delete' => 130103,
-
-            // ===================   食堂跟进 =======================
-            'follow/canteenlist' => 130300,
-            'follow/canteenall' => 130300,
-            'follow/canteeninfo' => 130300,
-            'follow/canteenadd' => 130301,
+            // ===================   餐饮公司  =======================
+            'company/list' => 120100,
+            'company/getselectlist' => 120100,
+            'company/info' => 120100,
+            'company/add' => 120101,
+            'company/edit' => 120102,
+            'company/updateattr' => 120102,
+            'company/delete' => 120103,
+
+            // ===================   餐饮公司关系  =======================
+            'companyrelation/list' => 120200,
+            'companyrelation/getselectlist' => 120200,
+            'companyrelation/info' => 120200,
+            'companyrelation/add' => 120201,
+            'companyrelation/edit' => 120202,
+            'companyrelation/updateattr' => 120202,
+            'companyrelation/delete' => 120203,
+
+            // ===================   餐饮公司跟进 =======================
+            'follow/companylist' => 120300,
+            'follow/companyall' => 120300,
+            'follow/companyinfo' => 120300,
+            'follow/companyadd' => 120301,
+
 
         ];
 

+ 8 - 7
script/upgrade/1.0.0.sql

@@ -116,8 +116,7 @@ CREATE TABLE `wx_canteen_follow` (
   `detail` text COMMENT '详情',
   `create_date` datetime NOT NULL DEFAULT now() COMMENT '创建时间',
   PRIMARY KEY (`id`),
-  key `idx_school` (`school_id`) using btree,
-  key `idx_canteen` (`canteen_id`) using btree,
+  key `idx_school_canteen` (`school_id`, `canteen_id`) using btree,
   key `idx_user` (`user_id`) using btree
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci comment='学校食堂跟进表';
 
@@ -160,22 +159,24 @@ CREATE TABLE `wx_company_contact` (
 CREATE TABLE `wx_company_follow` (
   `id` INT(11) NOT NULL AUTO_INCREMENT,
   `company_id` int(11) NOT NULL default 0 COMMENT '餐饮公司ID',
-  `company_contact_id` int(11) NOT NULL default 0 COMMENT '餐饮公司联系人ID',
+  `contact_id` int(11) NOT NULL default 0 COMMENT '餐饮公司联系人ID',
   `user_id` int(11) NOT NULL default 0 COMMENT '跟进人ID',
   `chat_imgs` text COMMENT '聊天图片',
   `detail` text COMMENT '详情',
   `create_date` datetime NOT NULL DEFAULT now() COMMENT '创建时间',
   PRIMARY KEY (`id`),
-  key `idx_company` (`company_id`) using btree,
+  key `idx_company_contact` (`company_id`, `contact_id`) using btree,
   key `idx_user` (`user_id`) using btree
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci comment='餐饮公司跟进表';
 
 --------------------------------------- 餐饮公司和食堂关联表
-CREATE TABLE `wx_company_canteen_ relation` (
+CREATE TABLE `wx_company_canteen_relation` (
   `id` INT(11) NOT NULL AUTO_INCREMENT,
   `company_id` int(11) NOT NULL default 0 COMMENT '餐饮公司ID',
+  `school_id` int(11) NOT NULL default 0 COMMENT '学校ID',
   `canteen_id` int(11) NOT NULL default 0 COMMENT '食堂ID',
   PRIMARY KEY (`id`),
-  key `idx_school` (`company_id`) using btree,
-  key `idx_canteen` (`canteen_id`) using btree
+  key `idx_company` (`company_id`) using btree,
+  key `idx_canteen` (`canteen_id`) using btree,
+  key `idx_school` (`school_id`) using btree
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci comment='餐饮公司和食堂关联表';

+ 2 - 2
web/src/api/canteenApi.ts

@@ -33,8 +33,8 @@ export class canteenApi {
   // 下拉列表
   static selectList() {
     return request.post<Api.Common.SelectRelationInfo[]>({
-      url: 'canteen/getSelectList'
-      // showErrorMessage: false // 不显示错误消息
+      url: 'canteen/getSelectList',
+      showErrorMessage: false // 不显示错误消息
     })
   }
 

+ 64 - 0
web/src/api/companyApi.ts

@@ -0,0 +1,64 @@
+import request from '@/utils/http'
+
+export class companyApi {
+  // 列表
+  static list(params: Api.Common.PaginatingSearchParams) {
+    return request.post<Api.Company.ListData>({
+      url: 'company/list',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 详情
+  static info(id: number) {
+    return request.post<Api.Company.Info>({
+      url: 'company/info',
+      params: {id:id}
+      // 自定义请求头
+      // headers: {
+      //   'X-Custom-Header': 'your-custom-value'
+      // }
+    })
+  }
+
+  // 新增
+  static add(params: Form.Company) {
+    return request.post<any>({
+      url: 'company/add',
+      params
+    })
+  }
+
+  // 下拉列表
+  static selectList() {
+    return request.post<Api.Common.SelectInfo[]>({
+      url: 'company/getSelectList',
+      showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 编辑属性
+  static updateAttr(params: Form.UpdateAttr) {
+    return request.post<any>({
+      url: 'company/updateAttr',
+      params
+    })
+  }
+
+  // 编辑
+  static edit(params: Form.Company) {
+    return request.post<any>({
+      url: 'company/edit',
+      params
+    })
+  }
+
+  // 删除
+  static delete(params: Api.Common.DeleteParams) {
+    return request.post<any>({
+      url: 'company/delete',
+      params
+    })
+  }
+}

+ 64 - 0
web/src/api/companyRelationApi.ts

@@ -0,0 +1,64 @@
+import request from '@/utils/http'
+
+export class companyRelationApi {
+  // 列表
+  static list(params: Api.Common.PaginatingSearchParams) {
+    return request.post<Api.School.SchoolContactListData>({
+      url: 'companyRelation/list',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 下拉列表
+  static selectList() {
+    return request.post<Api.Common.SelectRelationInfo[]>({
+      url: 'companyRelation/getSelectList',
+      showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 详情
+  static info(id: number) {
+    return request.post<Api.School.SchoolContactItem>({
+      url: 'companyRelation/info',
+      params: {id:id}
+      // 自定义请求头
+      // headers: {
+      //   'X-Custom-Header': 'your-custom-value'
+      // }
+    })
+  }
+
+  // 新增
+  static add(params: Form.SchoolContact) {
+    return request.post<any>({
+      url: 'companyRelation/add',
+      params
+    })
+  }
+
+  // 编辑属性
+  static updateAttr(params: Form.UpdateAttr) {
+    return request.post<any>({
+      url: 'companyRelation/updateAttr',
+      params
+    })
+  }
+
+  // 编辑
+  static edit(params: Form.SchoolContact) {
+    return request.post<any>({
+      url: 'companyRelation/edit',
+      params
+    })
+  }
+
+  // 删除
+  static delete(params: Api.Common.DeleteParams) {
+    return request.post<any>({
+      url: 'companyRelation/delete',
+      params
+    })
+  }
+}

+ 2 - 2
web/src/api/roleApi.ts

@@ -13,8 +13,8 @@ export class roleService {
   // 角色下拉列表
   static roleSelectList() {
     return request.post<Api.Common.SelectInfo[]>({
-      url: 'useradmin/getRoleSelect'
-      // showErrorMessage: false // 不显示错误消息
+      url: 'useradmin/getRoleSelect',
+      showErrorMessage: false // 不显示错误消息
     })
   }
 

+ 2 - 2
web/src/api/schoolApi.ts

@@ -33,8 +33,8 @@ export class schoolApi {
   // 下拉列表
   static selectList() {
     return request.post<Api.Common.SelectInfo[]>({
-      url: 'school/getSelectList'
-      // showErrorMessage: false // 不显示错误消息
+      url: 'school/getSelectList',
+      showErrorMessage: false // 不显示错误消息
     })
   }
 

+ 2 - 2
web/src/api/schoolRelationApi.ts

@@ -13,8 +13,8 @@ export class schoolRelationApi {
   // 下拉列表
   static selectList() {
     return request.post<Api.Common.SelectRelationInfo[]>({
-      url: 'schoolRelation/getSelectList'
-      // showErrorMessage: false // 不显示错误消息
+      url: 'schoolRelation/getSelectList',
+      showErrorMessage: false // 不显示错误消息
     })
   }
 

+ 8 - 0
web/src/api/usersApi.ts

@@ -30,6 +30,14 @@ export class UserService {
     })
   }
 
+  // 下拉列表
+  static selectList() {
+    return request.post<Api.Common.SelectInfo[]>({
+      url: 'useradmin/getSelectList',
+      showErrorMessage: false // 不显示错误消息
+    })
+  }
+
   // 编辑用户
   static editUser(params: Api.User.UserInfo) {
     return request.post<any>({

+ 1 - 2
web/src/components/custom/FollowDrawer.vue

@@ -36,8 +36,7 @@
               <label>详情:</label>
             </el-col>
             <el-col :sm="22">
-<!--              <ElCard shadow="never" v-html="follow.detail" style="padding: 10px;max-height: 200px;overflow-y: scroll"/>-->
-              <div v-html="follow.detail" style="padding: 10px;max-height: 200px;overflow-y: scroll"/>
+              <ElCard shadow="never" v-html="follow.detail" style="padding: 10px;max-height: 200px;overflow-y: scroll"/>
             </el-col>
           </el-row>
         </el-col>

+ 9 - 0
web/src/locales/langs/en.json

@@ -259,6 +259,15 @@
       "followInfo": "canteen follow info",
       "follow": "canteen follow list"
     },
+    "company": {
+      "list": "company list",
+      "relation": "company relation",
+      "follow": "follow list",
+      "followInfo": "follow info",
+      "info": "company info",
+      "add": "add company",
+      "edit": "edit company"
+    },
     "safeguard": {
       "title": "Safeguard",
       "server": "Server"

+ 9 - 0
web/src/locales/langs/zh.json

@@ -251,6 +251,15 @@
       "followInfo": "食堂跟进详情",
       "follow": "食堂跟进记录"
     },
+    "company": {
+      "list": "餐饮公司信息",
+      "relation": "餐饮公司方关系",
+      "follow": "餐饮公司跟进记录",
+      "followInfo": "餐饮公司跟进详情",
+      "info": "餐饮公司详情",
+      "add": "新建餐饮公司",
+      "edit": "编辑餐饮公司"
+    },
     "safeguard": {
       "title": "运维管理",
       "server": "服务器管理"

+ 371 - 285
web/src/router/routes/asyncRoutes.ts

@@ -1,6 +1,6 @@
-import { RoutesAlias } from '../routesAlias'
-import { AppRouteRecord } from '@/types/router'
-import { WEB_LINKS } from '@/utils/constants'
+import {RoutesAlias} from '../routesAlias'
+import {AppRouteRecord} from '@/types/router'
+import {WEB_LINKS} from '@/utils/constants'
 
 /**
  * 菜单列表、异步路由
@@ -18,329 +18,415 @@ import { WEB_LINKS } from '@/utils/constants'
  * 3、id没有设置或为0表示不需要权限,id 和 LewaimaiAdminPingtaiAuth.php 保持一致
  */
 export const asyncRoutes: AppRouteRecord[] = [
-  // 主页一级菜单配置示例:
-  {
-    name: 'Home',
-    path: '/dashboard/console',
-    component: RoutesAlias.Dashboard,
-    meta: {
-      title: 'menus.dashboard.console',
-      icon: '&#xe733;',
-      keepAlive: false,
-      fixedTab:true,
-    }
-  },
-  {
-    id: 12,
-    path: '/school',
-    name: 'SchoolManage',
-    component: RoutesAlias.Layout,
-    meta: {
-      title: 'menus.school.list',
-      icon: '&#xe7b9;'
+    // 主页一级菜单配置示例:
+    {
+        name: 'Home',
+        path: '/dashboard/console',
+        component: RoutesAlias.Dashboard,
+        meta: {
+            title: 'menus.dashboard.console',
+            icon: '&#xe733;',
+            keepAlive: false,
+            fixedTab: true,
+        }
     },
-    children: [
-      {
-        id: 1201,
-        path: 'list',
-        name: 'school',
-        component: RoutesAlias.SchoolList,
+    {
+        id: 12,
+        path: '/school',
+        name: 'SchoolManage',
+        component: RoutesAlias.Layout,
         meta: {
-          title: 'menus.school.list',
-          keepAlive: false,
-          authList: [
-            {
-              id: 120100,
-              title: '列表',
-              authMark: 'list'
-            },
-            {
-              id: 120101,
-              title: '新增',
-              authMark: 'add'
-            },
+            title: 'menus.school.list',
+            icon: '&#xe7b9;'
+        },
+        children: [
             {
-              id: 120102,
-              title: '编辑',
-              authMark: 'edit'
+                id: 1201,
+                path: 'list',
+                name: 'school',
+                component: RoutesAlias.SchoolList,
+                meta: {
+                    title: 'menus.school.list',
+                    keepAlive: false,
+                    authList: [
+                        {
+                            id: 120100,
+                            title: '列表',
+                        },
+                        {
+                            id: 120101,
+                            title: '新增',
+                        },
+                        {
+                            id: 120102,
+                            title: '编辑',
+                        }
+                    ]
+                }
             },
             {
-              id: 120103,
-              title: '删除',
-              authMark: 'delete'
+                path: 'edit',
+                name: 'SchoolEdit',
+                component: RoutesAlias.SchoolEdit,
+                meta: {
+                    title: 'menus.school.edit',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/school/list' // 激活菜单路径
+                }
             },
-          ]
-        }
-      },
-      {
-        path: 'edit',
-        name: 'SchoolEdit',
-        component: RoutesAlias.SchoolEdit,
-        meta: {
-          title: 'menus.school.edit',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/school/list' // 激活菜单路径
-        }
-      },
-      {
-        path: 'info',
-        name: 'SchoolInfo',
-        component: RoutesAlias.SchoolInfo,
-        meta: {
-          title: 'menus.school.info',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/school/list' // 激活菜单路径
-        }
-      },
-      {
-        id: 1202,
-        path: 'relation',
-        name: 'relation',
-        component: RoutesAlias.SchoolRelation,
-        meta: {
-          title: 'menus.school.relation',
-          keepAlive: true,
-          authList: [
             {
-              id: 120200,
-              title: '列表',
-              authMark: 'list'
+                path: 'info',
+                name: 'SchoolInfo',
+                component: RoutesAlias.SchoolInfo,
+                meta: {
+                    title: 'menus.school.info',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/school/list' // 激活菜单路径
+                }
             },
             {
-              id: 120201,
-              title: '新增',
-              authMark: 'add'
+                id: 1202,
+                path: 'relation',
+                name: 'schoolRelation',
+                component: RoutesAlias.SchoolRelation,
+                meta: {
+                    title: 'menus.school.relation',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 120200,
+                            title: '列表',
+                        },
+                        {
+                            id: 120201,
+                            title: '新增',
+                        },
+                        {
+                            id: 120202,
+                            title: '编辑',
+                        },
+                        {
+                            id: 120203,
+                            title: '删除',
+                        },
+                    ]
+                }
             },
             {
-              id: 120202,
-              title: '编辑',
-              authMark: 'edit'
+                id: 1203,
+                path: 'follow',
+                name: 'schoolFollow',
+                component: RoutesAlias.SchoolFollow,
+                meta: {
+                    title: 'menus.school.follow',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 120300,
+                            title: '列表',
+                        },
+                        {
+                            id: 120302,
+                            title: '编辑',
+                        }
+                    ]
+                }
             },
             {
-              id: 120203,
-              title: '删除',
-              authMark: 'delete'
+                path: 'follow/info',
+                name: 'SchoolFollowInfo',
+                component: RoutesAlias.SchoolFollowInfo,
+                meta: {
+                    title: 'menus.school.followInfo',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/school/list' // 激活菜单路径
+                }
             },
-          ]
-        }
-      },
-      {
-        id: 1203,
-        path: 'follow',
-        name: 'schoolFollow',
-        component: RoutesAlias.SchoolFollow,
+        ]
+    },
+    {
+        id: 13,
+        path: '/canteen',
+        name: 'CanteenManage',
+        component: RoutesAlias.Layout,
         meta: {
-          title: 'menus.school.follow',
-          keepAlive: true,
-          authList: [
+            title: 'menus.canteen.list',
+            icon: '&#xe7b9;'
+        },
+        children: [
             {
-              id: 120300,
-              title: '列表',
-              authMark: 'list'
+                id: 1301,
+                path: 'list',
+                name: 'canteen',
+                component: RoutesAlias.CanteenList,
+                meta: {
+                    title: 'menus.canteen.list',
+                    keepAlive: false,
+                    authList: [
+                        {
+                            id: 130100,
+                            title: '列表',
+                        },
+                        {
+                            id: 130101,
+                            title: '新增',
+                        },
+                        {
+                            id: 130102,
+                            title: '编辑',
+                        }
+                    ]
+                }
             },
             {
-              id: 120302,
-              title: '编辑',
-              authMark: 'edit'
-            }
-          ]
-        }
-      },
-      {
-        path: 'follow/info',
-        name: 'SchoolFollowInfo',
-        component: RoutesAlias.SchoolFollowInfo,
-        meta: {
-          title: 'menus.school.followInfo',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/school/list' // 激活菜单路径
-        }
-      },
-    ]
-  },
-  {
-    id: 13,
-    path: '/canteen',
-    name: 'CanteenManage',
-    component: RoutesAlias.Layout,
-    meta: {
-      title: 'menus.canteen.list',
-      icon: '&#xe7b9;'
-    },
-    children: [
-      {
-        id: 1301,
-        path: 'list',
-        name: 'canteen',
-        component: RoutesAlias.CanteenList,
-        meta: {
-          title: 'menus.canteen.list',
-          keepAlive: false,
-          authList: [
-            {
-              id: 130100,
-              title: '列表',
-              authMark: 'list'
+                path: 'edit',
+                name: 'canteenEdit',
+                component: RoutesAlias.CanteenEdit,
+                meta: {
+                    title: 'menus.canteen.edit',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/canteen/list' // 激活菜单路径
+                }
             },
             {
-              id: 130101,
-              title: '新增',
-              authMark: 'add'
+                path: 'info',
+                name: 'canteenInfo',
+                component: RoutesAlias.CanteenInfo,
+                meta: {
+                    title: 'menus.canteen.info',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/canteen/list' // 激活菜单路径
+                },
             },
             {
-              id: 130102,
-              title: '编辑',
-              authMark: 'edit'
+                id: 1303,
+                path: 'follow',
+                name: 'canteenFollow',
+                component: RoutesAlias.CanteenFollow,
+                meta: {
+                    title: 'menus.canteen.follow',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 130300,
+                            title: '列表',
+                        },
+                        {
+                            id: 130301,
+                            title: '编辑',
+                        }
+                    ]
+                }
             },
             {
-              id: 120103,
-              title: '删除',
-              authMark: 'delete'
+                path: 'follow/info',
+                name: 'canteenFollowInfo',
+                component: RoutesAlias.CanteenFollowInfo,
+                meta: {
+                    title: 'menus.canteen.followInfo',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/canteen/follow' // 激活菜单路径
+                }
             },
-          ]
-        }
-      },
-      {
-        path: 'edit',
-        name: 'canteenEdit',
-        component: RoutesAlias.CanteenEdit,
-        meta: {
-          title: 'menus.canteen.edit',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/canteen/list' // 激活菜单路径
-        }
-      },
-      {
-        path: 'info',
-        name: 'canteenInfo',
-        component: RoutesAlias.CanteenInfo,
+        ]
+    },
+    {
+        id: 14,
+        path: '/company',
+        name: 'CompanyManage',
+        component: RoutesAlias.Layout,
         meta: {
-          title: 'menus.canteen.info',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/canteen/list' // 激活菜单路径
+            title: 'menus.company.list',
+            icon: '&#xe7b9;'
         },
-      },
-      {
-        id: 1303,
-        path: 'follow',
-        name: 'canteenFollow',
-        component: RoutesAlias.CanteenFollow,
-        meta: {
-          title: 'menus.canteen.follow',
-          keepAlive: true,
-          authList: [
+        children: [
             {
-              id: 130300,
-              title: '列表',
-              authMark: 'list'
+                id: 1401,
+                path: 'list',
+                name: 'company',
+                component: RoutesAlias.CompanyList,
+                meta: {
+                    title: 'menus.company.list',
+                    keepAlive: false,
+                    authList: [
+                        {
+                            id: 140100,
+                            title: '列表',
+                        },
+                        {
+                            id: 140101,
+                            title: '新增',
+                        },
+                        {
+                            id: 140102,
+                            title: '编辑',
+                        },
+                    ]
+                }
             },
             {
-              id: 130301,
-              title: '编辑',
-              authMark: 'edit'
-            }
-          ]
-        }
-      },
-      {
-        path: 'follow/info',
-        name: 'canteenFollowInfo',
-        component: RoutesAlias.CanteenFollowInfo,
-        meta: {
-          title: 'menus.canteen.followInfo',
-          isHide: true,
-          keepAlive: true,
-          activePath: '/canteen/follow' // 激活菜单路径
-        }
-      },
-    ]
-  },
-  {
-    id: 11,
-    path: '/system',
-    name: 'System',
-    component: RoutesAlias.Layout,
-    meta: {
-      title: 'menus.system.title',
-      icon: '&#xe7b9;'
-    },
-    children: [
-      {
-        id: 1101,
-        path: 'user',
-        name: 'User',
-        component: RoutesAlias.User,
-        meta: {
-          title: 'menus.system.user',
-          keepAlive: true,
-          authList: [
+                path: 'edit',
+                name: 'CompanyEdit',
+                component: RoutesAlias.CompanyEdit,
+                meta: {
+                    title: 'menus.company.edit',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/company/list' // 激活菜单路径
+                }
+            },
             {
-              id: 110100,
-              title: '列表',
-              authMark: 'list'
+                path: 'info',
+                name: 'CompanyInfo',
+                component: RoutesAlias.CompanyInfo,
+                meta: {
+                    title: 'menus.company.info',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/company/list' // 激活菜单路径
+                }
             },
             {
-              id: 110101,
-              title: '编辑',
-              authMark: 'edit'
+                id: 1402,
+                path: 'relation',
+                name: 'companyRelation',
+                component: RoutesAlias.CompanyRelation,
+                meta: {
+                    title: 'menus.company.relation',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 140200,
+                            title: '列表',
+                        },
+                        {
+                            id: 140201,
+                            title: '新增',
+                        },
+                        {
+                            id: 140202,
+                            title: '编辑',
+                        },
+                        {
+                            id: 140203,
+                            title: '删除',
+                        },
+                    ]
+                }
             },
             {
-              id: 110102,
-              title: '删除',
-              authMark: 'delete'
-            }
-
-          ]
-        }
-      },
-      {
-        path: 'user-center',
-        name: 'userCenter',
-        component: RoutesAlias.UserCenter,
-        meta: {
-          title: 'menus.system.userCenter',
-          isHide: true,
-          keepAlive: false,
-          activePath: '/system/user' // 激活菜单路径
-        }
-      },
-      {
-        id: 1102,
-        path: 'role',
-        name: 'Role',
-        component: RoutesAlias.Role,
-        meta: {
-          title: 'menus.system.role',
-          keepAlive: true,
-          authList: [
+                id: 1403,
+                path: 'follow',
+                name: 'companyFollow',
+                component: RoutesAlias.CompanyFollow,
+                meta: {
+                    title: 'menus.company.follow',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 140300,
+                            title: '列表',
+                        },
+                        {
+                            id: 140302,
+                            title: '编辑',
+                        }
+                    ]
+                }
+            },
             {
-              id: 110200,
-              title: '列表',
-              authMark: 'list'
+                path: 'follow/info',
+                name: 'CompanyFollowInfo',
+                component: RoutesAlias.CompanyFollowInfo,
+                meta: {
+                    title: 'menus.company.followInfo',
+                    isHide: true,
+                    keepAlive: true,
+                    activePath: '/company/list' // 激活菜单路径
+                }
             },
+        ]
+    },
+    {
+        id: 11,
+        path: '/system',
+        name: 'System',
+        component: RoutesAlias.Layout,
+        meta: {
+            title: 'menus.system.title',
+            icon: '&#xe7b9;'
+        },
+        children: [
             {
-              id: 110201,
-              title: '分配权限',
-              authMark: 'add'
+                id: 1101,
+                path: 'user',
+                name: 'User',
+                component: RoutesAlias.User,
+                meta: {
+                    title: 'menus.system.user',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 110100,
+                            title: '列表',
+                        },
+                        {
+                            id: 110101,
+                            title: '编辑',
+                        },
+                        {
+                            id: 110102,
+                            title: '删除',
+                        }
+
+                    ]
+                }
             },
             {
-              id: 110202,
-              title: '编辑',
-              authMark: 'edit'
+                path: 'user-center',
+                name: 'userCenter',
+                component: RoutesAlias.UserCenter,
+                meta: {
+                    title: 'menus.system.userCenter',
+                    isHide: true,
+                    keepAlive: false,
+                    activePath: '/system/user' // 激活菜单路径
+                }
             },
             {
-              id: 110203,
-              title: '删除',
-              authMark: 'delete'
+                id: 1102,
+                path: 'role',
+                name: 'Role',
+                component: RoutesAlias.Role,
+                meta: {
+                    title: 'menus.system.role',
+                    keepAlive: true,
+                    authList: [
+                        {
+                            id: 110200,
+                            title: '列表',
+                        },
+                        {
+                            id: 110201,
+                            title: '分配权限',
+                        },
+                        {
+                            id: 110202,
+                            title: '编辑',
+                        },
+                        {
+                            id: 110203,
+                            title: '删除',
+                        }
+                    ]
+                }
             }
-          ]
-        }
-      }
-    ]
-  },
+        ]
+    },
 ]

+ 6 - 0
web/src/router/routesAlias.ts

@@ -24,4 +24,10 @@ export enum RoutesAlias {
   CanteenInfo = '/canteen/info', // 食堂详情
   CanteenFollow = '/canteen/follow', // 食堂跟进
   CanteenFollowInfo = '/canteen/follow/info', // 食堂跟进详情
+  CompanyList = '/company/list', // 餐饮公司列表
+  CompanyInfo = '/company/info', // 餐饮公司详情
+  CompanyEdit = '/company/edit', // 编辑餐饮公司
+  CompanyRelation = '/company/relation', // 餐饮公司关系
+  CompanyFollow = '/company/follow', // 餐饮公司跟进
+  CompanyFollowInfo = '/company/follow/info', // 餐饮公司跟进详情
 }

+ 0 - 1
web/src/types/router/index.ts

@@ -28,7 +28,6 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
   authList?: Array<{
     id?: number
     title: string
-    authMark: string
   }>
   /** 是否为一级菜单 */
   isFirstLevel?: boolean

+ 71 - 0
web/src/typings/api.d.ts

@@ -261,6 +261,77 @@ declare namespace Api {
     }
   }
 
+  namespace Company {
+
+    interface relationInfo {
+      id: number
+      name: string // 联系人名称
+      phone: string // 手机号
+      weixin: string // 微信号
+      position: string // 职位
+    }
+
+    interface contactItem {
+      id: number
+      name: string // 名称,
+      company_id: number // 学校ID,
+      company_name: string // 学校,
+      phone: string // 手机号,
+      weixin: string // 微信号,
+      position: string // 职位,
+      memo: string // 备注,
+      create_date?: string // 创建时间,
+      update_date?: string // 更新时间,
+    }
+
+    interface contactListData {
+      records: contactItem[]
+      current: number
+      size: number
+      total: number
+    }
+
+    interface Info {
+      id: number
+      name: string // 名称
+      province: string // 省
+      city: string // 市
+      area: string // 区
+      address: string // 详细地址
+      bind_user_id: number // 负责人
+      memo: string // 备注
+      last_user_id: number // 最后一次跟进人
+      last_date: string // 最后一次跟进时间
+      last_user_name: string // 最后一次跟进人
+      bind_user_name: string // 最后一次跟进人
+      canteens: int[][]
+      canteen_names: string[]
+      relations: relationInfo[]
+    }
+
+    interface ListItem {
+      id: number
+      name: string // 名称
+      province: string // 省
+      city: string // 市
+      area: string // 区
+      address: string // 详细地址
+      bind_user_id: number // 负责人
+      memo: string // 备注
+      last_user_id: number // 最后一次跟进人
+      last_date: string // 最后一次跟进时间
+      last_user_name: string // 最后一次跟进人
+      bind_user_name: string // 最后一次跟进人
+    }
+
+    interface ListData {
+      records: ListItem[]
+      current: number
+      size: number
+      total: number
+    }
+  }
+
   namespace Follow {
 
     interface FollowInfo {

+ 23 - 0
web/src/typings/form.d.ts

@@ -88,4 +88,27 @@ declare namespace Form {
     weixin: string // 微信号
     memo: string // 备注
   }
+
+  interface Company {
+    id: number
+    name: string // 名称
+    distinct: string[] // 地区,
+    canteens: number[] // 地区,
+    province: string // 省
+    city: string // 市
+    area: string // 区
+    address: string // 详细地址
+    bind_user_id?: number // 负责人
+    memo: string // 备注
+  }
+
+  interface CompanyContact {
+    id?: number
+    name: string // 名称,
+    company_id: number // 学校ID,
+    phone: string // 手机号,
+    weixin: string // 微信号,
+    position: string // 职位,
+    memo: string // 备注,
+  }
 }

+ 3 - 2
web/src/views/canteen/edit.vue

@@ -155,8 +155,9 @@
 
   const selectList = ref<Api.Common.SelectRelationInfo[]>([])
   const getSelectList = async () => {
-    const data = await canteenApi.selectList()
-    selectList.value = data
+    await canteenApi.selectList().then(res => {
+      selectList.value = res
+    })
   }
 
   onMounted(() => {

+ 4 - 3
web/src/views/canteen/follow/index.vue

@@ -68,13 +68,14 @@
   const searchForm = ref({
     name: '',
     phone: '',
-    school_id: parseInt(<string>useRoute().query.school_id)  || '',
+    company_id: parseInt(<string>useRoute().query.company_id)  || '',
   })
 
   const selectList = ref<Api.Common.SelectRelationInfo[]>([])
   const getSelectList = async () => {
-    const data = await canteenApi.selectList()
-    selectList.value = data
+    await canteenApi.selectList().then(res => {
+      selectList.value = res
+    })
   }
   getSelectList()
 

+ 2 - 2
web/src/views/canteen/follow/info.vue

@@ -7,7 +7,7 @@
       </el-col>
 
       <el-col :span="24">
-        <label>学校(校区):</label> <span>{{ info.first_name }}</span>
+        <label>餐饮公司:</label> <span>{{ info.first_name }}</span>
       </el-col>
 
       <el-col :span="24">
@@ -65,7 +65,7 @@ const DefaultData = <Api.Follow.FollowInfo>{
 }
 const info = reactive<Api.Follow.FollowInfo>({ ...DefaultData })
 onMounted(() => {
-  followApi.canteenInfo(parseInt(useRoute().query.id as string)).then((res) => {
+  followApi.companyInfo(parseInt(useRoute().query.id as string)).then((res) => {
     Object.assign(info, res)
     console.log(`%c info == `, 'background:#41b883 ; padding:1px; color:#fff', info);
   })

+ 7 - 10
web/src/views/canteen/list/index.vue

@@ -153,8 +153,8 @@ const followDialogVisible = ref(false)
 // 搜索表单
 const searchForm = ref({
   name: '',
-  is_cooperate: -1,
-  address: []
+  school_id: parseInt(<string>useRoute().query.school_id)  || '',
+  company_id: parseInt(<string>useRoute().query.company_id)  || '',
 })
 
 const defaultValue = <Api.Canteen.ListItem>{
@@ -180,8 +180,9 @@ const currentRow = ref<Api.Canteen.ListItem>({...defaultValue})
 
 const selectList = ref<Api.Common.SelectRelationInfo[]>([])
 const getSelectList = async () => {
-  const data = await canteenApi.selectList()
-  selectList.value = data
+  await canteenApi.selectList().then(res => {
+    selectList.value = res
+  })
 }
 getSelectList()
 
@@ -232,7 +233,7 @@ const {
       { prop:'school_name', label:'学校' },
       { prop:'stall_num', label:'档口数量' },
       { prop:'is_direct', label:'是否直营', formatter: (row) => {
-        return h(ElTag, { type: row.is_direct ? 'success' : 'danger' }, () => row.is_direct ? '是' : '不是')
+        return h(ElTag, { type: row.is_direct ? 'success' : 'danger' }, () => row.is_direct ? '直营' : '非直营')
       } },
       { prop:'stall_imgs', label:'档口照片', formatter: (row) => {
         if (row.stall_imgs.length > 0) {
@@ -259,6 +260,7 @@ const {
       {
         prop: '', label: '跟进记录', formatter: (row) => {
           return h(ElButton, {
+            size: 'small',
             type: 'primary',
             onClick: () => follow(row),
           }, () => '跟进')
@@ -279,11 +281,6 @@ const {
                 type: 'edit',
                 onClick: () => edit(row.id)
               }),
-              useUserStore().checkAuth(130203) &&
-              h(ArtButtonTable, {
-                type: 'delete',
-                onClick: () => deleteUser(row.id)
-              })
             ])
       }
     ]

+ 0 - 1
web/src/views/canteen/list/user-search.vue

@@ -12,7 +12,6 @@
 
 <script setup lang="ts">
   import { ref, computed, onMounted, h } from 'vue'
-  import { cityJson } from '@/router/utils/city'
 
   interface Props {
     modelValue: Record<string, any>

+ 191 - 0
web/src/views/company/edit.vue

@@ -0,0 +1,191 @@
+<template>
+  <ElForm ref="formRef" :model="formData" :rules="rules" label-width="auto">
+    <el-row :gutter="20">
+      <el-col :span="24">
+        <ElFormItem label="名称" prop="name">
+          <ElInput v-model="formData.name" maxlength="20" type="text" style="max-width: 300px"/>
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label="地区" prop="distinct" style="max-width: 600px">
+          <el-cascader
+              v-model="formData.distinct"
+              :options="cityJson"
+              filterable
+              placeholder="可搜索"
+              clearable
+              style="width: 100%"
+          />
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label="详细地址" prop="address">
+          <ElInput v-model="formData.address" maxlength="255" type="textarea"/>
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label="负责人" prop="bind_user_id">
+          <ElSelect v-model="formData.bind_user_id" :empty-values="[0]" :value-on-clear="0" placeholder="请选择" style="max-width: 200px">
+            <ElOption v-for="item in users" :key="item.id" :value="item.id" :label="item.name"></ElOption>
+          </ElSelect>
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label="负责学校(校区)食堂" prop="canteens">
+          <el-cascader
+              v-model="formData.canteens"
+              :options="selectList.map(item => ({
+                  value: item.id,
+                  label: item.name,
+                  children: item.children.map((item1:any) => ({
+                    value: item1.id,
+                    label: item1.name
+                  }))
+                }))"
+              filterable
+              placeholder="可搜索"
+              clearable
+              :props="casProps"
+              style="width: 100%"
+          />
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label="备注" prop="memo">
+          <ElInput v-model="formData.memo" maxlength="255" type="textarea" :rows="4" />
+        </ElFormItem>
+      </el-col>
+
+      <el-col :span="24">
+        <ElFormItem label=" " prop="">
+          <ElButton type="primary" @click="handleSubmit">提交</ElButton>
+        </ElFormItem>
+      </el-col>
+
+    </el-row>
+  </ElForm>
+</template>
+
+<script setup lang="ts">
+  import type { FormInstance, FormRules } from 'element-plus'
+  import { companyApi as Api } from '@/api/companyApi'
+  import { onMounted } from 'vue'
+  import { router } from '@/router'
+  import { cityJson } from '@/router/utils/city'
+  import { ElMessageBox } from 'element-plus'
+  import { RoutesAlias } from '@/router/routesAlias'
+  import {canteenApi} from "@/api/canteenApi";
+  import {UserService} from "@/api/usersApi";
+
+  const casProps = { multiple: true }
+
+  // 表单实例
+  const formRef = ref<FormInstance>()
+
+  const DefaultData = <Form.Company>{
+    id: 0,
+    name: '',
+    distinct: [],
+    canteens: [],
+    province: '',
+    city: '',
+    area: '',
+    address: '',
+    bind_user_id: 0,
+    memo: '',
+  }
+  // 表单数据
+  const formData = reactive<Form.Company>({...DefaultData})
+
+  // 表单验证规则
+  const rules: FormRules = {
+    name: [
+      { required: true, message: '请输入名称', trigger: 'blur' },
+      { max: 20, message: '长度最多20个字符', trigger: 'blur' }
+    ],
+    distinct: [
+      { required: true, message: '请选择地区', trigger: 'blur' },
+    ],
+    address: [
+      { required: true, message: '请输入详细地址', trigger: 'blur' },
+      { max: 255, message: '长度最多255个字符', trigger: 'blur' }
+    ],
+    bind_user_id: [{ required: true, message: '请选择负责人', trigger: 'blur' }],
+    memo: [{ max: 255, message: '长度最多255个字符', trigger: 'blur' }]
+  }
+
+  const route = useRoute()
+  let id = 0
+
+  // 初始化表单数据
+  const initFormData = () => {
+    if (id == 0) {
+      Object.assign(formData, DefaultData)
+    } else {
+      Api.info(id).then((res) => {
+        Object.assign(formData, res)
+      })
+    }
+  }
+
+  const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+  const getSelectList = async () => {
+    await canteenApi.selectList().then(res => {
+      selectList.value = res
+    })
+  }
+
+  const users = ref<Api.Common.SelectInfo[]>([])
+  const getUserList = async () => {
+    const data = await UserService.selectList()
+    users.value = data
+  }
+
+  onMounted(() => {
+    id = route.query.id ? parseInt(route.query.id as string) : 0
+    initFormData()
+    getSelectList()
+    getUserList()
+    console.log(`%c users == `, 'background:#41b883 ; padding:1px; color:#fff', users);
+    nextTick(() => {
+      formRef.value?.clearValidate()
+    })
+  })
+
+  const afterOk = () => {
+    ElMessageBox.confirm('操作成功,是否返回列表', 'Success', {
+      confirmButtonText: '返回列表',
+      cancelButtonText: '留在此页',
+      type: 'info'
+    })
+      .then(() => {
+        router.push({ path: RoutesAlias.CompanyList })
+      })
+      .catch(() => {
+        // ... do nothing
+      })
+  }
+
+  // 提交表单
+  const handleSubmit = async () => {
+    if (!formRef.value) return
+    await formRef.value.validate((valid) => {
+      if (valid) {
+        if (id == 0) {
+          Api.add(formData).then(() => {
+            afterOk()
+          })
+        } else {
+          Api.edit(formData).then(() => {
+            afterOk()
+          })
+        }
+      }
+    })
+  }
+</script>

+ 215 - 0
web/src/views/company/follow/index.vue

@@ -0,0 +1,215 @@
+<!-- 学校关系管理 -->
+<!-- art-full-height 自动计算出页面剩余高度 -->
+<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
+<!-- 更多 useTable 使用示例请移步至 功能示例 下面的 高级表格示例 -->
+<template>
+  <div class="user-page art-full-height">
+    <!-- 搜索栏 -->
+    <UserSearch
+      v-model="searchForm"
+      @search="handleSearch"
+      @reset="resetSearchParams"
+      :selectList="selectList"
+    ></UserSearch>
+
+    <ElCard class="art-table-card" shadow="never">
+      <!-- 表格头部 -->
+      <ArtTableHeader v-model:columns="columnChecks" @refresh="refreshData">
+        <template #left>
+          <ElButton type="primary" @click="follow()" v-ripple v-auth="140301">新增跟进记录</ElButton>
+        </template>
+      </ArtTableHeader>
+
+      <!-- 表格 -->
+      <ArtTable
+        :loading="loading"
+        :data="data"
+        :columns="columns"
+        :pagination="pagination"
+        @pagination:size-change="handleSizeChange"
+        @pagination:current-change="handleCurrentChange"
+      >
+      </ArtTable>
+
+      <!--   跟进弹窗   -->
+      <FollowDialog
+        v-model:visible="followDialogVisible"
+        :user-data="currentUserData"
+        :type="'company'"
+        :first_id="0"
+        :second_id="0"
+        :selectList="selectList"
+        @submit="handleDialogSubmit"
+      />
+    </ElCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+  import {ElMessageBox, ElMessage, ElTag, ElImage, ElButton} from 'element-plus'
+  import { useTable } from '@/composables/useTable'
+  import UserSearch from './modules/user-search.vue'
+  import { followApi } from '@/api/followApi'
+  import {RoutesAlias} from "@/router/routesAlias";
+  import {router} from "@/router";
+  import {companyRelationApi} from "@/api/companyRelationApi";
+
+  defineOptions({ name: 'CompanyFollow' })
+
+  type CompanyContactItem = Api.Company.contactItem
+
+  // 弹窗相关
+  const dialogVisible = ref(false)
+  const followDialogVisible = ref(false)
+  const currentUserData = ref<Partial<CompanyContactItem>>({})
+
+  // 搜索表单
+  const searchForm = ref({
+    name: '',
+    phone: '',
+    company_id: parseInt(<string>useRoute().query.company_id)  || '',
+  })
+
+  const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+  const getSelectList = async () => {
+    await companyRelationApi.selectList().then(res => {
+      selectList.value = res
+    })
+  }
+  getSelectList()
+
+  const {
+    columns,
+    columnChecks,
+    data,
+    loading,
+    pagination,
+    getData,
+    searchParams,
+    resetSearchParams,
+    handleSizeChange,
+    handleCurrentChange,
+    refreshData
+  } = useTable<Api.Follow.FollowInfo>({
+    // 核心配置
+    core: {
+      apiFn: followApi.companyList,
+      apiParams: {
+        current: 1,
+        size: 20,
+        ...searchForm.value
+      },
+      // 排除 apiParams 中的属性
+      excludeParams: ['daterange'],
+      columnsFactory: () => [
+        { prop:'first_name', label:'校园(园区)' },
+        { prop:'second_name', label:'关系人' },
+        { prop:'position', label:'职位' },
+        { prop:'phone', label:'手机号' },
+        { prop:'weixin', label:'微信号' },
+        { prop:'chat_imgs', label:'微信聊天记录', formatter: (row) => {
+          if (row.chat_imgs.length > 0) {
+              return h(ElImage, {
+                src: row.chat_imgs[0],
+                previewSrcList: row.chat_imgs,
+                showProgress: true,
+                fit: "cover",
+                title: '点击预览全部图片',
+                style: {"max-width": "50px", "max-height": "50px"},
+                // 图片预览是否插入至 body 元素上,用于解决表格内部图片预览样式异常
+                previewTeleported: true
+              })
+            } else {
+              return '';
+            }
+          }
+        },
+        { prop:'user_name', label:'跟进人员' },
+        { prop:'create_date', label:'跟进时间' },
+        {
+          prop: 'operation',
+          label: '操作',
+          width: 120,
+          fixed: 'right', // 固定列
+          formatter: (row) =>
+            h('div', [
+              h(ArtButtonTable, {
+                type: 'view',
+                onClick: () => view(row.id)
+              }),
+            ])
+        }
+      ]
+    }
+  })
+
+  /**
+   * 查看
+   */
+  const view = (id: number): void => {
+    router.push({
+      path: RoutesAlias.CompanyFollowInfo,
+      query: {
+        id: id
+      }
+    })
+  }
+
+  /**
+   * 搜索处理
+   * @param params 参数
+   */
+  const handleSearch = (params: Record<string, any>) => {
+    Object.assign(searchParams, { ...params })
+    getData()
+  }
+
+    /**
+   * 显示跟进弹窗
+   */
+  const follow = (): void => {
+    nextTick(() => {
+      followDialogVisible.value = true
+    })
+  }
+
+  /**
+   * 处理弹窗提交事件
+   */
+  const handleDialogSubmit = async () => {
+    try {
+      dialogVisible.value = false
+      followDialogVisible.value = false
+      currentUserData.value = {}
+      // 延迟更新 不然数据可能没更新
+      setTimeout(() => {
+        refreshData()
+      }, 1000)
+    } catch (error) {
+      console.error('提交失败:', error)
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .user-page {
+    :deep(.user) {
+      .avatar {
+        width: 40px;
+        height: 40px;
+        margin-left: 0;
+        border-radius: 6px;
+      }
+
+      > div {
+        margin-left: 10px;
+
+        .user-name {
+          font-weight: 500;
+          color: var(--art-text-gray-800);
+        }
+      }
+    }
+  }
+</style>

+ 87 - 0
web/src/views/company/follow/info.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <el-row :gutter="20" class="detail">
+
+      <el-col :span="24">
+        <h3>基本信息</h3>
+      </el-col>
+
+      <el-col :span="24">
+        <label>学校(校区):</label> <span>{{ info.first_name }}</span>
+      </el-col>
+
+      <el-col :span="24">
+        <label>跟进关系人:</label>
+        <el-table :data="[{second_name: info.second_name, weixin: info.weixin, phone: info.phone, position: info.position}]" style="width: 60%; margin-top: 10px">
+          <el-table-column prop="second_name" label="姓名"  />
+          <el-table-column prop="position" label="职位"  />
+          <el-table-column prop="phone" label="手机号" />
+          <el-table-column prop="weixin" label="微信号" />
+        </el-table>
+      </el-col>
+
+      <el-col :span=24>
+        <label>微信聊天记录:</label>
+        <div style="margin-top: 10px">
+          <el-image
+              style="max-width: 100px; max-height: 180px; margin-right: 10px"
+              v-for="(url, index) in info.chat_imgs"
+              :key="index"
+              :src="url"
+              show-progress
+              :initial-index="index"
+              :preview-src-list="info.chat_imgs"
+              fit="cover"
+          />
+        </div>
+      </el-col>
+
+      <el-col :span="24">
+        <label>跟进记录详情:</label>
+        <ElCard shadow="never" v-html="info.detail" style="padding: 10px; margin-top: 20px"/>
+      </el-col>
+
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 初始化表单数据
+import {followApi} from "@/api/followApi";
+import {onMounted} from "vue";
+
+const DefaultData = <Api.Follow.FollowInfo>{
+  id: 0,
+  chat_imgs: [],
+  detail: '',
+  create_date: '',
+  first_name: '', //
+  second_name: '', //
+  user_name: '', //
+  avatar:'',
+  phone: '', // 手机号,
+  weixin: '', // 微信号,
+  position: '', // 职位,
+}
+const info = reactive<Api.Follow.FollowInfo>({ ...DefaultData })
+onMounted(() => {
+  followApi.schoolInfo(parseInt(useRoute().query.id as string)).then((res) => {
+    Object.assign(info, res)
+    console.log(`%c info == `, 'background:#41b883 ; padding:1px; color:#fff', info);
+  })
+})
+</script>
+
+<style scoped>
+.detail {
+  padding-top: 20px;
+  font-size: 14px;
+  .el-col {
+    margin-bottom: 30px;
+    label {
+      font-weight: bold;
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 87 - 0
web/src/views/company/follow/modules/user-search.vue

@@ -0,0 +1,87 @@
+<template>
+  <ArtSearchBar
+      ref="searchBarRef"
+      v-model="formData"
+      :items="formItems"
+      :selectList="selectList"
+      :rules="rules"
+      @reset="handleReset"
+      @search="handleSearch"
+  >
+  </ArtSearchBar>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, h } from 'vue'
+
+interface Props {
+  modelValue: Record<string, any>
+  selectList: Api.Common.SelectRelationInfo[]
+}
+interface Emits {
+  (e: 'update:modelValue', value: Record<string, any>): void
+  (e: 'search', params: Record<string, any>): void
+  (e: 'reset'): void
+}
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 表单数据双向绑定
+const searchBarRef = ref()
+const formData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 校验规则
+const rules = {
+  // name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
+}
+
+const props1 = {
+  checkStrictly: true
+}
+
+// 表单配置
+const formItems = computed(() => [
+  {
+    label: '手机号',
+    key: 'phone',
+    type: 'input',
+    placeholder: '请输入手机号',
+    clearable: true
+  },
+  {
+    label: '餐饮公司',
+    key: 'school',
+    type: 'cascader',
+    props: {
+      options: props.selectList.map(item => ({
+        value: item.id,
+        label: item.name,
+        children: item.children.map(item => ({
+          value: item.id,
+          label: item.name
+        }))
+      })),
+      style: { width: '280px' },
+      filterable: true,
+      placeholder: '可搜索',
+      clearable: true,
+      props: props1
+    }
+  },
+])
+
+// 事件
+function handleReset() {
+  console.log('重置表单')
+  emit('reset')
+}
+
+async function handleSearch() {
+  await searchBarRef.value.validate()
+  emit('search', formData.value)
+  console.log('表单数据', formData.value)
+}
+</script>

+ 123 - 0
web/src/views/company/info.vue

@@ -0,0 +1,123 @@
+<template>
+  <div>
+    <el-row :gutter="20" class="detail">
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>名称:</label> <span>{{ info.name }}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>地区:</label> <span>{{ [info.province, info.city, info.area].join(' / ') }}</span>
+      </el-col>
+
+      <el-col :span=24>
+        <label>详细地址:</label> <span>{{ info.address }}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>在校人数:</label> <span>{{ info.person_num }}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>负责人:</label> <span>{{ info.bind_user_id }}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否有饿了么校内站:</label> <span>{{ info.is_eleme_in_school ? '有' : '无'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否有饿了么校外站:</label> <span>{{ info.is_eleme_out_school ? '有' : '无'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否有美团校内站:</label> <span>{{ info.is_meituan_in_school ? '有' : '无'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否有美团校外站:</label> <span>{{ info.is_meituan_out_school ? '有' : '无'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否能上楼:</label> <span>{{ info.can_go_upstairs ? '不能' : '能'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否合作:</label> <span>{{ info.is_cooperate ? '已合作' : '未合作'}}</span>
+      </el-col>
+
+      <el-col :xs="24" :lg="8" :sm="12">
+        <label>是否允许骑电动车:</label> <span>{{ info.can_ride ? '不能' : '能'}}</span>
+      </el-col>
+
+      <el-col :span=24>
+        <label>宿舍分布情况:</label> <span>{{ info.dormitory_distribution }}</span>
+      </el-col>
+
+      <el-col :span=24>
+        <label>校门口取餐点离宿舍情况:</label> <span>{{ info.qucan_station_distribution }}</span>
+      </el-col>
+
+      <el-col :span=24>
+        <label>校外商圈情况:</label> <span>{{ info.out_business_description }}</span>
+      </el-col>
+
+      <el-col :span=24>
+        <label>备注:</label> <span>{{ info.memo }}</span>
+      </el-col>
+
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 初始化表单数据
+import {schoolApi} from "@/api/schoolApi";
+import {onMounted} from "vue";
+
+const DefaultData = <Api.School.SchoolInfo>{
+  id:0,
+  province: '',
+  city: '',
+  area: '',
+  name: '',
+  distinct:[],
+  address: '',
+  person_num: '',
+  bind_user_id: 0,
+  is_eleme_in_school: 0,
+  is_eleme_out_school: 0,
+  is_meituan_in_school: 0,
+  is_meituan_out_school: 0,
+  can_go_upstairs: 0,
+  is_cooperate: 0,
+  can_ride: 0,
+  dormitory_distribution: '',
+  qucan_station_distribution: '',
+  out_business_description: '',
+  memo: '',
+  create_date:'',
+  update_date:'',
+}
+const info = reactive<Api.School.SchoolInfo>({ ...DefaultData })
+onMounted(() => {
+  schoolApi.info(parseInt(useRoute().query.id as string)).then((res) => {
+    Object.assign(info, res)
+    console.log(`%c info == `, 'background:#41b883 ; padding:1px; color:#fff', info);
+  })
+})
+</script>
+
+<style scoped>
+.detail {
+  padding-top: 20px;
+  font-size: 14px;
+  .el-col {
+    margin-bottom: 30px;
+    label {
+      font-weight: bold;
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 406 - 0
web/src/views/company/list/index.vue

@@ -0,0 +1,406 @@
+<!-- 用户管理 -->
+<!-- art-full-height 自动计算出页面剩余高度 -->
+<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
+<!-- 更多 useTable 使用示例请移步至 功能示例 下面的 高级表格示例 -->
+<template>
+  <div class="user-page art-full-height">
+    <!-- 搜索栏 -->
+    <UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" :schools="schools"></UserSearch>
+
+    <!--   跟进弹窗   -->
+    <FollowDialog
+        v-model:visible="followDialogVisible"
+        :user-data="currentRow"
+        :type="'company'"
+        :first_id="currentRow.id || 0"
+        :second_id="0"
+        :selectList="selectList"
+        @submit="handleDialogSubmit"
+    />
+
+    <ElCard class="art-table-card" shadow="never">
+      <!-- 表格头部 -->
+      <ArtTableHeader v-model:columns="columnChecks" @refresh="refreshData">
+        <template #left>
+          <ElButton type="primary" @click="edit()" v-ripple v-auth="120101">新增餐饮公司</ElButton>
+        </template>
+      </ArtTableHeader>
+
+      <!-- 表格 -->
+      <ArtTable
+          :loading="loading"
+          :data="data"
+          :columns="columns"
+          :pagination="pagination"
+          @pagination:size-change="handleSizeChange"
+          @pagination:current-change="handleCurrentChange"
+      >
+        <template #person_num="scope">
+          <ElInput
+              v-model="scope.row.person_num"
+              placeholder="scope.row.person_num"
+              @blur="doUpdateAttr(scope)"
+          />
+        </template>
+      </ArtTable>
+
+    </ElCard>
+
+    <el-drawer
+        v-model="drawer"
+        direction="rtl"
+        size="70%"
+    >
+      <template #header>
+        <span style="font-size: 20px; font-weight: bold;">{{ currentRow.name }}</span>
+      </template>
+      <ElRow>
+        <ElCol :sm="10">
+          <ElRow class="detail">
+
+            <el-col>
+              <label>地区:</label> <span>{{
+                [currentRow.province, currentRow.city, currentRow.area].join(' / ')
+              }}</span>
+            </el-col>
+
+            <el-col>
+              <label>详细地址:</label> <span>{{ currentRow.address }}</span>
+            </el-col>
+
+            <el-col>
+              <label>关联学校(校区)食堂:</label>
+              <div class="flex gap-2">
+                <ElTag v-for="item in companyInfo?.canteen_names" :key="item" type="primary" style="margin: 10px 5px 0 0; font-weight: normal">{{ item }}</ElTag>
+              </div>
+            </el-col>
+
+            <el-col>
+              <label>负责人:</label> <span>{{ currentRow.bind_user_name }}</span>
+            </el-col>
+
+            <el-col>
+              <label>备注:</label> <span>{{ currentRow.memo }}</span>
+            </el-col>
+
+            <el-col>
+              <label>关系人:</label>
+              <el-table border :data="companyInfo?.relations || []" style="width: 100%; margin-top: 10px">
+                <el-table-column prop="name" label="姓名" />
+                <el-table-column prop="position" label="职位"  />
+                <el-table-column prop="phone" label="手机号" />
+                <el-table-column prop="weixin" label="微信号" />
+              </el-table>
+            </el-col>
+
+
+          </ElRow>
+        </ElCol>
+        <ElCol :sm="14">
+          <FollowDrawer :first_id="currentRow.id" :second_id="0" type="company" :uid="drawerUid"/>
+        </ElCol>
+      </ElRow>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import {ElMessageBox, ElMessage, ElTag, ElImage, ElButton, ElInput} from 'element-plus'
+import {useTable} from '@/composables/useTable'
+import {companyApi} from '@/api/companyApi'
+import UserSearch from './user-search.vue'
+import {useUserStore} from '@/store/modules/user'
+import EmojiText from '@utils/ui/emojo'
+import {router} from '@/router'
+import {RoutesAlias} from '@/router/routesAlias'
+import {followApi} from "@/api/followApi";
+import {companyRelationApi} from "@/api/companyRelationApi";
+import {schoolApi} from "@/api/schoolApi";
+
+defineOptions({name: 'User'})
+
+const {list} = companyApi
+const drawer = ref(false)
+const followDialogVisible = ref(false)
+
+// 搜索表单
+const searchForm = ref({
+  name: '',
+  is_cooperate: -1,
+  address: []
+})
+
+const defaultValue = <Api.Company.ListItem>{
+  id: 0,
+  name: '',
+  province: '',
+  city: '',
+  area: '',
+  address: '',
+  bind_user_id: 0,
+  memo: '',
+  last_user_id: 0,
+  last_date: '',
+  last_user_name: '',
+  bind_user_name: '',
+}
+
+const currentRow = ref<Api.Company.ListItem>({...defaultValue})
+
+const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+const getSelectList = async () => {
+  await companyRelationApi.selectList().then(res => {
+    selectList.value = res
+  })
+}
+getSelectList()
+
+const drawerUid = ref(0)
+
+/**
+ * 处理弹窗提交事件
+ */
+const handleDialogSubmit = async () => {
+  followDialogVisible.value = false
+  drawerUid.value++
+}
+
+const {
+  columns,
+  columnChecks,
+  data,
+  loading,
+  pagination,
+  getData,
+  searchParams,
+  resetSearchParams,
+  handleSizeChange,
+  handleCurrentChange,
+  refreshData
+} = useTable<Api.Company.ListItem>({
+  // 核心配置
+  core: {
+    apiFn: list,
+    apiParams: {
+      current: 1,
+      size: 20,
+      ...searchForm.value
+    },
+    // 排除 apiParams 中的属性
+    excludeParams: [],
+    columnsFactory: () => [
+      {
+        prop: 'name', label: '餐饮公司名称', formatter: (row) => {
+          return h(ElButton, {
+            type: 'primary',
+            link: true,
+            onClick: () => showDrawer(row),
+            style: {"text-decoration": 'underline'}
+          }, () => row.name)
+        }
+      },
+      {prop: 'area', label: '地区', formatter: (row) => row.province + row.city + row.area},
+      {prop: 'address', label: '详细地址'},
+      {
+        prop: 'canteen',
+        label: '关联食堂',
+        // sortable: true,
+        // checked: false, // 隐藏列
+        formatter: (row) => {
+          return h(ElButton, {type: 'primary', size: 'small', onClick: () => showCanteen(row)}, () => '查看')
+        }
+      },
+      {
+        prop: 'concat',
+        label: '关系人',
+        formatter: (row) => {
+          return h(ElButton, {type: 'primary', size: 'small', onClick: () => showContact(row)}, () => '查看')
+        }
+      },
+      { prop:'last_user_name', label:'最后一次跟进人' },
+      { prop:'last_date', label:'最后一次跟进时间' },
+      { prop:'bind_user_name', label:'负责人' },
+      {
+        prop: '', label: '跟进记录', formatter: (row) => {
+          return h(ElButton, {
+            type: 'primary',
+            size: 'small',
+            onClick: () => follow(row),
+          }, () => '跟进')
+        }
+      },
+      {prop: 'memo', label: '备注', showOverflowTooltip: true},
+      {
+        prop: 'operation',
+        label: '操作',
+        width: 120,
+        fixed: 'right', // 固定列
+        formatter: (row) =>
+            useUserStore().checkAuth(110202) &&
+            h('div', [
+              h(ArtButtonTable, {
+                type: 'view',
+                onClick: () => view(row.id)
+              }),
+              h(ArtButtonTable, {
+                type: 'edit',
+                onClick: () => edit(row.id)
+              }),
+            ])
+      }
+    ]
+  }
+})
+
+// TODO:不知道为什么初始化会触发switch的change事件
+const isMounted = ref(false)
+onMounted(() => {
+  setTimeout(() => {
+    isMounted.value = true
+  }, 1000)
+})
+
+const doUpdateAttr = (scope: any) => {
+  console.log(`%c scope == `, 'background:#41b883 ; padding:1px; color:#fff', scope)
+  if (!scope.row.id || !isMounted.value) {
+    return
+  }
+  companyApi
+      .updateAttr({id: scope.row.id, attr: scope.prop, value: scope.row[scope.prop]})
+      .then(() => {
+        ElMessage.success(`${EmojiText[200]} 修改成功`)
+      })
+}
+
+/**
+ * 搜索处理
+ * @param params 参数
+ */
+const handleSearch = (params: Record<string, any>) => {
+  Object.assign(searchParams, {...params})
+  getData()
+}
+
+/**
+ * 编辑
+ */
+const edit = (id?: number): void => {
+  router.push({
+    path: RoutesAlias.CompanyEdit,
+    query: {
+      id: id
+    }
+  })
+}
+
+/**
+ * 显示跟进弹窗
+ */
+const follow = (row: Api.Company.ListItem): void => {
+  currentRow.value = row || {}
+  nextTick(() => {
+    followDialogVisible.value = true
+  })
+}
+
+/**
+ * 查看
+ */
+const view = (id: number): void => {
+  router.push({
+    path: RoutesAlias.CompanyInfo,
+    query: {
+      id: id
+    }
+  })
+}
+
+const companyInfo = ref<Api.Company.Info>()
+
+const showDrawer = (row: Api.Company.ListItem): void => {
+  drawer.value = true;
+  currentRow.value = row
+  companyApi.info(row.id).then((res) => {
+    companyInfo.value = res
+  })
+}
+
+const schools = ref<Api.Common.SelectInfo[]>([])
+schoolApi.selectList().then((res) => {
+  schools.value = res
+})
+
+const showContact = (row: Api.Company.ListItem):void => {
+  router.push({
+    path: RoutesAlias.CompanyRelation,
+    query: {
+      company_id: row.id
+    }
+  })
+}
+
+const showCanteen = (row: Api.Company.ListItem):void => {
+  router.push({
+    path: RoutesAlias.CanteenList,
+    query: {
+      company_id: row.id
+    }
+  })
+}
+
+/**
+ * 删除
+ */
+const deleteUser = (id: number): void => {
+  ElMessageBox.confirm(`确定要删除该学校吗?`, '删除学校', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'error'
+  }).then(() => {
+    companyApi.delete({id: id}).then(() => {
+      ElMessage.success(`${EmojiText[200]} 删除成功`)
+      setTimeout(() => {
+        getData()
+      }, 1000)
+    })
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.el-button::after {
+  content: none !important;
+}
+.user-page {
+  :deep(.user) {
+    .avatar {
+      width: 40px;
+      height: 40px;
+      margin-left: 0;
+      border-radius: 6px;
+    }
+
+    > div {
+      margin-left: 10px;
+
+      .user-name {
+        font-weight: 500;
+        color: var(--art-text-gray-800);
+      }
+    }
+  }
+}
+.detail {
+  //padding-top: 20px;
+  font-size: 14px;
+  padding-right: 5px;
+  .el-col {
+    margin-bottom: 30px;
+    label {
+      font-weight: bold;
+      margin-right: 5px;
+    }
+  }
+}
+</style>

+ 92 - 0
web/src/views/company/list/user-search.vue

@@ -0,0 +1,92 @@
+<template>
+  <ArtSearchBar
+    ref="searchBarRef"
+    v-model="formData"
+    :items="formItems"
+    :rules="rules"
+    @reset="handleReset"
+    @search="handleSearch"
+  >
+  </ArtSearchBar>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, onMounted, h } from 'vue'
+  import { cityJson } from '@/router/utils/city'
+
+  interface Props {
+    modelValue: Record<string, any>
+    schools: Api.Common.SelectInfo[]
+  }
+  interface Emits {
+    (e: 'update:modelValue', value: Record<string, any>): void
+    (e: 'search', params: Record<string, any>): void
+    (e: 'reset'): void
+  }
+  const props = defineProps<Props>()
+  const emit = defineEmits<Emits>()
+
+  // 表单数据双向绑定
+  const searchBarRef = ref()
+  const formData = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+  })
+
+  // 校验规则
+  const rules = {
+    // name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
+  }
+
+  const props1 = {
+    checkStrictly: true
+  }
+
+  // 表单配置
+  const formItems = computed(() => [
+    {
+      label: '名称',
+      key: 'name',
+      type: 'input',
+      placeholder: '请输入名称',
+      clearable: true
+    },
+    {
+      label: '地区',
+      key: 'address',
+      type: 'cascader',
+      props: {
+        options: cityJson,
+        style: { width: '280px' },
+        filterable: true,
+        placeholder: '可搜索',
+        clearable: true,
+        props: props1
+      }
+    },
+    {
+      label: '学校',
+      key: 'school_id',
+      type: 'select',
+      filterable: true,
+      props: {
+        options: props.schools.map(item => ({
+          value: item.id,
+          label: item.name
+        }))
+      }
+    }
+  ])
+
+  // 事件
+  function handleReset() {
+    console.log('重置表单')
+    emit('reset')
+  }
+
+  async function handleSearch() {
+    await searchBarRef.value.validate()
+    emit('search', formData.value)
+    console.log('表单数据', formData.value)
+  }
+</script>

+ 336 - 0
web/src/views/company/relation/index.vue

@@ -0,0 +1,336 @@
+<!-- 餐饮公司关系管理 -->
+<!-- art-full-height 自动计算出页面剩余高度 -->
+<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
+<!-- 更多 useTable 使用示例请移步至 功能示例 下面的 高级表格示例 -->
+<template>
+  <div class="user-page art-full-height">
+    <!-- 搜索栏 -->
+    <UserSearch
+      v-model="searchForm"
+      @search="handleSearch"
+      @reset="resetSearchParams"
+      :selectList="selectList"
+    ></UserSearch>
+
+    <ElCard class="art-table-card" shadow="never">
+      <!-- 表格头部 -->
+      <ArtTableHeader v-model:columns="columnChecks" @refresh="refreshData">
+        <template #left>
+          <ElButton type="primary" @click="showDialog('add')" v-ripple v-auth="120201">新增餐饮公司关系</ElButton>
+        </template>
+      </ArtTableHeader>
+
+      <!-- 表格 -->
+      <ArtTable
+        :loading="loading"
+        :data="data"
+        :columns="columns"
+        :pagination="pagination"
+        @selection-change="handleSelectionChange"
+        @pagination:size-change="handleSizeChange"
+        @pagination:current-change="handleCurrentChange"
+      >
+      </ArtTable>
+
+      <!-- 餐饮公司关系弹窗 -->
+      <UserDialog
+        v-model:visible="dialogVisible"
+        :type="dialogType"
+        :user-data="currentUserData"
+        :selectList="selectList"
+        @submit="handleDialogSubmit"
+      />
+
+      <!--   跟进弹窗   -->
+      <FollowDialog
+        v-model:visible="followDialogVisible"
+        :user-data="currentUserData"
+        :type="'company'"
+        :first_id="currentUserData.company_id || 0"
+        :second_id="currentUserData.id || 0"
+        :selectList="selectList"
+        @submit="handleDialogSubmit"
+      />
+    </ElCard>
+
+    <!--   跟进详情显示   -->
+    <el-drawer
+        v-model="drawer"
+        direction="rtl"
+        size="60%"
+    >
+      <template #header>
+        <span style="font-size: 20px; font-weight: bold;">校方联系人</span>
+      </template>
+      <ElRow>
+        <ElCol :sm="8">
+          <ElRow class="detail">
+
+            <el-col>
+              <label>关系人:</label> <span>{{ currentRow.name }}</span>
+            </el-col>
+
+            <el-col>
+              <label>手机:</label> <span>{{ currentRow.phone }}</span>
+            </el-col>
+
+            <el-col>
+              <label>微信:</label> <span>{{ currentRow.weixin }}</span>
+            </el-col>
+
+            <el-col>
+              <label>职位:</label> <span>{{ currentRow.position }}</span>
+            </el-col>
+
+            <el-col>
+              <label>餐饮公司:</label> <span>{{ currentRow.company_name }}</span>
+            </el-col>
+
+            <el-col>
+              <label>备注:</label> <span>{{ currentRow.memo }}</span>
+            </el-col>
+
+          </ElRow>
+        </ElCol>
+        <ElCol :sm="16">
+          <FollowDrawer :first_id="currentRow.company_id" :second_id="currentRow.id" type="company" :uid="drawerUid"/>
+        </ElCol>
+      </ElRow>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+  import {ElMessageBox, ElMessage, ElTag, ElImage, ElButton} from 'element-plus'
+  import { useTable } from '@/composables/useTable'
+  import UserSearch from './modules/user-search.vue'
+  import UserDialog from './modules/user-dialog.vue'
+  import { companyRelationApi } from '@/api/companyRelationApi'
+  import { useUserStore } from '@/store/modules/user'
+  import {followApi} from "@/api/followApi";
+
+  defineOptions({ name: 'companyRelation' })
+
+  type CompanyContactItem = Api.Company.contactItem
+  const { list } = companyRelationApi
+
+  // 弹窗相关
+  const dialogType = ref<Form.DialogType>('add')
+  const dialogVisible = ref(false)
+  const followDialogVisible = ref(false)
+  const currentUserData = ref<Partial<CompanyContactItem>>({})
+
+  // 选中行
+  const selectedRows = ref<CompanyContactItem[]>([])
+
+  // 搜索表单
+  const searchForm = ref({
+    name: '',
+    phone: '',
+    company_id: parseInt(<string>useRoute().query.company_id)  || '',
+  })
+
+  const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+  const getSelectList = async () => {
+    await companyRelationApi.selectList().then(res => {
+      selectList.value = res
+    })
+  }
+  getSelectList()
+
+  const {
+    columns,
+    columnChecks,
+    data,
+    loading,
+    pagination,
+    getData,
+    searchParams,
+    resetSearchParams,
+    handleSizeChange,
+    handleCurrentChange,
+    refreshData
+  } = useTable<CompanyContactItem>({
+    // 核心配置
+    core: {
+      apiFn: list,
+      apiParams: {
+        current: 1,
+        size: 20,
+        ...searchForm.value
+      },
+      // 排除 apiParams 中的属性
+      excludeParams: ['daterange'],
+      columnsFactory: () => [
+        {
+          prop: 'name', label: '关系人', formatter: (row) => {
+            return h(ElButton, {
+              type: 'primary',
+              link: true,
+              onClick: () => showDrawer(row),
+              style: {"text-decoration": 'underline'}
+            }, () => row.name)
+          }
+        },
+        { prop: 'company_name', label: '餐饮公司' },
+        { prop: 'phone', label: '手机号' },
+        { prop: 'weixin', label: '微信号' },
+        { prop: 'position', label: '职位' },
+        {
+          prop: '', label: '跟进记录', formatter: (row) => {
+            return h(ElButton, {
+              size: 'small',
+              type: 'primary',
+              onClick: () => follow(row),
+            }, () => '跟进')
+          }
+        },
+        { prop: 'memo', label: '备注', showOverflowTooltip: true },
+        {
+          prop: 'operation',
+          label: '操作',
+          width: 120,
+          fixed: 'right', // 固定列
+          formatter: (row) =>
+            useUserStore().checkAuth(120202) &&
+            h('div', [
+              h(ArtButtonTable, {
+                type: 'edit',
+                onClick: () => showDialog('edit', row)
+              }),
+              useUserStore().checkAuth(120203) &&
+                h(ArtButtonTable, {
+                  type: 'delete',
+                  onClick: () => deleteUser(row)
+                })
+            ])
+        }
+      ]
+    }
+  })
+
+  /**
+   * 搜索处理
+   * @param params 参数
+   */
+  const handleSearch = (params: Record<string, any>) => {
+    Object.assign(searchParams, { ...params })
+    getData()
+  }
+
+  const detaltValue = <Api.Company.CompanyContactItem>{
+    id: 0,
+    name: '',
+    company_id: 0, // 餐饮公司ID,
+    company_name: '',
+    phone: '',
+    weixin: '',
+    position: '',
+    memo: '',
+  }
+
+  const currentRow = ref<Api.Company.CompanyContactItem>({...detaltValue})
+  const drawer = ref(false)
+
+  const showDrawer = (row: Api.Company.CompanyContactItem): void => {
+    drawer.value = true;
+    currentRow.value = row
+  }
+
+  /**
+   * 显示餐饮公司关系弹窗
+   */
+  const showDialog = (type: Form.DialogType, row?: CompanyContactItem): void => {
+    dialogType.value = type
+    currentUserData.value = row || {}
+    nextTick(() => {
+      dialogVisible.value = true
+    })
+  }
+
+    /**
+   * 显示跟进弹窗
+   */
+  const follow = (row: CompanyContactItem): void => {
+    currentUserData.value = row || {}
+    nextTick(() => {
+      followDialogVisible.value = true
+    })
+  }
+
+  /**
+   * 删除餐饮公司关系
+   */
+  const deleteUser = (row: CompanyContactItem): void => {
+    ElMessageBox.confirm(`确定要注销该餐饮公司关系吗?`, '注销餐饮公司关系', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'error'
+    }).then(() => {
+      companyRelationApi.delete({ id: row.id })
+      refreshData()
+    })
+  }
+
+  const drawerUid = ref(0)
+
+  /**
+   * 处理弹窗提交事件
+   */
+  const handleDialogSubmit = async () => {
+    try {
+      dialogVisible.value = false
+      followDialogVisible.value = false
+      drawerUid.value++
+      await getSelectList()
+      // 延迟更新 不然数据可能没更新
+      setTimeout(() => {
+        refreshData()
+      }, 1000)
+    } catch (error) {
+      console.error('提交失败:', error)
+    }
+  }
+
+  /**
+   * 处理表格行选择变化
+   */
+  const handleSelectionChange = (selection: CompanyContactItem[]): void => {
+    selectedRows.value = selection
+    console.log('选中行数据:', selectedRows.value)
+  }
+</script>
+
+<style lang="scss" scoped>
+  .user-page {
+    :deep(.user) {
+      .avatar {
+        width: 40px;
+        height: 40px;
+        margin-left: 0;
+        border-radius: 6px;
+      }
+
+      > div {
+        margin-left: 10px;
+
+        .user-name {
+          font-weight: 500;
+          color: var(--art-text-gray-800);
+        }
+      }
+    }
+  }
+  .detail {
+    //padding-top: 20px;
+    font-size: 14px;
+    .el-col {
+      margin-bottom: 30px;
+      label {
+        font-weight: bold;
+        margin-right: 5px;
+      }
+    }
+  }
+</style>

+ 157 - 0
web/src/views/company/relation/modules/user-dialog.vue

@@ -0,0 +1,157 @@
+<template>
+  <ElDialog
+    v-model="dialogVisible"
+    :title="dialogType === 'add' ? '添加餐饮公司关系' : '编辑餐饮公司关系'"
+    width="30%"
+    align-center
+  >
+    <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
+      <ElFormItem label="关系人" prop="name">
+        <ElInput v-model="formData.name" maxlength="20" type="text" />
+      </ElFormItem>
+      <ElFormItem label="餐饮公司" prop="company_id">
+        <ElSelect v-model="formData.company_id" :empty-values="[0]" :value-on-clear="0">
+          <ElOption
+            v-for="item in selectList"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          />
+        </ElSelect>
+      </ElFormItem>
+      <ElFormItem label="手机号" prop="phone">
+        <ElInput v-model="formData.phone" maxlength="20" type="text" />
+      </ElFormItem>
+      <ElFormItem label="微信号" prop="weixin">
+        <ElInput v-model="formData.weixin" maxlength="20" type="text" />
+      </ElFormItem>
+      <ElFormItem label="职位" prop="position">
+        <ElInput v-model="formData.position" maxlength="20" type="text" />
+      </ElFormItem>
+      <ElFormItem label="备注" prop="memo" >
+        <ElInput v-model="formData.memo" maxlength="255" type="textarea" :rows="4"/>
+      </ElFormItem>
+    </ElForm>
+    <template #footer>
+      <div class="dialog-footer">
+        <ElButton @click="dialogVisible = false">取消</ElButton>
+        <ElButton type="primary" @click="handleSubmit">提交</ElButton>
+      </div>
+    </template>
+  </ElDialog>
+</template>
+
+<script setup lang="ts">
+  import type { FormInstance, FormRules } from 'element-plus'
+  import { companyRelationApi } from '@/api/companyRelationApi'
+
+  interface Props {
+    visible: boolean
+    type: string
+    userData?: any
+    selectList: Api.Common.SelectRelationInfo[]
+  }
+
+  interface Emits {
+    (e: 'update:visible', value: boolean): void
+    (e: 'submit'): void
+  }
+
+  const props = defineProps<Props>()
+  const emit = defineEmits<Emits>()
+
+  // 对话框显示控制
+  const dialogVisible = computed({
+    get: () => props.visible,
+    set: (value) => emit('update:visible', value)
+  })
+
+  const dialogType = computed(() => props.type)
+
+  // 表单实例
+  const formRef = ref<FormInstance>()
+
+  // 表单数据
+  const formData = reactive<Form.CompanyContact>({
+    id: 0,
+    name: '', // 关系人,
+    company_id: 0, // 餐饮公司ID,
+    phone: '', // 手机号,
+    weixin: '', // 微信号,
+    position: '', // 职位,
+    memo: '' // 备注,
+  })
+
+  // 表单验证规则
+  const rules: FormRules = {
+    name: [
+      { required: true, message: '请输入关系人', trigger: 'blur' },
+      { max: 20, message: '长度最多20个字符', trigger: 'blur' }
+    ],
+    company_id: [{ required: true, message: '请输入餐饮公司', trigger: 'blur' }],
+    phone: [
+      { required: true, message: '请输入手机号', trigger: 'blur' },
+      { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
+    ],
+    weixin: [
+      { required: true, message: '请输入微信号', trigger: 'blur' },
+      { max: 20, message: '长度最多20个字符', trigger: 'blur' }
+    ],
+    position: [
+      { required: true, message: '请输入职位', trigger: 'blur' },
+      { max: 20, message: '长度最多20个字符', trigger: 'blur' }
+    ],
+    memo: [{ max: 255, message: '长度最多255个字符', trigger: 'blur' }]
+  }
+
+  const isEdit = ref(false)
+  // 初始化表单数据
+  const initFormData = () => {
+    isEdit.value = props.type === 'edit' && props.userData
+    const row = props.userData
+    Object.assign(formData, {
+      id: props.userData.id,
+      name: isEdit.value ? row.name || '' : '',
+      phone: isEdit.value ? row.phone || '' : '',
+      company_id: isEdit.value ? row.company_id : 0,
+      weixin: isEdit.value ? row.weixin || '' : '',
+      position: isEdit.value ? row.position || '' : '',
+      memo: isEdit.value ? row.memo || '' : ''
+    })
+  }
+
+  // 统一监听对话框状态变化
+  watch(
+    () => [props.visible, props.type, props.userData],
+    ([visible]) => {
+      if (visible) {
+        initFormData()
+        nextTick(() => {
+          formRef.value?.clearValidate()
+        })
+      }
+    },
+    { immediate: true }
+  )
+
+  // 提交表单
+  const handleSubmit = async () => {
+    if (!formRef.value) return
+    await formRef.value.validate((valid) => {
+      if (valid) {
+        console.log(`%c formData == `, 'background:#41b883 ; padding:1px; color:#fff', formData)
+        if (isEdit.value) {
+          companyRelationApi.edit(formData).then(() => {
+            dialogVisible.value = false
+            emit('submit')
+          })
+        } else {
+          companyRelationApi.add(formData).then(() => {
+            dialogVisible.value = false
+            emit('submit')
+          })
+        }
+      }
+    })
+  }
+</script>

+ 77 - 0
web/src/views/company/relation/modules/user-search.vue

@@ -0,0 +1,77 @@
+<template>
+  <ArtSearchBar
+    ref="searchBarRef"
+    v-model="formData"
+    :items="formItems"
+    :rules="rules"
+    @reset="handleReset"
+    @search="handleSearch"
+  >
+  </ArtSearchBar>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, onMounted, h } from 'vue'
+
+  interface Props {
+    modelValue: Record<string, any>
+    selectList: Api.Common.SelectRelationInfo[]
+  }
+  interface Emits {
+    (e: 'update:modelValue', value: Record<string, any>): void
+    (e: 'search', params: Record<string, any>): void
+    (e: 'reset'): void
+  }
+  const props = defineProps<Props>()
+  const emit = defineEmits<Emits>()
+
+  // 表单数据双向绑定
+  const searchBarRef = ref()
+  const formData = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+  })
+
+  // 校验规则
+  const rules = {
+    // name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
+  }
+
+  // 动态 options
+  const levelOptions = ref<{ label: string; value: string; disabled?: boolean }[]>([])
+
+  // 表单配置
+  const formItems = computed(() => [
+    { label: '用户名', key: 'name', type: 'input', placeholder: '请输入用户名', clearable: true },
+    {
+      label: '手机号',
+      key: 'phone',
+      type: 'input',
+      props: { placeholder: '请输入手机号', maxlength: '11' }
+    },
+    {
+      label: '餐饮公司',
+      key: 'company_id',
+      type: 'select',
+      props: {
+        placeholder: '请选择',
+        options: props.selectList.map((item) => ({
+          label: item.name,
+          value: item.id
+        }))
+      }
+    }
+  ])
+
+  // 事件
+  function handleReset() {
+    console.log('重置表单')
+    emit('reset')
+  }
+
+  async function handleSearch() {
+    await searchBarRef.value.validate()
+    emit('search', formData.value)
+    console.log('表单数据', formData.value)
+  }
+</script>

+ 3 - 2
web/src/views/school/follow/index.vue

@@ -73,8 +73,9 @@
 
   const selectList = ref<Api.Common.SelectRelationInfo[]>([])
   const getSelectList = async () => {
-    const data = await schoolRelationApi.selectList()
-    selectList.value = data
+    await schoolRelationApi.selectList().then(res => {
+      selectList.value = res
+    })
   }
   getSelectList()
 

+ 55 - 30
web/src/views/school/list/index.vue

@@ -147,8 +147,29 @@
             </el-col>
 
             <el-col :span=24>
-              <label>备注:</label> <span>{{ currentRow.memo }}</span>
+              <label>负责人:</label> <span>{{ currentRow.bind_user_name }}</span>
             </el-col>
+
+            <el-col>
+              <label>校方关系人:</label>
+              <el-table border :data="schoolInfo?.canteens || []" style="width: 100%; margin-top: 10px">
+                <el-table-column prop="name" label="食堂" />
+                <el-table-column prop="username" label="食堂经理"  />
+                <el-table-column prop="phone" label="手机号" />
+                <el-table-column prop="weixin" label="微信号" />
+              </el-table>
+            </el-col>
+
+            <el-col>
+              <label>食堂信息:</label>
+              <el-table border :data="schoolInfo?.relations || []" style="width: 100%; margin-top: 10px">
+                <el-table-column prop="name" label="姓名" />
+                <el-table-column prop="position" label="职位"  />
+                <el-table-column prop="phone" label="手机号" />
+                <el-table-column prop="weixin" label="微信号" />
+              </el-table>
+            </el-col>
+
           </ElRow>
         </ElCol>
         <ElCol :sm="14">
@@ -211,8 +232,9 @@ const currentRow = ref<Api.School.SchoolListItem>({...detaltValue})
 
 const selectList = ref<Api.Common.SelectRelationInfo[]>([])
 const getSelectList = async () => {
-  const data = await schoolRelationApi.selectList()
-  selectList.value = data
+  await schoolRelationApi.selectList().then(res => {
+    selectList.value = res
+  })
 }
 getSelectList()
 
@@ -268,14 +290,14 @@ const {
         // sortable: true,
         // checked: false, // 隐藏列
         formatter: (row) => {
-          return h(ElButton, {type: 'primary'}, () => '查看')
+          return h(ElButton, {type: 'primary', size: 'small', onClick: () => showCanteen(row)}, () => '查看')
         }
       },
       {
         prop: 'concat',
         label: '关系人',
         formatter: (row) => {
-          return h(ElButton, {type: 'primary', onClick: () => showContact(row)}, () => '查看')
+          return h(ElButton, {type: 'primary', size: 'small', onClick: () => showContact(row)}, () => '查看')
         }
       },
       {
@@ -287,8 +309,13 @@ const {
           )
         }
       },
-      {prop: 'person_num', label: '在校人数', useSlot: true},
-      {prop: 'is_eleme_in_school', label: '是否有饿了么校内站', useSlot: true},
+      {prop: 'person_num', label: '在校人数'},
+      // {prop: 'is_eleme_in_school', label: '是否有饿了么校内站', useSlot: true},
+      {prop: 'is_eleme_in_school', label: '是否有饿了么校内站', formatter: (row) => {
+          return h(ElTag, {type: row.is_eleme_out_school ? 'success' : 'danger'}, () =>
+              row.is_eleme_out_school ? '有' : '无'
+          )
+        }},
       {
         prop: 'is_eleme_out_school',
         label: '是否有饿了么校外站',
@@ -338,28 +365,13 @@ const {
         prop: '', label: '跟进记录', formatter: (row) => {
           return h(ElButton, {
             type: 'primary',
+            size: 'small',
             onClick: () => follow(row),
           }, () => '跟进')
         }
       },
-      {
-        prop: 'dormitory_distribution',
-        label: '宿舍分布情况',
-        formatter: (row) => {
-          return h(ElTag, {type: row.dormitory_distribution ? 'success' : 'danger'}, () =>
-              row.dormitory_distribution ? '是' : '否'
-          )
-        }
-      },
-      {
-        prop: 'qucan_station_distribution',
-        label: '校门口取餐点离宿舍情况',
-        formatter: (row) => {
-          return h(ElTag, {type: row.qucan_station_distribution ? 'success' : 'danger'}, () =>
-              row.qucan_station_distribution ? '是' : '否'
-          )
-        }
-      },
+      { prop: 'dormitory_distribution', label: '宿舍分布情况', showOverflowTooltip: true },
+      { prop: 'qucan_station_distribution', label: '校门口取餐点离宿舍情况', showOverflowTooltip: true },
       {prop: 'out_business_description', label: '校外商圈情况', showOverflowTooltip: true},
       {prop: 'memo', label: '备注', showOverflowTooltip: true},
       {
@@ -378,11 +390,6 @@ const {
                 type: 'edit',
                 onClick: () => edit(row.id)
               }),
-              useUserStore().checkAuth(110203) &&
-              h(ArtButtonTable, {
-                type: 'delete',
-                onClick: () => deleteUser(row.id)
-              })
             ])
       }
     ]
@@ -452,9 +459,14 @@ const view = (id: number): void => {
   })
 }
 
+const schoolInfo = ref<Api.School.SchoolInfo>()
+
 const showDrawer = (row: Api.School.SchoolListItem): void => {
   drawer.value = true;
   currentRow.value = row
+  schoolApi.info(row.id).then((res) => {
+    schoolInfo.value = res
+  })
 }
 
 const showContact = (row: Api.School.SchoolListItem):void => {
@@ -466,6 +478,15 @@ const showContact = (row: Api.School.SchoolListItem):void => {
   })
 }
 
+const showCanteen = (row: Api.School.SchoolListItem):void => {
+  router.push({
+    path: RoutesAlias.CanteenList,
+    query: {
+      school_id: row.id
+    }
+  })
+}
+
 /**
  * 删除
  */
@@ -486,6 +507,9 @@ const deleteUser = (id: number): void => {
 </script>
 
 <style lang="scss" scoped>
+.el-button::after {
+  content: none !important;
+}
 .user-page {
   :deep(.user) {
     .avatar {
@@ -508,6 +532,7 @@ const deleteUser = (id: number): void => {
 .detail {
   //padding-top: 20px;
   font-size: 14px;
+  padding-right: 5px;
   .el-col {
     margin-bottom: 30px;
     label {

+ 6 - 3
web/src/views/school/relation/index.vue

@@ -110,7 +110,7 @@
   import { useUserStore } from '@/store/modules/user'
   import {followApi} from "@/api/followApi";
 
-  defineOptions({ name: 'SchoolRelation' })
+  defineOptions({ name: 'schoolRelation' })
 
   type SchoolContactItem = Api.School.SchoolContactItem
   const { list } = schoolRelationApi
@@ -133,8 +133,9 @@
 
   const selectList = ref<Api.Common.SelectRelationInfo[]>([])
   const getSelectList = async () => {
-    const data = await schoolRelationApi.selectList()
-    selectList.value = data
+    await schoolRelationApi.selectList().then(res => {
+      selectList.value = res
+    })
   }
   getSelectList()
 
@@ -179,6 +180,7 @@
         {
           prop: '', label: '跟进记录', formatter: (row) => {
             return h(ElButton, {
+              size: 'small',
               type: 'primary',
               onClick: () => follow(row),
             }, () => '跟进')
@@ -281,6 +283,7 @@
       dialogVisible.value = false
       followDialogVisible.value = false
       drawerUid.value++
+      await getSelectList()
       // 延迟更新 不然数据可能没更新
       setTimeout(() => {
         refreshData()

+ 2 - 2
web/src/views/system/role/index.vue

@@ -162,9 +162,9 @@
       if (node.meta && node.meta.authList && node.meta.authList.length) {
         const authNodes = node.meta.authList.map((auth: any) => ({
           id: auth.id || false,
-          name: `${node.name}_${auth.authMark}`,
+          name: `${node.name}_${auth.id}`,
           label: auth.title,
-          authMark: auth.authMark,
+          authMark: auth.id,
           isAuth: true,
           checked: auth.checked || false
         }))