feat(components): 添加业务周范围选择器组件并增强表格搜索功能
- 新增 BusinessWeekRangePicker 组件到全局组件注册 - 在 performance 页面中实现默认选中团队视角 - 移除周报周期格式化工具函数,优化搜索面板 - 添加自定义周范围配对搜索字段类型支持 - 实现日期选择器快捷选项 - 添加周范围选择器 UI 组件和相应的样式布局
This commit is contained in:
@@ -11,15 +11,20 @@ interface Option {
|
|||||||
value: string | number;
|
value: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DatePickerShortcut {
|
||||||
|
text: string;
|
||||||
|
value: Date | [Date, Date] | (() => Date) | (() => [Date, Date]);
|
||||||
|
}
|
||||||
|
|
||||||
export interface SearchField {
|
export interface SearchField {
|
||||||
/** 字段键名 */
|
/** 字段键名 */
|
||||||
key: string;
|
key: string;
|
||||||
/** 字段标签 */
|
/** 字段标签 */
|
||||||
label: string;
|
label: string;
|
||||||
/** 字段类型 */
|
/** 字段类型 */
|
||||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
type: 'input' | 'select' | 'date' | 'dateRange' | 'weekRangePair' | 'dict';
|
||||||
/** date 字段的日期粒度 */
|
/** date 字段的日期粒度 */
|
||||||
dateType?: 'date' | 'month';
|
dateType?: 'date' | 'month' | 'week';
|
||||||
/** dateRange 字段的日期范围粒度 */
|
/** dateRange 字段的日期范围粒度 */
|
||||||
dateRangeType?: 'daterange' | 'monthrange';
|
dateRangeType?: 'daterange' | 'monthrange';
|
||||||
/** 日期面板展示格式 */
|
/** 日期面板展示格式 */
|
||||||
@@ -28,6 +33,12 @@ export interface SearchField {
|
|||||||
rangeSeparator?: string;
|
rangeSeparator?: string;
|
||||||
/** 日期字段提交格式 */
|
/** 日期字段提交格式 */
|
||||||
valueFormat?: string;
|
valueFormat?: string;
|
||||||
|
/** 日期快捷选项 */
|
||||||
|
shortcuts?: DatePickerShortcut[];
|
||||||
|
/** 日期面板切换回调 */
|
||||||
|
onCalendarChange?: (value: unknown) => void;
|
||||||
|
/** 日期弹层显示状态回调 */
|
||||||
|
onVisibleChange?: (visible: boolean) => void;
|
||||||
/** 占位列数,默认 1 */
|
/** 占位列数,默认 1 */
|
||||||
span?: number;
|
span?: number;
|
||||||
/** select 类型的选项 */
|
/** select 类型的选项 */
|
||||||
@@ -142,6 +153,55 @@ function getFieldValue(field: SearchField) {
|
|||||||
const value = props.modelValue[field.key];
|
const value = props.modelValue[field.key];
|
||||||
return field.resolveValue ? field.resolveValue(value) : value;
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
@@ -188,7 +248,10 @@ function getFieldValue(field: SearchField) {
|
|||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:format="field.format"
|
:format="field.format"
|
||||||
|
:shortcuts="field.shortcuts"
|
||||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
|
@calendar-change="field.onCalendarChange?.($event)"
|
||||||
|
@visible-change="field.onVisibleChange?.($event)"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
@@ -199,12 +262,38 @@ function getFieldValue(field: SearchField) {
|
|||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:format="field.format"
|
:format="field.format"
|
||||||
|
:shortcuts="field.shortcuts"
|
||||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
:range-separator="field.rangeSeparator || '至'"
|
:range-separator="field.rangeSeparator || '至'"
|
||||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
:end-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)"
|
@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
|
<DictSelect
|
||||||
v-else-if="field.type === 'dict'"
|
v-else-if="field.type === 'dict'"
|
||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
@@ -290,7 +379,10 @@ function getFieldValue(field: SearchField) {
|
|||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:format="field.format"
|
:format="field.format"
|
||||||
|
:shortcuts="field.shortcuts"
|
||||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
|
@calendar-change="field.onCalendarChange?.($event)"
|
||||||
|
@visible-change="field.onVisibleChange?.($event)"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
@@ -301,12 +393,38 @@ function getFieldValue(field: SearchField) {
|
|||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:format="field.format"
|
:format="field.format"
|
||||||
|
:shortcuts="field.shortcuts"
|
||||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
:range-separator="field.rangeSeparator || '至'"
|
:range-separator="field.rangeSeparator || '至'"
|
||||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
:end-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)"
|
@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
|
<DictSelect
|
||||||
v-else-if="field.type === 'dict'"
|
v-else-if="field.type === 'dict'"
|
||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
@@ -362,4 +480,23 @@ function getFieldValue(field: SearchField) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
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>
|
</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']
|
BusinessRichTextView: typeof import('./../components/custom/business-rich-text-view.vue')['default']
|
||||||
BusinessUserPicker: typeof import('./../components/custom/business-user-picker.vue')['default']
|
BusinessUserPicker: typeof import('./../components/custom/business-user-picker.vue')['default']
|
||||||
BusinessUserSelect: typeof import('./../components/custom/business-user-select.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']
|
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||||
CurrentUserRoleTags: typeof import('./../components/custom/current-user-role-tags.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 { hasAuth } = useAuth();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const canUseTeamDashboard = computed(() => hasAuth(PerformancePermission.TeamDashboard));
|
||||||
const searchParams = reactive(createSearchParams());
|
const searchParams = reactive(createSearchParams());
|
||||||
const teamViewMode = ref<TeamViewMode>('self');
|
const teamViewMode = ref<TeamViewMode>(canUseTeamDashboard.value ? 'team' : 'self');
|
||||||
const subordinateTreeLoading = ref(false);
|
const subordinateTreeLoading = ref(false);
|
||||||
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
||||||
const selectedSubordinateUserId = ref<string | null>(null);
|
const selectedSubordinateUserId = ref<string | null>(null);
|
||||||
@@ -134,7 +135,6 @@ const ACTION_ICON_MAP = {
|
|||||||
response: markRaw(IconMdiFileDocumentEditOutline)
|
response: markRaw(IconMdiFileDocumentEditOutline)
|
||||||
};
|
};
|
||||||
|
|
||||||
const canUseTeamDashboard = computed(() => hasAuth(PerformancePermission.TeamDashboard));
|
|
||||||
const canCreate = computed(() => hasAuth(PerformancePermission.SheetCreate));
|
const canCreate = computed(() => hasAuth(PerformancePermission.SheetCreate));
|
||||||
const canUpdate = computed(() => hasAuth(PerformancePermission.SheetUpdate));
|
const canUpdate = computed(() => hasAuth(PerformancePermission.SheetUpdate));
|
||||||
const canDelete = computed(() => hasAuth(PerformancePermission.SheetDelete));
|
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 type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
import TableSearchFields 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 { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||||
import { formatIsoWeekRangeLabel, normalizeWeeklySearchRange } from '../utils';
|
import { normalizeWeeklySearchRange } from '../utils';
|
||||||
|
|
||||||
dayjs.extend(isoWeek);
|
dayjs.extend(isoWeek);
|
||||||
|
|
||||||
@@ -44,11 +44,43 @@ const statusOptions = computed(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const weeklyPeriodPlaceholder = computed(() => {
|
const projectPeriodDraftStartDate = ref('');
|
||||||
const range = normalizeWeeklySearchRange(model.value.periodStartDate as string[] | undefined);
|
|
||||||
if (!range?.length) return '请选择周报周期';
|
function parseProjectPeriodStartDate() {
|
||||||
return formatIsoWeekRangeLabel(range[0], range[1]) || '请选择周报周期';
|
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 fields = computed<SearchField[]>(() => {
|
||||||
const baseFields: SearchField[] = [
|
const baseFields: SearchField[] = [
|
||||||
@@ -69,10 +101,7 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
const weeklyPeriodField: SearchField = {
|
const weeklyPeriodField: SearchField = {
|
||||||
key: 'periodStartDate',
|
key: 'periodStartDate',
|
||||||
label: '周期',
|
label: '周期',
|
||||||
type: 'dateRange',
|
type: 'weekRangePair'
|
||||||
placeholder: weeklyPeriodPlaceholder.value,
|
|
||||||
format: 'YYYY[年第]ww[周]',
|
|
||||||
rangeSeparator: '至'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const teamReporterField: SearchField[] = props.teamMode
|
const teamReporterField: SearchField[] = props.teamMode
|
||||||
@@ -107,7 +136,15 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
if (props.reportType === 'project') {
|
if (props.reportType === 'project') {
|
||||||
return [
|
return [
|
||||||
baseFields[0],
|
baseFields[0],
|
||||||
{ key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' },
|
{
|
||||||
|
key: 'periodStartDate',
|
||||||
|
label: '周期',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '请选择周期',
|
||||||
|
shortcuts: projectPeriodShortcuts,
|
||||||
|
onCalendarChange: handleProjectPeriodCalendarChange,
|
||||||
|
onVisibleChange: handleProjectPeriodVisibleChange
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'projectId',
|
key: 'projectId',
|
||||||
label: '项目',
|
label: '项目',
|
||||||
@@ -165,6 +202,13 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => model.value.periodStartDate,
|
() => model.value.periodStartDate,
|
||||||
value => {
|
value => {
|
||||||
|
if (props.reportType === 'project') {
|
||||||
|
if (!Array.isArray(value) || !value[0]) {
|
||||||
|
projectPeriodDraftStartDate.value = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (props.reportType !== 'weekly') return;
|
if (props.reportType !== 'weekly') return;
|
||||||
|
|
||||||
const normalizedValue = normalizeWeeklySearchRange(value as string[] | undefined);
|
const normalizedValue = normalizeWeeklySearchRange(value as string[] | undefined);
|
||||||
|
|||||||
Reference in New Issue
Block a user