This commit is contained in:
pengxiaolong
2025-05-13 19:39:53 +08:00
parent 37da6765b8
commit c006a8e63d
1232 changed files with 96963 additions and 883 deletions

View File

@@ -0,0 +1,308 @@
<template>
<div
:class="[n('')]"
@mouseup.stop
>
<div :class="[n('body')]">
<div :class="[n('body-header')]">
<div :class="[n('body-header-prev')]">
<div
v-if="canYearLess"
:class="[n('icon')]"
@click="change('year', -1)"
>
<Icon
:file="dLeftArrowIcon"
:width="'12px'"
:height="'12px'"
/>
</div>
<div
v-if="canMonthLess"
:class="[n('icon')]"
@click="change('month', -1)"
>
<Icon
:file="leftArrowIcon"
:width="'10px'"
:height="'10px'"
/>
</div>
</div>
<div :class="[n('body-header-label')]">
<div :class="[n('body-header-label-item')]">
{{ year }}
</div>
<div :class="[n('body-header-label-item')]">
{{ TUITranslateService.t(`time.${month}`) }}
</div>
</div>
<div :class="[n('body-header-next')]">
<div
v-if="canMonthMore"
:class="[n('icon')]"
@click="change('month', 1)"
>
<Icon
:file="rightArrowIcon"
:width="'10px'"
:height="'10px'"
/>
</div>
<div
v-if="canYearMore"
:class="[n('icon')]"
@click="change('year', 1)"
>
<Icon
:file="dRightArrowIcon"
:width="'12px'"
:height="'12px'"
/>
</div>
</div>
</div>
<div :class="[n('body-content')]">
<DateTable
:type="props.type"
:date="props.date"
:startDate="props.startDate"
:endDate="props.endDate"
:currentPanelDate="currentPanelDate"
@pick="handlePick"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onBeforeMount } from '../../../adapter-vue';
import dayjs, { Dayjs, ManipulateType } from 'dayjs';
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { DateCell } from './date-picker';
import DateTable from './date-table.vue';
import Icon from '../Icon.vue';
import leftArrowIcon from '../../../assets/icon/left-arrow.svg';
import rightArrowIcon from '../../../assets/icon/right-arrow.svg';
import dLeftArrowIcon from '../../../assets/icon/d-left-arrow.svg';
import dRightArrowIcon from '../../../assets/icon/d-right-arrow.svg';
import { isPC } from '../../../utils/env';
const props = defineProps({
type: {
type: String,
default: 'range', // "single"/"range"
},
// Unique attribute when type is single
date: {
type: Dayjs,
default: () => dayjs(),
},
// Unique attribute when type is range
startDate: {
type: Dayjs,
default: null,
},
endDate: {
type: Dayjs,
default: null,
},
rangeType: {
type: String,
default: '', // "left"/"right"
},
currentOtherPanelValue: {
type: Dayjs,
default: null,
},
});
const emit = defineEmits(['pick', 'change']);
const n = (className: string) => {
return className
? [
'tui-date-picker-panel-' + className,
!isPC && 'tui-date-picker-panel-h5-' + className,
]
: ['tui-date-picker-panel', !isPC && 'tui-date-picker-panel-h5'];
};
const currentPanelDate = ref<typeof Dayjs>();
const year = computed(() => currentPanelDate.value?.get('year'));
const month = computed(() => currentPanelDate.value?.format('MMMM'));
const canYearMore = computed(() => {
const prevYearNumber = props.currentOtherPanelValue?.year() - 1;
const prevYear = props.currentOtherPanelValue?.year(prevYearNumber);
return (
props.rangeType === 'right'
|| currentPanelDate.value?.isBefore(prevYear, 'year')
);
});
const canMonthMore = computed(() => {
const prevMonthNumber = props.currentOtherPanelValue?.month() - 1;
const prevMonth = props.currentOtherPanelValue?.month(prevMonthNumber);
return (
props.rangeType === 'right'
|| currentPanelDate.value?.isBefore(prevMonth, 'month')
);
});
const canYearLess = computed(() => {
const nextYearNumber = props.currentOtherPanelValue?.year() + 1;
const nextYear = props.currentOtherPanelValue?.year(nextYearNumber);
return (
props.rangeType === 'left'
|| currentPanelDate.value?.isAfter(nextYear, 'year')
);
});
const canMonthLess = computed(() => {
const nextMonthNumber = props.currentOtherPanelValue?.month() + 1;
const nextMonth = props.currentOtherPanelValue?.month(nextMonthNumber);
return (
props.rangeType === 'left'
|| currentPanelDate.value?.isAfter(nextMonth, 'month')
);
});
// Range judgment:
// Premise: If there is only one, it must be the start.
// If there is a startDate:
// When the left side of the interface first displays the month/year of the startDate.
// If there is both a startDate and an endDate:
// If they are in the same month:
// Both are displayed on the left, and the next month is displayed on the right.
// If they are not in the same month:
// The start is displayed on the left, and the end is displayed on the right.
// That is, to determine whether the start and end are in the same month.
// If neither is present, the left displays the current month, and the right displays the next month.
const handleSingleDate = (): { date: typeof Dayjs } => {
if (props.date && dayjs(props.date)?.isValid()) {
// props.date year and month
return {
date: props?.date,
};
}
// nowadays year and month
return {
date: dayjs(),
};
};
const handleRangeDate = (): { date: typeof Dayjs } => {
switch (props.rangeType) {
case 'left':
if (props.startDate && dayjs.isDayjs(props.startDate)) {
return {
date: props?.startDate,
};
} else {
return {
date: dayjs(),
};
}
case 'right':
if (
props.endDate
&& dayjs.isDayjs(props.endDate)
&& props?.endDate?.isAfter(props.startDate, 'month')
) {
return {
date: props?.endDate,
};
} else {
const _month = (props.startDate || dayjs()).month();
return {
date: (props.startDate || dayjs()).month(_month + 1),
};
}
default:
return {
date: dayjs(),
};
}
};
function handlePick(cell: DateCell) {
emit('pick', cell);
}
function change(type: typeof ManipulateType, num: number) {
currentPanelDate.value = dayjs(currentPanelDate.value.toDate()).add(
num,
type,
);
emit('change', currentPanelDate.value);
}
onBeforeMount(() => {
switch (props.type) {
case 'single':
currentPanelDate.value = handleSingleDate().date;
emit('change', currentPanelDate.value);
break;
case 'range':
currentPanelDate.value = handleRangeDate().date;
emit('change', currentPanelDate.value);
break;
}
});
</script>
<style scoped lang="scss">
.tui-date-picker-panel {
width: 200px;
margin: 5px;
&-body {
width: 200px;
display: flex;
flex-direction: column;
&-header {
width: 100%;
display: flex;
flex-direction: row;
height: 30px;
padding: 0 5px;
box-sizing: border-box;
&-prev {
display: flex;
flex-direction: row;
cursor: pointer;
width: 24px;
}
&-label {
flex: 1;
display: flex;
flex-direction: row;
text-align: center;
align-items: center;
justify-content: center;
user-select: none;
color: #666;
&-item {
padding: 0 5px;
color: #666;
}
}
&-next {
display: flex;
flex-direction: row;
cursor: pointer;
width: 24px;
}
}
}
&-icon {
display: flex;
justify-content: center;
align-items: center;
width: 12px;
}
}
</style>

