feat(export): 优化时间范围选择逻辑,限制跨度不超过1年

- 修改后端时间跨度验证逻辑,定义1年为从开始日期往后推1年
- 前端日期选择组件新增选择过程监听,记录首选日期以限制第二个日期范围
- 实现禁用时间函数,禁止选择超过1年跨度的时间范围
- 添加时间跨度变化检测,超出1年时清空选择并弹窗提示
- 前端导入中文语言包,支持ElementPlus中文本地化
- API模块统一业务错误码处理,非0码抛出带错误信息异常
This commit is contained in:
zhouyonggao 2025-12-22 16:38:56 +08:00
parent c56c738992
commit 07eafb5684
5 changed files with 183 additions and 26 deletions

View File

@ -23,6 +23,7 @@ import (
) )
// validateTimeRange 验证时间范围不超过1年 // validateTimeRange 验证时间范围不超过1年
// 1年的定义从开始日期往后推1年
func validateTimeRange(startVal, endVal interface{}) error { func validateTimeRange(startVal, endVal interface{}) error {
var startStr, endStr string var startStr, endStr string
@ -60,12 +61,14 @@ func validateTimeRange(startVal, endVal interface{}) error {
return errors.New("结束时间必须晚于开始时间") return errors.New("结束时间必须晚于开始时间")
} }
// 计算时间跨度 // 计算开始日期往后推1年的时间点
duration := endTime.Sub(startTime) oneYearLater := startTime.AddDate(1, 0, 0)
oneYear := 365 * 24 * time.Hour
if duration > oneYear { // 如果结束时间超过这个时间点则超过1年限制
return fmt.Errorf("时间跨度超过1年限制当前跨度为 %.1f 天,最多允许 365 天", duration.Hours()/24) if endTime.After(oneYearLater) {
duration := endTime.Sub(startTime)
daysCount := duration.Hours() / 24
return fmt.Errorf("时间跨度超过1年限制当前跨度为 %.1f 天,最多允许 365 天", daysCount)
} }
return nil return nil

View File

@ -235,12 +235,15 @@
<el-date-picker <el-date-picker
v-model="exportForm.dateRange" v-model="exportForm.dateRange"
type="datetimerange" type="datetimerange"
range-separator="至"
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="结束时间" end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
:shortcuts="dateShortcuts" :shortcuts="dateShortcuts"
:default-time="dateDefaultTime" :default-time="dateDefaultTime"
:disabled-date="disabledDate" :disabled-date="disabledDate"
@calendar-change="onCalendarChange"
@change="onDateRangeChange"
style="width:100%" style="width:100%"
/> />
</el-form-item> </el-form-item>
@ -308,6 +311,7 @@
<script src="./config.js"></script> <script src="./config.js"></script>
<script src="./vendor/vue.global.prod.js"></script> <script src="./vendor/vue.global.prod.js"></script>
<script src="./vendor/element-plus.full.min.js"></script> <script src="./vendor/element-plus.full.min.js"></script>
<script src="./vendor/element-plus-zh-cn.js"></script>
<!-- 模块化架构 --> <!-- 模块化架构 -->
<script src="./modules/config.js"></script> <script src="./modules/config.js"></script>
<script src="./modules/utils.js"></script> <script src="./modules/utils.js"></script>

View File

@ -190,34 +190,44 @@ const app = createApp({
new Date(2000, 0, 1, 23, 59, 59), new Date(2000, 0, 1, 23, 59, 59),
]; ];
// 用于记录用户正在选择的第一个日期(用于限制第二个日期的可选范围)
const pickerMinDate = Vue.ref(null);
/**
* 日历面板变化事件 - 当用户点击日历时触发
* 用于记录用户选择的第一个日期
* @param {Array} dates - 已选日期数组
*/
const onCalendarChange = (dates) => {
if (dates && dates.length === 1) {
// 用户选择了第一个日期
pickerMinDate.value = dates[0];
} else {
// 选择完成或清空
pickerMinDate.value = null;
}
};
/** /**
* 禁用超过一年的日期 * 禁用超过一年的日期
* 当用户选择了第一个日期后自动限制第二个日期的可选范围
* @param {Date} date - 要检查的日期 * @param {Date} date - 要检查的日期
* @returns {boolean} 是否禁用 * @returns {boolean} 是否禁用
*/ */
const disabledDate = (date) => { const disabledDate = (date) => {
if (!date) return false; if (!date) return false;
// 如果已经选择了开始时间,计算结束时间限制 // 如果用户正在选择第二个日期限制范围不超过1年
if (state.exportForm.dateRange && state.exportForm.dateRange[0]) { if (pickerMinDate.value) {
const startTime = new Date(state.exportForm.dateRange[0]); const selectedDate = new Date(pickerMinDate.value);
const oneYearLater = new Date(startTime); const oneYearMs = 365 * 24 * 60 * 60 * 1000;
oneYearLater.setFullYear(startTime.getFullYear() + 1);
oneYearLater.setDate(oneYearLater.getDate() - 1); // 减去一天以确保不超过一年
// 只限制结束时间不能超过开始时间一年后 // 计算允许的日期范围
if (date > oneYearLater) return true; const minAllowed = new Date(selectedDate.getTime() - oneYearMs);
} const maxAllowed = new Date(selectedDate.getTime() + oneYearMs);
// 如果已经选择了结束时间,计算开始时间限制
if (state.exportForm.dateRange && state.exportForm.dateRange[1]) {
const endTime = new Date(state.exportForm.dateRange[1]);
const oneYearEarlier = new Date(endTime);
oneYearEarlier.setFullYear(endTime.getFullYear() - 1);
oneYearEarlier.setDate(oneYearEarlier.getDate() + 1); // 加上一天
// 只限制开始时间不能早于结束时间一年前 // 禁用超出范围的日期
if (date < oneYearEarlier) return true; return date < minAllowed || date > maxAllowed;
} }
return false; return false;
@ -288,6 +298,41 @@ const app = createApp({
}, },
]; ];
/**
* 验证时间跨度是否超过1年
* 1年的定义从开始日期往后推1年
* @param {Array} dateRange - 时间范围 [开始时间, 结束时间]
* @returns {boolean} 是否超过1年
*/
const isDateRangeExceedOneYear = (dateRange) => {
if (!dateRange || dateRange.length !== 2 || !dateRange[0] || !dateRange[1]) {
return false;
}
const start = new Date(dateRange[0]);
const end = new Date(dateRange[1]);
// 计算开始日期往后推1年的时间点
const oneYearLater = new Date(start);
oneYearLater.setFullYear(start.getFullYear() + 1);
// 如果结束时间超过这个时间点则超过1年
return end > oneYearLater;
};
/**
* 时间范围变化处理函数
* 如果时间跨度超过1年清空并提示
* @param {Array} val - 时间范围
*/
const onDateRangeChange = (val) => {
if (isDateRangeExceedOneYear(val)) {
// 清空时间范围
state.exportForm.dateRange = null;
// 显示提示
showMessage('时间跨度不能超过1年365天请重新选择', 'warning');
}
};
// ==================== 表单引用 ==================== // ==================== 表单引用 ====================
const createFormRef = Vue.ref(null); const createFormRef = Vue.ref(null);
const editFormRef = Vue.ref(null); const editFormRef = Vue.ref(null);
@ -820,7 +865,11 @@ const app = createApp({
showMessage('任务创建返回异常', 'error'); showMessage('任务创建返回异常', 'error');
} }
} catch (error) { } catch (error) {
showMessage(error.message || '导出失败', 'error'); // 使用模态框显示错误信息
ElementPlus.ElMessageBox.alert(error.message || '导出失败', '提示', {
type: 'error',
confirmButtonText: '确定'
});
} finally { } finally {
state.exportSubmitting = false; state.exportSubmitting = false;
} }
@ -1093,6 +1142,8 @@ const app = createApp({
dateDefaultTime, dateDefaultTime,
dateShortcuts, dateShortcuts,
disabledDate, disabledDate,
onCalendarChange,
onDateRangeChange,
// 表单引用 // 表单引用
createFormRef, createFormRef,
editFormRef, editFormRef,
@ -1132,7 +1183,9 @@ const app = createApp({
} }
}); });
app.use(ElementPlus); app.use(ElementPlus, {
locale: window.ElementPlusLocaleZhCn
});
app.mount('#app'); app.mount('#app');
})(); })();

