فهرست منبع

feat:学校关系

lizhi 3 ماه پیش
والد
کامیت
f1a6c7745f

+ 1 - 0
index.php

@@ -10,6 +10,7 @@ defined('LWM_ENV') or define('LWM_ENV', $config['params']['env']?? 'dev');
 defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL', 5);
 
 defined('PROJECT_PATH') or define('PROJECT_PATH', __DIR__);
+defined('RUNTIME_PATH') or define('RUNTIME_PATH', PROJECT_PATH . '/protected/runtime');
 // 这里是与乐外卖业务相关的一些定义
 defined('LEWAIMAI_DEBUG') or define('LEWAIMAI_DEBUG', false);
 

+ 1 - 1
protected/components/DB.php

@@ -400,7 +400,7 @@ class DB
      * 通过 DbCriteria 来搜索
      * @param  string  $table_name  表名
      * @param  DbCriteria  $criteria  分页通过 setPage 设置,否则不会查询分页信息
-     * @return array|\CDbDataReader 格式: ['page'=>1, 'pageSize'=>1, 'totalPage'=>1, 'counts'=>1, 'records'=>[]]
+     * @return array|\CDbDataReader 格式: ['current'=>1, 'size'=>1, 'totalPage'=>1, 'total'=>1, 'records'=>[]]
      * @throws \CException
      */
     public static function getListWithCriteria(string $table_name, DbCriteria $criteria)

+ 9 - 12
protected/controllers/SchoolController.php

@@ -2,13 +2,14 @@
 
 class SchoolController extends Controller
 {
+    public static string $table = 'school';
     public function actionInfo()
     {
         $id = Helper::getPostInt('id');
         if ($id <= 0) {
             Helper::error('参数错误');
         }
-        $data = DB::getInfoById('school', $id);
+        $data = DB::getInfoById(self::$table, $id);
         if (!$data) {
             Helper::error('数据不存在');
         }
@@ -35,7 +36,7 @@ class SchoolController extends Controller
              $filter['is_cooperate'] = $is_cooperate;
         }
         $cri = DbCriteria::simpleCompareWithPage($filter);
-        $data = DB::getListWithCriteria('school', $cri);
+        $data = DB::getListWithCriteria(self::$table, $cri);
         if (!empty($data['records'])) {
             $data['records'] = array_map(function ($item) {
                 return $item;
@@ -51,7 +52,7 @@ class SchoolController extends Controller
     public function actionGetSelectList()
     {
         $cri = DbCriteria::simpleCompare([])->setSelect('id, name');
-        $data = DB::getListWithCriteria('school', $cri);
+        $data = DB::getListWithCriteria(self::$table, $cri);
         Helper::ok($data['records']??[]);
     }
 
@@ -61,7 +62,7 @@ class SchoolController extends Controller
         if ($id < 1) {
             Helper::error('参数错误');
         }
-        Db::updateById('school', ['is_del' => 1], $id);
+        Db::updateById(self::$table, ['is_del' => 1], $id);
         Helper::ok();
     }
 
@@ -125,14 +126,14 @@ class SchoolController extends Controller
         if ($id > 0) {
             $cri->addCondition('id!=' . $id);
         }
-        if ($fid = DB::getScalerWithCriteria('school', $cri)) {
+        if ($fid = DB::getScalerWithCriteria(self::$table, $cri)) {
             Helper::error('学校名称已存在 ' . $fid);
         }
 
         if ($id) {
-            DB::updateById('school', $data, $id);
+            DB::updateById(self::$table, $data, $id);
         } else {
-            DB::addData('school', $data);
+            DB::addData(self::$table, $data);
         }
         Helper::ok();
     }
@@ -151,13 +152,9 @@ class SchoolController extends Controller
         if ($attr == 'is_eleme_in_school' && !in_array($value, [1, 0])) {
             Helper::error('参数错误3');
         }
-        if (DB::updateById('school', [$attr => $value], $id) === false) {
+        if (DB::updateById(self::$table, [$attr => $value], $id) === false) {
             Helper::error('更新失败');
         }
         Helper::ok();
     }
-
-    private function _edit()
-    {
-    }
 }

+ 138 - 0
protected/controllers/SchoolRelationController.php

@@ -0,0 +1,138 @@
+<?php
+
+class SchoolRelationController extends Controller
+{
+    public string $table = 'school_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.school_id' => Helper::getPostInt('school_id') ?: null,
+        ];
+        if ($name = Helper::getPostString('name')) {
+            $filter['r.name'] = '%'.$name;
+        }
+        $cri = DbCriteria::simpleCompareWithPage($filter)
+            ->setAlias('r')
+            ->setSelect('r.*, s.name as school_name')
+            ->setJoin('LEFT JOIN wx_school s ON s.id=r.school_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([])->setSelect('id, name');
+        $schools = Helper::arrayColumn(DB::getListWithCriteria(SchoolController::$table, $cri), null, 'id');
+        if (empty($schools)) {
+            Helper::ok([]);
+        }
+        $relations = DB::getListWithCriteria($this->table, $cri);
+        foreach ($relations['records'] as $relation) {
+            $sid = $relation['school_id'];
+            if (!isset($schools[$sid])) {
+                continue;
+            }
+            if (!isset($schools[$sid]['children'])) {
+                $schools[$sid]['children'] = [];
+            }
+            $schools[$sid]['children'][] = [
+                'id'   => $relation['id'],
+                'name' => $relation['name'],
+            ];
+        }
+        Helper::ok($schools);
+    }
+
+    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'),
+            'school_id' => Helper::getPostInt('school_id'),
+            'phone'     => Helper::getPostString('phone'),
+            'weixin'    => Helper::getPostString('weixin'),
+            'position'  => Helper::getPostString('position'),
+            'memo'      => Helper::getPostString('memo'),
+        ];
+
+        $notNullField = ["name", "school_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();
+    }
+}

