yjwang 11 годин тому
батько
коміт
aea4e13e73
2 змінених файлів з 492 додано та 0 видалено
  1. 60 0
      src/api/his/userBindWx.js
  2. 432 0
      src/views/his/userBindWx/unassigned.vue

+ 60 - 0
src/api/his/userBindWx.js

@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询未分配用户列表
+export function unassignedList(query) {
+  return request({
+    url: '/his/userBindWx/unassignedList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 批量绑定用户到销售公司
+export function batchBind(data) {
+  return request({
+    url: '/his/userBindWx/batchBind',
+    method: 'post',
+    data: data
+  })
+}
+
+// 查询用户详情及绑定列表
+export function detail(fsUserId) {
+  return request({
+    url: '/his/userBindWx/detail/' + fsUserId,
+    method: 'get'
+  })
+}
+
+// 解绑指定绑定记录
+export function unbind(id) {
+  return request({
+    url: '/his/userBindWx/unbind/' + id,
+    method: 'delete'
+  })
+}
+
+// 查询销售公司列表
+export function companyList() {
+  return request({
+    url: '/his/company/companyList',
+    method: 'get'
+  })
+}
+
+// 解码手机号
+export function decryptPhone(phone) {
+  return request({
+    url: '/his/userBindWx/decryptPhone',
+    method: 'post',
+    data: { phone }
+  })
+}
+
+// 查询绑定微信用户详情
+export function getBoundUserInfo(userId) {
+  return request({
+    url: '/his/userBindWx/boundUserInfo/' + userId,
+    method: 'get'
+  })
+}

+ 432 - 0
src/views/his/userBindWx/unassigned.vue

@@ -0,0 +1,432 @@
+<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">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          :disabled="selectionList.length === 0"
+          @click="handleBatchBind"
+          v-hasPermi="['his:userBindWx:add']"
+        >批量分配</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['his:userBindWx:query']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="50" align="center" />
+      <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-image
+            v-if="scope.row.avatar"
+            style="width: 40px; height: 40px; border-radius: 50%"
+            :src="scope.row.avatar"
+            fit="cover"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号码" align="center" prop="phone" min-width="130">
+        <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.bindStatus == 1" 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>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="100" fixed="right">
+        <template slot-scope="scope">
+          <el-button type="text" size="mini" @click="handleDetail(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="bindDialogVisible" width="500px" append-to-body>
+      <el-form ref="bindForm" :model="bindForm" label-width="100px">
+        <el-form-item label="已选用户">
+          <el-tag v-for="item in selectionList" :key="item.userId" style="margin: 2px 4px 2px 0;" size="small">
+            {{ item.nickName || item.userId }}
+          </el-tag>
+        </el-form-item>
+        <el-form-item label="销售公司" prop="companyId" :rules="[{ required: true, message: '请选择销售公司', trigger: 'change' }]">
+          <el-select v-model="bindForm.companyId" placeholder="请选择销售公司" filterable clearable style="width: 100%">
+            <el-option
+              v-for="item in companyOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="bindDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="bindLoading" @click="submitBind">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 用户详情弹窗 -->
+    <el-dialog title="用户详情" :visible.sync="detailDialogVisible" width="600px" append-to-body>
+      <div v-loading="detailLoading">
+        <template v-if="detailData.userInfo && detailData.userInfo.userId">
+          <!-- 基础信息 -->
+          <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-image
+                v-if="detailData.userInfo.avatar"
+                style="width: 56px; height: 56px; border-radius: 50%;"
+                :src="detailData.userInfo.avatar"
+                fit="cover"
+              />
+              <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;">{{ detailData.userInfo.userId }}</span>
+              </p>
+              <p style="font-size: 14px;">
+                <span style="color: #909399;">昵称:</span>
+                <span style="color: #303133;">{{ detailData.userInfo.nickName || '-' }}</span>
+              </p>
+              <p style="font-size: 14px;">
+                <span style="color: #909399;">手机号码:</span>
+                <span>{{ phoneMap[detailData.userInfo.phone] || detailData.userInfo.phone || '-' }}</span>
+                <el-button
+                  v-if="detailData.userInfo.phone && !phoneMap[detailData.userInfo.phone]"
+                  type="text"
+                  icon="el-icon-search"
+                  size="mini"
+                  title="查看手机号"
+                  @click="handleDecryptPhone(detailData.userInfo.phone)"
+                />
+              </p>
+            </div>
+          </div>
+
+          <!-- 绑定记录 -->
+          <div v-if="detailData.bindList && detailData.bindList.length > 0">
+            <el-divider content-position="left">绑定记录</el-divider>
+            <div v-for="(bind, index) in detailData.bindList" :key="bind.id" class="bind-section" style="margin-bottom: 16px;">
+              <div class="bind-section-title">
+                <i class="el-icon-office-building"></i> 绑定{{ index + 1 }}
+                <el-tag v-if="bind.type === 2" type="warning" size="mini" style="margin-left: 8px;">自动发课</el-tag>
+                <el-tag v-else type="primary" size="mini" style="margin-left: 8px;">手动发课</el-tag>
+              </div>
+              <div style="padding: 12px 16px;">
+                <!-- 公司信息 -->
+                <p style="font-size: 14px; margin: 8px 0;">
+                  <span style="color: #909399;">销售公司ID:</span>
+                  <span style="color: #303133;">{{ bind.companyId || '-' }}</span>
+                </p>
+                <p style="font-size: 14px; margin: 8px 0;">
+                  <span style="color: #909399;">公司名称:</span>
+                  <span style="color: #303133;">{{ bind.companyName || '-' }}</span>
+                </p>
+                <!-- 销售信息 -->
+                <p v-if="bind.companyUserId" style="font-size: 14px; margin: 8px 0;">
+                  <span style="color: #909399;">销售ID:</span>
+                  <span style="color: #303133;">{{ bind.companyUserId }}</span>
+                </p>
+                <p v-if="bind.companyUserAccount" style="font-size: 14px; margin: 8px 0;">
+                  <span style="color: #909399;">销售账号:</span>
+                  <span style="color: #303133;">{{ bind.companyUserAccount }}</span>
+                </p>
+                <p v-if="bind.companyUserName" style="font-size: 14px; margin: 8px 0;">
+                  <span style="color: #909399;">销售昵称:</span>
+                  <span style="color: #303133;">{{ bind.companyUserName }}</span>
+                </p>
+                <!-- 绑定微信用户 -->
+                <div v-if="bind.bindUserId && boundUserMap[bind.id]" style="margin-top: 8px; padding: 8px; background: #f5f7fa; border-radius: 6px;">
+                  <p style="margin: 0 0 4px 0; font-size: 13px; color: #909399;">
+                    绑定微信用户ID:{{ bind.bindUserId }}
+                  </p>
+                  <span style="color: #909399; font-size: 13px;">微信用户:</span>
+                  <el-image
+                    v-if="boundUserMap[bind.id].avatar"
+                    style="width: 32px; height: 32px; border-radius: 50%; vertical-align: middle; margin: 0 8px;"
+                    :src="boundUserMap[bind.id].avatar"
+                    fit="cover"
+                  />
+                  <span style="color: #303133; font-size: 13px;">{{ boundUserMap[bind.id].nickName || boundUserMap[bind.id].userId || '-' }}</span>
+                </div>
+                <!-- 外部联系人 -->
+                <div v-if="bind.externalContactId" style="margin-top: 8px; padding: 8px; background: #f5f7fa; border-radius: 6px;">
+                  <p style="margin: 0 0 4px 0; font-size: 13px; color: #909399;">
+                    外部联系人ID:{{ bind.externalContactId }}
+                  </p>
+                  <span style="color: #909399; font-size: 13px;">外部联系人:</span>
+                  <el-image
+                    v-if="bind.externalContactAvatar"
+                    style="width: 32px; height: 32px; border-radius: 50%; vertical-align: middle; margin: 0 8px;"
+                    :src="bind.externalContactAvatar"
+                    fit="cover"
+                  />
+                  <span style="color: #303133; font-size: 13px;">{{ bind.externalContactName || '-' }}</span>
+                </div>
+                <!-- 看课项目 -->
+                <div v-if="bind.projectId" style="margin-top: 8px; padding: 8px; background: #f5f7fa; border-radius: 6px;">
+                  <span style="color: #909399; font-size: 13px;">看课项目:</span>
+                  <span style="color: #303133; font-size: 13px;">{{ projectMap[bind.projectId] || bind.projectId }}</span>
+                </div>
+                <div style="margin-top: 8px;">
+                  <el-button type="text" size="mini" style="color: #f56c6c;" @click="handleUnbind(bind)">
+                    解绑
+                  </el-button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <el-empty v-else description="暂无绑定记录" :image-size="80" />
+        </template>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { unassignedList, batchBind, detail, unbind, companyList, decryptPhone, getBoundUserInfo } from '@/api/his/userBindWx'
+
+export default {
+  name: 'UnassignedUser',
+  data() {
+    return {
+      loading: false,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      userList: [],
+      selectionList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: undefined,
+        nickName: undefined,
+        phone: undefined,
+        bindStatus: undefined
+      },
+      // 批量分配
+      bindDialogVisible: false,
+      bindLoading: false,
+      bindForm: { companyId: undefined },
+      companyOptions: [],
+      // 详情
+      detailDialogVisible: false,
+      detailLoading: false,
+      detailData: { userInfo: {}, bindList: [] },
+      boundUserMap: {},
+      // 手机号解码缓存
+      phoneMap: {},
+      projectMap: {}
+    }
+  },
+  created() {
+    this.getList()
+    this.loadCompanyList()
+    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
+      unassignedList(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()
+    },
+    handleSelectionChange(selection) {
+      this.selectionList = selection
+    },
+    handleExport() {
+      // 暂无导出逻辑
+    },
+    // 加载销售公司列表
+    loadCompanyList() {
+      companyList().then(response => {
+        this.companyOptions = response.data || []
+      })
+    },
+    // 批量分配
+    handleBatchBind() {
+      if (this.selectionList.length === 0) {
+        this.$message.warning('请先选择用户')
+        return
+      }
+      this.bindForm.companyId = undefined
+      this.bindDialogVisible = true
+    },
+    submitBind() {
+      this.$refs.bindForm.validate(valid => {
+        if (!valid) return
+        this.bindLoading = true
+        const userIds = this.selectionList.map(item => item.userId)
+        batchBind({ userIds, companyId: this.bindForm.companyId }).then(response => {
+          this.$message.success(response.msg || '批量分配成功')
+          this.bindDialogVisible = false
+          this.bindLoading = false
+          this.getList()
+        }).catch(() => {
+          this.bindLoading = false
+        })
+      })
+    },
+    // 查看详情
+    handleDetail(row) {
+      this.detailDialogVisible = true
+      this.detailLoading = true
+      this.detailData = { userInfo: {}, bindList: [] }
+      this.boundUserMap = {}
+      detail(row.userId).then(response => {
+        this.detailData = response
+        this.detailLoading = false
+        // 为每个有 bindUserId 的绑定记录获取微信用户信息
+        const bindList = response.bindList || []
+        bindList.forEach(bind => {
+          if (bind.bindUserId) {
+            getBoundUserInfo(bind.bindUserId).then(res => {
+              this.$set(this.boundUserMap, bind.id, res.data || res)
+            })
+          }
+        })
+      }).catch(() => {
+        this.detailLoading = false
+        this.detailDialogVisible = false
+      })
+    },
+    // 解绑
+    handleUnbind(row) {
+      this.$confirm('确定要解绑该销售与用户的绑定关系吗?', '解绑确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        unbind(row.id).then(() => {
+          this.$message.success('解绑成功')
+          // 刷新详情数据
+          this.detailData.bindList = this.detailData.bindList.filter(item => item.id !== row.id)
+        })
+      }).catch(() => {})
+    },
+    // 解码手机号
+    handleDecryptPhone(encryptedPhone) {
+      decryptPhone(encryptedPhone).then(response => {
+        this.$set(this.phoneMap, encryptedPhone, response.data)
+      })
+    }
+  }
+}
+</script>
+<style scoped>
+.bind-section {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  overflow: hidden;
+}
+.bind-section-title {
+  background: #f5f7fa;
+  padding: 10px 16px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  border-bottom: 1px solid #e4e7ed;
+}
+.bind-section-title i {
+  margin-right: 6px;
+}
+</style>