feat(components): 添加业务周范围选择器组件并增强表格搜索功能
- 新增 BusinessWeekRangePicker 组件到全局组件注册 - 在 performance 页面中实现默认选中团队视角 - 移除周报周期格式化工具函数,优化搜索面板 - 添加自定义周范围配对搜索字段类型支持 - 实现日期选择器快捷选项 - 添加周范围选择器 UI 组件和相应的样式布局
This commit is contained in:
@@ -11,15 +11,20 @@ interface Option {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface DatePickerShortcut {
|
||||
text: string;
|
||||
value: Date | [Date, Date] | (() => Date) | (() => [Date, Date]);
|
||||
}
|
||||
|
||||
export interface SearchField {
|
||||
/** 字段键名 */
|
||||
key: string;
|
||||
/** 字段标签 */
|
||||
label: string;
|
||||
/** 字段类型 */
|
||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'weekRangePair' | 'dict';
|
||||
/** date 字段的日期粒度 */
|
||||
dateType?: 'date' | 'month';
|
||||
dateType?: 'date' | 'month' | 'week';
|
||||
/** dateRange 字段的日期范围粒度 */
|
||||
dateRangeType?: 'daterange' | 'monthrange';
|
||||
/** 日期面板展示格式 */
|
||||
@@ -28,6 +33,12 @@ export interface SearchField {
|
||||
rangeSeparator?: string;
|
||||
/** 日期字段提交格式 */
|
||||
valueFormat?: string;
|
||||
/** 日期快捷选项 */
|
||||
shortcuts?: DatePickerShortcut[];
|
||||
/** 日期面板切换回调 */
|
||||
onCalendarChange?: (value: unknown) => void;
|
||||
/** 日期弹层显示状态回调 */
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
/** 占位列数,默认 1 */
|
||||
span?: number;
|
||||
/** select 类型的选项 */
|
||||
@@ -142,6 +153,55 @@ function getFieldValue(field: SearchField) {
|
||||
const value = props.modelValue[field.key];
|
||||
return field.resolveValue ? field.resolveValue(value) : value;
|
||||
}
|
||||
|
||||
function resolveWeekPickerValue(value: unknown, index: 0 | 1) {
|
||||
if (!Array.isArray(value)) return '';
|
||||
return value[index] || '';
|
||||
}
|
||||
|
||||
function normalizeWeekPickerValue(value: string | null | undefined) {
|
||||
if (!value) return '';
|
||||
|
||||
const [year, month, day] = value.split('-').map(item => Number(item));
|
||||
const date = new Date(year, (month || 1) - 1, day || 1);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
if (dayOfWeek === 0) {
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
|
||||
const currentDay = date.getDay() || 7;
|
||||
date.setDate(date.getDate() - currentDay + 1);
|
||||
|
||||
const normalizedYear = date.getFullYear();
|
||||
const normalizedMonth = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const normalizedDay = String(date.getDate()).padStart(2, '0');
|
||||
return `${normalizedYear}-${normalizedMonth}-${normalizedDay}`;
|
||||
}
|
||||
|
||||
function updateWeekRangeFieldValue(field: SearchField, index: 0 | 1, rawValue: string | null | undefined) {
|
||||
const currentValue = getFieldValue(field);
|
||||
const nextValue: [string, string] = [
|
||||
resolveWeekPickerValue(currentValue, 0),
|
||||
resolveWeekPickerValue(currentValue, 1)
|
||||
];
|
||||
|
||||
nextValue[index] = normalizeWeekPickerValue(rawValue);
|
||||
|
||||
if (!nextValue[0] && !nextValue[1]) {
|
||||
updateFieldValue(field, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextValue[0] && nextValue[1] && nextValue[0] > nextValue[1]) {
|
||||
const temp = nextValue[0];
|
||||
nextValue[0] = nextValue[1];
|
||||
nextValue[1] = temp;
|
||||
}
|
||||
|
||||
updateFieldValue(field, nextValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
@@ -188,7 +248,10 @@ function getFieldValue(field: SearchField) {
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
:format="field.format"
|
||||
:shortcuts="field.shortcuts"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
@calendar-change="field.onCalendarChange?.($event)"
|
||||
@visible-change="field.onVisibleChange?.($event)"
|
||||
@update:model-value="val => updateFieldValue(field, val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
@@ -199,12 +262,38 @@ function getFieldValue(field: SearchField) {
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
:format="field.format"
|
||||
:shortcuts="field.shortcuts"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
:range-separator="field.rangeSeparator || '至'"
|
||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||
@calendar-change="field.onCalendarChange?.($event)"
|
||||
@visible-change="field.onVisibleChange?.($event)"
|
||||
@update:model-value="val => updateFieldValue(field, val)"
|
||||
/>
|
||||
<div v-else-if="field.type === 'weekRangePair'" class="table-search-fields__week-range-pair">
|
||||
<ElDatePicker
|
||||
:model-value="resolveWeekPickerValue(getFieldValue(field), 0)"
|
||||
type="week"
|
||||
placeholder="开始周"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => updateWeekRangeFieldValue(field, 0, val)"
|
||||
/>
|
||||
<span class="table-search-fields__week-range-separator">至</span>
|
||||
<ElDatePicker
|
||||
:model-value="resolveWeekPickerValue(getFieldValue(field), 1)"
|
||||
type="week"
|
||||
placeholder="结束周"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => updateWeekRangeFieldValue(field, 1, val)"
|
||||
/>
|
||||
</div>
|
||||
<DictSelect
|
||||
v-else-if="field.type === 'dict'"
|
||||
:model-value="getFieldValue(field)"
|
||||
@@ -290,7 +379,10 @@ function getFieldValue(field: SearchField) {
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
:format="field.format"
|
||||
:shortcuts="field.shortcuts"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
@calendar-change="field.onCalendarChange?.($event)"
|
||||
@visible-change="field.onVisibleChange?.($event)"
|
||||
@update:model-value="val => updateFieldValue(field, val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
@@ -301,12 +393,38 @@ function getFieldValue(field: SearchField) {
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
:format="field.format"
|
||||
:shortcuts="field.shortcuts"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
:range-separator="field.rangeSeparator || '至'"
|
||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||
@calendar-change="field.onCalendarChange?.($event)"
|
||||
@visible-change="field.onVisibleChange?.($event)"
|
||||
@update:model-value="val => updateFieldValue(field, val)"
|
||||
/>
|
||||
<div v-else-if="field.type === 'weekRangePair'" class="table-search-fields__week-range-pair">
|
||||
<ElDatePicker
|
||||
:model-value="resolveWeekPickerValue(getFieldValue(field), 0)"
|
||||
type="week"
|
||||
placeholder="开始周"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => updateWeekRangeFieldValue(field, 0, val)"
|
||||
/>
|
||||
<span class="table-search-fields__week-range-separator">至</span>
|
||||
<ElDatePicker
|
||||
:model-value="resolveWeekPickerValue(getFieldValue(field), 1)"
|
||||
type="week"
|
||||
placeholder="结束周"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => updateWeekRangeFieldValue(field, 1, val)"
|
||||
/>
|
||||
</div>
|
||||
<DictSelect
|
||||
v-else-if="field.type === 'dict'"
|
||||
:model-value="getFieldValue(field)"
|
||||
@@ -362,4 +480,23 @@ function getFieldValue(field: SearchField) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-search-fields__week-range-pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-search-fields__week-range-pair :deep(.el-date-editor) {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-search-fields__week-range-separator {
|
||||
flex: 0 0 auto;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -20,6 +20,7 @@ declare module 'vue' {
|
||||
BusinessRichTextView: typeof import('./../components/custom/business-rich-text-view.vue')['default']
|
||||
BusinessUserPicker: typeof import('./../components/custom/business-user-picker.vue')['default']
|
||||
BusinessUserSelect: typeof import('./../components/custom/business-user-select.vue')['default']
|
||||
BusinessWeekRangePicker: typeof import('./../components/custom/business-week-range-picker.vue')['default']
|
||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
CurrentUserRoleTags: typeof import('./../components/custom/current-user-role-tags.vue')['default']
|
||||
|
||||
@@ -97,8 +97,9 @@ function transformPageResult(response: PerformanceSheetPageResponse, pageNo: num
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const authStore = useAuthStore();
|
||||
const canUseTeamDashboard = computed(() => hasAuth(PerformancePermission.TeamDashboard));
|
||||
const searchParams = reactive(createSearchParams());
|
||||
const teamViewMode = ref<TeamViewMode>('self');
|
||||
const teamViewMode = ref<TeamViewMode>(canUseTeamDashboard.value ? 'team' : 'self');
|
||||
const subordinateTreeLoading = ref(false);
|
||||
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
||||
const selectedSubordinateUserId = ref<string | null>(null);
|
||||
@@ -134,7 +135,6 @@ const ACTION_ICON_MAP = {
|
||||
response: markRaw(IconMdiFileDocumentEditOutline)
|
||||
};
|
||||
|
||||
const canUseTeamDashboard = computed(() => hasAuth(PerformancePermission.TeamDashboard));
|
||||
const canCreate = computed(() => hasAuth(PerformancePermission.SheetCreate));
|
||||
const canUpdate = computed(() => hasAuth(PerformancePermission.SheetUpdate));
|
||||
const canDelete = computed(() => hasAuth(PerformancePermission.SheetDelete));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { fetchGetWorkReportStatusDict } from '@/service/api';
|
||||
import type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||
import { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||
import { formatIsoWeekRangeLabel, normalizeWeeklySearchRange } from '../utils';
|
||||
import { normalizeWeeklySearchRange } from '../utils';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
|
||||
@@ -44,11 +44,43 @@ const statusOptions = computed(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const weeklyPeriodPlaceholder = computed(() => {
|
||||
const range = normalizeWeeklySearchRange(model.value.periodStartDate as string[] | undefined);
|
||||
if (!range?.length) return '请选择周报周期';
|
||||
return formatIsoWeekRangeLabel(range[0], range[1]) || '请选择周报周期';
|
||||
});
|
||||
const projectPeriodDraftStartDate = ref('');
|
||||
|
||||
function parseProjectPeriodStartDate() {
|
||||
const startDate = projectPeriodDraftStartDate.value || (model.value.periodStartDate as string[] | undefined)?.[0];
|
||||
const parsed = dayjs(startDate);
|
||||
return parsed.isValid() ? parsed.toDate() : new Date();
|
||||
}
|
||||
|
||||
function handleProjectPeriodCalendarChange(value: unknown) {
|
||||
const startDate = Array.isArray(value) ? value[0] : '';
|
||||
const parsed = dayjs(startDate);
|
||||
projectPeriodDraftStartDate.value = parsed.isValid() ? parsed.format('YYYY-MM-DD') : '';
|
||||
}
|
||||
|
||||
function handleProjectPeriodVisibleChange(visible: boolean) {
|
||||
if (!visible) {
|
||||
projectPeriodDraftStartDate.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildProjectPeriodShortcut(text: string, mutator: (date: Date) => void) {
|
||||
return {
|
||||
text,
|
||||
value: () => {
|
||||
const startDate = parseProjectPeriodStartDate();
|
||||
const endDate = new Date(startDate.getTime());
|
||||
mutator(endDate);
|
||||
return [startDate, endDate] as [Date, Date];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const projectPeriodShortcuts = [
|
||||
buildProjectPeriodShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||
buildProjectPeriodShortcut('三星期', date => date.setDate(date.getDate() + 21)),
|
||||
buildProjectPeriodShortcut('一个月', date => date.setMonth(date.getMonth() + 1))
|
||||
];
|
||||
|
||||
const fields = computed<SearchField[]>(() => {
|
||||
const baseFields: SearchField[] = [
|
||||
@@ -69,10 +101,7 @@ const fields = computed<SearchField[]>(() => {
|
||||
const weeklyPeriodField: SearchField = {
|
||||
key: 'periodStartDate',
|
||||
label: '周期',
|
||||
type: 'dateRange',
|
||||
placeholder: weeklyPeriodPlaceholder.value,
|
||||
format: 'YYYY[年第]ww[周]',
|
||||
rangeSeparator: '至'
|
||||
type: 'weekRangePair'
|
||||
};
|
||||
|
||||
const teamReporterField: SearchField[] = props.teamMode
|
||||
@@ -107,7 +136,15 @@ const fields = computed<SearchField[]>(() => {
|
||||
if (props.reportType === 'project') {
|
||||
return [
|
||||
baseFields[0],
|
||||
{ key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' },
|
||||
{
|
||||
key: 'periodStartDate',
|
||||
label: '周期',
|
||||
type: 'dateRange',
|
||||
placeholder: '请选择周期',
|
||||
shortcuts: projectPeriodShortcuts,
|
||||
onCalendarChange: handleProjectPeriodCalendarChange,
|
||||
onVisibleChange: handleProjectPeriodVisibleChange
|
||||
},
|
||||
{
|
||||
key: 'projectId',
|
||||
label: '项目',
|
||||
@@ -165,6 +202,13 @@ watch(
|
||||
watch(
|
||||
() => model.value.periodStartDate,
|
||||
value => {
|
||||
if (props.reportType === 'project') {
|
||||
if (!Array.isArray(value) || !value[0]) {
|
||||
projectPeriodDraftStartDate.value = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.reportType !== 'weekly') return;
|
||||
|
||||
const normalizedValue = normalizeWeeklySearchRange(value as string[] | undefined);
|
||||
|
||||
Reference in New Issue
Block a user