+ 11 - 7
protected/include/DBColumn.php

@@ -104,12 +104,16 @@ class DBColumn {
                 }
                 $html = $this->getInputHtml($inputType, $max);
                 // rule
-                if ($min > 0 && $max > 0) {
-                    $rule[] = "{ min: {$min}, max: {$max}, message: '长度在{$min}到{$max}个字符', trigger: 'blur' }";
-                } elseif ($min > 0) {
-                    $rule[] = "{ min: {$min}, message: '长度最少{$min}个字符', trigger: 'blur' }";
-                } elseif ($max > 0) {
-                    $rule[] = "{ max: {$max}, message: '长度最多{$max}个字符', trigger: 'blur' }";
+                if (Helper::hasAnyString($this->field, ['phone', 'tel'])) {
+                    $rule[] = "{ pattern: /^1[3456789]\\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }";
+                } else {
+                    if ($min > 0 && $max > 0) {
+                        $rule[] = "{ min: {$min}, max: {$max}, message: '长度在{$min}到{$max}个字符', trigger: 'blur' }";
+                    } elseif ($min > 0) {
+                        $rule[] = "{ min: {$min}, message: '长度最少{$min}个字符', trigger: 'blur' }";
+                    } elseif ($max > 0) {
+                        $rule[] = "{ max: {$max}, message: '长度最多{$max}个字符', trigger: 'blur' }";
+                    }
                 }
             }
         } elseif ($type == 'number') {
@@ -275,7 +279,7 @@ select;
 
         // 长字段列表需要添加showOverflowTooltip
         if ($this->getMaxLength() > 64 || Helper::hasAnyString($this->field, ['text'])) {
-            $JsTableArr['showOverflowTooltip'] = true;
+            $JsTableArr['showOverflowTooltip'] = 'true';
         }
 
         if (str_contains($this->field, 'ids')) {

+ 8 - 9
protected/include/DBTable.php

@@ -6,10 +6,10 @@ class DBTable {
     public array $columnArray;
     public string $line = '<br/>';
 
-    const array FORM_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_delete', 'id', 'city', 'area', 'province'];
-    const array INFO_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_delete'];
-    const array TABLE_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_delete'];
-    const array DETAIL_EXCLUDE_COLUMNS = ['is_delete'];
+    const array FORM_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_del', 'id', 'city', 'area', 'province'];
+    const array INFO_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_del'];
+    const array TABLE_EXCLUDE_COLUMNS = ['create_date', 'update_date', 'is_del'];
+    const array DETAIL_EXCLUDE_COLUMNS = ['is_del'];
     public function __construct(string $name) {
         $this->name = strtolower($name);
         $this->tableName = DB::formTableName($name);
@@ -62,9 +62,9 @@ class DBTable {
         $editTmpl = str_replace(
             ['{{template}}', '{{table}}', '{{formDefault}}', '{{formRule}}', '{{ucTable}}'],
             [$template, $this->name, $formDefault, $rule, ucfirst($this->name)],
-            file_get_contents(PROJECT_PATH . '/protected/runtime/edit.tmpl')
+            file_get_contents(RUNTIME_PATH . '/edit.tmpl')
          );
-        $filePath = PROJECT_PATH . "/web/src/views/{$this->name}/edit.vue";
+        $filePath = RUNTIME_PATH . "/edit.vue";
         file_put_contents($filePath, $editTmpl);
         // 格式化代码
         echo "<h4>命令后执行下面代码</h4>";
@@ -191,8 +191,7 @@ typescript;
             $tableContent.= $this->getHtmlWithJsTableArr($arr);
         }
         $template = <<<typescript
-{{$this->line}
-$tableContent}{$this->line}{$this->line}
+$tableContent
 typescript;
         return $template;
     }
@@ -204,7 +203,7 @@ typescript;
         }
         $str = '{';
         foreach ($arr as $k => $v) {
-            $str.= " {$k}:{$v},";
+            $str.= " {$k}:'{$v}',";
         }
         return trim($str, ',') . ' },' . $this->line;
     }

+ 1 - 0
protected/include/LewaimaiAdminPingtaiAuth.php

@@ -84,6 +84,7 @@ class LewaimaiAdminPingtaiAuth
 
             // ===================   学校相关  =======================
             'school/list' => 1201,
+            'school/getselectlist' => 1201,
             'school/info' => 1201,
             'school/add' => 120101,
             'school/edit' => 120102,

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

@@ -30,6 +30,14 @@ export class schoolApi {
     })
   }
 
