Pārlūkot izejas kodu

修复同步用户不能翻页多选的问题

liuq 1 mēnesi atpakaļ
vecāks
revīzija
1c06b54523
2 mainītis faili ar 203 papildinājumiem un 14 dzēšanām
  1. 2 0
      frontend/src/api/users.ts
  2. 201 14
      frontend/src/views/apps/AppSync.vue

+ 2 - 0
frontend/src/api/users.ts

@@ -5,6 +5,8 @@ export interface User {
   mobile: string
   role: string
   status: string
+  name?: string
+  english_name?: string
 }
 
 export const searchUsers = (keyword: string) => {

+ 201 - 14
frontend/src/views/apps/AppSync.vue

@@ -29,6 +29,51 @@
         </el-form-item>
 
         <div v-if="form.mode === 'SELECTED'" class="user-selection-area">
+          <!-- 添加搜索框 -->
+          <div class="search-container">
+            <el-input
+              v-model="searchKeyword"
+              placeholder="搜索用户(姓名、手机号、英文名)"
+              clearable
+              style="width: 300px"
+              @clear="handleSearch"
+              @keyup.enter="handleSearch"
+            >
+              <template #prefix>
+                <el-icon><Search /></el-icon>
+              </template>
+              <template #append>
+                <el-button @click="handleSearch">搜索</el-button>
+              </template>
+            </el-input>
+            <div class="selected-count" v-if="selectedUserIds.size > 0">
+              已选择 {{ selectedUserIds.size }} 个用户
+            </div>
+          </div>
+
+          <!-- 已选择用户列表 -->
+          <div v-if="selectedUsersList.length > 0" class="selected-users-panel">
+            <div class="panel-header">
+              <span class="panel-title">已选择的用户 ({{ selectedUsersList.length }})</span>
+              <el-button type="danger" size="small" text @click="clearAllSelection">
+                <el-icon><Delete /></el-icon>
+                清空全部
+              </el-button>
+            </div>
+            <div class="selected-users-list">
+              <el-tag
+                v-for="user in selectedUsersList"
+                :key="user.id"
+                closable
+                @close="removeUser(user.id)"
+                class="user-tag"
+              >
+                {{ user.name || user.english_name || user.mobile }}
+                <span class="user-mobile">({{ user.mobile }})</span>
+              </el-tag>
+            </div>
+          </div>
+
           <el-table
             ref="userTableRef"
             :data="users"
@@ -56,8 +101,8 @@
               :page-sizes="[10, 20, 50, 100]"
               layout="total, sizes, prev, pager, next"
               :total="total"
-              @size-change="fetchUsers"
-              @current-change="fetchUsers"
+              @size-change="handleSizeChange"
+              @current-change="handleCurrentChange"
             />
           </div>
         </div>
@@ -79,7 +124,7 @@
         </el-form-item>
 
         <el-form-item>
-          <el-button type="primary" @click="handlePreSync" :disabled="form.mode === 'SELECTED' && selectedUsers.length === 0">
+          <el-button type="primary" @click="handlePreSync" :disabled="form.mode === 'SELECTED' && selectedUserIds.size === 0">
             开始同步
           </el-button>
         </el-form-item>
@@ -145,12 +190,13 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, computed } from 'vue'
+import { ref, reactive, onMounted, computed, nextTick, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { getApp, Application, syncAppUsersV2 } from '../../api/apps'
 import { getUsers, User } from '../../api/users'
 import { sendImportVerificationCode } from '../../api/mapping' // Reusing the send verification code API
 import { ElMessage } from 'element-plus'
+import { Search, Delete } from '@element-plus/icons-vue'
 
 const route = useRoute()
 const router = useRouter()
@@ -172,7 +218,13 @@ const users = ref<User[]>([])
 const total = ref(0)
 const page = ref(1)
 const pageSize = ref(10)
-const selectedUsers = ref<User[]>([])
+const searchKeyword = ref('')
+
+// 使用 Set 保存所有已选择的用户ID(跨页保存)
+const selectedUserIds = ref<Set<number>>(new Set())
+// 使用 Map 保存已选择用户的详细信息
+const selectedUsersMap = ref<Map<number, User>>(new Map())
+const userTableRef = ref()
 
 // Security
 const confirmDialogVisible = ref(false)
@@ -181,10 +233,23 @@ const securityForm = reactive({
 })
 const timer = ref(0)
 
+// Computed - 已选择用户列表(用于显示)
+const selectedUsersList = computed(() => {
+  return Array.from(selectedUsersMap.value.values())
+})
+
 // Computed
 const syncCount = computed(() => {
   if (form.mode === 'ALL') return total.value // Approximation if we don't fetch all. Ideally backend tells us, but for now we use total.
-  return selectedUsers.value.length
+  return selectedUserIds.value.size
+})
+
+// 监听 mode 变化,切换模式时清空选择
+watch(() => form.mode, (newMode) => {
+  if (newMode === 'ALL') {
+    selectedUserIds.value.clear()
+    selectedUsersMap.value.clear()
+  }
 })
 
 onMounted(async () => {
@@ -209,15 +274,23 @@ const fetchApp = async () => {
 const fetchUsers = async () => {
   loading.value = true
   try {
-    // We only need to fetch users if we are displaying the table. 
-    // But we also need 'total' for "ALL" mode estimation? 
-    // Yes, fetchUsers gets total.
-    const res = await getUsers({
+    const params: any = {
       skip: (page.value - 1) * pageSize.value,
       limit: pageSize.value
-    })
+    }
+    
+    // 添加搜索关键词
+    if (searchKeyword.value) {
+      params.keyword = searchKeyword.value.trim()
+    }
+    
+    const res = await getUsers(params)
     users.value = res.data.items
     total.value = res.data.total
+    
+    // 数据加载完成后,恢复选择状态
+    await nextTick()
+    restoreSelection()
   } catch (e) {
     // error
   } finally {
@@ -225,8 +298,79 @@ const fetchUsers = async () => {
   }
 }
 
+// 恢复表格选择状态
+const restoreSelection = () => {
+  if (!userTableRef.value) return
+  
+  // 清空当前选择
+  userTableRef.value.clearSelection()
+  
+  // 遍历当前页的用户,如果ID在 selectedUserIds 中,则选中
+  users.value.forEach((user) => {
+    if (selectedUserIds.value.has(user.id)) {
+      userTableRef.value.toggleRowSelection(user, true)
+      // 确保用户信息也在 Map 中
+      if (!selectedUsersMap.value.has(user.id)) {
+        selectedUsersMap.value.set(user.id, user)
+      }
+    }
+  })
+}
+
 const handleSelectionChange = (val: User[]) => {
-  selectedUsers.value = val
+  // 获取当前页所有用户的ID
+  const currentPageUserIds = new Set(users.value.map(u => u.id))
+  
+  // 先移除当前页的所有选择(因为可能取消选择了某些项)
+  currentPageUserIds.forEach(id => {
+    selectedUserIds.value.delete(id)
+    selectedUsersMap.value.delete(id)
+  })
+  
+  // 再将当前页新选中的用户ID添加到 selectedUserIds,并保存用户信息
+  val.forEach(user => {
+    selectedUserIds.value.add(user.id)
+    selectedUsersMap.value.set(user.id, user)
+  })
+}
+
+// 移除单个用户
+const removeUser = (userId: number) => {
+  selectedUserIds.value.delete(userId)
+  selectedUsersMap.value.delete(userId)
+  
+  // 如果该用户在当前页,需要更新表格选择状态
+  const currentUser = users.value.find(u => u.id === userId)
+  if (currentUser && userTableRef.value) {
+    userTableRef.value.toggleRowSelection(currentUser, false)
+  }
+}
+
+// 清空所有选择
+const clearAllSelection = () => {
+  selectedUserIds.value.clear()
+  selectedUsersMap.value.clear()
+  
+  // 清空表格选择状态
+  if (userTableRef.value) {
+    userTableRef.value.clearSelection()
+  }
+}
+
+const handleSearch = () => {
+  page.value = 1 // 搜索时重置到第一页
+  fetchUsers()
+}
+
+const handleSizeChange = (val: number) => {
+  pageSize.value = val
+  page.value = 1 // 改变每页数量时重置到第一页
+  fetchUsers()
+}
+
+const handleCurrentChange = (val: number) => {
+  page.value = val
+  fetchUsers()
 }
 
 const goBack = () => {
@@ -256,7 +400,7 @@ const handlePreSync = () => {
       }
   }
   
-  if (form.mode === 'SELECTED' && selectedUsers.value.length === 0) {
+  if (form.mode === 'SELECTED' && selectedUserIds.value.size === 0) {
     ElMessage.warning('请选择要同步的用户')
     return
   }
@@ -287,7 +431,8 @@ const confirmSync = async () => {
   
   syncing.value = true
   try {
-    const userIds = form.mode === 'SELECTED' ? selectedUsers.value.map(u => u.id) : []
+    // 将 Set 转换为数组
+    const userIds = form.mode === 'SELECTED' ? Array.from(selectedUserIds.value) : []
     
     const res = await syncAppUsersV2(appId, {
       mode: form.mode as 'ALL' | 'SELECTED',
@@ -328,6 +473,48 @@ const confirmSync = async () => {
   margin-top: 20px;
   margin-bottom: 20px;
 }
+.search-container {
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+.selected-count {
+  color: #409EFF;
+  font-weight: 500;
+}
+/* 已选择用户面板样式 */
+.selected-users-panel {
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+}
+.panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+.panel-title {
+  font-weight: 600;
+  color: #303133;
+  font-size: 14px;
+}
+.selected-users-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.user-tag {
+  margin: 0;
+}
+.user-mobile {
+  margin-left: 5px;
+  color: #909399;
+  font-size: 12px;
+}
 .pagination-container {
   margin-top: 15px;
   display: flex;