소스 검색

feat:学校跟进记录列表

lizhi 3 달 전
부모
커밋
7e9e46b559

+ 0 - 26
protected/controllers/CommonController.php

@@ -64,30 +64,4 @@ class CommonController extends Controller
         Yii::app()->user->logout();
         Helper::ok();
     }
-
-    /*******************************   测试相关代码   ***************************************/
-    public function actionPhp()
-    {
-        (new DBTable(Helper::getGetString('t1')))->echoEditHtml();
-    }
-
-    public function actionTs()
-    {
-        echo (new DBTable(Helper::getGetString('t1')))->getTsInterFace();
-    }
-
-    public function actionForm()
-    {
-        (new DBTable(Helper::getGetString('t1')))->editVue();
-    }
-
-    public function actionTable()
-    {
-        echo (new DBTable(Helper::getGetString('t1')))->getTableHtml();
-    }
-
-    public function actionInfo()
-    {
-        echo (new DBTable(Helper::getGetString('t1')))->getDetailHtml();
-    }
 }

+ 76 - 16
protected/controllers/FollowController.php

@@ -29,21 +29,25 @@ class FollowController extends Controller
     public array $tableArr = [];
     public string $type = '';
 
-    public function beforeAction($action): bool
+    public function actionSchoolAdd()
     {
-        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;
+        $this->_add('school');
+    }
+
+    public function actionCanteenAdd()
+    {
+        $this->_add('canteen');
     }
 
-    public function actionAdd()
+    public function actionCompanyAdd()
     {
+        $this->_add('company');
+    }
+
+    private function _add($type)
+    {
+        $this->type = $type;
+        $this->tableArr = self::TYPE_TABLE_MAP[$this->type];
         $firstId = Helper::getPostInt('first_id');
         $secondId = Helper::getPostInt('second_id');
         $chatImgs = Helper::getArrParam($_POST, 'chat_imgs', Helper::PARAM_KEY_TYPE['array_string']);
@@ -61,8 +65,25 @@ class FollowController extends Controller
         Helper::ok();
     }
 
