|
|
@@ -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;
|