参考引航计划的布局方式,页面应包含以下区域:
<template>
<div class="page-container">
<!-- 页面标题 -->
<h2>页面名称</h2>
<!-- 查询条件区域 -->
<div class="search-section">
<h3>查询条件</h3>
<el-form :model="queryForm" class="search-form">
<div class="search-row">
<el-form-item label="字段名">
<el-input v-model="queryForm.field" placeholder="请输入..." clearable></el-input>
</el-form-item>
<!-- 更多查询条件 -->
<div class="search-actions">
<el-button type="primary" @click="query">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
</div>
</el-form>
</div>
<!-- 操作按钮区域 -->
<div class="action-section">
<el-button type="primary" @click="add">新增</el-button>
<el-button type="danger" @click="deleteSelected" :disabled="selectedIds.length === 0">删除</el-button>
</div>
<!-- 表格区域 -->
<div class="table-container">
<el-table>...</el-table>
</div>
</div>
</template>
.page-container {
padding: 15px;
height: 100vh;
overflow-y: auto;
}
.page-container h2 {
margin: 0 0 15px 0;
font-size: 18px;
color: #303133;
}
.search-section {
background: #fff;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search-section h3 {
margin: 0 0 12px 0;
font-size: 15px;
color: #606266;
}
.search-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.search-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.search-row .el-form-item {
margin-bottom: 0;
min-width: 200px;
}
.search-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.action-section {
margin-bottom: 15px;
display: flex;
gap: 8px;
}
.table-container {
background: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
flex: 1;
display: flex;
flex-direction: column;
}
.pagination {
margin-top: 15px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
关键间距说明:
注意: 所有间距都应保持紧凑,避免过大的空白区域,确保界面紧凑、专业。
<el-table
:data="tableData"
stripe
border
size="default"
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 'bold' }"
style="width: calc(100vw - 331px); height: calc(100vh - 395px);">
说明:
calc(100vw - 331px),确保表格宽度适应视口宽度,减去侧边栏和边距calc(100vh - 395px),确保表格高度适应视口高度,减去标题、查询条件、操作按钮和分页的高度size="default"确保表格使用紧凑模式const loading = ref(false)
const deleteLoading = ref(false)
const getUsers = async () => {
if (loading.value) return
loading.value = true
try {
const response = await request.get('/user/list', {
params: {
...queryForm,
page: currentPage.value,
pageSize: pageSize.value,
sortBy: sortMap.value.sortBy,
sortOrder: sortMap.value.sortOrder
}
})
userList.value = response.data.content
total.value = response.data.totalElements
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
Loading状态应用:
<el-button type="primary" @click="getUsers" :loading="loading">查询</el-button>
<el-button @click="resetSearchForm" :loading="loading">重置</el-button>
<el-button type="danger" @click="deleteSelectedUsers" :disabled="selectedIds.length === 0" :loading="deleteLoading">删除</el-button>
<el-table v-loading="loading" :data="userList">...</el-table>
注意:
loading状态deleteLoading状态loading状态loading.value,防止重复点击try-finally确保loading状态一定会被恢复<el-table
:data="tableData"
stripe
border
size="default"
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 'bold' }"
:height="tableHeight"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange">
<el-table-column type="selection" width="55" fixed align="center"></el-table-column>
<el-table-column label="序号" width="60" fixed align="center">
<template #default="{ $index }">
{{ (currentPage - 1) * pageSize + $index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="用户名" width="100" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="loginId" label="账号" width="100" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="roleName" label="用户角色" width="150" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="weChatUserId" label="企业微信账号" width="120" sortable show-overflow-tooltip></el-table-column>
<el-table-column prop="recordStatusName" label="状态" width="80" sortable align="center">
<template #default="scope">
<el-tag :type="scope.row.recordStatus === 1 ? 'success' : 'info'" size="small">
{{ scope.row.recordStatusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" sortable align="center" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="modifyTime" label="修改时间" width="160" sortable align="center" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.modifyTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-tooltip content="编辑" placement="top">
<el-button type="primary" :icon="Edit" circle size="small" @click="edit(row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" circle size="small" @click="delete(row.id)"></el-button>
</el-tooltip>
</template>
</el-table-column>
重要说明:
(currentPage - 1) * pageSize + $index + 1,确保分页时序号连续操作列应使用图标按钮,而不是文字按钮,使界面更加简洁美观。
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-tooltip content="编辑" placement="top">
<el-button type="primary" :icon="Edit" circle size="small" @click="edit(row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" circle size="small" @click="delete(row.id)"></el-button>
</el-tooltip>
</template>
</el-table-column>
import { Edit, Delete, User, Lock, Search } from '@element-plus/icons-vue'
export default {
components: {
Edit,
Delete,
User,
Lock,
Search
}
}
| 图标 | 名称 | 用途 |
|---|---|---|
Edit |
编辑 | 修改数据 |
Delete |
删除 | 删除数据 |
User |
用户 | 用户/人员管理 |
Lock |
锁定 | 锁定/重置密码 |
Search |
搜索 | 搜索/查询 |
Plus |
加号 | 新增 |
View |
查看 | 查看详情 |
Download |
下载 | 下载文件 |
Upload |
上传 | 上传文件 |
Refresh |
刷新 | 刷新数据 |
type:按钮类型,如primary、success、warning、danger、info:icon:图标组件,如:icon="Edit"circle:圆形按钮size:按钮尺寸,推荐使用small@click:点击事件使用el-tooltip为图标按钮提供功能提示:
<el-tooltip content="编辑" placement="top">
<el-button type="primary" :icon="Edit" circle size="small" @click="edit(row)"></el-button>
</el-tooltip>
Tooltip属性:
content:提示文本placement:提示位置,可选值:top、bottom、left、rightconst formatDateTime = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
所有删除操作都必须显示确认对话框,防止误操作。
const deleteUser = async (id) => {
try {
await ElMessageBox.confirm(
'确定要删除该用户吗?删除后用户及其角色关联将被永久删除,不可恢复。',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await request.delete(`/user/delete/${id}`)
ElMessage.success('删除成功')
getUsers()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败')
}
}
}
const deleteSelectedUsers = async () => {
if (selectedUserIds.value.length === 0) {
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedUserIds.value.length} 个用户吗?删除后用户及其角色关联将被永久删除,不可恢复。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await request.delete('/user/delete', {
data: selectedUserIds.value
})
ElMessage.success('删除成功')
selectedUserIds.value = []
getUsers()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败')
}
}
}
删除主表数据时,必须先删除关联表数据。
@Transactional
public void deleteUsers(List<String> userIds) {
for (String userId : userIds) {
// 先删除关联表数据
sysUserRoleRepository.deleteByUserId(userId);
// 再删除主表数据
userRepository.deleteById(userId);
}
}
const handleSortChange = ({ prop, order }) => {
if (order) {
queryForm.value.sortBy = prop
queryForm.value.sortOrder = order === 'ascending' ? 'asc' : 'desc'
} else {
queryForm.value.sortBy = null
queryForm.value.sortOrder = null
}
getUsers()
}
public Page<User> queryUsers(UserQuery query, Pageable pageable) {
Sort sort = pageable.getSort();
if (query.getSortBy() != null && query.getSortOrder() != null) {
sort = Sort.by(
"asc".equalsIgnoreCase(query.getSortOrder()) ? Sort.Direction.ASC : Sort.Direction.DESC,
query.getSortBy()
);
}
return userRepository.findAll(pageable.withSort(sort));
}
所有API请求都应该使用统一的request工具,自动处理认证和错误。
import request from '@/utils/request'
export const getUsers = (params) => {
return request.get('/user/list', { params })
}
export const createUser = (data) => {
return request.post('/user/create', data)
}
export const deleteUser = (data) => {
return request.delete('/user/delete', { data })
}
try {
await request.post('/api/endpoint', data)
ElMessage.success('操作成功')
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="用户名" prop="name">
<el-input v-model="form.name" placeholder="请输入用户名"></el-input>
</el-form-item>
</el-form>
<script>
const rules = {
name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
]
}
</script>
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
private String name;
@NotBlank(message = "账号不能为空")
private String loginId;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;
}
UserManagement.vuegetUsers、handleSortChangeuserList、selectedIdsMAX_PAGE_SIZE/**
* 获取用户列表
*/
const getUsers = async () => {
// 实现代码
}
/**
* 处理排序变化
* @param {Object} param - 排序参数
* @param {String} param.prop - 排序字段
* @param {String} param.order - 排序方向
*/
const handleSortChange = ({ prop, order }) => {
// 实现代码
}
import { debounce } from 'lodash-es'
const search = debounce(() => {
getUsers()
}, 300)
对于大量数据,使用虚拟滚动提高性能。
<el-table
:data="tableData"
height="600"
v-loading="loading">
</el-table>
使用Vue的插值表达式,避免直接使用v-html。
<!-- 正确 -->
<div>{{ user.name }}</div>
<!-- 错误 -->
<div v-html="user.name"></div>
所有POST、PUT、DELETE请求都应该携带CSRF token。
import request from '@/utils/request'
request.interceptors.request.use(config => {
const token = localStorage.getItem('csrf_token')
if (token) {
config.headers['X-CSRF-TOKEN'] = token
}
return config
})
<nav>...</nav>
<main>
<h1>页面标题</h1>
<section class="search-section">...</section>
<section class="table-section">...</section>
</main>
<footer>...</footer>
确保所有交互元素都可以通过键盘访问。
<el-button @click="handleClick" @keyup.enter="handleClick">
点击
</el-button>
对于选项数量较多的下拉选择框(如船公司、代理公司、港口等),应使用可搜索的下拉框,并实现远程搜索和分页加载功能。
<el-form-item label="船公司">
<el-select
v-model="searchForm.shipownerId"
placeholder="请选择"
style="width: 180px"
clearable
filterable
remote
:remote-method="searchShipCompany"
:loading="shipCompanyLoading"
@visible-change="handleShipCompanyVisibleChange"
@scroll="handleShipCompanyScroll"
>
<el-option
v-for="company in shipCompanyList"
:key="company.companyId"
:label="company.companyName"
:value="company.companyId">
</el-option>
</el-select>
</el-form-item>
data() {
return {
searchForm: {
shipownerId: ''
},
shipCompanyList: [],
shipCompanyQuery: '',
shipCompanyPage: 1,
shipCompanyPageSize: 10,
shipCompanyLoading: false,
shipCompanyHasMore: true
}
}
methods: {
getShipCompanyList() {
this.getShipCompanyListByPage('', 1);
},
getShipCompanyListByPage(query, page) {
this.shipCompanyLoading = true;
this.$axios.get('/api/ship-company/list', {
params: {
companyName: query,
page: page,
pageSize: this.shipCompanyPageSize
}
})
.then(response => {
if (page === 1) {
this.shipCompanyList = response.data.content || [];
} else {
this.shipCompanyList = [...this.shipCompanyList, ...(response.data.content || [])];
}
this.shipCompanyHasMore = response.data.content && response.data.content.length >= this.shipCompanyPageSize;
})
.catch(error => {
console.error('获取船公司列表失败:', error);
this.$message.error('获取船公司列表失败,请稍后重试');
})
.finally(() => {
this.shipCompanyLoading = false;
});
},
searchShipCompany(query) {
this.shipCompanyQuery = query || '';
this.shipCompanyPage = 1;
this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
},
handleShipCompanyVisibleChange(visible) {
if (visible && !this.shipCompanyQuery) {
this.shipCompanyPage = 1;
this.getShipCompanyListByPage('', this.shipCompanyPage);
}
},
handleShipCompanyScroll() {
if (this.shipCompanyHasMore && !this.shipCompanyLoading) {
this.shipCompanyPage++;
this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
}
}
}
@GetMapping("/list")
public Page<ShipCompanyDTO> list(
@RequestParam(required = false) String companyName,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize
) {
Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by("companyName").ascending());
return shipCompanyService.searchCompanies(companyName, pageable);
}
@Service
public class ShipCompanyService {
@Autowired
private ShipCompanyRepository shipCompanyRepository;
public Page<ShipCompanyDTO> searchCompanies(String companyName, Pageable pageable) {
Specification<ShipCompany> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (companyName != null && !companyName.isEmpty()) {
predicates.add(cb.like(root.get("companyName"), "%" + companyName + "%"));
}
predicates.add(cb.equal(root.get("recordStatus"), 1));
return cb.and(predicates.toArray(new Predicate[0]));
};
Page<ShipCompany> companies = shipCompanyRepository.findAll(spec, pageable);
return companies.map(ShipCompanyDTO::fromEntity);
}
}
| 属性 | 说明 |
|---|---|
| filterable | 启用本地过滤功能 |
| remote | 启用远程搜索 |
| remote-method | 远程搜索方法,当用户输入时触发 |
| loading | 加载状态,用于显示加载动画 |
| visible-change | 下拉框显示/隐藏时触发 |
| scroll | 下拉列表滚动时触发,用于实现瀑布加载 |
?.)和默认值处理可能的字段缺失