blog-admin-web/src/views/system/SystemUser.vue
灌糖包子 1e7748db64
feat: 表格区域独立滚动,表头固定
- 修改 common.less:
  - 新增 .page-wrapper 类,使用 flex 纵向布局占满父容器
  - 修改 .table-container 为 flex 布局,让表格自适应高度
  - .el-table 使用 flex:1 实现内容区域滚动、表头固定

- 7个列表页面添加 page-wrapper 类:
  - SystemUser.vue, SystemRole.vue
  - Hitokoto.vue, PhotoWall.vue, Music.vue, SourceImage.vue
  - Article.vue(特殊处理:左右分栏布局适配)

现在设置 100 条数据时,表格内容独立滚动,分页始终可见,不会触发整页滚动。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 22:16:49 +08:00

197 lines
6.4 KiB
Vue

<template>
<div class="page-wrapper">
<el-form inline :model="search" @submit.prevent>
<el-form-item label="用户名/昵称">
<el-input v-model="search.username" />
</el-form-item>
</el-form>
<div class="btn-container">
<el-button type="primary" @click="add">添加</el-button>
<div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
</div>
</div>
<div class="table-container">
<el-table :data="systemUserData" v-loading="loading" stripe>
<el-table-column prop="username" label="用户名" />
<el-table-column prop="realname" label="昵称" />
<el-table-column prop="created_at" label="创建时间" >
<template #default="scope">
{{ datetimeFormat(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" >
<template #default="scope">
{{ datetimeFormat(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column label="操作" >
<template #default="scope">
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="page-container">
<el-pagination background
:page-sizes="store.state.pageSizeOpts"
:layout="store.state.pageLayout"
:current-page="search.pageNum"
:total="total"
@size-change="pageSizeChange"
@current-change="pageChange">
</el-pagination>
</div>
<el-dialog v-model="addModal" :title="modalTitle" >
<el-form ref="userForm" :model="formData" :rules="ruleValidate" :label-width="120">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" type="password" />
</el-form-item>
<el-form-item label="昵称" prop="realname">
<el-input v-model="formData.realname" />
</el-form-item>
<el-form-item label="角色" prop="realname">
<el-select v-model="formData.role_ids" multiple >
<el-option v-for="role in roles" :key="role._id" :value="role._id" :label="role.name"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addModal = false">取消</el-button>
<el-button type="primary" @click="save" :loading="modalLoading">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { useStore } from 'vuex'
import { useBaseList } from '@/model/baselist'
import { Page } from '@/model/common.dto'
import http from '@/utils/http'
import { SystemUserModel } from '@/model/system/system-user'
import { SystemRoleModel } from '@/model/system/system-role'
import { ElMessage, ElMessageBox } from 'element-plus'
import { VForm } from '@/types'
const store = useStore()
class SystemUserPage extends Page {
username?: string
reset() {
super.reset()
this.username = undefined
}
}
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new SystemUserPage())
const systemUserData = ref<SystemUserModel[]>([])
const roles = ref<SystemRoleModel[]>([])
const addModal = ref(false)
const modalTitle = ref<string | null>(null)
const formData = reactive<SystemUserModel>({
_id: null,
username: null,
password: null,
realname: null,
role_ids: []
})
const userForm = ref<VForm>()
const ruleValidate = computed(() => ({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ validator: async (rule: object, value: string, callback: Function) => {
const data = await http.get<any, any>('/api/v1/system/user/exists', {params: {username: value, id: formData._id}})
if(data.data.exists) {
callback(new Error('用户名已存在'))
} else {
callback()
}
}, trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 16, message: '密码长度8~16位', trigger: 'blur' },
{ pattern: /^(?![\d]+$)(?![a-zA-Z]+$)(?![-=+_.,]+$)[\da-zA-Z-=+_.,]{8,16}$/, message: '密码由字母、数字、特殊字符中的任意两种组成', trigger: 'blur' }
],
}))
async function loadData() {
loading.value = true
const data = await http.get<{params: SystemUserPage}, any>('/api/v1/system/user/list', {params: search})
loading.value = false
total.value = data.total
systemUserData.value = data.data
}
setLoadData(loadData)
function add() {
Object.assign(formData, {
_id: null,
username: null,
password: null,
realname: null,
role_ids: []
})
modalTitle.value = '新增用户'
addModal.value = true
clearValidate()
}
function update(row: SystemUserModel) {
Object.assign(formData, {
_id: row._id,
username: row.username,
realname: row.realname,
role_ids: row.role_ids
})
modalTitle.value = '修改用户'
addModal.value = true
clearValidate()
}
async function save() {
userForm.value?.validate(async (valid: boolean) => {
if (!valid) return
modalLoading.value = true
const data = await http.post<SystemUserModel, any>('/api/v1/system/user/save', formData)
modalLoading.value = false
addModal.value = false
ElMessage.success(data.message)
loadData()
})
}
function remove(row: SystemUserModel) {
ElMessageBox.confirm(`是否确认删除 ${row.username} 用户?`, '确认删除', {type: 'warning'}).then(async () => {
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/user/delete', {params: {id: row._id}})
if(data.status) {
ElMessage.success(data.message)
loadData()
} else {
ElMessage.warning(data.message)
}
}).catch(() => {})
}
function clearValidate() {
nextTick(() => {
userForm.value?.clearValidate()
})
}
loadData()
http.get<never, any>('/api/v1/system/role/listAll').then(data => {
roles.value = data
})
</script>