From 2ac36bd4f626ddff7b64a5ef4f7ffd7312b88d3f Mon Sep 17 00:00:00 2001 From: dk <1260500659@qq.com> Date: Fri, 24 Jul 2026 13:59:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(components):=20=E6=B7=BB=E5=8A=A0=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E5=91=A8=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=B9=B6=E5=A2=9E=E5=BC=BA=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BusinessWeekRangePicker 组件到全局组件注册 - 在 performance 页面中实现默认选中团队视角 - 移除周报周期格式化工具函数,优化搜索面板 - 添加自定义周范围配对搜索字段类型支持 - 实现日期选择器快捷选项 - 添加周范围选择器 UI 组件和相应的样式布局 --- src/components/custom/table-search-fields.vue | 141 +++++++++++++++++- src/typings/components.d.ts | 1 + .../personal-center/my-performance/index.vue | 4 +- .../shared/components/search-panel.vue | 66 ++++++-- 4 files changed, 197 insertions(+), 15 deletions(-) diff --git a/src/components/custom/table-search-fields.vue b/src/components/custom/table-search-fields.vue index 1a2fa6d..f414dbe 100644 --- a/src/components/custom/table-search-fields.vue +++ b/src/components/custom/table-search-fields.vue @@ -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); +} @@ -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)" /> +
+ + + +
+
+ + + +
diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index bf60d36..9ae8062 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -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'] diff --git a/src/views/personal-center/my-performance/index.vue b/src/views/personal-center/my-performance/index.vue index 8d61773..2de760a 100644 --- a/src/views/personal-center/my-performance/index.vue +++ b/src/views/personal-center/my-performance/index.vue @@ -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('self'); +const teamViewMode = ref(canUseTeamDashboard.value ? 'team' : 'self'); const subordinateTreeLoading = ref(false); const subordinateTree = ref(null); const selectedSubordinateUserId = ref(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)); diff --git a/src/views/personal-center/work-report/shared/components/search-panel.vue b/src/views/personal-center/work-report/shared/components/search-panel.vue index 0890ddb..f6929fa 100644 --- a/src/views/personal-center/work-report/shared/components/search-panel.vue +++ b/src/views/personal-center/work-report/shared/components/search-panel.vue @@ -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(() => { const baseFields: SearchField[] = [ @@ -69,10 +101,7 @@ const fields = computed(() => { 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(() => { 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);