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

76
App.vue Normal file
View File

@@ -0,0 +1,76 @@
<script>
export default {
onLaunch: function() {
// 检查是否已登录
const token = uni.getStorageSync('token')
if (!token) {
// 未登录,跳转到登录页
uni.reLaunch({
url: '/pages/login/login'
})
}
},
onShow: function() {
},
onHide: function() {
}
}
</script>
<style>
/* 全局样式 */
page, body {
background-color: #F8F9FA;
color: #333333;
font-size: 28rpx;
line-height: 1.6;
height: 100%;
margin: 0;
padding: 0;
}
/* 通用容器 */
view {
box-sizing: border-box;
}
/* 移除所有按钮的默认边框和阴影 */
button {
border: none !important;
box-shadow: none !important;
outline: none !important;
}
button::after {
border: none !important;
box-shadow: none !important;
}
button:active {
background: none !important;
}
/* 主按钮样式 */
.btn-primary {
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%) !important;
color: #FFFFFF !important;
border: none !important;
box-shadow: 0 4rpx 12rpx rgba(255, 122, 0, 0.3);
}
/* 次要按钮样式 */
.btn-secondary {
background: #F5F5F5 !important;
color: #666666 !important;
border: 2rpx solid #E0E0E0 !important;
}
/* 线框按钮样式 */
.btn-outline {
background: transparent !important;
color: #FF7A00 !important;
border: 2rpx solid #FF7A00 !important;
}
</style>

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

24
main.js Normal file
View File

@@ -0,0 +1,24 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
import '@tdesign/uniapp/common/style/theme/index.css';
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif

80
manifest.json Normal file
View File

@@ -0,0 +1,80 @@
{
"name" : "yu",
"appid" : "__UNI__90009A1",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx7677406cd2dfb7ac",
"setting" : {
"urlCheck" : false,
"minified" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"optimization" : {
"treeShaking" : {
"enable" : true
}
}
}
}

47
pages.json Normal file
View File

@@ -0,0 +1,47 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^t-(.*)": "@/uni_modules/tdesign-uniapp/components/$1/$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "预约系统"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/register/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/booking/booking",
"style": {
"navigationBarTitleText": "我要预约"
}
},
{
"path": "pages/appointments/appointments",
"style": {
"navigationBarTitleText": "我的预约"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "预约系统",
"navigationBarBackgroundColor": "#FF7A00",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

View File

@@ -0,0 +1,237 @@
<template>
<view class="container">
<!-- 状态筛选 -->
<view class="tabs">
<t-tabs :value="activeTab" @change="onTabChange">
<t-tab-panel value="all" label="全部" />
<t-tab-panel value="pending" label="待确认" />
<t-tab-panel value="confirmed" label="已确认" />
<t-tab-panel value="completed" label="已完成" />
<t-tab-panel value="cancelled" label="已取消" />
</t-tabs>
</view>
<!-- 预约列表 -->
<view class="appointments-list">
<t-loading v-if="loading" loading />
<view v-else-if="appointments.length === 0" class="empty-state">
<t-empty description="暂无预约记录" />
</view>
<view v-else>
<view v-for="appointment in appointments" :key="appointment.id" class="appointment-card">
<view class="appointment-header">
<view class="appointment-status">
<t-tag :theme="getStatusTheme(appointment.status)" size="small">
{{ getStatusText(appointment.status) }}
</t-tag>
</view>
<view class="appointment-date">
{{ formatDate(appointment.created_at) }}
</view>
</view>
<view class="appointment-body">
<view class="appointment-row">
<text class="row-label">时间段</text>
<text class="row-value">
{{ appointment.time_slot ? (appointment.time_slot.date).split('T')[0] : '' }}
{{ appointment.time_slot ? formatTime(appointment.time_slot.start_time) : '' }} -
{{ appointment.time_slot ? formatTime(appointment.time_slot.end_time) : '' }}
</text>
</view>
<view class="appointment-row">
<text class="row-label">人数</text>
<text class="row-value">{{ appointment.people_count }}</text>
</view>
</view>
<view class="appointment-footer" v-if="appointment.status === 'pending'">
<t-button class="btn-outline" size="small" theme="danger" variant="outline"
@click="cancelAppointment(appointment)">
取消预约
</t-button>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { api, type Appointment } from '@/utils/api'
const activeTab = ref('all')
const appointments = ref<Appointment[]>([])
const loading = ref(false)
onMounted(() => {
loadAppointments()
})
// 加载预约列表
const loadAppointments = async () => {
loading.value = true
try {
const params: any = {}
if (activeTab.value !== 'all') {
params.status = activeTab.value
}
appointments.value = await api.appointments.getList(params)
// 调试:打印第一个预约的数据
} catch (error) {
console.error('加载预约失败', error)
} finally {
loading.value = false
}
}
// 切换标签
const onTabChange = (value: any) => {
// TDesign 可能传递对象或字符串
const tabValue = typeof value === 'string' ? value : (value?.value || 'all')
activeTab.value = tabValue
loadAppointments()
}
// 获取状态主题
const getStatusTheme = (status: string) => {
const themes: Record<string, any> = {
pending: 'warning',
confirmed: 'success',
completed: 'primary',
cancelled: 'default'
}
return themes[status] || 'default'
}
// 获取状态文本
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
pending: '待确认',
confirmed: '已确认',
completed: '已完成',
cancelled: '已取消'
}
return texts[status] || status
}
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
try {
return dateStr.replace('T', ' ').split('+')[0];
} catch (error) {
console.error('Date format error:', error, dateStr)
return dateStr
}
}
// 格式化时间
const formatTime = (timeStr: string) => {
// 直接提取时间部分,避免时区转换问题
const timePart = timeStr.split('T')[1] || timeStr
const timeWithoutZone = timePart.split('+')[0].split('Z')[0]
const [hours, minutes] = timeWithoutZone.split(':')
return `${hours}:${minutes}`
}
// 取消预约
const cancelAppointment = (appointment: Appointment) => {
uni.showModal({
title: '确认取消',
content: '确定要取消这个预约吗?',
success: async (res) => {
if (res.confirm) {
try {
await api.appointments.cancel(appointment.id)
uni.showToast({
title: '取消成功',
icon: 'success'
})
loadAppointments()
} catch (error) {
console.error('取消预约失败', error)
}
}
}
})
}
</script>
<style scoped>
.container {
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
background: #F8F9FA;
overflow: hidden;
}
.tabs {
flex-shrink: 0;
background: #ffffff;
}
.appointments-list {
flex: 1;
overflow-y: auto;
padding: 20rpx 32rpx;
}
.empty-state {
padding: 120rpx 0;
}
.appointment-card {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.appointment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.appointment-date {
font-size: 24rpx;
color: #999;
}
.appointment-body {
padding: 24rpx 0;
border-top: 1rpx solid #f0f0f0;
border-bottom: 1rpx solid #f0f0f0;
}
.appointment-row {
display: flex;
margin-bottom: 16rpx;
}
.row-label {
font-size: 28rpx;
color: #666;
min-width: 120rpx;
}
.row-value {
font-size: 28rpx;
color: #333;
flex: 1;
}
.appointment-footer {
padding-top: 24rpx;
display: flex;
justify-content: flex-end;
}
</style>

533
pages/booking/booking.vue Normal file
View File

@@ -0,0 +1,533 @@
<template>
<view class="container">
<!-- 自定义日历选择器 -->
<view class="date-selector">
<view class="calendar-header">
<text class="month-title">{{ currentDateStr }}</text>
</view>
<view class="calendar-weekdays">
<text v-for="day in weekdays" :key="day" class="weekday">{{ day }}</text>
</view>
<view class="calendar-days">
<view v-for="(day, index) in calendarDays" :key="day.dateStr || `placeholder-${index}`" class="calendar-day"
:class="{
'placeholder': day.isPlaceholder,
'selected': day.dateStr === selectedDate,
'today': day.isToday,
'disabled': !day.hasBookings && !day.isPlaceholder,
'has-bookings': day.hasBookings
}" @click="!day.isPlaceholder && selectDate(day)">
<text v-if="!day.isPlaceholder" class="day-number">{{ day.day }}</text>
<text v-if="!day.isPlaceholder && day.bookingCount > 0" class="booking-count">
{{ day.bookingCount }}
</text>
</view>
</view>
</view>
<!-- 时间槽列表 -->
<scroll-view class="timeslots-scroll" scroll-y="true">
<view class="timeslots-section">
<view class="section-title">
{{ selectedDate }} 可选时间段
<text v-if="totalBookingCount > 0" class="total-booking">
(总预约: {{ totalBookingCount }})
</text>
</view>
<t-loading v-if="loading" loading />
<view v-else-if="timeslots.length === 0" class="empty-state">
<t-empty description="暂无可预约时间段" />
</view>
<view v-else class="timeslots-list">
<view v-for="slot in timeslots" :key="slot.id" class="timeslot-card"
:class="{ disabled: !slot.is_active || slot.current_people >= slot.max_people }">
<view class="timeslot-info">
<view class="time-range">
{{ formatTime(slot.start_time) }} - {{ formatTime(slot.end_time) }}
</view>
<view class="slot-status">
<t-tag :theme="slot.current_people >= slot.max_people ? 'danger' : 'success'" size="small">
{{ slot.current_people }}/{{ slot.max_people }}
</t-tag>
</view>
</view>
<t-button t-class="btn-primary" size="small" theme="primary"
:disabled="!slot.is_active || slot.current_people >= slot.max_people"
@click.stop="showBookingModal(slot)">
{{ slot.current_people >= slot.max_people ? '已满' : '预约' }}
</t-button>
</view>
</view>
</view>
</scroll-view>
<!-- 预约弹窗 -->
<t-dialog v-model:visible="showDialog" title="预约确认" cancelBtn="取消"
confirmBtn="确认预约" t-class-confirm="btn-primary" @confirm="confirmBooking"
@cancel="showDialog = false">
<template #content>
<!-- 适配skyline增加type="list" -->
<scroll-view type="list" scroll-y class="long-content">
<view class="booking-dialog">
<view class="dialog-item">
<text class="dialog-label">日期</text>
<text class="dialog-value">{{ selectedDate }}</text>
</view>
<view class="dialog-item">
<text class="dialog-label">时间段</text>
<text class="dialog-value">{{ selectedSlot?.start_time ? formatTime(selectedSlot.start_time) : '' }} - {{
selectedSlot?.end_time ? formatTime(selectedSlot.end_time) : '' }}</text>
</view>
<view class="dialog-item">
<text class="dialog-label">人数</text>
<t-stepper v-model="peopleCount" :min="1"
:max="selectedSlot?.max_people - selectedSlot?.current_people || 1" :disabled="true" size="small" />
</view>
</view>
</scroll-view>
</template>
</t-dialog>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { api, type TimeSlot } from '@/utils/api'
// 获取本地时区的日期字符串(东八区)
const getLocalDateStr = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const selectedDate = ref(getLocalDateStr())
const timeslots = ref<TimeSlot[]>([])
const loading = ref(false)
const showDialog = ref(false)
const selectedSlot = ref<TimeSlot | null>(null)
const peopleCount = ref(1)
const notes = ref('')
const isBooking = ref(false) // 防止重复点击
// 日历相关
const currentDate = ref(new Date())
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const dateBookings = ref<Record<string, number>>({})
const dateHasSlots = ref<Record<string, boolean>>({})
// 计算当前月份显示
const currentDateStr = computed(() => {
const year = currentDate.value.getFullYear()
const month = currentDate.value.getMonth() + 1
return `${year}${month}`
})
// 计算总预约人数
const totalBookingCount = computed(() => {
return timeslots.value.reduce((sum, slot) => sum + slot.current_people, 0)
})
// 生成日历数据 - 只显示本月(但保留上个月的占位以对齐星期)
const calendarDays = computed(() => {
const year = currentDate.value.getFullYear()
const month = currentDate.value.getMonth()
// 获取当月第一天和最后一天
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const today = new Date()
// 获取当月第一天是星期几
const startWeekday = firstDay.getDay()
const days: any[] = []
// 添加上个月的日期占位(用于对齐),但设置为不可点击
for (let i = startWeekday - 1; i >= 0; i--) {
days.push({
day: '',
dateStr: '',
isPlaceholder: true,
isOtherMonth: false,
isToday: false,
bookingCount: 0,
hasBookings: false
})
}
// 获取当月的日期
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = new Date(year, month, i)
// 使用本地时间获取日期部分,避免时区问题
const yearNum = date.getFullYear()
const monthNum = date.getMonth() + 1
const dayNum = date.getDate()
const dateStr = `${yearNum}-${String(monthNum).padStart(2, '0')}-${String(dayNum).padStart(2, '0')}`
// 使用本地时间比较是否为今天
const todayYear = today.getFullYear()
const todayMonth = today.getMonth()
const todayDay = today.getDate()
const isToday = yearNum === todayYear && monthNum - 1 === todayMonth && dayNum === todayDay
days.push({
day: i,
dateStr,
isPlaceholder: false,
isOtherMonth: false,
isToday,
bookingCount: dateBookings.value[dateStr] || 0,
hasBookings: dateHasSlots.value[dateStr] || false
})
}
return days
})
// 格式化日期字符串
const formatDateStr = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const dateStr = `${year}-${month}-${day}`
return dateStr
}
// 检查登录状态
onMounted(() => {
const token = uni.getStorageSync('token')
if (!token) {
uni.redirectTo({ url: '/pages/login/login' })
} else {
loadTimeSlots()
loadDateBookings()
}
})
// 加载日期预约统计
const loadDateBookings = async () => {
try {
const slots = await api.timeslots.getList({ is_active: true })
const bookings: Record<string, number> = {}
const slotsMap: Record<string, boolean> = {}
slots.forEach(slot => {
const dateStr = slot.date.split('T')[0]
bookings[dateStr] = (bookings[dateStr] || 0) + slot.current_people
slotsMap[dateStr] = true
})
dateBookings.value = bookings
dateHasSlots.value = slotsMap
} catch (error) {
console.error('加载日期预约统计失败', error)
}
}
// 加载时间槽
const loadTimeSlots = async () => {
loading.value = true
try {
const slots = await api.timeslots.getList({
date: selectedDate.value,
is_active: true
})
timeslots.value = slots
} catch (error) {
console.error('加载时间槽失败', error)
} finally {
loading.value = false
}
}
// 选择日期
const selectDate = (day: any) => {
selectedDate.value = day.dateStr
loadTimeSlots()
}
// 格式化时间
const formatTime = (timeStr: string) => {
// 直接提取时间部分,避免时区转换问题
const timePart = timeStr.split('T')[1] || timeStr
const timeWithoutZone = timePart.split('+')[0].split('Z')[0]
const [hours, minutes] = timeWithoutZone.split(':')
return `${hours}:${minutes}`
}
// 显示预约弹窗
const showBookingModal = (slot: TimeSlot) => {
// 防止重复点击
if (isBooking.value) {
uni.showToast({
title: '正在处理,请稍候',
icon: 'none'
})
return
}
if (!slot.is_active || slot.current_people >= slot.max_people) {
console.log('时间槽不可用', slot)
uni.showToast({
title: '该时间段已不可用',
icon: 'none'
})
return
}
selectedSlot.value = slot
peopleCount.value = 1
notes.value = ''
showDialog.value = true
console.log('打开弹窗', showDialog.value)
}
// 确认预约
const confirmBooking = async () => {
if (!selectedSlot.value) return
isBooking.value = true
try {
await api.appointments.create(selectedSlot.value.id, peopleCount.value, notes.value)
uni.showToast({
title: '预约成功',
icon: 'success'
})
showDialog.value = false
loadTimeSlots()
loadDateBookings()
} catch (error) {
showDialog.value = false
} finally {
isBooking.value = false
}
}
</script>
<style scoped>
.container {
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
background: #f5f5f5;
overflow: hidden;
}
.date-selector {
flex-shrink: 0;
background: #ffffff;
margin: 24rpx 32rpx;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.calendar-header {
text-align: center;
margin-bottom: 24rpx;
}
.month-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8rpx;
margin-bottom: 16rpx;
padding: 0 10rpx;
}
.weekday {
font-size: 24rpx;
color: #999;
text-align: center;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8rpx;
padding: 0 10rpx;
}
.calendar-day {
height: 100rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 8rpx;
cursor: pointer;
transition: all 0.3s ease;
}
.calendar-day.placeholder {
visibility: hidden;
}
.calendar-day.disabled {
opacity: 0.4;
pointer-events: none;
}
.calendar-day.has-bookings {
background: rgba(255, 122, 0, 0.08);
border: 2rpx solid #FF7A00;
}
.calendar-day.today {
background: rgba(255, 122, 0, 0.12);
}
.calendar-day.selected {
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%);
}
.calendar-day.selected .day-number {
color: #ffffff;
}
.calendar-day.selected .booking-count {
color: #ffffff;
}
.day-number {
font-size: 28rpx;
color: #333;
margin-bottom: 4rpx;
}
.booking-count {
font-size: 20rpx;
color: #FF7A00;
}
.timeslots-scroll {
flex: 1;
overflow: hidden;
}
.timeslots-section {
padding: 32rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
display: flex;
align-items: center;
}
.total-booking {
font-size: 24rpx;
color: #FF7A00;
margin-left: 16rpx;
font-weight: normal;
}
.empty-state {
padding: 80rpx 0;
}
.timeslots-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.timeslot-card {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.timeslot-card.disabled {
opacity: 0.6;
}
.timeslot-info {
flex: 1;
}
.time-range {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.slot-status {
margin-top: 8rpx;
}
.booking-dialog {
padding: 32rpx 0;
}
.dialog-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.dialog-item:last-child {
border-bottom: none;
}
.dialog-label {
width: 120rpx;
font-size: 28rpx;
color: #666;
}
.dialog-value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
</style>
<style>
/* 按钮自定义样式 - 使用全局样式 */
.btn-primary {
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%) !important;
border: none !important;
border-radius: 8rpx !important;
color: #FFFFFF !important;
box-shadow: 0 4rpx 12rpx rgba(255, 122, 0, 0.3) !important;
outline: none !important;
}
.btn-primary::after {
border: none !important;
box-shadow: none !important;
}
.btn-primary:active {
background: linear-gradient(135deg, #FF6900 0%, #FF8500 100%) !important;
}
</style>

183
pages/index/index.vue Normal file
View File

@@ -0,0 +1,183 @@
<template>
<view class="container">
<!-- 轮播图 -->
<view class="swiper-container">
<swiper
class="swiper"
:indicator-dots="true"
:autoplay="true"
:interval="3000"
:duration="500"
indicator-color="rgba(255, 255, 255, 0.5)"
indicator-active-color="#FF7A00"
>
<swiper-item v-for="(item, index) in banners" :key="index" class="swiper-item">
<image :src="item.image" class="banner-image" mode="aspectFill" lazy-load />
</swiper-item>
</swiper>
</view>
<!-- 九宫格导航 -->
<view class="grid-scroll">
<view class="grid-container">
<view
v-for="(item, index) in menuItems"
:key="item.path"
class="grid-item"
@click="navigateTo(item.path)"
>
<view class="grid-icon">{{ item.icon }}</view>
<text class="grid-text">{{ item.text }}</text>
</view>
</view>
</view>
<!-- FAB 悬浮退出登录按钮 -->
<t-fab
icon="caret-right"
aria-label="退出"
@click="logout"
/>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { api, type User } from '@/utils/api'
const user = ref<User | null>(null)
// 轮播图数据
const banners = ref([
{ image: 'https://picsum.photos/800/400?random=1', title: '专业美容服务' },
{ image: 'https://picsum.photos/800/400?random=2', title: '预约更便捷' },
{ image: 'https://picsum.photos/800/400?random=3', title: '优惠活动' }
])
// 九宫格菜单数据
const menuItems = ref([
{ icon: '📅', text: '我要预约', path: '/pages/booking/booking' },
{ icon: '📋', text: '我的预约', path: '/pages/appointments/appointments' }
])
// 检查登录状态
onMounted(() => {
const token = uni.getStorageSync('token')
if (!token) {
uni.redirectTo({ url: '/pages/login/login' })
} else {
user.value = uni.getStorageSync('user')
}
})
// 导航跳转
const navigateTo = (path: string) => {
uni.navigateTo({ url: path })
}
// 退出登录
const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token')
uni.removeStorageSync('user')
uni.redirectTo({ url: '/pages/login/login' })
}
}
})
}
</script>
<style scoped>
.container {
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
background: #f5f5f5;
overflow: hidden;
}
.welcome-section {
flex-shrink: 0;
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%);
padding: 32rpx;
}
.welcome-text {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
}
/* 轮播图样式 */
.swiper-container {
flex-shrink: 0;
margin: 24rpx 32rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.swiper {
width: 100%;
height: 360rpx;
}
.swiper-item {
width: 100%;
height: 100%;
}
.banner-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%);
}
/* 九宫格样式 */
.grid-scroll {
flex: 1;
overflow: hidden;
}
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24rpx;
padding: 0 32rpx;
width: 100%;
align-content: center;
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
.grid-item:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.grid-icon {
font-size: 64rpx;
margin-bottom: 16rpx;
}
.grid-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
</style>

264
pages/login/login.vue Normal file
View File

@@ -0,0 +1,264 @@
<template>
<view class="login-container">
<view class="login-header">
<text class="title">欢迎使用预约系统</text>
<text class="subtitle">请登录或注册</text>
</view>
<view class="login-form">
<!-- 手机号输入 -->
<view class="form-item">
<t-input v-model:value="phone" placeholder="请输入手机号" type="number" :maxlength="11" clearable>
<template #prefixIcon>
<text class="prefix-icon">📱</text>
</template>
</t-input>
</view>
<!-- 验证码登录 -->
<view class="form-item" v-if="loginType === 'code'">
<t-input v-model:value="code" placeholder="请输入验证码" type="number" :maxlength="6" clearable>
<template #prefixIcon>
<text class="prefix-icon">🔐</text>
</template>
<template #suffix>
<t-button size="small" variant="text" :disabled="codeDisabled" @click="sendCode">
{{ codeButtonText }}
</t-button>
</template>
</t-input>
</view>
<!-- 登录按钮 -->
<view class="form-actions">
<t-button v-if="loginType === 'code'" t-class="btn-primary" theme="primary" size="large" block :loading="loading"
@click="codeLogin">
验证码登录
</t-button>
<t-button v-else t-class="btn-primary" theme="primary" size="large" block :loading="loading" @click="oneClickLogin">
一键登录
</t-button>
</view>
<view class="form-switch">
<text @click="toggleLoginType">
{{ loginType === 'code' ? '使用一键登录' : '使用验证码登录' }}
</text>
<text class="register-link" @click="goToRegister">注册新账号</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { api } from '@/utils/api'
const phone = ref('13777777777')
const code = ref('')
const loginType = ref<'code' | 'one-click'>('one-click')
const loading = ref(false)
const codeDisabled = ref(false)
const countdown = ref(0)
const codeButtonText = ref('发送验证码')
// 切换登录方式
const toggleLoginType = () => {
loginType.value = loginType.value === 'code' ? 'one-click' : 'code'
}
// 发送验证码
const sendCode = async () => {
console.log(phone.value);
if (!phone.value || phone.value.length !== 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
try {
await api.auth.sendCode('+86' + phone.value)
uni.showToast({
title: '验证码已发送',
icon: 'success'
})
// 开始倒计时
codeDisabled.value = true
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
codeButtonText.value = `${countdown.value}秒后重发`
if (countdown.value <= 0) {
clearInterval(timer)
codeDisabled.value = false
codeButtonText.value = '发送验证码'
}
}, 1000)
} catch (error) {
console.error('发送验证码失败', error)
}
}
// 验证码登录
const codeLogin = async () => {
if (!phone.value || phone.value.length !== 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
if (!code.value || code.value.length !== 6) {
uni.showToast({
title: '请输入验证码',
icon: 'none'
})
return
}
loading.value = true
try {
const res = await api.auth.verificationLogin('+86' + phone.value, code.value)
uni.setStorageSync('token', res.token)
uni.setStorageSync('user', res.user)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 直接跳转到首页
uni.reLaunch({ url: '/pages/index/index' })
} catch (error) {
console.error('登录失败', error)
} finally {
loading.value = false
}
}
// 一键登录
const oneClickLogin = async () => {
if (!phone.value || phone.value.length !== 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
loading.value = true
try {
const res = await api.auth.oneClickLogin('+86' + phone.value)
uni.setStorageSync('token', res.token)
uni.setStorageSync('user', res.user)
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 直接跳转到首页
uni.reLaunch({ url: '/pages/index/index' })
} catch (error) {
console.error('登录失败', error)
} finally {
loading.value = false
}
}
// 跳转到注册页
const goToRegister = () => {
uni.navigateTo({ url: '/pages/register/register' })
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%);
padding: 80rpx 40rpx;
}
.login-header {
text-align: center;
margin-bottom: 100rpx;
}
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 20rpx;
}
.subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.login-form {
background: #ffffff;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 32rpx;
}
.prefix-icon {
font-size: 32rpx;
color: #FF7A00;
}
.form-actions {
margin-top: 60rpx;
}
.form-switch {
display: flex;
justify-content: space-between;
margin-top: 32rpx;
font-size: 28rpx;
color: #666;
}
.form-switch text {
padding: 16rpx 0;
}
.register-link {
color: #FF7A00 !important;
font-weight: bold;
}
</style>
<style>
/* 按钮自定义样式 - 使用全局样式 */
.btn-primary {
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%) !important;
border: none !important;
border-radius: 8rpx !important;
color: #FFFFFF !important;
box-shadow: 0 4rpx 12rpx rgba(255, 122, 0, 0.3) !important;
outline: none !important;
}
.btn-primary::after {
border: none !important;
box-shadow: none !important;
}
.btn-primary:active {
background: linear-gradient(135deg, #FF6900 0%, #FF8500 100%) !important;
}
</style>

170
pages/register/register.vue Normal file
View File

@@ -0,0 +1,170 @@
<template>
<view class="register-container">
<view class="register-header">
<text class="title">注册账号</text>
<text class="subtitle">创建您的预约系统账号</text>
</view>
<view class="register-form">
<view class="form-item">
<t-input
v-model:value="phone"
placeholder="请输入手机号"
type="number"
:maxlength="11"
clearable
>
<template #prefixIcon>
<text class="prefix-icon">📱</text>
</template>
</t-input>
</view>
<view class="form-item">
<t-input
v-model:value="nickname"
placeholder="请输入昵称"
:maxlength="20"
clearable
>
<template #prefixIcon>
<text class="prefix-icon">👤</text>
</template>
</t-input>
</view>
<view class="form-actions">
<t-button
class="btn-primary"
theme="primary"
size="large"
block
:loading="loading"
@click="register"
>
注册
</t-button>
</view>
<view class="form-switch">
<text @click="goToLogin">已有账号去登录</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { api } from '@/utils/api'
const phone = ref('')
const nickname = ref('')
const loading = ref(false)
const register = async () => {
if (!phone.value || String(phone.value).length !== 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
if (!nickname.value || nickname.value.trim().length === 0) {
uni.showToast({
title: '请输入昵称',
icon: 'none'
})
return
}
loading.value = true
try {
const res = await api.auth.register('+86' + phone.value, nickname.value.trim())
uni.setStorageSync('token', res.token)
uni.setStorageSync('user', res.user)
uni.showToast({
title: '注册成功',
icon: 'success'
})
// 直接跳转到首页
uni.reLaunch({ url: '/pages/index/index' })
} catch (error) {
console.error('注册失败', error)
} finally {
loading.value = false
}
}
const goToLogin = () => {
uni.navigateBack()
}
</script>
<style scoped>
.register-container {
min-height: 100vh;
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%);
padding: 80rpx 40rpx;
}
.register-header {
text-align: center;
margin-bottom: 100rpx;
}
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 20rpx;
}
.subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.register-form {
background: #ffffff;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 32rpx;
}
.prefix-icon {
font-size: 32rpx;
color: #FF7A00;
}
.form-actions {
margin-top: 60rpx;
}
.form-switch {
display: flex;
justify-content: center;
margin-top: 32rpx;
font-size: 28rpx;
color: #FF7A00;
}
.form-switch text {
padding: 16rpx 0;
}
/* 按钮自定义样式 */
.btn-primary :deep(.t-button) {
background: linear-gradient(135deg, #FF7A00 0%, #FF9500 100%) !important;
border: none !important;
box-shadow: 0 4rpx 12rpx rgba(255, 122, 0, 0.3);
}
</style>

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"@/*": ["*"],
"@tdesign/uniapp/*": ["./uni_modules/tdesign-uniapp/*"]
},
"types": ["@dcloudio/types"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.vue"],
"exclude": ["node_modules", "unpackage"]
}

13
uni.promisify.adaptor.js Normal file
View File

@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

76
uni.scss Normal file
View File

@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

View File

@@ -0,0 +1,212 @@
## 0.7.02026-02-02
### Features
- `ActionSheet`: `item` 属性补充 `description` 字段 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Badge`:
- `shape` 属性新增 `ribbon-right/ribbon-left/triangle-right/triangle-left` 可选项,其中 `ribbon``ribbon-right` 等效 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 优化 `ribbon` 实现,改用 `background: linear-gradient()`,移除伪元素相关样式 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Calendar`: 新增 `allowSameDay` 属性,允许 `type='range'` 场景的起止时间相同 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Cascader`:
- 支持通过 `keys` 属性定义 `children / disabled``options` 中对应的字段别名 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 新增 `middle-content` 插槽,用于自定义中间区域内容 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `CollapsePanel`: 新增 `--td-collapse-disabled-color``--td-collapse-left-icon-color`,用于自定义禁用态颜色和左侧图标颜色 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `ImageViewer`: 新增 `image-props` 属性 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Navbar`: 新增 `placeholder` 属性,默认值为 `false`;新增 `zIndex` 属性,默认值为 `1` @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Picker`:
- 优化性能减少掉帧 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 优化大量数据时列表滚动性能 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `itemHeight` 默认单位改用 `px`,避免单位转换带来的精度问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 新增 `visibleItemCount` 属性,可自定义可视区域 `PickerItem` 的子项个数 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Popover`:
- 新增 `fixed` API适用于触发元素为 `fixed` 场景。当触发元素为 `fixed` 时,除了需要显示指定 `fixed` 属性为 `true`,还需在触发元素层添加 `t-popover-wrapper--fixed` 类,用于定位触发元素 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 新增 `--td-popover-[theme]-color``--td-popover-[theme]-bg-color` 系列 `CSS Vars` @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `QRCode`: 组件新增 `init()`,用于外部调用,重新绘制二维码 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Search`:
- 确保点击清空按钮后,组件内容清空但保持聚焦 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 新增 `cursor-color` 属性 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `SidebarItem`:
- 新增默认插槽,可自定义侧边栏子项内容 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 支持由标签内容撑开高度 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 完善激活项的前缀和后缀元素显示逻辑 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `TabBar`: 新增 `placeholder` 属性,默认值为 `false`;新增 `zIndex` 属性,默认值为 `1` @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
### Bug Fixes
- `ActionSheet`:
- 修复左对齐场景下,子项 `border` 左间距错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `grid` 主题 + 无 `description` 描述文本场景下,顶部间距错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `list` 主题最后一项不应设置底边框 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `item` 属性的 `disabled` 配置无效 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `BackTop`: 修复文本字重错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Badge`: 修复 `count` 插槽异常 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Calendar`:
- 修复 `value[]` 结合 `swich-mode` 时,初始化错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复翻页按钮状态错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Checkbox`: 修复 `icon` 属性使用 `svg` 资源时在 `iOS` 上不显示 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `CollapsePanel`:
- 修复深色模式下面板右侧图标颜色错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复左侧图标颜色错误,默认主题色,支持使用 `css vars` 自定义 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `ColorPicker`: 修复组件深色模式背景、边框、文本色错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `DateTimePicker`: 修复插槽名重复导致的控制台告警 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `DropdownItem`: 修复在 `iOS 26` 中弹窗定位不准 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Fab`: 修复 `yBounds` 未传值时,控制台报错问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Grid`: 修复 `column` 小于 4 或大于 4 时,文本字号大小错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Message`: 修复 `error` 主题图标错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Picker`:
- 修复 `autoClose``false` 时,点击遮罩层会重置选项为拨动前选项值的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复平铺模式 `value` 变化未能准确监听 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `keys` 动态变更时,子项列表数据不显示 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `popupProps.showOverlay` 无效 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Popup`: 修复 `duration` 参数无效的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Progress`:
- 修复深色模式下环形进度条内部背景色错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复环形进度条内部文本间距错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复深色模式下环形进度条内部背景色错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复环形进度条内部文本间距错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Slider`: 修复受控 + 双游标滑块模式下陷入死循环的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `SwipeCell`: 消除 `IntersectionObserver is using slowest path` 警告 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `TabBar`: 修复子项背景色叠加的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Tabs`: 消除 `IntersectionObserver is using slowest path` 警告 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Toast`:
- 修复 `Toast` 嵌套调用时 `close` 回调陷入循环的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复弹窗与遮罩消失不同步的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复圆角样式错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `showOverlay``preventScrollThrough` 均为 `true` 时,遮罩背景色错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `Upload`:
- 修复企业微信/桌面端环境中部分机型无法唤起上传 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复当 `request-method` 返回 `Promise` 时,无法上传的问题 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复 `draggable` 值变换时组件显示错误 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- 修复拖拽结束后拖拽元素 `zIndex` 异常 @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
### Others
- `改用 Font token`,调整部分组件的 CSS Vars @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
- `--td-xx-icon-font-size` 统一更名为 `--td-xx-icon-size` @novlan1 ([#4201](https://github.com/Tencent/tdesign-miniprogram/pull/4201))
## 0.6.32026-01-05
### Features
- `Form`: 支持 `change` 类型 `trigger` @novlan1 ([#143](https://github.com/novlan1/tdesign-uniapp/pull/143))
- `Image`: 点击事件不禁用冒泡 @novlan1 ([#142](https://github.com/novlan1/tdesign-uniapp/pull/142))
## 0.6.22025-12-30
### Bug Fixes
- `DateTimePicker`: 修复滚动日期时其他列向上跳动问题 @novlan1 ([#138](https://github.com/novlan1/tdesign-uniapp/pull/138))
## 0.6.12025-12-29
### Features
- `Navbar`: 增加 `right` 插槽 @novlan1 ([#136](https://github.com/novlan1/tdesign-uniapp/pull/136))
### Bug Fixes
- `Upload`: 修复小程序下图片点击事件不触发问题 @novlan1 ([#136](https://github.com/novlan1/tdesign-uniapp/pull/136))
## 0.6.02025-12-17
### Features
- `Popover`: 新增组件 @novlan1 ([#126](https://github.com/novlan1/tdesign-uniapp/pull/126))
## 0.5.92025-12-11
### Bug Fixes
- `Input`: 修复数据回显问题 @novlan1 ([#121](https://github.com/novlan1/tdesign-uniapp/pull/121))
## 0.5.82025-12-01
#### Features
- `Form`: 新增 `contentAlign` 属性 @novlan1 ([#115](https://github.com/novlan1/tdesign-uniapp/pull/115))
#### Bug Fixes
- `SwipeCell`: 修复多个组件共存时的复位问题 @novlan1 ([#114](https://github.com/novlan1/tdesign-uniapp/pull/114))
- `Form`: 修复 `help/label` 插槽不存在的问题 @novlan1 ([#115](https://github.com/novlan1/tdesign-uniapp/pull/115))
## 0.5.72025-11-27
#### Features
- `Textarea`: 支持 `v-model:value` @novlan1 ([#102](https://github.com/novlan1/tdesign-uniapp/pull/102))
- `Toast`: 函数式调用组件时,支持组件 Dom 预埋在页面下 @novlan1 ([#103](https://github.com/novlan1/tdesign-uniapp/pull/103))
#### Bug Fixes
- `Input`: 修复 `clear` 事件的意外冒泡问题 @novlan1 ([#100](https://github.com/novlan1/tdesign-uniapp/pull/100))
- `Calendar`: 修复 `switchMode``year-month` 时的编译问题 @novlan1 ([#106](https://github.com/novlan1/tdesign-uniapp/pull/106))
## 0.5.62025-11-25
#### Features
- `Popup`: 支持 `v-model:visible` @novlan1 ([#90](https://github.com/novlan1/tdesign-uniapp/pull/90))
- `Form`: 支持 `validate` 方法传参 @novlan1 ([#90](https://github.com/novlan1/tdesign-uniapp/pull/90))
- `Input`: 支持 `v-model:value` @novlan1 ([#89](https://github.com/novlan1/tdesign-uniapp/pull/89))
#### Bug Fixes
- `Form`: 修复深色模式背景错误问题 @novlan1 ([#91](https://github.com/novlan1/tdesign-uniapp/pull/91))
## 0.5.52025-11-18
#### Features
- `Picker`: 支持 `v-model:visible` 语法糖 @novlan1 ([#77](https://github.com/novlan1/tdesign-uniapp/pull/77))
- `PullDownRefresh`: 修改 `dragstart/dragging/dragend` 事件参数为 `TouchEvent` @novlan1 ([#63](https://github.com/novlan1/tdesign-uniapp/pull/63))
#### Bug Fixes
- `Input`: 修复 `readonly` 时点击无效问题 @novlan1 ([#76](https://github.com/novlan1/tdesign-uniapp/pull/76))
- `PullDownRefresh`: 修复加载中再次点击时触发的 `value` 变化问题 @novlan1 ([#63](https://github.com/novlan1/tdesign-uniapp/pull/63))
## 0.5.42025-11-14
#### Bug Fixes
- `DropdownMenu`: 修复使用自带导航栏时的高度错误 @novlan1 ([#56](https://github.com/novlan1/tdesign-uniapp/pull/56))
## 0.5.3.beta-42025-11-12
#### 🐞 Bug Fixes
- `Form`: 修复 `FormItem``padding` 错误问题 @novlan1
#### 🚧 Others
- site: 使用 `history` 模式 @novlan1 ([#35](https://github.com/novlan1/tdesign-uniapp/pull/35))
- ci: 使用多种 CI @novlan1 ([#42](https://github.com/novlan1/tdesign-uniapp/pull/42))
## 0.5.3.beta-32025-11-12
#### Bug Fixes
- `Form`: 修复 `FormItem``padding` 错误问题 @novlan1
#### Others
- site: 使用 `history` 模式 @novlan1 ([#35](https://github.com/novlan1/tdesign-uniapp/pull/35))
- ci: 使用多种 CI @novlan1 ([#42](https://github.com/novlan1/tdesign-uniapp/pull/42))
## 0.5.3.beta-22025-11-12
### Bug Fixes
- `Form`: 修复 `FormItem``padding` 错误问题 @novlan1
### Others
- site: 使用 `history` 模式 @novlan1 ([#35](https://github.com/novlan1/tdesign-uniapp/pull/35))
- ci: 使用多种 CI @novlan1 ([#42](https://github.com/novlan1/tdesign-uniapp/pull/42))
## 0.5.3.beta-12025-11-12
### Bug Fixes
- `Form`: 修复 `FormItem``padding` 错误问题 @novlan1
### Others
- site: 使用 `history` 模式 @novlan1 ([#35](https://github.com/novlan1/tdesign-uniapp/pull/35))
- ci: 使用多种 CI @novlan1 ([#42](https://github.com/novlan1/tdesign-uniapp/pull/42))
## 0.5.32025-11-12
🐞 Bug Fixes
- `Form`: 修复 `FormItem``padding` 错误问题 @novlan1
🚧 Others
- site: 使用 `history` 模式 @novlan1 ([#35](https://github.com/novlan1/tdesign-uniapp/pull/35))
- ci: 使用多种 CI @novlan1 ([#42](https://github.com/novlan1/tdesign-uniapp/pull/42))
## 0.5.22025-11-10
- Popup: 修复滚动穿透问题
- Tabs修复scroll-view事件参数问题
## 0.5.12025-11-07
- PullDownRefresh: 支持 successDuration 属性
## 0.5.02025-11-07
- 支持类型提示
## 0.4.22025-11-01
- 优化多个组件在支付宝小程序的表现
## 0.4.02025-10-31
- 支持支付宝小程序
## 0.3.22025-10-30
- `Calendar`: 修复引入问题
## 0.3.12025-10-27
- Calendar: 支持 title 和 confirm-btn 的 slot
## 0.3.02025-10-25
- `Guide`: 新增组件
- `Watermark`: 新增组件
## 0.2.52025-10-22
- 统一多个组件的事件参数
## 0.2.32025-10-22
### 🚀 Features
- `ColorPicker`: 支持 APP-PLUS
- `Collapse`: 支持 APP-PLUS
- `QRCode`: 支持 APP-PLUS
## 0.2.22025-10-17
- Swiper: 新增组件

View File

@@ -0,0 +1,60 @@
:: BASE_DOC ::
## API
### ActionSheet Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
align | String | center | options: center/left | N
cancel-text | String | - | \- | N
count | Number | 8 | \- | N
description | String | - | \- | N
items | Array | [] | Typescript: `Array<string \| ActionSheetItem>` `interface ActionSheetItem { label: string; description?: string; color?: string; disabled?: boolean; icon?: string; suffixIcon?: string }`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts) | N
popup-props | Object | {} | Typescript: `PopupProps`[Popup API Documents](./popup?tab=api)。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts) | N
show-cancel | Boolean | true | \- | N
show-overlay | Boolean | true | \- | N
theme | String | list | options: list/grid | N
using-custom-navbar | Boolean | false | \- | N
visible | Boolean | false | `v-model:visible` is supported | N
default-visible | Boolean | false | uncontrolled property | N
### ActionSheet Events
name | params | description
-- | -- | --
cancel | \- | \-
close | `(context: { trigger: ActionSheetTriggerSource })` | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts)。<br/>`type ActionSheetTriggerSource = 'overlay' \| 'command' \| 'select' `<br/>
selected | `(context: { selected: ActionSheetItem \| string, index: number })` | \-
### ActionSheet Slots
name | Description
-- | --
\- | \-
### ActionSheet External Classes
className | Description
-- | --
t-class | \-
t-class-cancel | \-
t-class-content | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-action-sheet-border-color | @component-stroke | -
--td-action-sheet-border-radius | @radius-extraLarge | -
--td-action-sheet-cancel-color | @text-color-primary | -
--td-action-sheet-color | @text-color-primary | -
--td-action-sheet-description-color | @text-color-placeholder | -
--td-action-sheet-description-font | @font-body-medium | -
--td-action-sheet-disabled-color | @text-color-disabled | -
--td-action-sheet-dot-active-color | @brand-color | -
--td-action-sheet-dot-color | @text-color-disabled | -
--td-action-sheet-dot-size | 16rpx | -
--td-action-sheet-gap-color | @bg-color-page | -

View File

@@ -0,0 +1,131 @@
---
title: ActionSheet 动作面板
description: 由用户操作后触发的一种特定的模态弹出框 ,呈现一组与当前情境相关的两个或多个选项。
spline: data
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TActionSheet from '@tdesign/uniapp/action-sheet/action-sheet.vue';
```
### 组件类型
列表型动作面板
{{ list }}
宫格型动作面板
{{ grid }}
### 组件状态
宫格型动作面板
{{ status }}
### 组件样式
列表型对齐方式
{{ align }}
### 支持指令调用
```javascript
import ActionSheet, { ActionSheetTheme } from '@tdesign/uniapp/action-sheet/index';
// 指令调用不同于组件引用不需要传入visible
const basicListOption: ActionSheetShowOption = {
theme: ActionSheetTheme.List,
selector: '#t-action-sheet',
items: [
{
label: '默认选项',
},
{
label: '失效选项',
disabled: true,
},
{
label: '警告选项',
color: '#e34d59',
},
],
};
const handler = ActionSheet.show(basicListOption);
```
指令调用的关闭如下
```javascript
handler.close();
```
## API
### ActionSheet Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
align | String | center | 水平对齐方式。可选项center/left | N
cancel-text | String | - | 设置取消按钮的文本 | N
count | Number | 8 | 设置每页展示菜单的数量,仅当 type=grid 时有效 | N
description | String | - | 动作面板描述文字 | N
items | Array | [] | 菜单项。TS 类型:`Array<string \| ActionSheetItem>` `interface ActionSheetItem { label: string; description?: string; color?: string; disabled?: boolean; icon?: string; suffixIcon?: string }`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts) | N
popup-props | Object | {} | 透传 Popup 组件全部属性。TS 类型:`PopupProps`[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts) | N
show-cancel | Boolean | true | 是否显示取消按钮 | N
show-overlay | Boolean | true | 是否显示遮罩层 | N
theme | String | list | 展示类型列表和表格形式展示。可选项list/grid | N
using-custom-navbar | Boolean | false | 是否使用了自定义导航栏 | N
visible | Boolean | false | 显示与隐藏。支持语法糖 `v-model:visible` | N
default-visible | Boolean | false | 显示与隐藏。非受控属性 | N
### ActionSheet Events
名称 | 参数 | 描述
-- | -- | --
cancel | \- | 点击取消按钮时触发
close | `(context: { trigger: ActionSheetTriggerSource })` | 关闭时触发。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/action-sheet/type.ts)。<br/>`type ActionSheetTriggerSource = 'overlay' \| 'command' \| 'select' `<br/>
selected | `(context: { selected: ActionSheetItem \| string, index: number })` | 选择菜单项时触发
### ActionSheet Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
### ActionSheet External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-cancel | 取消样式类
t-class-content | 内容样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-action-sheet-border-color | @component-stroke | -
--td-action-sheet-border-radius | @radius-extraLarge | -
--td-action-sheet-cancel-color | @text-color-primary | -
--td-action-sheet-color | @text-color-primary | -
--td-action-sheet-description-color | @text-color-placeholder | -
--td-action-sheet-description-font | @font-body-medium | -
--td-action-sheet-disabled-color | @text-color-disabled | -
--td-action-sheet-dot-active-color | @brand-color | -
--td-action-sheet-dot-color | @text-color-disabled | -
--td-action-sheet-dot-size | 16rpx | -
--td-action-sheet-gap-color | @bg-color-page | -