+  // 下拉列表
+  static selectList() {
+    return request.post<Api.Common.SelectInfo[]>({
+      url: 'school/getSelectList'
+      // showErrorMessage: false // 不显示错误消息
+    })
+  }
+
   // 编辑属性
   static updateAttr(params: Form.UpdateAttr) {
     return request.post<any>({

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

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

+ 85 - 84
web/src/router/routes/asyncRoutes.ts

@@ -26,35 +26,41 @@ export const asyncRoutes: AppRouteRecord[] = [
     meta: {
       title: 'menus.dashboard.console',
       icon: '&#xe733;',
-      keepAlive: false
+      keepAlive: false,
+      fixedTab:true,
     }
   },
   {
-    id: 11,
-    path: '/system',
-    name: 'System',
+    id: 12,
+    path: '/school',
+    name: 'SchoolManage',
     component: RoutesAlias.Layout,
     meta: {
-      title: 'menus.system.title',
+      title: 'menus.school.list',
       icon: '&#xe7b9;'
     },
     children: [
       {
-        id: 1101,
-        path: 'user',
-        name: 'User',
-        component: RoutesAlias.User,
+        id: 1201,
+        path: 'list',
+        name: 'school',
+        component: RoutesAlias.SchoolList,
         meta: {
-          title: 'menus.system.user',
-          keepAlive: true,
+          title: 'menus.school.list',
+          keepAlive: false,
           authList: [
             {
-              id: 110101,
+              id: 120101,
+              title: '新增',
+              authMark: 'add'
+            },
+            {
+              id: 120102,
               title: '编辑',
               authMark: 'edit'
             },
             {
-              id: 110102,
+              id: 120203,
               title: '删除',
               authMark: 'delete'
             }
@@ -62,26 +68,48 @@ export const asyncRoutes: AppRouteRecord[] = [
         }
       },
       {
-        id: 1102,
-        path: 'role',
-        name: 'Role',
-        component: RoutesAlias.Role,
+        path: 'edit',
+        name: 'SchoolEdit',
+        component: RoutesAlias.SchoolEdit,
         meta: {
-          title: 'menus.system.role',
+          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: 110201,
-              title: '分配权限',
+              id: 120201,
+              title: '新增',
               authMark: 'add'
             },
             {
-              id: 110202,
+              id: 120202,
               title: '编辑',
               authMark: 'edit'
             },
             {
-              id: 110203,
+              id: 120203,
               title: '删除',
               authMark: 'delete'
             }
@@ -91,36 +119,31 @@ export const asyncRoutes: AppRouteRecord[] = [
     ]
   },
   {
-    id: 12,
-    path: '/school',
-    name: 'SchoolManage',
+    id: 11,
+    path: '/system',
+    name: 'System',
     component: RoutesAlias.Layout,
     meta: {
-      title: 'menus.school.list',
+      title: 'menus.system.title',
       icon: '&#xe7b9;'
     },
     children: [
       {
-        id: 1201,
-        path: 'list',
-        name: 'school',
-        component: RoutesAlias.SchoolList,
+        id: 1101,
+        path: 'user',
+        name: 'User',
+        component: RoutesAlias.User,
         meta: {
-          title: 'menus.school.list',
-          keepAlive: false,
+          title: 'menus.system.user',
+          keepAlive: true,
           authList: [
             {
-              id: 120101,
-              title: '新增',
-              authMark: 'add'
-            },
-            {
-              id: 120102,
+              id: 110101,
               title: '编辑',
               authMark: 'edit'
             },
             {
-              id: 110203,
+              id: 110102,
               title: '删除',
               authMark: 'delete'
             }
@@ -128,54 +151,32 @@ export const asyncRoutes: AppRouteRecord[] = [
         }
       },
       {
-        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,
+        id: 1102,
+        path: 'role',
+        name: 'Role',
+        component: RoutesAlias.Role,
         meta: {
-          title: 'menus.school.info',
-          isHide: true,
+          title: 'menus.system.role',
           keepAlive: true,
-          activePath: '/school/list' // 激活菜单路径
+          authList: [
+            {
+              id: 110201,
+              title: '分配权限',
+              authMark: 'add'
+            },
+            {
+              id: 110202,
+              title: '编辑',
+              authMark: 'edit'
+            },
+            {
+              id: 110203,
+              title: '删除',
+              authMark: 'delete'
+            }
+          ]
         }
       }
-      // {
-      //   id: 1202,
-      //   path: 'relation',
-      //   name: 'relation',
-      //   component: RoutesAlias.SchoolRelation,
-      //   meta: {
-      //     title: 'menus.school.relation',
-      //     keepAlive: true,
-      //     authList: [
-      //       {
-      //         id: 110201,
-      //         title: '新增',
-      //         authMark: 'add'
-      //       },
-      //       {
-      //         id: 110202,
-      //         title: '编辑',
-      //         authMark: 'edit'
-      //       },
-      //       {
-      //         id: 110203,
-      //         title: '删除',
-      //         authMark: 'delete'
-      //       }
-      //     ]
-      //   }
-      // }
     ]
-  }
+  },
 ]

+ 9 - 2
web/src/typings/api.d.ts

@@ -44,6 +44,12 @@ declare namespace Api {
       name: string
     }
 
+    interface SelectRelationInfo {
+      id: number
+      name: string,
+      children: selectInfo[]
+    }
+
     /** 启用状态 */
     type EnableStatus = '1' | '2'
   }
@@ -189,12 +195,13 @@ declare namespace Api {
       id: number
       name: string // 名称,
       school_id: number // 学校ID,
+      school_name: string // 学校,
       phone: string // 手机号,
       weixin: string // 微信号,
       position: string // 职位,
       memo: string // 备注,
-      create_date: string // 创建时间,
-      update_date: string // 更新时间,
+      create_date?: string // 创建时间,
+      update_date?: string // 更新时间,
     }
 
     interface SchoolContactListData {

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

@@ -43,4 +43,14 @@ declare namespace Form {
     out_business_description: string // 校外商圈情况,
     memo: string // 备注,
   }
+
+  interface SchoolContact {
+    id?: number
+    name: string // 名称,
+    school_id: number // 学校ID,
+    phone: string // 手机号,
+    weixin: string // 微信号,
+    position: string // 职位,
+    memo: string // 备注,
+  }
 }

+ 216 - 0
web/src/views/school/relation/index.vue

@@ -0,0 +1,216 @@
+<!-- 学校关系管理 -->
+<!-- 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 @click="showDialog('add')" v-ripple v-auth="110101">新增学校关系</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"
+      />
+    </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 { useTable } from '@/composables/useTable'
+  import UserSearch from './modules/user-search.vue'
+  import UserDialog from './modules/user-dialog.vue'
+  import { schoolRelationApi } from '@/api/schoolRelationApi'
+  import { useUserStore } from '@/store/modules/user'
+
+  defineOptions({ name: 'SchoolRelation' })
+
+  type SchoolContactItem = Api.School.SchoolContactItem
+  const { list } = schoolRelationApi
+
+  // 弹窗相关
+  const dialogType = ref<Form.DialogType>('add')
+  const dialogVisible = ref(false)
+  const currentUserData = ref<Partial<SchoolContactItem>>({})
+
+  // 选中行
+  const selectedRows = ref<SchoolContactItem[]>([])
+
+  // 搜索表单
+  const searchForm = ref({
+    name: '',
+    phone: '',
+    school_id: ''
+  })
+
+  const selectList = ref<Api.Common.SelectRelationInfo[]>([])
+  const getSelectList = async () => {
+    const data = await schoolRelationApi.selectList()
+    selectList.value = data
+  }
+  getSelectList()
+
+  const {
+    columns,
+    columnChecks,
+    data,
+    loading,
+    pagination,
+    getData,
+    searchParams,
+    resetSearchParams,
+    handleSizeChange,
+    handleCurrentChange,
+    refreshData
+  } = useTable<SchoolContactItem>({
+    // 核心配置
+    core: {
+      apiFn: list,
+      apiParams: {
+        current: 1,
+        size: 20,
+        ...searchForm.value
+      },
+      // 排除 apiParams 中的属性
+      excludeParams: ['daterange'],
+      columnsFactory: () => [
+        { prop: 'name', label: '名称' },
+        { prop: 'school_name', label: '学校' },
+        { prop: 'phone', label: '手机号' },
+        { prop: 'weixin', label: '微信号' },
+        { prop: 'position', label: '职位' },
+        { prop: 'memo', label: '备注', showOverflowTooltip: true },
+        {
+          prop: 'operation',
+          label: '操作',
+          width: 120,
+          fixed: 'right', // 固定列
+          formatter: (row) =>
+            useUserStore().checkAuth(110101) &&
+            h('div', [
+              h(ArtButtonTable, {
+                type: 'edit',
+                onClick: () => showDialog('edit', row)
+              }),
+              useUserStore().checkAuth(110102) &&
+                h(ArtButtonTable, {
+                  type: 'delete',
+                  onClick: () => deleteUser(row)
+                })
+            ])
+        }
+      ]
+    }
+  })
+
+  /**
+   * 搜索处理
+   * @param params 参数
+   */
+  const handleSearch = (params: Record<string, any>) => {
+    Object.assign(searchParams, { ...params })
+    getData()
+  }
+
+  /**
+   * 显示学校关系弹窗
+   */
+  const showDialog = (type: Form.DialogType, row?: SchoolContactItem): void => {
+    dialogType.value = type
+    currentUserData.value = row || {}
+    nextTick(() => {
+      dialogVisible.value = true
+    })
+  }
+
+  /**
+   * 删除学校关系
+   */
+  const deleteUser = (row: SchoolContactItem): void => {
+    console.log('删除学校关系:', row)
+    ElMessageBox.confirm(`确定要注销该学校关系吗?`, '注销学校关系', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'error'
+    }).then(() => {
+      schoolRelationApi.delete({ id: row.id })
+      refreshData()
+    })
+  }
+
+  /**
+   * 处理弹窗提交事件
+   */
+  const handleDialogSubmit = async () => {
+    try {
+      dialogVisible.value = false
+      currentUserData.value = {}
+      // 延迟更新 不然数据可能没更新
+      setTimeout(() => {
+        refreshData()
+      }, 1000)
+    } catch (error) {
+      console.error('提交失败:', error)
+    }
+  }
+
+  /**
+   * 处理表格行选择变化
+   */
+  const handleSelectionChange = (selection: SchoolContactItem[]): 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);
+        }
+      }
+    }
+  }
+</style>

+ 157 - 0
web/src/views/school/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="school_id">
+        <ElSelect v-model="formData.school_id">
+          <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 { schoolRelationApi } from '@/api/schoolRelationApi'
+
+  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({
+    id: 0,
+    name: '', // 名称,
+    school_id: null, // 学校ID,
+    phone: '', // 手机号,
+    weixin: '', // 微信号,
+    position: '', // 职位,
+    memo: '' // 备注,
+  })
+
+  // 表单验证规则
+  const rules: FormRules = {
+    name: [
+      { required: true, message: '请输入名称', trigger: 'blur' },
+      { max: 20, message: '长度最多20个字符', trigger: 'blur' }
+    ],
+    school_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 || '' : '',
+      school_id: isEdit.value ? row.school_id : null,
+      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) {
+          schoolRelationApi.edit(formData).then(() => {
+            dialogVisible.value = false
+            emit('submit')
+          })
+        } else {
+          schoolRelationApi.add(formData).then(() => {
+            dialogVisible.value = false
+            emit('submit')
+          })
+        }
+      }
+    })
+  }
+</script>

+ 77 - 0
web/src/views/school/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: 'school_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>