-    public function actionAll()
+    public function actionSchoolAll()
     {
+        $this->_all('school');
+    }
+
+    public function actionCanteenAll()
+    {
+        $this->_all('canteen');
+    }
+
+    public function actionCompanyAll()
+    {
+        $this->_all('company');
+    }
+
+    private function _all($type)
+    {
+        $this->type = $type;
+        $this->tableArr = self::TYPE_TABLE_MAP[$this->type];
         $firstId = Helper::getPostInt('first_id');
         if ($firstId <= 0) {
             Helper::error('参数错误');
@@ -77,8 +98,25 @@ class FollowController extends Controller
         Helper::ok($data['records']);
     }
 
-    public function actionInfo()
+    public function actionSchoolInfo()
     {
+        $this->_info('school');
+    }
+
+    public function actionCanteenInfo()
+    {
+        $this->_info('canteen');
+    }
+
+    public function actionCompanyInfo()
+    {
+        $this->_info('company');
+    }
+
+    private function _info($type)
+    {
+        $this->type = $type;
+        $this->tableArr = self::TYPE_TABLE_MAP[$this->type];
         $id = Helper::getPostInt('id');
         if (empty($id)) {
             Helper::error('参数错误');
@@ -88,12 +126,34 @@ class FollowController extends Controller
         Helper::ok($data);
     }
 
-    public function actionList()
+    public function actionSchoolList()
+    {
+        $this->_list('school');
+    }
+
+    public function actionCanteenList()
+    {
+        $this->_list('canteen');
+    }
+
+    public function actionCompanyList()
     {
+        $this->_list('company');
+    }
+
+    private function _list($type)
+    {
+        $this->type = $type;
+        $this->tableArr = self::TYPE_TABLE_MAP[$this->type];
+        $schoolArr = Helper::getArrParam($_POST, 'school', Helper::PARAM_KEY_TYPE['array_int']);
         $filter = [
-            $this->tableArr['first_id'] => Helper::getPostInt('second_id'),
-            $this->tableArr['second_id'] => Helper::getPostInt('second_id'),
+            $this->tableArr['first_id'] => Helper::getPostInt('first_id')? : null,
+            $this->tableArr['second_id'] => Helper::getPostInt('first_id')? : null,
         ];
+        if ($phone = Helper::getPostString('phone')) {
+            $rs = Helper::arrayColumn(DB::getListWithCriteria($this->tableArr['table2'], DbCriteria::simpleCompare(['phone' => $phone])->setSelect('id')), 'id');
+            $filter[$this->tableArr['second_id']] = $rs?: [-1];
+        }
         $criteria = DbCriteria::simpleCompareWithPage($filter)->setOrder('id desc');
         $data = DB::getListWithCriteria($this->tableArr['table'], $criteria);
         $data['records'] = $this->formatFollowList($data['records']);

+ 26 - 0
protected/controllers/SiteController.php

@@ -77,4 +77,30 @@ class SiteController extends Controller
         DB::updateById('useradmin', ['password' => md5($password)], $id);
         Helper::ok();
     }
+
+    /*******************************   测试相关代码   ***************************************/
+    public function actionPhp()
+    {
+        (new DBTable(Helper::getGetString('t1')))->echoEditHtml();
+    }
+
+    public function actionTs()
+    {
+        echo (new DBTable(Helper::getGetString('t1')))->getTsInterFace();
+    }
+
+    public function actionForm()
+    {
+        (new DBTable(Helper::getGetString('t1')))->editVue();
+    }
+
+    public function actionTable()
+    {
+        echo (new DBTable(Helper::getGetString('t1')))->getTableHtml();
+    }
+
+    public function actionInfo()
+    {
+        echo (new DBTable(Helper::getGetString('t1')))->getDetailHtml();
+    }
 }

+ 8 - 2
protected/include/LewaimaiAdminPingtaiAuth.php

@@ -82,7 +82,7 @@ class LewaimaiAdminPingtaiAuth
             'useradmin/edituser' => 110101, // 编辑用户
             'useradmin/deleteuser' => 110102, // 删除用户
 
-            // ===================   学校相关  =======================
+            // ===================   学校  =======================
             'school/list' => 120100,
             'school/getselectlist' => 120100,
             'school/info' => 120100,
@@ -91,7 +91,7 @@ class LewaimaiAdminPingtaiAuth
             'school/updateattr' => 120102,
             'school/delete' => 120103,
 
-            // ===================   学校关系相关  =======================
+            // ===================   学校关系  =======================
             'schoolrelation/list' => 120200,
             'schoolrelation/getselectlist' => 120200,
             'schoolrelation/info' => 120200,
@@ -100,6 +100,12 @@ class LewaimaiAdminPingtaiAuth
             'schoolrelation/updateattr' => 120202,
             'schoolrelation/delete' => 120203,
 
+            // ===================   学校跟进 =======================
+            'follow/schoollist' => 120300,
+            'follow/schoolall' => 120300,
+            'follow/schoolinfo' => 120200,
+            'follow/schooladd' => 120301,
+
         ];
 
         return !empty($pageAuth[$page]) && self::getAuth($pageAuth[$page]);

+ 15 - 15
web/src/api/followApi.ts

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

+ 1 - 1
web/src/components/custom/FollowDialog.vue

@@ -17,7 +17,7 @@
         </ElSelect>
       </ElFormItem>
       <ElFormItem :label="selectLabelArr[1]" prop="second_id">
-        <ElSelect v-model="formData.second_id">
+        <ElSelect v-model="formData.second_id" :empty-values="[0]" :value-on-clear="0">
           <ElOption
               v-for="item in secondSelectList"
               :key="item.id"

+ 102 - 0
web/src/components/custom/FollowDrawer.vue