View File

@@ -0,0 +1,162 @@
.t-action-sheet__content {
color: var(--td-action-sheet-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
border-top-left-radius: var(--td-action-sheet-border-radius, var(--td-radius-extraLarge, 24rpx));
border-top-right-radius: var(--td-action-sheet-border-radius, var(--td-radius-extraLarge, 24rpx));
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
overflow: hidden;
}
.t-action-sheet__content:focus {
outline: 0;
}
.t-action-sheet--grid {
padding-top: var(--td-spacer, 16rpx);
}
.t-action-sheet--grid .t-action-sheet__description::after {
display: none;
}
.t-action-sheet--left .t-action-sheet__description {
text-align: left;
}
.t-action-sheet--left .t-action-sheet__list-item-content,
.t-action-sheet--left .t-action-sheet__list-item-desc {
justify-content: start;
}
.t-action-sheet--left .t-action-sheet__list-item-icon--suffix {
margin-left: auto;
}
.t-action-sheet__grid {
padding-bottom: 16rpx;
}
.t-action-sheet__grid--swiper {
padding-bottom: 48rpx;
}
.t-action-sheet__description {
color: var(--td-action-sheet-description-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
font: var(--td-action-sheet-description-font, var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
text-align: center;
padding: var(--td-spacer-1, 24rpx) var(--td-spacer-2, 32rpx);
position: relative;
}
.t-action-sheet__description:focus {
outline: 0;
}
.t-action-sheet__description::after {
content: '';
display: block;
position: absolute;
top: unset;
bottom: 0;
left: unset;
right: unset;
background-color: var(--td-action-sheet-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
}
.t-action-sheet__description::after {
height: 1px;
left: 0;
right: 0;
transform: scaleY(0.5);
}
.t-action-sheet__list-item {
flex-direction: column;
padding: var(--td-spacer-2, 32rpx);
display: flex;
align-items: center;
justify-content: center;
}
.t-action-sheet__list-item:not(:last-child) {
position: relative;
}
.t-action-sheet__list-item:not(:last-child)::after {
content: '';
display: block;
position: absolute;
top: unset;
bottom: 0;
left: unset;
right: unset;
background-color: var(--td-action-sheet-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
}
.t-action-sheet__list-item:not(:last-child)::after {
height: 1px;
left: 0;
right: 0;
transform: scaleY(0.5);
}
.t-action-sheet__list-item:focus {
outline: 0;
}
.t-action-sheet__list-item--disabled {
color: var(--td-action-sheet-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-action-sheet__list-item-content,
.t-action-sheet__list-item-desc {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.t-action-sheet__list-item-text {
font: var(--td-font-body-large, 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
}
.t-action-sheet__list-item-desc {
font: var(--td-font-body-small, 24rpx / 40rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
color: var(--td-action-sheet-description-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
margin-top: var(--td-spacer, 16rpx);
}
.t-action-sheet__list-item-text,
.t-action-sheet__list-item-desc {
word-wrap: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.t-action-sheet__list-item-icon {
font-size: 48rpx;
margin-right: var(--td-spacer, 16rpx);
}
.t-action-sheet__list-item-icon--suffix {
margin-right: 0;
margin-left: var(--td-spacer, 16rpx);
}
.t-action-sheet__swiper-wrap {
margin-top: 8rpx;
position: relative;
}
.t-action-sheet__footer {
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
}
.t-action-sheet__gap-list {
height: 16rpx;
background-color: var(--td-action-sheet-gap-color, var(--td-bg-color-page, var(--td-gray-color-1, #f3f3f3)));
}
.t-action-sheet__gap-grid {
height: 1rpx;
background-color: var(--td-action-sheet-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
}
.t-action-sheet__cancel {
padding: var(--td-spacer-1, 24rpx) var(--td-spacer-2, 32rpx);
font: var(--td-font-body-large, 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
color: var(--td-action-sheet-cancel-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
display: flex;
align-items: center;
justify-content: center;
}
.t-action-sheet__dots {
position: absolute;
left: 50%;
bottom: var(--td-spacer-2, 32rpx);
transform: translateX(-50%);
display: flex;
flex-direction: row;
}
.t-action-sheet__dots-item {
width: var(--td-action-sheet-dot-size, 16rpx);
height: var(--td-action-sheet-dot-size, 16rpx);
background-color: var(--td-action-sheet-dot-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
border-radius: 50%;
margin: 0 var(--td-spacer, 16rpx);
transition: all 0.4s ease-in;
}
.t-action-sheet__dots-item.t-is-active {
background-color: var(--td-action-sheet-dot-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}

View File

@@ -0,0 +1,350 @@
<template>
<view
:id="classPrefix"
:style="tools._style([rootCustomStyle])"
:class="tools.cls(classPrefix, [dataAlign, dataTheme, ['no-description', !description]]) + ' ' + tClass"
>
<t-popup
:visible="dataVisible"
placement="bottom"
:using-custom-navbar="dataUsingCustomNavbar"
:custom-navbar-height="customNavbarHeight"
:show-overlay="dataShowOverlay"
:z-index="(dataPopupProps && dataPopupProps.zIndex) || defaultPopUpzIndex"
:overlay-props="(dataPopupProps && dataPopupProps.overlayProps) || defaultPopUpProps"
@visible-change="onPopupVisibleChange"
>
<view
:class="classPrefix + '__content ' + tClassContent"
tabindex="0"
>
<view
v-if="dataDescription"
tabindex="0"
:class="classPrefix + '__description'"
>
{{ dataDescription }}
</view>
<block v-if="gridThemeItems.length">
<block v-if="gridThemeItems.length === 1">
<t-grid
align="center"
:t-class="classPrefix + '__grid'"
:column="dataCount / 2"
:class="classPrefix + '__single-wrap'"
>
<t-grid-item
v-for="(item, index) in gridThemeItems[0]"
:key="index"
:t-class="classPrefix + '__grid-item ' + classPrefix + '__square'"
:data-index="index"
:icon="{ name: item.icon, color: item.color }"
:text="item.label || ''"
:description="item.description || ''"
:image="item.image || ''"
:style="'--td-grid-item-text-color: ' + item.color"
@click="onSelect($event, { index })"
/>
</t-grid>
</block>
<block v-else-if="gridThemeItems.length > 1">
<view :class="classPrefix + '__swiper-wrap'">
<swiper
:style="heightStyle"
:autoplay="false"
:current="currentSwiperIndex"
@change="onSwiperChange"
>
<swiper-item
v-for="(item, index) in gridThemeItems"
:key="index"
>
<t-grid
align="center"
:t-class="classPrefix + '__grid ' + classPrefix + '__grid--swiper'"
:column="dataCount / 2"
:custom-style="gridStyle"
>
<t-grid-item
v-for="(item, index1) in item"
:key="index1"
:t-class="classPrefix + '__grid-item'"
:class="classPrefix + '__square'"
:data-index="index"
:icon="{ name: item.icon, color: item.color }"
:text="item.label || ''"
:description="item.description || ''"
:image="item.image || ''"
:style="'--td-grid-item-text-color: ' + item.color"
@click="onSelect($event, { index })"
/>
</t-grid>
</swiper-item>
</swiper>
<view :class="classPrefix + '__nav'">
<view :class="classPrefix + '__dots'">
<view
v-for="(item, index) in gridThemeItems.length"
:key="index"
:class="classPrefix + '__dots-item ' + (index === currentSwiperIndex ? prefix + '-is-active' : '')"
/>
</view>
</view>
</view>
</block>
</block>
<view
v-else-if="dataItems && dataItems.length"
:class="classPrefix + '__list'"
>
<block
v-for="(item, index) in dataItems"
:key="index"
>
<view
:data-index="index"
:style="item.color ? 'color: ' + item.color : ''"
:class="tools.cls(classPrefix + '__list-item', [['disabled', item.disabled]])"
:aria-role="ariaRole || 'button'"
:aria-label="item.label || item"
tabindex="0"
@click="() => onSelect(item, { index })"
>
<view :class="classPrefix + '__list-item-content'">
<t-icon
v-if="item.icon"
:name="item.icon"
:t-class="classPrefix + '__list-item-icon'"
:custom-style="iconCustomStyle"
/>
<view :class="classPrefix + '__list-item-text'">
{{ item.label || item }}
</view>
<t-icon
v-if="item.suffixIcon"
:name="item.suffixIcon"
:t-class="classPrefix + '__list-item-icon ' + classPrefix + '__list-item-icon--suffix'"
style="margin-left: auto;"
:custom-style="suffixIconCustomStyle"
/>
</view>
<view
v-if="item.description"
:class="classPrefix + '__list-item-desc'"
>
{{ item.description }}
</view>
</view>
</block>
</view>
</view>
<slot />
<view
v-if="dataShowCancel"
:class="classPrefix + '__footer'"
>
<view :class="classPrefix + '__gap-' + dataTheme" />
<view
:class="classPrefix + '__cancel ' + tClassCancel"
:hover-class="classPrefix + '__cancel--hover'"
hover-stay-time="70"
aria-role="button"
@click="onCancel"
>
{{ dataCancelText || '取消' }}
</view>
</view>
</t-popup>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import TPopup from '../popup/popup';
import TGrid from '../grid/grid';
import TGridItem from '../grid-item/grid-item';
import { chunk } from '../common/utils';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import { actionSheetTheme } from './show';
import props from './props';
import useCustomNavbar from '../mixins/using-custom-navbar';
import tools from '../common/utils.wxs';
import { getFunctionalMixin } from '../common/functional/mixin';
const name = `${prefix}-action-sheet`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [{
key: 'visible',
event: 'visible-change',
}],
externalClasses: [
`${prefix}-class`,
`${prefix}-class-content`,
`${prefix}-class-cancel`,
],
mixins: [getFunctionalMixin(props), useCustomNavbar],
components: {
TIcon,
TPopup,
TGrid,
TGridItem,
},
props: {
...props,
},
emits: [
'visible-change',
'update:visible',
],
data() {
return {
prefix,
classPrefix: name,
gridThemeItems: [],
currentSwiperIndex: 0,
defaultPopUpProps: {},
defaultPopUpzIndex: 11500,
tools,
heightStyle: 'height: 456rpx;',
};
},
computed: {
rootCustomStyle() {
return tools._style([
this.customStyle,
this.dataTheme === 'grid' ? 'padding-bottom: 16rpx' : '',
]);
},
iconCustomStyle() {
return 'margin-right: 8px;';
},
suffixIconCustomStyle() {
return 'margin-right: 8px;margin-left: auto;';
},
gridStyle() {
return `${this.heightStyle}padding-bottom: 48rpx;`;
},
},
watch: {
dataVisible: {
handler(e) {
if (e) {
this.init();
}
},
immediate: true,
},
dataItems: {
handler() {
if (this.dataVisible) {
this.init();
}
},
immediate: true,
},
},
methods: {
init() {
this.memoInitialData();
this.splitGridThemeActions();
},
memoInitialData() {
this.initialData = {
};
},
splitGridThemeActions() {
if (this.dataTheme !== actionSheetTheme.Grid) return;
this.gridThemeItems = chunk(this.dataItems, this.dataCount);
},
/** 指令调用显示 */
show(options) {
const defaultOptions = [
'align',
'cancelText',
'count',
'description',
'items',
'popupProps',
'showCancel',
'showOverlay',
'theme',
'usingCustomNavbar',
].reduce((acc, key) => ({
...acc,
[key]: props[key].default,
}));
this.setData({
...defaultOptions,
...options,
visible: true,
});
this.splitGridThemeActions();
this.autoClose = true;
this._trigger('visible-change', { visible: true });
},
/** 指令调用隐藏 */
close() {
this.$emit('close', { trigger: 'command' });
this._trigger('visible-change', { visible: false });
},
/** 默认点击遮罩关闭 */
onPopupVisibleChange(detail) {
if (!detail.visible) {
this.$emit('close', { trigger: 'overlay' });
this._trigger('visible-change', { visible: false });
}
if (this.autoClose) {
this.dataVisible = false;
this.autoClose = false;
}
},
onSwiperChange(e) {
const { current } = e.detail;
this.currentSwiperIndex = current;
},
onSelect(event, { index }) {
const { currentSwiperIndex, dataItems, gridThemeItems, dataCount, dataTheme } = this;
const isSwiperMode = dataTheme === actionSheetTheme.Grid;
const item = isSwiperMode ? gridThemeItems[currentSwiperIndex][index] : dataItems[index];
const realIndex = isSwiperMode ? index + currentSwiperIndex * dataCount : index;
if (item) {
this.$emit('selected', { selected: item, index: realIndex });
if (!item.disabled) {
this.$emit('close', { trigger: 'select' });
this._trigger('visible-change', { visible: false });
}
}
},
onCancel() {
this.$emit('cancel');
if (this.autoClose) {
this.dataVisible = false;
this.autoClose = false;
}
},
},
});
</script>
<style scoped>
@import './action-sheet.css';
</style>

View File

@@ -0,0 +1,15 @@
export const getListThemeItemClass = function (props) {
const { classPrefix } = props;
const { item } = props;
const { prefix } = props;
const classList = [`${classPrefix}__list-item`];
if (item.disabled) {
classList.push(`${prefix}-is-disabled`);
}
return classList.join(' ');
};
export const isImage = function (name) {
return name.indexOf('/') !== -1;
};

View File

@@ -0,0 +1,11 @@
import type { ActionSheetItem, ActionSheetTheme, ActionSheetShowOption } from './show.d.ts';
export { ActionSheetItem, ActionSheetTheme, ActionSheetShowOption };
declare type Instance = any;
declare const Handler: {
show(options: ActionSheetShowOption): Instance;
close(options: ActionSheetShowOption): void;
};
export default Handler;

View File

@@ -0,0 +1,13 @@
import { show, close, ActionSheetTheme, actionSheetTheme } from './show';
export { ActionSheetTheme, actionSheetTheme };
export default {
show(options) {
return show(options);
},
close(options) {
return close(options);
},
};

View File

@@ -0,0 +1,86 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdActionSheetProps } from './type';
export default {
/** 水平对齐方式 */
align: {
type: String,
default: 'center' as TdActionSheetProps['align'],
validator(val: TdActionSheetProps['align']): boolean {
if (!val) return true;
return ['center', 'left'].includes(val);
},
},
/** 设置取消按钮的文本 */
cancelText: {
type: String,
default: '',
},
/** 设置每页展示菜单的数量,仅当 type=grid 时有效 */
count: {
type: Number,
default: 8,
},
/** 动作面板描述文字 */
description: {
type: String,
default: '',
},
/** 菜单项 */
items: {
type: Array,
default: (): TdActionSheetProps['items'] => [],
},
/** 透传 Popup 组件全部属性 */
popupProps: {
type: Object,
default: () => ({}),
},
/** 是否显示取消按钮 */
showCancel: {
type: Boolean,
default: true,
},
/** 是否显示遮罩层 */
showOverlay: {
type: Boolean,
default: true,
},
/** 展示类型,列表和表格形式展示 */
theme: {
type: String,
default: 'list' as TdActionSheetProps['theme'],
validator(val: TdActionSheetProps['theme']): boolean {
if (!val) return true;
return ['list', 'grid'].includes(val);
},
},
/** 是否使用了自定义导航栏 */
usingCustomNavbar: Boolean,
/** 显示与隐藏 */
visible: {
type: Boolean,
default: undefined,
},
/** 显示与隐藏,非受控属性 */
defaultVisible: Boolean,
/** 点击取消按钮时触发 */
onCancel: {
type: Function,
default: () => ({}),
},
/** 关闭时触发 */
onClose: {
type: Function,
default: () => ({}),
},
/** 选择菜单项时触发 */
onSelected: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,28 @@
import { ActionSheetItem } from './type';
export { ActionSheetItem };
declare type Instance = any;
declare type Context = any;
export declare enum ActionSheetTheme {
List = 'list',
Grid = 'grid'
}
interface ActionSheetProps {
align: 'center' | 'left';
cancelText?: string;
count?: number;
description: string;
items: Array<string | ActionSheetItem>;
showCancel?: boolean;
theme?: ActionSheetTheme;
visible: boolean;
defaultVisible?: boolean;
}
export interface ActionSheetShowOption extends Omit<ActionSheetProps, 'visible'> {
context?: Context;
selector?: string;
}
export declare const show: (options: ActionSheetShowOption) => Instance;
export declare const close: (options: ActionSheetShowOption) => void;

View File

@@ -0,0 +1,33 @@
import { getInstance } from '../common/utils';
export const ActionSheetTheme = {
List: 'list',
Grid: 'grid',
};
export const actionSheetTheme = {
List: ActionSheetTheme.List,
Grid: ActionSheetTheme.Grid,
} ;
export const show = function (options) {
const { context, selector = '#t-action-sheet', ...otherOptions } = { ...options };
const instance = getInstance(context, selector);
if (instance) {
instance.show({
...otherOptions,
});
return instance;
}
console.error('未找到组件,请确认 selector && context 是否正确');
};
export const close = function (options) {
const { context, selector = '#t-action-sheet' } = { ...options };
const instance = getInstance(context, selector);
if (instance) {
instance.close();
}
};

View File

@@ -0,0 +1,93 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdPopupProps as PopupProps } from '../popup/type';
export interface TdActionSheetProps {
/**
* 水平对齐方式
* @default center
*/
align?: 'center' | 'left';
/**
* 设置取消按钮的文本
* @default ''
*/
cancelText?: string;
/**
* 设置每页展示菜单的数量,仅当 type=grid 时有效
* @default 8
*/
count?: number;
/**
* 动作面板描述文字
* @default ''
*/
description?: string;
/**
* 菜单项
* @default []
*/
items?: Array<string | ActionSheetItem>;
/**
* 透传 Popup 组件全部属性
* @default {}
*/
popupProps?: PopupProps;
/**
* 是否显示取消按钮
* @default true
*/
showCancel?: boolean;
/**
* 是否显示遮罩层
* @default true
*/
showOverlay?: boolean;
/**
* 展示类型,列表和表格形式展示
* @default list
*/
theme?: 'list' | 'grid';
/**
* 是否使用了自定义导航栏
* @default false
*/
usingCustomNavbar?: boolean;
/**
* 显示与隐藏
* @default false
*/
visible?: boolean;
/**
* 显示与隐藏,非受控属性
* @default false
*/
defaultVisible?: boolean;
/**
* 点击取消按钮时触发
*/
onCancel?: () => void;
/**
* 关闭时触发
*/
onClose?: (context: { trigger: ActionSheetTriggerSource }) => void;
/**
* 选择菜单项时触发
*/
onSelected?: (context: { selected: ActionSheetItem | string; index: number }) => void;
}
export interface ActionSheetItem {
label: string;
description?: string;
color?: string;
disabled?: boolean;
icon?: string;
suffixIcon?: string;
}
export type ActionSheetTriggerSource = 'overlay' | 'command' | 'select';

View File

@@ -0,0 +1,187 @@
.t-avatar-group {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
}
.t-avatar-group-offset-left .t-avatar__wrapper,
.t-avatar-group-offset-right .t-avatar__wrapper {
padding: var(--td-avatar-group-line-spacing, 4rpx) 0;
}
.t-avatar-group-offset-left-small,
.t-avatar-group-offset-right-small {
--td-avatar-margin-left: var(--td-avatar-group-margin-left-small, -16rpx);
}
.t-avatar-group-offset-left-medium,
.t-avatar-group-offset-right-medium {
--td-avatar-margin-left: var(--td-avatar-group-margin-left-medium, -16rpx);
}
.t-avatar-group-offset-left-large,
.t-avatar-group-offset-right-large {
--td-avatar-margin-left: var(--td-avatar-group-margin-left-large, -16rpx);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(1) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 1);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(2) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 2);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(3) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 3);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(4) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 4);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(5) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 5);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(6) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 6);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(7) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 7);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(8) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 8);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(9) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 9);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(10) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 10);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(11) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 11);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(12) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 12);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(13) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 13);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(14) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 14);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(15) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 15);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(16) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 16);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(17) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 17);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(18) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 18);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(19) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 19);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(20) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 20);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(21) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 21);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(22) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 22);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(23) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 23);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(24) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 24);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(25) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 25);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(26) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 26);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(27) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 27);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(28) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 28);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(29) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 29);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(30) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 30);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(31) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 31);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(32) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 32);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(33) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 33);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(34) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 34);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(35) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 35);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(36) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 36);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(37) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 37);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(38) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 38);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(39) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 39);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(40) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 40);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(41) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 41);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(42) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 42);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(43) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 43);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(44) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 44);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(45) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 45);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(46) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 46);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(47) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 47);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(48) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 48);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(49) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 49);
}
.t-avatar-group-offset-left .t-avatar__wrapper:nth-child(50) {
z-index: calc(var(--td-avatar-group-init-z-index, 50) - 50);
}
.t-avatar-group__collapse--slot,
.t-avatar-group__collapse--default {
z-index: 0;
font-weight: 600;
}
.t-avatar-group__collapse--slot {
float: left;
}
.t-avatar-group__collapse--slot:not(:empty) + .t-avatar-group__collapse--default {
display: none;
float: left;
}
.t-avatar-group__collapse--slot:empty + .t-avatar-group__collapse--default {
display: block;
float: left;
}

View File

@@ -0,0 +1,121 @@
<template>
<view
:style="tools._style([customStyle])"
:class="className"
>
<slot />
<view :class="classPrefix + '__collapse--slot'">
<slot name="collapse-avatar" />
</view>
<view
v-if="max && max < length"
:class="classPrefix + '__collapse--default'"
@click="onCollapsedItemClick"
>
<t-avatar
:t-class-image="prefix + '-avatar--border ' + prefix + '-avatar--border-' + size + ' ' + tClassImage"
:t-class-content="tClassContent"
:size="size"
:shape="shape"
:icon="collapseAvatar ? '' : 'user-add'"
aria-role="none"
>
{{ collapseAvatar }}
</t-avatar>
</view>
</view>
</template>
<script>
import TAvatar from '../avatar/avatar';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import avatarGroupProps from './props';
import tools from '../common/utils.wxs';
import { ParentMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-avatar-group`;
const AVATAR_GROUP_INIT_Z_INDEX = 50;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-content`,
`${prefix}-class-image`,
],
mixins: [ParentMixin(RELATION_MAP.Avatar)],
components: {
TAvatar,
},
props: {
...avatarGroupProps,
},
data() {
return {
prefix,
classPrefix: name,
hasChild: true,
length: 0,
className: '',
tools,
};
},
watch: {
cascading: 'setClass',
size: 'setClass',
},
mounted() {
this.setClass();
this.length = this.children?.length || 0;
if (this.length) {
this.handleMax();
}
},
methods: {
setClass() {
const { cascading, size } = this;
const direction = cascading.split('-')[0];
const classList = [
name,
this.tClass,
`${name}-offset-${direction}`,
`${name}-offset-${direction}-${size.indexOf('px') > -1 ? 'medium' : size || 'medium'}`,
];
this.className = classList.join(' ');
},
handleMax() {
const { max, cascading } = this;
const len = this.children.length;
if (!max || max > len) return;
const restAvatars = this.children.splice(max, len - max);
const isLeft = cascading === 'left-up';
this.children.forEach((child, index) => {
child.setStyle({
zIndex: isLeft && `calc(var(--td-avatar-group-init-z-index, ${AVATAR_GROUP_INIT_Z_INDEX}) - ${index})`,
padding: 'var(--td-avatar-group-line-spacing, 2px) 0',
});
});
restAvatars.forEach((child) => {
child.hide();
});
},
onCollapsedItemClick(e) {
this.$emit('collapsed-item-click', e);
},
},
});
</script>
<style scoped>
@import './avatar-group.css';
</style>

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdAvatarGroupProps } from './type';
export default {
/** 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上 */
cascading: {
type: String,
default: 'left-up' as TdAvatarGroupProps['cascading'],
validator(val: TdAvatarGroupProps['cascading']): boolean {
if (!val) return true;
return ['left-up', 'right-up'].includes(val);
},
},
/** 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5``...`, `更多` */
collapseAvatar: {
type: String,
},
/** 能够同时显示的最多头像数量 */
max: {
type: Number,
},
/** 形状。优先级低于 Avatar.shape */
shape: {
type: String,
validator(val: TdAvatarGroupProps['shape']): boolean {
if (!val) return true;
return ['circle', 'round'].includes(val);
},
},
/** 尺寸示例值small/medium/large/24px/38px 等。优先级低于 Avatar.size */
size: {
type: String,
default: '',
},
/** 点击头像折叠元素触发 */
onCollapsedItemClick: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,38 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { ShapeEnum } from '../common/common';
export interface TdAvatarGroupProps {
/**
* 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上
* @default 'left-up'
*/
cascading?: CascadingValue;
/**
* 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5``...`, `更多`
*/
collapseAvatar?: string;
/**
* 能够同时显示的最多头像数量
*/
max?: number;
/**
* 形状。优先级低于 Avatar.shape
*/
shape?: ShapeEnum;
/**
* 尺寸示例值small/medium/large/24px/38px 等。优先级低于 Avatar.size
* @default ''
*/
size?: string;
/**
* 点击头像折叠元素触发
*/
onCollapsedItemClick?: (e: MouseEvent) => void;
}
export type CascadingValue = 'left-up' | 'right-up';

View File

@@ -0,0 +1,102 @@
:: BASE_DOC ::
## API
### Avatar Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
alt | String | - | show it when url is not valid | N
badge-props | Object | {} | Typescript`BadgeProps`[Badge API Documents](./badge?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar/type.ts) | N
bordered | Boolean | false | \- | N
hide-on-load-failed | Boolean | false | hide image when loading image failed | N
icon | String / Object | - | \- | N
image | String | - | images url | N
image-props | Object | - | Typescript`ImageProps`[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar/type.ts) | N
shape | String | - | shape。options: circle/round。Typescript`ShapeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
size | String | - | size | N
### Avatar Events
name | params | description
-- | -- | --
error | `(e: Event)` | trigger on image load failed
### Avatar Slots
name | Description
-- | --
\- | \-
### Avatar External Classes
className | Description
-- | --
t-class | \-
t-class-alt | \-
t-class-content | \-
t-class-icon | \-
t-class-image | \-
### AvatarGroup Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
cascading | String | 'left-up' | multiple images cascading。options: left-up/right-up。Typescript`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar-group/type.ts) | N
collapse-avatar | String | - | \- | N
max | Number | - | \- | N
shape | String | - | shape。options: circle/round。Typescript`ShapeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
size | String | - | size | N
### AvatarGroup Events
name | params | description
-- | -- | --
collapsed-item-click | `(e: MouseEvent)` | \-
### AvatarGroup Slots
name | Description
-- | --
\- | \-
collapse-avatar | \-
### AvatarGroup External Classes
className | Description
-- | --
t-class | \-
t-class-content | \-
t-class-image | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-avatar-group-init-z-index | @avatar-group-init-zIndex | -
--td-avatar-group-line-spacing | 4rpx | -
--td-avatar-group-margin-left-large | -16rpx | -
--td-avatar-group-margin-left-medium | -16rpx | -
--td-avatar-group-margin-left-small | -16rpx | -
--td-avatar-bg-color | @brand-color-light-active | -
--td-avatar-border-color | #fff | -
--td-avatar-border-width-large | 6rpx | -
--td-avatar-border-width-medium | 4rpx | -
--td-avatar-border-width-small | 2rpx | -
--td-avatar-circle-border-radius | @radius-circle | -
--td-avatar-content-color | @brand-color | -
--td-avatar-icon-large-font-size | 64rpx | -
--td-avatar-icon-medium-font-size | 48rpx | -
--td-avatar-icon-small-font-size | 40rpx | -
--td-avatar-large-width | 128rpx | -
--td-avatar-margin-left | 0 | -
--td-avatar-medium-width | 96rpx | -
--td-avatar-round-border-radius | @radius-default | -
--td-avatar-small-width | 80rpx | -
--td-avatar-text-large-font-size | @font-size-xl | -
--td-avatar-text-medium-font-size | @font-size-m | -
--td-avatar-text-small-font-size | @font-size-base | -

View File

@@ -0,0 +1,152 @@
---
title: Avatar 头像
description: 用于展示用户头像信息,除了纯展示也可点击进入个人详情等操作。
spline: data
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TAvatar from '@tdesign/uniapp/avatar/avatar.vue';
import TAvatarGroup from '@tdesign/uniapp/avatar-group/avatar-group.vue';
```
### 头像类型
图片头像
{{ image-avatar }}
字符头像
{{ character-avatar }}
图标头像
{{ icon-avatar }}
徽标头像
{{ badge-avatar }}
### 组合头像
纯展示
{{ exhibition }}
带操作
{{ action }}
### 头像尺寸
头像 large/medium/small 尺寸
{{ size }}
## API
### Avatar Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
alt | String | - | 头像替换文本,仅当图片加载失败时有效 | N
badge-props | Object | {} | 头像右上角提示信息,继承 Badge 组件的全部特性。如小红点或者数字。TS 类型:`BadgeProps`[Badge API Documents](./badge?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar/type.ts) | N
bordered | Boolean | false | 已废弃。是否显示外边框 | N
hide-on-load-failed | Boolean | false | 加载失败时隐藏图片 | N
icon | String / Object | - | 图标。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon` | N
image | String | - | 图片地址 | N
image-props | Object | - | 透传至 Image 组件。TS 类型:`ImageProps`[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar/type.ts) | N
shape | String | - | 形状。优先级高于 AvatarGroup.shape 。Avatar 单独存在时,默认值为 circle。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.shape 决定。可选项circle/round。TS 类型:`ShapeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
size | String | - | 尺寸示例值small/medium/large/24px/38px 等。优先级高于 AvatarGroup.size 。Avatar 单独存在时,默认值为 medium。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.size 决定 | N
### Avatar Events
名称 | 参数 | 描述
-- | -- | --
error | `(e: Event)` | 图片加载失败时触发
### Avatar Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
### Avatar External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-alt | 替代文本样式类
t-class-content | 内容样式类
t-class-icon | 图标样式类
t-class-image | 图片样式类
### AvatarGroup Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
cascading | String | 'left-up' | 图片之间的层叠关系可选值左侧图片在上和右侧图片在上。可选项left-up/right-up。TS 类型:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/avatar-group/type.ts) | N
collapse-avatar | String | - | 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5``...`, `更多` | N
max | Number | - | 能够同时显示的最多头像数量 | N
shape | String | - | 形状。优先级低于 Avatar.shape。可选项circle/round。TS 类型:`ShapeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
size | String | - | 尺寸示例值small/medium/large/24px/38px 等。优先级低于 Avatar.size | N
### AvatarGroup Events
名称 | 参数 | 描述
-- | -- | --
collapsed-item-click | `(e: MouseEvent)` | 点击头像折叠元素触发
### AvatarGroup Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
collapse-avatar | 自定义 `collapse-avatar` 显示内容
### AvatarGroup External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-content | 内容样式类
t-class-image | 图片样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-avatar-group-init-z-index | @avatar-group-init-zIndex | -
--td-avatar-group-line-spacing | 4rpx | -
--td-avatar-group-margin-left-large | -16rpx | -
--td-avatar-group-margin-left-medium | -16rpx | -
--td-avatar-group-margin-left-small | -16rpx | -
--td-avatar-bg-color | @brand-color-light-active | -
--td-avatar-border-color | #fff | -
--td-avatar-border-width-large | 6rpx | -
--td-avatar-border-width-medium | 4rpx | -
--td-avatar-border-width-small | 2rpx | -
--td-avatar-circle-border-radius | @radius-circle | -
--td-avatar-content-color | @brand-color | -
--td-avatar-icon-large-font-size | 64rpx | -
--td-avatar-icon-medium-font-size | 48rpx | -
--td-avatar-icon-small-font-size | 40rpx | -
--td-avatar-large-width | 128rpx | -
--td-avatar-margin-left | 0 | -
--td-avatar-medium-width | 96rpx | -
--td-avatar-round-border-radius | @radius-default | -
--td-avatar-small-width | 80rpx | -
--td-avatar-text-large-font-size | @font-size-xl | -
--td-avatar-text-medium-font-size | @font-size-m | -
--td-avatar-text-small-font-size | @font-size-base | -

