first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
:: BASE_DOC ::
## API
### CountDown Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
auto-start | Boolean | true | \- | N
content | String | 'default' | \- | N
format | String | HH:mm:ss | \- | N
millisecond | Boolean | false | \- | N
size | String | 'medium' | options: small/medium/large | N
split-with-unit | Boolean | false | \- | N
theme | String | 'default' | options: default/round/square | N
time | Number | 0 | required | Y
### CountDown Events
name | params | description
-- | -- | --
change | `(time: TimeData)` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/count-down/type.ts)。<br/>`interface TimeData { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }`<br/>
finish | \- | \-
### CountDown Slots
name | Description
-- | --
\- | \-
content | \-
### CountDown External Classes
className | Description
-- | --
t-class | \-
t-class-count | \-
t-class-split | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-countdown-bg-color | @error-color | -
--td-countdown-default-color | @text-color-primary | -
--td-countdown-round-border-radius | @radius-circle | -
--td-countdown-round-color | @text-color-anti | -
--td-countdown-square-border-radius | @radius-small | -

View File

@@ -0,0 +1,76 @@
---
title: CountDown 倒计时
description: 用于实时展示倒计时数值。
spline: data
isComponent: true
---
> CountDown 组件用于实时展示倒计时数值。
如果需要与站点演示一致的数字字体效果,推荐您到 <a href="https://tdesign.tencent.com/design/fonts">数字字体章节</a>,将 TCloudNumber 字体下载并将包含的 TCloudNumberVF.ttf 做为 TCloudNumber 字体资源引入到具体项目中使用。
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCountDown from '@tdesign/uniapp/count-down/count-down.vue';
```
### 基础倒计时
{{ base }}
### 调整尺寸
{{ size }}
## API
### CountDown Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
auto-start | Boolean | true | 是否自动开始倒计时 | N
content | String | 'default' | 最终倒计时的展示内容,值为'default'时使用默认的格式,否则使用自定义样式插槽 | N
format | String | HH:mm:ss | 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒 | N
millisecond | Boolean | false | 是否开启毫秒级渲染 | N
size | String | 'medium' | 倒计时尺寸。可选项small/medium/large | N
split-with-unit | Boolean | false | 使用时间单位分割 | N
theme | String | 'default' | 倒计时风格。可选项default/round/square | N
time | Number | 0 | 必需。倒计时时长,单位毫秒 | Y
### CountDown Events
名称 | 参数 | 描述
-- | -- | --
change | `(time: TimeData)` | 时间变化时触发。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/count-down/type.ts)。<br/>`interface TimeData { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }`<br/>
finish | \- | 倒计时结束时触发
### CountDown Slots
名称 | 描述
-- | --
\- | 默认插槽,作用同 `content` 插槽
content | 自定义 `content` 显示内容
### CountDown External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-count | 计数样式类
t-class-split | 分隔线样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-countdown-bg-color | @error-color | -
--td-countdown-default-color | @text-color-primary | -
--td-countdown-round-border-radius | @radius-circle | -
--td-countdown-round-color | @text-color-anti | -
--td-countdown-square-border-radius | @radius-small | -

View File

@@ -0,0 +1,3 @@
export const format = function (num) {
return num < 10 ? `0${num}` : num;
};

View File

@@ -0,0 +1,110 @@
.t-count-down--small.t-count-down--default {
font-size: var(--td-font-size-base, 28rpx);
}
.t-count-down--small.t-count-down--round > .t-count-down__item {
font-size: var(--td-font-size-s, 24rpx);
}
.t-count-down--small.t-count-down--square > .t-count-down__item {
font-size: var(--td-font-size-s, 24rpx);
}
.t-count-down--small.t-count-down--round > .t-count-down__item,
.t-count-down--small.t-count-down--square > .t-count-down__item {
width: 40rpx;
height: 40rpx;
}
.t-count-down--small.t-count-down--round > .t-count-down__split--dot,
.t-count-down--small.t-count-down--square > .t-count-down__split--dot {
margin: 0 4rpx;
font-size: var(--td-font-size-base, 28rpx);
font-weight: 700;
}
.t-count-down--small.t-count-down--round > .t-count-down__split--text,
.t-count-down--small.t-count-down--square > .t-count-down__split--text {
margin: 0 8rpx;
font-size: var(--td-font-size, 20rpx);
}
.t-count-down--medium.t-count-down--default {
font-size: var(--td-font-size-m, 32rpx);
}
.t-count-down--medium.t-count-down--round > .t-count-down__item {
font-size: var(--td-font-size-base, 28rpx);
}
.t-count-down--medium.t-count-down--square > .t-count-down__item {
font-size: var(--td-font-size-base, 28rpx);
}
.t-count-down--medium.t-count-down--round > .t-count-down__item,
.t-count-down--medium.t-count-down--square > .t-count-down__item {
width: 48rpx;
height: 48rpx;
}
.t-count-down--medium.t-count-down--round > .t-count-down__split--dot,
.t-count-down--medium.t-count-down--square > .t-count-down__split--dot {
margin: 0 6rpx;
font-size: var(--td-font-size-m, 32rpx);
font-weight: 700;
}
.t-count-down--medium.t-count-down--round > .t-count-down__split--text,
.t-count-down--medium.t-count-down--square > .t-count-down__split--text {
margin: 0 10rpx;
font-size: var(--td-font-size-s, 24rpx);
}
.t-count-down--large.t-count-down--default {
font-size: 36rpx;
}
.t-count-down--large.t-count-down--round > .t-count-down__item {
font-size: var(--td-font-size-m, 32rpx);
}
.t-count-down--large.t-count-down--square > .t-count-down__item {
font-size: var(--td-font-size-m, 32rpx);
}
.t-count-down--large.t-count-down--round > .t-count-down__item,
.t-count-down--large.t-count-down--square > .t-count-down__item {
width: 56rpx;
height: 56rpx;
}
.t-count-down--large.t-count-down--round > .t-count-down__split--dot,
.t-count-down--large.t-count-down--square > .t-count-down__split--dot {
margin: 0 12rpx;
font-size: 36rpx;
font-weight: 700;
}
.t-count-down--large.t-count-down--round > .t-count-down__split--text,
.t-count-down--large.t-count-down--square > .t-count-down__split--text {
margin: 0 12rpx;
font-size: var(--td-font-size-base, 28rpx);
}
.t-count-down {
font-family: TCloudNumber, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Hiragino Sans GB, Microsoft YaHei UI, Microsoft YaHei, Source Han Sans CN, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
display: flex;
}
.t-count-down .t-count-down__item,
.t-count-down .t-count-down__split {
display: flex;
align-items: center;
justify-content: center;
}
.t-count-down--square > .t-count-down__split--dot,
.t-count-down--round > .t-count-down__split--dot {
color: var(--td-error-color, var(--td-error-color-6, #d54941));
}
.t-count-down--square > .t-count-down__split--text,
.t-count-down--round > .t-count-down__split--text {
color: var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9)));
}
.t-count-down--default {
color: var(--td-countdown-default-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-count-down--square {
color: var(--td-countdown-round-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-count-down--square > .t-count-down__item {
border-radius: var(--td-countdown-square-border-radius, var(--td-radius-small, 6rpx));
background: var(--td-countdown-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-count-down--round {
color: var(--td-countdown-round-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-count-down--round > .t-count-down__item {
border-radius: var(--td-countdown-round-border-radius, var(--td-radius-circle, 50%));
background: var(--td-countdown-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}

View File

@@ -0,0 +1,166 @@
<template>
<view
:style="tools._style([customStyle])"
:class="classPrefix + ' ' + classPrefix + '--' + theme + ' ' + classPrefix + '--' + size + ' ' + tClass"
aria-role="option"
>
<slot
v-if="content !== 'default'"
name="content"
/>
<slot v-if="content !== 'default'" />
<block v-else-if="theme == 'default' && !splitWithUnit">
{{ formattedTime }}
</block>
<block v-else>
<block
v-for="(item, index) in timeRange"
:key="index"
>
<text :class="classPrefix + '__item ' + tClassCount">
{{ formatUtil(timeData[timeRange[index]]) }}
</text>
<text
v-if="splitWithUnit || timeRange.length - 1 !== index"
:class="classPrefix + '__split ' + classPrefix + '__split--' + (splitWithUnit ? 'text' : 'dot') + ' ' + tClassSplit"
>
{{ splitWithUnit ? timeDataUnit[timeRange[index]] : ':' }}
</text>
</block>
</block>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { isSameSecond, parseFormat, parseTimeData, TimeDataUnit } from './utils';
import tools from '../common/utils.wxs';
import { format as formatUtil } from './computed.js';
const name = `${prefix}-count-down`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-count`,
`${prefix}-class-split`,
],
components: {
TIcon,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
timeDataUnit: TimeDataUnit,
timeData: parseTimeData(0),
formattedTime: '0',
tools,
timeoutId: null,
isInitialTime: false,
};
},
watch: {
time: {
handler() {
this.reset();
},
immediate: true,
},
},
mounted() {
},
beforeUnMount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
},
methods: {
formatUtil,
start() {
if (this.counting) {
return;
}
this.counting = true;
this.endTime = Date.now() + this.remain;
this.doCount();
},
pause() {
this.counting = false;
this.timeoutId && clearTimeout(this.timeoutId);
},
reset() {
this.pause();
this.remain = this.time;
this.updateTime(this.remain);
if (this.autoStart && this.remain > 0) {
this.start();
}
this.isInitialTime = true;
},
getTime() {
return Math.max(this.endTime - Date.now(), 0);
},
updateTime(remain) {
const { format } = this;
this.remain = remain;
const timeData = parseTimeData(remain);
this.$emit('change', timeData);
const { timeText } = parseFormat(remain, format);
const timeRange = format.split(':');
this.timeRange = timeRange;
this.timeData = timeData;
this.formattedTime = timeText.replace(/:/g, ' : ');
if (remain === 0 && (this.counting || this.isInitialTime)) {
this.pause();
this.$emit('finish');
this.counting = false;
}
},
doCount() {
this.timeoutId = setTimeout(() => {
const time = this.getTime();
if (this.millisecond) {
this.updateTime(time);
} else if (!isSameSecond(time, this.remain) || time === 0) {
this.updateTime(time);
}
if (time !== 0) {
this.doCount();
}
}, 33); // 30 帧,因此 1000 / 30 = 33
},
},
});
</script>
<style scoped>
@import './count-down.css';
</style>