@@ -0,0 +1,102 @@
+<template>
+  <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>
+</template>
+
+<script setup lang="ts">
+
+  import {followApi} from "@/api/followApi";
+
+  interface Props {
+    type:Api.FollowTye
+    first_id:number
+    second_id:number
+  }
+
+  const props = defineProps<Props>()
+  const timelineData = reactive<Api.Follow.timeLineItem[]>([])
+  const delRes = (res:any) => {
+    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.findIndex((item:Api.Follow.timeLineItem) => item.date === date)
+      if (index === -1) {
+        timelineData.push({
+          date: date,
+          color: `#${Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')}`,
+          list: [item]
+        })
+      } else {
+        timelineData[index].list.push(item)
+      }
+    }
+    console.log(`%c timelineData == `, 'background:#41b883 ; padding:1px; color:#fff', timelineData);
+  }
+
+  watch(() => [props.type, props.first_id, props.second_id], async () => {
+    timelineData.splice(0, timelineData.length)
+    followApi.schoolAll(props.first_id, props.second_id).then((res:any) => delRes(res)).catch((e) => {
+      ElMessage.error('获取数据失败')
+      console.log(`%c e == `, 'background:#41b883 ; padding:1px; color:#fff', e);
+    })
+  })
+
+</script>
+
+<style scoped>
+.follow-div {
+  font-size: 14px;
+  .el-col {
+    margin-bottom: 10px;
+    label {
+      font-weight: bold;
+      margin-right: 5px;
+    }
+  }
+}
+</style>

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

@@ -300,6 +300,8 @@
     "school": {
       "list": "school list",
       "relation": "school relation",
+      "follow": "follow list",
+      "followInfo": "follow info",
       "info": "school info",
       "add": "add school",
       "edit": "edit school"

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

@@ -292,6 +292,8 @@
     "school": {
       "list": "校园信息",
       "relation": "校方关系",
+      "follow": "跟进记录",
+      "followInfo": "跟进详情",
       "info": "校园详情",
       "add": "新建校园",
       "edit": "编辑校园"

+ 34 - 1
web/src/router/routes/asyncRoutes.ts

@@ -125,7 +125,40 @@ export const asyncRoutes: AppRouteRecord[] = [
             },
           ]
         }
-      }
+      },
+      {
+        id: 1203,
+        path: 'follow',
+        name: 'schoolFollow',
+        component: RoutesAlias.SchoolFollow,
+        meta: {
+          title: 'menus.school.follow',
+          keepAlive: true,
+          authList: [
+            {
+              id: 120300,
+              title: '列表',
+              authMark: 'list'
+            },
+            {
+              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' // 激活菜单路径
+        }
+      },
     ]
   },
   {

+ 3 - 1
web/src/router/routesAlias.ts

@@ -58,5 +58,7 @@ export enum RoutesAlias {
   SchoolList = '/school/list', // 学校列表
   SchoolInfo = '/school/info', // 学校详情
   SchoolEdit = '/school/edit', // 编辑学校
-  SchoolRelation = '/school/relation' // 学校关系
+  SchoolRelation = '/school/relation', // 学校关系
+  SchoolFollow = '/school/follow', // 学校跟进
+  SchoolFollowInfo = '/school/follow/info', // 学校跟进详情
 }

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

@@ -237,5 +237,11 @@ declare namespace Api {
       total: number
     }
 
+    interface timeLineItem {
+      date: string
+      color: string
+      list: Api.Follow.FollowInfo[]
+    }
+
   }
 }

+ 214 - 0
web/src/views/school/follow/index.vue

