浏览代码

fix:更近记录编辑和显示

lizhi 3 月之前
父节点
当前提交
3414bfe9e3

+ 1 - 1
protected/components/Controller.php

@@ -66,7 +66,7 @@ class Controller extends CController
 		$controller = Yii::app()->controller->id;
 		$action = $this->getAction()->getId();
         $path = strtolower($controller . '/'. $action);
-		if( !in_array($controller, ['common', 'site'])
+		if( !in_array($controller, ['site'])
             &&!in_array($path, LewaimaiAdminPingtaiAuth::$noLoginRouters)
             && Yii::app()->user->isGuest
         ){

+ 3 - 3
protected/components/DB.php

@@ -185,7 +185,7 @@ class DB
 
     public static function formTableName($tableName)
     {
-        return 'wx_' . trim($tableName, 'wx_');
+        return 'wx_' . str_replace('wx_', '', $tableName);
     }
 
 
@@ -461,7 +461,7 @@ class DB
         $retData['records'] = $build->queryAll() ?: [];
 
         // debug
-        if ($criteria->getDebugMode()) {
+        if ($criteria->getDebugMode() || LWM_ENV != 'prod') {
             Logger::info(
                 json_encode(
                     [
@@ -501,7 +501,7 @@ class DB
         !empty($criteria->order) && $build->order($criteria->order);
 
         // debug
-        if ($criteria->getDebugMode()) {
+        if ($criteria->getDebugMode() || LWM_ENV != 'prod') {
             Logger::info(
                 json_encode(
                     [

+ 52 - 45
protected/controllers/CommonController.php

@@ -1,62 +1,69 @@
 <?php
+
+/**
+ * 只需要登入,无需检测权限的公共方法
+ * 前端直接请求的话必须添加header  Authorization: userStore.accessToken
+ */
 class CommonController extends Controller
 {
-    /**
-     * 发送验证码
-     */
-    public function actionSendCode()
+	/**
+	 *  图片上传
+     *  不同类型放到不同目录,返回格式也会不同
+	 */
+	public function actionUploadImg()
     {
-        $phone = Helper::getPostString('phone', '');
-        if (!Helper::isPhone($phone)) {
-            Helper::error('手机号码格式错误');
+        $upType = '';
+        if (!empty($_FILES['follow'])) {
+            $upType = 'follow';
+            $upArr = $_FILES['follow'];
+        } elseif (!empty($_FILES['editor'])) {
+            $upType = 'editor';
+            $upArr = $_FILES['editor'];
+        } elseif (!empty($_FILES['avatar'])) {
+            $upType = 'avatar';
+            $upArr = $_FILES['avatar'];
+        } else {
+            Helper::error('上传有误');
         }
-
-        // 验证码发送限制
-        Helper::dealCommonResult(Helper::limitSmsSend(10, $phone, 5), false);
-
-        if (!DB::getScalerWithCriteria('useradmin', DbCriteria::simpleCompare(['phone' => $phone])->setSelect('id'))) {
-            Helper::error('该手机号用户不存在');
+        $type = strtolower($upArr['type']);
+        if (!Helper::hasAnyString($type, ['png', 'jpeg', 'jpg'])) {
+            Helper::error('图片格式不正确 ' . $type);
         }
+        if ($upArr['size'] > 1024 * 1024 * 3) {
+            Helper::error('图片大小不能超过3M');
+        }
+        $ext = strtolower(pathinfo($upArr['name'], PATHINFO_EXTENSION));
+        $upPath = "zqcrm/{$upType}/" . date('Ymd') . '/' . Helper::getRandomString(16) . '.' . $ext;
+        Helper::imageUpload($upArr['tmp_name'], $upPath);
+        if ($upType == 'editor') {
+            exit(json_encode([
+                'errno' => 0,
+                'data' => [
+                    'url' => Helper::getImageUrl($upPath),
+                ],
+            ]));
+        } else {
+            Helper::ok(['name' => $upPath, 'url' => Helper::getImageUrl($upPath)]);
+        }
+	}
 
-        $code = (string)random_int(100000,999999);
-        RedisInstance::getInstance()->set('user_code:'.$phone, $code, 600);
-        // 发送短信
-        Helper::dealCommonResult(SMS::getInstance()->send($phone, '2094847', [$code]));
-    }
-
-    /**
-     * 找回密码
-     */
-    public function actionSetPassword()
+    public function actionDeleteImg()
     {
-        $phone = Helper::getPostString('phone');
-        $code = Helper::getPostString('code');
-        $password = Helper::getPostString('password');
-        if (!Helper::isPhone($phone)) {
-            Helper::error('手机号码格式错误');
-        }
-        if (!$code || !$password) {
+        $path = Helper::getPostString('path');
+        if (empty($path)) {
             Helper::error('参数错误');
         }
-        if (RedisInstance::getInstance()->get('user_code:'.$phone) != $code) {
-            Helper::error('验证码错误');
-        }
-        $id = DB::getScalerWithCriteria('useradmin', DbCriteria::simpleCompare(['phone' => $phone])->setSelect('id'));
-        if (!$id) {
-            Helper::error('该手机号用户不存在');
-        }
-        DB::updateById('useradmin', ['password' => md5($password)], $id);
-        Helper::ok();
+        Helper::dealCommonResult(Helper::imageDelete($path));
     }
 
-	/**
-	 *  图片上传
-	 */
-	public function actionUploadImg()
+    /**
+     * Logs out the current user and redirect to homepage.
+     */
+    public function actionLogout()
     {
+        Yii::app()->user->logout();
         Helper::ok();
-	}
-
+    }
 
     /*******************************   测试相关代码   ***************************************/
     public function actionPhp()

+ 153 - 0
protected/controllers/FollowController.php

@@ -0,0 +1,153 @@
+<?php
+
+class FollowController extends Controller
+{
+    const TYPE_TABLE_MAP = [
+        'school' => [
+            'table' => 'school_follow',
+            'first_id' => 'school_id',
+            'second_id' => 'contact_id',
+            'table1' => 'school',
+            'table2' => 'school_contact',
+        ],
+        'canteen' => [
+            'table' => 'canteen_follow',
+            'first_id' => 'school_id',
+            'second_id' => 'canteen_id',
+            'table1' => 'school',
+            'table2' => 'canteen_id',
+        ],
+        'company' => [
+            'table' => 'company_follow',
+            'first_id' => 'company_id',
+            'second_id' => 'contact_id',
+            'table1' => 'company',
+            'table2' => 'company_contact',
+        ]
+    ];
+
+    public array $tableArr = [];
+    public string $type = '';
+
+    public function beforeAction($action): bool
+    {
+        if (!parent::beforeAction($action)) {
+            return false;
+        }
+        $this->type = Helper::getPostString('type');
+        if (!$this->type || !isset(self::TYPE_TABLE_MAP[$this->type])) {
+            Helper::error('类型错误');
+        }
+        $this->tableArr = self::TYPE_TABLE_MAP[$this->type];
+        return true;
+    }
+
+    public function actionAdd()
+    {
+        $firstId = Helper::getPostInt('first_id');
+        $secondId = Helper::getPostInt('second_id');
+        $chatImgs = Helper::getArrParam($_POST, 'chat_imgs', Helper::PARAM_KEY_TYPE['array_string']);
+        $detail = Helper::getPostString('detail');
+        if (empty($firstId) || empty($secondId) || empty($detail) || empty($chatImgs)) {
+            Helper::error('参数错误');
+        }
+        DB::addData($this->tableArr['table'], [
+            $this->tableArr['first_id'] => $firstId,
+            $this->tableArr['second_id'] => $secondId,
+            'detail' => $detail,
+            'chat_imgs' => implode(',', $chatImgs),
+            'user_id' => Yii::app()->user->_id,
+        ]);
+        Helper::ok();
+    }
+
+    public function actionAll()
+    {
+        $firstId = Helper::getPostInt('first_id');
+        if ($firstId <= 0) {
+            Helper::error('参数错误');
+        }
+        $filter = [
+            $this->tableArr['first_id'] => $firstId,
+            $this->tableArr['second_id'] => Helper::getPostInt('second_id') ? : null,
+        ];
+        $criteria = DbCriteria::simpleCompare($filter)->setOrder('id desc');
+        $data = DB::getListWithCriteria($this->tableArr['table'], $criteria);
+        $data['records'] = $this->formatFollowList($data['records']);
+        Helper::ok($data['records']);
+    }
+
+    public function actionInfo()
+    {
+        $id = Helper::getPostInt('id');
+        if (empty($id)) {
+            Helper::error('参数错误');
+        }
+        $data = DB::getInfoById($this->tableArr['table'], $id);
+        $data = $this->formatFollowList([$data])[0];
+        Helper::ok($data);
+    }
+
+    public function actionList()
+    {
+        $filter = [
+            $this->tableArr['first_id'] => Helper::getPostInt('second_id'),
+            $this->tableArr['second_id'] => Helper::getPostInt('second_id'),
+        ];
+        $criteria = DbCriteria::simpleCompareWithPage($filter)->setOrder('id desc');
+        $data = DB::getListWithCriteria($this->tableArr['table'], $criteria);
+        $data['records'] = $this->formatFollowList($data['records']);
+        Helper::ok($data);
+    }
+
+    public function formatFollowList($list)
+    {
+        if (empty($list)) {
+            return [];
+        }
+        // 跟进人员
+        $userIds = array_unique(array_filter(array_column($list, 'user_id')));
+        $users = [];
+        if ($userIds) {
+            $cri = DbCriteria::simpleCompare(['id' => $userIds])->setSelect('id,username,avatar');
+            $users = Helper::arrayColumn(DB::getListWithCriteria('useradmin', $cri), null, 'id');
+        }
+        $field1 = $this->tableArr['first_id'];
+        $field2 = $this->tableArr['second_id'];
+        // 校园/公司
+        $firstIds = array_unique(array_filter(array_column($list, $field1)));
+        $firsts = [];
+        if ($firstIds) {
+            $cri = DbCriteria::simpleCompare(['id' => $firstIds])->setSelect('id,name');
+            $firsts = Helper::arrayColumn(DB::getListWithCriteria($this->tableArr['table1'], $cri), 'name', 'id');
+        }
+        // 关系人
+        $secondIds = array_unique(array_filter(array_column($list, $field2)));
+        $seconds = [];
+        if ($secondIds) {
+            $cri = DbCriteria::simpleCompare(['id' => $secondIds])->setSelect('id,name,position,weixin,phone');
+            if ($this->type == 'canteen') {
+                $cri = DbCriteria::simpleCompare(['t.id' => $secondIds])
+                    ->setAlias('t')
+                    ->setSelect('t.id,c.name,c.position,c.weixin,c.phone')
+                    ->setJoin('left join wx_canteen_contact c on c.canteen_id = t.id');
+            }
+            $seconds = Helper::arrayColumn(DB::getListWithCriteria($this->tableArr['table2'], $cri), null, 'id');
+        }
+        foreach ($list as &$item) {
+            $uid = $item['user_id'];
+            $f1 = $item[$field1];
+            $f2 = $item[$field2];
+            $item['chat_imgs'] = Helper::formatImgFiled($item['chat_imgs']);
+            $item['create_date'] = date('Y-m-d H:i', strtotime($item['create_date']));
+            $item['user_name'] = $users[$uid]['username'] ?? '';
+            $item['avatar'] = $users[$uid]['avatar'] ? Helper::getImageUrl($users[$uid]['avatar']) : '';
+            $item['first_name'] = $firsts[$f1] ?? '';
+            $item['second_name'] = $seconds[$f2]['name'] ?? '';
+            $item['position'] = $seconds[$f2]['position'] ?? '';
+            $item['weixin'] = $seconds[$f2]['weixin'] ?? '';
+            $item['phone'] = $seconds[$f2]['phone'] ?? '';
+        }
+        return $list;
+    }
+}

+ 6 - 5
protected/controllers/SchoolRelationController.php

@@ -46,12 +46,13 @@ class SchoolRelationController extends Controller
      */
     public function actionGetSelectList()
     {
-        $cri = DbCriteria::simpleCompare([])->setSelect('id, name');
-        $schools = Helper::arrayColumn(DB::getListWithCriteria(SchoolController::$table, $cri), null, 'id');
+        $cri = DbCriteria::simpleCompare(['is_del' => 0])->setSelect('id, name');
+        $schools = Helper::arrayColumn(DB::getListWithCriteria('school', $cri), null, 'id');
         if (empty($schools)) {
-            Helper::ok([]);
+            Helper::ok();
         }
-        $relations = DB::getListWithCriteria($this->table, $cri);
+        $cri1 = DbCriteria::simpleCompare(['is_del' => 0])->setSelect('id, name, school_id');
+        $relations = DB::getListWithCriteria($this->table, $cri1);
         foreach ($relations['records'] as $relation) {
             $sid = $relation['school_id'];
             if (!isset($schools[$sid])) {
@@ -65,7 +66,7 @@ class SchoolRelationController extends Controller
                 'name' => $relation['name'],
             ];
         }
-        Helper::ok($schools);
+        Helper::ok(array_values($schools));
     }
 
     public function actionDelete()

+ 50 - 29
protected/controllers/SiteController.php

@@ -1,28 +1,10 @@
 <?php
 
+/**
+ * 这个方法不会检测登入和权限,如果需要登入后操作的请放到 CommonController
+ */
 class SiteController extends Controller
 {
-	public $layout='//layouts/site_index';
-	
-	/**
-	 * Declares class-based actions.
-	 */
-	public function actions()
-	{
-		return array(
-			// captcha action renders the CAPTCHA image displayed on the contact page
-			'captcha'=>array(
-				'class'=>'CCaptchaAction',
-				'backColor'=>0xFFFFFF,
-			),
-			// page action renders "static" pages stored under 'protected/views/site/pages'
-			// They can be accessed via: index.php?r=site/page&view=FileName
-			'page'=>array(
-				'class'=>'CViewAction',
-			),
-		);
-	}
-
 	/**
 	 * This is the action to handle external exceptions.
 	 */
@@ -48,12 +30,51 @@ class SiteController extends Controller
         Helper::error('参数错误');
 	}
 
-	/**
-	 * Logs out the current user and redirect to homepage.
-	 */
-	public function actionLogout()
-	{
-		Yii::app()->user->logout();
-		Helper::ok();
-	}
+    /**
+     * 发送验证码
+     */
+    public function actionSendCode()
+    {
+        $phone = Helper::getPostString('phone', '');
+        if (!Helper::isPhone($phone)) {
+            Helper::error('手机号码格式错误');
+        }
+
+        // 验证码发送限制
+        Helper::dealCommonResult(Helper::limitSmsSend(10, $phone, 5), false);
+
+        if (!DB::getScalerWithCriteria('useradmin', DbCriteria::simpleCompare(['phone' => $phone])->setSelect('id'))) {
+            Helper::error('该手机号用户不存在');
+        }
+
+        $code = (string)random_int(100000,999999);
+        RedisInstance::getInstance()->set('user_code:'.$phone, $code, 600);
+        // 发送短信
+        Helper::dealCommonResult(SMS::getInstance()->send($phone, '2094847', [$code]));
+    }
+
+    /**
+     * 找回密码
+     */
+    public function actionSetPassword()
+    {
+        $phone = Helper::getPostString('phone');
+        $code = Helper::getPostString('code');
+        $password = Helper::getPostString('password');
+        if (!Helper::isPhone($phone)) {
+            Helper::error('手机号码格式错误');
+        }
+        if (!$code || !$password) {
+            Helper::error('参数错误');
+        }
+        if (RedisInstance::getInstance()->get('user_code:'.$phone) != $code) {
+            Helper::error('验证码错误');
+        }
+        $id = DB::getScalerWithCriteria('useradmin', DbCriteria::simpleCompare(['phone' => $phone])->setSelect('id'));
+        if (!$id) {
+            Helper::error('该手机号用户不存在');
+        }
+        DB::updateById('useradmin', ['password' => md5($password)], $id);
+        Helper::ok();
+    }
 }

+ 75 - 25
protected/include/Helper.php

@@ -62,6 +62,22 @@ class Helper
         return $imageUrl;
     }
 
+    /**
+     * 格式化处理 imgs 字段
+     * @param  string|null  $data
+     * @param  string  $separator
+     * @return array
+     */
+    public static function formatImgFiled(?string $data, string $separator = ','): array
+    {
+        if (!$data) {
+            return [];
+        }
+        $data = array_filter(explode($separator, $data));
+        return array_map(function ($img) {
+            return Helper::getImageUrl($img);
+        }, $data);
+    }
 
     public static function time_tran($time)
     {
@@ -346,37 +362,17 @@ class Helper
         \Yii::app()->end();
     }
 
-    public static function imageUpload($imagepath, $uploadpath)
+    public static function error($msg, $code = 400, $data = [])
     {
-        try {
-            // 腾讯云
-            $region = 'ap-shanghai'; // 用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket
-            $cosClient = new \Qcloud\Cos\Client(
-                array(
-                    'region' => $region,
-                    // 'scheme' => 'https', //协议头部,默认为 http
-                    'credentials'=> array(
-                        'secretId'  => 'AKIDviIeLyQluythLAykSJ6oXH91upgR6iT8' ,
-                        'secretKey' => 'tobSCsOn7yc6ToBSWegaM9rVGyiR6f95'
-                    )
-                )
-            );
-            $bucket = 'lewaimai-image-1251685925';
-            $file = fopen($imagepath, "rb");
-            if ($file) {
-                $result = $cosClient->putObject(['Bucket' => $bucket, 'Key' => $uploadpath, 'Body' => $file]);
-            }
-        } catch(Exception $e) {
-            throw $e;
-        }
+        self::toJson($code, $data, $msg);
     }
 
-    public static function error($msg, $code = 400, $data = [])
+    public static function ok($data = [], $msg = '操作成功', $code = 200)
     {
         self::toJson($code, $data, $msg);
     }
 
-    public static function ok($data = [], $msg = '操作成功', $code = 200)
+    public static function success($data = [], $msg = '操作成功', $code = 200)
     {
         self::toJson($code, $data, $msg);
     }
@@ -402,6 +398,16 @@ class Helper
         }
     }
 
+    public static function getRandomString($length = 20)
+    {
+        $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        $str = "";
+        for ($i = 0; $i < $length; $i++) {
+            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
+        }
+        return $str;
+    }
+
     //php防注入和XSS攻击通用过滤
     public static function safeFilter (&$arr)
     {
@@ -524,7 +530,7 @@ class Helper
      */
     public static function arrayColumn($array, $column_key, $index_key = null): array
     {
-        return $array && $array['datas'] ? array_column($array['datas'], $column_key, $index_key) : [];
+        return $array && $array['records'] ? array_column($array['records'], $column_key, $index_key) : [];
     }
 
     /**
@@ -576,4 +582,48 @@ class Helper
         }
         return $ip ? $ip : '127.0.0.1';
     }
+
+    public static function imageUpload($imagepath, $uploadpath)
+    {
+        try {
+            $cosClient = new \Qcloud\Cos\Client(
+                array(
+                    'region' => 'ap-shanghai',
+                    // 'scheme' => 'https', //协议头部,默认为 http
+                    'credentials'=> array(
+                        'secretId'  => 'AKIDviIeLyQluythLAykSJ6oXH91upgR6iT8' ,
+                        'secretKey' => 'tobSCsOn7yc6ToBSWegaM9rVGyiR6f95'
+                    )
+                )
+            );
+            $file = fopen($imagepath, "rb");
+            if (!$file) {
+                throw new Exception("读取图片失败");
+            }
+            $cosClient->putObject(['Bucket' => 'lewaimai-image-1251685925', 'Key' => $uploadpath, 'Body' => $file]);
+            return Helper::commonReturn();
+        } catch(Exception $e) {
+            return Helper::commonError($e->getMessage());
+        }
+    }
+
+    public static function imageDelete($path)
+    {
+        try {
+            $cosClient = new \Qcloud\Cos\Client(
+                array(
+                    'region' => 'ap-shanghai',
+                    // 'scheme' => 'https', //协议头部,默认为 http
+                    'credentials'=> array(
+                        'secretId'  => 'AKIDviIeLyQluythLAykSJ6oXH91upgR6iT8' ,
+                        'secretKey' => 'tobSCsOn7yc6ToBSWegaM9rVGyiR6f95'
+                    )
+                )
+            );
+            $cosClient->deleteObject(['Bucket' => 'lewaimai-image-1251685925', 'Key' => $path]);
+            return Helper::commonReturn();
+        } catch(Exception $e) {
+            return Helper::commonError($e->getMessage());
+        }
+    }
 }

+ 2 - 2
script/upgrade/1.0.0.sql

@@ -41,10 +41,10 @@ CREATE TABLE `wx_school` (
   `is_eleme_out_school` TINYINT(4) NOT NULL default 0 COMMENT '是否有饿了么校外站 0-无 1-有',
   `is_meituan_in_school` TINYINT(4) NOT NULL default 0 COMMENT '是否有美团校内站 0-无 1-有',
   `is_meituan_out_school` TINYINT(4) NOT NULL default 0 COMMENT '是否有美团校外站 0-无 1-有',
-  `can_go_upstairs` TINYINT(4) NOT NULL default 0 COMMENT '是否能上楼 0-能 1-不能',
+  `can_go_upstairs` TINYINT(4) NOT NULL default 0 COMMENT '是否能上楼 1-能 0-不能',
   `is_cooperate` TINYINT(4) NOT NULL default 0 COMMENT '是否合作 0-否 1-是',
   `is_del` TINYINT(4) NOT NULL default 0 COMMENT '是否删除 0-否 1-是',
-  `can_ride` TINYINT(4) NOT NULL default 0 COMMENT '是否允许骑电动车 0-能 1-不能',
+  `can_ride` TINYINT(4) NOT NULL default 0 COMMENT '是否允许骑电动车 1-能 0-不能',
   `dormitory_distribution` VARCHAR(1000) default '' COMMENT '宿舍分布情况',
   `qucan_station_distribution` VARCHAR(1000) default '' COMMENT '校门口取餐点离宿舍情况',
   `out_business_description` VARCHAR(1000) default '' COMMENT '校外商圈情况',

+ 3 - 0
web/.env

@@ -12,6 +12,9 @@ VITE_BASE_URL = /
 # API 地址前缀
 VITE_API_URL =
 
+# 上传地址
+VITE_UPLOAD_URL = 'common/uploadImg'
+
 # 权限模式( frontend(前端) | backend(后端) )
 VITE_ACCESS_MODE = backend
 

+ 41 - 0
web/src/api/commonApi.ts

@@ -0,0 +1,41 @@
+import request from '@/utils/http'
+
+export class commonApi {
+
+  // 删除图片
+  static delImg(path:string) {
+    const params = {path}
+    return request.post<Api.Http.BaseResponse>({
+      url: 'common/deleteImg',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 登出
+  static logout() {
+    return request.post<Api.Http.BaseResponse>({
+      url: 'common/logout',
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 发送验证码
+  static sendCode(phone: string) {
+    const params = { phone }
+    return request.post<Api.Auth.LoginResponse>({
+      url: 'site/sendCode',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 修改密码
+  static findPassword(params: Form.FindPassword) {
+    return request.post<Api.Auth.LoginResponse>({
+      url: 'site/setPassword',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+}

+ 40 - 0
web/src/api/followApi.ts

@@ -0,0 +1,40 @@
+import request from '@/utils/http'
+
+export class followApi {
+
+  // 跟进详情
+  static info(id:number, type:Api.FollowTye) {
+    return request.post<Api.Follow.FollowInfo>({
+      url: 'follow/info',
+      params:{id, type}
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 添加跟进
+  static follow(params:Form.Follow) {
+    return request.post<Api.Http.BaseResponse>({
+      url: 'follow/add',
+      params
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 跟进列表
+  static list(type:Api.FollowTye, first_id?:number,  second_id?:number) {
+    return request.post<Api.Follow.FollowListData>({
+      url: 'follow/list',
+      params:{first_id, type, second_id}
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
+  // 获取关联的所以跟进
+  static all(type:Api.FollowTye, first_id:number,  second_id?:number) {
+    return request.post<Api.Follow.FollowInfo[]>({
+      url: 'follow/all',
+      params:{first_id, type, second_id}
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+}

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

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

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

@@ -10,25 +10,6 @@ export class UserService {
     })
   }
 
-  // 发送验证码
-  static sendCode(phone: string) {
-    const params = { phone }
-    return request.post<Api.Auth.LoginResponse>({
-      url: 'common/sendCode',
-      params
-      // showErrorMessage: false // 不显示错误消息
-    })
-  }
-
-  // 修改密码
-  static findPassword(params: Form.FindPassword) {
-    return request.post<Api.Auth.LoginResponse>({
-      url: 'common/setPassword',
-      params
-      // showErrorMessage: false // 不显示错误消息
-    })
-  }
-
   // 获取用户信息
   static getUserInfo() {
     return request.post<Api.User.UserInfo>({

+ 2 - 2
web/src/components/core/forms/art-wang-editor/index.vue

@@ -67,7 +67,7 @@
   const DEFAULT_UPLOAD_CONFIG = {
     maxFileSize: 3 * 1024 * 1024, // 3MB
     maxNumberOfFiles: 10,
-    fieldName: 'file',
+    fieldName: 'editor',
     allowedFileTypes: ['image/*']
   } as const
 
@@ -100,7 +100,7 @@
   // 计算属性:上传服务器地址
   const uploadServer = computed(
     () =>
-      props.uploadConfig?.server || `${import.meta.env.VITE_API_URL}/api/common/upload/wangeditor`
+      props.uploadConfig?.server || `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_UPLOAD_URL}`
   )
 
   // 合并上传配置

+ 1 - 1
web/src/components/core/forms/art-wang-editor/style.scss

@@ -2,7 +2,7 @@ $box-radius: calc(var(--custom-radius) / 3 + 2px);
 
 /* 编辑器容器 */
 .editor-wrapper {
-  z-index: 5000;
+  z-index: 100;
   width: 100%;
   height: 100%;
   border: 1px solid rgba(var(--art-gray-300-rgb), 0.8);

+ 2 - 0
web/src/components/core/layouts/art-header-bar/index.vue

@@ -205,6 +205,7 @@
   import { themeAnimation } from '@/utils/theme/animation'
   import { useCommon } from '@/composables/useCommon'
   import { useHeaderBar } from '@/composables/useHeaderBar'
+  import {commonApi} from "@/api/commonApi";
 
   defineOptions({ name: 'ArtHeaderBar' })
 
@@ -340,6 +341,7 @@
         cancelButtonText: t('common.cancel'),
         customClass: 'login-out-dialog'
       }).then(() => {
+        commonApi.logout();
         userStore.logOut()
       })
     }, 200)

+ 2 - 0
web/src/components/core/layouts/art-screen-lock/index.vue

@@ -92,6 +92,7 @@
   import CryptoJS from 'crypto-js'
   import { useUserStore } from '@/store/modules/user'
   import { mittBus } from '@/utils/sys'
+  import {commonApi} from "@/api/commonApi";
 
   // 国际化
   const { t } = useI18n()
@@ -370,6 +371,7 @@
   }
 
   const toLogin = () => {
+    commonApi.logout()
     userStore.logOut()
   }
 

+ 226 - 0
web/src/components/custom/FollowDialog.vue

@@ -0,0 +1,226 @@
+<template>
+  <ElDialog
+    v-model="dialogVisible"
+    title="新增跟进记录"
+    width="50%"
+    align-center
+  >
+    <ElForm ref="formRef" :model="formData" :rules="rules" label-width="auto">
+      <ElFormItem :label="selectLabelArr[0]" prop="first_id">
+        <ElSelect v-model="formData.first_id">
+          <ElOption
+            v-for="item in selectList"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          />
+        </ElSelect>
+      </ElFormItem>
+      <ElFormItem :label="selectLabelArr[1]" prop="second_id">
+        <ElSelect v-model="formData.second_id">
+          <ElOption
+              v-for="item in secondSelectList"
+              :key="item.id"
+              :value="item.id"
+              :label="item.name"
+          />
+        </ElSelect>
+      </ElFormItem>
+      <ElFormItem label="微信聊天记录" prop="chat_imgs">
+        <el-upload
+            :file-list="fileList"
+            name="follow"
+            :action="uploadServer"
+            list-type="picture-card"
+            :limit="12"
+            :on-preview="handlePictureCardPreview"
+            :on-remove="handleRemove"
+            :before-upload="beforeUpload"
+            :on-success="handleSuccess"
+            :headers="{Authorization: useUserStore().accessToken}"
+            drag
+        >
+          <i class="el-icon-plus"></i>
+          <el-icon><Plus /></el-icon>
+          <template #tip>
+            <div style="color:red">支持拖拽上传,限 jpg、png、jpeg 图片,最大{{maxFileSizeMB}}M,最多12张</div>
+          </template>
+        </el-upload>
+        <!--    图片预览    -->
+        <el-image-viewer
+            v-if="picDialogVisible"
+            :url-list="[dialogImageUrl]"
+            show-progress
+            :initial-index="0"
+            @close="picDialogVisible = false"
+        />
+      </ElFormItem>
+      <ElFormItem label="跟进详情" prop="detail">
+        <!-- 富文本编辑器 -->
+        <ArtWangEditor class="el-top" v-model="formData.detail" height="250px"/>
+      </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 {computed, ref} from 'vue'
+  import { useUserStore } from '@/store/modules/user'
+  import { Plus } from '@element-plus/icons-vue'
+  import {ElMessage, ElMessageBox, UploadProps, UploadUserFile} from 'element-plus'
+  import type { FormInstance, FormRules } from 'element-plus'
+  import {commonApi} from "@/api/commonApi";
+
+  interface Props {
+    visible: boolean
+    type:Api.FollowTye
+    userData?: any
+    first_id?: number
+    second_id?: number
+    selectList: Api.Common.SelectRelationInfo[]
+  }
+
+  interface Emits {
+    (e: 'update:visible', value: boolean): void
+    (e: 'submit'): void
+  }
+
+  const props = defineProps<Props>()
+  const emit = defineEmits<Emits>()
+  const uploadServer = computed(() => import.meta.env.VITE_API_URL + import.meta.env.VITE_UPLOAD_URL)
+
+  const fileList = ref<UploadUserFile[]>([])
+
+  const dialogImageUrl = ref('')
+  const picDialogVisible = ref(false)
+
+  // 对话框显示控制
+  const dialogVisible = computed({
+    get: () => props.visible,
+    set: (value) => emit('update:visible', value)
+  })
+
+  const selectLabelArr = computed(() => {
+    return {
+      'school' : ['校园(园区)', '校方关系人'],
+      'canteen' : ['校园(园区)', '食堂'],
+      'company' : ['餐饮公司', '餐饮公司方关系人'],
+    }[props.type]
+  })
+
+  // 表单实例
+  const formRef = ref<FormInstance>()
+
+  let defalutValue = {
+    first_id: null,
+    second_id: null,
+    chat_imgs: [],
+    detail: '',
+    type: props.type
+  }
+  // 表单数据
+  const formData = reactive<Form.Follow>({...defalutValue})
+
+  // 表单验证规则
+  const rules: FormRules = {
+    first_id: [
+      { required: true, message: '请选择' + selectLabelArr.value[0], trigger: 'blur' },
+    ],
+    second_id: [
+      { required: true, message: '请选择' + selectLabelArr.value[1], trigger: 'blur' },
+    ],
+    detail: [
+      { required: true, message: '请填写详情', trigger: 'blur' },
+    ],
+  }
+
+  const handleRemove: UploadProps['onRemove'] = (uploadFile) => {
+    let upName = uploadFile.response ? uploadFile.response.data.name : uploadFile.name
+    commonApi.delImg(upName)
+    for (let i = 0; i < formData.chat_imgs.length; i++) {
+      if (formData.chat_imgs[i] === upName) {
+        formData.chat_imgs.splice(i, 1)
+        break
+      }
+    }
+
+  }
+  const maxFileSizeMB = 3
+
+  const beforeUpload: UploadProps['onChange'] = (uploadFile) => {
+    if (uploadFile.size && uploadFile.size > 1024 * 1024 * maxFileSizeMB) {
+      ElMessage.error('上传图片大小不能超过2M')
+      return false
+    }
+    if (!uploadFile.type.startsWith('image/')) {
+      ElMessage.error('只能上传图片文件')
+      return false
+    }
+    return true
+  }
+
+  const handleSuccess = (response:any, uploadFile: UploadFile) => {
+    if (response.code === 200) {
+      formData.chat_imgs.push(response.data.name)
+      ElMessage.success('上传成功!')
+    } else {
+      fileList.value = fileList.value.filter((file) => file.name !== uploadFile.name)
+      ElMessage.error(response.msg)
+    }
+  }
+
+  const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
+    dialogImageUrl.value = uploadFile.url!
+    picDialogVisible.value = true
+  }
+
+  const secondSelectList = computed(() => {
+    if (formData.first_id) {
+      let tmp = props.selectList.find(item => item.id === formData.first_id)
+      if (tmp && tmp.children) {
+        if (!tmp.children.find(item => item.id === formData.second_id)) {
+          formData.second_id = null
+        }
+        return tmp.children
+      }
+    }
+    formData.second_id = null
+    return []
+  })
+
+  // 统一监听对话框状态变化
+  watch(
+      () => [props.visible, props.type, props.userData],
+      ([visible]) => {
+        if (visible) {
+          formData.first_id = props.first_id || null
+          formData.second_id = props.second_id || null
+        }
+      },
+      { immediate: true }
+  )
+
+  // 提交表单
+  const handleSubmit = async () => {
+    if (!formRef.value) return
+    await formRef.value.validate((valid) => {
+      if (valid) {
+        commonApi.follow(formData).then(() => {
+          ElMessage.success('提交成功')
+          Object.assign(formData, defalutValue)
+          formData.chat_imgs = [];
+          fileList.value = []
+          console.log(`%c formData == `, 'background:#41b883 ; padding:1px; color:#fff', formData);
+          dialogVisible.value = false
+          emit('submit')
+        })
+      }
+    })
+  }
+</script>

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

@@ -18,6 +18,8 @@ declare namespace Api {
     }
   }
 
+  type FollowTye = 'school'|'canteen'|'company'
+
   /** 通用类型 */
   namespace Common {
     /** 分页参数 */
@@ -211,4 +213,28 @@ declare namespace Api {
       total: number
     }
   }
+
+  namespace Follow {
+
+    interface FollowInfo {
+      id: number
+      chat_imgs: string[]
+      detail: string
+      create_date: string
+      first_name: string //
+      second_name: string //
+      user_name: string //
+      phone: string // 手机号,
+      weixin: string // 微信号,
+      position: string // 职位,
+    }
+
+    interface FollowListData {
+      records: FollowInfo[]
+      current: number
+      size: number
+      total: number
+    }
+
+  }
 }

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

@@ -53,4 +53,12 @@ declare namespace Form {
     position: string // 职位,
     memo: string // 备注,
   }
+
+  interface Follow {
+    first_id: ?number,
+    second_id: ?number,
+    chat_imgs: string[],
+    detail: string,
+    type: string
+  }
 }

+ 3 - 3
web/src/utils/http/index.ts

@@ -4,6 +4,7 @@ import { ApiStatus } from './status'
 import { HttpError, handleError, showError } from './error'
 import { $t } from '@/locales'
 import CryptoJS from 'crypto-js'
+import {commonApi} from "@/api/commonApi";
 
 /** 请求配置常量 */
 const REQUEST_TIMEOUT = 15000
@@ -113,9 +114,8 @@ function resetUnauthorizedError() {
 
 /** 退出登录函数 */
 function logOut() {
-  setTimeout(() => {
-    useUserStore().logOut()
-  }, LOGOUT_DELAY)
+  commonApi.logout()
+  useUserStore().logOut()
 }
 
 /** 是否需要重试 */

+ 1 - 1
web/src/views/article/publish/index.vue

@@ -28,7 +28,7 @@
 
         <div class="form-wrap">
           <h2>发布设置</h2>
-          <!-- 图片上传 -->
+          <!-- 图片上传 TODO:封装成组件 -->
           <ElForm>
             <ElFormItem label="封面">
               <div class="el-top upload-container">

+ 3 - 2
web/src/views/auth/forget-password/index.vue

@@ -89,6 +89,7 @@
   import { watch, ref, onMounted, onBeforeUnmount } from 'vue'
   import { FormRules, FormInstance, ElMessage } from 'element-plus'
   import { UserService } from '@/api/usersApi'
+  import {commonApi} from "@/api/commonApi";
 
   defineOptions({ name: 'ForgetPassword' })
 
@@ -178,7 +179,7 @@
 
   const sendCode = async () => {
     if (phonePattern.test(form.phone)) {
-      UserService.sendCode(form.phone)
+      commonApi.sendCode(form.phone)
         .then(() => {
           ElMessage.success('验证码发送成功')
           disable.value = true
@@ -198,7 +199,7 @@
     if (!formEl) return
     await formEl.validate((valid, fields) => {
       if (valid) {
-        UserService.findPassword(form)
+        commonApi.findPassword(form)
           .then(() => {
             ElMessage.success('密码重置成功')
             setTimeout(() => {

+ 142 - 11
web/src/views/school/list/index.vue

@@ -67,12 +67,14 @@
 
     <el-drawer
         v-model="drawer"
-        :title="currentRow.name"
         direction="rtl"
-        size="60%"
+        size="70%"
     >
+      <template #title>
+        <span style="font-size: 20px; font-weight: bold;">{{ currentRow.name }}</span>
+      </template>
       <ElRow>
-        <ElCol :sm="12">
+        <ElCol :sm="10">
           <ElRow class="detail">
 
             <el-col :sm="12">
@@ -138,12 +140,64 @@
             </el-col>
           </ElRow>
         </ElCol>
-        <ElCol :sm="12">
-          <ElImage src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"/>
+        <ElCol :sm="14" style="max-height: 100%">
+            <ElCard
+                shadow="never"
+                v-for="(activity, index) in timelineData"
+                :key="index"
+                style="margin-bottom: 10px"
+            >
+              <template #header><span style="font-weight: 400; color: #f59a23;">{{ activity.date.replaceAll('-', ' / ') }}</span> </template>
+                <ElCard shadow="never" v-for="(follow, index) in activity.list" :key="index" style="margin-bottom: 5px" class="follow-div">
+                  <el-row>
+                    <el-col :span="3">
+                      <el-avatar style="background-color: #f59a23;color: white; margin-bottom: 10px">{{ parseInt(activity.date.split('-')[2]) }}日 </el-avatar>
+                      <br/>
+                      {{ follow.create_date.split(' ')[1] }}
+                    </el-col>
+                    <el-col :span="21">
+                      <el-row>
+                        <el-avatar :src="follow.avatar"/> <span style="top: 13px; left:10px; position: relative;" >{{ follow.user_name }}</span>
+                        <el-col :span="24">
+                          <label>关系人:</label> <span>{{ follow.second_name }}</span>
+                        </el-col>
+                        <el-col :span="24">
+                          <label style="position: relative;top: -10px;">聊天记录:</label>
+                          <el-image
+                              v-if="follow.chat_imgs.length"
+                              ref="imageRef"
+                              style="width: 30px; height: 30px"
+                              :src="follow.chat_imgs[0]"
+                              show-progress
+                              :preview-src-list="follow.chat_imgs"
+                              fit="cover"
+                          />
+                        </el-col>
+                        <el-col :sm="2">
+                          <label>详情:</label>
+                        </el-col>
+                        <el-col :sm="22">
+                          <ElCard shadow="never" v-html="follow.detail" style="padding: 10px;max-height: 200px;overflow-y: scroll"/>
+                        </el-col>
+                      </el-row>
+                    </el-col>
+                  </el-row>
+                </ElCard>
+            </ElCard>
         </ElCol>
       </ElRow>
     </el-drawer>
   </div>
+
+  <!--   跟进弹窗   -->
+  <FollowDialog
+      v-model:visible="followDialogVisible"
+      :user-data="currentRow"
+      :type="'school'"
+      :first_id="currentRow.id"
+      :selectList="selectList"
+      @submit="handleDialogSubmit"
+  />
 </template>
 
 <script setup lang="ts">
@@ -156,11 +210,14 @@ 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 {schoolRelationApi} from "@/api/schoolRelationApi";
 
 defineOptions({name: 'User'})
 
 const {list} = schoolApi
 const drawer = ref(false)
+const followDialogVisible = ref(false)
 
 // 搜索表单
 const searchForm = ref({
@@ -171,6 +228,25 @@ const searchForm = ref({
 
 const currentRow = ref<Partial<Api.School.SchoolListItem>>({})
 
+const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+const getSelectList = async () => {
+  const data = await schoolRelationApi.selectList()
+  selectList.value = data
+}
+getSelectList()
+
+/**
+ * 处理弹窗提交事件
+ */
+const handleDialogSubmit = async () => {
+  followDialogVisible.value = false
+  currentRow.value = {}
+  // // 延迟更新 不然数据可能没更新
+  // setTimeout(() => {
+  //   refreshData()
+  // }, 1000)
+}
+
 const {
   columns,
   columnChecks,
@@ -266,7 +342,7 @@ const {
         label: '是否能上楼',
         formatter: (row) => {
           return h(ElTag, {type: row.can_go_upstairs ? 'success' : 'danger'}, () =>
-              row.can_go_upstairs ? '能' : '能'
+              row.can_go_upstairs ? '能' : '能'
           )
         }
       },
@@ -275,11 +351,19 @@ const {
         label: '是否允许骑电动车',
         formatter: (row) => {
           return h(ElTag, {type: row.can_ride ? 'success' : 'danger'}, () =>
-              row.can_ride ? '能' : '能'
+              row.can_ride ? '能' : '能'
           )
         }
       },
       {
+        prop: '', label: '跟进记录', formatter: (row) => {
+          return h(ElButton, {
+            type: 'primary',
+            onClick: () => follow(row),
+          }, () => '跟进')
+        }
+      },
+      {
         prop: 'dormitory_distribution',
         label: '宿舍分布情况',
         formatter: (row) => {
@@ -372,6 +456,16 @@ const edit = (id?: number): void => {
 }
 
 /**
+ * 显示跟进弹窗
+ */
+const follow = (row: Api.School.SchoolListItem): void => {
+  currentRow.value = row || {}
+  nextTick(() => {
+    followDialogVisible.value = true
+  })
+}
+
+/**
  * 查看
  */
 const view = (id: number): void => {
@@ -383,9 +477,40 @@ const view = (id: number): void => {
   })
 }
 
+interface timeLineItem {
+  date: string
+  color: string
+  list: Api.Follow.FollowInfo[]
+}
+const timelineData = ref<timeLineItem[]>([])
+
+const followList = ref<Api.Follow.FollowInfo[]>([])
+
 const showDrawer = (row: Api.School.SchoolListItem): void => {
   drawer.value = true;
   currentRow.value = row
+  timelineData.value = []
+  followApi.all('school', row.id, 0).then((res) => {
+    console.log(`%c res == `, 'background:#41b883 ; padding:1px; color:#fff', res)
+    if (res.length == 0) {
+      return
+    }
+    for (let i = 0; i < res.length; i++) {
+      const item = res[i]
+      const date = item.create_date.split(' ')[0]
+      const index = timelineData.value.findIndex((item) => item.date === date)
+      if (index === -1) {
+        timelineData.value.push({
+          date: date,
+          color: `#${Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')}`,
+          list: [item]
+        })
+      } else {
+        timelineData.value[index].list.push(item)
+      }
+    }
+  })
+  console.log(`%c timelineData.value == `, 'background:#41b883 ; padding:1px; color:#fff', timelineData.value);
 }
 
 /**
@@ -427,10 +552,6 @@ const deleteUser = (id: number): void => {
     }
   }
 }
-.el-drawer__title {
-  font-size: 20px;
-  font-weight: bold;
-}
 .detail {
   //padding-top: 20px;
   font-size: 14px;
@@ -442,4 +563,14 @@ const deleteUser = (id: number): void => {
     }
   }
 }
+.follow-div {
+  font-size: 14px;
+  .el-col {
+    margin-bottom: 10px;
+    label {
+      font-weight: bold;
+      margin-right: 5px;
+    }
+  }
+}
 </style>

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

@@ -40,13 +40,24 @@
         :selectList="selectList"
         @submit="handleDialogSubmit"
       />
+
+      <!--   跟进弹窗   -->
+      <FollowDialog
+        v-model:visible="followDialogVisible"
+        :user-data="currentUserData"
+        :type="'school'"
+        :first_id="currentUserData.school_id"
+        :second_id="currentUserData.id"
+        :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 } from 'element-plus'
+  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'
@@ -61,6 +72,7 @@
   // 弹窗相关
   const dialogType = ref<Form.DialogType>('add')
   const dialogVisible = ref(false)
+  const followDialogVisible = ref(false)
   const currentUserData = ref<Partial<SchoolContactItem>>({})
 
   // 选中行
@@ -104,11 +116,19 @@
       // 排除 apiParams 中的属性
       excludeParams: ['daterange'],
       columnsFactory: () => [
-        { prop: 'name', label: '名称' },
+        { prop: 'name', label: '关系人' },
         { prop: 'school_name', label: '学校' },
         { prop: 'phone', label: '手机号' },
         { prop: 'weixin', label: '微信号' },
         { prop: 'position', label: '职位' },
+        {
+          prop: '', label: '跟进记录', formatter: (row) => {
+            return h(ElButton, {
+              type: 'primary',
+              onClick: () => follow(row),
+            }, () => '跟进')
+          }
+        },
         { prop: 'memo', label: '备注', showOverflowTooltip: true },
         {
           prop: 'operation',
@@ -153,11 +173,20 @@
     })
   }
 
+    /**
+   * 显示跟进弹窗
+   */
+  const follow = (row: SchoolContactItem): void => {
+    currentUserData.value = row || {}
+    nextTick(() => {
+      followDialogVisible.value = true
+    })
+  }
+
   /**
    * 删除学校关系
    */
   const deleteUser = (row: SchoolContactItem): void => {
-    console.log('删除学校关系:', row)
     ElMessageBox.confirm(`确定要注销该学校关系吗?`, '注销学校关系', {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
@@ -174,6 +203,7 @@
   const handleDialogSubmit = async () => {
     try {
       dialogVisible.value = false
+      followDialogVisible.value = false
       currentUserData.value = {}
       // 延迟更新 不然数据可能没更新
       setTimeout(() => {

+ 3 - 3
web/src/views/school/relation/modules/user-dialog.vue

@@ -6,7 +6,7 @@
     align-center
   >
     <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
-      <ElFormItem label="名称" prop="name">
+      <ElFormItem label="关系人" prop="name">
         <ElInput v-model="formData.name" maxlength="20" type="text" />
       </ElFormItem>
       <ElFormItem label="学校" prop="school_id">
@@ -74,7 +74,7 @@
   // 表单数据
   const formData = reactive({
     id: 0,
-    name: '', // 名称,
+    name: '', // 关系人,
     school_id: null, // 学校ID,
     phone: '', // 手机号,
     weixin: '', // 微信号,
@@ -85,7 +85,7 @@
   // 表单验证规则
   const rules: FormRules = {
     name: [
-      { required: true, message: '请输入名称', trigger: 'blur' },
+      { required: true, message: '请输入关系人', trigger: 'blur' },
       { max: 20, message: '长度最多20个字符', trigger: 'blur' }
     ],
     school_id: [{ required: true, message: '请输入学校', trigger: 'blur' }],