feat(user): 新增用户管理功能

- 添加用户列表页面,包含搜索、分页等功能
- 实现用户状态切换和详情查看功能
- 新增用户选择组件,用于选择用户
- 优化表格组件,支持自定义列和操作
- 添加面包屑组件,用于展示导航路径
This commit is contained in:
2025-04-28 17:57:16 +08:00
parent 749d285a0d
commit e0868a10af
17 changed files with 1132 additions and 46 deletions

View File

@@ -0,0 +1,77 @@
<template>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom: 1px solid #eee;
"
>
<div>
<div class="myTitle" :style="isBorder ? {} : { border: 'unset' }">
<img :src="sonSvg" alt="" class="icon" :style="iconStyle" v-if="showIcon && sonSvg" />
<img
src="@/assets/icons/bookmark-fill.svg"
alt=""
class="icon"
:style="iconStyle"
v-else-if="showIcon"
/>
<span class="title">{{ title }}</span>
</div>
</div>
<div style="padding-right: 20px">
<slot name="right"></slot>
</div>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
isBorder: {
type: Boolean,
default: true
},
iconStyle: {
type: Object,
default: () => ({})
},
showIcon: {
type: Boolean,
default: () => true
}
})
const sonSvg = ref(inject('svgData'))
console.log(sonSvg, 'data')
</script>
<style lang="scss" scoped>
.myTitle {
width: 100%;
height: 60px;
display: flex;
align-items: center;
user-select: none;
.icon {
width: 20px;
height: 20px;
margin-left: 20px;
img {
width: 100%;
height: 100%;
}
}
.title {
font-size: 16px;
color: #233041;
margin-left: 10px;
font-family: 'SourceHanSansCN-Medium';
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div class="dialog">
<el-dialog
class="operate"
v-model="computedVis"
:title="title"
:width="width"
@close="handleClose"
:lock-scroll="false"
:draggable="draggable"
>
<template #default>
<slot></slot>
</template>
<template #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {computed } from 'vue'
const props = defineProps({
overlayBg: {
type: String,
default: 'transparent'
},
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
},
width: {
type: [Number, String],
default: () => 870
},
draggable: {
type: Boolean,
default: () => false
}
})
const emit = defineEmits(['update:modelValue', 'close'])
const computedVis = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
function handleClose() {
emit('close')
}
</script>
<style lang="scss" scoped>
.dialog {
:deep(.el-overlay) {
background-color: v-bind('props.overlayBg');
}
:deep(.el-dialog.operate) {
padding: 0 !important;
margin-top: 15vh !important;
border-radius: 6px;
.el-dialog__header {
padding: 0 0 0 20px;
height: 52px;
background-color: #f1f3f7;
line-height: 52px;
box-sizing: border-box;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom: 1px solid #dddddd;
.el-dialog__headerbtn {
width: 52px;
height: 52px;
}
}
.el-dialog__body {
padding: 20px 30px 0 30px;
.download {
box-sizing: border-box;
width: 260px;
height: 40px;
background-color: rgba(232, 241, 255, 1);
border-radius: 4px;
padding: 10px 16px;
box-sizing: border-box;
line-height: 20px;
color: rgba(0, 118, 232, 1);
cursor: pointer;
}
}
.selectbtn {
color: #0076e8;
border-color: #0076e8;
// cursor: pointer;
&:hover {
background-color: #0076e8;
color: #fff;
}
}
.el-dialog__footer {
text-align: center;
padding: 0 0 20px 0;
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="column-item">
<MyTitle v-if="!!title" :title="title" :showIcon="showIcon">
<template #right>
<slot name="right"></slot>
</template>
</MyTitle>
<div class="cont">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
import MyTitle from '../myTitle/MyTitle.vue'
const props = defineProps({
title: {
type: String,
default: ''
},
showIcon: {
type: Boolean,
default: () => true
}
})
</script>
<style lang="scss" scoped>
.column-item {
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
.cont {
padding: 20px;
}
}
</style>

View File

@@ -28,7 +28,7 @@
v-if="config.index"
type="index"
align="center"
width="50px"
width="80px"
label="序号"
/>
<template v-for="scheme in config.schemes" :key="scheme.attrs.label">
@@ -65,9 +65,9 @@
</template>
<script setup>
import { computed, onBeforeMount, onMounted, watch } from 'vue'
import { computed, onBeforeMount, onMounted, watch, ref } from 'vue'
import Sortable from 'sortablejs'
import { nanoid } from '@/utils/jsencrypt'
// import { nanoid } from '@/utils/jsencrypt'
const props = defineProps({
data: {
@@ -148,9 +148,9 @@
(data) => {
// if (props.config.drag) {
computedData.value = data.map((item) => {
if (!item.nanoId) {
item.nanoId = nanoid()
}
// if (!item.nanoId) {
// item.nanoId = nanoid()
// }
return item
})
// } else {
@@ -204,7 +204,7 @@
}
function handleTableCurrentChange(data) {
console.log(data, 'data')
// console.log(data, 'data')
emits('update:checkData', data)
}
@@ -234,9 +234,9 @@
onBeforeMount(() => {
// if (props.config.drag) {
computedData.value = props.data.map((item) => {
if (!item.nanoId) {
item.nanoId = nanoid()
}
// if (!item.nanoId) {
// item.nanoId = nanoid()
// }
return item
})
// } else {
@@ -258,7 +258,7 @@
.content {
background: #fff;
border-radius: 4px;
padding: 20px 20px 0 20px;
//padding: 20px 20px 0 20px;
box-sizing: border-box;
:deep(.el-table) {
.current-row {

View File

@@ -8,7 +8,7 @@
<Editor
v-model="valueHtml"
class="overflow-y-hidden mt-0.5"
style="height: 18rem"
style="min-height: 18rem"
:default-config="editorConfig"
mode="default"
@onCreated="handleCreated"

View File

@@ -0,0 +1,236 @@
<template>
<PmDialog
v-model="dialoagVisible"
:title="'选择' + title"
width="1200px"
style="max-height: 50vh"
>
<div class="container">
<!-- <div class="left">-->
<!-- <el-input-->
<!-- style="width: 80%; margin-bottom: 0.625rem"-->
<!-- placeholder="请输入部门名称"-->
<!-- v-model="deptName"-->
<!-- />-->
<!-- <el-scrollbar height="31.25rem">-->
<!-- <el-tree-->
<!-- ref="deptTreeRef"-->
<!-- :data="deptTree"-->
<!-- :props="defaultProps"-->
<!-- :filter-node-method="filterNode"-->
<!-- @node-click="handleNodeClick"-->
<!-- value-key="id"-->
<!-- placeholder="请选择归属部门"-->
<!-- />-->
<!-- </el-scrollbar>-->
<!-- </div>-->
<div class="right">
<ColumnItem class="content">
<el-form inline >
<el-form-item :label="title + '名称'">
<el-input placeholder="请输入" v-model="queryParams.nickName" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchData">查询</el-button>
<el-button @click="resaltData">重置</el-button>
</el-form-item>
</el-form>
</ColumnItem>
<ColumnItem class="content">
<el-table
ref="treeRef"
style="height: 50vh; overflow: auto"
:data="tabList"
v-loading="loading"
:highlight-current-row="!props.multiple"
@current-change="nodeClick"
@selection-change="handleSelectionChange"
row-key="ID"
>
<el-table-column v-if="props.multiple" type="selection" width="55" />
<el-table-column type="index" width="50" />
<el-table-column prop="nickName" label="用户名称" />
<el-table-column prop="userName" label="用户账号" />
</el-table>
</ColumnItem>
<el-pagination
class="pagination"
v-model:current-page="queryParams.page"
background
v-model:page-size="queryParams.pageSize"
:total="total"
@size-change="getStaffList"
@current-change="getStaffList"
/>
</div>
</div>
<div style="padding: 10px; display: flex; justify-content: center">
<el-button
@click="requireDiolag"
type="primary"
:disabled="props.multiple ? choosedTableRows.length < 1 : !selectDept"
>确定</el-button
>
<el-button @click="dialoagVisible = false">取消</el-button>
</div>
</PmDialog>
</template>
<script setup>
import {onMounted, ref, watch, nextTick } from 'vue'
import PmDialog from '@/components/PmDialog/pm-dialog.vue'
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
import { getUserList } from '@/api/user.js'
// import { listUser } from '@/api/system/user'
const treeRef = ref()
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
title: {
type: String,
default: '收款人'
}
})
const emit = defineEmits(['getRecipientInfo'])
const loading = ref(false)
const dialoagVisible = ref(false)
const deptName = ref(null)
const selectDept = ref(null),
choosedTableRows = ref([]),
unChoosed = ref([])
// 使用Set存储选中ID更高效的存储方式
const selectedIds = ref(new Set())
// const deptTree = ref([])
const tabList = ref([])
const total = ref(0)
// const defaultProps = ref({ value: 'value', label: 'label', children: 'children' })
const queryParams = ref({
pageSize: 10,
page: 1,
deptId: null,
nickName: ''
})
onMounted(() => {
// getDeptList()
getStaffList()
})
const deptTreeRef = ref()
watch(
() => deptName.value,
(val) => {
deptTreeRef.value.filter(val)
}
)
function open() {
dialoagVisible.value = true
}
// async function getDeptList() {
// deptTree.value = await getDeptTree()
// }
let depClick = ref(false)
function getStaffList() {
loading.value = true
getUserList(queryParams.value)
.then(async (res) => {
tabList.value = res.data.list
total.value = res.data.total
if(props.multiple) {
// 下一Tick执行确保DOM更新完成
depClick.value = true
await nextTick(() => {
// 根据存储的ID恢复选中状态
tabList.value.forEach((row) => {
if (selectedIds.value.has(row.ID)) {
treeRef.value.toggleRowSelection(row, true)
}
})
})
depClick.value = false
}
})
.finally(() => {
loading.value = false
})
}
function searchData() {
getStaffList()
}
function resaltData() {
queryParams.value.pageSize = 10
queryParams.value.page = 1
queryParams.value.nickName = ''
getStaffList()
}
// function handleNodeClick(node) {
// queryParams.value.deptId = node.id
// getStaffList()
// }
function nodeClick(e) {
selectDept.value = e
}
function handleSelectionChange(selection) {
console.log(selection)
// 更新选中ID集合
selection.forEach((row) => selectedIds.value.add(row.ID))
// 删除当前页取消选中的ID,前提是非点击部门或分页节点触发
if (!depClick.value) {
// 找出当前页需要删除的ID
unChoosed.value = []
if (selection.length > 0) {
selection = selection.map((item) => item.ID)
tabList.value.forEach((item) => {
if (!selection.includes(item.ID)) {
unChoosed.value.push(item.ID)
}
})
} else {
unChoosed.value = JSON.parse(JSON.stringify(tabList.value)).map((item) => item.ID)
}
selectedIds.value.forEach((id) => {
if (unChoosed.value.includes(id)) {
selectedIds.value.delete(id)
}
})
}
// 更新已选数据(使用过滤确保有效性)
choosedTableRows.value = Array.from(selectedIds.value)
.map((id) => [...tabList.value, ...choosedTableRows.value].find((r) => r.ID === id))
.filter(Boolean)
}
// function filterNode(value, data) {
// if (!value) return true
// return data.label.includes(value)
// }
function requireDiolag() {
emit('getRecipientInfo', props.multiple ? choosedTableRows.value : selectDept.value)
dialoagVisible.value = false
}
defineExpose({
open
})
</script>
<style scoped lang="scss">
.container {
display: flex;
.left {
width: 17.5rem;
flex-shrink: 0;
}
.right {
flex: 1;
:deep(.content:first-child) {
margin-bottom: 0;
}
:deep(.cont) {
padding: 0;
}
}
}
</style>