@@ -0,0 +1,214 @@
+<!-- 学校关系管理 -->
+<!-- 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="120301">新增跟进记录</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="'school'"
+        :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 {schoolRelationApi} from "@/api/schoolRelationApi";
+
+  defineOptions({ name: 'SchoolFollow' })
+
+  type SchoolContactItem = Api.School.SchoolContactItem
+
+  // 弹窗相关
+  const dialogVisible = ref(false)
+  const followDialogVisible = ref(false)
+  const currentUserData = ref<Partial<SchoolContactItem>>({})
+
+  // 选中行
+  const selectedRows = ref<SchoolContactItem[]>([])
+
+
+  // 搜索表单
+  const searchForm = ref({
+    name: '',
+    phone: '',
+    school_id: parseInt(<string>useRoute().query.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<Api.Follow.FollowInfo>({
+    // 核心配置
+    core: {
+      apiFn: followApi.schoolList,
+      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) => {
+            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
+            })
+          }
+        },
+        { 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.SchoolFollowInfo,
+      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/school/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/school/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>

+ 26 - 86
web/src/views/school/list/index.vue

@@ -151,50 +151,8 @@
             </el-col>
           </ElRow>
         </ElCol>
-        <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 :sm="14">
+          <FollowDrawer :first_id="currentRow.id" :second_id="0" type="school"/>
         </ElCol>
       </ElRow>
     </el-drawer>
@@ -227,7 +185,29 @@ const searchForm = ref({
   address: []
 })
 
-const currentRow = ref<Partial<Api.School.SchoolListItem>>({})
+const detaltValue = <Api.School.SchoolListItem>{
+  id: 0,
+  name: '',
+  province: '',
+  city: '',
+  area: '',
+  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: ''
+}
+
+const currentRow = ref<Api.School.SchoolListItem>({...detaltValue})
 
 const selectList = ref<Api.Common.SelectRelationInfo[]>([])
 const getSelectList = async () => {
@@ -241,7 +221,7 @@ getSelectList()
  */
 const handleDialogSubmit = async () => {
   followDialogVisible.value = false
-  currentRow.value = {}
+  Object.assign(currentRow.value, {...detaltValue})
   // // 延迟更新 不然数据可能没更新
   // setTimeout(() => {
   //   refreshData()
@@ -478,39 +458,9 @@ 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)
-      }
-    }
-  })
 }
 
 const showContact = (row: Api.School.SchoolListItem):void => {
@@ -572,14 +522,4 @@ const deleteUser = (id: number): void => {
     }
   }
 }
-.follow-div {
-  font-size: 14px;
-  .el-col {
-    margin-bottom: 10px;
-    label {
-      font-weight: bold;
-      margin-right: 5px;
-    }
-  }
-}
 </style>

+ 86 - 1
web/src/views/school/relation/index.vue

@@ -52,6 +52,51 @@
         @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.school_name }}</span>
+            </el-col>
+
+            <el-col>
+              <label>备注:</label> <span>{{ currentRow.memo }}</span>
+            </el-col>
+
+          </ElRow>
+        </ElCol>
+        <ElCol :sm="16">
+          <FollowDrawer :first_id="currentRow.school_id" :second_id="currentRow.id" type="school"/>
+        </ElCol>
+      </ElRow>
+    </el-drawer>
   </div>
 </template>
 
@@ -63,6 +108,7 @@
   import UserDialog from './modules/user-dialog.vue'
   import { schoolRelationApi } from '@/api/schoolRelationApi'
   import { useUserStore } from '@/store/modules/user'
+  import {followApi} from "@/api/followApi";
 
   defineOptions({ name: 'SchoolRelation' })
 
@@ -117,7 +163,16 @@
       // 排除 apiParams 中的属性
       excludeParams: ['daterange'],
       columnsFactory: () => [
-        { prop: 'name', label: '关系人' },
+        {
+          prop: 'name', label: '关系人', formatter: (row) => {
+            return h(ElButton, {
+              type: 'primary',
+              link: true,
+              onClick: () => showDrawer(row),
+              style: {"text-decoration": 'underline'}
+            }, () => row.name)
+          }
+        },
         { prop: 'school_name', label: '学校' },
         { prop: 'phone', label: '手机号' },
         { prop: 'weixin', label: '微信号' },
@@ -163,6 +218,25 @@
     getData()
   }
 
+  const detaltValue = <Api.School.SchoolContactItem>{
+    id: 0,
+    name: '',
+    school_id: 0, // 学校ID,
+    school_name: '',
+    phone: '',
+    weixin: '',
+    position: '',
+    memo: '',
+  }
+
+  const currentRow = ref<Api.School.SchoolContactItem>({...detaltValue})
+  const drawer = ref(false)
+
+  const showDrawer = (row: Api.School.SchoolContactItem): void => {
+    drawer.value = true;
+    currentRow.value = row
+  }
+
   /**
    * 显示学校关系弹窗
    */
@@ -244,4 +318,15 @@
       }
     }
   }
+  .detail {
+    //padding-top: 20px;
+    font-size: 14px;
+    .el-col {
+      margin-bottom: 30px;
+      label {
+        font-weight: bold;
+        margin-right: 5px;
+      }
+    }
+  }
 </style>