View File

@@ -0,0 +1,62 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCountDownProps } from './type';
export default {
/** 是否自动开始倒计时 */
autoStart: {
type: Boolean,
default: true,
},
/** 最终倒计时的展示内容,值为'default'时使用默认的格式,否则使用自定义样式插槽 */
content: {
type: String,
default: 'default' as TdCountDownProps['content'],
},
/** 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒 */
format: {
type: String,
default: 'HH:mm:ss',
},
/** 是否开启毫秒级渲染 */
millisecond: Boolean,
/** 倒计时尺寸 */
size: {
type: String,
default: 'medium' as TdCountDownProps['size'],
validator(val: TdCountDownProps['size']): boolean {
if (!val) return true;
return ['small', 'medium', 'large'].includes(val);
},
},
/** 使用时间单位分割 */
splitWithUnit: Boolean,
/** 倒计时风格 */
theme: {
type: String,
default: 'default' as TdCountDownProps['theme'],
validator(val: TdCountDownProps['theme']): boolean {
if (!val) return true;
return ['default', 'round', 'square'].includes(val);
},
},
/** 倒计时时长,单位毫秒 */
time: {
type: Number,
default: 0,
required: true,
},
/** 时间变化时触发 */
onChange: {
type: Function,
default: () => ({}),
},
/** 倒计时结束时触发 */
onFinish: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,64 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdCountDownProps {
/**
* 是否自动开始倒计时
* @default true
*/
autoStart?: boolean;
/**
* 最终倒计时的展示内容,值为'default'时使用默认的格式,否则使用自定义样式插槽
* @default 'default'
*/
content?: string;
/**
* 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒
* @default HH:mm:ss
*/
format?: string;
/**
* 是否开启毫秒级渲染
* @default false
*/
millisecond?: boolean;
/**
* 倒计时尺寸
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large';
/**
* 使用时间单位分割
* @default false
*/
splitWithUnit?: boolean;
/**
* 倒计时风格
* @default 'default'
*/
theme?: 'default' | 'round' | 'square';
/**
* 倒计时时长,单位毫秒
* @default 0
*/
time: number;
/**
* 时间变化时触发
*/
onChange?: (time: TimeData) => void;
/**
* 倒计时结束时触发
*/
onFinish?: () => void;
}
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}

