初始化

This commit is contained in:
2026-03-03 09:53:54 +08:00
commit 3f349a35a4
437 changed files with 65639 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
<template>
<el-select
v-model="selectedValue"
v-bind="$attrs"
:placeholder="placeholder"
:disabled="disabled"
:clearable="clearable"
:filterable="false"
:multiple="multiple"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
:loading="loading"
popper-class="sa-user-select-popper"
@visible-change="handleVisibleChange"
@clear="handleClear"
>
<template #header>
<div class="sa-user-select-header" @click.stop>
<el-input
v-model="searchKeyword"
placeholder="搜索用户名、姓名、手机号"
clearable
@input="handleSearch"
@click.stop
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</template>
<!-- 隐藏的选项用于显示已选中的用户 -->
<el-option
v-for="user in selectedUsers"
:key="user.id"
:value="user.id"
:label="user.username"
style="display: none"
/>
<!-- 使用 el-option 包装表格内容 -->
<el-option value="" disabled style="height: auto; padding: 0">
<div class="sa-user-select-table" @click.stop>
<el-table
ref="tableRef"
:data="userList"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
size="small"
v-loading="loading"
>
<el-table-column
v-if="multiple"
type="selection"
width="45"
:selectable="checkSelectable"
/>
<el-table-column prop="id" label="编号" align="center" width="80" />
<el-table-column prop="avatar" label="头像" width="60">
<template #default="{ row }">
<el-avatar :size="32" :src="row.avatar">
{{ row.username?.charAt(0) }}
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="100" show-overflow-tooltip />
<el-table-column prop="realname" label="姓名" width="100" show-overflow-tooltip />
<el-table-column prop="phone" label="手机号" width="110" show-overflow-tooltip />
</el-table>
<div class="sa-user-select-pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:total="pagination.total"
layout="total, prev, pager, next"
small
background
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</el-option>
<template #empty>
<el-empty description="暂无用户数据" :image-size="60" />
</template>
</el-select>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { getUserList } from '@/api/auth'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import type { TableInstance } from 'element-plus'
defineOptions({ name: 'SaUser', inheritAttrs: false })
interface UserItem {
id: number
username: string
email: string
phone: string
avatar?: string
status: string
[key: string]: any
}
interface Props {
/** 占位符 */
placeholder?: string
/** 是否禁用 */
disabled?: boolean
/** 是否可清空 */
clearable?: boolean
/** 是否可搜索 */
filterable?: boolean
/** 是否多选 */
multiple?: boolean
/** 多选时是否折叠标签 */
collapseTags?: boolean
/** 多选折叠时是否显示提示 */
collapseTagsTooltip?: boolean
/** 返回值类型:'id' 返回用户ID'object' 返回完整用户对象 */
valueType?: 'id' | 'object'
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择用户',
disabled: false,
clearable: true,
filterable: true,
multiple: false,
collapseTags: true,
collapseTagsTooltip: true,
valueType: 'id'
})
// 支持单选(number/object) 或 多选(Array)
const modelValue = defineModel<number | null | UserItem | Array<number | UserItem>>()
// 内部选中值
const selectedValue = ref<any>()
const searchKeyword = ref('')
const loading = ref(false)
const userList = ref<UserItem[]>([])
const tableRef = ref<TableInstance>()
// 缓存所有已选中的用户信息
const allSelectedUsers = ref<UserItem[]>([])
// 计算已选中的用户列表(用于显示)
const selectedUsers = computed(() => {
if (!selectedValue.value) return []
const selectedIds = props.multiple
? Array.isArray(selectedValue.value)
? selectedValue.value
: []
: [selectedValue.value]
// 从缓存中查找用户信息
return selectedIds
.map((id) => {
const cached = allSelectedUsers.value.find((u) => u.id === id)
if (cached) return cached
// 从当前列表中查找
const fromList = userList.value.find((u) => u.id === id)
if (fromList) {
// 添加到缓存
allSelectedUsers.value.push(fromList)
return fromList
}
// 如果都找不到,返回一个临时对象
return { id, username: `用户${id}`, email: '', phone: '', status: '1' }
})
.filter(Boolean)
})
// 分页参数
const pagination = ref({
page: 1,
limit: 5,
total: 0
})
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
try {
const params: any = {
page: pagination.value.page,
limit: pagination.value.limit
}
// 添加搜索条件
if (searchKeyword.value) {
params.keyword = searchKeyword.value
}
const response = await getUserList(params)
if (response && response.data) {
userList.value = response.data || []
pagination.value.total = response.total || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 搜索防抖
let searchTimer: any = null
const handleSearch = () => {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
pagination.value.page = 1
fetchUserList()
}, 300)
}
// 下拉框显示/隐藏
const handleVisibleChange = (visible: boolean) => {
if (visible) {
// 打开时加载数据
fetchUserList()
}
}
// 清空选择
const handleClear = () => {
selectedValue.value = props.multiple ? [] : null
if (tableRef.value) {
if (props.multiple) {
tableRef.value.clearSelection()
} else {
tableRef.value.setCurrentRow(null)
}
}
}
// 单选 - 行点击
const handleRowClick = (row: UserItem) => {
if (!props.multiple) {
handleCurrentChange(row)
}
}
// 单选 - 当前行改变
const handleCurrentChange = (row: UserItem | undefined) => {
if (!props.multiple && row) {
// 添加到缓存
const existingIndex = allSelectedUsers.value.findIndex((u) => u.id === row.id)
if (existingIndex === -1) {
allSelectedUsers.value.push(row)
} else {
allSelectedUsers.value[existingIndex] = row
}
selectedValue.value = props.valueType === 'id' ? row.id : row
}
}
// 多选 - 选择改变
const handleSelectionChange = (selection: UserItem[]) => {
if (props.multiple) {
// 更新缓存
selection.forEach((row) => {
const existingIndex = allSelectedUsers.value.findIndex((u) => u.id === row.id)
if (existingIndex === -1) {
allSelectedUsers.value.push(row)
} else {
allSelectedUsers.value[existingIndex] = row
}
})
selectedValue.value = selection.map((item) => (props.valueType === 'id' ? item.id : item))
}
}
// 检查行是否可选
const checkSelectable = () => {
// 可以根据需要添加更多条件
return !props.disabled
}
// 分页改变
const handlePageChange = () => {
fetchUserList()
}
const handleSizeChange = () => {
pagination.value.page = 1
fetchUserList()
}
// 监听内部选中值变化,同步到 v-model
watch(
selectedValue,
(newVal) => {
modelValue.value = newVal
},
{ deep: true }
)
// 监听 v-model 变化,同步到内部选中值
watch(
() => modelValue.value,
(newVal) => {
selectedValue.value = newVal
},
{ immediate: true, deep: true }
)
// 组件挂载时初始化
onMounted(() => {
// 如果有初始值,加载数据
if (modelValue.value) {
fetchUserList()
}
})
</script>
<style scoped lang="scss">
.sa-user-select-header {
padding: 8px 12px;
border-bottom: 1px solid var(--el-border-color-light);
}
.sa-user-select-table {
min-height: 480px;
max-height: 600px;
display: flex;
flex-direction: column;
:deep(.el-table) {
.el-table__header-wrapper {
th {
background-color: var(--el-fill-color-light);
}
}
.el-table__row {
cursor: pointer;
&:hover {
background-color: var(--el-fill-color-light);
}
}
}
}
.sa-user-select-pagination {
padding: 8px 12px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-fill-color-blank);
display: flex;
justify-content: center;
}
</style>
<style lang="scss">
// 全局样式,不使用 scoped
.sa-user-select-popper {
max-width: 90vw !important;
.el-select-dropdown__item {
height: auto !important;
min-height: 320px !important;
max-height: 360px !important;
padding: 0 !important;
line-height: normal !important;
&.is-disabled {
cursor: default;
background-color: transparent !important;
}
}
.el-select-dropdown__wrap {
max-height: 340px !important;
}
// 确保下拉框列表容器也不限制高度
.el-select-dropdown__list {
padding: 0 !important;
}
// 确保滚动容器正确显示
.el-scrollbar__view {
padding: 0 !important;
}
}
</style>