View File

@ -157,7 +157,18 @@ const post = async (endpoint, body, options = {}) => {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(errorText || `请求失败: ${response.status}`); throw new Error(errorText || `请求失败: ${response.status}`);
} }
return response.json();
const result = await response.json();
// 检查业务错误码
if (result && result.code !== undefined && result.code !== 0) {
const error = new Error(result.msg || '操作失败');
error.code = result.code;
error.data = result.data;
throw error;
}
return result;
}; };
/** /**

86
web/vendor/element-plus-zh-cn.js vendored Normal file
View File

@ -0,0 +1,86 @@
/*! Element Plus v2.13.0 - Chinese (zh-cn) */
(function(global) {
var zhCn = {
name: "zh-cn",
el: {
breadcrumb: { label: "面包屑" },
colorpicker: {
confirm: "确定",
clear: "清空",
defaultLabel: "颜色选择器",
description: "当前颜色 {color},按 Enter 键选择新颜色"
},
datepicker: {
now: "此刻",
today: "今天",
cancel: "取消",
clear: "清空",
confirm: "确定",
dateTablePrompt: "使用方向键与 Enter 键可选择日期",
monthTablePrompt: "使用方向键与 Enter 键可选择月份",
yearTablePrompt: "使用方向键与 Enter 键可选择年份",
selectedDate: "已选日期",
selectDate: "选择日期",
selectTime: "选择时间",
startDate: "开始日期",
startTime: "开始时间",
endDate: "结束日期",
endTime: "结束时间",
prevYear: "前一年",
nextYear: "后一年",
prevMonth: "上个月",
nextMonth: "下个月",
year: "年",
month1: "1 月",
month2: "2 月",
month3: "3 月",
month4: "4 月",
month5: "5 月",
month6: "6 月",
month7: "7 月",
month8: "8 月",
month9: "9 月",
month10: "10 月",
month11: "11 月",
month12: "12 月",
weeks: { sun: "日", mon: "一", tue: "二", wed: "三", thu: "四", fri: "五", sat: "六" },
weeksFull: { sun: "星期日", mon: "星期一", tue: "星期二", wed: "星期三", thu: "星期四", fri: "星期五", sat: "星期六" },
months: { jan: "一月", feb: "二月", mar: "三月", apr: "四月", may: "五月", jun: "六月", jul: "七月", aug: "八月", sep: "九月", oct: "十月", nov: "十一月", dec: "十二月" }
},
inputNumber: { decrease: "减少数值", increase: "增加数值" },
select: { loading: "加载中", noMatch: "无匹配数据", noData: "无数据", placeholder: "请选择" },
mention: { loading: "加载中" },
dropdown: { toggleDropdown: "切换下拉选项" },
cascader: { noMatch: "无匹配数据", loading: "加载中", placeholder: "请选择", noData: "暂无数据" },
pagination: {
goto: "前往",
pagesize: "条/页",
total: "共 {total} 条",
pageClassifier: "页",
page: "页",
prev: "上一页",
next: "下一页",
currentPage: "第 {pager} 页",
prevPages: "向前 {pager} 页",
nextPages: "向后 {pager} 页"
},
dialog: { close: "关闭此对话框" },
drawer: { close: "关闭此对话框" },
messagebox: { title: "提示", confirm: "确定", cancel: "取消", error: "输入的数据不合法!", close: "关闭此对话框" },
upload: { deleteTip: "按 Delete 键可删除", delete: "删除", preview: "查看图片", continue: "继续上传" },
slider: { defaultLabel: "滑块介于 {min} 至 {max}", defaultRangeStartLabel: "选择起始值", defaultRangeEndLabel: "选择结束值" },
table: { emptyText: "暂无数据", confirmFilter: "筛选", resetFilter: "重置", clearFilter: "全部", sumText: "合计" },
tag: { close: "关闭此标签" },
tour: { next: "下一步", previous: "上一步", finish: "结束导览", close: "关闭此对话框" },
tree: { emptyText: "暂无数据" },
transfer: { noMatch: "无匹配数据", noData: "无数据", titles: ["列表 1", "列表 2"], filterPlaceholder: "请输入搜索内容", noCheckedFormat: "共 {total} 项", hasCheckedFormat: "已选 {checked}/{total} 项" },
image: { error: "加载失败" },
pageHeader: { title: "返回" },
popconfirm: { confirmButtonText: "确定", cancelButtonText: "取消" },
carousel: { leftArrow: "上一张幻灯片", rightArrow: "下一张幻灯片", indicator: "幻灯片切换至索引 {index}" }
}
};
// 暴露到全局
global.ElementPlusLocaleZhCn = zhCn;
})(typeof window !== 'undefined' ? window : this);