View File

@@ -0,0 +1,77 @@
.t-avatar {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
background-color: var(--td-avatar-bg-color, var(--td-brand-color-light-active, var(--td-primary-color-2, #d9e1ff)));
color: var(--td-avatar-content-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-avatar__wrapper {
display: inline-flex;
position: relative;
vertical-align: top;
margin-left: var(--td-avatar-margin-left, 0);
}
.t-avatar--large {
width: var(--td-avatar-large-width, 128rpx);
height: var(--td-avatar-large-width, 128rpx);
font-size: var(--td-avatar-text-large-font-size, var(--td-font-size-xl, 40rpx));
}
.t-avatar--large .t-avatar__icon {
font-size: var(--td-avatar-icon-large-font-size, 64rpx);
}
.t-avatar--medium {
width: var(--td-avatar-medium-width, 96rpx);
height: var(--td-avatar-medium-width, 96rpx);
font-size: var(--td-avatar-text-medium-font-size, var(--td-font-size-m, 32rpx));
}
.t-avatar--medium .t-avatar__icon {
font-size: var(--td-avatar-icon-medium-font-size, 48rpx);
}
.t-avatar--small {
width: var(--td-avatar-small-width, 80rpx);
height: var(--td-avatar-small-width, 80rpx);
font-size: var(--td-avatar-text-small-font-size, var(--td-font-size-base, 28rpx));
}
.t-avatar--small .t-avatar__icon {
font-size: var(--td-avatar-icon-small-font-size, 40rpx);
}
.t-avatar .t-image,
.t-avatar__image {
width: 100%;
height: 100%;
}
.t-avatar--circle {
border-radius: var(--td-avatar-circle-border-radius, var(--td-radius-circle, 50%));
overflow: hidden;
}
.t-avatar--round {
border-radius: var(--td-avatar-round-border-radius, var(--td-radius-default, 12rpx));
overflow: hidden;
}
.t-avatar__text,
.t-avatar__icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.t-avatar__text:empty,
.t-avatar__icon:empty {
width: 0;
height: 0;
}
.t-avatar--border {
border-color: var(--td-avatar-border-color, #fff);
border-style: solid;
}
.t-avatar--border-small {
border-width: var(--td-avatar-border-width-small, 2rpx);
}
.t-avatar--border-medium {
border-width: var(--td-avatar-border-width-medium, 4rpx);
}
.t-avatar--border-large {
border-width: var(--td-avatar-border-width-large, 6rpx);
}

View File

@@ -0,0 +1,200 @@
<template>
<view
:class="[
classPrefix + '__wrapper',
tClass
]"
:style="tools._style([utils.getStyles(isShow), customStyle, innerStyle])"
>
<t-badge
v-if="badgeProps"
:color="badgeProps.color || ''"
:content="badgeProps.content || ''"
:count="badgeProps.count || 0"
:dot="badgeProps.dot || false"
:max-count="badgeProps.maxCount || 99"
:offset="badgeProps.offset || []"
:shape="badgeProps.shape || 'circle'"
:show-zero="badgeProps.showZero || false"
:size="badgeProps.size || 'medium'"
:t-class="badgeProps.tClass"
:t-class-content="badgeProps.tClassContent"
:t-class-count="badgeProps.tClassCount"
>
<view
:class="[
utils.getClass(classPrefix, dataSize || 'medium', dataShape, dataBordered),
tClassImage
]"
:style="utils.getSize(dataSize, windowWidth)"
:aria-label="ariaLabel || alt || '头像'"
:aria-role="ariaRole || 'img'"
:aria-hidden="ariaHidden"
>
<t-image
v-if="image"
:t-class="prefix + '-image ' + classPrefix + '__image'"
:t-class-load="tClassAlt"
:custom-style="imageCustomStyle"
style="width: 100%;height: 100%;"
:src="image"
:mode="(imageProps && imageProps.mode) || 'aspectFill'"
:lazy="(imageProps && imageProps.lazy) || false"
:loading="(imageProps && imageProps.loading) || 'default'"
:shape="(imageProps && imageProps.shape) || 'round'"
:webp="(imageProps && imageProps.webp) || false"
:error="alt || 'default'"
@error="onLoadError"
/>
<block
v-else-if="iconName || tools.isNoEmptyObj(iconData)"
name="icon"
>
<t-icon
:t-class="classPrefix + '__icon ' + classPrefix + '__icon--' + (iconData.activeIdx == iconData.index ? 'active ' : ' ') + tClassIcon"
:prefix="iconData.prefix"
:name="iconName || iconData.name"
:size="iconData.size"
:color="iconData.color"
:aria-hidden="!!iconData.ariaHidden"
:aria-label="iconData.ariaLabel"
:aria-role="iconData.ariaRole"
:custom-style="iconCustomStyle"
@click="iconData.click || ''"
/>
</block>
<view
v-else
:class="[
classPrefix + '__text ',
tClassContent
]"
>
<slot />
</view>
</view>
</t-badge>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import TBadge from '../badge/badge';
import TImage from '../image/image';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import avatarProps from './props';
import { setIcon, systemInfo } from '../common/utils';
import tools from '../common/utils.wxs';
import * as utils from './computed.js';
import { ChildrenMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-avatar`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-image`,
`${prefix}-class-icon`,
`${prefix}-class-alt`,
`${prefix}-class-content`,
],
mixins: [ChildrenMixin(RELATION_MAP.Avatar)],
components: {
TIcon,
TBadge,
TImage,
},
props: {
...avatarProps,
},
data() {
return {
prefix,
classPrefix: name,
isShow: true,
zIndex: 0,
windowWidth: systemInfo.windowWidth,
utils,
tools,
iconName: '',
iconData: {},
dataShape: this.shape,
dataSize: this.size,
dataBordered: this.bordered,
innerStyle: '',
};
},
computed: {
iconCustomStyle() {
const fontSize = {
small: 'var(--td-avatar-icon-small-font-size, 20px)',
medium: 'var(--td-avatar-icon-medium-font-size, 24px)',
large: 'var(--td-avatar-icon-large-font-size, 32px)',
};
if (!fontSize[this.dataSize]) return '';
return tools._style([
{
fontSize: fontSize[this.dataSize],
},
this.iconData.style || '',
]);
},
imageCustomStyle() {
return tools._style([
{
width: '100%',
height: '100%',
},
this.imageProps?.style || '',
]);
},
},
watch: {
icon: {
handler(t) {
const obj = setIcon('icon', t, '');
Object.keys(obj).forEach((key) => {
this[key] = obj[key];
});
},
immediate: true,
},
},
mounted() {
},
methods: {
innerAfterLinked() {
this.dataShape = this.shape || this[RELATION_MAP.Avatar]?.shape || 'circle';
this.dataSize = this.size || this[RELATION_MAP.Avatar]?.size;
this.dataBordered = true;
},
hide() {
this.isShow = false;
},
onLoadError(e) {
if (this.hideOnLoadFailed) {
this.isShow = false;
}
this.$emit('error', e && e.e);
},
setStyle(val = '') {
this.innerStyle = val;
},
},
});
</script>
<style scoped>
@import './avatar.css';
</style>

View File

@@ -0,0 +1,30 @@
import { getRegExp } from '../common/runtime/wxs-polyfill';
export function getClass(classPrefix, size, shape, bordered) {
const hasPx = (size || '').indexOf('px') > -1;
const borderSize = hasPx ? 'medium' : size;
const classNames = [
classPrefix,
classPrefix + (shape === 'round' ? '--round' : '--circle'),
bordered ? `${classPrefix}--border ${classPrefix}--border-${borderSize}` : '',
hasPx ? '' : `${classPrefix}--${size}`,
];
return classNames.join(' ');
}
export function getSize(size = 'medium', windowWidth) {
const res = getRegExp('^([0-9]+)(px|rpx)$').exec(size);
if (res && res.length >= 3) {
let px = res[1];
if (res[2] === 'rpx') {
px = Math.floor((windowWidth * res[1]) / 750);
}
return `width:${size};height:${size};font-size:${(px / 8) * 3 + 2}px`;
}
}
export function getStyles(isShow) {
return isShow ? '' : 'display: none;';
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdAvatarProps } from './type';
export default {
/** 头像替换文本,仅当图片加载失败时有效 */
alt: {
type: String,
default: '',
},
/** 头像右上角提示信息,继承 Badge 组件的全部特性。如:小红点,或者数字 */
badgeProps: {
type: Object,
default: () => ({}),
},
/** 已废弃。是否显示外边框 */
bordered: Boolean,
/** 加载失败时隐藏图片 */
hideOnLoadFailed: Boolean,
/** 图标。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon` */
icon: {
type: [String, Object],
},
/** 图片地址 */
image: {
type: String,
default: '',
},
/** 透传至 Image 组件 */
imageProps: {
type: Object,
},
/** 形状。优先级高于 AvatarGroup.shape 。Avatar 单独存在时,默认值为 circle。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.shape 决定 */
shape: {
type: String,
validator(val: TdAvatarProps['shape']): boolean {
if (!val) return true;
return ['circle', 'round'].includes(val);
},
},
/** 尺寸示例值small/medium/large/24px/38px 等。优先级高于 AvatarGroup.size 。Avatar 单独存在时,默认值为 medium。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.size 决定 */
size: {
type: String,
default: '',
},
/** 图片加载失败时触发 */
onError: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,58 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdBadgeProps as BadgeProps } from '../badge/type';
import type { TdImageProps as ImageProps } from '../image/type';
import type { ShapeEnum } from '../common/common';
export interface TdAvatarProps {
/**
* 头像替换文本,仅当图片加载失败时有效
* @default ''
*/
alt?: string;
/**
* 头像右上角提示信息,继承 Badge 组件的全部特性。如:小红点,或者数字
* @default {}
*/
badgeProps?: BadgeProps;
/**
* 已废弃。是否显示外边框
* @default false
*/
bordered?: boolean;
/**
* 加载失败时隐藏图片
* @default false
*/
hideOnLoadFailed?: boolean;
/**
* 图标。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon`
*/
icon?: string | object;
/**
* 图片地址
* @default ''
*/
image?: string;
/**
* 透传至 Image 组件
*/
imageProps?: ImageProps;
/**
* 形状。优先级高于 AvatarGroup.shape 。Avatar 单独存在时,默认值为 circle。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.shape 决定
*/
shape?: ShapeEnum;
/**
* 尺寸示例值small/medium/large/24px/38px 等。优先级高于 AvatarGroup.size 。Avatar 单独存在时,默认值为 medium。如果父组件 AvatarGroup 存在,默认值便由 AvatarGroup.size 决定
* @default ''
*/
size?: string;
/**
* 图片加载失败时触发
*/
onError?: (e: Event) => void;
}

View File

@@ -0,0 +1,49 @@
:: BASE_DOC ::
## API
### BackTop Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
fixed | Boolean | true | \- | N
icon | String / Boolean / Object | true | \- | N
scroll-top | Number | 0 | \- | N
text | String | '' | \- | N
theme | String | round | options: round/half-round/round-dark/half-round-dark | N
visibility-height | Number | 200 | \- | N
### BackTop Events
name | params | description
-- | -- | --
to-top | \- | \-
### BackTop Slots
name | Description
-- | --
\- | \-
icon | \-
### BackTop External Classes
className | Description
-- | --
t-class | \-
t-class-icon | \-
t-class-text | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-back-top-half-round-border-radius | @radius-round | -
--td-back-top-round-bg-color | @bg-color-container | -
--td-back-top-round-border-color | @component-border | -
--td-back-top-round-border-radius | @radius-circle | -
--td-back-top-round-color | @text-color-primary | -
--td-back-top-round-dark-bg-color | @gray-color-13 | -
--td-back-top-round-dark-color | @text-color-anti | -

View File

@@ -0,0 +1,67 @@
---
title: BackTop 返回顶部
description: 用于当页面过长往下滑动时,帮助用户快速回到页面顶部。
spline: navigation
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TBackTop from '@tdesign/uniapp/back-top/back-top.vue';
```
### 基础返回顶部
{{ base }}
## API
### BackTop Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
fixed | Boolean | true | 是否绝对定位固定到屏幕右下方 | N
icon | String / Boolean / Object | true | 图标。值为 `false` 表示不显示图标。不传表示使用默认图标 `'backtop'` | N
scroll-top | Number | 0 | 页面滚动距离 | N
text | String | '' | 文案 | N
theme | String | round | 预设的样式类型。可选项round/half-round/round-dark/half-round-dark | N
visibility-height | Number | 200 | 滚动高度达到此参数值才出现 | N
### BackTop Events
名称 | 参数 | 描述
-- | -- | --
to-top | \- | 点击触发
### BackTop Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
icon | 自定义图标内容
### BackTop External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-icon | 图标样式类
t-class-text | 文本样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-back-top-half-round-border-radius | @radius-round | -
--td-back-top-round-bg-color | @bg-color-container | -
--td-back-top-round-border-color | @component-border | -
--td-back-top-round-border-radius | @radius-circle | -
--td-back-top-round-color | @text-color-primary | -
--td-back-top-round-dark-bg-color | @gray-color-13 | -
--td-back-top-round-dark-color | @text-color-anti | -

View File

@@ -0,0 +1,65 @@
.t-back-top {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: transparent;
overflow: hidden;
box-sizing: border-box;
transition: height 0.2s;
height: auto;
}
.t-back-top--fixed {
position: fixed;
right: var(--td-spacer, 16rpx);
bottom: calc(var(--td-spacer-2, 32rpx) + env(safe-area-inset-bottom));
}
.t-back-top--round,
.t-back-top--round-dark {
width: 96rpx;
height: 96rpx;
border-radius: var(--td-back-top-round-border-radius, var(--td-radius-circle, 50%));
}
.t-back-top--round,
.t-back-top--half-round {
color: var(--td-back-top-round-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
border: 1rpx solid var(--td-back-top-round-border-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
background-color: var(--td-back-top-round-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-back-top--round-dark,
.t-back-top--half-round-dark {
color: var(--td-back-top-round-dark-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
background-color: var(--td-back-top-round-dark-bg-color, var(--td-gray-color-13, #242424));
}
.t-back-top--half-round,
.t-back-top--half-round-dark {
width: 120rpx;
height: 80rpx;
border-radius: 0;
border-top-left-radius: var(--td-back-top-half-round-border-radius, var(--td-radius-round, 999px));
border-bottom-left-radius: var(--td-back-top-half-round-border-radius, var(--td-radius-round, 999px));
flex-direction: row;
right: 0;
}
.t-back-top__text--round,
.t-back-top__text--round-dark,
.t-back-top__text--half-round,
.t-back-top__text--half-round-dark {
font-weight: 600;
font-size: var(--td-font-size, 20rpx);
line-height: 24rpx;
}
.t-back-top__text--half-round,
.t-back-top__text--half-round-dark {
width: 48rpx;
}
.t-back-top__icon:not(:empty) + .t-back-top__text--half-round,
.t-back-top__icon:not(:empty) + .t-back-top__text--half-round-dark {
margin-left: 8rpx;
}
.t-back-top__icon {
display: flex;
justify-content: center;
align-items: center;
font-size: 44rpx;
}

View File

@@ -0,0 +1,122 @@
<template>
<view
v-if="!hidden"
:style="tools._style([customStyle])"
:class="tClass + ' ' + tools.cls(classPrefix, [['fixed', fixed], theme])"
aria-role="button"
@click="toTop"
>
<view
:class="classPrefix + '__icon'"
aria-hidden
>
<slot name="icon" />
<block
v-if="_icon"
name="icon"
>
<t-icon
:custom-style="_icon.style || ''"
:t-class="tClassIcon"
:prefix="_icon.prefix"
:name="_icon.name"
:size="_icon.size"
:color="_icon.color"
:aria-hidden="!!_icon.ariaHidden"
:aria-label="_icon.ariaLabel"
:aria-role="_icon.ariaRole"
@click="_icon.click || ''"
/>
</block>
</view>
<view
v-if="!!text"
:class="classPrefix + '__text--' + theme + ' ' + tClassText"
>
{{ text }}
</view>
<slot />
</view>
</template>
<script>
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { calcIcon } from '../common/utils';
import tools from '../common/utils.wxs';
import { ChildrenMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-back-top`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-icon`,
`${prefix}-class-text`,
],
mixins: [
ChildrenMixin(RELATION_MAP.BackTop),
],
components: {
TIcon,
},
props: {
...props,
},
emits: [
'to-top',
],
watch: {
icon() {
this.setIcon();
},
scrollTop: {
handler(value) {
const { visibilityHeight } = this;
this.hidden = value < visibilityHeight;
},
immediate: true,
},
},
mounted() {
const { icon } = this;
this.setIcon(icon);
},
methods: {
setIcon(v) {
this._icon = calcIcon(v, 'backtop');
},
toTop() {
this.$emit('to-top');
if (this[RELATION_MAP.BackTop]) {
this[RELATION_MAP.BackTop]?.scrollToTop();
this.hidden = true;
} else {
uni.pageScrollTo({
scrollTop: 0,
duration: 300,
});
}
},
},
data() {
return {
prefix,
classPrefix: name,
_icon: null,
hidden: true,
tools,
};
},
});
</script>
<style scoped>
@import './back-top.css';
</style>

View File

@@ -0,0 +1,48 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdBackTopProps } from './type';
export default {
/** 是否绝对定位固定到屏幕右下方 */
fixed: {
type: Boolean,
default: true,
},
/** 图标。值为 `false` 表示不显示图标。不传表示使用默认图标 `'backtop'` */
icon: {
type: [String, Boolean, Object],
default: true as TdBackTopProps['icon'],
},
/** 页面滚动距离 */
scrollTop: {
type: Number,
default: 0,
},
/** 文案 */
text: {
type: String,
default: '',
},
/** 预设的样式类型 */
theme: {
type: String,
default: 'round' as TdBackTopProps['theme'],
validator(val: TdBackTopProps['theme']): boolean {
if (!val) return true;
return ['round', 'half-round', 'round-dark', 'half-round-dark'].includes(val);
},
},
/** 滚动高度达到此参数值才出现 */
visibilityHeight: {
type: Number,
default: 200,
},
/** 点击触发 */
onToTop: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,42 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdBackTopProps {
/**
* 是否绝对定位固定到屏幕右下方
* @default true
*/
fixed?: boolean;
/**
* 图标。值为 `false` 表示不显示图标。不传表示使用默认图标 `'backtop'`
* @default true
*/
icon?: string | boolean | object;
/**
* 页面滚动距离
* @default 0
*/
scrollTop?: number;
/**
* 文案
* @default ''
*/
text?: string;
/**
* 预设的样式类型
* @default round
*/
theme?: 'round' | 'half-round' | 'round-dark' | 'half-round-dark';
/**
* 滚动高度达到此参数值才出现
* @default 200
*/
visibilityHeight?: number;
/**
* 点击触发
*/
onToTop?: () => void;
}

View File

@@ -0,0 +1,54 @@
:: BASE_DOC ::
## API
### Badge Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
color | String | - | \- | N
content | String | - | \- | N
count | String / Number | 0 | \- | N
dot | Boolean | false | \- | N
max-count | Number | 99 | \- | N
offset | Array | - | Typescript: `Array<string \| number>` | N
shape | String | circle | options: circle/square/bubble/ribbon/ribbon-right/ribbon-left/triangle-right/triangle-left | N
show-zero | Boolean | false | \- | N
size | String | medium | options: medium/large | N
### Badge Slots
name | Description
-- | --
\- | \-
count | \-
### Badge External Classes
className | Description
-- | --
t-class | \-
t-class-content | \-
t-class-count | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-badge-basic-height | 32rpx | -
--td-badge-basic-padding | 8rpx | -
--td-badge-basic-width | 32rpx | -
--td-badge-bg-color | @error-color | -
--td-badge-border-radius | 4rpx | -
--td-badge-bubble-border-radius | 20rpx 20rpx 20rpx 1px | -
--td-badge-content-text-color | @text-color-primary | -
--td-badge-dot-size | 16rpx | -
--td-badge-font | @font-mark-extraSmall | -
--td-badge-large-font | @font-mark-small | -
--td-badge-large-height | 40rpx | -
--td-badge-large-padding | 10rpx | -
--td-badge-text-color | @text-color-anti | -
--td-line-height-mark-extraSmall | 32rpx | -
--td-line-height-mark-small | 40rpx | -

View File

@@ -0,0 +1,85 @@
---
title: Badge 徽标
description: 用于告知用户,该区域的状态变化或者待处理任务的数量。
spline: data
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TBadge from '@tdesign/uniapp/badge/badge.vue';
```
### 组件类型
{{ base }}
### 组件样式
{{ theme }}
### 组件尺寸
{{ size }}
## FAQ
### 如何处理由 ribbon 徽标溢出导致页面出现横向滚动?
角标溢出问题建议从父容器组件处理。如 <a href="https://github.com/Tencent/tdesign-miniprogram/issues/3063" title="如 #3063 " target="_blank" rel="noopener noreferrer"> #3063 </a>,可以给父容器 `cell` 组件添加 `overflow: hidden`,处理溢出造成页面出现横向滚动的问题。
## API
### Badge Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
color | String | - | 颜色 | N
content | String | - | 徽标内容,示例:`content='自定义内容'`。也可以使用默认插槽定义 | N
count | String / Number | 0 | 徽标右上角内容。可以是数字,也可以是文字。如:'new'/3/99+。特殊:值为空表示使用插槽渲染 | N
dot | Boolean | false | 是否为红点 | N
max-count | Number | 99 | 封顶的数字值 | N
offset | Array | - | 设置状态点的位置偏移,示例:[-10, 20] 或 ['10em', '8rem']。TS 类型:`Array<string \| number>` | N
shape | String | circle | 徽标形状,其中 ribbon 和 ribbon-right 等效。可选项circle/square/bubble/ribbon/ribbon-right/ribbon-left/triangle-right/triangle-left | N
show-zero | Boolean | false | 当数值为 0 时,是否展示徽标 | N
size | String | medium | 尺寸。可选项medium/large | N
### Badge Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
count | 徽标右上角内容
### Badge External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-content | 内容样式类
t-class-count | 计数样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-badge-basic-height | 32rpx | -
--td-badge-basic-padding | 8rpx | -
--td-badge-basic-width | 32rpx | -
--td-badge-bg-color | @error-color | -
--td-badge-border-radius | 4rpx | -
--td-badge-bubble-border-radius | 20rpx 20rpx 20rpx 1px | -
--td-badge-content-text-color | @text-color-primary | -
--td-badge-dot-size | 16rpx | -
--td-badge-font | @font-mark-extraSmall | -
--td-badge-large-font | @font-mark-small | -
--td-badge-large-height | 40rpx | -
--td-badge-large-padding | 10rpx | -
--td-badge-text-color | @text-color-anti | -
--td-line-height-mark-extraSmall | 32rpx | -
--td-line-height-mark-small | 40rpx | -

View File

@@ -0,0 +1,140 @@
.t-badge {
position: relative;
display: inline-flex;
align-items: start;
}
.t-badge--basic {
z-index: 100;
padding: 0 var(--td-badge-basic-padding, 8rpx);
font: var(--td-badge-font, var(--td-font-mark-extraSmall, 600 20rpx / 32rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-badge-text-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
background-color: var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
text-align: center;
height: var(--td-badge-basic-height, 32rpx);
border-radius: var(--td-badge-border-radius, 4rpx);
}
.t-badge--dot {
height: var(--td-badge-dot-size, 16rpx);
border-radius: 50%;
min-width: var(--td-badge-dot-size, 16rpx);
padding: 0;
}
.t-badge--count {
min-width: var(--td-badge-basic-width, 32rpx);
white-space: nowrap;
box-sizing: border-box;
}
.t-badge--circle {
border-radius: calc(var(--td-badge-basic-height, 32rpx) / 2);
}
.t-badge__ribbon-outer,
.t-badge__ribbon-right-outer,
.t-badge__triangle-right-outer,
.t-badge__ribbon-left-outer,
.t-badge__triangle-left-outer {
position: absolute;
top: 0;
}
.t-badge__ribbon-outer,
.t-badge__ribbon-right-outer,
.t-badge__triangle-right-outer {
right: 0;
}
.t-badge__ribbon-left-outer,
.t-badge__triangle-left-outer {
left: 0;
}
.t-badge--bubble {
border-radius: var(--td-badge-bubble-border-radius, 20rpx 20rpx 20rpx 1px);
}
.t-badge--ribbon,
.t-badge--ribbon-right,
.t-badge--ribbon-left,
.t-badge--triangle-left,
.t-badge--triangle-right {
width: calc(var(--td-badge-basic-height, 32rpx) * 2);
height: calc(var(--td-badge-basic-height, 32rpx) * 2);
border-radius: 0;
padding: 0;
position: absolute;
top: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.t-badge--ribbon,
.t-badge--ribbon-right {
background: linear-gradient(45deg, transparent 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 85%, transparent 85%);
}
.t-badge--triangle-right {
background: linear-gradient(45deg, transparent 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 50%);
}
.t-badge--ribbon,
.t-badge--ribbon-right,
.t-badge--triangle-right {
right: 0;
}
.t-badge--ribbon .t-badge__count,
.t-badge--ribbon-right .t-badge__count,
.t-badge--triangle-right .t-badge__count {
transform: rotate(45deg) translateY(calc(-1 * var(--td-line-height-mark-extraSmall, 32rpx) / 2 + 1rpx));
}
.t-badge--ribbon-left {
background: linear-gradient(-45deg, transparent 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 85%, transparent 85%);
}
.t-badge--triangle-left {
background: linear-gradient(-45deg, transparent 50%, var(--td-badge-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941))) 50%);
}
.t-badge--ribbon-left,
.t-badge--triangle-left {
left: 0;
}
.t-badge--ribbon-left .t-badge__count,
.t-badge--triangle-left .t-badge__count {
transform: rotate(-45deg) translateY(calc(-1 * var(--td-line-height-mark-extraSmall, 32rpx) / 2 + 1rpx));
}
.t-badge--large {
font: var(--td-badge-large-font, var(--td-font-mark-small, 600 24rpx / 40rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
height: var(--td-badge-large-height, 40rpx);
min-width: var(--td-badge-large-height, 40rpx);
padding: 0 var(--td-badge-large-padding, 10rpx);
}
.t-badge--large.t-badge--circle {
border-radius: calc(var(--td-badge-large-height, 40rpx) / 2);
}
.t-badge--large.t-badge--ribbon,
.t-badge--large.t-badge--ribbon-right,
.t-badge--large.t-badge--ribbon-left,
.t-badge--large.t-badge--triangle-right,
.t-badge--large.t-badge--triangle-left {
width: calc(var(--td-badge-large-height, 40rpx) * 2);
height: calc(var(--td-badge-large-height, 40rpx) * 2);
padding: 0;
}
.t-badge--large.t-badge--ribbon .t-badge__count,
.t-badge--large.t-badge--ribbon-right .t-badge__count,
.t-badge--large.t-badge--triangle-right .t-badge__count {
transform: rotate(45deg) translateY(calc(-1 * var(--td-line-height-mark-small, 40rpx) / 2 + 3rpx));
}
.t-badge--large.t-badge--ribbon-left .t-badge__count,
.t-badge--large.t-badge--triangle-left .t-badge__count {
transform: rotate(-45deg) translateY(calc(-1 * var(--td-line-height-mark-small, 40rpx) / 2 + 3rpx));
}
.t-badge__content:not(:empty) + .t-badge--bubble.t-has-count,
.t-badge__content:not(:empty) + .t-badge--circle.t-has-count,
.t-badge__content:not(:empty) + .t-badge--square.t-has-count {
transform-origin: center center;
transform: translate(-50%, -50%);
position: absolute;
top: 0;
left: 100%;
}
.t-badge__content-text {
display: block;
font: var(--td-font-body-large, 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
color: var(--td-badge-content-text-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-badge__count:empty {
display: none;
}

View File

@@ -0,0 +1,134 @@
<template>
<view
:style="tools._style([customStyle])"
:class="classPrefix + ' ' + (useOuterClass ? classPrefix + '__' + shape + '-outer' : '') + tClass"
:aria-labelledby="labelID"
:aria-describedby="descriptionID"
:aria-role="ariaRole || 'option'"
>
<view
:id="labelID"
:class="tools.cls(classPrefix + '__content', [['empty', !content && !hasChild]]) + ' ' + tClassContent"
:aria-hidden="true"
>
<!-- #ifdef H5 -->
<slot
v-if="!content"
:class="classPrefix + '__content-slot'"
/>
<!-- #endif -->
<!-- 小程序下在 slot 下加 :class 属性会导致渲染失败 -->
<!-- #ifndef H5 -->
<slot
v-if="!content"
/>
<!-- #endif -->
<text
v-else
:class="classPrefix + '__content-text'"
>
{{ content }}
</text>
</view>
<view
v-if="isShowBadge({ dot, count, showZero }) || count === null"
:id="descriptionID"
:aria-hidden="true"
:aria-label="ariaLabel || tools.getBadgeAriaLabel({ dot, count, maxCount })"
:class="[
getBadgeInnerClass({ classPrefix, dot, size, shape, count }) + ' ' + prefix + '-has-count ',
tClassCount
]"
:style="tools._style([getBadgeStyles({ color, offset })])"
>
<view :class="classPrefix + '__count'">
<template v-if="isShowBadge({ dot, count, showZero })">
{{ getBadgeValue({ dot, count, maxCount }) }}
</template>
<slot
else
name="count"
/>
</view>
</view>
</view>
</template>
<script>
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { uniqueFactory, getRect } from '../common/utils';
import tools from '../common/utils.wxs';
import {
getBadgeValue,
getBadgeStyles,
getBadgeInnerClass,
isShowBadge,
} from './computed.js';
const name = `${prefix}-badge`;
const getUniqueID = uniqueFactory('badge');
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-count`,
`${prefix}-class-content`,
],
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
value: '',
labelID: '',
descriptionID: '',
tools,
useOuterClass: false,
};
},
computed: {
hasChild() {
return !!this.$slots?.default;
},
},
mounted() {
const e = getUniqueID();
this.labelID = `${e}_label`;
this.descriptionID = `${e}_description`;
this.checkForActualContent();
},
methods: {
getBadgeValue,
getBadgeStyles,
getBadgeInnerClass,
isShowBadge,
checkForActualContent() {
const target = ['ribbon', 'ribbon-right', 'ribbon-left', 'triangle-right', 'triangle-left'];
if (this.content || !target.includes(this.shape)) {
this.useOuterClass = false;
return;
}
return getRect(this, `.${name}__content`).then((rect) => {
const hasSlotContent = rect.width > 0 || rect.height > 0;
this.useOuterClass = !hasSlotContent;
});
},
},
});
</script>
<style scoped>
@import './badge.css';
</style>

View File

@@ -0,0 +1,63 @@
export const getBadgeValue = function (props) {
if (props.dot) {
return '';
}
// eslint-disable-next-line no-restricted-globals
if (isNaN(props.count) || isNaN(props.maxCount)) {
return props.count;
}
return parseInt(props.count, 10) > props.maxCount ? `${props.maxCount}+` : props.count;
};
export const hasUnit = function (unit) {
return (
unit.indexOf('px') > 0
|| unit.indexOf('rpx') > 0
|| unit.indexOf('em') > 0
|| unit.indexOf('rem') > 0
|| unit.indexOf('%') > 0
|| unit.indexOf('vh') > 0
|| unit.indexOf('vm') > 0
);
};
export const getBadgeStyles = function (props) {
let styleStr = '';
if (props.color) {
styleStr += `background:${props.color};`;
}
if (props.offset?.[0]) {
styleStr
+= `left: calc(100% + ${hasUnit(props.offset[0].toString()) ? props.offset[0] : `${props.offset[0]}px`});`;
}
if (props.offset?.[1]) {
styleStr += `top:${hasUnit(props.offset[1].toString()) ? props.offset[1] : `${props.offset[1]}px`};`;
}
return styleStr;
};
export const getBadgeInnerClass = function (props) {
const baseClass = props.classPrefix;
const classNames = [
`${baseClass}--basic`,
props.dot ? `${baseClass}--dot` : '',
`${baseClass}--${props.size}`,
`${baseClass}--${props.shape}`,
!props.dot ? `${baseClass}--count` : '',
];
return classNames.join(' ');
};
export const isShowBadge = function (props) {
if (props.dot) {
return true;
}
// eslint-disable-next-line no-restricted-globals
if (!props.showZero && !isNaN(props.count) && parseInt(props.count, 10) === 0) {
return false;
}
if (props.count == null) return false;
return true;
};

View File

@@ -0,0 +1,55 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdBadgeProps } from './type';
export default {
/** 颜色 */
color: {
type: String,
default: '',
},
/** 徽标内容,示例:`content='自定义内容'`。也可以使用默认插槽定义 */
content: {
type: String,
default: '',
},
/** 徽标右上角内容。可以是数字,也可以是文字。如:'new'/3/99+。特殊:值为空表示使用插槽渲染 */
count: {
type: [String, Number],
default: 0 as TdBadgeProps['count'],
},
/** 是否为红点 */
dot: Boolean,
/** 封顶的数字值 */
maxCount: {
type: Number,
default: 99,
},
/** 设置状态点的位置偏移,示例:[-10, 20] 或 ['10em', '8rem'] */
offset: {
type: Array,
},
/** 徽标形状,其中 ribbon 和 ribbon-right 等效 */
shape: {
type: String,
default: 'circle' as TdBadgeProps['shape'],
validator(val: TdBadgeProps['shape']): boolean {
if (!val) return true;
return ['circle', 'square', 'bubble', 'ribbon', 'ribbon-right', 'ribbon-left', 'triangle-right', 'triangle-left'].includes(val);
},
},
/** 当数值为 0 时,是否展示徽标 */
showZero: Boolean,
/** 尺寸 */
size: {
type: String,
default: 'medium' as TdBadgeProps['size'],
validator(val: TdBadgeProps['size']): boolean {
if (!val) return true;
return ['medium', 'large'].includes(val);
},
},
};

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdBadgeProps {
/**
* 颜色
* @default ''
*/
color?: string;
/**
* 徽标内容,示例:`content='自定义内容'`。也可以使用默认插槽定义
* @default ''
*/
content?: string;
/**
* 徽标右上角内容。可以是数字,也可以是文字。如:'new'/3/99+。特殊:值为空表示使用插槽渲染
* @default 0
*/
count?: string | number;
/**
* 是否为红点
* @default false
*/
dot?: boolean;
/**
* 封顶的数字值
* @default 99
*/
maxCount?: number;
/**
* 设置状态点的位置偏移,示例:[-10, 20] 或 ['10em', '8rem']
*/
offset?: Array<string | number>;
/**
* 徽标形状,其中 ribbon 和 ribbon-right 等效
* @default circle
*/
shape?:
| 'circle'
| 'square'
| 'bubble'
| 'ribbon'
| 'ribbon-right'
| 'ribbon-left'
| 'triangle-right'
| 'triangle-left';
/**
* 当数值为 0 时,是否展示徽标
* @default false
*/
showZero?: boolean;
/**
* 尺寸
* @default medium
*/
size?: 'medium' | 'large';
}

View File

@@ -0,0 +1,238 @@
---
title: Button
description: Buttons are used to open a closed-loop task, such as "delete" an object, "buy" an item, etc.
spline: base
isComponent: true
---
## Usage
For global import, configure it in `app.json` in the root directory of the miniprogram. For local import, configure it in `index.json` of the page or component that needs to be imported.
```json
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button"
}
```
## Code Demo
### 01 Component Type
#### Basic Buttons
{{ base }}
#### Icon Button
{{ icon-btn }}
#### Ghost Button
{{ ghost-btn }}
#### Combination Button
{{ group-btn }}
#### Banner Button
{{ block-btn }}
### 02 Component State
#### Buttons for different states
{{ disabled }}
### 03 Component Style
#### Different sizes of buttons
{{ size }}
#### Different shaped buttons
{{ shape }}
#### Different color theme buttons
{{ theme }}
## API
### Button Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
app-parameter | String | - | \- | N
block | Boolean | false | make button to be a block-level element | N
content | String | - | button's children elements | N
custom-dataset | String / Number / Boolean / Object / Array | {} | Typescript`string \| number \| boolean \| object \| Array<string \| number \| boolean \| object>` | N
disabled | Boolean | undefined | disable the button, make it can not be clicked | N
ghost | Boolean | false | make background-color to be transparent | N
hover-class | String | - | \- | N
hover-start-time | Number | 20 | \- | N
hover-stay-time | Number | 70 | \- | N
hover-stop-propagation | Boolean | false | \- | N
icon | String / Object | - | icon name | N
lang | String | - | message language。options: en/zh_CN/zh_TW | N
loading | Boolean | false | set button to be loading state | N
loading-props | Object | {} | Typescript`LoadingProps`[Loading API Documents](./loading?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/button/type.ts) | N
open-type | String | - | open type of button, [Miniprogram Button](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)。options: contact/share/getPhoneNumber/getUserInfo/launchApp/openSetting/feedback/chooseAvatar/agreePrivacyAuthorization | N
phone-number-no-quota-toast | Boolean | true | \- | N
send-message-img | String | 截图 | \- | N
send-message-path | String | 当前分享路径 | \- | N
send-message-title | String | 当前标题 | \- | N
session-from | String | - | \- | N
shape | String | rectangle | button shape。options: rectangle/square/round/circle | N
show-message-card | Boolean | false | \- | N
size | String | medium | a button has four size。options: extra-small/small/medium/large | N
t-id | String | - | id | N
theme | String | default | button theme。options: default/primary/danger/light | N
type | String | - | type of button element, same as formType of Miniprogram。options: submit/reset | N
variant | String | base | variant of button。options: base/outline/dashed/text | N
### Button Events
name | params | description
-- | -- | --
agreeprivacyauthorization | \- | \-
chooseavatar | \- | \-
click | `(e: MouseEvent)` | trigger on click
contact | \- | \-
createliveactivity | \- | \-
error | \- | \-
getphonenumber | \- | \-
getrealtimephonenumber | \- | \-
getuserinfo | \- | \-
launchapp | \- | \-
opensetting | \- | \-
### Button Slots
name | Description
-- | --
\- | \-
content | button's children elements
suffix | \-
### Button External Classes
className | Description
-- | --
t-class | \-
t-class-icon | class name of icon
t-class-loading | class name of loading
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-button-border-radius | @radius-default | -
--td-button-border-width | 4rpx | -
--td-button-danger-active-bg-color | @error-color-active | -
--td-button-danger-active-border-color | @error-color-active | -
--td-button-danger-bg-color | @error-color | -
--td-button-danger-border-color | @error-color | -
--td-button-danger-color | @text-color-anti | -
--td-button-danger-dashed-border-color | @button-danger-dashed-color | -
--td-button-danger-dashed-color | @error-color | -
--td-button-danger-dashed-disabled-color | @button-danger-disabled-color | -
--td-button-danger-disabled-bg | @error-color-3 | -
--td-button-danger-disabled-border-color | @error-color-3 | -
--td-button-danger-disabled-color | @font-white-1 | -
--td-button-danger-outline-active-bg-color | @bg-color-container-active | -
--td-button-danger-outline-active-border-color | @error-color-active | -
--td-button-danger-outline-border-color | @button-danger-outline-color | -
--td-button-danger-outline-color | @error-color | -
--td-button-danger-outline-disabled-color | @error-color-3 | -
--td-button-danger-text-active-bg-color | @bg-color-container-active | -
--td-button-danger-text-color | @error-color | -
--td-button-danger-text-disabled-color | @button-danger-disabled-color | -
--td-button-default-active-bg-color | @bg-color-component-active | -
--td-button-default-active-border-color | @bg-color-component-active | -
--td-button-default-bg-color | @bg-color-component | -
--td-button-default-border-color | @bg-color-component | -
--td-button-default-color | @text-color-primary | -
--td-button-default-disabled-bg | @bg-color-component-disabled | -
--td-button-default-disabled-border-color | @bg-color-component-disabled | -
--td-button-default-disabled-color | @text-color-disabled | -
--td-button-default-outline-active-bg-color | @bg-color-container-active | -
--td-button-default-outline-active-border-color | @component-border | -
--td-button-default-outline-border-color | @component-border | -
--td-button-default-outline-color | @text-color-primary | -
--td-button-default-outline-disabled-color | @component-border | -
--td-button-default-text-active-bg-color | @bg-color-container-active | -
--td-button-extra-small-font-size | @font-size-base | -
--td-button-extra-small-height | 56rpx | -
--td-button-extra-small-icon-size | 36rpx | -
--td-button-extra-small-padding-horizontal | 16rpx | -
--td-button-font-weight | 600 | -
--td-button-ghost-border-color | @button-ghost-color | -
--td-button-ghost-color | @text-color-anti | -
--td-button-ghost-danger-border-color | @error-color | -
--td-button-ghost-danger-color | @error-color | -
--td-button-ghost-danger-hover-color | @error-color-active | -
--td-button-ghost-disabled-color | @font-white-4 | -
--td-button-ghost-hover-color | @font-white-2 | -
--td-button-ghost-primary-border-color | @brand-color | -
--td-button-ghost-primary-color | @brand-color | -
--td-button-ghost-primary-hover-color | @brand-color-active | -
--td-button-icon-border-radius | 8rpx | -
--td-button-icon-spacer | @spacer | -
--td-button-large-font-size | @font-size-m | -
--td-button-large-height | 96rpx | -
--td-button-large-icon-size | 48rpx | -
--td-button-large-padding-horizontal | 40rpx | -
--td-button-light-active-bg-color | @brand-color-light-active | -
--td-button-light-active-border-color | @brand-color-light-active | -
--td-button-light-bg-color | @brand-color-light | -
--td-button-light-border-color | @brand-color-light | -
--td-button-light-color | @brand-color | -
--td-button-light-disabled-bg | @brand-color-light | -
--td-button-light-disabled-border-color | @brand-color-light | -
--td-button-light-disabled-color | @brand-color-disabled | -
--td-button-light-outline-active-bg-color | @brand-color-light-active | -
--td-button-light-outline-active-border-color | @brand-color-active | -
--td-button-light-outline-bg-color | @brand-color-light | -
--td-button-light-outline-border-color | @button-light-outline-color | -
--td-button-light-outline-color | @brand-color | -
--td-button-light-outline-disabled-color | @brand-color-disabled | -
--td-button-light-text-active-bg-color | @bg-color-container-active | -
--td-button-light-text-color | @brand-color | -
--td-button-medium-font-size | @font-size-m | -
--td-button-medium-height | 80rpx | -
--td-button-medium-icon-size | 40rpx | -
--td-button-medium-padding-horizontal | 32rpx | -
--td-button-primary-active-bg-color | @brand-color-active | -
--td-button-primary-active-border-color | @brand-color-active | -
--td-button-primary-bg-color | @brand-color | -
--td-button-primary-border-color | @brand-color | -
--td-button-primary-color | @text-color-anti | -
--td-button-primary-dashed-border-color | @button-primary-dashed-color | -
--td-button-primary-dashed-color | @brand-color | -
--td-button-primary-dashed-disabled-color | @brand-color-disabled | -
--td-button-primary-disabled-bg | @brand-color-disabled | -
--td-button-primary-disabled-border-color | @brand-color-disabled | -
--td-button-primary-disabled-color | @text-color-anti | -
--td-button-primary-outline-active-bg-color | @bg-color-container-active | -
--td-button-primary-outline-active-border-color | @brand-color-active | -
--td-button-primary-outline-border-color | @button-primary-outline-color | -
--td-button-primary-outline-color | @brand-color | -
--td-button-primary-outline-disabled-color | @brand-color-disabled | -
--td-button-primary-text-active-bg-color | @bg-color-container-active | -
--td-button-primary-text-color | @brand-color | -
--td-button-primary-text-disabled-color | @brand-color-disabled | -
--td-button-small-font-size | @font-size-base | -
--td-button-small-height | 64rpx | -
--td-button-small-icon-size | 36rpx | -
--td-button-small-padding-horizontal | 24rpx | -

View File

@@ -0,0 +1,229 @@
---
title: Button 按钮
description: 用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
spline: base
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TButton from '@tdesign/uniapp/button/button.vue';
```
### 01 组件类型
#### 基础按钮
{{ base }}
#### 图标按钮
{{ icon-btn }}
#### 幽灵按钮
{{ ghost-btn }}
#### 组合按钮
{{ group-btn }}
#### 通栏按钮
{{ block-btn }}
### 02 组件状态
#### 按钮禁用态
{{ disabled }}
### 03 组件样式
#### 按钮尺寸
{{ size }}
#### 按钮形状
{{ shape }}
#### 按钮主题
{{ theme }}
## API
### Button Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
app-parameter | String | - | 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效 | N
block | Boolean | false | 是否为块级元素 | N
content | String | - | 按钮内容 | N
custom-dataset | String / Number / Boolean / Object / Array | {} | 自定义 dataset可通过 event.currentTarget.dataset.custom 获取。TS 类型:`string \| number \| boolean \| object \| Array<string \| number \| boolean \| object>` | N
disabled | Boolean | undefined | 禁用状态。优先级Button.disabled > Form.disabled | N
ghost | Boolean | false | 是否为幽灵按钮(镂空按钮) | N
hover-class | String | - | 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果 | N
hover-start-time | Number | 20 | 按住后多久出现点击态,单位毫秒 | N
hover-stay-time | Number | 70 | 手指松开后点击态保留时间,单位毫秒 | N
hover-stop-propagation | Boolean | false | 指定是否阻止本节点的祖先节点出现点击态 | N
icon | String / Object | - | 图标名称。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon` | N
lang | String | - | 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)。可选项en/zh_CN/zh_TW | N
loading | Boolean | false | 是否显示为加载状态 | N
loading-props | Object | {} | 透传 Loading 组件全部属性。TS 类型:`LoadingProps`[Loading API Documents](./loading?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/button/type.ts) | N
open-type | String | - | 微信开放能力。<br />具体释义:<br />`contact` 打开客服会话,如果用户在会话中点击消息卡片后返回小程序,可以从 bindcontact 回调中获得具体信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/customer-message.html">具体说明</a> *鸿蒙 OS 暂不支持*<br />`liveActivity` 通过前端获取<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message-2.html">新的一次性订阅消息下发机制</a>使用的 code<br />`share` 触发用户转发,使用前建议先阅读<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html#使用指引">使用指引</a><br />`getPhoneNumber` 获取用户手机号,可以从 bindgetphonenumber 回调中获取到用户信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html">具体说明</a> *小程序插件中不能使用*<br />`getUserInfo` 获取用户信息,可以从 bindgetuserinfo 回调中获取到用户信息 *小程序插件中不能使用*<br />`launchApp` 打开APP可以通过 app-parameter 属性设定向 APP 传的参数<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/launchApp.html">具体说明</a><br />`openSetting` 打开授权设置页;<br />`feedback` 打开“意见反馈”页面,用户可提交反馈内容并上传<a href="https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.getLogManager.html">日志</a>,开发者可以登录<a href="https://mp.weixin.qq.com/">小程序管理后台</a>后进入左侧菜单“客服反馈”页面获取到反馈内容;<br />`chooseAvatar` 获取用户头像,可以从 bindchooseavatar 回调中获取到头像信息;<br />`agreePrivacyAuthorization`用户同意隐私协议按钮。用户点击一次此按钮后,所有隐私接口可以正常调用。可通过`bindagreeprivacyauthorization`监听用户同意隐私协议事件。隐私合规开发指南详情可见《<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html">小程序隐私协议开发指南</a>》。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)。可选项contact/share/getPhoneNumber/getUserInfo/launchApp/openSetting/feedback/chooseAvatar/agreePrivacyAuthorization | N
phone-number-no-quota-toast | Boolean | true | 原生按钮属性当手机号快速验证或手机号实时验证额度用尽时是否对用户展示“申请获取你的手机号但该功能使用次数已达当前小程序上限暂时无法使用”的提示默认展示open-type="getPhoneNumber" 或 open-type="getRealtimePhoneNumber" 时有效 | N
send-message-img | String | 截图 | 会话内消息卡片图片open-type="contact"时有效 | N
send-message-path | String | 当前分享路径 | 会话内消息卡片点击跳转小程序路径open-type="contact"时有效 | N
send-message-title | String | 当前标题 | 会话内消息卡片标题open-type="contact"时有效 | N
session-from | String | - | 会话来源open-type="contact"时有效 | N
shape | String | rectangle | 按钮形状,有 4 种长方形、正方形、圆角长方形、圆形。可选项rectangle/square/round/circle | N
show-message-card | Boolean | false | 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息open-type="contact"时有效 | N
size | String | medium | 组件尺寸。可选项extra-small/small/medium/large | N
t-id | String | - | 按钮标签id | N
theme | String | default | 组件风格依次为品牌色、危险色。可选项default/primary/danger/light | N
type | String | - | 同小程序的 formType。可选项submit/reset | N
variant | String | base | 按钮形式基础、线框、虚线、文字。可选项base/outline/dashed/text | N
### Button Events
名称 | 参数 | 描述
-- | -- | --
agreeprivacyauthorization | \- | 原生按钮属性用户同意隐私协议事件回调open-type=agreePrivacyAuthorization时有效 Tips: 如果使用 onNeedPrivacyAuthorization 接口,需要在 bindagreeprivacyauthorization 触发后再调用 resolve({ event: "agree", buttonId })
chooseavatar | \- | 原生按钮属性,获取用户头像回调,`open-type=chooseAvatar` 时有效。返回 `e.detail.avatarUrl` 为头像临时文件链接
click | `(e: MouseEvent)` | 点击时触发
contact | \- | 原生按钮属性,客服消息回调,`open-type="contact"` 时有效
createliveactivity | \- | 新的一次性订阅消息下发机制回调,`open-type=liveActivity` 时有效
error | \- | 原生按钮属性,当使用开放能力时,发生错误的回调,`open-type=launchApp` 时有效
getphonenumber | \- | 原生按钮属性手机号快速验证回调open-type=getPhoneNumber时有效。Tips在触发 bindgetphonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用
getrealtimephonenumber | \- | 原生按钮属性手机号实时验证回调open-type=getRealtimePhoneNumber 时有效。Tips在触发 bindgetrealtimephonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用
getuserinfo | \- | 原生按钮属性用户点击该按钮时会返回获取到的用户信息回调的detail数据与wx.getUserInfo返回的一致open-type="getUserInfo"时有效
launchapp | \- | 打开 APP 成功的回调,`open-type=launchApp` 时有效
opensetting | \- | 原生按钮属性在打开授权设置页后回调open-type=openSetting时有效
### Button Slots
名称 | 描述
-- | --
\- | 默认插槽,作用同 `content` 插槽
content | 自定义 `content` 显示内容
suffix | 右侧内容,可用于定义右侧图标
### Button External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-icon | 图标样式类
t-class-loading | 加载样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-button-border-radius | @radius-default | -
--td-button-border-width | 4rpx | -
--td-button-danger-active-bg-color | @error-color-active | -
--td-button-danger-active-border-color | @error-color-active | -
--td-button-danger-bg-color | @error-color | -
--td-button-danger-border-color | @error-color | -
--td-button-danger-color | @text-color-anti | -
--td-button-danger-dashed-border-color | @button-danger-dashed-color | -
--td-button-danger-dashed-color | @error-color | -
--td-button-danger-dashed-disabled-color | @button-danger-disabled-color | -
--td-button-danger-disabled-bg | @error-color-3 | -
--td-button-danger-disabled-border-color | @error-color-3 | -
--td-button-danger-disabled-color | @font-white-1 | -
--td-button-danger-outline-active-bg-color | @bg-color-container-active | -
--td-button-danger-outline-active-border-color | @error-color-active | -
--td-button-danger-outline-border-color | @button-danger-outline-color | -
--td-button-danger-outline-color | @error-color | -
--td-button-danger-outline-disabled-color | @error-color-3 | -
--td-button-danger-text-active-bg-color | @bg-color-container-active | -
--td-button-danger-text-color | @error-color | -
--td-button-danger-text-disabled-color | @button-danger-disabled-color | -
--td-button-default-active-bg-color | @bg-color-component-active | -
--td-button-default-active-border-color | @bg-color-component-active | -
--td-button-default-bg-color | @bg-color-component | -
--td-button-default-border-color | @bg-color-component | -
--td-button-default-color | @text-color-primary | -
--td-button-default-disabled-bg | @bg-color-component-disabled | -
--td-button-default-disabled-border-color | @bg-color-component-disabled | -
--td-button-default-disabled-color | @text-color-disabled | -
--td-button-default-outline-active-bg-color | @bg-color-container-active | -
--td-button-default-outline-active-border-color | @component-border | -
--td-button-default-outline-border-color | @component-border | -
--td-button-default-outline-color | @text-color-primary | -
--td-button-default-outline-disabled-color | @component-border | -
--td-button-default-text-active-bg-color | @bg-color-container-active | -
--td-button-extra-small-font-size | @font-size-base | -
--td-button-extra-small-height | 56rpx | -
--td-button-extra-small-icon-size | 36rpx | -
--td-button-extra-small-padding-horizontal | 16rpx | -
--td-button-font-weight | 600 | -
--td-button-ghost-border-color | @button-ghost-color | -
--td-button-ghost-color | @text-color-anti | -
--td-button-ghost-danger-border-color | @error-color | -
--td-button-ghost-danger-color | @error-color | -
--td-button-ghost-danger-hover-color | @error-color-active | -
--td-button-ghost-disabled-color | @font-white-4 | -
--td-button-ghost-hover-color | @font-white-2 | -
--td-button-ghost-primary-border-color | @brand-color | -
--td-button-ghost-primary-color | @brand-color | -
--td-button-ghost-primary-hover-color | @brand-color-active | -
--td-button-icon-border-radius | 8rpx | -
--td-button-icon-spacer | @spacer | -
--td-button-large-font-size | @font-size-m | -
--td-button-large-height | 96rpx | -
--td-button-large-icon-size | 48rpx | -
--td-button-large-padding-horizontal | 40rpx | -
--td-button-light-active-bg-color | @brand-color-light-active | -
--td-button-light-active-border-color | @brand-color-light-active | -
--td-button-light-bg-color | @brand-color-light | -
--td-button-light-border-color | @brand-color-light | -
--td-button-light-color | @brand-color | -
--td-button-light-disabled-bg | @brand-color-light | -
--td-button-light-disabled-border-color | @brand-color-light | -
--td-button-light-disabled-color | @brand-color-disabled | -
--td-button-light-outline-active-bg-color | @brand-color-light-active | -
--td-button-light-outline-active-border-color | @brand-color-active | -
--td-button-light-outline-bg-color | @brand-color-light | -
--td-button-light-outline-border-color | @button-light-outline-color | -
--td-button-light-outline-color | @brand-color | -
--td-button-light-outline-disabled-color | @brand-color-disabled | -
--td-button-light-text-active-bg-color | @bg-color-container-active | -
--td-button-light-text-color | @brand-color | -
--td-button-medium-font-size | @font-size-m | -
--td-button-medium-height | 80rpx | -
--td-button-medium-icon-size | 40rpx | -
--td-button-medium-padding-horizontal | 32rpx | -
--td-button-primary-active-bg-color | @brand-color-active | -
--td-button-primary-active-border-color | @brand-color-active | -
--td-button-primary-bg-color | @brand-color | -
--td-button-primary-border-color | @brand-color | -
--td-button-primary-color | @text-color-anti | -
--td-button-primary-dashed-border-color | @button-primary-dashed-color | -
--td-button-primary-dashed-color | @brand-color | -
--td-button-primary-dashed-disabled-color | @brand-color-disabled | -
--td-button-primary-disabled-bg | @brand-color-disabled | -
--td-button-primary-disabled-border-color | @brand-color-disabled | -
--td-button-primary-disabled-color | @text-color-anti | -
--td-button-primary-outline-active-bg-color | @bg-color-container-active | -
--td-button-primary-outline-active-border-color | @brand-color-active | -
--td-button-primary-outline-border-color | @button-primary-outline-color | -
--td-button-primary-outline-color | @brand-color | -
--td-button-primary-outline-disabled-color | @brand-color-disabled | -
--td-button-primary-text-active-bg-color | @bg-color-container-active | -
--td-button-primary-text-color | @brand-color | -
--td-button-primary-text-disabled-color | @brand-color-disabled | -
--td-button-small-font-size | @font-size-base | -
--td-button-small-height | 64rpx | -
--td-button-small-icon-size | 36rpx | -
--td-button-small-padding-horizontal | 24rpx | -

View File

@@ -0,0 +1,514 @@
.t-button--size-extra-small {
font-size: var(--td-button-extra-small-font-size, var(--td-font-size-base, 28rpx));
padding-left: var(--td-button-extra-small-padding-horizontal, 16rpx);
padding-right: var(--td-button-extra-small-padding-horizontal, 16rpx);
height: var(--td-button-extra-small-height, 56rpx);
line-height: var(--td-button-extra-small-height, 56rpx);
}
.t-button--size-extra-small .t-button__icon {
font-size: var(--td-button-extra-small-icon-size, 36rpx);
}
.t-button--size-small {
font-size: var(--td-button-small-font-size, var(--td-font-size-base, 28rpx));
padding-left: var(--td-button-small-padding-horizontal, 24rpx);
padding-right: var(--td-button-small-padding-horizontal, 24rpx);
height: var(--td-button-small-height, 64rpx);
line-height: var(--td-button-small-height, 64rpx);
}
.t-button--size-small .t-button__icon {
font-size: var(--td-button-small-icon-size, 36rpx);
}
.t-button--size-medium {
font-size: var(--td-button-medium-font-size, var(--td-font-size-m, 32rpx));
padding-left: var(--td-button-medium-padding-horizontal, 32rpx);
padding-right: var(--td-button-medium-padding-horizontal, 32rpx);
height: var(--td-button-medium-height, 80rpx);
line-height: var(--td-button-medium-height, 80rpx);
}
.t-button--size-medium .t-button__icon {
font-size: var(--td-button-medium-icon-size, 40rpx);
}
.t-button--size-large {
font-size: var(--td-button-large-font-size, var(--td-font-size-m, 32rpx));
padding-left: var(--td-button-large-padding-horizontal, 40rpx);
padding-right: var(--td-button-large-padding-horizontal, 40rpx);
height: var(--td-button-large-height, 96rpx);
line-height: var(--td-button-large-height, 96rpx);
}
.t-button--size-large .t-button__icon {
font-size: var(--td-button-large-icon-size, 48rpx);
}
.t-button--default {
color: var(--td-button-default-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
background-color: var(--td-button-default-bg-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-button-default-border-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--default::after {
border-width: var(--td-button-border-width, 4rpx);
border-color: var(--td-button-default-border-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--default.t-button--hover {
z-index: 0;
}
.t-button--default.t-button--hover,
.t-button--default.t-button--hover::after {
background-color: var(--td-button-default-active-bg-color, var(--td-bg-color-component-active, var(--td-gray-color-6, #a6a6a6)));
border-color: var(--td-button-default-active-border-color, var(--td-bg-color-component-active, var(--td-gray-color-6, #a6a6a6)));
}
.t-button--default.t-button--disabled {
color: var(--td-button-default-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
background-color: var(--td-button-default-disabled-bg, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
}
.t-button--default.t-button--disabled,
.t-button--default.t-button--disabled::after {
border-color: var(--td-button-default-disabled-border-color, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
}
.t-button--primary {
color: var(--td-button-primary-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
background-color: var(--td-button-primary-bg-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
border-color: var(--td-button-primary-border-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--primary::after {
border-width: var(--td-button-border-width, 4rpx);
border-color: var(--td-button-primary-border-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--primary.t-button--hover {
z-index: 0;
}
.t-button--primary.t-button--hover,
.t-button--primary.t-button--hover::after {
background-color: var(--td-button-primary-active-bg-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
border-color: var(--td-button-primary-active-border-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--primary.t-button--disabled {
color: var(--td-button-primary-disabled-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
background-color: var(--td-button-primary-disabled-bg, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--primary.t-button--disabled,
.t-button--primary.t-button--disabled::after {
border-color: var(--td-button-primary-disabled-border-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--light {
color: var(--td-button-light-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-button-light-bg-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
border-color: var(--td-button-light-border-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-button--light::after {
border-width: var(--td-button-border-width, 4rpx);
border-color: var(--td-button-light-border-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-button--light.t-button--hover {
z-index: 0;
}
.t-button--light.t-button--hover,
.t-button--light.t-button--hover::after {
background-color: var(--td-button-light-active-bg-color, var(--td-brand-color-light-active, var(--td-primary-color-2, #d9e1ff)));
border-color: var(--td-button-light-active-border-color, var(--td-brand-color-light-active, var(--td-primary-color-2, #d9e1ff)));
}
.t-button--light.t-button--disabled {
color: var(--td-button-light-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
background-color: var(--td-button-light-disabled-bg, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-button--light.t-button--disabled,
.t-button--light.t-button--disabled::after {
border-color: var(--td-button-light-disabled-border-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-button--danger {
color: var(--td-button-danger-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
background-color: var(--td-button-danger-bg-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
border-color: var(--td-button-danger-border-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--danger::after {
border-width: var(--td-button-border-width, 4rpx);
border-color: var(--td-button-danger-border-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--danger.t-button--hover {
z-index: 0;
}
.t-button--danger.t-button--hover,
.t-button--danger.t-button--hover::after {
background-color: var(--td-button-danger-active-bg-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
border-color: var(--td-button-danger-active-border-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
}
.t-button--danger.t-button--disabled {
color: var(--td-button-danger-disabled-color, var(--td-font-white-1, #ffffff));
background-color: var(--td-button-danger-disabled-bg, var(--td-error-color-3, #ffb9b0));
}
.t-button--danger.t-button--disabled,
.t-button--danger.t-button--disabled::after {
border-color: var(--td-button-danger-disabled-border-color, var(--td-error-color-3, #ffb9b0));
}
.t-button {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
white-space: nowrap;
text-align: center;
background-image: none;
transition: all 0.3s;
touch-action: manipulation;
border-radius: var(--td-button-border-radius, var(--td-radius-default, 12rpx));
outline: none;
font-family: PingFang SC, Microsoft YaHei, Arial Regular;
font-weight: var(--td-button-font-weight, 600);
vertical-align: top;
box-sizing: border-box;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
/* stylelint-disable-next-line */
-webkit-appearance: none;
}
.t-button::after {
border-radius: calc(var(--td-button-border-radius, var(--td-radius-default, 12rpx)) * 2);
}
.t-button--text {
color: var(--td-button-default-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
background-color: transparent;
}
.t-button--text,
.t-button--text::after {
border: 0;
}
.t-button--text.t-button--hover,
.t-button--text.t-button--hover::after {
background-color: var(--td-button-default-text-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--text.t-button--primary {
color: var(--td-button-primary-text-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: transparent;
}
.t-button--text.t-button--primary.t-button--hover,
.t-button--text.t-button--primary.t-button--hover::after {
background-color: var(--td-button-primary-text-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--text.t-button--primary.t-button--disabled {
color: var(--td-button-primary-text-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
}
.t-button--text.t-button--danger {
color: var(--td-button-danger-text-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
background-color: transparent;
}
.t-button--text.t-button--danger.t-button--hover,
.t-button--text.t-button--danger.t-button--hover::after {
background-color: var(--td-button-danger-text-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--text.t-button--danger.t-button--disabled {
color: var(--td-button-danger-text-disabled-color, var(--td-button-danger-disabled-color, var(--td-font-white-1, #ffffff)));
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
}
.t-button--text.t-button--light {
color: var(--td-button-light-text-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: transparent;
}
.t-button--text.t-button--light.t-button--hover,
.t-button--text.t-button--light.t-button--hover::after {
background-color: var(--td-button-light-text-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
}
.t-button--text.t-button--disabled {
color: var(--td-button-default-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-button--outline {
color: var(--td-button-default-outline-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
}
.t-button--outline,
.t-button--outline::after {
border-color: var(--td-button-default-outline-border-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}
.t-button--outline.t-button--hover,
.t-button--outline.t-button--hover::after {
background-color: var(--td-button-default-outline-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-button-default-outline-active-border-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}
.t-button--outline.t-button--disabled {
color: var(--td-button-default-outline-disabled-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}
.t-button--outline.t-button--disabled,
.t-button--outline.t-button--disabled::after {
border-color: var(--td-button-default-outline-disabled-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}
.t-button--outline.t-button--primary {
color: var(--td-button-primary-outline-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--outline.t-button--primary,
.t-button--outline.t-button--primary::after {
border-color: var(--td-button-primary-outline-border-color, var(--td-button-primary-outline-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9))));
}
.t-button--outline.t-button--primary.t-button--hover {
color: var(--td-button-primary-outline-active-border-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--outline.t-button--primary.t-button--hover::after {
background-color: var(--td-button-primary-outline-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-button-primary-outline-active-border-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--outline.t-button--primary.t-button--disabled {
background-color: transparent;
color: var(--td-button-primary-outline-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--outline.t-button--primary.t-button--disabled,
.t-button--outline.t-button--primary.t-button--disabled::after {
border-color: var(--td-button-primary-outline-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--outline.t-button--danger {
color: var(--td-button-danger-outline-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--outline.t-button--danger,
.t-button--outline.t-button--danger::after {
border-color: var(--td-button-danger-outline-border-color, var(--td-button-danger-outline-color, var(--td-error-color, var(--td-error-color-6, #d54941))));
}
.t-button--outline.t-button--danger.t-button--hover {
color: var(--td-button-danger-outline-active-border-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
}
.t-button--outline.t-button--danger.t-button--hover::after {
background-color: var(--td-button-danger-outline-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-button-danger-outline-active-border-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
}
.t-button--outline.t-button--danger.t-button--disabled {
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
color: var(--td-button-danger-outline-disabled-color, var(--td-error-color-3, #ffb9b0));
}
.t-button--outline.t-button--danger.t-button--disabled,
.t-button--outline.t-button--danger.t-button--disabled::after {
border-color: var(--td-button-danger-outline-disabled-color, var(--td-error-color-3, #ffb9b0));
}
.t-button--outline.t-button--light {
color: var(--td-button-light-outline-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-button-light-outline-bg-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-button--outline.t-button--light,
.t-button--outline.t-button--light::after {
border-color: var(--td-button-light-outline-border-color, var(--td-button-light-outline-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9))));
}
.t-button--outline.t-button--light.t-button--hover {
color: var(--td-button-light-outline-active-border-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--outline.t-button--light.t-button--hover,
.t-button--outline.t-button--light.t-button--hover::after {
background-color: var(--td-button-light-outline-active-bg-color, var(--td-brand-color-light-active, var(--td-primary-color-2, #d9e1ff)));
border-color: var(--td-button-light-outline-active-border-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--outline.t-button--light.t-button--disabled {
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
color: var(--td-button-light-outline-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--outline.t-button--light.t-button--disabled,
.t-button--outline.t-button--light.t-button--disabled::after {
border-color: var(--td-button-light-outline-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--dashed {
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
border-style: dashed;
border-width: 2rpx;
}
.t-button--dashed::after {
border: 0;
}
.t-button--dashed.t-button--hover,
.t-button--dashed.t-button--hover::after {
background-color: var(--td-button-default-outline-active-bg-color, var(--td-bg-color-container-active, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-button-default-outline-active-border-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}
.t-button--dashed.t-button--primary {
color: var(--td-button-primary-dashed-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--dashed.t-button--primary,
.t-button--dashed.t-button--primary::after {
border-color: var(--td-button-primary-dashed-border-color, var(--td-button-primary-dashed-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9))));
}
.t-button--dashed.t-button--primary.t-button--disabled {
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
color: var(--td-button-primary-dashed-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--dashed.t-button--primary.t-button--disabled,
.t-button--dashed.t-button--primary.t-button--disabled::after {
border-color: var(--td-button-primary-dashed-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-button--dashed.t-button--danger {
color: var(--td-button-danger-dashed-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--dashed.t-button--danger,
.t-button--dashed.t-button--danger::after {
border-color: var(--td-button-danger-dashed-border-color, var(--td-button-danger-dashed-color, var(--td-error-color, var(--td-error-color-6, #d54941))));
}
.t-button--dashed.t-button--danger.t-button--disabled {
background-color: transparent;
color: var(--td-button-danger-dashed-disabled-color, var(--td-button-danger-disabled-color, var(--td-font-white-1, #ffffff)));
}
.t-button--dashed.t-button--danger.t-button--disabled::after {
border-color: var(--td-button-danger-dashed-disabled-color, var(--td-button-danger-disabled-color, var(--td-font-white-1, #ffffff)));
}
.t-button--ghost {
background-color: transparent;
color: var(--td-button-ghost-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-button--ghost,
.t-button--ghost::after {
border-color: var(--td-button-ghost-border-color, var(--td-button-ghost-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff))));
}
.t-button--ghost.t-button--default.t-button--hover {
color: var(--td-button-ghost-hover-color, var(--td-font-white-2, rgba(255, 255, 255, 0.55)));
}
.t-button--ghost.t-button--default.t-button--hover,
.t-button--ghost.t-button--default.t-button--hover::after {
background-color: transparent;
border-color: var(--td-button-ghost-hover-color, var(--td-font-white-2, rgba(255, 255, 255, 0.55)));
}
.t-button--ghost.t-button--primary {
color: var(--td-button-ghost-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--ghost.t-button--primary,
.t-button--ghost.t-button--primary::after {
border-color: var(--td-button-ghost-primary-border-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-button--ghost.t-button--primary.t-button--hover {
color: var(--td-button-ghost-primary-hover-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--ghost.t-button--primary.t-button--hover,
.t-button--ghost.t-button--primary.t-button--hover::after {
background-color: transparent;
border-color: var(--td-button-ghost-primary-hover-color, var(--td-brand-color-active, var(--td-primary-color-8, #003cab)));
}
.t-button--ghost.t-button--primary.t-button--text.t-button--hover,
.t-button--ghost.t-button--primary.t-button--text.t-button--hover::after {
background-color: var(--td-gray-color-10, #4b4b4b);
}
.t-button--ghost.t-button--primary.t-button--disabled {
background-color: transparent;
color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button--ghost.t-button--primary.t-button--disabled,
.t-button--ghost.t-button--primary.t-button--disabled::after {
border-color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button--ghost.t-button--danger {
color: var(--td-button-ghost-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--ghost.t-button--danger,
.t-button--ghost.t-button--danger::after {
border-color: var(--td-button-ghost-danger-border-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-button--ghost.t-button--danger.t-button--hover {
color: var(--td-button-ghost-danger-hover-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
}
.t-button--ghost.t-button--danger.t-button--hover,
.t-button--ghost.t-button--danger.t-button--hover::after {
background-color: transparent;
border-color: var(--td-button-ghost-danger-hover-color, var(--td-error-color-active, var(--td-error-color-7, #ad352f)));
}
.t-button--ghost.t-button--danger.t-button--text.t-button--hover,
.t-button--ghost.t-button--danger.t-button--text.t-button--hover::after {
background-color: var(--td-gray-color-10, #4b4b4b);
}
.t-button--ghost.t-button--danger.t-button--disabled {
background-color: transparent;
color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button--ghost.t-button--danger.t-button--disabled,
.t-button--ghost.t-button--danger.t-button--disabled::after {
border-color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button--ghost.t-button--default.t-button--text.t-button--hover,
.t-button--ghost.t-button--default.t-button--text.t-button--hover::after {
background-color: var(--td-gray-color-10, #4b4b4b);
}
.t-button--ghost.t-button--default.t-button--disabled {
background-color: transparent;
color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button--ghost.t-button--default.t-button--disabled,
.t-button--ghost.t-button--default.t-button--disabled::after {
border-color: var(--td-button-ghost-disabled-color, var(--td-font-white-4, rgba(255, 255, 255, 0.22)));
}
.t-button__loading + .t-button__content:not(:empty),
.t-button__icon + .t-button__content:not(:empty) {
margin-left: 8rpx;
}
.t-button__icon {
border-radius: var(--td-button-icon-border-radius, 8rpx);
}
.t-button--round.t-button--size-large {
border-radius: calc(var(--td-button-large-height, 96rpx) / 2);
}
.t-button--round.t-button--size-large::after {
border-radius: var(--td-button-large-height, 96rpx);
}
.t-button--round.t-button--size-medium {
border-radius: calc(var(--td-button-medium-height, 80rpx) / 2);
}
.t-button--round.t-button--size-medium::after {
border-radius: var(--td-button-medium-height, 80rpx);
}
.t-button--round.t-button--size-small {
border-radius: calc(var(--td-button-small-height, 64rpx) / 2);
}
.t-button--round.t-button--size-small::after {
border-radius: var(--td-button-small-height, 64rpx);
}
.t-button--round.t-button--size-extra-small {
border-radius: calc(var(--td-button-extra-small-height, 56rpx) / 2);
}
.t-button--round.t-button--size-extra-small::after {
border-radius: var(--td-button-extra-small-height, 56rpx);
}
.t-button--square {
padding: 0;
}
.t-button--square.t-button--size-large {
width: var(--td-button-large-height, 96rpx);
}
.t-button--square.t-button--size-medium {
width: var(--td-button-medium-height, 80rpx);
}
.t-button--square.t-button--size-small {
width: var(--td-button-small-height, 64rpx);
}
.t-button--square.t-button--size-extra-small {
width: var(--td-button-extra-small-height, 56rpx);
}
.t-button--circle {
padding: 0;
border-radius: 50%;
}
.t-button--circle.t-button--size-large {
width: var(--td-button-large-height, 96rpx);
}
.t-button--circle.t-button--size-large::after {
border-radius: 50%;
}
.t-button--circle.t-button--size-medium {
width: var(--td-button-medium-height, 80rpx);
}
.t-button--circle.t-button--size-medium::after {
border-radius: 50%;
}
.t-button--circle.t-button--size-small {
width: var(--td-button-small-height, 64rpx);
}
.t-button--circle.t-button--size-small::after {
border-radius: 50%;
}
.t-button--circle.t-button--size-extra-small {
width: var(--td-button-extra-small-height, 56rpx);
}
.t-button--circle.t-button--size-extra-small::after {
border-radius: 50%;
}
.t-button--block {
display: flex;
width: 100%;
}
.t-button--disabled {
cursor: not-allowed;
}
.t-button__loading--wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.t-button.t-button--hover::after {
z-index: -1;
}

View File

@@ -0,0 +1,244 @@
<template>
<button
:id="tId"
:style="tools._style([customStyle])"
:data-custom="customDataset"
:class="className"
:form-type="disabled || loading ? '' : type"
:open-type="disabled || loading ? '' : openType"
:hover-stop-propagation="hoverStopPropagation"
:hover-start-time="hoverStartTime"
:hover-stay-time="hoverStayTime"
:lang="lang"
:session-from="sessionFrom"
:hover-class="disabled || loading ? '' : hoverClass || classPrefix + '--hover'"
:send-message-title="sendMessageTitle"
:send-message-path="sendMessagePath"
:send-message-img="sendMessageImg"
:app-parameter="appParameter"
:show-message-card="showMessageCard"
:aria-label="ariaLabel"
@click.stop.prevent="handleTap"
@getuserinfo="getuserinfo"
@contact="contact"
@getphonenumber="getphonenumber"
@error="error"
@opensetting="opensetting"
@launchapp="launchapp"
@chooseavatar="chooseavatar"
@agreeprivacyauthorization="agreeprivacyauthorization"
>
<block
v-if="_icon"
name="icon"
>
<t-icon
:custom-style="iconCustomStyle"
:t-class="classPrefix + '__icon ' + classPrefix + '__icon--' + (_icon.activeIdx == _icon.index ? 'active ' : ' ') + tClassIcon"
:prefix="_icon.prefix"
:name="_icon.name || ''"
:size="_icon.size"
:color="_icon.color"
@click="'handleClose' || ''"
/>
</block>
<t-loading
v-if="loading"
:delay="loadingProps.delay || 0"
:duration="loadingProps.duration || 800"
:indicator="loadingProps.indicator || true"
:inherit-color="loadingProps.inheritColor || true"
:layout="loadingProps.layout || 'horizontal'"
:pause="loadingProps.pause || false"
:progress="loadingProps.progress || 0"
:reverse="loadingProps.reverse || false"
:size="loadingProps.size || '40rpx'"
:text="loadingProps.text || ''"
:theme="loadingProps.theme || 'circular'"
loading
:t-class="classPrefix + '__loading ' + classPrefix + '__loading--wrapper'"
:t-class-indicator="classPrefix + '__loading--indicator ' + tClassLoading"
:custom-style="loadingCustomStyle"
/>
<view
:class="classPrefix + '__content '
+ ((_icon && _icon.name || loading) && content ? classPrefix + '__content--has-icon' : '')"
>
<slot name="content" />
<block v-if="content">
{{ content }}
</block>
<slot />
</view>
<slot name="suffix" />
</button>
</template>
<script>
import TIcon from '../icon/icon';
import TLoading from '../loading/loading';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { calcIcon } from '../common/utils';
import tools from '../common/utils.wxs';
const name = `${prefix}-button`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-icon`,
`${prefix}-class-loading`,
],
components: {
TIcon,
TLoading,
},
props: {
...props,
},
emits: [
'click',
],
data() {
return {
tools,
prefix,
className: '',
classPrefix: name,
_icon: undefined,
};
},
computed: {
iconCustomStyle() {
const fontSize = {
'extra-small': 'var(--td-button-extra-small-icon-font-size, 18px)',
small: 'var(--td-button-small-icon-font-size, 18px)',
medium: 'var(--td-button-medium-icon-font-size, 20px)',
large: 'var(--td-button-large-icon-font-size, 24px)',
};
return tools._style([
{
fontSize: fontSize[this.size || 'medium'],
borderRadius: 'var(--td-button-icon-border-radius, 4px)',
},
this._icon.style || '',
]);
},
loadingCustomStyle() {
return tools._style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});
},
},
watch: {
icon: {
handler(value) {
this._icon = calcIcon(value, '');
},
immediate: true,
},
theme: 'setClass',
size: 'setClass',
plain: 'setClass',
block: 'setClass',
shape: 'setClass',
disabled: 'setClass',
loading: 'setClass',
variant: 'setClass',
},
mounted() {
this.setClass();
},
methods: {
setClass() {
const t = [
name,
this.tClass,
`${name}--${this.variant || 'base'}`,
`${name}--${this.theme || 'default'}`,
`${name}--${this.shape || 'rectangle'}`,
`${name}--size-${this.size || 'medium'}`,
];
if (this.block) {
t.push(`${name}--block`);
}
if (this.disabled) {
t.push(`${name}--disabled`);
}
if (this.ghost) {
t.push(`${name}--ghost`);
}
this.className = t.join(' ');
},
getuserinfo(t) {
this.$emit('getuserinfo', t);
},
contact(t) {
this.$emit('contact', t);
},
getphonenumber(t) {
this.$emit('getphonenumber', t);
},
error(t) {
this.$emit('error', t);
},
opensetting(t) {
this.$emit('opensetting', t);
},
launchapp(t) {
this.$emit('launchapp', t);
},
chooseavatar(t) {
this.$emit('chooseavatar', t);
},
agreeprivacyauthorization(t) {
this.$emit('agreeprivacyauthorization', t);
},
handleTap(t) {
if (this.disabled || this.loading) return;
this.$emit('click', t);
},
},
});
</script>
<style scoped>
@import './button.css';
/* #ifdef MP-QQ */
/* 适配 qq 小程序 */
.t-button--outline {
border-style: solid;
border-width: 1px;
}
.t-button--ghost {
border-style: solid;
border-width: 1px;
}
/* #endif */
/* #ifdef H5 || MP-WEIXIN */
:deep(.t-button__loading) + .t-button__content:not(:empty) {
margin-left: 4px;
}
:deep(.t-button__icon) + .t-button__content:not(:empty) {
margin-left: 4px;
}
/* #endif */
.t-button {
/* support my-alipay */
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -0,0 +1,207 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdButtonProps } from './type';
export default {
/** 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效 */
appParameter: {
type: String,
default: '',
},
/** 是否为块级元素 */
block: Boolean,
/** 按钮内容 */
content: {
type: String,
},
/** 自定义 dataset可通过 event.currentTarget.dataset.custom 获取 */
customDataset: {
type: [String, Number, Boolean, Object, Array],
default: () => ({}) as TdButtonProps['customDataset'],
},
/** 禁用状态。优先级Button.disabled > Form.disabled */
disabled: {
type: Boolean,
default: undefined,
},
/** 是否为幽灵按钮(镂空按钮) */
ghost: Boolean,
/** 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果 */
hoverClass: {
type: String,
default: '',
},
/** 按住后多久出现点击态,单位毫秒 */
hoverStartTime: {
type: Number,
default: 20,
},
/** 手指松开后点击态保留时间,单位毫秒 */
hoverStayTime: {
type: Number,
default: 70,
},
/** 指定是否阻止本节点的祖先节点出现点击态 */
hoverStopPropagation: Boolean,
/** 图标名称。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon` */
icon: {
type: [String, Object],
},
/** 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) */
lang: {
type: String,
validator(val: TdButtonProps['lang']): boolean {
if (!val) return true;
return ['en', 'zh_CN', 'zh_TW'].includes(val);
},
},
/** 是否显示为加载状态 */
loading: Boolean,
/** 透传 Loading 组件全部属性 */
loadingProps: {
type: Object,
default: () => ({}),
},
/** 微信开放能力。<br />具体释义:<br />`contact` 打开客服会话,如果用户在会话中点击消息卡片后返回小程序,可以从 bindcontact 回调中获得具体信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/customer-message.html">具体说明</a> *鸿蒙 OS 暂不支持*<br />`liveActivity` 通过前端获取<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message-2.html">新的一次性订阅消息下发机制</a>使用的 code<br />`share` 触发用户转发,使用前建议先阅读<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html#使用指引">使用指引</a><br />`getPhoneNumber` 获取用户手机号,可以从 bindgetphonenumber 回调中获取到用户信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html">具体说明</a> *小程序插件中不能使用*<br />`getUserInfo` 获取用户信息,可以从 bindgetuserinfo 回调中获取到用户信息 *小程序插件中不能使用*<br />`launchApp` 打开APP可以通过 app-parameter 属性设定向 APP 传的参数<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/launchApp.html">具体说明</a><br />`openSetting` 打开授权设置页;<br />`feedback` 打开“意见反馈”页面,用户可提交反馈内容并上传<a href="https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.getLogManager.html">日志</a>,开发者可以登录<a href="https://mp.weixin.qq.com/">小程序管理后台</a>后进入左侧菜单“客服反馈”页面获取到反馈内容;<br />`chooseAvatar` 获取用户头像,可以从 bindchooseavatar 回调中获取到头像信息;<br />`agreePrivacyAuthorization`用户同意隐私协议按钮。用户点击一次此按钮后,所有隐私接口可以正常调用。可通过`bindagreeprivacyauthorization`监听用户同意隐私协议事件。隐私合规开发指南详情可见《<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html">小程序隐私协议开发指南</a>》。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) */
openType: {
type: String,
validator(val: TdButtonProps['openType']): boolean {
if (!val) return true;
return ['contact', 'share', 'getPhoneNumber', 'getUserInfo', 'launchApp', 'openSetting', 'feedback', 'chooseAvatar', 'agreePrivacyAuthorization'].includes(val);
},
},
/** 原生按钮属性当手机号快速验证或手机号实时验证额度用尽时是否对用户展示“申请获取你的手机号但该功能使用次数已达当前小程序上限暂时无法使用”的提示默认展示open-type="getPhoneNumber" 或 open-type="getRealtimePhoneNumber" 时有效 */
phoneNumberNoQuotaToast: {
type: Boolean,
default: true,
},
/** 会话内消息卡片图片open-type="contact"时有效 */
sendMessageImg: {
type: String,
default: '截图',
},
/** 会话内消息卡片点击跳转小程序路径open-type="contact"时有效 */
sendMessagePath: {
type: String,
default: '当前分享路径',
},
/** 会话内消息卡片标题open-type="contact"时有效 */
sendMessageTitle: {
type: String,
default: '当前标题',
},
/** 会话来源open-type="contact"时有效 */
sessionFrom: {
type: String,
default: '',
},
/** 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形 */
shape: {
type: String,
default: 'rectangle' as TdButtonProps['shape'],
validator(val: TdButtonProps['shape']): boolean {
if (!val) return true;
return ['rectangle', 'square', 'round', 'circle'].includes(val);
},
},
/** 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息open-type="contact"时有效 */
showMessageCard: Boolean,
/** 组件尺寸 */
size: {
type: String,
default: 'medium' as TdButtonProps['size'],
validator(val: TdButtonProps['size']): boolean {
if (!val) return true;
return ['extra-small', 'small', 'medium', 'large'].includes(val);
},
},
/** 按钮标签id */
tId: {
type: String,
default: '',
},
/** 组件风格,依次为品牌色、危险色 */
theme: {
type: String,
default: 'default' as TdButtonProps['theme'],
validator(val: TdButtonProps['theme']): boolean {
if (!val) return true;
return ['default', 'primary', 'danger', 'light'].includes(val);
},
},
/** 同小程序的 formType */
type: {
type: String,
validator(val: TdButtonProps['type']): boolean {
if (!val) return true;
return ['submit', 'reset'].includes(val);
},
},
/** 按钮形式,基础、线框、虚线、文字 */
variant: {
type: String,
default: 'base' as TdButtonProps['variant'],
validator(val: TdButtonProps['variant']): boolean {
if (!val) return true;
return ['base', 'outline', 'dashed', 'text'].includes(val);
},
},
/** 原生按钮属性用户同意隐私协议事件回调open-type=agreePrivacyAuthorization时有效 Tips: 如果使用 onNeedPrivacyAuthorization 接口,需要在 bindagreeprivacyauthorization 触发后再调用 resolve({ event: "agree", buttonId }) */
onAgreeprivacyauthorization: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性,获取用户头像回调,`open-type=chooseAvatar` 时有效。返回 `e.detail.avatarUrl` 为头像临时文件链接 */
onChooseavatar: {
type: Function,
default: () => ({}),
},
/** 点击时触发 */
onClick: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性,客服消息回调,`open-type="contact"` 时有效 */
onContact: {
type: Function,
default: () => ({}),
},
/** 新的一次性订阅消息下发机制回调,`open-type=liveActivity` 时有效 */
onCreateliveactivity: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性,当使用开放能力时,发生错误的回调,`open-type=launchApp` 时有效 */
onError: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性手机号快速验证回调open-type=getPhoneNumber时有效。Tips在触发 bindgetphonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用 */
onGetphonenumber: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性手机号实时验证回调open-type=getRealtimePhoneNumber 时有效。Tips在触发 bindgetrealtimephonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用 */
onGetrealtimephonenumber: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性用户点击该按钮时会返回获取到的用户信息回调的detail数据与wx.getUserInfo返回的一致open-type="getUserInfo"时有效 */
onGetuserinfo: {
type: Function,
default: () => ({}),
},
/** 打开 APP 成功的回调,`open-type=launchApp` 时有效 */
onLaunchapp: {
type: Function,
default: () => ({}),
},
/** 原生按钮属性在打开授权设置页后回调open-type=openSetting时有效 */
onOpensetting: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,192 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdLoadingProps as LoadingProps } from '../loading/type';
export interface TdButtonProps {
/**
* 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效
* @default ''
*/
appParameter?: string;
/**
* 是否为块级元素
* @default false
*/
block?: boolean;
/**
* 按钮内容
*/
content?: string;
/**
* 自定义 dataset可通过 event.currentTarget.dataset.custom 获取
* @default {}
*/
customDataset?: string | number | boolean | object | Array<string | number | boolean | object>;
/**
* 禁用状态。优先级Button.disabled > Form.disabled
*/
disabled?: boolean;
/**
* 是否为幽灵按钮(镂空按钮)
* @default false
*/
ghost?: boolean;
/**
* 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果
* @default ''
*/
hoverClass?: string;
/**
* 按住后多久出现点击态,单位毫秒
* @default 20
*/
hoverStartTime?: number;
/**
* 手指松开后点击态保留时间,单位毫秒
* @default 70
*/
hoverStayTime?: number;
/**
* 指定是否阻止本节点的祖先节点出现点击态
* @default false
*/
hoverStopPropagation?: boolean;
/**
* 图标名称。值为字符串表示图标名称,值为 `Object` 类型,表示透传至 `icon`
*/
icon?: string | object;
/**
* 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)
*/
lang?: 'en' | 'zh_CN' | 'zh_TW';
/**
* 是否显示为加载状态
* @default false
*/
loading?: boolean;
/**
* 透传 Loading 组件全部属性
* @default {}
*/
loadingProps?: LoadingProps;
/**
* 微信开放能力。<br />具体释义:<br />`contact` 打开客服会话,如果用户在会话中点击消息卡片后返回小程序,可以从 bindcontact 回调中获得具体信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/customer-message.html">具体说明</a> *鸿蒙 OS 暂不支持*<br />`liveActivity` 通过前端获取<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message-2.html">新的一次性订阅消息下发机制</a>使用的 code<br />`share` 触发用户转发,使用前建议先阅读<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html#使用指引">使用指引</a><br />`getPhoneNumber` 获取用户手机号,可以从 bindgetphonenumber 回调中获取到用户信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html">具体说明</a> *小程序插件中不能使用*<br />`getUserInfo` 获取用户信息,可以从 bindgetuserinfo 回调中获取到用户信息 *小程序插件中不能使用*<br />`launchApp` 打开APP可以通过 app-parameter 属性设定向 APP 传的参数<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/launchApp.html">具体说明</a><br />`openSetting` 打开授权设置页;<br />`feedback` 打开“意见反馈”页面,用户可提交反馈内容并上传<a href="https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.getLogManager.html">日志</a>,开发者可以登录<a href="https://mp.weixin.qq.com/">小程序管理后台</a>后进入左侧菜单“客服反馈”页面获取到反馈内容;<br />`chooseAvatar` 获取用户头像,可以从 bindchooseavatar 回调中获取到头像信息;<br />`agreePrivacyAuthorization`用户同意隐私协议按钮。用户点击一次此按钮后,所有隐私接口可以正常调用。可通过`bindagreeprivacyauthorization`监听用户同意隐私协议事件。隐私合规开发指南详情可见《<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html">小程序隐私协议开发指南</a>》。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)
*/
openType?:
| 'contact'
| 'share'
| 'getPhoneNumber'
| 'getUserInfo'
| 'launchApp'
| 'openSetting'
| 'feedback'
| 'chooseAvatar'
| 'agreePrivacyAuthorization';
/**
* 原生按钮属性当手机号快速验证或手机号实时验证额度用尽时是否对用户展示“申请获取你的手机号但该功能使用次数已达当前小程序上限暂时无法使用”的提示默认展示open-type="getPhoneNumber" 或 open-type="getRealtimePhoneNumber" 时有效
* @default true
*/
phoneNumberNoQuotaToast?: boolean;
/**
* 会话内消息卡片图片open-type="contact"时有效
* @default 截图
*/
sendMessageImg?: string;
/**
* 会话内消息卡片点击跳转小程序路径open-type="contact"时有效
* @default 当前分享路径
*/
sendMessagePath?: string;
/**
* 会话内消息卡片标题open-type="contact"时有效
* @default 当前标题
*/
sendMessageTitle?: string;
/**
* 会话来源open-type="contact"时有效
* @default ''
*/
sessionFrom?: string;
/**
* 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形
* @default rectangle
*/
shape?: 'rectangle' | 'square' | 'round' | 'circle';
/**
* 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息open-type="contact"时有效
* @default false
*/
showMessageCard?: boolean;
/**
* 组件尺寸
* @default medium
*/
size?: 'extra-small' | 'small' | 'medium' | 'large';
/**
* 按钮标签id
* @default ''
*/
tId?: string;
/**
* 组件风格,依次为品牌色、危险色
* @default default
*/
theme?: 'default' | 'primary' | 'danger' | 'light';
/**
* 同小程序的 formType
*/
type?: 'submit' | 'reset';
/**
* 按钮形式,基础、线框、虚线、文字
* @default base
*/
variant?: 'base' | 'outline' | 'dashed' | 'text';
/**
* 原生按钮属性用户同意隐私协议事件回调open-type=agreePrivacyAuthorization时有效 Tips: 如果使用 onNeedPrivacyAuthorization 接口,需要在 bindagreeprivacyauthorization 触发后再调用 resolve({ event: "agree", buttonId })
*/
onAgreeprivacyauthorization?: () => void;
/**
* 原生按钮属性,获取用户头像回调,`open-type=chooseAvatar` 时有效。返回 `e.detail.avatarUrl` 为头像临时文件链接
*/
onChooseavatar?: () => void;
/**
* 点击时触发
*/
onClick?: (e: MouseEvent) => void;
/**
* 原生按钮属性,客服消息回调,`open-type="contact"` 时有效
*/
onContact?: () => void;
/**
* 新的一次性订阅消息下发机制回调,`open-type=liveActivity` 时有效
*/
onCreateliveactivity?: () => void;
/**
* 原生按钮属性,当使用开放能力时,发生错误的回调,`open-type=launchApp` 时有效
*/
onError?: () => void;
/**
* 原生按钮属性手机号快速验证回调open-type=getPhoneNumber时有效。Tips在触发 bindgetphonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用
*/
onGetphonenumber?: () => void;
/**
* 原生按钮属性手机号实时验证回调open-type=getRealtimePhoneNumber 时有效。Tips在触发 bindgetrealtimephonenumber 回调后应立即隐藏手机号按钮组件,或置为 disabled 状态,避免用户重复授权手机号产生额外费用
*/
onGetrealtimephonenumber?: () => void;
/**
* 原生按钮属性用户点击该按钮时会返回获取到的用户信息回调的detail数据与wx.getUserInfo返回的一致open-type="getUserInfo"时有效
*/
onGetuserinfo?: () => void;
/**
* 打开 APP 成功的回调,`open-type=launchApp` 时有效
*/
onLaunchapp?: () => void;
/**
* 原生按钮属性在打开授权设置页后回调open-type=openSetting时有效
*/
onOpensetting?: () => void;
}