View File

@@ -0,0 +1,72 @@
export const TimeDataUnit = {
DD: '天',
HH: '时',
mm: '分',
ss: '秒',
SSS: '毫秒',
};
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
export const parseTimeData = function (time) {
const days = Math.floor(time / DAY);
const hours = Math.floor((time % DAY) / HOUR);
const minutes = Math.floor((time % HOUR) / MINUTE);
const seconds = Math.floor((time % MINUTE) / SECOND);
const milliseconds = Math.floor(time % SECOND);
return {
DD: days,
HH: hours,
mm: minutes,
ss: seconds,
SSS: milliseconds,
};
};
export const isSameSecond = function (time1, time2) {
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000);
};
/**
*
* @param time 倒计时时间,毫秒单位
* @param format 倒计时格式化字符串例如dd天hh小时mm分ss秒SSS毫秒hh:mm:ss.SSShh:mm:ss
*/
export const parseFormat = function (time, format) {
const obj = {
'D+': Math.floor(time / 86400000), // 日
'H+': Math.floor((time % 86400000) / 3600000), // 小时
'm+': Math.floor((time % 3600000) / 60000), // 分
's+': Math.floor((time % 60000) / 1000), // 秒
'S+': Math.floor(time % 1000), // 毫秒
};
const timeList = [];
let timeText = format;
Object.keys(obj).forEach((prop) => {
if (new RegExp(`(${prop})`).test(timeText)) {
timeText = timeText.replace(RegExp.$1, (match, offset, source) => {
const v = `${(obj)[prop]}`;
let digit = v;
if (match.length > 1) {
digit = (match.replace(new RegExp(match[0], 'g'), '0') + v).substr(v.length);
}
const unit = source.substr(offset + match.length);
const last = timeList[timeList.length - 1];
if (last) {
const index = last.unit.indexOf(match);
if (index !== -1) {
last.unit = last.unit.substr(0, index);
}
}
timeList.push({ digit, unit, match });
return digit;
});
}
});
return { timeText, timeList };
};