Ver Fonte

代码提交

yjwang há 9 horas atrás
pai
commit
45094d7ef7

+ 89 - 0
src/api/company/userBindWx.js

@@ -0,0 +1,89 @@
+import request from '@/utils/request'
+
+// 查询用户绑定列表
+export function listUserBindWx(query) {
+  return request({
+    url: '/company/userBindWx/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 解码手机号
+export function decryptPhone(phone) {
+  return request({
+    url: '/company/userBindWx/decryptPhone',
+    method: 'post',
+    data: { phone }
+  })
+}
+
+// 搜索可绑定微信的用户
+export function searchBindUser(query) {
+  return request({
+    url: '/company/userBindWx/searchBindUser',
+    method: 'get',
+    params: query
+  })
+}
+
+// 绑定微信用户
+export function bindWxUser(data) {
+  return request({
+    url: '/company/userBindWx/bindWxUser',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取公司销售人员列表
+export function getCompanyUserList() {
+  return request({
+    url: '/company/userBindWx/companyUserList',
+    method: 'get'
+  })
+}
+
+// 解绑微信用户
+export function unbindWxUser(id) {
+  return request({
+    url: '/company/userBindWx/unbindWxUser',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// 查询外部联系人列表
+export function searchExternalContact(query) {
+  return request({
+    url: '/company/userBindWx/externalContactList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询绑定的微信用户详情
+export function getBoundUserInfo(userId) {
+  return request({
+    url: '/company/userBindWx/boundUserInfo/' + userId,
+    method: 'get'
+  })
+}
+
+// 批量分配销售人员
+export function batchAssign(data) {
+  return request({
+    url: '/company/userBindWx/batchAssign',
+    method: 'post',
+    data: data
+  })
+}
+
+// 解除分配
+export function unassignCompanyUser(id) {
+  return request({
+    url: '/company/userBindWx/unassignCompanyUser',
+    method: 'post',
+    data: { id }
+  })
+}

+ 24 - 2
src/views/app/imAddFriend/index.vue

@@ -119,6 +119,16 @@
               />
             </el-select>
           </el-form-item>
+          <el-form-item v-if="bindForm.addType === 1" label="看课项目" prop="projectId" :rules="[{ required: true, message: '请选择看课项目', trigger: 'change' }]">
+            <el-select v-model="bindForm.projectId" placeholder="请选择看课项目" clearable style="width: 320px">
+              <el-option
+                v-for="item in projectOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="Number(item.dictValue)"
+              />
+            </el-select>
+          </el-form-item>
         </el-form>
 
         <el-divider content-position="left">选择用户</el-divider>
@@ -276,9 +286,12 @@ export default {
       },
       bindForm: {
         addType: 1,
-        companyUserId: null
+        companyUserId: null,
+        projectId: null
       },
       companyUserList: [],
+      projectOptions: [],
+      projectMap: {},
       userLoading: false,
       userList: [],
       userTotal: 0,
@@ -309,6 +322,13 @@ export default {
   created() {
     this.getList()
     this.loadCompanyUserList()
+    this.getDicts('sys_course_project').then(res => {
+      this.projectOptions = res.data || []
+      this.projectMap = {}
+      ;(this.projectOptions || []).forEach(item => {
+        this.projectMap[Number(item.dictValue)] = item.dictLabel
+      })
+    })
   },
   methods: {
     loadCompanyUserList() {
@@ -348,6 +368,7 @@ export default {
     },
     handleAddTypeChange() {
       this.bindForm.companyUserId = null
+      this.bindForm.projectId = null
       this.selectedUsers = []
       this.selectedUser = null
       this.selectedExternal = null
@@ -443,7 +464,8 @@ export default {
         this.submitLoading = true
         const payload = {
           addType: 1,
-          userIds: this.selectedUsers.map(item => this.resolveRowUserId(item))
+          userIds: this.selectedUsers.map(item => this.resolveRowUserId(item)),
+          projectId: this.bindForm.projectId
         }
         if (this.bindForm.companyUserId != null && this.bindForm.companyUserId !== '') {
           payload.companyUserId = this.bindForm.companyUserId

+ 453 - 0
src/views/app/userBindWx/assign.vue

@@ -0,0 +1,453 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号码" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="微信绑定" prop="bindStatus">
+        <el-select v-model="queryParams.bindStatus" placeholder="请选择绑定状态" clearable size="small">
+          <el-option label="未绑定" :value="0" />
+          <el-option label="已绑定" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="分配状态" prop="assignStatus">
+        <el-select v-model="queryParams.assignStatus" placeholder="请选择分配状态" clearable size="small">
+          <el-option label="未分配" :value="0" />
+          <el-option label="已分配" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-s-operation"
+          size="mini"
+          :disabled="selectedIds.length === 0"
+          @click="handleBatchAssign"
+        >批量分配</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="45" align="center" :selectable="row => row.companyUserId == null" />
+      <el-table-column label="会员ID" align="center" prop="userId" width="120" />
+      <el-table-column label="用户昵称" align="center" prop="nickName" min-width="150" />
+      <el-table-column label="会员头像" align="center" width="80">
+        <template slot-scope="scope">
+          <el-popover v-if="scope.row.avatar" placement="right" trigger="hover">
+            <img
+              slot="reference"
+              :src="scope.row.avatar"
+              style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover"
+            />
+            <img :src="scope.row.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+          </el-popover>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号码" align="center" min-width="140">
+        <template slot-scope="scope">
+          <span>{{ phoneMap[scope.row.phone] || scope.row.phone }}</span>
+          <el-button
+            v-if="scope.row.phone && !phoneMap[scope.row.phone]"
+            type="text"
+            icon="el-icon-search"
+            size="mini"
+            title="查看手机号"
+            @click="handleDecryptPhone(scope.row.phone)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="微信绑定" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.bindUserId != null || scope.row.externalContactId != null" type="success" size="mini">已绑定</el-tag>
+          <el-tag v-else type="info" size="mini">未绑定</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否分配" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.companyUserId != null" type="success" size="mini">已分配</el-tag>
+          <el-tag v-else type="danger" size="mini">未分配</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="分配销售" align="center" min-width="120">
+        <template slot-scope="scope">
+          <span v-if="scope.row.companyUserName">{{ scope.row.companyUserName }}</span>
+          <el-tag v-else type="danger" size="mini">未分配</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="分配时间" align="center" width="180">
+        <template slot-scope="scope">
+          <span>{{ formatDate(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="140" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="mini"
+            @click="handleShowDetail(scope.row)"
+          >详情</el-button>
+          <el-button
+            v-if="scope.row.companyUserId != null"
+            type="text"
+            size="mini"
+            style="color: #F56C6C;"
+            @click="handleUnassign(scope.row)"
+          >解除分配</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 批量分配弹窗 -->
+    <el-dialog title="批量分配销售" :visible.sync="assignDialogVisible" width="420px" append-to-body :close-on-click-modal="false">
+      <el-form ref="assignForm" label-width="80px" size="small">
+        <el-form-item label="已选会员">
+          <span style="color: #409eff; font-weight: 500;">{{ selectedIds.length }} 人</span>
+        </el-form-item>
+        <el-form-item label="选择销售" required>
+          <el-select v-model="assignCompanyUserId" placeholder="请选择销售人员" filterable size="small" style="width: 100%;">
+            <el-option
+              v-for="item in companyUserList"
+              :key="item.userId"
+              :label="item.nickName || item.userName"
+              :value="item.userId"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="assignDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="assignLoading" :disabled="!assignCompanyUserId" @click="confirmAssign">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 绑定用户详情弹窗 -->
+    <el-dialog title="用户详情" :visible.sync="detailDialogVisible" width="450px" append-to-body>
+      <div v-if="currentDetailRow" v-loading="detailLoading">
+        <!-- 基础信息 -->
+        <div class="bind-section" style="margin-bottom: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-user"></i> 基础信息
+          </div>
+          <div style="text-align: center; padding: 16px 0;">
+            <el-popover v-if="currentDetailRow.avatar" placement="right" trigger="hover">
+              <img
+                slot="reference"
+                :src="currentDetailRow.avatar"
+                style="width: 56px; height: 56px; border-radius: 50%; object-fit: cover"
+              />
+              <img :src="currentDetailRow.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+            </el-popover>
+            <div v-else style="width: 56px; height: 56px; border-radius: 50%; background: #eee; display: inline-block; line-height: 56px; color: #999;">无</div>
+            <p style="margin-top: 8px; font-size: 14px;">
+              <span style="color: #909399;">会员ID:</span>
+              <span style="color: #303133;">{{ currentDetailRow.userId }}</span>
+            </p>
+            <p style="font-size: 14px;">
+              <span style="color: #909399;">昵称:</span>
+              <span style="color: #303133;">{{ currentDetailRow.nickName || '-' }}</span>
+            </p>
+          </div>
+        </div>
+
+        <!-- 分配销售信息 -->
+        <div v-if="currentDetailRow.companyUserId" class="bind-section" style="margin-bottom: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-s-custom"></i> 分配销售
+          </div>
+          <div style="text-align: center; padding: 12px 0;">
+            <p style="font-size: 14px; margin: 6px 0;">
+              <span style="color: #909399;">销售ID:</span>
+              <span style="color: #303133;">{{ currentDetailRow.companyUserId }}</span>
+            </p>
+            <p style="font-size: 14px; margin: 6px 0;">
+              <span style="color: #909399;">账号:</span>
+              <span style="color: #303133;">{{ currentDetailRow.companyUserAccount || '-' }}</span>
+            </p>
+            <p style="font-size: 14px; margin: 6px 0;">
+              <span style="color: #909399;">昵称:</span>
+              <span style="color: #303133;">{{ currentDetailRow.companyUserName || '-' }}</span>
+            </p>
+          </div>
+        </div>
+
+        <!-- 绑定微信用户信息 -->
+        <div v-if="currentDetailRow.bindUserId && boundUserDetail" class="bind-section" style="margin-bottom: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-chat-dot-round"></i> 绑定微信用户
+          </div>
+          <div style="text-align: center; padding: 16px 0;">
+            <el-popover v-if="boundUserDetail.avatar" placement="right" trigger="hover">
+              <img
+                slot="reference"
+                :src="boundUserDetail.avatar"
+                style="width: 56px; height: 56px; border-radius: 50%; object-fit: cover"
+              />
+              <img :src="boundUserDetail.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+            </el-popover>
+            <div v-else style="width: 56px; height: 56px; border-radius: 50%; background: #eee; display: inline-block; line-height: 56px; color: #999;">无</div>
+            <p style="margin-top: 8px; font-size: 14px;">
+              <span style="color: #909399;">用户ID:</span>
+              <span style="color: #303133;">{{ boundUserDetail.userId }}</span>
+            </p>
+            <p style="font-size: 14px;">
+              <span style="color: #909399;">昵称:</span>
+              <span style="color: #303133;">{{ boundUserDetail.nickName || '-' }}</span>
+            </p>
+          </div>
+        </div>
+
+        <!-- 外部联系人信息 -->
+        <div v-if="currentDetailRow.type === 2 && currentDetailRow.externalContactId" class="bind-section" style="margin-bottom: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-connection"></i> 外部联系人
+          </div>
+          <div style="text-align: center; padding: 16px 0;">
+            <el-popover v-if="currentDetailRow.externalContactAvatar" placement="right" trigger="hover">
+              <img
+                slot="reference"
+                :src="currentDetailRow.externalContactAvatar"
+                style="width: 56px; height: 56px; border-radius: 50%; object-fit: cover"
+              />
+              <img :src="currentDetailRow.externalContactAvatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+            </el-popover>
+            <div v-else style="width: 56px; height: 56px; border-radius: 50%; background: #eee; display: inline-block; line-height: 56px; color: #999;">无</div>
+            <p style="margin-top: 8px; font-size: 14px;">
+              <span style="color: #909399;">外部联系人ID:</span>
+              <span style="color: #303133;">{{ currentDetailRow.externalContactId }}</span>
+            </p>
+            <p style="font-size: 14px;">
+              <span style="color: #909399;">昵称:</span>
+              <span style="color: #303133;">{{ currentDetailRow.externalContactName || '-' }}</span>
+            </p>
+          </div>
+        </div>
+
+        <!-- 看课项目 -->
+        <div v-if="currentDetailRow.projectId" class="bind-section" style="margin-bottom: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-s-flag"></i> 看课项目
+          </div>
+          <div style="text-align: center; padding: 12px 0;">
+            <p style="font-size: 14px; margin: 0;">
+              <span style="color: #303133;">{{ projectMap[currentDetailRow.projectId] || currentDetailRow.projectId }}</span>
+            </p>
+          </div>
+        </div>
+
+        <p style="text-align: center; font-size: 14px;">
+          <span style="color: #909399;">绑定方式:</span>
+          <el-tag v-if="currentDetailRow.type === 2" type="warning" size="mini">自动发课</el-tag>
+          <el-tag v-else type="primary" size="mini">手动发课</el-tag>
+        </p>
+      </div>
+      <div v-else v-loading="detailLoading" style="min-height: 80px;" />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listUserBindWx, decryptPhone, getBoundUserInfo, getCompanyUserList, batchAssign, unassignCompanyUser } from '@/api/company/userBindWx'
+
+export default {
+  name: 'CompanyUserBindWxAssign',
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      userList: [],
+      selectedIds: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: undefined,
+        nickName: undefined,
+        phone: undefined,
+        bindStatus: undefined,
+        assignStatus: undefined
+      },
+      phoneMap: {},
+      // 批量分配
+      assignDialogVisible: false,
+      assignLoading: false,
+      assignCompanyUserId: undefined,
+      companyUserList: [],
+      // 详情弹窗
+      currentDetailRow: null,
+      detailDialogVisible: false,
+      detailLoading: false,
+      boundUserDetail: null,
+      projectMap: {}
+    }
+  },
+  created() {
+    this.getList()
+    this.getDicts('sys_course_project').then(res => {
+      const data = res.data || []
+      data.forEach(item => {
+        this.projectMap[Number(item.dictValue)] = item.dictLabel
+      })
+    })
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listUserBindWx(this.queryParams).then(response => {
+        this.userList = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDecryptPhone(encryptedPhone) {
+      decryptPhone(encryptedPhone).then(response => {
+        this.$set(this.phoneMap, encryptedPhone, response.data)
+      })
+    },
+    formatDate(timestamp) {
+      if (!timestamp) return ''
+      const date = new Date(timestamp)
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hours = String(date.getHours()).padStart(2, '0')
+      const minutes = String(date.getMinutes()).padStart(2, '0')
+      const seconds = String(date.getSeconds()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+    },
+    handleSelectionChange(selection) {
+      this.selectedIds = selection.map(item => item.id)
+    },
+    // 批量分配
+    handleBatchAssign() {
+      if (this.selectedIds.length === 0) {
+        this.$message.warning('请先勾选会员')
+        return
+      }
+      this.assignCompanyUserId = undefined
+      this.assignDialogVisible = true
+      getCompanyUserList().then(response => {
+        this.companyUserList = response.data
+      })
+    },
+    confirmAssign() {
+      if (!this.assignCompanyUserId) {
+        this.$message.warning('请选择销售人员')
+        return
+      }
+      this.assignLoading = true
+      batchAssign({
+        ids: this.selectedIds,
+        companyUserId: this.assignCompanyUserId
+      }).then(response => {
+        this.$message.success(response.msg || '分配成功')
+        this.assignDialogVisible = false
+        this.assignLoading = false
+        this.getList()
+      }).catch(() => {
+        this.assignLoading = false
+      })
+    },
+    // 详情
+    handleShowDetail(row) {
+      this.currentDetailRow = row
+      this.boundUserDetail = null
+      this.detailDialogVisible = true
+      if (row.bindUserId) {
+        this.detailLoading = true
+        getBoundUserInfo(row.bindUserId).then(response => {
+          this.boundUserDetail = response.data
+          this.detailLoading = false
+        }).catch(() => {
+          this.detailLoading = false
+        })
+      } else {
+        this.detailLoading = false
+      }
+    },
+    // 解除分配
+    handleUnassign(row) {
+      this.$confirm('确认解除该会员的销售分配?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        unassignCompanyUser(row.id).then(response => {
+          this.$message.success(response.msg || '解除分配成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.bind-section {
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  overflow: hidden;
+}
+.bind-section-title {
+  padding: 10px 16px;
+  background: #fafafa;
+  border-bottom: 1px solid #ebeef5;
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+.bind-section-title i {
+  margin-right: 6px;
+  color: #409eff;
+}
+</style>

+ 689 - 0
src/views/app/userBindWx/index.vue

@@ -0,0 +1,689 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号码" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="微信绑定" prop="bindStatus">
+        <el-select v-model="queryParams.bindStatus" placeholder="请选择绑定状态" clearable size="small">
+          <el-option label="未绑定" :value="0" />
+          <el-option label="已绑定" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="userList">
+      <el-table-column label="会员ID" align="center" prop="userId" width="120" />
+      <el-table-column label="用户昵称" align="center" prop="nickName" min-width="150" />
+      <el-table-column label="会员头像" align="center" width="80">
+        <template slot-scope="scope">
+          <el-popover v-if="scope.row.avatar" placement="right" trigger="hover">
+            <img
+              slot="reference"
+              :src="scope.row.avatar"
+              style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover"
+            />
+            <img :src="scope.row.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+          </el-popover>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号码" align="center" min-width="140">
+        <template slot-scope="scope">
+          <span>{{ phoneMap[scope.row.phone] || scope.row.phone }}</span>
+          <el-button
+            v-if="scope.row.phone && !phoneMap[scope.row.phone]"
+            type="text"
+            icon="el-icon-search"
+            size="mini"
+            title="查看手机号"
+            @click="handleDecryptPhone(scope.row.phone)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="微信绑定" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.bindUserId != null || scope.row.externalContactId != null" type="success" size="mini">已绑定</el-tag>
+          <el-tag v-else type="info" size="mini">未绑定</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="分配时间" align="center" width="180">
+        <template slot-scope="scope">
+          <span>{{ formatDate(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="180" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.bindUserId == null && scope.row.externalContactId == null"
+            type="text"
+            size="mini"
+            @click="handleOpenBind(scope.row)"
+          >绑定用户微信</el-button>
+          <template v-else>
+            <el-button
+              type="text"
+              size="mini"
+              @click="handleShowDetail(scope.row)"
+            >详情</el-button>
+            <el-button
+              type="text"
+              size="mini"
+              style="color: #F56C6C;"
+              @click="handleUnbind(scope.row)"
+            >解绑</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 绑定微信用户弹窗 -->
+    <el-dialog title="选择要绑定的微信用户" :visible.sync="bindDialogVisible" width="780px" append-to-body :close-on-click-modal="false">
+      <div class="bind-mode-switch">
+        <span class="bind-mode-label">绑定方式:</span>
+        <el-radio-group v-model="bindMode" size="small">
+          <el-radio-button label="manual">
+            <i class="el-icon-user"></i> 手动发课
+          </el-radio-button>
+          <el-radio-button label="auto">
+            <i class="el-icon-connection"></i> 自动发课
+          </el-radio-button>
+        </el-radio-group>
+      </div>
+
+      <!-- 手动发课:会员列表 + 看课项目 -->
+      <template v-if="bindMode === 'manual'">
+        <div class="bind-section" style="margin-top: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-search"></i> 会员列表
+          </div>
+          <el-form :model="bindSearchParams" :inline="true" size="small">
+            <el-form-item label="会员ID">
+              <el-input v-model="bindSearchParams.userId" placeholder="请输入会员ID" clearable @keyup.enter.native="searchBindUserList" />
+            </el-form-item>
+            <el-form-item label="用户昵称">
+              <el-input v-model="bindSearchParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter.native="searchBindUserList" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="el-icon-search" size="mini" @click="searchBindUserList">搜索</el-button>
+              <el-button icon="el-icon-refresh" size="mini" @click="resetBindSearch">重置</el-button>
+            </el-form-item>
+          </el-form>
+          <el-table ref="manualMemberTable" border v-loading="bindListLoading" :data="bindUserList" @selection-change="handleManualMemberSelect" max-height="260">
+            <el-table-column type="selection" width="45" align="center" />
+            <el-table-column label="会员ID" align="center" prop="userId" width="120" />
+            <el-table-column label="用户昵称" align="center" prop="nickName" min-width="160" />
+            <el-table-column label="头像" align="center" width="70">
+              <template slot-scope="scope">
+                <el-popover v-if="scope.row.avatar" placement="right" trigger="hover">
+                  <img
+                    slot="reference"
+                    :src="scope.row.avatar"
+                    style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover"
+                  />
+                  <img :src="scope.row.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+                </el-popover>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+          </el-table>
+          <pagination
+            v-show="bindTotal > 0"
+            :total="bindTotal"
+            :page.sync="bindSearchParams.pageNum"
+            :limit.sync="bindSearchParams.pageSize"
+            @pagination="searchBindUserList"
+            layout="total, prev, pager, next"
+            small
+          />
+        </div>
+        <el-divider />
+        <el-form ref="projectForm" size="small" label-width="80px">
+          <el-form-item label="看课项目" prop="projectId" :rules="[{ required: true, message: '请选择看课项目', trigger: 'change' }]">
+            <el-select v-model="projectId" placeholder="请选择看课项目" clearable style="width: 100%;">
+              <el-option
+                v-for="item in projectOptions"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="Number(item.dictValue)"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </template>
+
+      <!-- 自动发课:会员列表 + 外部联系人 -->
+      <template v-else>
+        <div class="bind-section" style="margin-top: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-user"></i> 会员列表
+          </div>
+          <el-form :model="bindSearchParams" :inline="true" size="small">
+            <el-form-item label="会员ID">
+              <el-input v-model="bindSearchParams.userId" placeholder="请输入会员ID" clearable @keyup.enter.native="searchBindUserList" />
+            </el-form-item>
+            <el-form-item label="用户昵称">
+              <el-input v-model="bindSearchParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter.native="searchBindUserList" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="el-icon-search" size="mini" @click="searchBindUserList">搜索</el-button>
+              <el-button icon="el-icon-refresh" size="mini" @click="resetBindSearch">重置</el-button>
+            </el-form-item>
+          </el-form>
+          <el-table ref="autoMemberTable" border v-loading="bindListLoading" :data="bindUserList" @selection-change="handleAutoMemberSelect" max-height="200">
+            <el-table-column type="selection" width="45" align="center" />
+            <el-table-column label="会员ID" align="center" prop="userId" width="120" />
+            <el-table-column label="用户昵称" align="center" prop="nickName" min-width="160" />
+            <el-table-column label="头像" align="center" width="70">
+              <template slot-scope="scope">
+                <el-popover v-if="scope.row.avatar" placement="right" trigger="hover">
+                  <img
+                    slot="reference"
+                    :src="scope.row.avatar"
+                    style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover"
+                  />
+                  <img :src="scope.row.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+                </el-popover>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+          </el-table>
+          <pagination
+            v-show="bindTotal > 0"
+            :total="bindTotal"
+            :page.sync="bindSearchParams.pageNum"
+            :limit.sync="bindSearchParams.pageSize"
+            @pagination="searchBindUserList"
+            layout="total, prev, pager, next"
+            small
+          />
+        </div>
+        <div class="bind-section" style="margin-top: 16px;">
+          <div class="bind-section-title">
+            <i class="el-icon-connection"></i> 外部联系人
+          </div>
+          <el-form :model="externalContactSearchParams" :inline="true" size="small">
+            <el-form-item label="用户昵称">
+              <el-input v-model="externalContactSearchParams.name" placeholder="请输入用户昵称" clearable @keyup.enter.native="searchExternalContactList" />
+            </el-form-item>
+            <el-form-item label="企业微信">
+              <el-input v-model="externalContactSearchParams.corpName" placeholder="请输入企业微信" clearable @keyup.enter.native="searchExternalContactList" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="el-icon-search" size="mini" @click="searchExternalContactList">搜索</el-button>
+              <el-button icon="el-icon-refresh" size="mini" @click="resetExternalSearch">重置</el-button>
+            </el-form-item>
+          </el-form>
+          <el-table ref="externalContactTable" border v-loading="externalListLoading" :data="externalContactList" @selection-change="handleExternalSelect" max-height="200">
+            <el-table-column type="selection" width="45" align="center" />
+            <el-table-column label="外部联系人ID" align="center" prop="id" width="110" />
+            <el-table-column label="头像" align="center" width="60">
+              <template slot-scope="scope">
+                <el-popover v-if="scope.row.avatar" placement="right" trigger="hover">
+                  <img
+                    slot="reference"
+                    :src="scope.row.avatar"
+                    style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover"
+                  />
+                  <img :src="scope.row.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+                </el-popover>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="用户昵称" align="center" prop="name" min-width="120" />
+            <el-table-column label="企业微信" align="center" prop="corpName" min-width="150" />
+          </el-table>
+          <pagination
+            v-show="externalTotal > 0"
+            :total="externalTotal"
+            :page.sync="externalContactSearchParams.pageNum"
+            :limit.sync="externalContactSearchParams.pageSize"
+            @pagination="searchExternalContactList"
+            layout="total, prev, pager, next"
+            small
+          />
+        </div>
+      </template>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="bindDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="bindConfirmLoading" :disabled="!canConfirmBind" @click="confirmBind">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 绑定用户详情弹窗 -->
+    <el-dialog title="绑定用户详情" :visible.sync="detailDialogVisible" width="450px" append-to-body>
+      <div v-if="boundUserDetail || currentDetailRow" v-loading="detailLoading">
+        <!-- 自动发课:会员信息 + 外部联系人信息 -->
+        <template v-if="currentDetailRow && currentDetailRow.type === 2">
+          <!-- 会员信息 -->
+          <div class="bind-section" style="margin-bottom: 16px;">
+            <div class="bind-section-title">
+              <i class="el-icon-user"></i> 绑定会员
+            </div>
+            <div style="text-align: center; padding: 16px 0;">
+              <el-popover v-if="boundUserDetail && boundUserDetail.avatar" placement="right" trigger="hover">
+                <img
+                  slot="reference"
+                  :src="boundUserDetail.avatar"
+                  style="width: 56px; height: 56px; border-radius: 50%; object-fit: cover"
+                />
+                <img :src="boundUserDetail.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+              </el-popover>
+              <div v-else style="width: 56px; height: 56px; border-radius: 50%; background: #eee; display: inline-block; line-height: 56px; color: #999;">无</div>
+              <p style="margin-top: 8px; font-size: 14px;">
+                <span style="color: #909399;">会员ID:</span>
+                <span style="color: #303133; font-weight: 500;">{{ boundUserDetail && boundUserDetail.userId }}</span>
+              </p>
+              <p style="font-size: 14px;">
+                <span style="color: #909399;">昵称:</span>
+                <span style="color: #303133;">{{ boundUserDetail ? (boundUserDetail.nickName || '-') : '-' }}</span>
+              </p>
+            </div>
+          </div>
+          <!-- 外部联系人信息 -->
+          <div class="bind-section">
+            <div class="bind-section-title">
+              <i class="el-icon-connection"></i> 外部联系人
+            </div>
+            <div style="text-align: center; padding: 16px 0;">
+              <el-popover v-if="currentDetailRow.externalContactAvatar" placement="right" trigger="hover">
+                <img
+                  slot="reference"
+                  :src="currentDetailRow.externalContactAvatar"
+                  style="width: 56px; height: 56px; border-radius: 50%; object-fit: cover"
+                />
+                <img :src="currentDetailRow.externalContactAvatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+              </el-popover>
+              <div v-else style="width: 56px; height: 56px; border-radius: 50%; background: #eee; display: inline-block; line-height: 56px; color: #999;">无</div>
+              <p style="margin-top: 8px; font-size: 14px;">
+                <span style="color: #909399;">外部联系人ID:</span>
+                <span style="color: #303133;">{{ currentDetailRow.externalContactId }}</span>
+              </p>
+              <p style="font-size: 14px;">
+                <span style="color: #909399;">昵称:</span>
+                <span style="color: #303133;">{{ currentDetailRow.externalContactName || '-' }}</span>
+              </p>
+            </div>
+          </div>
+          <p style="text-align: center; margin-top: 12px; font-size: 14px;">
+            <span style="color: #909399;">类型:</span>
+            <el-tag type="warning" size="mini">自动发课</el-tag>
+          </p>
+        </template>
+        <!-- 手动发课:会员信息 + 看课项目 -->
+        <template v-else>
+          <div style="text-align: center;">
+            <el-popover v-if="boundUserDetail && boundUserDetail.avatar" placement="right" trigger="hover">
+              <img
+                slot="reference"
+                :src="boundUserDetail.avatar"
+                style="width: 64px; height: 64px; border-radius: 50%; object-fit: cover"
+              />
+              <img :src="boundUserDetail.avatar" style="max-width: 200px; max-height: 200px; border-radius: 4px" />
+            </el-popover>
+            <div v-else style="width: 64px; height: 64px; border-radius: 50%; background: #eee; display: inline-block; line-height: 64px; color: #999;">无</div>
+            <p style="margin-top: 12px; font-size: 14px;">
+              <span style="color: #909399;">会员ID:</span>
+              <span style="color: #303133; font-weight: 500;">{{ boundUserDetail && boundUserDetail.userId }}</span>
+            </p>
+            <p style="font-size: 14px;">
+              <span style="color: #909399;">昵称:</span>
+              <span style="color: #303133;">{{ boundUserDetail ? (boundUserDetail.nickName || '-') : '-' }}</span>
+            </p>
+            <p v-if="currentDetailRow && currentDetailRow.projectId" style="font-size: 14px;">
+              <span style="color: #909399;">看课项目:</span>
+              <span style="color: #303133;">{{ projectMap[currentDetailRow.projectId] || currentDetailRow.projectId }}</span>
+            </p>
+            <p style="font-size: 14px;">
+              <span style="color: #909399;">类型:</span>
+              <el-tag type="primary" size="mini">手动发课</el-tag>
+            </p>
+          </div>
+        </template>
+      </div>
+      <div v-else v-loading="detailLoading" style="min-height: 80px;" />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listUserBindWx, decryptPhone, searchBindUser, bindWxUser, unbindWxUser, getBoundUserInfo, searchExternalContact } from '@/api/company/userBindWx'
+
+export default {
+  name: 'CompanyUserBindWx',
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      userList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: undefined,
+        nickName: undefined,
+        phone: undefined,
+        bindStatus: undefined
+      },
+      phoneMap: {},
+      // 绑定弹窗
+      bindDialogVisible: false,
+      bindListLoading: false,
+      bindConfirmLoading: false,
+      currentBindRow: null,
+      selectedBindUser: null,
+      bindUserList: [],
+      bindTotal: 0,
+      bindSearchParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: undefined,
+        nickName: undefined
+      },
+      // 绑定模式:manual=手动 auto=自动
+      bindMode: 'manual',
+      // 外部联系人
+      externalContactSearchParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: undefined,
+        corpName: undefined
+      },
+      externalContactList: [],
+      externalTotal: 0,
+      externalListLoading: false,
+      selectedExternalContact: null,
+      selectedAutoMember: null,
+      // 看课项目
+      projectId: null,
+      projectOptions: [],
+      projectMap: {},
+      // 详情弹窗
+      currentDetailRow: null,
+      detailDialogVisible: false,
+      detailLoading: false,
+      boundUserDetail: null
+    }
+  },
+  computed: {
+    canConfirmBind() {
+      if (this.bindMode === 'manual') {
+        return this.selectedBindUser != null && this.projectId != null
+      }
+      return this.selectedAutoMember != null && this.selectedExternalContact != null
+    }
+  },
+  watch: {
+    bindMode(val) {
+      // 切换模式时清除选中
+      this.selectedBindUser = null
+      this.selectedAutoMember = null
+      this.selectedExternalContact = null
+      this.projectId = null
+      if (val === 'auto') {
+        this.searchExternalContactList()
+        this.searchBindUserList()
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.getDicts('sys_course_project').then(response => {
+      this.projectOptions = response.data || []
+      // 构建 projectId -> label 映射
+      const map = {}
+      ;(response.data || []).forEach(item => {
+        map[Number(item.dictValue)] = item.dictLabel
+      })
+      this.projectMap = map
+    })
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listUserBindWx(this.queryParams).then(response => {
+        this.userList = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDecryptPhone(encryptedPhone) {
+      decryptPhone(encryptedPhone).then(response => {
+        this.$set(this.phoneMap, encryptedPhone, response.data)
+      })
+    },
+    formatDate(timestamp) {
+      if (!timestamp) return ''
+      const date = new Date(timestamp)
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hours = String(date.getHours()).padStart(2, '0')
+      const minutes = String(date.getMinutes()).padStart(2, '0')
+      const seconds = String(date.getSeconds()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+    },
+    // 绑定微信用户相关
+    handleOpenBind(row) {
+      this.currentBindRow = row
+      this.bindMode = 'manual'
+      this.selectedBindUser = null
+      this.selectedAutoMember = null
+      this.selectedExternalContact = null
+      this.projectId = null
+      this.bindSearchParams = { pageNum: 1, pageSize: 10, userId: undefined, nickName: undefined }
+      this.externalContactSearchParams = { pageNum: 1, pageSize: 10, name: undefined, corpName: undefined }
+      this.bindDialogVisible = true
+      this.searchBindUserList()
+    },
+    searchBindUserList() {
+      this.bindListLoading = true
+      searchBindUser(this.bindSearchParams).then(response => {
+        this.bindUserList = response.rows
+        this.bindTotal = response.total
+        this.bindListLoading = false
+      }).catch(() => {
+        this.bindListLoading = false
+      })
+    },
+    resetBindSearch() {
+      this.bindSearchParams = { pageNum: 1, pageSize: 10, userId: undefined, nickName: undefined }
+      this.searchBindUserList()
+    },
+    // 通用:单选框限制只能选一个
+    singleSelect(tableRef, selection) {
+      if (selection.length > 1) {
+        const refTable = this.$refs[tableRef]
+        if (refTable) {
+          refTable.clearSelection()
+          refTable.toggleRowSelection(selection[selection.length - 1])
+        }
+        return selection[selection.length - 1]
+      }
+      return selection.length === 1 ? selection[0] : null
+    },
+    // 手动发课:会员勾选
+    handleManualMemberSelect(selection) {
+      this.selectedBindUser = this.singleSelect('manualMemberTable', selection)
+    },
+    // 自动发课:会员列表勾选
+    handleAutoMemberSelect(selection) {
+      this.selectedAutoMember = this.singleSelect('autoMemberTable', selection)
+    },
+    // 自动发课:外部联系人勾选
+    handleExternalSelect(selection) {
+      this.selectedExternalContact = this.singleSelect('externalContactTable', selection)
+    },
+    confirmBind() {
+      const isAuto = this.bindMode === 'auto'
+      if (isAuto && (!this.selectedAutoMember || !this.selectedExternalContact)) {
+        this.$message.warning('请分别勾选一个会员和一个外部联系人')
+        return
+      }
+      if (!isAuto && !this.selectedBindUser) {
+        this.$message.warning('请选择一个用户')
+        return
+      }
+      this.bindConfirmLoading = true
+      const params = {
+        bindId: this.currentBindRow.id
+      }
+      if (isAuto) {
+        params.bindUserId = this.selectedAutoMember.userId
+        params.externalContactId = this.selectedExternalContact.id
+      } else {
+        params.bindUserId = this.selectedBindUser.userId
+        params.projectId = this.projectId
+      }
+      bindWxUser(params).then(response => {
+        this.$message.success(response.msg || '绑定成功')
+        this.bindDialogVisible = false
+        this.bindConfirmLoading = false
+        this.getList()
+      }).catch(() => {
+        this.bindConfirmLoading = false
+      })
+    },
+    handleUnbind(row) {
+      this.$confirm('确认解绑该用户的微信绑定信息?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        unbindWxUser(row.id).then(response => {
+          this.$message.success(response.msg || '解绑成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    },
+    handleShowDetail(row) {
+      this.currentDetailRow = row
+      this.boundUserDetail = null
+      this.detailDialogVisible = true
+      // 查询微信用户详情
+      if (row.bindUserId) {
+        this.detailLoading = true
+        getBoundUserInfo(row.bindUserId).then(response => {
+          this.boundUserDetail = response.data
+          this.detailLoading = false
+        }).catch(() => {
+          this.detailLoading = false
+        })
+      } else {
+        this.detailLoading = false
+      }
+    },
+    // 外部联系人列表
+    searchExternalContactList() {
+      this.externalListLoading = true
+      searchExternalContact(this.externalContactSearchParams).then(response => {
+        this.externalContactList = response.rows
+        this.externalTotal = response.total
+        this.externalListLoading = false
+      }).catch(() => {
+        this.externalListLoading = false
+      })
+    },
+    resetExternalSearch() {
+      this.externalContactSearchParams = { pageNum: 1, pageSize: 10, name: undefined, corpName: undefined }
+      this.searchExternalContactList()
+    },
+  }
+}
+</script>
+
+<style scoped>
+.bind-mode-switch {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 12px 16px;
+  background: #f5f7fa;
+  border-radius: 6px;
+}
+.bind-mode-label {
+  font-size: 14px;
+  color: #606266;
+  margin-right: 12px;
+  font-weight: 500;
+}
+.bind-section {
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  overflow: hidden;
+}
+.bind-section-title {
+  padding: 10px 16px;
+  background: #fafafa;
+  border-bottom: 1px solid #ebeef5;
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+.bind-section-title i {
+  margin-right: 6px;
+  color: #409eff;
+}
+.bind-section .el-form {
+  padding: 12px 16px 0;
+}
+.bind-section .el-table {
+  border-top: none;
+  border-left: none;
+  border-right: none;
+}
+.bind-section .pagination-container {
+  padding: 0 16px;
+}
+</style>