View File

@@ -0,0 +1,63 @@
:: BASE_DOC ::
## API
### Calendar Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
allow-same-day | Boolean | false | \- | N
auto-close | Boolean | true | \- | N
confirm-btn | String / Object | '' | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
first-day-of-week | Number | 0 | \- | N
format | Function | - | Typescript: `CalendarFormatType ` `type CalendarFormatType = (day: TDate) => TDate` `type TDateType = 'selected' \| 'disabled' \| 'start' \| 'start-end' \|'centre' \| 'end' \| ''` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string;}`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
locale-text | Object | - | Typescript: `CalendarLocaleText` `interface CalendarLocaleText {title?: string; weekdays?: string[]; monthTitle?: string; months?: string[]; confirm?: string;}`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
max-date | Number | - | \- | N
min-date | Number | - | \- | N
readonly | Boolean | - | \- | N
switch-mode | String | none | options: none/month/year-month | N
title | String | - | \- | N
type | String | single | options: single/multiple/range | N
use-popup | Boolean | true | \- | N
using-custom-navbar | Boolean | false | \- | N
value | Number / Array | - | `v-model:value` is supported。Typescript: `number \| number[]` | N
default-value | Number / Array | - | uncontrolled property。Typescript: `number \| number[]` | N
visible | Boolean | false | \- | N
### Calendar Events
name | params | description
-- | -- | --
change | `(context: { value: number \| number[] })` | \-
close | `(context: { trigger: CalendarTrigger })` | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts)。<br/>`type CalendarTrigger = 'close-btn' \| 'confirm-btn' \| 'overlay' \| 'auto-close'`<br/>
confirm | `(context: { value: number \| number[] })` | \-
panel-change | `(context: { year: number, month: number })` | \-
scroll | `(context: {scrollLeft: number, scrollTop: number, scrollHeight: number, scrollWidth: number, deltaX: number, deltaY: number})` | triggered when scrolling
select | `(context: { value: number \| number[] })` | \-
### Calendar Slots
name | Description
-- | --
confirm-btn | \-
title | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-calendar-active-color | @brand-color | -
--td-calendar-bg-color | @bg-color-container | -
--td-calendar-days-color | @text-color-secondary | -
--td-calendar-item-centre-color | @brand-color-light | -
--td-calendar-item-disabled-color | @text-color-disabled | -
--td-calendar-item-suffix-color | @text-color-placeholder | -
--td-calendar-radius | 24rpx | -
--td-calendar-selected-border-radius | @radius-default | -
--td-calendar-selected-color | @text-color-anti | -
--td-calendar-switch-mode-icon-color | @text-color-secondary | -
--td-calendar-switch-mode-icon-disabled-color | @text-color-disabled | -
--td-calendar-title-color | @text-color-primary | -
--td-calendar-title-font | @font-title-large | -

View File

@@ -0,0 +1,113 @@
---
title: Calendar 日历
description: 按照日历形式展示数据或日期的容器。
spline: form
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCalendar from '@tdesign/uniapp/calendar/calendar.vue';
```
### 组件类型
#### 单个选择日历
{{ base }}
#### 多个选择日历
{{ multiple }}
#### 带单行/双行描述的日历
{{ custom-text }}
#### 带翻页功能的日历
{{ switch-mode }}
#### 可选择区间日期的日历
{{ range }}
### 组件样式
#### 国际化
{{ local-text }}
#### 含不可选的日历
{{ custom-range }}
#### 不使用 Popup
{{ without-popup }}
## API
### Calendar Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
allow-same-day | Boolean | false | 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效 | N
auto-close | Boolean | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible | N
confirm-btn | String / Object | '' | 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
first-day-of-week | Number | 0 | 第一天从星期几开始,默认 0 = 周日 | N
format | Function | - | 用于格式化日期的函数。TS 类型:`CalendarFormatType ` `type CalendarFormatType = (day: TDate) => TDate` `type TDateType = 'selected' \| 'disabled' \| 'start' \| 'start-end' \|'centre' \| 'end' \| ''` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string;}`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
locale-text | Object | - | 国际化文案。TS 类型:`CalendarLocaleText` `interface CalendarLocaleText {title?: string; weekdays?: string[]; monthTitle?: string; months?: string[]; confirm?: string;}`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
max-date | Number | - | 最大可选的日期,不传则默认半年后 | N
min-date | Number | - | 最小可选的日期,不传则默认今天 | N
readonly | Boolean | - | 是否只读,只读状态下不能选择日期 | N
switch-mode | String | none | 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换也支持按月切换。可选项none/month/year-month | N
title | String | - | 标题,不传默认为“请选择日期” | N
type | String | single | 日历的选择类型single = 单选multiple = 多选; range = 区间选择。可选项single/multiple/range | N
use-popup | Boolean | true | 是否使用弹出层包裹日历 | N
using-custom-navbar | Boolean | false | 是否使用了自定义导航栏 | N
value | Number / Array | - | 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组。支持语法糖 `v-model:value`。TS 类型:`number \| number[]` | N
default-value | Number / Array | - | 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组。非受控属性。TS 类型:`number \| number[]` | N
visible | Boolean | false | 是否显示日历;`usePopup` 为 true 时有效 | N
### Calendar Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { value: number \| number[] })` | 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple
close | `(context: { trigger: CalendarTrigger })` | 关闭按钮时触发。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts)。<br/>`type CalendarTrigger = 'close-btn' \| 'confirm-btn' \| 'overlay' \| 'auto-close'`<br/>
confirm | `(context: { value: number \| number[] })` | 点击确认按钮时触发
panel-change | `(context: { year: number, month: number })` | 切换月或年时触发switch-mode 不为 none 时有效)
scroll | `(context: {scrollLeft: number, scrollTop: number, scrollHeight: number, scrollWidth: number, deltaX: number, deltaY: number})` | 滚动时触发
select | `(context: { value: number \| number[] })` | 点击日期时触发
### Calendar Slots
名称 | 描述
-- | --
confirm-btn | 确认按钮
title | 标题
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-calendar-active-color | @brand-color | -
--td-calendar-bg-color | @bg-color-container | -
--td-calendar-days-color | @text-color-secondary | -
--td-calendar-item-centre-color | @brand-color-light | -
--td-calendar-item-disabled-color | @text-color-disabled | -
--td-calendar-item-suffix-color | @text-color-placeholder | -
--td-calendar-radius | 24rpx | -
--td-calendar-selected-border-radius | @radius-default | -
--td-calendar-selected-color | @text-color-anti | -
--td-calendar-switch-mode-icon-color | @text-color-secondary | -
--td-calendar-switch-mode-icon-disabled-color | @text-color-disabled | -
--td-calendar-title-color | @text-color-primary | -
--td-calendar-title-font | @font-title-large | -

