name: "web-dev-best-practices"
@DateTimeFormat(pattern = "yyyy-MM-dd")@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date planTimeStart;
// 前端日期解析和格式化
const parsedDate = new Date(dateString);
if (!isNaN(parsedDate.getTime())) {
// 格式化为所需格式
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, {
locale: zhCn
})
COALESCE 函数处理空值判断@Query("SELECT new com.example.dto(...) " +
"FROM ... " +
"WHERE ... AND (COALESCE(:query, '') = '' OR ...) ")
Page<SearchResult> searchWithPage(@Param("query") String query, Pageable pageable);
COMMON → tugboatcommonBRANCH → liandatugboatmispublic enum DataSourceType {
COMMON("common"), // tugboatcommon: 系统公共库
BRANCH("branch"); // liandatugboatmis: 分支机构业务库
}
<el-select
v-model="value"
filterable
remote
popper-class="custom-popper"
:remote-method="searchMethod">
<div v-if="loadingMore" class="loading">加载中...</div>
<el-option
v-for="item in options"
:key="item.id"
:label="item.displayField" <!-- 显示字段 -->
:value="item.valueField"> <!-- 值字段 -->
</el-option>
</el-select>
在所有触发后台操作的前端事件中添加进度条和防重复点击机制。
<template>
<el-button
:loading="loading"
@click="handleAction"
:disabled="loading">
{{ loading ? '处理中...' : '提交' }}
</el-button>
</template>
<script>
export default {
data() {
return {
loading: false
}
},
methods: {
async handleAction() {
if (this.loading) return; // 防止重复点击
this.loading = true;
try {
// 执行后台操作
await this.apiCall();
} finally {
this.loading = false; // 确保无论成功失败都恢复状态
}
}
}
}
</script>
注解使用场景:
@DateTimeFormat 用于请求参数绑定@JsonFormat 用于JSON序列化/反序列化命名规范:
数据一致性:
性能考虑:
在开发新页面功能、调试常见Web应用问题、处理日期时间、配置数据源、优化前端组件时使用此技能。
按钮并排布局实现:
数据库级排序和分页实现:
size="small"或size="default"属性white-space: nowrap防止内容换行text-overflow: ellipsis截断显示position: sticky或Element Plus的sticky-header属性fixed="left"锁定序号列、操作列等fixed="right"锁定操作列<template>
<div class="search-section">
<h3>查询条件</h3>
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<!-- 第一行(3个控件 + 按钮) -->
<template v-if="showAllSearch || searchFormIndex < 3">
<el-form-item label="作业时间">
<el-date-picker
v-model="searchForm.planTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px"
></el-date-picker>
</el-form-item>
<el-form-item label="港区作业类型">
<el-select
v-model="searchForm.portAreaOperationType"
placeholder="请选择"
style="width: 180px"
>
<el-option label="全部" value=""></el-option>
<el-option label="本港计划" value="本港计划"></el-option>
<el-option label="外港计划" value="外港计划"></el-option>
</el-select>
</el-form-item>
<el-form-item label="港口">
<el-select
v-model="searchForm.portId"
placeholder="请选择"
style="width: 180px"
clearable
>
<el-option
v-for="port in portList"
:key="port.portId"
:label="port.portName"
:value="port.portId">
</el-option>
</el-select>
</el-form-item>
<div class="search-actions">
<el-button
v-if="hasExtraRows"
type="primary"
@click="toggleSearchForm">
{{ showAllSearch ? '收起' : '展开' }}
</el-button>
<el-button type="primary" @click="getPilotPlans">查询</el-button>
<el-button @click="resetSearchForm">重置</el-button>
</div>
</template>
<!-- 折叠的控件 -->
<template v-if="showAllSearch">
<el-form-item label="英文船名">
<el-input
v-model="searchForm.englishShipName"
placeholder="请输入英文船名"
style="width: 180px"
clearable
></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="searchForm.status"
placeholder="请选择"
style="width: 180px"
>
<el-option label="全部" value=""></el-option>
<el-option label="未调度" value="未调度"></el-option>
<el-option label="已调度" value="已调度"></el-option>
</el-select>
</el-form-item>
<el-form-item label="船公司">
<el-select
v-model="searchForm.shipownerId"
placeholder="请选择"
style="width: 180px"
clearable
>
<el-option
v-for="company in shipCompanyList"
:key="company.companyId"
:label="company.companyName"
:value="company.companyId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="吃水">
<el-input-group>
<el-input
v-model="searchForm.deepStart"
placeholder="最小值"
style="width: 80px"
type="number"
></el-input>
<span style="padding: 0 5px">-</span>
<el-input
v-model="searchForm.deepEnd"
placeholder="最大值"
style="width: 80px"
type="number"
></el-input>
</el-input-group>
</el-form-item>
<el-form-item label="交通船">
<el-input
v-model="searchForm.trafficboat"
placeholder="请输入交通船"
style="width: 180px"
clearable
></el-input>
</el-form-item>
</template>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
showAllSearch: false, // 控制是否显示所有查询条件
searchForm: {
planTimeRange: [],
portAreaOperationType: '',
portId: '',
englishShipName: '',
status: '',
shipownerId: '',
deepStart: null,
deepEnd: null,
trafficboat: ''
}
}
},
computed: {
hasExtraRows() {
return true; // 根据实际情况判断是否有额外的查询条件行
}
},
methods: {
toggleSearchForm() {
this.showAllSearch = !this.showAllSearch;
},
getPilotPlans() {
// 查询逻辑
},
resetSearchForm() {
// 重置逻辑
}
}
}
</script>
<style scoped>
.search-section {
background-color: white;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search-section h3 {
color: #606266;
margin-bottom: 12px;
font-size: 15px;
}
.demo-form-inline {
display: flex;
flex-direction: column;
gap: 12px;
}
.search-actions {
display: flex;
gap: 8px;
align-items: center;
}
</style>
<template>
<el-table
:data="tableData"
style="width: 100%"
@sort-change="handleSortChange">
<el-table-column
prop="index"
label="序号"
width="50"
fixed
type="index"
align="center">
</el-table-column>
<el-table-column
prop="planTime"
label="计划时间"
width="160"
sortable
align="center">
<template #default="scope">
{{ formatDateTime(scope.row.planTime) }}
</template>
</el-table-column>
<el-table-column
prop="cnShipName"
label="中文船名"
min-width="120"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="enShipName"
label="英文船名"
min-width="120"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="companyName"
label="船公司"
min-width="150"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="nation"
label="国籍"
width="100"
sortable
align="center">
</el-table-column>
<el-table-column
prop="shipLength"
label="船长"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="deep"
label="吃水"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="pilotType"
label="动态"
width="100"
sortable
align="center">
</el-table-column>
<el-table-column
prop="fromBerthageName"
label="起点泊位"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="toBerthageName"
label="终点泊位"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="mainPilotName"
label="主引"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="trafficBoat"
label="交通船"
width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="agencyName"
label="代理"
min-width="150"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="portAreaOperationType"
label="港区作业类型"
width="120"
sortable
align="center">
</el-table-column>
<el-table-column
prop="waterwayName"
label="航道"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
label="操作"
width="100"
fixed="right"
align="center">
<template #default="scope">
<el-button
type="text"
size="small"
@click="handleView(scope.row)">
查看
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [],
sortMap: {} // 存储排序字段和顺序
}
},
methods: {
handleSortChange({ prop, order }) {
if (prop) {
this.sortMap = {
field: this.sortFieldMap[prop] || prop,
order: order === 'ascending' ? 'ASC' : 'DESC'
};
} else {
this.sortMap = {};
}
this.getPilotPlans();
}
}
}
</script>
<style>
/* 表格样式优化 */
.el-table {
font-size: 12px;
}
.el-table th.el-table__cell {
background-color: #f5f7fa;
color: #303133;
font-weight: 600;
}
.el-table .el-table__cell {
padding: 8px 0;
}
/* 固定列样式 */
.el-table .fixed-column {
position: sticky;
background-color: #fff;
z-index: 2;
}
/* 表头排序箭头样式 */
.el-table .el-table__header th.el-table__cell > .el-table__cell.sortable {
cursor: pointer;
}
.el-table .el-table__header th.el-table__cell > .el-table__cell.sortable:hover {
background-color: #e6e6e6;
}
/* 内容截断样式 */
.el-table .cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<template>
<div class="table-container">
<el-table
:data="tableData"
style="width: 100%"
size="default"
stripe
border
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 'bold' }"
@sort-change="handleSortChange"
max-height="600">
<!-- 序号列 - 固定在左侧 -->
<el-table-column
type="index"
label="序号"
width="50"
fixed
align="center">
</el-table-column>
<!-- 数据列 -->
<el-table-column
prop="planTime"
label="计划时间"
width="160"
sortable
align="center"
show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.planTime) }}
</template>
</el-table-column>
<el-table-column
prop="cnShipName"
label="中文船名"
min-width="120"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="enShipName"
label="英文船名"
min-width="120"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="companyName"
label="船公司"
min-width="150"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="nation"
label="国籍"
width="100"
sortable
align="center">
</el-table-column>
<el-table-column
prop="shipLength"
label="船长"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="deep"
label="吃水"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="pilotType"
label="动态"
width="100"
sortable
align="center">
</el-table-column>
<el-table-column
prop="fromBerthageName"
label="起点泊位"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="toBerthageName"
label="终点泊位"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="mainPilotName"
label="主引"
width="80"
sortable
align="center">
</el-table-column>
<el-table-column
prop="trafficBoat"
label="交通船"
width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="agencyName"
label="代理"
min-width="150"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="portAreaOperationType"
label="港区作业类型"
width="120"
sortable
align="center">
</el-table-column>
<el-table-column
prop="waterwayName"
label="航道"
min-width="100"
sortable
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="80"
sortable
align="center">
</el-table-column>
<!-- 操作列 - 固定在右侧 -->
<el-table-column
label="操作"
width="100"
fixed="right"
align="center">
<template #default="scope">
<el-button
type="text"
size="small"
@click="handleView(scope.row)">
查看
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
style="margin-top: 15px">
</el-pagination>
</div>
</template>
<style scoped>
.table-container {
max-width: 100%;
overflow: auto;
}
/* 表格样式优化 */
:deep(.el-table) {
font-size: 12px;
}
:deep(.el-table th.el-table__cell) {
background-color: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table .el-table__cell) {
padding: 8px 0;
}
/* 内容截断样式 */
:deep(.el-table .cell) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 固定列背景色 */
:deep(.el-table .fixed-column) {
background-color: #fff;
z-index: 2;
}
/* 表头排序箭头样式 */
:deep(.el-table .el-table__header th.el-table__cell > .el-table__cell.sortable) {
cursor: pointer;
}
:deep(.el-table .el-table__header th.el-table__cell > .el-table__cell.sortable:hover) {
background-color: #e6e6e6;
}
</style>
@Service
public class PilotPlanService {
@Autowired
private DispPilotPlanReadRepository dispPilotPlanReadRepository;
/**
* 获取引航计划列表(支持数据库级分页和排序)
*/
@DataSource(value = RoutingDataSourceConfig.DataSourceType.BRANCH)
public List<PilotPlanDTO> getPilotPlans(PilotPlanQueryDTO queryDTO) {
// 获取分页参数(BaseQueryDTO中定义)
Integer page = queryDTO.getPage() != null ? queryDTO.getPage() - 1 : 0;
Integer pageSize = queryDTO.getPageSize() != null ? queryDTO.getPageSize() : 20;
// 创建分页对象
Pageable pageable = PageRequest.of(page, pageSize);
// 如果有排序参数,则添加排序
if (queryDTO.getSortBy() != null && !queryDTO.getSortBy().isEmpty()) {
String sortBy = queryDTO.getSortBy();
String sortOrder = queryDTO.getSortOrder();
if (sortOrder != null && !sortOrder.isEmpty()) {
Sort sort = Sort.by(Sort.Order.asc(sortBy));
if ("DESC".equalsIgnoreCase(sortOrder)) {
sort = Sort.by(Sort.Order.desc(sortBy));
}
// 更新分页对象,添加排序
pageable = PageRequest.of(page, pageSize, sort);
}
}
// 使用Repository查询,传入分页和排序参数
Page<PilotPlanProjection> result = dispPilotPlanReadRepository.findWithFiltersProjected(
planTimeStart,
planTimeEnd,
null,
isEmpty(queryDTO.getPortId()) ? null : queryDTO.getPortId(),
null,
isEmpty(queryDTO.getStatus()) ? null : queryDTO.getStatus(),
isEmpty(queryDTO.getShipownerId()) ? null : queryDTO.getShipownerId(),
queryDTO.getDeepStart(),
queryDTO.getDeepEnd(),
isEmpty(queryDTO.getTrafficboat()) ? null : queryDTO.getTrafficboat(),
queryDTO.getIsInnerPort(),
isEmpty(queryDTO.getEnglishShipName()) ? null : queryDTO.getEnglishShipName(),
pageable // 传入分页和排序参数
);
// 获取查询结果
List<PilotPlanProjection> projections = result.getContent();
// 转换为DTO并返回
List<PilotPlanDTO> pilotPlanDTOs = new ArrayList<>();
for (int i = 0; i < projections.size(); i++) {
PilotPlanProjection projection = projections.get(i);
PilotPlanDTO pilotPlanDTO = new PilotPlanDTO();
// 设置序号(基于分页计算)
pilotPlanDTO.setIndex(page * pageSize + i + 1);
// 设置其他字段...
pilotPlanDTO.setPilotPlanId(projection.getPilotPlanId());
pilotPlanDTO.setPlanTime(projection.getPlanTime());
pilotPlanDTOs.add(pilotPlanDTO);
}
return pilotPlanDTOs;
}
}
关键点说明:
page * pageSize + i + 1,确保序号连续且正确@RestController
@RequestMapping("/pilot-plan")
public class PilotPlanController {
@Autowired
private PilotPlanService pilotPlanService;
/**
* 获取引航计划列表(支持分页和排序)
*/
@GetMapping("/list")
public ResponseEntity<PageResponse<PilotPlanDTO>> getPilotPlans(PilotPlanQueryDTO queryDTO) {
PageResponse<PilotPlanDTO> pilotPlans = pilotPlanService.getPilotPlans(queryDTO);
return new ResponseEntity<>(pilotPlans, HttpStatus.OK);
}
}
methods: {
getPilotPlans() {
const params = {
planTimeStart: this.searchForm.planTimeRange && this.searchForm.planTimeRange.length > 0
? this.formatDate(this.searchForm.planTimeRange[0])
: '',
planTimeEnd: this.searchForm.planTimeRange && this.searchForm.planTimeRange.length > 1
? this.formatDate(this.searchForm.planTimeRange[1])
: '',
portAreaOperationType: this.searchForm.portAreaOperationType,
portId: this.searchForm.portId,
englishShipName: this.searchForm.englishShipName,
status: this.searchForm.status,
shipownerId: this.searchForm.shipownerId,
deepStart: this.searchForm.deepStart,
deepEnd: this.searchForm.deepEnd,
trafficboat: this.searchForm.trafficboat,
page: this.currentPage, // 当前页码
pageSize: this.pageSize, // 每页大小
sortBy: this.sortMap.sortBy, // 排序字段
sortOrder: this.sortMap.sortOrder // 排序方向
};
this.$axios.get('/api/pilot-plan/list', { params })
.then(response => {
// 从PageResponse中获取数据
this.pilotPlans = response.data.content; // 当前页数据
this.total = response.data.totalElements; // 总记录数
// 可选:获取其他分页信息
// this.totalPages = response.data.totalPages; // 总页数
// this.currentPage = response.data.currentPage; // 当前页码
})
.catch(error => {
console.error('获取引航计划失败:', error);
this.$message.error('获取引航计划失败,请稍后重试');
});
}
}
关键点说明:
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import router from './router'
import axios from 'axios'
import tokenRefreshManager from './utils/tokenRefreshManager'
import PermissionManager from './utils/permission'
import permissionDirective from './directives/permission'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn
})
app.use(router)
// 注册权限指令
app.directive('permission', permissionDirective)
// 配置axios
app.config.globalProperties.$axios = axios
// 设置token刷新回调,刷新时更新权限信息
tokenRefreshManager.setOnTokenRefresh(async (newToken) => {
console.log('Token已刷新,更新权限信息...');
try {
await PermissionManager.initializePermissions(`Bearer ${newToken}`);
console.log('权限信息更新完成');
} catch (error) {
console.error('更新权限信息失败:', error);
}
});
app.mount('#app')
// 权限指令,用于控制按钮显示
const permissionDirective = {
// 在绑定元素插入父节点时调用
mounted(el, binding) {
// 获取按钮需要的功能代码
const requiredFunctionCode = binding.value;
// 从localStorage中获取当前用户的功能代码列表
const userFunctionCodesStr = localStorage.getItem('userFunctionCodes');
if (!userFunctionCodesStr) {
// 如果没有功能代码列表,隐藏按钮
el.style.display = 'none';
return;
}
try {
const userFunctionCodes = JSON.parse(userFunctionCodesStr);
// 检查用户是否拥有该功能代码
if (!userFunctionCodes.includes(requiredFunctionCode)) {
// 用户没有权限,隐藏按钮
el.style.display = 'none';
}
} catch (error) {
console.error('解析用户功能代码失败:', error);
// 解析失败时隐藏按钮
el.style.display = 'none';
}
},
// 在绑定元素的父节点更新时调用
updated(el, binding) {
// 获取按钮需要的功能代码
const requiredFunctionCode = binding.value;
// 从localStorage中获取当前用户的功能代码列表
const userFunctionCodesStr = localStorage.getItem('userFunctionCodes');
if (!userFunctionCodesStr) {
// 如果没有功能代码列表,隐藏按钮
el.style.display = 'none';
return;
}
try {
const userFunctionCodes = JSON.parse(userFunctionCodesStr);
// 检查用户是否拥有该功能代码
if (!userFunctionCodes.includes(requiredFunctionCode)) {
// 用户没有权限,隐藏按钮
el.style.display = 'none';
} else {
// 用户有权限,显示按钮
el.style.display = '';
}
} catch (error) {
console.error('解析用户功能代码失败:', error);
// 解析失败时隐藏按钮
el.style.display = 'none';
}
}
};
export default permissionDirective;
<template>
<div>
<!-- 查询按钮 -->
<el-button type="primary" @click="getPilotPlans" v-permission="'pilotplan:query'">查询</el-button>
<!-- 重置按钮 -->
<el-button @click="resetSearchForm" v-permission="'pilotplan:reset'">重置</el-button>
<!-- 导入按钮 -->
<el-button type="primary" @click="openImportDialog" v-permission="'pilotplan:import'">导入引航计划</el-button>
<!-- 导出按钮 -->
<el-button type="success" @click="exportPilotPlans" v-permission="'pilotplan:export'">导出Excel</el-button>
<!-- 导入对话框中的按钮 -->
<el-button @click="importDialogVisible = false" v-permission="'pilotplan:import:cancel'">取消</el-button>
<el-button type="primary" @click="parseImportData" v-permission="'pilotplan:import:parse'">解析数据</el-button>
<!-- 预览对话框中的按钮 -->
<el-button @click="previewDialogVisible = false" v-permission="'pilotplan:import:preview:cancel'">取消</el-button>
<el-button type="primary" @click="confirmImport" v-permission="'pilotplan:import:preview:confirm'">
确认导入
</el-button>
</div>
</template>
根据常见命名规范,引航计划相关的功能代码可能如下:
| 按钮 | 功能代码 | 说明 |
|---|---|---|
| 展开 | pilotplan:expand | 展开/收起查询条件 |
| 查询 | pilotplan:query | 查询引航计划列表 |
| 重置 | pilotplan:reset | 重置查询条件 |
| 导入 | pilotplan:import | 导入引航计划 |
| 导出 | pilotplan:export | 导出引航计划Excel |
| 取消(导入) | pilotplan:import:cancel | 取消导入操作 |
| 解析数据 | pilotplan:import:parse | 解析导入数据 |
| 取消(预览) | pilotplan:import:preview:cancel | 取消预览 |
| 确认导入 | pilotplan:import:preview:confirm | 确认导入数据 |
注意:以上功能代码是根据常见命名规范推测的,实际功能代码需要从数据库Sys_FunctionCode表中查询确认。