View File

@@ -0,0 +1,19 @@
import { Dayjs } from 'dayjs';
export type DateCellType =
| 'normal'
| 'today'
| 'week'
| 'next-month'
| 'prev-month';
export interface DateCell {
text?: number;
disabled?: boolean;
isSelected?: boolean;
isSelectedStart?: boolean;
isSelectedEnd?: boolean;
isInRange?: boolean;
isCurrent?: boolean;
date: typeof Dayjs;
type?: DateCellType;
}

View File

@@ -0,0 +1,321 @@
<template>
<table
:class="['tui-date-table', !isPC && 'tui-date-table-h5']"
cellspacing="0"
cellpadding="0"
role="grid"
>
<tbody class="tui-date-table-body">
<tr class="tui-date-table-body-weeks">
<th
v-for="item in WEEKS"
:key="item"
class="tui-date-table-body-weeks-item"
:aria-label="item + ''"
scope="col"
>
{{ TUITranslateService.t(`time.${item}`) }}
</th>
</tr>
<tr
v-for="(row, rowKey) in rows"
:key="rowKey"
class="tui-date-table-body-days"
>
<td
v-for="(col, colKey) in row"
:key="colKey"
:class="['tui-date-table-body-days-item', col.type]"
>
<div
:class="[
'tui-date-table-body-days-item-cell',
col.isSelected && 'selected',
col.isSelectedStart && 'selected-start',
col.isSelectedEnd && 'selected-end',
col.isInRange && 'range',
]"
@click="handlePick(col)"
>
<span class="tui-date-table-body-days-item-cell-text">
{{ col.text }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import {
computed,
ref,
getCurrentInstance,
nextTick,
watch,
} from '../../../adapter-vue';
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';
import { DateCell, DateCellType } from './date-picker';
import { isPC } from '../../../utils/env';
const props = defineProps({
type: {
type: String,
default: 'range', // "single"/"range"
},
currentPanelDate: {
type: Dayjs,
default: () => dayjs(),
},
// Unique attribute when type is single
date: {
type: Dayjs,
default: null,
},
// Unique attribute when type is range
startDate: {
type: Dayjs,
default: null,
},
endDate: {
type: Dayjs,
default: null,
},
});
const emit = defineEmits(['pick']);
// vue instance
const instance = getCurrentInstance();
const tableRows = ref<DateCell[][]>([[], [], [], [], [], []]);
const currentPanelDateObject = ref<typeof Dayjs>(
dayjs(props.currentPanelDate || null),
);
const dateObject = ref<typeof Dayjs>(dayjs(props.date || null));
const startDateObject = ref<typeof Dayjs>(dayjs(props.startDate || null));
const endDateObject = ref<typeof Dayjs>(dayjs(props.endDate || null));
const WEEKS_CONSTANT = computed(() => {
return dayjs.weekdaysShort();
});
const WEEKS = computed(() =>
WEEKS_CONSTANT.value.map((w: string) => w.substring(1)),
);
const startDateOnTable = computed(() => {
const startDayOfMonth = currentPanelDateObject.value?.startOf('month');
return startDayOfMonth?.subtract(startDayOfMonth?.day() || 7, 'day');
});
// Table data
const rows = computed(() => {
const rows_ = tableRows.value;
const cols = WEEKS.value.length;
const startOfMonth = currentPanelDateObject.value?.startOf('month');
const startOfMonthDay = startOfMonth?.day() || 7; // day of this month first day
const dateCountOfMonth = startOfMonth?.daysInMonth(); // total days of this month
let count = 1;
for (let row = 0; row < tableRows.value.length; row++) {
for (let col = 0; col < cols; col++) {
const cellDate = startDateOnTable.value?.add(count, 'day');
const text = cellDate?.date();
// For type === "single", select the entered date
// For type === "range", select the entered start and end dates
const isSelected
= props.type === 'single'
&& cellDate?.format('YYYY-MM-DD')
=== dateObject.value?.format('YYYY-MM-DD');
const isSelectedStart
= props.type === 'range'
&& cellDate?.format('YYYY-MM-DD')
=== startDateObject.value?.format('YYYY-MM-DD');
const isSelectedEnd
= props.type === 'range'
&& cellDate?.format('YYYY-MM-DD')
=== endDateObject.value?.format('YYYY-MM-DD');
// For type === "range", check if it is within the selected range.
const isInRange
= cellDate?.isSameOrBefore(endDateObject.value, 'day')
&& cellDate?.isSameOrAfter(startDateObject.value, 'day');
let type: DateCellType = 'normal';
if (count < startOfMonthDay) {
// Prev month's date
type = 'prev-month';
} else if (count - startOfMonthDay >= dateCountOfMonth) {
// Next month's date
type = 'next-month';
}
rows_[row][col] = {
type,
date: cellDate,
text,
isSelected: isSelected || isSelectedStart || isSelectedEnd,
isSelectedStart,
isSelectedEnd,
isInRange,
};
count++;
}
}
return rows_;
});
const handlePick = (cell: DateCell) => {
if (cell?.type !== 'normal') {
return;
}
emit('pick', cell);
};
watch(
() => [props.currentPanelDate, props.date, props.startDate, props.endDate],
() => {
currentPanelDateObject.value = dayjs(props.currentPanelDate || null);
dateObject.value = dayjs(props.date || null);
startDateObject.value = dayjs(props.startDate || null);
endDateObject.value = dayjs(props.endDate || null);
nextTick(() => {
instance?.proxy?.$forceUpdate();
});
},
{
deep: true,
immediate: true,
},
);
</script>
<style scoped lang="scss">
/* stylelint-disable selector-class-pattern */
.tui-date-table {
border-spacing: 0;
-webkit-border-horizontal-spacing: 0;
-webkit-border-vertical-spacing: 0;
font-size: 12px;
user-select: none;
table-layout: fixed;
width: 100%;
box-sizing: border-box;
&::after,
&::before {
box-sizing: border-box;
}
&-body {
width: 100%;
background-color: #fff;
&-weeks,
&-days {
box-sizing: border-box;
min-width: 0;
display: flex;
flex-direction: row;
justify-content: space-around;
overflow: hidden;
}
&-weeks {
width: 100%;
&-item {
color: #666;
font-size: 12px;
font-weight: 400px;
}
}
&-days {
color: #000;
&-item {
&-cell {
text-align: center;
padding: 2px;
margin: 2px 0;
&-text {
display: inline-flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
user-select: none;
cursor: pointer;
box-sizing: border-box;
}
}
.selected {
border-radius: 12px;
.tui-date-table-body-days-item-cell-text {
box-sizing: border-box;
color: #007aff;
border: 1px solid #007aff;
background-color: #fff;
}
}
.range {
background-color: #007aff33;
}
.selected-start {
border-radius: 12px 0 0 12px;
}
.selected-end {
border-radius: 0 12px 12px 0;
}
.selected-start.selected-end {
border-radius: 12px;
}
}
.prev-month,
.next-month {
color: #666;
background-color: #fff;
.range {
color: #666;
background-color: #fff;
}
.selected {
.tui-date-table-body-days-item-cell-text {
box-sizing: border-box;
color: #666;
border: none;
}
}
}
}
}
}
.tui-date-table-h5 {
/* stylelint-disable-next-line no-descending-specificity */
.tui-date-table-body-days-item-cell-text {
cursor: none !important;
}
}
td,
._td,
.tui-date-table-body-days-item {
flex: 1;
}
</style>

View File

@@ -0,0 +1,2 @@
import DatePicker from './index.vue';
export default DatePicker;

View File

@@ -0,0 +1,270 @@
<template>
<div :class="[n([''])]">
<div
:class="[n(['input']), isDatePanelShow && n(['input-active'])]"
@click="setDatePanelDisplay(!isDatePanelShow)"
>
<slot name="start-icon" />
<input
v-model="startFormatDate"
:placeholder="startPlaceholderVal"
:class="[n(['input-start'])]"
style="pointer-events: none"
type="text"
:readonly="true"
:disabled="isUniFrameWork"
autocomplete="false"
>
<span v-if="type !== 'single'">-</span>
<input
v-if="type !== 'single'"
v-model="endFormatDate"
:placeholder="endPlaceholderVal"
:class="[n(['input-end'])]"
style="pointer-events: none"
type="text"
:readonly="true"
:disabled="isUniFrameWork"
autocomplete="false"
>
<slot name="end-icon" />
</div>
<div
v-if="isDatePanelShow"
:class="[n(['dialog'])]"
>
<div
:class="[
n([
'dialog-container',
'dialog-container-' + rangeTableType,
'dialog-container-' + popupPosition,
]),
]"
>
<DatePickerPanel
:type="props.type"
rangeType="left"
:date="dateValue"
:startDate="startValue"
:endDate="endValue"
:currentOtherPanelValue="rightCurrentPanelValue"
@pick="handlePick"
@change="handleLeftPanelChange"
/>
<DatePickerPanel
v-if="props.type === 'range' && isPC && rangeTableType === 'two'"
:type="props.type"
rangeType="right"
:date="dateValue"
:startDate="startValue"
:endDate="endValue"
:currentOtherPanelValue="leftCurrentPanelValue"
@pick="handlePick"
@change="handleRightPanelChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from '../../../adapter-vue';
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
// dayjs extension
import dayjs, { Dayjs } from 'dayjs';
import localeData from 'dayjs/plugin/localeData.js';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js';
import 'dayjs/locale/zh-cn';
import DatePickerPanel from './date-picker-panel.vue';
import { DateCell } from './date-picker';
import { isPC, isUniFrameWork } from '../../../utils/env';
dayjs.extend(localeData);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.locale('zh-cn');
const emit = defineEmits(['pick', 'change']);
const props = defineProps({
type: {
type: String,
default: 'range', // "single" / "range"
},
rangeTableType: {
type: String,
default: 'one', // "one"/ "two"
},
startPlaceholder: {
type: String,
default: () => TUITranslateService.t('开始时间'),
},
endPlaceholder: {
type: String,
default: () => TUITranslateService.t('开始时间'),
},
popupPosition: {
type: String,
default: 'bottom', // "top" / "bottom"
},
// Default single-select date
defaultSingleDate: {
type: Dayjs,
default: null,
required: false,
},
});
const isDatePanelShow = ref<boolean>(false);
const dateValue = ref<typeof Dayjs>(props.type === 'single' ? props?.defaultSingleDate : null);
const startValue = ref<typeof Dayjs>(props.type === 'single' ? props?.defaultSingleDate : null);
const endValue = ref<typeof Dayjs>(props.type === 'single' ? props?.defaultSingleDate : null);
const startFormatDate = computed(() => startValue?.value?.format('YYYY/MM/DD'));
const endFormatDate = computed(() => endValue?.value?.format('YYYY/MM/DD'));
const startPlaceholderVal = props.startPlaceholder;
const endPlaceholderVal = props.endPlaceholder;
const leftCurrentPanelValue = ref<typeof Dayjs>();
const rightCurrentPanelValue = ref<typeof Dayjs>();
const setDatePanelDisplay = (show: boolean) => {
isDatePanelShow.value = show;
};
const n = (classNameList: string[]) => {
const resultClassList: string[] = [];
classNameList.forEach((className: string) => {
if (className) {
resultClassList.push('tui-date-picker-' + className);
!isPC && resultClassList.push('tui-date-picker-h5-' + className);
} else {
resultClassList.push('tui-date-picker');
!isPC && resultClassList.push('tui-date-picker-h5');
}
});
return resultClassList;
};
const handlePick = (cell: DateCell) => {
switch (props.type) {
case 'single':
startValue.value = cell.date;
endValue.value = cell.date;
dateValue.value = cell.date;
emit('change', cell);
emit('pick', dateValue.value);
setTimeout(() => {
setDatePanelDisplay(false);
}, 300);
break;
case 'range':
if (!startValue?.value) {
startValue.value = cell.date;
} else if (!endValue?.value) {
if (startValue?.value?.isSameOrBefore(cell.date, 'day')) {
endValue.value = cell.date;
} else {
endValue.value = startValue.value;
startValue.value = cell.date;
}
emit('pick', {
startDate: startValue?.value?.startOf('date'),
endDate: endValue?.value?.endOf('date'),
});
setTimeout(() => {
setDatePanelDisplay(false);
}, 200);
} else {
startValue.value = cell.date;
endValue.value = null;
}
emit('change', {
startDate: startValue.value,
endDate: endValue.value,
leftCurrentPanel: leftCurrentPanelValue.value,
rightCurrentPanel: leftCurrentPanelValue.value,
});
break;
}
};
const handleLeftPanelChange = (value: typeof Dayjs) => {
leftCurrentPanelValue.value = value;
emit('change', {
startDate: startValue.value,
endDate: endValue.value,
leftCurrentPanel: leftCurrentPanelValue.value,
rightCurrentPanel: leftCurrentPanelValue.value,
});
};
const handleRightPanelChange = (value: typeof Dayjs) => {
rightCurrentPanelValue.value = value;
emit('change', {
startDate: startValue.value,
endDate: endValue.value,
leftCurrentPanel: leftCurrentPanelValue.value,
rightCurrentPanel: leftCurrentPanelValue.value,
});
};
</script>
<style scoped lang="scss">
.tui-date-picker {
&-input {
min-width: 160px;
display: flex;
flex-direction: row;
color: #666;
border-radius: 5px;
font-size: 12px;
&-start,
&-end {
flex: 1;
color: #666;
height: 17px;
border: none;
width: 67px;
background-color: transparent;
font-size: 12px;
text-align: center;
&:focus {
border: none;
outline: none;
}
&::placeholder {
text-align: center;
}
}
}
&-dialog {
position: relative;
&-container {
position: absolute;
display: flex;
flex-direction: row;
padding: 10px;
left: 5px;
background-color: #fff;
box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px, rgba(0, 0, 0, 0.23) 0 3px 6px;
z-index: 1000;
&-bottom {
left: 5px;
}
&-top {
bottom: 30px;
}
&-one {
left: -5px;
}
}
}
}
</style>