View File

@@ -0,0 +1,42 @@
export default {
tClass: {
type: String,
default: '',
},
classPrefix: {
type: String,
default: '',
},
switchMode: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
preYearBtnDisable: {
type: Boolean,
},
prevMonthBtnDisable: {
type: Boolean,
},
nextYearBtnDisable: {
type: Boolean,
},
nextMonthBtnDisable: {
type: Boolean,
},
tId: {
type: String,
default: '',
},
// realLocalText: {
// type: Object,
// default: () => ({}),
// },
// currentMonth: {
// type: Array,
// default: () => ([]),
// },
};

View File

@@ -0,0 +1,98 @@
<template>
<view
:id="tId"
:class="' ' + tClass + ' ' + classPrefix + ' '+(switchMode !== 'none' ? classPrefix + '__with-action' : '')"
>
<view
v-if="switchMode !== 'none'"
:class="classPrefix + '__action'"
>
<view
v-if="switchMode === 'year-month'"
:class="utils.cls(classPrefix + '__icon', [['disabled', preYearBtnDisable]])"
:data-disabled="preYearBtnDisable"
data-type="pre-year"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-left-double" />
</view>
<view
:class="utils.cls(classPrefix + '__icon', [['disabled', prevMonthBtnDisable]])"
:data-disabled="prevMonthBtnDisable"
data-type="pre-month"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-left" />
</view>
</view>
<view :class="classPrefix + '__title'">
{{ title }}
</view>
<view
v-if="switchMode !== 'none'"
:class="classPrefix + '__action'"
>
<view
:class="utils.cls(classPrefix + '__icon', [['disabled', nextMonthBtnDisable]])"
:data-disabled="nextMonthBtnDisable"
data-type="next-month"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-right" />
</view>
<view
v-if="switchMode === 'year-month'"
:class="utils.cls(classPrefix + '__icon', [['disabled', nextYearBtnDisable]])"
:data-disabled="nextYearBtnDisable"
data-type="next-year"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-right-double" />
</view>
</view>
</view>
</template>
<script>
import TIcon from '../icon/icon.vue';
import utils from '../common/utils.wxs';
import props from './calendar-header.props';
import { getMonthTitle } from './computed';
export default {
name: 'TCalendarHeader',
options: {
styleIsolation: 'shared',
},
components: {
TIcon,
},
props: {
...props,
},
emits: [
'handleSwitchModeChange',
],
data() {
return {
utils,
};
},
watch: {
},
mounted() {
},
methods: {
handleSwitchModeChange(...args) {
this.$emit('handleSwitchModeChange', ...args);
},
getMonthTitle,
},
};
</script>
<style scoped>
@import './calendar.css';
</style>

View File

@@ -0,0 +1,205 @@
.t-calendar {
width: inherit;
position: relative;
z-index: 9999;
background: var(--td-calendar-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
overflow-x: hidden;
}
.t-calendar--popup {
border-top-left-radius: var(--td-calendar-radius, 24rpx);
border-top-right-radius: var(--td-calendar-radius, 24rpx);
}
.t-calendar__title {
display: flex;
align-items: center;
justify-content: center;
font: var(--td-calendar-title-font, var(--td-font-title-large, 600 36rpx / 52rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
padding: var(--td-spacer-2, 32rpx);
}
.t-calendar__title:focus {
outline: 0;
}
.t-calendar__close-btn {
position: absolute;
top: var(--td-spacer-2, 32rpx);
right: var(--td-spacer-2, 32rpx);
margin: calc(-1 * var(--td-spacer-1, 24rpx));
padding: var(--td-spacer-1, 24rpx);
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-calendar__days {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-column-gap: 8rpx;
padding: 0 32rpx;
text-align: center;
line-height: 92rpx;
}
.t-calendar__days-item {
height: 92rpx;
font-size: 28rpx;
color: var(--td-calendar-days-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
}
.t-calendar__content {
min-height: 400rpx;
display: flex;
flex-direction: column;
}
.t-calendar__month {
font: var(--td-font-title-small, 600 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
padding: 32rpx 0 0;
}
.t-calendar__months {
height: 712rpx;
padding: 0 32rpx 32rpx;
box-sizing: border-box;
}
.t-calendar__months::-webkit-scrollbar {
display: none;
}
.t-calendar__dates {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-column-gap: 8rpx;
}
.t-calendar__dates-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
font: var(--td-font-title-medium, 600 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
height: 120rpx;
margin-top: 16rpx;
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
cursor: pointer;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
}
.t-calendar__dates-item-prefix,
.t-calendar__dates-item-suffix {
position: absolute;
font: var(--td-font-body-extraSmall, 20rpx / 32rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
width: 100%;
text-align: center;
}
.t-calendar__dates-item-prefix {
top: 8rpx;
}
.t-calendar__dates-item-suffix {
bottom: 8rpx;
color: var(--td-calendar-item-suffix-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
}
.t-calendar__dates-item-suffix--selected,
.t-calendar__dates-item-suffix--start,
.t-calendar__dates-item-suffix--end {
color: var(--td-calendar-selected-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-calendar__dates-item-suffix--disabled {
color: var(--td-calendar-item-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-calendar__dates-item--selected,
.t-calendar__dates-item--start-end,
.t-calendar__dates-item--start,
.t-calendar__dates-item--end {
background: var(--td-calendar-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
color: var(--td-calendar-selected-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
}
.t-calendar__dates-item--start {
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) 0 0 var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
}
.t-calendar__dates-item--end {
border-radius: 0 var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) 0;
}
.t-calendar__dates-item--start + .t-calendar__dates-item--end::before {
content: '';
display: block;
position: absolute;
top: 0;
width: 8rpx;
height: 100%;
background: var(--td-calendar-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-calendar__dates-item--start + .t-calendar__dates-item--end:before {
left: -8rpx;
}
.t-calendar__dates-item--centre {
border-radius: 0;
background-color: var(--td-calendar-item-centre-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-calendar__dates-item--centre::before,
.t-calendar__dates-item--centre::after {
content: '';
display: block;
position: absolute;
top: 0;
width: 8rpx;
height: 100%;
background-color: var(--td-calendar-item-centre-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-calendar__dates-item--centre:before {
left: -8rpx;
}
.t-calendar__dates-item--centre:after {
right: -8rpx;
}
.t-calendar__dates-item--disabled {
color: var(--td-calendar-item-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
cursor: default;
}
.t-calendar__footer {
padding: 32rpx;
}
.t-calendar-switch-mode--none > .t-calendar__months {
height: 60vh;
}
.t-calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.t-calendar-header__with-action {
padding: 0rpx 32rpx 16rpx 32rpx;
box-sizing: border-box;
position: relative;
}
.t-calendar-header__with-action::after {
content: '';
display: block;
position: absolute;
top: unset;
bottom: 0;
left: unset;
right: unset;
background-color: var(--td-border-color, var(--td-gray-color-3, #e7e7e7));
}
.t-calendar-header__with-action::after {
height: 1px;
left: 0;
right: 0;
transform: scaleY(0.5);
}
.t-calendar-header__with-action .t-calendar-header__title {
flex: 1;
text-align: center;
font: var(--td-font-title-small, 600 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
}
.t-calendar-header__action {
display: flex;
font-size: 40rpx;
color: var(--td-calendar-switch-mode-icon-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
}
.t-calendar-header__icon {
padding: 16rpx;
}
.t-calendar-header__icon--disabled {
color: var(--td-calendar-switch-mode-icon-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-calendar-header__title {
text-align: left;
}

View File

@@ -0,0 +1,454 @@
<template>
<view>
<t-popup
v-if="usePopup"
:visible="visible"
:using-custom-navbar="usingCustomNavbar"
:custom-navbar-height="customNavbarHeight"
placement="bottom"
@visible-change="onVisibleChange"
>
<CalendarTemplate
:class-prefix="classPrefix"
:use-popup="usePopup"
:switch-mode="switchMode"
:t-class="tClass"
:custom-style="tools._style([customStyle])"
:title="title"
:real-local-text="realLocalText"
:months="months"
:current-month="currentMonth"
:action-buttons="actionButtons"
:days="days"
:scroll-into-view="scrollIntoView"
:first-day-of-week="firstDayOfWeek"
:inner-confirm-btn="innerConfirmBtn"
@scroll="onScroll"
@close="handleClose"
@select="handleSelect"
@clickButton="onTplButtonTap"
@handleSwitchModeChange="handleSwitchModeChange"
>
<template #confirm-btn>
<slot name="confirm-btn" />
</template>
<template #title>
<slot name="title" />
</template>
</CalendarTemplate>
</t-popup>
<block v-else>
<CalendarTemplate
:class-prefix="classPrefix"
:use-popup="usePopup"
:switch-mode="switchMode"
:t-class="tClass"
:custom-style="tools._style([customStyle])"
:title="title"
:real-local-text="realLocalText"
:months="months"
:current-month="currentMonth"
:action-buttons="actionButtons"
:days="days"
:scroll-into-view="scrollIntoView"
:first-day-of-week="firstDayOfWeek"
:inner-confirm-btn="innerConfirmBtn"
@scroll="onScroll"
@close="handleClose"
@select="handleSelect"
@clickButton="onTplButtonTap"
@handleSwitchModeChange="handleSwitchModeChange"
>
<template #confirm-btn>
<slot name="confirm-btn" />
</template>
<template #title>
<slot name="title" />
</template>
</CalendarTemplate>
</block>
</view>
</template>
<script>
import TPopup from '../popup/popup';
import TButton from '../button/button';
import TIcon from '../icon/icon';
import CalendarTemplate from './template.vue';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import { coalesce } from '../common/utils';
import props from './props';
import TCalendar from '../common/shared/calendar/index';
import useCustomNavbar from '../mixins/using-custom-navbar';
import { getPrevMonth, getPrevYear, getNextMonth, getNextYear } from './utils';
import tools from '../common/utils.wxs';
import {
getMonthTitle,
getDateLabel,
isDateSelected,
} from './computed.js';
const name = `${prefix}-calendar`;
const defaultLocaleText = {
title: '请选择日期',
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
monthTitle: '{year} 年 {month}',
months: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'],
confirm: '确认',
};
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'value',
event: 'confirm',
},
{
key: 'value',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
],
mixins: [useCustomNavbar],
components: {
TPopup,
TButton,
TIcon,
CalendarTemplate,
},
props: {
...props,
},
emits: [
'update:visible',
],
data() {
return {
prefix,
classPrefix: name,
months: [],
scrollIntoView: '',
innerConfirmBtn: {},
realLocalText: {},
currentMonth: {},
actionButtons: {
preYearBtnDisable: false,
prevMonthBtnDisable: false,
nextMonthBtnDisable: false,
nextYearBtnDisable: false,
},
tools,
dataVisible: this.visible,
dataValue: coalesce(this.value, this.defaultValue),
days: [],
};
},
watch: {
type: {
handler(v) {
this.base.type = v;
},
},
allowSameDay(v) {
this.base.allowSameDay = v;
},
confirmBtn: {
handler(v) {
if (typeof v === 'string') {
this.innerConfirmBtn = v === 'slot' ? 'slot' : { content: v };
} else if (typeof v === 'object') {
this.innerConfirmBtn = v;
}
},
immediate: true,
},
firstDayOfWeek: 'onWatchMinMaxDate',
minDate: 'onWatchMinMaxDate',
maxDate: 'onWatchMinMaxDate',
value: {
handler(v) {
this.dataValue = v;
},
immediate: true,
deep: true,
},
visible: {
handler(v) {
this.dataVisible = v;
},
immediate: true,
},
dataValue: {
handler(v) {
this.base.value = v;
this.calcMonths();
this.updateCurrentMonth(Array.isArray(v) ? v[0] : v);
},
deep: true,
},
dataVisible: {
handler(v) {
if (v) {
this.onScrollIntoView();
this.base.value = this.dataValue;
this.calcMonths();
}
},
immediate: true,
},
format: {
handler(v) {
const { usePopup, dataVisible: visible } = this;
if (this.base) {
this.base.format = v;
}
if (!usePopup || visible) {
this.calcMonths();
}
},
immediate: true,
},
},
created() {
const values = Object.keys(props).reduce((acc, key) => ({
...acc,
[key]: this[key],
}));
this.base = new TCalendar(values);
},
mounted() {
const realLocalText = { ...defaultLocaleText, ...this.localeText };
this.initialValue();
this.onWatchMinMaxDate();
this.days = this.base.getDays(realLocalText.weekdays);
this.realLocalText = realLocalText;
this.calcMonths();
this.updateCurrentMonth();
if (!this.usePopup) {
this.onScrollIntoView();
}
},
methods: {
getMonthTitle,
getDateLabel,
isDateSelected,
initialValue() {
const { dataValue: value, type, minDate } = this;
if (!value) {
const today = new Date();
const now = minDate || new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); // 获取 0 点的时间戳
const initialValue = type === 'single' ? now : [now];
if (type === 'range') {
initialValue[1] = now + 24 * 3600 * 1000; // 第二天
}
this.dataValue = initialValue;
this.base.value = initialValue;
}
},
onScrollIntoView() {
const { dataValue: value } = this;
if (!value) return;
const date = new Date(Array.isArray(value) ? value[0] : value);
if (date) {
this.scrollIntoView = `year_${date.getFullYear()}_month_${date.getMonth()}`;
}
},
getCurrentYearAndMonth(v) {
const date = new Date(v);
return { year: date.getFullYear(), month: date.getMonth() };
},
updateActionButton(value) {
const _min = this.getCurrentYearAndMonth(this.base.minDate);
const _max = this.getCurrentYearAndMonth(this.base.maxDate);
const _value = this.getCurrentYearAndMonth(value);
const _minTimestamp = new Date(_min.year, _min.month, 1).getTime();
const _maxTimestamp = new Date(_max.year, _max.month, 1).getTime();
const _dateValue = new Date(_value.year, _value.month, 1);
const _prevYearTimestamp = getPrevYear(_dateValue).getTime();
const _prevMonthTimestamp = getPrevMonth(_dateValue).getTime();
const _nextMonthTimestamp = getNextMonth(_dateValue).getTime();
const _nextYearTimestamp = getNextYear(_dateValue).getTime();
const preYearBtnDisable = _prevYearTimestamp < _minTimestamp || _prevMonthTimestamp < _minTimestamp;
const prevMonthBtnDisable = _prevMonthTimestamp < _minTimestamp;
const nextYearBtnDisable = _nextMonthTimestamp > _maxTimestamp || _nextYearTimestamp > _maxTimestamp;
const nextMonthBtnDisable = _nextMonthTimestamp > _maxTimestamp;
this.actionButtons = {
preYearBtnDisable,
prevMonthBtnDisable,
nextYearBtnDisable,
nextMonthBtnDisable,
};
},
updateCurrentMonth(newValue) {
if (this.switchMode === 'none') return;
this.calcCurrentMonth(newValue);
},
calcCurrentMonth(newValue) {
const date = newValue || this.getCurrentDate();
const { year, month } = this.getCurrentYearAndMonth(date);
const currentMonth = this.months.filter(item => item.year === year && item.month === month);
this.updateActionButton(date);
this.currentMonth = currentMonth.length > 0 ? currentMonth : [this.months[0]];
},
calcMonths() {
if (!this.base) return;
const months = this.base.getMonths();
this.months = months;
},
close(trigger) {
if (this.autoClose) {
this.$emit('update:visible', false);
this.dataVisible = false;
}
this.$emit('close', { trigger });
},
onVisibleChange() {
this.close('overlay');
},
handleClose() {
this.close('close-btn');
},
handleSelect(e) {
const { readonly } = this;
const { date, year, month } = e.currentTarget.dataset;
if (date.type === 'disabled' || readonly) return;
const rawValue = this.base.select({ cellType: date.type, year, month, date: date.day });
const value = this.toTime(rawValue);
this.calcMonths();
this.updateCurrentMonth();
if (this.confirmBtn == null) {
// 不显示确认按钮,则选择完即关闭 popup
if (this.type === 'single' || rawValue.length === 2) {
this.dataVisible = false;
this._trigger('change', { value }); // 受控
}
}
this.$emit('select', { value });
},
onTplButtonTap() {
const rawValue = this.base.getTrimValue();
const value = this.toTime(rawValue);
this.close('confirm-btn');
this._trigger('confirm', { value });
},
toTime(val) {
if (!val) return null;
if (Array.isArray(val)) {
return val.map(item => item.getTime());
}
return val.getTime();
},
onScroll(e) {
this.$emit('scroll', e.detail);
},
getCurrentDate() {
let time = Array.isArray(this.base.value) ? this.base.value[0] : this.base.value;
if (this.currentMonth.length > 0) {
const year = this.currentMonth[0]?.year;
const month = this.currentMonth[0]?.month;
time = new Date(year, month, 1).getTime();
}
return time;
},
handleSwitchModeChange(e) {
const { type, disabled } = e.currentTarget.dataset;
if (disabled) return;
const date = this.getCurrentDate();
const funcMap = {
'pre-year': () => getPrevYear(date),
'pre-month': () => getPrevMonth(date),
'next-month': () => getNextMonth(date),
'next-year': () => getNextYear(date),
};
const newValue = funcMap[type]();
if (!newValue) return;
const { year, month } = this.getCurrentYearAndMonth(newValue);
this.$emit('panel-change', { year, month: month + 1 });
this.calcCurrentMonth(newValue);
},
onWatchMinMaxDate() {
const { firstDayOfWeek, minDate, maxDate } = this;
firstDayOfWeek && (this.base.firstDayOfWeek = firstDayOfWeek);
minDate && (this.base.minDate = minDate);
maxDate && (this.base.maxDate = maxDate);
this.calcMonths();
},
},
});
</script>
<style scoped>
@import './calendar.css';
</style>
<style scoped>
.t-calendar-switch-mode--none > .t-calendar__months {
/* support mp-alipay */
width: 100%;
}
</style>

View File

@@ -0,0 +1,42 @@
import { getRegExp } from '../common/runtime/wxs-polyfill';
export function isDateSelected(dateItem) {
return ['start', 'end', 'selected', 'centre'].indexOf(dateItem.type) >= 0;
}
export function getMonthTitle(year, month, pattern = '') {
// prettier-ignore
const REGEXP = getRegExp('\\{year\\}|\\{month\\}', 'g');
return pattern.replace(REGEXP, (match) => {
const replacements = {
'{year}': year,
'{month}': month < 10 ? `0${month}` : month,
};
return replacements[match] || match;
});
}
export function getDateLabel(monthItem, dateItem) {
const weekdayText = ['日', '一', '二', '三', '四', '五', '六'];
const weekday = (monthItem.weekdayOfFirstDay + dateItem.day - 1) % 7;
let label = `${monthItem.month + 1}${dateItem.day}日, 星期${weekdayText[weekday]}`;
if (dateItem.type === 'start') {
label = `开始日期:${label}`;
}
if (dateItem.type === 'end') {
label = `结束日期:${label}`;
}
if (isDateSelected(dateItem)) {
label = `已选中, ${label}`;
}
if (dateItem.prefix) {
label += `, ${dateItem.prefix}`;
}
if (dateItem.suffix) {
label += `, ${dateItem.suffix}`;
}
return label;
}

View File

@@ -0,0 +1,113 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCalendarProps } from './type';
export default {
/** 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效 */
allowSameDay: Boolean,
/** 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible */
autoClose: {
type: Boolean,
default: true,
},
/** 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性 */
confirmBtn: {
type: [String, Object],
default: '' as TdCalendarProps['confirmBtn'],
},
/** 第一天从星期几开始,默认 0 = 周日 */
firstDayOfWeek: {
type: Number,
default: 0,
},
/** 用于格式化日期的函数 */
format: {
type: Function,
},
/** 国际化文案 */
localeText: {
type: Object,
},
/** 最大可选的日期,不传则默认半年后 */
maxDate: {
type: Number,
},
/** 最小可选的日期,不传则默认今天 */
minDate: {
type: Number,
},
/** 是否只读,只读状态下不能选择日期 */
readonly: Boolean,
/** 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换,也支持按月切换 */
switchMode: {
type: String,
default: 'none' as TdCalendarProps['switchMode'],
validator(val: TdCalendarProps['switchMode']): boolean {
if (!val) return true;
return ['none', 'month', 'year-month'].includes(val);
},
},
/** 标题,不传默认为“请选择日期” */
title: {
type: String,
},
/** 日历的选择类型single = 单选multiple = 多选; range = 区间选择 */
type: {
type: String,
default: 'single' as TdCalendarProps['type'],
validator(val: TdCalendarProps['type']): boolean {
if (!val) return true;
return ['single', 'multiple', 'range'].includes(val);
},
},
/** 是否使用弹出层包裹日历 */
usePopup: {
type: Boolean,
default: true,
},
/** 是否使用了自定义导航栏 */
usingCustomNavbar: Boolean,
/** 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组 */
value: {
type: [Number, Array],
},
/** 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组,非受控属性 */
defaultValue: {
type: [Number, Array],
},
/** 是否显示日历;`usePopup` 为 true 时有效 */
visible: Boolean,
/** 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple */
onChange: {
type: Function,
default: () => ({}),
},
/** 关闭按钮时触发 */
onClose: {
type: Function,
default: () => ({}),
},
/** 点击确认按钮时触发 */
onConfirm: {
type: Function,
default: () => ({}),
},
/** 切换月或年时触发switch-mode 不为 none 时有效) */
onPanelChange: {
type: Function,
default: () => ({}),
},
/** 滚动时触发 */
onScroll: {
type: Function,
default: () => ({}),
},
/** 点击日期时触发 */
onSelect: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,57 @@
export default {
classPrefix: {
type: String,
default: '',
},
usePopup: {
type: Boolean,
},
switchMode: {
type: String,
default: '',
},
tClass: {
type: String,
default: '',
},
customStyle: {
type: [String, Object],
default: '',
},
title: {
type: String,
default: '',
},
realLocalText: {
type: Object,
default: () => ({}),
},
months: {
type: Array,
default: () => ([]),
},
currentMonth: {
type: [Array, Object],
default: () => ([]),
},
actionButtons: {
type: Object,
default: () => ({}),
},
days: {
type: Array,
default: () => ([]),
},
scrollIntoView: {
type: String,
default: '',
},
firstDayOfWeek: {
type: Number,
default: 0,
},
innerConfirmBtn: {
type: [Object, String],
default: '',
},
};

View File

@@ -0,0 +1,261 @@
<template>
<view
:class="utils.cls(classPrefix, [['popup', usePopup]]) + ' ' + classPrefix + '-switch-mode--' + switchMode + ' ' + tClass"
:style="customStyle"
>
<view
:class="classPrefix + '__title'"
tabindex="0"
>
<slot name="title" />
<text v-if="title || realLocalText.title">
{{ title || realLocalText.title }}
</text>
</view>
<t-icon
v-if="usePopup"
name="close"
:t-class="classPrefix + '__close-btn'"
size="48rpx"
aria-role="button"
aria-label="关闭"
:custom-style="closeBtnCustomStyle"
@click="handleClose"
/>
<block
v-if="switchMode !== 'none'"
name="calendar-header"
>
<calendar-header
:class-prefix="classPrefix + '-header'"
:switch-mode="switchMode"
:title="getMonthTitle(
currentMonth[0] && currentMonth[0].year,
realLocalText.months && currentMonth[0] && realLocalText.months[currentMonth[0].month],
realLocalText.monthTitle
)"
:pre-year-btn-disable="actionButtons.preYearBtnDisable"
:prev-month-btn-disable="actionButtons.prevMonthBtnDisable"
:next-year-btn-disable="actionButtons.nextYearBtnDisable"
:next-month-btn-disable="actionButtons.nextMonthBtnDisable"
@handleSwitchModeChange="handleSwitchModeChange"
/>
</block>
<view
aria-hidden
:class="classPrefix + '__days'"
>
<view
v-for="(item, index) in days"
:key="index"
:class="classPrefix + '__days-item'"
>
{{ item }}
</view>
</view>
<scroll-view
:class="classPrefix + '__months'"
:scroll-into-view="scrollIntoView"
scroll-y
enhanced
:show-scrollbar="false"
@scroll="onScroll"
>
<block
v-for="(item, index) in switchMode === 'none' ? months : currentMonth"
:key="index"
>
<block
v-if="switchMode === 'none'"
name="calendar-header"
>
<calendar-header
:t-class="classPrefix + '__month'"
:class-prefix="classPrefix + '-header'"
:switch-mode="switchMode"
:t-id="'year_' + item.year + '_month_' + item.month"
:title="getMonthTitle(item.year, realLocalText.months && realLocalText.months[item.month], realLocalText.monthTitle)"
:pre-year-btn-disable="actionButtons.preYearBtnDisable"
:prev-month-btn-disable="actionButtons.prevMonthBtnDisable"
:next-year-btn-disable="actionButtons.nextYearBtnDisable"
:next-month-btn-disable="actionButtons.nextMonthBtnDisable"
@handleSwitchModeChange="handleSwitchModeChange"
/>
</block>
<view :class="classPrefix + '__dates'">
<view
v-for="(item, index1) in (item.weekdayOfFirstDay - firstDayOfWeek + 7) % 7"
:key="index1"
/>
<block
v-for="(dateItem, dateIndex) in item.months"
:key="dateIndex"
>
<view
:class="classPrefix + '__dates-item ' + dateItem.className + ' ' + classPrefix + '__dates-item--' + dateItem.type"
:data-year="item.year"
:data-month="item.month"
:data-date="dateItem"
aria-role="button"
:aria-label="getDateLabel(item, dateItem)"
:aria-disabled="dateItem.type === 'disabled'"
@click="handleSelect"
>
<view
v-if="dateItem.prefix"
:class="classPrefix + '__dates-item-prefix'"
>
{{ dateItem.prefix }}
</view>
{{ dateItem.day }}
<view
v-if="dateItem.suffix"
:class="classPrefix + '__dates-item-suffix ' + classPrefix + '__dates-item-suffix--' + dateItem.type"
>
{{ dateItem.suffix }}
</view>
</view>
</block>
</view>
</block>
</scroll-view>
<view
v-if="innerConfirmBtn != null && usePopup"
:class="classPrefix + '__footer'"
>
<slot
v-if="innerConfirmBtn === 'slot'"
name="confirm-btn"
/>
<block v-else-if="innerConfirmBtn">
<t-button
:t-id="innerConfirmBtn.tId"
:custom-style="innerConfirmBtn.style"
:block="coalesce(innerConfirmBtn.block, true)"
:t-class="coalesce(coalesce(innerConfirmBtn.tClass, prefix + '-class-action'))"
:class="prefix + '-calendar__confirm-btn'"
:disabled="innerConfirmBtn.disabled"
:data-type="'action'"
:data-extra="innerConfirmBtn.dataExtra"
:custom-dataset="innerConfirmBtn.customDataset"
:content="innerConfirmBtn.content || realLocalText.confirm"
:icon="innerConfirmBtn.icon"
:loading="innerConfirmBtn.loading"
:loading-props="innerConfirmBtn.loadingProps"
:theme="coalesce(innerConfirmBtn.theme, 'primary')"
:ghost="innerConfirmBtn.ghost"
:shape="innerConfirmBtn.shape"
:size="innerConfirmBtn.size"
:variant="innerConfirmBtn.variant"
:open-type="innerConfirmBtn.openType"
:hover-class="innerConfirmBtn.hoverClass"
:hover-stop-propagation="innerConfirmBtn.hoverStopPropagation"
:hover-start-time="innerConfirmBtn.hoverStartTime"
:hover-stay-time="innerConfirmBtn.hoverStayTime"
:lang="innerConfirmBtn.lang"
:session-from="innerConfirmBtn.sessionFrom"
:send-message-title="innerConfirmBtn.sendMessageTitle"
:send-message-path="innerConfirmBtn.sendMessagePath"
:send-message-img="innerConfirmBtn.sendMessageImg"
:app-parameter="innerConfirmBtn.appParameter"
:show-message-card="innerConfirmBtn.showMessageCard"
:aria-label="innerConfirmBtn.ariaLabel"
@click="onTplButtonTap"
@getuserinfo="onTplButtonTap"
@contact="onTplButtonTap"
@getphonenumber="onTplButtonTap"
@error="onTplButtonTap"
@opensetting="onTplButtonTap"
@launchapp="onTplButtonTap"
@agreeprivacyauthorization="onTplButtonTap"
>
<slot v-if="innerConfirmBtn.useDefaultSlot" />
</t-button>
</block>
</view>
</view>
</template>
<script>
import TIcon from '../icon/icon.vue';
import TButton from '../button/button.vue';
import utils from '../common/utils.wxs';
import {
getDateLabel,
getMonthTitle,
} from './computed.js';
import CalendarHeader from './calendar-header.vue';
import { prefix } from '../common/config';
import { coalesce } from '../common/utils';
import props from './template.props';
export default {
name: 'TCalendarContent',
options: {
styleIsolation: 'shared',
},
components: {
CalendarHeader,
TIcon,
TButton,
},
props: {
...props,
},
emits: [
'clickButton',
'close',
'scroll',
'select',
'handleSwitchModeChange',
],
data() {
return {
prefix,
utils,
};
},
computed: {
closeBtnCustomStyle() {
return utils._style({
position: 'absolute',
top: '16px',
right: '16px',
margin: '-12px',
padding: '12px',
color: 'var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, .9))))',
});
},
},
watch: {
},
mounted() {
},
methods: {
coalesce,
getDateLabel,
getMonthTitle,
onTplButtonTap() {
this.$emit('clickButton');
},
handleSelect(...args) {
this.$emit('select', ...args);
},
handleClose() {
this.$emit('close');
},
onScroll(...args) {
this.$emit('scroll', ...args);
},
handleSwitchModeChange(...args) {
this.$emit('handleSwitchModeChange', ...args);
},
},
};
</script>
<style scoped>
@import './calendar.css';
</style>

View File

@@ -0,0 +1,141 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdButtonProps as ButtonProps } from '../button/type';
export interface TdCalendarProps {
/**
* 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效
* @default false
*/
allowSameDay?: boolean;
/**
* 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible
* @default true
*/
autoClose?: boolean;
/**
* 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性
* @default ''
*/
confirmBtn?: string | ButtonProps | null;
/**
* 第一天从星期几开始,默认 0 = 周日
* @default 0
*/
firstDayOfWeek?: number;
/**
* 用于格式化日期的函数
*/
format?: CalendarFormatType;
/**
* 国际化文案
*/
localeText?: CalendarLocaleText;
/**
* 最大可选的日期,不传则默认半年后
*/
maxDate?: number;
/**
* 最小可选的日期,不传则默认今天
*/
minDate?: number;
/**
* 是否只读,只读状态下不能选择日期
*/
readonly?: boolean;
/**
* 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换,也支持按月切换
* @default none
*/
switchMode?: 'none' | 'month' | 'year-month';
/**
* 标题,不传默认为“请选择日期”
*/
title?: string;
/**
* 日历的选择类型single = 单选multiple = 多选; range = 区间选择
* @default single
*/
type?: 'single' | 'multiple' | 'range';
/**
* 是否使用弹出层包裹日历
* @default true
*/
usePopup?: boolean;
/**
* 是否使用了自定义导航栏
* @default false
*/
usingCustomNavbar?: boolean;
/**
* 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组
*/
value?: number | number[];
/**
* 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组,非受控属性
*/
defaultValue?: number | number[];
/**
* 是否显示日历;`usePopup` 为 true 时有效
* @default false
*/
visible?: boolean;
/**
* 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple
*/
onChange?: (context: { value: number | number[] }) => void;
/**
* 关闭按钮时触发
*/
onClose?: (context: { trigger: CalendarTrigger }) => void;
/**
* 点击确认按钮时触发
*/
onConfirm?: (context: { value: number | number[] }) => void;
/**
* 切换月或年时触发switch-mode 不为 none 时有效)
*/
onPanelChange?: (context: { year: number; month: number }) => void;
/**
* 滚动时触发
*/
onScroll?: (context: {
scrollLeft: number;
scrollTop: number;
scrollHeight: number;
scrollWidth: number;
deltaX: number;
deltaY: number;
}) => void;
/**
* 点击日期时触发
*/
onSelect?: (context: { value: number | number[] }) => void;
}
export type CalendarFormatType = (day: TDate) => TDate;
export type TDateType = 'selected' | 'disabled' | 'start' | 'start-end' | 'centre' | 'end' | '';
export interface TDate {
date: Date;
day: number;
type: TDateType;
className?: string;
prefix?: string;
suffix?: string;
}
export interface CalendarLocaleText {
title?: string;
weekdays?: string[];
monthTitle?: string;
months?: string[];
confirm?: string;
}
export type CalendarTrigger = 'close-btn' | 'confirm-btn' | 'overlay' | 'auto-close';

View File

@@ -0,0 +1,16 @@
export function getMonthByOffset(date, offset) {
const _date = new Date(date);
_date.setMonth(_date.getMonth() + offset);
return _date;
}
export function getYearByOffset(date, offset) {
const _date = new Date(date);
_date.setFullYear(_date.getFullYear() + offset);
return _date;
}
export const getPrevMonth = date => getMonthByOffset(date, -1);
export const getNextMonth = date => getMonthByOffset(date, 1);
export const getPrevYear = date => getYearByOffset(date, -1);
export const getNextYear = date => getYearByOffset(date, 1);

View File

@@ -0,0 +1,56 @@
:: BASE_DOC ::
## API
### Cascader Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
check-strictly | Boolean | false | \- | N
close-btn | Boolean | true | \- | N
keys | Object | - | Typescript: `CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/common/common.ts)。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/cascader/type.ts) | N
options | Array | [] | Typescript: `Array<CascaderOption>` | N
placeholder | String | 选择选项 | \- | N
sub-titles | Array | [] | Typescript: `Array<string>` | N
theme | String | step | options: step/tab | N
title | String | - | \- | N
value | String / Number | - | `v-model:value` is supported | N
default-value | String / Number | - | uncontrolled property | N
visible | Boolean | false | \- | N
### Cascader Events
name | params | description
-- | -- | --
change | `(context: { value: string \| number, selectedOptions: string[] })` | \-
close | `(context: { trigger: CascaderTriggerSource })` | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/cascader/type.ts)。<br/>`type CascaderTriggerSource = 'overlay' \| 'close-btn' \| 'finish'`<br/>
pick | `(context: { value: string \| number, label: string, index: number, level: number })` | \-
### Cascader Slots
name | Description
-- | --
close-btn | \-
header | \-
middle-content | \-
title | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-cascader-active-color | @brand-color | -
--td-cascader-bg-color | @bg-color-container | -
--td-cascader-border-color | @component-stroke | -
--td-cascader-content-height | 78vh | -
--td-cascader-disabled-color | @text-color-disabled | -
--td-cascader-options-height | calc(100% - @cascader-step-height) | -
--td-cascader-options-title-color | @text-color-placeholder | -
--td-cascader-step-arrow-color | @text-color-placeholder | -
--td-cascader-step-dot-size | 16rpx | -
--td-cascader-step-height | 88rpx | -
--td-cascader-title-color | @text-color-primary | -
--td-cascader-title-font | @font-title-large | -
--td-cascader-title-padding | @spacer-2 | -

View File

@@ -0,0 +1,96 @@
---
title: Cascader 级联选择器
description: 级联选择器适用于有清晰层级结构的数据集合,用户可以通过逐级查看并选择。
spline: form
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCascader from '@tdesign/uniapp/cascader/cascader.vue';
```
### 基础用法
{{ base }}
### 选项卡风格
{{ theme-tab }}
### 进阶
#### 带初始值
{{ with-value }}
#### 自定义 keys
{{ keys }}
#### 使用次级标题
{{ with-title }}
#### 选择任意一项
{{ check-strictly }}
## API
### Cascader Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
check-strictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N
close-btn | Boolean | true | 关闭按钮 | N
keys | Object | - | 用来定义 value / label / children / disabled 在 `options` 中对应的字段别名。TS 类型:`CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[通用类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/common/common.ts)。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/cascader/type.ts) | N
options | Array | [] | 可选项数据源。TS 类型:`Array<CascaderOption>` | N
placeholder | String | 选择选项 | 未选中时的提示文案 | N
sub-titles | Array | [] | 每级展示的次标题。TS 类型:`Array<string>` | N
theme | String | step | 展示风格。可选项step/tab | N
title | String | - | 标题 | N
value | String / Number | - | 选项值。支持语法糖 `v-model:value` | N
default-value | String / Number | - | 选项值。非受控属性 | N
visible | Boolean | false | 是否展示 | N
### Cascader Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { value: string \| number, selectedOptions: string[] })` | 值发生变更时触发
close | `(context: { trigger: CascaderTriggerSource })` | 关闭时触发。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/cascader/type.ts)。<br/>`type CascaderTriggerSource = 'overlay' \| 'close-btn' \| 'finish'`<br/>
pick | `(context: { value: string \| number, label: string, index: number, level: number })` | 选择后触发
### Cascader Slots
名称 | 描述
-- | --
close-btn | 自定义 `close-btn` 显示内容
header | 头部
middle-content | 中间内容
title | 自定义 `title` 显示内容
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-cascader-active-color | @brand-color | -
--td-cascader-bg-color | @bg-color-container | -
--td-cascader-border-color | @component-stroke | -
--td-cascader-content-height | 78vh | -
--td-cascader-disabled-color | @text-color-disabled | -
--td-cascader-options-height | calc(100% - @cascader-step-height) | -
--td-cascader-options-title-color | @text-color-placeholder | -
--td-cascader-step-arrow-color | @text-color-placeholder | -
--td-cascader-step-dot-size | 16rpx | -
--td-cascader-step-height | 88rpx | -
--td-cascader-title-color | @text-color-primary | -
--td-cascader-title-font | @font-title-large | -
--td-cascader-title-padding | @spacer-2 | -

View File

@@ -0,0 +1,102 @@
.t-cascader {
display: flex;
flex-direction: column;
background-color: var(--td-cascader-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
color: var(--td-cascader-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
border-radius: var(--td-radius-extraLarge, 24rpx) var(--td-radius-extraLarge, 24rpx) 0 0;
--td-radio-icon-checked-color: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
--td-tab-item-active-color: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
--td-tab-track-color: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-cascader__close-btn {
right: var(--td-spacer-2, 32rpx);
top: var(--td-spacer-2, 32rpx);
position: absolute;
}
.t-cascader__title {
position: relative;
font: var(--td-cascader-title-font, var(--td-font-title-large, 600 36rpx / 52rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
text-align: center;
padding: var(--td-cascader-title-padding, var(--td-spacer-2, 32rpx));
}
.t-cascader__content {
width: 100%;
height: var(--td-cascader-content-height, 78vh);
display: flex;
flex-direction: column;
}
.t-cascader__options {
width: 100vw;
}
.t-cascader__options-title {
color: var(--td-cascader-options-title-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
font: var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
padding-top: 40rpx;
padding-left: var(--td-spacer-2, 32rpx);
box-sizing: border-box;
}
.t-cascader__options-container {
flex: 1;
display: flex;
transition: all ease 0.3s;
}
.t-cascader__step {
display: flex;
align-items: center;
height: var(--td-cascader-step-height, 88rpx);
}
.t-cascader__steps {
padding: 0 32rpx 10rpx;
position: relative;
}
.t-cascader__steps::after {
content: '';
display: block;
position: absolute;
top: unset;
bottom: 0;
left: unset;
right: unset;
background-color: var(--td-cascader-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
}
.t-cascader__steps::after {
height: 1px;
left: 0;
right: 0;
transform: scaleY(0.5);
}
.t-cascader__step-dot {
position: relative;
width: var(--td-cascader-step-dot-size, 16rpx);
height: var(--td-cascader-step-dot-size, 16rpx);
border-radius: 50%;
border: 2rpx solid var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
box-sizing: border-box;
}
.t-cascader__step-dot:not(.t-cascader__step-dot--last)::after {
content: '';
display: block;
position: absolute;
left: 50%;
top: calc(var(--td-cascader-step-dot-size, 16rpx) + 14rpx);
height: 36rpx;
width: 2rpx;
background: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
transform: translateX(-50%);
}
.t-cascader__step-dot--active {
background: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
border-color: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-cascader__step-label {
padding-left: var(--td-spacer-2, 32rpx);
font: var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
}
.t-cascader__step-label--active {
color: var(--td-cascader-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
font-weight: 600;
}
.t-cascader__step-arrow {
color: var(--td-cascader-step-arrow-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
margin-left: auto;
}

View File

@@ -0,0 +1,519 @@
<template>
<view>
<TPopup
:class="tClass"
:visible="dataVisible"
placement="bottom"
@visible-change="onVisibleChange"
>
<view
:style="tools._style([customStyle])"
:class="name"
>
<view :class="name + '__title'">
<slot name="title" />
{{ title }}
</view>
<view
:class="name + '__close-btn'"
@click="onClose"
>
<slot name="close-btn" />
<TIcon
v-if="closeBtn"
size="48rpx"
name="close"
/>
</view>
<slot name="header" />
<view :class="name + '__content'">
<block v-if="steps && steps.length">
<view
v-if="theme == 'step'"
:class="name + '__steps'"
>
<view
v-for="(item, index) in steps"
:key="index"
:class="name + '__step'"
:data-index="index"
@click="() => onStepClick(index)"
>
<view
:class="
name +
'__step-dot ' +
name +
'__step-dot--' +
(item !== placeholder ? 'active' : '') +
' ' +
name +
'__step-dot--' +
(index === steps.length - 1 ? 'last' : '')
"
/>
<view :class="name + '__step-label ' + name + '__step-label--' + (index === stepIndex ? 'active' : '')">
{{ item }}
</view>
<TIcon
name="chevron-right"
size="44rpx"
:t-class="name + '__step-arrow'"
:custom-style="stepArrowCustomStyle"
style="margin-left: auto"
/>
</view>
</view>
<TTabs
v-if="theme == 'tab'"
ref="tabs"
:value="stepIndex"
:space-evenly="false"
@change="({value}) => onTabChange(value)"
>
<TTabPanel
v-for="(item, index) in steps"
:key="index"
:ref="`tab-${index}`"
:value="index"
:label="item"
/>
</TTabs>
</block>
<slot name="middle-content" />
<view
v-if="subTitles && subTitles[stepIndex]"
:class="name + '__options-title'"
>
{{ subTitles[stepIndex] }}
</view>
<view
:class="name + '__options-container'"
:style="'width: ' + (items.length + 1) + '00vw; transform: translateX(-' + stepIndex + '00vw)'"
>
<scroll-view
v-for="(options, index) in items"
:key="index"
:class="name + '__options'"
scroll-y
:scroll-top="scrollTopList[index]"
type="list"
:style="'height: ' + _optionsHeight + 'px'"
>
<view :class="'cascader-radio-group-' + index">
<TRadioGroup
:value="selectedValue[index]"
:keys="keys"
:options="options"
:data-level="index"
placement="right"
icon="line"
borderless
@change="({ value }) => handleSelect($event, { level: index, value })"
/>
</view>
</scroll-view>
</view>
</view>
</view>
</TPopup>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import TPopup from '../popup/popup';
import TTabs from '../tabs/tabs';
import TTabPanel from '../tab-panel/tab-panel';
import TRadioGroup from '../radio-group/radio-group';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { getRect, coalesce, nextTick } from '../common/utils';
import tools from '../common/utils.wxs';
const name = `${prefix}-cascader`;
function parseOptions(options, keys) {
const label = coalesce(keys?.label, 'label');
const value = coalesce(keys?.value, 'value');
const disabled = coalesce(keys?.disabled, 'disabled');
return options.map(item => ({
[label]: item[label],
[value]: item[value],
[disabled]: item[disabled],
}));
}
const defaultState = {
contentHeight: 0,
stepHeight: 0,
tabsHeight: 0,
subTitlesHeight: 0,
stepsInitHeight: 0,
};
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'value',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
],
components: {
TIcon,
TPopup,
TTabs,
TTabPanel,
TRadioGroup,
},
props: {
...props,
},
emits: [
'update:visible',
],
data() {
return {
prefix,
name,
stepIndex: 0,
selectedIndexes: [],
selectedValue: [],
scrollTopList: [],
steps: [],
_optionsHeight: 0,
tools,
dataVisible: this.visible,
dataValue: coalesce(this.value, this.defaultValue),
items: [],
};
},
computed: {
stepArrowCustomStyle() {
return tools._style({
color: 'var(--td-cascader-step-arrow-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, .4))))',
marginLeft: 'auto',
});
},
},
watch: {
visible: {
handler(v) {
this.dataVisible = v;
},
immediate: true,
},
dataVisible: {
handler(v) {
if (v) {
nextTick().then(() => {
const $tabs = this.$refs.tabs;
$tabs?.setTrack();
$tabs?.getTabHeight().then((res) => {
this.state.tabsHeight = res.height;
});
});
// 不能使用 this.$nextTick在头条小程序下会报错
nextTick().then(() => {
this.initOptionsHeight(this.steps.length);
this.updateScrollTop();
this.initWithValue();
});
} else {
this.state = { ...defaultState };
}
},
immediate: true,
},
value: {
handler(v) {
this.dataValue = v;
},
immediate: true,
},
dataValue: {
handler() {
this.initWithValue();
},
immediate: true,
},
options: {
handler() {
const { selectedValue, steps, items } = this.genItems();
this.steps = steps;
this.items = items;
this.selectedValue = selectedValue;
this.stepIndex = items.length - 1;
this.setTabParent();
},
immediate: true,
deep: true,
},
selectedIndexes: {
handler() {
const { visible, theme } = this;
const { selectedValue, steps, items } = this.genItems();
this.steps = steps;
this.setTabParent();
this.selectedValue = selectedValue;
this.stepIndex = items.length - 1;
if (JSON.stringify(items) !== JSON.stringify(this.items)) {
this.items = items;
}
if (visible && theme === 'step') {
this.updateOptionsHeight(steps.length);
}
},
immediate: true,
deep: true,
},
stepIndex: {
handler() {
const { dataVisible: visible } = this;
if (visible) {
this.updateScrollTop();
}
},
immediate: true,
deep: true,
},
},
created() {
this.state = {
...defaultState,
};
},
mounted() {
},
methods: {
setTabParent() {
// #ifdef MP-TOUTIAO
nextTick().then(() => {
const tabsRef = this.$refs.tabs;
this.steps.forEach((tools, index) => {
const tabRef = this.$refs[`tab-${index}`];
tabRef?.[0]?.setParent(tabsRef);
});
});
// #endif
},
updateOptionsHeight(steps) {
const { contentHeight, stepsInitHeight, stepHeight, subTitlesHeight } = this.state;
this._optionsHeight = contentHeight - stepsInitHeight - subTitlesHeight - (steps - 1) * stepHeight;
},
async initOptionsHeight(steps) {
const { theme, subTitles } = this;
const { height } = await getRect(this, `.${name}__content`);
this.state.contentHeight = height;
if (theme === 'step') {
await Promise.all([
getRect(this, `.${name}__steps`),
getRect(this, `.${name}__step`),
])
.then(([stepsRect, stepRect]) => {
this.state.stepsInitHeight = stepsRect.height - (steps - 1) * stepRect.height;
this.state.stepHeight = stepRect.height;
})
.catch(() => {
});
}
if (subTitles.length > 0) {
const { height } = await getRect(this, `.${name}__options-title`);
this.state.subTitlesHeight = height;
}
const optionsInitHeight = this.state.contentHeight - this.state.subTitlesHeight;
this._optionsHeight = theme === 'step'
? optionsInitHeight - this.state.stepsInitHeight - (steps - 1) * this.state.stepHeight
: optionsInitHeight - this.state.tabsHeight;
},
initWithValue() {
if (this.dataValue != null && this.dataValue !== '') {
const selectedIndexes = this.getIndexesByValue(this.options, this.dataValue);
if (selectedIndexes) {
this.selectedIndexes = selectedIndexes;
}
} else {
this.selectedIndexes = [];
}
},
getIndexesByValue(options, value) {
const { keys } = this;
for (let i = 0, size = options.length; i < size; i += 1) {
const opt = options[i];
if (opt[coalesce(keys?.value, 'value')] === value) {
return [i];
}
if (opt[coalesce(keys?.children, 'children')]) {
const res = this.getIndexesByValue(opt[coalesce(keys?.children, 'children')], value);
if (res) {
return [i, ...res];
}
}
}
},
updateScrollTop() {
const { dataVisible: visible, items, selectedIndexes, stepIndex } = this;
if (visible) {
getRect(this, '.cascader-radio-group-0').then((rect) => {
const eachRadioHeight = rect.height / items[0]?.length;
this[`scrollTopList[${stepIndex}]`] = eachRadioHeight * selectedIndexes[stepIndex];
})
.catch(() => {
});
}
},
hide(trigger) {
this.dataVisible = false;
this.$emit('close', { trigger });
this.$emit('update:visible', false);
},
onVisibleChange() {
this.hide('overlay');
},
onClose() {
if (this.checkStrictly) {
this.triggerChange();
}
this.hide('close-btn');
},
onStepClick(index) {
this.stepIndex = index;
},
onTabChange(value) {
this.stepIndex = value;
},
genItems() {
const { options, selectedIndexes, keys, placeholder } = this;
const selectedValue = [];
const steps = [];
const items = [parseOptions(options, keys)];
if (options.length > 0) {
let current = options;
for (let i = 0, size = selectedIndexes.length; i < size; i += 1) {
const index = selectedIndexes[i];
const next = current[index];
current = next[coalesce(keys?.children, 'children')];
selectedValue.push(next[coalesce(keys?.value, 'value')]);
steps.push(next[coalesce(keys?.label, 'label')]);
if (next[coalesce(keys?.children, 'children')]) {
items.push(parseOptions(next[coalesce(keys?.children, 'children')], keys));
}
}
}
if (steps.length < items.length) {
steps.push(placeholder);
}
return {
selectedValue,
steps,
items,
};
},
handleSelect(tools, { level, value }) {
const { checkStrictly } = this;
const { selectedIndexes, items, keys, options, selectedValue } = this;
const index = items[level].findIndex(item => item[coalesce(keys?.value, 'value')] === value);
let item = selectedIndexes.slice(0, level).reduce((acc, item, index) => {
if (index === 0) {
return acc[item];
}
return acc[coalesce(keys?.children, 'children')][item];
}, options);
if (level === 0) {
item = item[index];
} else {
item = item[coalesce(keys?.children, 'children')][index];
}
if (item[coalesce(keys?.disabled, 'disabled')]) {
return;
}
this.$emit('pick', {
value: item[coalesce(keys?.value, 'value')],
label: item[coalesce(keys?.label, 'label')],
index,
level,
});
selectedIndexes[level] = index;
if (checkStrictly && selectedValue.includes(String(value))) {
selectedIndexes.length = level;
this.selectedIndexes = selectedIndexes;
return;
}
selectedIndexes.length = level + 1;
const { items: newItems } = this.genItems();
if (item?.[coalesce(keys?.children, 'children')]?.length >= 0) {
this.selectedIndexes = selectedIndexes;
this[`items[${level + 1}]`] = newItems[level + 1];
} else {
// setCascaderValue(item.value);
this.selectedIndexes = selectedIndexes;
setTimeout(this.triggerChange);
this.hide('finish');
}
},
triggerChange() {
const { items, selectedValue, selectedIndexes } = this;
this._trigger('change', {
value: coalesce(selectedValue[selectedValue.length - 1], ''),
selectedOptions: items.map((item, index) => item[selectedIndexes[index]]).filter(Boolean),
});
},
},
});
</script>
<style scoped>
@import './cascader.css';
</style>

View File

@@ -0,0 +1,73 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCascaderProps } from './type';
export default {
/** 父子节点选中状态不再关联,可各自选中或取消 */
checkStrictly: Boolean,
/** 关闭按钮 */
closeBtn: {
type: Boolean,
default: true as TdCascaderProps['closeBtn'],
},
/** 用来定义 value / label / children / disabled 在 `options` 中对应的字段别名 */
keys: {
type: Object,
},
/** 可选项数据源 */
options: {
type: Array,
default: (): TdCascaderProps['options'] => [],
},
/** 未选中时的提示文案 */
placeholder: {
type: String,
default: '选择选项',
},
/** 每级展示的次标题 */
subTitles: {
type: Array,
default: (): TdCascaderProps['subTitles'] => [],
},
/** 展示风格 */
theme: {
type: String,
default: 'step' as TdCascaderProps['theme'],
validator(val: TdCascaderProps['theme']): boolean {
if (!val) return true;
return ['step', 'tab'].includes(val);
},
},
/** 标题 */
title: {
type: String,
},
/** 选项值 */
value: {
type: [String, Number],
},
/** 选项值,非受控属性 */
defaultValue: {
type: [String, Number],
},
/** 是否展示 */
visible: Boolean,
/** 值发生变更时触发 */
onChange: {
type: Function,
default: () => ({}),
},
/** 关闭时触发 */
onClose: {
type: Function,
default: () => ({}),
},
/** 选择后触发 */
onPick: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,77 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TreeOptionData, TreeKeysType } from '../common/common';
export interface TdCascaderProps<CascaderOption extends TreeOptionData = TreeOptionData> {
/**
* 父子节点选中状态不再关联,可各自选中或取消
* @default false
*/
checkStrictly?: boolean;
/**
* 关闭按钮
* @default true
*/
closeBtn?: boolean;
/**
* 用来定义 value / label / children / disabled 在 `options` 中对应的字段别名
*/
keys?: CascaderKeysType;
/**
* 可选项数据源
* @default []
*/
options?: Array<CascaderOption>;
/**
* 未选中时的提示文案
* @default 选择选项
*/
placeholder?: string;
/**
* 每级展示的次标题
* @default []
*/
subTitles?: Array<string>;
/**
* 展示风格
* @default step
*/
theme?: 'step' | 'tab';
/**
* 标题
*/
title?: string;
/**
* 选项值
*/
value?: string | number;
/**
* 选项值,非受控属性
*/
defaultValue?: string | number;
/**
* 是否展示
* @default false
*/
visible?: boolean;
/**
* 值发生变更时触发
*/
onChange?: (context: { value: string | number; selectedOptions: string[] }) => void;
/**
* 关闭时触发
*/
onClose?: (context: { trigger: CascaderTriggerSource }) => void;
/**
* 选择后触发
*/
onPick?: (context: { value: string | number; label: string; index: number; level: number }) => void;
}
export type CascaderKeysType = TreeKeysType;
export type CascaderTriggerSource = 'overlay' | 'close-btn' | 'finish';

View File

@@ -0,0 +1,44 @@
.t-cell-group {
position: relative;
}
.t-cell-group__title {
font-family: PingFangSC-Regular;
font-size: var(--td-cell-group-title-font-size, 28rpx);
color: var(--td-cell-group-title-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
text-align: left;
line-height: var(--td-cell-group-title-line-height, 90rpx);
background-color: var(--td-cell-group-title-bg-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
padding-left: var(--td-cell-group-title-padding-left, 32rpx);
}
.t-cell-group--bordered::before {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
left: 0;
top: 0;
border-top: 1px solid var(--td-cell-group-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
transform: scaleY(0.5);
transform-origin: 0 0;
transform-origin: top;
z-index: 1;
}
.t-cell-group--bordered::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
left: 0;
bottom: 0;
border-bottom: 1px solid var(--td-cell-group-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
transform: scaleY(0.5);
transform-origin: bottom;
z-index: 1;
}
.t-cell-group--card {
margin: 0 32rpx;
border-radius: var(--td-radius-large, 18rpx);
overflow: hidden;
}

View File

@@ -0,0 +1,69 @@
<template>
<view>
<view
v-if="title"
:class="[
classPrefix + '__title ',
tClassTitle
]"
>
{{ title }}
</view>
<view
:style="tools._style([customStyle])"
:class="[
tools.cls(classPrefix, [['bordered', bordered], theme]),
tClass
]"
>
<slot />
</view>
</view>
</template>
<script>
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import tools from '../common/utils.wxs';
import { ParentMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-cell-group`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [`${prefix}-class`, `${prefix}-class-title`],
mixins: [ParentMixin(RELATION_MAP.Cell)],
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
tools,
};
},
methods: {
innerAfterLinked() {
this.updateLastChid();
},
innerAfterUnLinked() {
this.updateLastChid();
},
updateLastChid() {
const { children } = this;
children.forEach((child, index) => {
child.isLastChild = index === children.length - 1;
});
},
},
});
</script>
<style scoped>
@import './cell-group.css';
</style>

View File

@@ -0,0 +1,25 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCellGroupProps } from './type';
export default {
/** 是否显示组边框 */
bordered: Boolean,
/** 单元格组风格 */
theme: {
type: String,
default: 'default' as TdCellGroupProps['theme'],
validator(val: TdCellGroupProps['theme']): boolean {
if (!val) return true;
return ['default', 'card'].includes(val);
},
},
/** 单元格组标题 */
title: {
type: String,
default: '',
},
};

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdCellGroupProps {
/**
* 是否显示组边框
* @default false
*/
bordered?: boolean;
/**
* 单元格组风格
* @default default
*/
theme?: 'default' | 'card';
/**
* 单元格组标题
* @default ''
*/
title?: string;
}

View File

@@ -0,0 +1,116 @@
:: BASE_DOC ::
## API
### Cell Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
align | String | middle | options: top/middle/bottom | N
arrow | Boolean / Object | false | \- | N
bordered | Boolean | true | \- | N
description | String | - | \- | N
hover | Boolean | - | \- | N
image | String | - | \- | N
jump-type | String | navigateTo | options: switchTab/reLaunch/redirectTo/navigateTo | N
left-icon | String / Object | - | \- | N
note | String | - | \- | N
note-style | String / Object | - | \- | N
required | Boolean | false | \- | N
right-icon | String / Object | - | \- | N
right-icon-style | String / Object | - | \- | N
title | String | - | \- | N
title-style | String / Object | - | \- | N
url | String | - | \- | N
### Cell Events
name | params | description
-- | -- | --
click | `(e: MouseEvent)` | \-
### Cell Slots
name | Description
-- | --
description | \-
image | \-
left-icon | \-
note | \-
right-icon | \-
title | \-
### Cell External Classes
className | Description
-- | --
t-class | \-
t-class-center | \-
t-class-description | \-
t-class-hover | \-
t-class-image | \-
t-class-left | \-
t-class-left-icon | \-
t-class-note | \-
t-class-right | \-
t-class-right-icon | \-
t-class-title | \-
### CellGroup Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
bordered | Boolean | false | \- | N
theme | String | default | options: default/card | N
title | String | - | \- | N
### CellGroup Slots
name | Description
-- | --
\- | \-
### CellGroup External Classes
className | Description
-- | --
t-class | \-
t-class-title | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-cell-group-border-color | @component-stroke | -
--td-cell-group-title-bg-color | @bg-color-secondarycontainer | -
--td-cell-group-title-color | @text-color-placeholder | -
--td-cell-group-title-font-size | 28rpx | -
--td-cell-group-title-line-height | 90rpx | -
--td-cell-group-title-padding-left | 32rpx | -
--td-cell-bg-color | @bg-color-container | -
--td-cell-border-color | @component-stroke | -
--td-cell-border-left-space | @cell-horizontal-padding | -
--td-cell-border-right-space | 0 | -
--td-cell-border-width | 1px | -
--td-cell-description-color | @text-color-secondary | -
--td-cell-description-font | @font-body-medium | -
--td-cell-height | auto | -
--td-cell-horizontal-padding | 32rpx | -
--td-cell-hover-color | @bg-color-secondarycontainer | -
--td-cell-image-height | 96rpx | -
--td-cell-image-width | 96rpx | -
--td-cell-left-icon-color | @brand-color | -
--td-cell-left-icon-size | 48rpx | -
--td-cell-note-color | @text-color-placeholder | -
--td-cell-note-font-size | @font-size-m | -
--td-cell-required-color | @error-color | -
--td-cell-required-font-size | @font-size-m | -
--td-cell-right-icon-color | @text-color-placeholder | -
--td-cell-right-icon-size | 48rpx | -
--td-cell-title-color | @text-color-primary | -
--td-cell-title-font | @font-body-large | -
--td-cell-vertical-padding | 32rpx | -

View File

@@ -0,0 +1,147 @@
---
title: Cell 单元格
description: 用于各个类别行的信息展示。
spline: data
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCell from '@tdesign/uniapp/cell/cell.vue';
import TCellGroup from '@tdesign/uniapp/cell-group/cell-group.vue';
```
### 类型
单行单元格
{{ base }}
多行单元格
{{ multiple }}
### 样式
卡片单元格
{{ theme }}
## API
### Cell Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
align | String | middle | 右侧内容的对齐方式默认居中对齐。可选项top/middle/bottom | N
arrow | Boolean / Object | false | 是否显示右侧箭头 | N
bordered | Boolean | true | 是否显示下边框 | N
description | String | - | 下方内容描述 | N
hover | Boolean | - | 是否开启点击反馈 | N
image | String | - | 主图 | N
jump-type | String | navigateTo | 链接跳转类型。可选项switchTab/reLaunch/redirectTo/navigateTo | N
left-icon | String / Object | - | 左侧图标,出现在单元格标题的左侧 | N
note | String | - | 和标题同行的说明文字 | N
note-style | String / Object | - | 说明文字自定义样式 | N
required | Boolean | false | 是否显示表单必填星号 | N
right-icon | String / Object | - | 最右侧图标 | N
right-icon-style | String / Object | - | 右侧图标自定义样式 | N
title | String | - | 标题 | N
title-style | String / Object | - | 标题自定义样式 | N
url | String | - | 点击后跳转链接地址。如果值为空,则表示不需要跳转 | N
### Cell Events
名称 | 参数 | 描述
-- | -- | --
click | `(e: MouseEvent)` | 右侧内容
### Cell Slots
名称 | 描述
-- | --
description | 自定义 `description` 显示内容
image | 自定义 `image` 显示内容
left-icon | 自定义 `left-icon` 显示内容
note | 自定义 `note` 显示内容
right-icon | 自定义 `right-icon` 显示内容
title | 自定义 `title` 显示内容
### Cell External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-center | 中间(`title`, `description`)内容样式类
t-class-description | 下方描述内容样式类
t-class-hover | 悬停样式类
t-class-image | 图片样式类
t-class-left | 左侧内容样式类
t-class-left-icon | 左侧图标样式类
t-class-note | 右侧说明文字样式类
t-class-right | 右侧内容样式类
t-class-right-icon | 右侧图标样式类
t-class-title | 标题样式类
### CellGroup Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
bordered | Boolean | false | 是否显示组边框 | N
theme | String | default | 单元格组风格。可选项default/card | N
title | String | - | 单元格组标题 | N
### CellGroup Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
### CellGroup External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-title | 标题样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-cell-group-border-color | @component-stroke | -
--td-cell-group-title-bg-color | @bg-color-secondarycontainer | -
--td-cell-group-title-color | @text-color-placeholder | -
--td-cell-group-title-font-size | 28rpx | -
--td-cell-group-title-line-height | 90rpx | -
--td-cell-group-title-padding-left | 32rpx | -
--td-cell-bg-color | @bg-color-container | -
--td-cell-border-color | @component-stroke | -
--td-cell-border-left-space | @cell-horizontal-padding | -
--td-cell-border-right-space | 0 | -
--td-cell-border-width | 1px | -
--td-cell-description-color | @text-color-secondary | -
--td-cell-description-font | @font-body-medium | -
--td-cell-height | auto | -
--td-cell-horizontal-padding | 32rpx | -
--td-cell-hover-color | @bg-color-secondarycontainer | -
--td-cell-image-height | 96rpx | -
--td-cell-image-width | 96rpx | -
--td-cell-left-icon-color | @brand-color | -
--td-cell-left-icon-size | 48rpx | -
--td-cell-note-color | @text-color-placeholder | -
--td-cell-note-font-size | @font-size-m | -
--td-cell-required-color | @error-color | -
--td-cell-required-font-size | @font-size-m | -
--td-cell-right-icon-color | @text-color-placeholder | -
--td-cell-right-icon-size | 48rpx | -
--td-cell-title-color | @text-color-primary | -
--td-cell-title-font | @font-body-large | -
--td-cell-vertical-padding | 32rpx | -

View File

@@ -0,0 +1,94 @@
.t-cell {
position: relative;
display: flex;
box-sizing: border-box;
width: 100%;
padding: var(--td-cell-vertical-padding, 32rpx) var(--td-cell-horizontal-padding, 32rpx);
height: var(--td-cell-height, auto);
background-color: var(--td-cell-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-cell--bordered::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
left: 0;
bottom: 0;
border-bottom: var(--td-cell-border-width, 1px) solid var(--td-cell-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
transform: scaleY(0.5);
transform-origin: bottom;
left: var(--td-cell-border-left-space, var(--td-cell-horizontal-padding, 32rpx));
right: var(--td-cell-border-right-space, 0);
}
.t-cell__description {
font: var(--td-cell-description-font, var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-cell-description-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
}
.t-cell__description-text {
margin-top: calc(var(--td-spacer, 16rpx) / 2);
}
.t-cell__note {
display: flex;
align-items: center;
justify-content: flex-end;
color: var(--td-cell-note-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
font-size: var(--td-cell-note-font-size, var(--td-font-size-m, 32rpx));
}
.t-cell__title {
margin-right: var(--td-spacer-2, 32rpx);
}
.t-cell__title,
.t-cell__note {
flex: 1 1 auto;
}
.t-cell__title:empty,
.t-cell__note:empty {
display: none;
}
.t-cell__title-text {
display: flex;
font: var(--td-cell-title-font, var(--td-font-body-large, 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-cell-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-cell__left,
.t-cell__right {
align-self: stretch;
}
.t-cell__left:not(:empty) {
margin-right: var(--td-spacer-1, 24rpx);
}
.t-cell__left-icon {
color: var(--td-cell-left-icon-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
font-size: var(--td-cell-left-icon-size, 48rpx);
}
.t-cell__left-image {
height: var(--td-cell-image-height, 96rpx);
width: var(--td-cell-image-width, 96rpx);
}
.t-cell__note:not(:empty) + .t-cell__right {
margin-left: calc(var(--td-spacer, 16rpx) / 2);
}
.t-cell__right {
display: flex;
}
.t-cell__right-icon {
color: var(--td-cell-right-icon-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
font-size: var(--td-cell-right-icon-size, 48rpx);
}
.t-cell__right--middle {
align-items: center;
}
.t-cell__right--top {
align-items: flex-start;
}
.t-cell__right--bottom {
align-items: flex-end;
}
.t-cell--hover {
background-color: var(--td-cell-hover-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
}
.t-cell--required {
font-size: var(--td-cell-required-font-size, var(--td-font-size-m, 32rpx));
color: var(--td-cell-required-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}

View File

@@ -0,0 +1,260 @@
<template>
<view
:style="tools._style([customStyle])"
:class="[
tClass,
tools.cls(classPrefix, [['bordered', bordered || isLastChild]])
]"
:hover-class="hover ? classPrefix + '--hover' : ''"
hover-stay-time="70"
:aria-role="ariaRole || (arrow ? 'button' : '')"
:aria-label="ariaLabel"
@click="onClick"
>
<view :class="classPrefix + '__left ' + tClassLeft">
<block
v-if="_leftIcon"
name="icon"
>
<t-icon
:custom-style="leftIconCustomStyle"
:t-class="classPrefix + '__left-icon ' + tClassLeftIcon"
:name="_leftIcon.name"
:size="_leftIcon.size"
:color="_leftIcon.color"
:aria-hidden="true"
:aria-label="_leftIcon.ariaLabel"
:aria-role="_leftIcon.ariaRole"
@click="'handleClose' || ''"
/>
</block>
<slot name="left-icon" />
<t-image
v-if="image"
shape="round"
:t-class="classPrefix + '__left-image ' + tClassImage"
:src="image"
:custom-style="leftImageCustomStyle"
/>
<slot name="image" />
</view>
<view :class="classPrefix + '__title ' + tClassCenter">
<view
:class="[
classPrefix + '__title-text ',
tClassTitle
]"
:style="tools._style(titleStyle)"
>
<block v-if="title">
{{ title }}
</block>
<slot name="title" />
<block v-if="required">
<text
decode
:class="classPrefix + '--required'"
>
&nbsp;*
</text>
</block>
</view>
<view
:class="[
classPrefix + '__description ',
tClassDescription
]"
>
<view
v-if="description"
:class="classPrefix + '__description-text'"
>
{{ description }}
</view>
<slot name="description" />
</view>
</view>
<view
:class="[
classPrefix + '__note ',
tClassNote
]"
:style="tools._style(noteStyle)"
>
<text v-if="note">
{{ note }}
</text>
<slot name="note" />
</view>
<view
:class="[
tools.cls(classPrefix + '__right', [align]),
tClassRight
]"
>
<t-icon
v-if="_arrow"
:custom-style="rightArrowCustomStyle"
:t-class=" classPrefix + '__right-icon ' + tClassRightIcon"
:name="_arrow.name || ''"
:size="_arrow.size"
:color="_arrow.color"
:aria-hidden="true"
:aria-label="_arrow.ariaLabel"
:aria-role="_arrow.ariaRole"
@click="'handleClose' || ''"
/>
<block v-else>
<block
v-if="_rightIcon"
name="icon"
>
<t-icon
:custom-style="rightIconCustomStyle"
:t-class=" classPrefix + '__right-icon ' + tClassRightIcon"
:name="_rightIcon.name"
:size="_rightIcon.size"
:color="_rightIcon.color || ''"
:aria-hidden="true"
:aria-label="_rightIcon.ariaLabel"
:aria-role="_rightIcon.ariaRole"
@click="'handleClose' || ''"
/>
</block>
<slot name="right-icon" />
</block>
</view>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import TImage from '../image/image';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { calcIcon } from '../common/utils';
import tools from '../common/utils.wxs';
import { ChildrenMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-cell`;
const COMMON_RIGHT_ICON_STYLE = {
color: 'var(--td-cell-right-icon-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, .4))))',
fontSize: 'var(--td-cell-right-icon-font-size, 24px)',
};
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-title`,
`${prefix}-class-description`,
`${prefix}-class-note`,
`${prefix}-class-hover`,
`${prefix}-class-image`,
`${prefix}-class-left`,
`${prefix}-class-left-icon`,
`${prefix}-class-center`,
`${prefix}-class-right`,
`${prefix}-class-right-icon`,
],
mixins: [ChildrenMixin(RELATION_MAP.Cell)],
components: {
TIcon,
TImage,
},
props: {
...props,
},
emits: [
'click',
],
data() {
return {
prefix,
classPrefix: name,
_arrow: null,
_rightIcon: null,
_leftIcon: null,
isLastChild: false,
tools,
};
},
computed: {
rightArrowCustomStyle() {
return tools._style([
COMMON_RIGHT_ICON_STYLE,
this.rightIconStyle || '',
this._arrow.style || '',
]);
},
rightIconCustomStyle() {
return tools._style([
COMMON_RIGHT_ICON_STYLE,
this.rightIconStyle || '',
this._rightIcon.style || '',
]);
},
leftIconCustomStyle() {
return tools._style([
{
color: 'var(--td-cell-left-icon-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)))',
fontSize: 'var(--td-cell-left-icon-font-size, 24px)',
},
this._leftIcon.style || '',
]);
},
leftImageCustomStyle() {
return tools._style({
height: 'var(--td-cell-image-height, 48px)',
width: 'var(--td-cell-image-width, 48px)',
});
},
},
watch: {
leftIcon: {
handler(e) {
this.setIcon('_leftIcon', e, '');
},
immediate: true,
},
rightIcon: {
handler(e) {
this.setIcon('_rightIcon', e, '');
},
immediate: true,
},
arrow: {
handler(e) {
this.setIcon('_arrow', e, 'chevron-right');
},
immediate: true,
},
},
methods: {
setIcon(e, t, s) {
this[e] = calcIcon(t, s);
},
onClick(e) {
this.$emit('click', e);
this.jumpLink();
},
jumpLink(e = 'url', t = 'jumpType') {
const s = this[e];
const i = this[t];
if (s) {
uni[i]({
url: s,
});
}
},
},
});
</script>
<style scoped>
@import './cell.css';
</style>

View File

@@ -0,0 +1,87 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCellProps } from './type';
export default {
/** 右侧内容的对齐方式,默认居中对齐 */
align: {
type: String,
default: 'middle' as TdCellProps['align'],
validator(val: TdCellProps['align']): boolean {
if (!val) return true;
return ['top', 'middle', 'bottom'].includes(val);
},
},
/** 是否显示右侧箭头 */
arrow: {
type: [Boolean, Object],
default: false as TdCellProps['arrow'],
},
/** 是否显示下边框 */
bordered: {
type: Boolean,
default: true,
},
/** 下方内容描述 */
description: {
type: String,
},
/** 是否开启点击反馈 */
hover: Boolean,
/** 主图 */
image: {
type: String,
},
/** 链接跳转类型 */
jumpType: {
type: String,
default: 'navigateTo' as TdCellProps['jumpType'],
validator(val: TdCellProps['jumpType']): boolean {
if (!val) return true;
return ['switchTab', 'reLaunch', 'redirectTo', 'navigateTo'].includes(val);
},
},
/** 左侧图标,出现在单元格标题的左侧 */
leftIcon: {
type: [String, Object],
},
/** 和标题同行的说明文字 */
note: {
type: String,
},
/** 说明文字自定义样式 */
noteStyle: {
type: [String, Object],
},
/** 是否显示表单必填星号 */
required: Boolean,
/** 最右侧图标 */
rightIcon: {
type: [String, Object],
},
/** 右侧图标自定义样式 */
rightIconStyle: {
type: [String, Object],
},
/** 标题 */
title: {
type: String,
},
/** 标题自定义样式 */
titleStyle: {
type: [String, Object],
},
/** 点击后跳转链接地址。如果值为空,则表示不需要跳转 */
url: {
type: String,
default: '',
},
/** 右侧内容 */
onClick: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,82 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdCellProps {
/**
* 右侧内容的对齐方式,默认居中对齐
* @default middle
*/
align?: 'top' | 'middle' | 'bottom';
/**
* 是否显示右侧箭头
* @default false
*/
arrow?: boolean | object;
/**
* 是否显示下边框
* @default true
*/
bordered?: boolean;
/**
* 下方内容描述
*/
description?: string;
/**
* 是否开启点击反馈
*/
hover?: boolean;
/**
* 主图
*/
image?: string;
/**
* 链接跳转类型
* @default navigateTo
*/
jumpType?: 'switchTab' | 'reLaunch' | 'redirectTo' | 'navigateTo';
/**
* 左侧图标,出现在单元格标题的左侧
*/
leftIcon?: string | object;
/**
* 和标题同行的说明文字
*/
note?: string;
/**
* 说明文字自定义样式
*/
noteStyle?: string | object;
/**
* 是否显示表单必填星号
* @default false
*/
required?: boolean;
/**
* 最右侧图标
*/
rightIcon?: string | object;
/**
* 右侧图标自定义样式
*/
rightIconStyle?: string | object;
/**
* 标题
*/
title?: string;
/**
* 标题自定义样式
*/
titleStyle?: string | object;
/**
* 点击后跳转链接地址。如果值为空,则表示不需要跳转
* @default ''
*/
url?: string;
/**
* 右侧内容
*/
onClick?: (e: MouseEvent) => void;
}

View File

@@ -0,0 +1,197 @@
.t-tag {
display: inline-flex;
align-items: center;
border: 2rpx solid transparent;
box-sizing: border-box;
border-radius: var(--td-tag-square-border-radius, 8rpx);
user-select: none;
vertical-align: middle;
}
.t-tag__text {
word-wrap: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.t-tag__icon {
display: flex;
align-items: center;
}
.t-tag__icon:not(:empty) + .t-tag__text:not(:empty) {
margin-left: 8rpx;
}
.t-tag--small {
padding: var(--td-tag-small-padding, 2rpx 10rpx);
font: var(--td-tag-small-font, var(--td-font-body-extraSmall, 20rpx / 32rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
}
.t-tag--small .t-icon,
.t-tag--small .t-icon-close {
font-size: var(--td-tag-small-icon-size, 24rpx);
}
.t-tag--medium {
padding: var(--td-tag-medium-padding, 2rpx 14rpx);
font: var(--td-tag-medium-font, var(--td-font-body-small, 24rpx / 40rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
}
.t-tag--medium .t-icon,
.t-tag--medium .t-icon-close {
font-size: var(--td-tag-medium-icon-size, 28rpx);
}
.t-tag--large {
padding: var(--td-tag-large-padding, 4rpx 14rpx);
font: var(--td-tag-large-font, var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
}
.t-tag--large .t-icon,
.t-tag--large .t-icon-close {
font-size: var(--td-tag-large-icon-size, 32rpx);
}
.t-tag--extra-large {
padding: var(--td-tag-extra-large-padding, 16rpx 30rpx);
font: var(--td-tag-extra-large-font, var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
}
.t-tag--extra-large .t-icon,
.t-tag--extra-large .t-icon-close {
font-size: var(--td-tag-extra-large-icon-size, 32rpx);
}
.t-tag.t-tag--square {
border-radius: var(--td-tag-square-border-radius, 8rpx);
}
.t-tag.t-tag--round {
border-radius: var(--td-tag-round-border-radius, 999px);
}
.t-tag.t-tag--mark {
border-radius: 0 var(--td-tag-mark-border-radius, var(--td-tag-round-border-radius, 999px)) var(--td-tag-mark-border-radius, var(--td-tag-round-border-radius, 999px)) 0;
}
.t-tag--dark.t-tag--default {
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
border-color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
background-color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
}
.t-tag--dark.t-tag--primary {
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
border-color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-tag--dark.t-tag--success {
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
border-color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
background-color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
}
.t-tag--dark.t-tag--warning {
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
border-color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
background-color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
}
.t-tag--dark.t-tag--danger {
color: var(--td-text-color-anti, var(--td-font-white-1, #ffffff));
border-color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
background-color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
}
.t-tag--dark.t-tag--default {
color: var(--td-tag-default-font-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-tag--outline.t-tag--default {
color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
background-color: var(--td-tag-default-light-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
}
.t-tag--outline.t-tag--primary {
color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
border-color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-tag-primary-light-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-tag--outline.t-tag--success {
color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
border-color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
background-color: var(--td-tag-success-light-color, var(--td-success-color-1, #e3f9e9));
}
.t-tag--outline.t-tag--warning {
color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
border-color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
background-color: var(--td-tag-warning-light-color, var(--td-warning-color-1, #fff1e9));
}
.t-tag--outline.t-tag--danger {
color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
border-color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
background-color: var(--td-tag-danger-light-color, var(--td-error-color-1, #fff0ed));
}
.t-tag--outline.t-tag--default {
color: var(--td-tag-default-font-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-tag--outline.t-tag--default {
background-color: var(--td-tag-outline-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-tag--outline.t-tag--primary {
background-color: var(--td-tag-outline-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-tag--outline.t-tag--success {
background-color: var(--td-tag-outline-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-tag--outline.t-tag--warning {
background-color: var(--td-tag-outline-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-tag--outline.t-tag--danger {
background-color: var(--td-tag-outline-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-tag--light.t-tag--default {
color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-tag-default-light-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
background-color: var(--td-tag-default-light-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
}
.t-tag--light.t-tag--primary {
color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
border-color: var(--td-tag-primary-light-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
background-color: var(--td-tag-primary-light-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-tag--light.t-tag--success {
color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
border-color: var(--td-tag-success-light-color, var(--td-success-color-1, #e3f9e9));
background-color: var(--td-tag-success-light-color, var(--td-success-color-1, #e3f9e9));
}
.t-tag--light.t-tag--warning {
color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
border-color: var(--td-tag-warning-light-color, var(--td-warning-color-1, #fff1e9));
background-color: var(--td-tag-warning-light-color, var(--td-warning-color-1, #fff1e9));
}
.t-tag--light.t-tag--danger {
color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
border-color: var(--td-tag-danger-light-color, var(--td-error-color-1, #fff0ed));
background-color: var(--td-tag-danger-light-color, var(--td-error-color-1, #fff0ed));
}
.t-tag--light.t-tag--default {
color: var(--td-tag-default-font-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-tag--light-outline.t-tag--default {
color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
border-color: var(--td-tag-default-color, var(--td-bg-color-component, var(--td-gray-color-3, #e7e7e7)));
background-color: var(--td-tag-default-light-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
}
.t-tag--light-outline.t-tag--primary {
color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
border-color: var(--td-tag-primary-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-tag-primary-light-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-tag--light-outline.t-tag--success {
color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
border-color: var(--td-tag-success-color, var(--td-success-color, var(--td-success-color-5, #2ba471)));
background-color: var(--td-tag-success-light-color, var(--td-success-color-1, #e3f9e9));
}
.t-tag--light-outline.t-tag--warning {
color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
border-color: var(--td-tag-warning-color, var(--td-warning-color, var(--td-warning-color-5, #e37318)));
background-color: var(--td-tag-warning-light-color, var(--td-warning-color-1, #fff1e9));
}
.t-tag--light-outline.t-tag--danger {
color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
border-color: var(--td-tag-danger-color, var(--td-error-color, var(--td-error-color-6, #d54941)));
background-color: var(--td-tag-danger-light-color, var(--td-error-color-1, #fff0ed));
}
.t-tag--light-outline.t-tag--default {
color: var(--td-tag-default-font-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
border-color: var(--td-component-border, var(--td-gray-color-4, #dcdcdc));
}
.t-tag.t-tag--closable.t-tag--disabled {
cursor: not-allowed;
color: var(--td-tag-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
background-color: var(--td-tag-disabled-background-color, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
border-color: var(--td-tag-disabled-border-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
}

View File

@@ -0,0 +1,149 @@
<template>
<view
:style="tools._style([customStyle])"
:class="className + ' ' + tClass"
@click="onClick"
>
<view
:aria-hidden="true"
:class="classPrefix + '__icon'"
>
<block
v-if="_icon"
name="icon"
>
<t-icon
:custom-style="_icon.style || ''"
:t-class="prefix + '-icon'"
:prefix="_icon.prefix"
:name="_icon.name"
:size="_icon.size"
:color="_icon.color"
:aria-hidden="!!_icon.ariaHidden"
:aria-label="_icon.ariaLabel"
:aria-role="_icon.ariaRole"
/>
</block>
<slot name="icon" />
</view>
<view :class="classPrefix + '__text'">
<slot />
<slot name="content" />
<block v-if="tools.isArray(content) && content.length == 2">
{{ dataChecked ? content[0] : content[1] }}
</block>
<block v-else>
{{ content }}
</block>
</view>
<t-icon
v-if="closable"
:class="classPrefix + '__icon-close'"
:t-class="prefix + '-icon'"
name="close"
aria-role="button"
aria-label="关闭"
@click="onClose"
/>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { classNames, calcIcon, coalesce } from '../common/utils';
import tools from '../common/utils.wxs';
const name = `${prefix}-tag`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [{
key: 'checked',
event: 'change',
}],
externalClasses: [
`${prefix}-class`,
],
components: {
TIcon,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
className: '',
tools,
_icon: null,
dataChecked: coalesce(this.checked, this.defaultChecked),
};
},
watch: {
size: 'setClass',
disabled: 'setClass',
dataChecked: 'setClass',
icon: {
handler(e) {
this._icon = calcIcon(e);
},
immediate: true,
},
checked: {
handler(value) {
this.dataChecked = value;
},
immediate: true,
},
},
mounted() {
this.setClass();
},
methods: {
setClass() {
const { classPrefix } = this;
const { size, variant, disabled, dataChecked, shape } = this;
const tagClass = [
classPrefix,
`${classPrefix}--checkable`,
disabled ? `${classPrefix}--disabled` : '',
dataChecked ? `${classPrefix}--checked` : '',
`${classPrefix}--${dataChecked ? 'primary' : 'default'}`,
`${classPrefix}--${size}`,
`${classPrefix}--${variant}`,
`${classPrefix}--${shape}`,
];
const className = classNames(tagClass);
this.className = className;
},
onClick() {
if (this.disabled) return;
const { dataChecked } = this;
this._trigger('click');
this._trigger('change', { checked: !dataChecked });
},
onClose(e) {
if (this.disabled) return;
this._trigger('close', e);
},
},
});
</script>
<style scoped>
@import './check-tag.css';
</style>

View File

@@ -0,0 +1,67 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCheckTagProps } from './type';
export default {
/** 标签选中的状态默认风格theme=default才有选中态 */
checked: Boolean,
/** 标签选中的状态默认风格theme=default才有选中态非受控属性 */
defaultChecked: Boolean,
/** 标签是否可关闭 */
closable: Boolean,
/** 组件子元素;传入数组时:[选中内容,非选中内容] */
content: {
type: [String, Number, Array],
},
/** 标签禁用态失效标签不能触发事件。默认风格theme=default才有禁用态 */
disabled: Boolean,
/** 标签图标 */
icon: {
type: [String, Object],
},
/** 标签类型,有三种:方形、圆角方形、标记型 */
shape: {
type: String,
default: 'square' as TdCheckTagProps['shape'],
validator(val: TdCheckTagProps['shape']): boolean {
if (!val) return true;
return ['square', 'round', 'mark'].includes(val);
},
},
/** 标签尺寸 */
size: {
type: String,
default: 'medium' as TdCheckTagProps['size'],
validator(val: TdCheckTagProps['size']): boolean {
if (!val) return true;
return ['small', 'medium', 'large'].includes(val);
},
},
/** 标签风格变体 */
variant: {
type: String,
default: 'dark' as TdCheckTagProps['variant'],
validator(val: TdCheckTagProps['variant']): boolean {
if (!val) return true;
return ['dark', 'light', 'outline', 'light-outline'].includes(val);
},
},
/** 状态切换时触发 */
onChange: {
type: Function,
default: () => ({}),
},
/** 点击标签时触发 */
onClick: {
type: Function,
default: () => ({}),
},
/** 如果关闭按钮存在,点击关闭按钮时触发 */
onClose: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,63 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { SizeEnum } from '../common/common';
export interface TdCheckTagProps {
/**
* 标签选中的状态默认风格theme=default才有选中态
*/
checked?: boolean;
/**
* 标签选中的状态默认风格theme=default才有选中态非受控属性
*/
defaultChecked?: boolean;
/**
* 标签是否可关闭
* @default false
*/
closable?: boolean;
/**
* 组件子元素;传入数组时:[选中内容,非选中内容]
*/
content?: string | number | string[];
/**
* 标签禁用态失效标签不能触发事件。默认风格theme=default才有禁用态
* @default false
*/
disabled?: boolean;
/**
* 标签图标
*/
icon?: string | object;
/**
* 标签类型,有三种:方形、圆角方形、标记型
* @default square
*/
shape?: 'square' | 'round' | 'mark';
/**
* 标签尺寸
* @default medium
*/
size?: SizeEnum;
/**
* 标签风格变体
* @default dark
*/
variant?: 'dark' | 'light' | 'outline' | 'light-outline';
/**
* 状态切换时触发
*/
onChange?: (context: { checked: boolean }) => void;
/**
* 点击标签时触发
*/
onClick?: () => void;
/**
* 如果关闭按钮存在,点击关闭按钮时触发
*/
onClose?: () => void;
}

View File

@@ -0,0 +1,261 @@
<template>
<view
:class="classPrefix + ' ' + tClass"
:style="tools._style([customStyle])"
>
<slot />
<t-checkbox
v-for="(item, index) in checkboxOptions"
:key="index"
:ref="prefix + '-checkbox-option'"
:class="prefix + '-checkbox-option'"
:data-item="item"
:label="item.label || item.text || ''"
:value="item.value == null ? '' : item.value"
:block="item.block || true"
:check-all="item.checkAll || false"
:checked="item.checked || false"
:content="item.content || ''"
:content-disabled="item.contentDisabled || false"
:icon="item.icon || 'circle'"
:indeterminate="item.indeterminate || false"
:disabled="item.disabled == null ? disabled : item.disabled"
:max-content-row="item.maxContentRow || 5"
:max-label-row="item.maxLabelRow || 3"
:name="item.name || ''"
:borderless="borderless"
:readonly="item.readonly || false"
:placement="item.placement || 'left'"
:relation-key="relationKey"
@change="({checked}) => handleInnerChildChange($event, { item, checked })"
/>
</view>
</template>
<script>
import TCheckbox from '../checkbox/checkbox';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import { coalesce } from '../common/utils';
import props from './props';
import tools from '../common/utils.wxs';
import { ParentMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-checkbox-group`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'value',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
],
inject: {
[RELATION_MAP.FormKey]: {
default: null,
},
},
mixins: [ParentMixin(RELATION_MAP.Checkbox)],
components: {
TCheckbox,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
checkboxOptions: [],
tools,
dataValue: coalesce(this.value, this.defaultValue),
};
},
watch: {
value: {
handler(v) {
this.dataValue = v;
},
immediate: true,
deep: true,
},
dataValue: {
handler() {
this.updateChildren();
},
immediate: true,
deep: true,
},
options: {
handler() {
this.initWithOptions();
},
immediate: true,
deep: true,
},
disabled: {
handler(v) {
if (this.options?.length) {
this.initWithOptions();
return;
}
this.getChildren()?.forEach((item) => {
item.setDisabled(v);
});
},
immediate: true,
},
},
created() {
this.$checkAll = null;
},
mounted() {
this.setCheckall();
this.getChildren()?.forEach((item) => {
item.setDisabled(this.disabled);
});
setTimeout(() => {
this.updateChildren();
}, 33);
},
methods: {
getChildren() {
let items = this.children;
if (!items?.length) {
items = this.$refs[`${prefix}-checkbox-option`];
}
return items || [];
},
updateChildren() {
const items = this.getChildren();
const { dataValue } = this;
if (items.length > 0) {
items.forEach((item) => {
if (!item.checkAll) {
item.dataChecked = dataValue?.includes(item.value);
}
});
// 关联可全选项
if (items.some(item => item.checkAll)) {
this.setCheckall();
}
}
},
updateValue({ validChildren, trigger, value, checked, checkAll, item, indeterminate }) {
let { dataValue: newValue } = this;
const { max } = this;
if (validChildren !== false) {
const keySet = new Set(this.getChildren().map(item => item.value));
newValue = newValue.filter(value => keySet.has(value));
}
if (max && checked && newValue.length === max) return;
if (checkAll) {
const items = this.getChildren();
newValue = !checked && indeterminate
? items
.filter(data => !(data.disabled && !newValue.includes(data.value)))
.map(item => item.value)
: items
.filter((data) => {
if (data.disabled) {
return newValue.includes(data.value);
}
return checked && !data.checkAll;
})
.map(data => data.value);
} else if (checked) {
newValue = newValue.concat(value);
} else {
const index = newValue.findIndex(v => v === value);
newValue.splice(index, 1);
}
if (trigger !== 'init') {
this._trigger('change', { value: newValue, context: item });
this.onChange(newValue);
}
},
onChange(value) {
if (this[RELATION_MAP.FormKey]
&& this[RELATION_MAP.FormKey].onValueChange) {
this[RELATION_MAP.FormKey].onValueChange(value);
}
},
initWithOptions() {
const { options, dataValue: value, keys } = this;
if (!options?.length || !Array.isArray(options)) return;
const checkboxOptions = options.map((item) => {
const isLabel = ['number', 'string'].includes(typeof item);
return isLabel
? {
label: `${item}`,
value: item,
checked: value?.includes(item),
}
: {
...item,
label: item[coalesce(keys?.label, 'label')],
value: item[coalesce(keys?.value, 'value')],
checked: value?.includes(item[coalesce(keys?.value, 'value')]),
};
});
this.checkboxOptions = checkboxOptions;
},
handleInnerChildChange(tools, { item, checked }) {
const rect = {};
if (item.checkAll) {
rect.indeterminate = this.$checkAll?.indeterminate;
}
this.updateValue({ ...item, checked, item, ...rect });
},
setCheckall() {
const items = this.getChildren();
if (!this.$checkAll) {
this.$checkAll = items.find(item => item.checkAll);
}
if (!this.$checkAll) return;
const { dataValue } = this;
const valueSet = new Set(dataValue?.filter(val => val !== this.$checkAll.value));
const isCheckall = items.every(item => (item.checkAll ? true : valueSet.has(item.value)));
this.$checkAll.dataChecked = valueSet.size > 0;
this.$checkAll.dataIndeterminate = !isCheckall;
},
},
});
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,58 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCheckboxGroupProps } from './type';
export default {
/** 是否开启无边框模式。优先级低于 Checkbox.borderless */
borderless: Boolean,
/** 是否禁用组件。优先级Form.disabled < CheckboxGroup.disabled < Checkbox.disabled */
disabled: {
type: Boolean,
default: undefined,
},
/** 用来定义 value / label / disabled 在 `options` 中对应的字段别名 */
keys: {
type: Object,
},
/** 支持最多选中的数量 */
max: {
type: Number,
default: undefined,
},
/** 统一设置内部复选框 HTML 属性 */
name: {
type: String,
default: '',
},
/** 以配置形式设置子元素。示例1`['北京', '上海']` 示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」 */
options: {
type: Array,
default: (): TdCheckboxGroupProps['options'] => [],
},
/** 只读状态 */
readonly: {
type: Boolean,
default: undefined,
},
/** -1 时代表独立,不再寻找 parent用于头条小程序 */
relationKey: {
type: String,
default: '',
},
/** 选中值 */
value: {
type: Array,
},
/** 选中值,非受控属性 */
defaultValue: {
type: Array,
},
/** 值变化时触发。`context` 表示当前点击项内容 */
onChange: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,72 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { KeysType } from '../common/common';
export interface TdCheckboxGroupProps<T = CheckboxGroupValue> {
/**
* 是否开启无边框模式。优先级低于 Checkbox.borderless
* @default false
*/
borderless?: boolean;
/**
* 是否禁用组件。优先级Form.disabled < CheckboxGroup.disabled < Checkbox.disabled
*/
disabled?: boolean;
/**
* 用来定义 value / label / disabled 在 `options` 中对应的字段别名
*/
keys?: KeysType;
/**
* 支持最多选中的数量
*/
max?: number;
/**
* 统一设置内部复选框 HTML 属性
* @default ''
*/
name?: string;
/**
* 以配置形式设置子元素。示例1`['北京', '上海']` 示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」
* @default []
*/
options?: Array<CheckboxOption>;
/**
* 只读状态
*/
readonly?: boolean;
/**
* -1 时代表独立,不再寻找 parent用于头条小程序
* @default ''
*/
relationKey?: string;
/**
* 选中值
*/
value?: T;
/**
* 选中值,非受控属性
*/
defaultValue?: T;
/**
* 值变化时触发。`context` 表示当前点击项内容
*/
onChange?: (context: {
value: CheckboxGroupValue;
context: { value: boolean | number | string; label: boolean | number | string };
}) => void;
}
export type CheckboxOption = string | number | CheckboxOptionObj;
export interface CheckboxOptionObj {
label?: string;
value?: string | number;
disabled?: boolean;
checkAll?: true;
}
export type CheckboxGroupValue = Array<string | number | boolean>;

View File

@@ -0,0 +1,103 @@
:: BASE_DOC ::
## API
### Checkbox Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
block | Boolean | true | \- | N
borderless | Boolean | undefined | \- | N
check-all | Boolean | false | \- | N
checked | Boolean | - | `v-model:checked` is supported | N
default-checked | Boolean | - | uncontrolled property | N
content | String | - | \- | N
content-disabled | Boolean | - | \- | N
disabled | Boolean | undefined | \- | N
icon | String / Array | 'circle' | Typescript`'circle' \| 'line' \| 'rectangle' \| string[]` | N
indeterminate | Boolean | false | \- | N
label | String | - | \- | N
max-content-row | Number | 5 | \- | N
max-label-row | Number | 3 | \- | N
name | String | - | \- | N
placement | String | left | options: left/right | N
readonly | Boolean | undefined | \- | N
relation-key | String | - | \- | N
value | String / Number / Boolean | - | value of checkbox。Typescript`string \| number \| boolean` | N
### Checkbox Events
name | params | description
-- | -- | --
change | `(context: { checked: boolean, context: { value: boolean\|number\|string, label: boolean\|number\|string }})` | \-
### Checkbox Slots
name | Description
-- | --
\- | \-
content | \-
label | \-
### Checkbox External Classes
className | Description
-- | --
t-class | \-
t-class-border | \-
t-class-content | \-
t-class-icon | \-
t-class-label | \-
### CheckboxGroup Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
borderless | Boolean | false | \- | N
disabled | Boolean | undefined | \- | N
keys | Object | - | Typescript`KeysType`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
max | Number | undefined | \- | N
name | String | - | \- | N
options | Array | [] | Typescript`Array<CheckboxOption>` `type CheckboxOption = string \| number \| CheckboxOptionObj` `interface CheckboxOptionObj { label?: string; value?: string \| number; disabled?: boolean; checkAll?: true }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
readonly | Boolean | undefined | \- | N
relation-key | String | - | \- | N
value | Array | - | `v-model:value` is supported。Typescript`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
default-value | Array | - | uncontrolled property。Typescript`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
### CheckboxGroup Events
name | params | description
-- | -- | --
change | `(context: { value: CheckboxGroupValue, context: { value: boolean\|number\|string, label: boolean\|number\|string }})` | \-
### CheckboxGroup Slots
name | Description
-- | --
\- | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-checkbox-bg-color | @bg-color-container | -
--td-checkbox-border-color | @component-stroke | -
--td-checkbox-description-color | @text-color-secondary | -
--td-checkbox-description-disabled-color | @text-color-disabled | -
--td-checkbox-description-font | @font-body-medium | -
--td-checkbox-icon-checked-color | @brand-color | -
--td-checkbox-icon-color | @component-border | -
--td-checkbox-icon-disabled-bg-color | @bg-color-component-disabled | -
--td-checkbox-icon-disabled-color | @brand-color-disabled | -
--td-checkbox-icon-size | 48rpx | -
--td-checkbox-tag-active-bg-color | @brand-color-light | -
--td-checkbox-tag-active-color | @brand-color | -
--td-checkbox-title-color | @text-color-primary | -
--td-checkbox-title-disabled-color | @text-color-disabled | -
--td-checkbox-title-font | @font-body-large | -
--td-checkbox-title-line-height | 48rpx | -
--td-checkbox-vertical-padding | @spacer-2 | -

View File

@@ -0,0 +1,158 @@
---
title: Checkbox 多选框
description: 用于预设的一组选项中执行多项选择,并呈现选择结果。
spline: form
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCheckbox from '@tdesign/uniapp/checkbox/checkbox.vue';
import TCheckboxGroup from '@tdesign/uniapp/checkbox-group/checkbox-group.vue';
```
### 组件类型
纵向多选框
{{ base }}
横向多选框
{{ horizontal }}
带全选多选框
{{ all }}
### 组件状态
多选框状态
{{ status }}
### 组件样式
勾选样式
{{ type }}
勾选显示位置
{{ right }}
非通栏多选样式
{{ card }}
### 组件规格
多选框尺寸规格
{{ special }}
## API
### Checkbox Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
block | Boolean | true | 是否为块级元素 | N
borderless | Boolean | undefined | 是否开启无边框模式 | N
check-all | Boolean | false | 用于标识是否为「全选选项」。单独使用无效,需在 CheckboxGroup 中使用 | N
checked | Boolean | - | 是否选中。支持语法糖 `v-model:checked` | N
default-checked | Boolean | - | 是否选中。非受控属性 | N
content | String | - | 多选框内容 | N
content-disabled | Boolean | - | 是否禁用组件内容content触发选中 | N
disabled | Boolean | undefined | 是否禁用组件。如果父组件存在 CheckboxGroup默认值由 CheckboxGroup.disabled 控制。优先级Checkbox.disabled > CheckboxGroup.disabled > Form.disabled | N
icon | String / Array | 'circle' | 自定义选中图标和非选中图标。使用 Array 时表示:`[选中态图标,非选中态图标,半选中态图标]`。使用 String 时,值为 circle 表示填充圆形图标、值为 line 表示描边型图标、值为 rectangle 表示填充矩形图标。TS 类型:`'circle' \| 'line' \| 'rectangle' \| string[]` | N
indeterminate | Boolean | false | 是否为半选 | N
label | String | - | 主文案 | N
max-content-row | Number | 5 | 内容最大行数限制 | N
max-label-row | Number | 3 | 主文案最大行数限制 | N
name | String | - | HTML 元素原生属性 | N
placement | String | left | 多选框和内容相对位置。可选项left/right | N
readonly | Boolean | undefined | 只读状态 | N
relation-key | String | - | -1 时代表独立,不再寻找 parent用于头条小程序 | N
value | String / Number / Boolean | - | 多选框的值。TS 类型:`string \| number \| boolean` | N
### Checkbox Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { checked: boolean, context: { value: boolean\|number\|string, label: boolean\|number\|string }})` | 值变化时触发。`context` 表示当前点击项内容
### Checkbox Slots
名称 | 描述
-- | --
\- | 默认插槽,主文案
content | 自定义 `content` 显示内容
label | 自定义 `label` 显示内容
### Checkbox External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-border | 边框样式类
t-class-content | 内容样式类
t-class-icon | 图标样式类
t-class-label | 标签样式类
### CheckboxGroup Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
borderless | Boolean | false | 是否开启无边框模式。优先级低于 Checkbox.borderless | N
disabled | Boolean | undefined | 是否禁用组件。优先级Form.disabled < CheckboxGroup.disabled < Checkbox.disabled | N
keys | Object | - | 用来定义 value / label / disabled `options` 中对应的字段别名TS 类型`KeysType`[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/common/common.ts) | N
max | Number | undefined | 支持最多选中的数量 | N
name | String | - | 统一设置内部复选框 HTML 属性 | N
options | Array | [] | 以配置形式设置子元素示例1`['北京', '上海']` 示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`checkAll 值为 true 表示当前选项为全选选项」。TS 类型`Array<CheckboxOption>` `type CheckboxOption = string \| number \| CheckboxOptionObj` `interface CheckboxOptionObj { label?: string; value?: string \| number; disabled?: boolean; checkAll?: true }`[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
readonly | Boolean | undefined | 只读状态 | N
relation-key | String | - | -1 时代表独立不再寻找 parent用于头条小程序 | N
value | Array | - | 选中值支持语法糖 `v-model:value`TS 类型`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
default-value | Array | - | 选中值非受控属性TS 类型`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/checkbox-group/type.ts) | N
### CheckboxGroup Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { value: CheckboxGroupValue, context: { value: boolean\|number\|string, label: boolean\|number\|string }})` | 值变化时触发`context` 表示当前点击项内容
### CheckboxGroup Slots
名称 | 描述
-- | --
\- | 默认插槽多选框组内容
### CSS Variables
组件提供了下列 CSS 变量可用于自定义样式
名称 | 默认值 | 描述
-- | -- | --
--td-checkbox-bg-color | @bg-color-container | -
--td-checkbox-border-color | @component-stroke | -
--td-checkbox-description-color | @text-color-secondary | -
--td-checkbox-description-disabled-color | @text-color-disabled | -
--td-checkbox-description-font | @font-body-medium | -
--td-checkbox-icon-checked-color | @brand-color | -
--td-checkbox-icon-color | @component-border | -
--td-checkbox-icon-disabled-bg-color | @bg-color-component-disabled | -
--td-checkbox-icon-disabled-color | @brand-color-disabled | -
--td-checkbox-icon-size | 48rpx | -
--td-checkbox-tag-active-bg-color | @brand-color-light | -
--td-checkbox-tag-active-color | @brand-color | -
--td-checkbox-title-color | @text-color-primary | -
--td-checkbox-title-disabled-color | @text-color-disabled | -
--td-checkbox-title-font | @font-body-large | -
--td-checkbox-title-line-height | 48rpx | -
--td-checkbox-vertical-padding | @spacer-2 | -

View File

@@ -0,0 +1,174 @@
.t-checkbox {
display: inline-flex;
vertical-align: middle;
position: relative;
background: var(--td-checkbox-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
}
.t-checkbox:focus {
outline: 0;
}
.t-checkbox--block {
display: flex;
padding: var(--td-checkbox-vertical-padding, var(--td-spacer-2, 32rpx));
}
.t-checkbox--right {
flex-direction: row-reverse;
}
.t-checkbox .limit-title-row {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.t-checkbox .image-center {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.t-checkbox__icon-left {
margin-right: 20rpx;
width: 40rpx;
}
.t-checkbox__icon-right {
right: 0px;
display: contents;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.t-checkbox__icon-image {
width: var(--td-checkbox-icon-size, 48rpx);
height: var(--td-checkbox-icon-size, 48rpx);
vertical-align: top;
}
.t-checkbox__icon {
position: relative;
display: block;
width: var(--td-checkbox-icon-size, 48rpx);
height: var(--td-checkbox-icon-size, 48rpx);
color: var(--td-checkbox-icon-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
font-size: var(--td-checkbox-icon-size, 48rpx);
margin-top: calc((var(--td-checkbox-title-line-height, 48rpx) - var(--td-checkbox-icon-size, 48rpx)) / 2);
}
.t-checkbox__icon:empty {
display: none;
}
.t-checkbox__icon--checked {
color: var(--td-checkbox-icon-checked-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-checkbox__icon--disabled {
cursor: not-allowed;
color: var(--td-checkbox-icon-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-checkbox__icon--left {
margin-right: 16rpx;
}
.t-checkbox__icon-circle {
width: calc((var(--td-checkbox-icon-size, 48rpx) - 4rpx) * 2);
height: calc((var(--td-checkbox-icon-size, 48rpx) - 4rpx) * 2);
border: calc(4rpx * 2) solid var(--td-checkbox-icon-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.5);
box-sizing: border-box;
}
.t-checkbox__icon-circle--disabled {
background: var(--td-checkbox-icon-disabled-bg-color, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
}
.t-checkbox__icon-rectangle {
width: calc((var(--td-checkbox-icon-size, 48rpx) - 4rpx * 2) * 2);
height: calc((var(--td-checkbox-icon-size, 48rpx) - 4rpx * 2) * 2);
border: calc(4rpx * 2) solid var(--td-checkbox-icon-color, var(--td-component-border, var(--td-gray-color-4, #dcdcdc)));
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.5);
box-sizing: border-box;
}
.t-checkbox__icon-rectangle--disabled {
background: var(--td-checkbox-icon-disabled-bg-color, var(--td-bg-color-component-disabled, var(--td-gray-color-2, #eeeeee)));
}
.t-checkbox__icon-line::before,
.t-checkbox__icon-line::after {
content: '';
display: block;
position: absolute;
width: 5rpx;
border-radius: 2rpx;
background: var(--td-checkbox-icon-checked-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
transform-origin: top center;
}
.t-checkbox__icon-line::before {
height: 16rpx;
left: 8rpx;
top: 22rpx;
transform: rotate(-45deg);
}
.t-checkbox__icon-line::after {
height: 26rpx;
right: 8rpx;
top: 14rpx;
transform: rotate(45deg);
}
.t-checkbox__icon-line--disabled::before,
.t-checkbox__icon-line--disabled::after {
background: var(--td-checkbox-icon-disabled-color, var(--td-brand-color-disabled, var(--td-primary-color-3, #b5c7ff)));
}
.t-checkbox__content {
flex: 1;
}
.t-checkbox__title {
color: var(--td-checkbox-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
font: var(--td-checkbox-title-font, var(--td-font-body-large, 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
line-height: var(--td-checkbox-title-line-height, 48rpx);
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.t-checkbox__title--disabled {
color: var(--td-checkbox-title-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-checkbox__description {
color: var(--td-checkbox-description-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
font: var(--td-checkbox-description-font, var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
}
.t-checkbox__description--disabled {
color: var(--td-checkbox-description-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-checkbox__title + .t-checkbox__description:not(:empty) {
margin-top: 8rpx;
}
.t-checkbox__border {
position: absolute;
bottom: 0;
left: 96rpx;
right: 0;
height: 1px;
background: var(--td-checkbox-border-color, var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7)));
transform: scaleY(0.5);
}
.t-checkbox__border--right {
left: 32rpx;
}
.t-checkbox--tag {
font-size: 28rpx;
padding-top: 16rpx;
padding-bottom: 16rpx;
text-align: center;
background-color: var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3));
border-radius: 12rpx;
}
.t-checkbox--tag.t-checkbox--checked {
color: var(--td-checkbox-tag-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-checkbox-tag-active-bg-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-checkbox--tag .t-checkbox__title--checked {
color: var(--td-checkbox-tag-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-checkbox--tag .t-checkbox__content {
margin-right: 0;
}

View File

@@ -0,0 +1,238 @@
<template>
<view
:id="tId"
:style="tools._style([customStyle])"
:class="tools.cls(classPrefix, [placement, theme, ['checked', dataChecked], ['block', block], ['disabled', _disabled]]) + ' ' + tClass"
aria-role="checkbox"
:aria-checked="dataChecked ? (dataIndeterminate ? 'mixed' : true) : false"
:aria-disabled="_disabled ? true : false"
@click.stop="handleTap"
>
<view
v-if="theme == 'default'"
:class="tools.cls(classPrefix + '__icon', [placement, ['checked', dataChecked], ['disabled', _disabled]]) + ' ' + tClassIcon"
>
<slot
v-if="icon === 'slot'"
name="icon"
/>
<view
v-else-if="tools.isArray(icon)"
:class="classPrefix + '__icon'"
>
<image
:src="dataChecked ? (dataIndeterminate && icon[2] ? icon[2] : icon[0]) : icon[1]"
:class="classPrefix + '__icon-image'"
/>
</view>
<block v-else>
<t-icon
v-if="dataChecked && (icon == 'circle' || icon == 'rectangle')"
:name="dataIndeterminate ? 'minus-' + icon + '-filled' : 'check-' + icon + '-filled'"
:class="tools.cls(classPrefix + '__icon-wrapper', [])"
/>
<t-icon
v-if="dataChecked && icon == 'line'"
:name="dataIndeterminate ? 'minus-' + icon + '-filled' : 'check'"
:class="tools.cls(classPrefix + '__icon-wrapper', [])"
/>
<view
v-else-if="!dataChecked && (icon == 'circle' || icon == 'rectangle')"
:class="tools.cls(classPrefix + '__icon-' + icon, [['disabled', _disabled]])"
/>
<view
v-if="!dataChecked && icon == 'line'"
class="placeholder"
/>
</block>
</view>
<view
:class="classPrefix + '__content'"
data-target="text"
@click.stop="handleTap"
>
<view
:class="
tools.cls(classPrefix + '__title', [
['disabled', _disabled],
['checked', dataChecked]
]) +
' ' +
tClassLabel
"
:style="'-webkit-line-clamp:' + maxLabelRow"
>
<block v-if="label">
{{ label }}
</block>
<slot />
<slot name="label" />
</view>
<view
:class="tools.cls(classPrefix + '__description', [['disabled', _disabled]]) + ' ' + tClassContent"
:style="'-webkit-line-clamp:' + maxContentRow"
>
<block v-if="content">
{{ content }}
</block>
<slot name="content" />
</view>
</view>
<view
v-if="theme == 'default' && !dataBorderless"
:class="tools.cls(classPrefix + '__border', [placement]) + ' ' + tClassBorder"
/>
</view>
</template>
<script>
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { coalesce } from '../common/utils';
import { prefix, ISOLATED_RELATION_KEY } from '../common/config';
import props from './props';
import tools from '../common/utils.wxs';
import { ChildrenMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-checkbox`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'checked',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
`${prefix}-class-label`,
`${prefix}-class-icon`,
`${prefix}-class-content`,
`${prefix}-class-border`,
],
mixins: [ChildrenMixin(RELATION_MAP.Checkbox)],
components: {
TIcon,
},
props: {
...props,
theme: {
type: String,
default: 'default',
},
tId: {
type: String,
},
},
data() {
return {
prefix,
classPrefix: name,
_disabled: false,
tools,
dataBorderless: this.borderless,
dataIndeterminate: this.indeterminate,
dataChecked: coalesce(this.checked, this.defaultChecked),
};
},
computed: {
isIsolated() {
return this.relationKey === ISOLATED_RELATION_KEY;
},
},
watch: {
borderless: {
handler(v) {
this.dataBorderless = v;
},
immediate: true,
},
indeterminate: {
handler(v) {
this.dataIndeterminate = v;
},
immediate: true,
},
checked: {
handler(v) {
this.dataChecked = v;
},
immediate: true,
},
disabled: {
handler(v) {
this._disabled = v;
},
immediate: true,
},
},
mounted() {
},
methods: {
innerAfterLinked() {
if (this.isIsolated) return;
const parent = this[RELATION_MAP.Checkbox];
const { value, disabled, borderless } = parent;
const { dataValue, checked, checkAll, item, dataIndeterminate } = this;
const valueSet = new Set(value);
const checkedFromParent = valueSet.has(this.value);
const data = {
_disabled: this.disabled == null ? disabled : this.disabled,
};
data.dataBorderless = !!(coalesce(this.borderless, borderless));
data.dataChecked = this.dataChecked || checkedFromParent;
if (this.dataChecked) {
parent.updateValue({
value: dataValue,
checked,
checkAll,
item,
indeterminate: dataIndeterminate,
trigger: 'init',
});
}
if (this.checkAll) {
data.dataChecked = valueSet.size > 0;
// data.indeterminate =
}
Object.keys(data).forEach((key) => {
this[key] = data[key];
});
},
handleTap(e) {
const { _disabled, readonly, contentDisabled } = this;
const { target } = e.currentTarget.dataset;
if (_disabled || readonly || (target === 'text' && contentDisabled)) return;
const { value, label, checkAll, dataIndeterminate } = this;
const checked = !this.dataChecked;
const parent = this[RELATION_MAP.Checkbox];
if (parent && !this.isIsolated) {
parent.updateValue({ value, checkAll, indeterminate: dataIndeterminate, checked, item: { label, value, checked } });
} else {
this._trigger('change', { context: { value, label }, checked });
}
},
setDisabled(disabled) {
this._disabled = this.disabled || disabled;
},
},
});
</script>
<style scoped>
@import './checkbox.css';
</style>

View File

@@ -0,0 +1,90 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCheckboxProps } from './type';
export default {
/** 是否为块级元素 */
block: {
type: Boolean,
default: true,
},
/** 是否开启无边框模式 */
borderless: {
type: Boolean,
default: undefined,
},
/** 用于标识是否为「全选选项」。单独使用无效,需在 CheckboxGroup 中使用 */
checkAll: Boolean,
/** 是否选中 */
checked: Boolean,
/** 是否选中,非受控属性 */
defaultChecked: Boolean,
/** 多选框内容 */
content: {
type: String,
},
/** 是否禁用组件内容content触发选中 */
contentDisabled: Boolean,
/** 是否禁用组件。如果父组件存在 CheckboxGroup默认值由 CheckboxGroup.disabled 控制。优先级Checkbox.disabled > CheckboxGroup.disabled > Form.disabled */
disabled: {
type: Boolean,
default: undefined,
},
/** 自定义选中图标和非选中图标。使用 Array 时表示:`[选中态图标,非选中态图标,半选中态图标]`。使用 String 时,值为 circle 表示填充圆形图标、值为 line 表示描边型图标、值为 rectangle 表示填充矩形图标 */
icon: {
type: [String, Array],
default: 'circle' as TdCheckboxProps['icon'],
},
/** 是否为半选 */
indeterminate: Boolean,
/** 主文案 */
label: {
type: String,
},
/** 内容最大行数限制 */
maxContentRow: {
type: Number,
default: 5,
},
/** 主文案最大行数限制 */
maxLabelRow: {
type: Number,
default: 3,
},
/** HTML 元素原生属性 */
name: {
type: String,
default: '',
},
/** 多选框和内容相对位置 */
placement: {
type: String,
default: 'left' as TdCheckboxProps['placement'],
validator(val: TdCheckboxProps['placement']): boolean {
if (!val) return true;
return ['left', 'right'].includes(val);
},
},
/** 只读状态 */
readonly: {
type: Boolean,
default: undefined,
},
/** -1 时代表独立,不再寻找 parent用于头条小程序 */
relationKey: {
type: String,
default: '',
},
/** 多选框的值 */
value: {
type: [String, Number, Boolean],
},
/** 值变化时触发。`context` 表示当前点击项内容 */
onChange: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,96 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdCheckboxProps {
/**
* 是否为块级元素
* @default true
*/
block?: boolean;
/**
* 是否开启无边框模式
*/
borderless?: boolean;
/**
* 用于标识是否为「全选选项」。单独使用无效,需在 CheckboxGroup 中使用
* @default false
*/
checkAll?: boolean;
/**
* 是否选中
*/
checked?: boolean;
/**
* 是否选中,非受控属性
*/
defaultChecked?: boolean;
/**
* 多选框内容
*/
content?: string;
/**
* 是否禁用组件内容content触发选中
*/
contentDisabled?: boolean;
/**
* 是否禁用组件。如果父组件存在 CheckboxGroup默认值由 CheckboxGroup.disabled 控制。优先级Checkbox.disabled > CheckboxGroup.disabled > Form.disabled
*/
disabled?: boolean;
/**
* 自定义选中图标和非选中图标。使用 Array 时表示:`[选中态图标,非选中态图标,半选中态图标]`。使用 String 时,值为 circle 表示填充圆形图标、值为 line 表示描边型图标、值为 rectangle 表示填充矩形图标
* @default 'circle'
*/
icon?: 'circle' | 'line' | 'rectangle' | string[];
/**
* 是否为半选
* @default false
*/
indeterminate?: boolean;
/**
* 主文案
*/
label?: string;
/**
* 内容最大行数限制
* @default 5
*/
maxContentRow?: number;
/**
* 主文案最大行数限制
* @default 3
*/
maxLabelRow?: number;
/**
* HTML 元素原生属性
* @default ''
*/
name?: string;
/**
* 多选框和内容相对位置
* @default left
*/
placement?: 'left' | 'right';
/**
* 只读状态
*/
readonly?: boolean;
/**
* -1 时代表独立,不再寻找 parent用于头条小程序
* @default ''
*/
relationKey?: string;
/**
* 多选框的值
*/
value?: string | number | boolean;
/**
* 值变化时触发。`context` 表示当前点击项内容
*/
onChange?: (context: {
checked: boolean;
context: { value: boolean | number | string; label: boolean | number | string };
}) => void;
}

View File

@@ -0,0 +1,31 @@
:: BASE_DOC ::
## API
### Col Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
offset | String / Number | - | \- | N
span | String / Number | - | \- | N
### Col Slots
name | Description
-- | --
\- | \-
### Row Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
gutter | String / Number | - | \- | N
### Row Slots
name | Description
-- | --
\- | \-

View File

@@ -0,0 +1,60 @@
---
title: Layout 布局
description: 以规则的网格阵列来指导和规范页面中的版面布局以及信息分布,提高界面内布局的一致性,节约成本。
spline: base
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TRow from '@tdesign/uniapp/row/row.vue';
import TCol from '@tdesign/uniapp/col/col.vue';
```
### 组件类型
基础
{{ base }}
增加间距
{{ offset }}
## API
### Col Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
offset | String / Number | - | 列的偏移量默认单位px | N
span | String / Number | - | 列的宽度默认单位px | N
### Col Slots
名称 | 描述
-- | --
\- | 默认插槽,列内容
### Row Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
gutter | String / Number | - | 列之间的间距默认单位px | N
### Row Slots
名称 | 描述
-- | --
\- | 默认插槽,行内容

View File

@@ -0,0 +1,149 @@
.t-col {
display: block;
box-sizing: border-box;
min-height: 1px;
}
.t-col--1 {
width: 4.16666667%;
}
.t-col--offset-1 {
margin-left: 4.16666667%;
}
.t-col--2 {
width: 8.33333333%;
}
.t-col--offset-2 {
margin-left: 8.33333333%;
}
.t-col--3 {
width: 12.5%;
}
.t-col--offset-3 {
margin-left: 12.5%;
}
.t-col--4 {
width: 16.66666667%;
}
.t-col--offset-4 {
margin-left: 16.66666667%;
}
.t-col--5 {
width: 20.83333333%;
}
.t-col--offset-5 {
margin-left: 20.83333333%;
}
.t-col--6 {
width: 25%;
}
.t-col--offset-6 {
margin-left: 25%;
}
.t-col--7 {
width: 29.16666667%;
}
.t-col--offset-7 {
margin-left: 29.16666667%;
}
.t-col--8 {
width: 33.33333333%;
}
.t-col--offset-8 {
margin-left: 33.33333333%;
}
.t-col--9 {
width: 37.5%;
}
.t-col--offset-9 {
margin-left: 37.5%;
}
.t-col--10 {
width: 41.66666667%;
}
.t-col--offset-10 {
margin-left: 41.66666667%;
}
.t-col--11 {
width: 45.83333333%;
}
.t-col--offset-11 {
margin-left: 45.83333333%;
}
.t-col--12 {
width: 50%;
}
.t-col--offset-12 {
margin-left: 50%;
}
.t-col--13 {
width: 54.16666667%;
}
.t-col--offset-13 {
margin-left: 54.16666667%;
}
.t-col--14 {
width: 58.33333333%;
}
.t-col--offset-14 {
margin-left: 58.33333333%;
}
.t-col--15 {
width: 62.5%;
}
.t-col--offset-15 {
margin-left: 62.5%;
}
.t-col--16 {
width: 66.66666667%;
}
.t-col--offset-16 {
margin-left: 66.66666667%;
}
.t-col--17 {
width: 70.83333333%;
}
.t-col--offset-17 {
margin-left: 70.83333333%;
}
.t-col--18 {
width: 75%;
}
.t-col--offset-18 {
margin-left: 75%;
}
.t-col--19 {
width: 79.16666667%;
}
.t-col--offset-19 {
margin-left: 79.16666667%;
}
.t-col--20 {
width: 83.33333333%;
}
.t-col--offset-20 {
margin-left: 83.33333333%;
}
.t-col--21 {
width: 87.5%;
}
.t-col--offset-21 {
margin-left: 87.5%;
}
.t-col--22 {
width: 91.66666667%;
}
.t-col--offset-22 {
margin-left: 91.66666667%;
}
.t-col--23 {
width: 95.83333333%;
}
.t-col--offset-23 {
margin-left: 95.83333333%;
}
.t-col--24 {
width: 100%;
}
.t-col--offset-24 {
margin-left: 100%;
}

Some files were not shown because too many files have changed in this diff Show More