1.登录时间判断 2.脚手架升级 3.主页显示优化 4.文件列表页面

This commit is contained in:
yangzhq68909 2025-05-09 18:01:58 +08:00
parent 09ce6fbb11
commit d9de5ae2de
364 changed files with 46992 additions and 145 deletions

View File

@ -14,7 +14,14 @@
getLocation()
},
onShow: function (options) {
if (uni.getStorageSync('logintime') && uni.getStorageSync('logintime') + 1800000 <= Date.now()) { //
console.log('token超时',uni.getStorageSync('logintime'));
uni.removeStorageSync('logintime')
uni.navigateTo({
url: '/pages/login/login'
})
return
}
//
setTimeout(() => {
const currentPage = options.path

View File

@ -0,0 +1,41 @@
import { http } from '@/utils/http';
export function queryDocumentApi(config) { //
return http({
url: '/cxcoagwfb/cxcOaGwfb/bpmlist',
method: 'GET',
data: config
})
}
export function queryNoticeApi(config) { //
return http({
url: '/cxctz/cxcTz/list',
method: 'GET',
data: config
})
}
export function querySuperiorSystemApi(config) { //
return http({
url: '/cxcjyglsjzdgl/cxcJyglSjzdgl/zslist',
method: 'GET',
data: config
})
}
export function queryFactorySystemApi(config) { //
return http({
url: '/cxczd/cxcZdgl/list',
method: 'GET',
data: config
})
}
export function queryRegulationsApi(config) { //
return http({
url: '/cxcoaflgf/cxcOaFlgf/zslist',
method: 'GET',
data: config
})
}

View File

@ -1,89 +0,0 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
}
</route>
<template>
<PageLayout navTitle="定位" backRouteName="people" routeMethod="pushTab">
<map
style="width: 100%; height: 100%"
:latitude="latitude"
:longitude="longitude"
:markers="marker"
:scale="scale"
></map>
</PageLayout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
const latitude = ref(40.009704)
const longitude = ref(116.374999)
const marker = reactive([
{
id: 0,
latitude: latitude.value, //
longitude: longitude.value, //
iconPath: '/static/location.png', //
rotate: 0, //
width: 20, //
height: 20, //
title: '你在哪了', //
alpha: 0.5, //
/* label:{//为标记点旁边增加标签 //H5
   content:'北京国炬公司',//
    color:'red',//
  fontSize:24,//
x:5,//label marker
y:1,//label marker
borderWidth:12,//
borderColor:'pink',//
  borderRadius:20,//
  bgColor:'black',//
  padding:5,//
textAlign:'right'//
}, */
callout: {
//
content: '北京国炬公司', //
color: '#ffffff', //
fontSize: 14, //
borderRadius: 2, //
bgColor: '#00c16f', //
display: 'ALWAYS', //
},
// anchor:{//
// x:0,
// y:0,
// }
},
])
const scale = 16
const getLocation = () => {
uni.getLocation({
type: 'gcj02',
success: function (res) {
console.log('当前位置的经度:' + res.longitude)
console.log('当前位置的纬度:' + res.latitude)
},
fail: function (res) {
console.log('当前位置的经度')
},
})
}
onLoad(() => {
getLocation()
})
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -172,6 +172,15 @@
"navigationBarTitleText": "工作台",
"navigationStyle": "custom"
}
},
{
"path": "pages/operate/file/index",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "公文/通知公告/法律法规/上级制度/厂级制度"
}
}
],
"subPackages": [
@ -233,15 +242,6 @@
{
"root": "pages-user",
"pages": [
{
"path": "location/location",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "userEdit/userEdit",
"type": "page",

View File

@ -8,7 +8,7 @@
}
</route>
<template>
<PageLayout :navbarShow="false">
<PageLayout :navbarShow="false" :class="{ 'gray': appStore.isGray == 1 }">
<view class="nav">
<view class="nav_box">
<view class="weather_calender">
@ -30,15 +30,16 @@
</view>
</view>
</view>
<!-- 滚动内容区 -->
<scroll-view class="scrollView" :scroll-y="true" scroll-with-animation>
<!--轮播图-->
<swiper class="swiper" :indicator-dots="true" :circular="true" :autoplay="true" :interval="5000"
:duration="500">
<swiper-item v-for="(item, index) in carouselList" :key="index">
<image :src="getFileAccessHttpUrl(item)" mode="aspectFill"></image>
</swiper-item>
</swiper>
<!--流程服务-->
<wd-row>
<wd-col :span="12" v-for="(item, index) in middleApps" :key="index">
<wd-img :width="50" :height="50" :src="getFileAccessHttpUrl(item.icon)"></wd-img>
@ -48,7 +49,7 @@
</view>
</wd-col>
</wd-row>
<!--常用服务-->
<view class="serveBox">
<view class="title">
<view class="dot"></view>
@ -56,7 +57,7 @@
</view>
<Grid :column="4" v-model="usList" @itemClik="goPage"></Grid>
</view>
<!--其他服务-->
<view class="serveBox">
<view class="title">
<view class="dot"></view>
@ -100,40 +101,10 @@
const res = wx.getSystemInfoSync();
const statusHeight = res.statusBarHeight; //
const cusnavbarheight = (statusHeight + 30) + "px";
const goPage = (item) => {
// let page = item.routeIndex
// console.log('-----------page------------', page)
// if (!page) {
// toast.info('')
// } else {
// if (['other', 'common'].includes(page)) {
// goPageMore(page)
// return
// }
// if (page === 'annotationList') {
// msgCount.value = 0
// }
// dot.value[page] = false
// if (page.indexOf('/app/online') == 0) {
// let code = page.substring(page.lastIndexOf('/') + 1)
// let real = { desformCode: code, desformName: item.title }
// uni.navigateTo({
// url: '/pages/check/onlineForm/add?item=' + encodeURIComponent(JSON.stringify(real)),
// })
// } else if (page.indexOf('/app/desform') == 0) {
// let code = page.substring(page.lastIndexOf('/') + 1)
// let real = { desformCode: code, desformName: item.title }
// uni.navigateTo({
// url: '/pages/check/designForm/designForm?item=' + encodeURIComponent(JSON.stringify(real)),
// })
// } else {
// if (!hasRoute({ name: page })) {
// router.replace({ name: 'demo', params: { backRouteName: 'index' } })
// } else {
// router.replace({ name: page, params: { backRouteName: 'index' } })
// }
// }
// }
const goPage = (item : any) => {
uni.navigateTo({
url: `${item.path}?title=${item.text}`
})
}
const goPageMore = (page) => {
@ -272,7 +243,7 @@
justify-content: flex-end;
}
}
.scrollView {
display: flex;
flex-direction: column;
@ -330,18 +301,17 @@
}
}
}
.swiper {
height: 575upx;
flex: none;
image {
width: 100%;
display: block;
height: 100%;
margin: 0;
}
}
</style>

View File

@ -124,6 +124,7 @@
localStorageTime: +new Date(),
})
savePwd() //
uni.setStorageSync('logintime', Date.now()) //
router.pushTab({ path: HOME_PAGE })
} else {
toast.warning(res.message)

View File

@ -0,0 +1,273 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '公文/通知公告/法律法规/上级制度/厂级制度',
},
}
</route>
<template>
<view :class="['fixed-header', { 'gray': appStore.isGray == 1 }]">
<wd-navbar left-text="返回" left-arrow
custom-style="padding-top: var(--status-bar-height, 0); background-image: linear-gradient(to right, #1890ff, #096dd9); color: #fff;"
@click-left="handleClickLeft">
<template #title>
<view class="search-box">
<wd-search v-model="keyword" hide-cancel placeholder-left placeholder="搜索" shape="round"
@change="onChange"></wd-search>
</view>
</template>
</wd-navbar>
</view>
<view class="container">
<!-- 加载提示 -->
<wd-loading v-if="loading && pageNo === 1" class="loading-tip">加载中...</wd-loading>
<!-- 列表内容 -->
<view v-for="(item, i) in list" :key="i">
<wd-card :title="item.title" title-bold border-radius="8" use-footer-slot>
<view class="card-content">
<view class="meta-info">
<wd-icon name="time" size="14px" color="#999"></wd-icon>
<text class="meta-text">{{item.time}}</text>
<wd-icon name="usergroup" size="14px" color="#999" style="margin-left: auto;"></wd-icon>
<text class="meta-text">{{item.depart}}</text>
</view>
</view>
</wd-card>
</view>
<view class="load-more" v-if="loading && pageNo > 1">
<wd-loading size="16px">正在加载...</wd-loading>
</view>
</view>
</template>
<script setup>
import {
onLoad,
onReachBottom,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
queryDocumentApi,
queryNoticeApi,
querySuperiorSystemApi,
queryFactorySystemApi,
queryRegulationsApi
} from '@/api/pages/file'
import {
useAppStore
} from '@/store'
const appStore = useAppStore()
let pageNo = 1
let pageSize = 10
let loading = false
const list = ref([]) //
const keyword = ref('') //
const type = ref('') //
const handleClickLeft = () => {
uni.navigateBack()
}
const getList = () => {
loading = true
if (type.value == '公文') getDocumentList()
if (type.value == '通知公告') getNoticeList()
if (type.value == '上级制度') getSuperiorSystemList()
if (type.value == '厂级制度') getFactorySystemList()
if (type.value == '法律法规') getRegulationsList()
}
/*公文*/
const getDocumentList = () => {
queryDocumentApi({
pageNo,
pageSize,
fwbt: formatSearchkey()
}).then((res) => {
if (res.success) {
list.value = [...list.value, ...formatObj(res.result.records, 'fwbt', 'fwtime', 'wjlb')]
}
loading = false
}).catch((err) => {
loading = false
})
}
/*通知公告*/
const getNoticeList = () => {
queryNoticeApi({
pageNo,
pageSize,
neirong: formatSearchkey()
}).then((res) => {
if (res.success) {
list.value = [...list.value, ...formatObj(res.result.records, 'neirong', 'createTime', 'fbdw')]
}
loading = false
}).catch((err) => {
loading = false
})
}
/*上级制度*/
const getSuperiorSystemList = () => {
querySuperiorSystemApi({
pageNo,
pageSize,
zdmc: formatSearchkey()
}).then((res) => {
if (res.success) {
list.value = [...list.value, ...formatObj(res.result.records, 'zdmc', 'updateTime2', 'zbbm')]
}
loading = false
}).catch((err) => {
loading = false
})
}
/*厂级制度*/
const getFactorySystemList = () => {
queryFactorySystemApi({
pageNo,
pageSize,
zdmc: formatSearchkey()
}).then((res) => {
if (res.success) {
list.value = [...list.value, ...formatObj(res.result.records, 'zdmc', 'fatime',
'zbbm_dictText')]
}
loading = false
}).catch((err) => {
loading = false
})
}
/*法律法规*/
const getRegulationsList = () => {
queryRegulationsApi({
pageNo,
pageSize,
flfgmc: formatSearchkey()
}).then((res) => {
if (res.success) {
list.value = [...list.value, ...formatObj(res.result.records, 'flfgmc', 'updateTime2',
'fabubumen')]
}
loading = false
}).catch((err) => {
console.log('err', err);
})
}
const formatObj = (arr, title, time, depart) => {
return arr.map((item) => ({
...item,
title: item[title],
time: item[time],
depart: item[depart]
}))
}
const formatSearchkey = () => {
if (keyword.value.trim()) {
return '*' + keyword.value + '*'
}
}
const onChange = () => {
pageNo = 1
list.value = []
getList()
}
onReachBottom(() => {
if (loading) return
pageNo++
getList()
})
onPullDownRefresh(() => {
pageNo = 1
list.value = []
getList()
uni.stopPullDownRefresh()
})
onLoad((options) => {
type.value = options.title
getList()
});
</script>
<style lang="scss">
/* 固定顶部区域 */
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.container {
padding: calc(60px + var(--status-bar-height, 0)) 5px 0 5px; /* 确保顶部 padding 包含状态栏高度 */
/* 顶部padding要大于固定头高度 */
min-height: 100vh;
background-color: #f7f7f7;
}
.search-box {
display: flex;
height: 100%;
align-items: center;
width: 100%;
padding: 0 10px;
:deep() {
.wd-search {
background: rgba(255, 255, 255, 0.2);
border-radius: 18px;
width: 100%;
.wd-search__input {
color: #fff;
}
.wd-search__placeholder {
color: rgba(255, 255, 255, 0.7);
}
}
}
}
.card-content {
padding: 8px 0;
.meta-info {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
.meta-text {
margin-left: 4px;
}
}
}
.loading-tip {
padding: 20px 0;
text-align: center;
}
.load-more {
padding: 16px 0;
text-align: center;
font-size: 14px;
color: #999;
}
</style>

View File

@ -10,7 +10,7 @@
<template>
<PageLayout :navbarShow="false">
<view :class="{ 'gray': appStore.isGray == 1 }">
<view :class="{'gray': appStore.isGray == 1 }">
<view class="avatar-area">
<wd-img width="100" height="100" :round="true" :radius="50"
:src="getFileAccessHttpUrl(userStore.userInfo.avatar)" @click="ChooseImage"></wd-img>
@ -168,6 +168,7 @@
})
.then(() => {
userStore.clearUserInfo()
uni.removeStorageSync('logintime') //
router.replaceAll({ name: 'login' })
})
}

View File

@ -18,12 +18,12 @@ interface NavigateToOptions {
"/pages/more/more" |
"/pages/user/people" |
"/pages/workHome/index" |
"/pages/operate/file/index" |
"/pages-home/home/home" |
"/pages-message/chat/chat" |
"/pages-message/contacts/contacts" |
"/pages-message/personPage/personPage" |
"/pages-message/tenant/tenant" |
"/pages-user/location/location" |
"/pages-user/userEdit/userEdit" |
"/pages-work/dragPage/index" |
"/pages-work/onlinePage/onlineAdd" |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
export class AbortablePromise<T> {
promise: Promise<T>
private _reject: ((res?: any) => void) | null = null
constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
this.promise = new Promise<T>((resolve, reject) => {
executor(resolve, reject)
this._reject = reject // reject便abort
})
}
// abortPromise
abort(error?: any) {
if (this._reject) {
this._reject(error) // rejectPromise
}
}
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected)
}
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult> {
return this.promise.catch(onrejected)
}
}

View File

@ -0,0 +1,7 @@
/**
* SCSS 配置项命名空间以及BEM
*/
$namespace: 'wd';
$elementSeparator: '__';
$modifierSeparator: '--';
$state-prefix: 'is-';

View File

@ -0,0 +1,89 @@
/**
* 辅助函数
*/
@import 'config';
$default-theme: #4d80f0 !default; // 正常色
/* 转换成字符串 */
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
/* 判断是否存在 Modifier */
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, $modifierSeparator) {
@return true;
}
@else {
@return false;
}
}
/* 判断是否存在伪类 */
@function containsPseudo($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true;
}
@else {
@return false;
}
}
/**
* 主题色切换
* @params $theme-color 主题色
* @params $type 变暗dark 变亮 'light'
* @params $mix-color 自己设置的混色
*/
@function themeColor($theme-color, $type: "", $mix-color: "") {
@if $default-theme !=#4d80f0 {
@if $type=="dark" {
@return darken($theme-color, 10%);
}
@else if $type=="light" {
@return lighten($theme-color, 10%);
}
@else {
@return $theme-color;
}
}
@else {
@return $mix-color;
}
}
/**
* 颜色结果切换 如果开启线性渐变色 使用渐变色如果没有开启那么使用主题色
* @params $open-linear 是否开启线性渐变色
* @params $deg 渐变色角度
* @params $theme-color 当前配色
* @params [Array] $set 主题色明暗设置 $color-list 数量对应
* @params [Array] $color-list 渐变色顺序 $color-list $per-list 数量相同
* @params [Array] $per-list 渐变色比例
*/
@function resultColor($deg, $theme-color, $set, $color-list, $per-list) {
// 开启渐变
$len: length($color-list);
$arg: $deg;
@for $i from 1 through $len {
$arg: $arg + ","+ themeColor($theme-color, nth($set, $i), nth($color-list, $i)) + " "+ nth($per-list, $i);
}
@return linear-gradient(unquote($arg));
}

View File

@ -0,0 +1,385 @@
/**
* 混合宏
*/
@import "config";
@import "function";
/**
* BEM定义块b)
*/
@mixin b($block) {
$B: $namespace + "-"+ $block !global;
.#{$B} {
@content;
}
}
/* 定义元素e对于伪类会自动将 e 嵌套在 伪类 底下 */
@mixin e($element...) {
$selector: &;
$selectors: "";
@if containsPseudo($selector) {
@each $item in $element {
$selectors: #{$selectors + "." + $B + $elementSeparator + $item + ","};
}
@at-root {
#{$selector} {
#{$selectors} {
@content;
}
}
}
}
@else {
@each $item in $element {
$selectors: #{$selectors + $selector + $elementSeparator + $item + ","};
}
@at-root {
#{$selectors} {
@content;
}
}
}
}
/* 此方法用于生成穿透样式 */
/* 定义元素e对于伪类会自动将 e 嵌套在 伪类 底下 */
@mixin edeep($element...) {
$selector: &;
$selectors: "";
@if containsPseudo($selector) {
@each $item in $element {
$selectors: #{$selectors + "." + $B + $elementSeparator + $item + ","};
}
@at-root {
#{$selector} {
:deep() {
#{$selectors} {
@content;
}
}
}
}
}
@else {
@each $item in $element {
$selectors: #{$selectors + $selector + $elementSeparator + $item + ","};
}
@at-root {
:deep() {
#{$selectors} {
@content;
}
}
}
}
}
/* 定义状态m */
@mixin m($modifier...) {
$selectors: "";
@each $item in $modifier {
$selectors: #{$selectors + & + $modifierSeparator + $item + ","};
}
@at-root {
#{$selectors} {
@content;
}
}
}
/* 定义状态m */
@mixin mdeep($modifier...) {
$selectors: "";
@each $item in $modifier {
$selectors: #{$selectors + & + $modifierSeparator + $item + ","};
}
@at-root {
:deep() {
#{$selectors} {
@content;
}
}
}
}
/* 对于需要需要嵌套在 m 底下的 e调用这个混合宏一般在切换整个组件的状态如切换颜色的时候 */
@mixin me($element...) {
$selector: &;
$selectors: "";
@if containsModifier($selector) {
@each $item in $element {
$selectors: #{$selectors + "." + $B + $elementSeparator + $item + ","};
}
@at-root {
#{$selector} {
#{$selectors} {
@content;
}
}
}
}
@else {
@each $item in $element {
$selectors: #{$selectors + $selector + $elementSeparator + $item + ","};
}
@at-root {
#{$selectors} {
@content;
}
}
}
}
/* 状态,生成 is-$state 类名 */
@mixin when($states...) {
@at-root {
@each $state in $states {
&.#{$state-prefix + $state} {
@content;
}
}
}
}
/**
* 常用混合宏
*/
/* 单行超出隐藏 */
@mixin lineEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 多行超出隐藏 */
@mixin multiEllipsis($lineNumber: 3) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $lineNumber;
overflow: hidden;
}
/* 清除浮动 */
@mixin clearFloat {
&::after {
display: block;
content: "";
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
}
/* 0.5px 边框 指定方向*/
@mixin halfPixelBorder($direction: "bottom", $left: 0, $color: $-color-border-light) {
position: relative;
&::after {
position: absolute;
display: block;
content: "";
@if ($left==0) {
width: 100%;
}
@else {
width: calc(100% - #{$left});
}
height: 1px;
left: $left;
@if ($direction=="bottom") {
bottom: 0;
}
@else {
top: 0;
}
transform: scaleY(0.5);
background: $color;
}
}
/* 0.5px 边框 环绕 */
@mixin halfPixelBorderSurround($color: $-color-border-light) {
position: relative;
&::after {
position: absolute;
display: block;
content: ' ';
pointer-events: none;
width: 200%;
height: 200%;
left: 0;
top: 0;
border: 1px solid $color;
transform: scale(0.5);
box-sizing: border-box;
transform-origin: left top;
}
}
@mixin buttonClear {
outline: none;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
background: transparent;
}
/**
* 三角形实现尖角样式适用于背景透明情况
* @param $size 三角形高底边为 $size * 2
* @param $bg 三角形背景颜色
*/
@mixin triangleArrow($size, $bg) {
@include e(arrow) {
position: absolute;
width: 0;
height: 0;
}
@include e(arrow-down) {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-top: $size solid $bg;
transform: translateX(-50%);
bottom: calc(-1 * $size)
}
@include e(arrow-up) {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-bottom: $size solid $bg;
transform: translateX(-50%);
top: calc(-1 * $size)
}
@include e(arrow-left) {
border-top: $size solid transparent;
border-bottom: $size solid transparent;
border-right: $size solid $bg;
transform: translateY(-50%);
left: calc(-1 * $size)
}
@include e(arrow-right) {
border-top: $size solid transparent;
border-bottom: $size solid transparent;
border-left: $size solid $bg;
transform: translateY(-50%);
right: calc(-1 * $size)
}
}
/**
* 正方形实现尖角样式适用于背景不透明情况
* @param $size 正方形边长
* @param $bg 正方形背景颜色
* @param $z-index z-index属性值不得大于外部包裹器
* @param $box-shadow 阴影
*/
@mixin squareArrow($size, $bg, $z-index, $box-shadow) {
@include e(arrow) {
position: absolute;
width: $size;
height: $size;
z-index: $z-index;
}
@include e(arrow-down) {
transform: translateX(-50%);
bottom: 0;
&:after {
content: "";
width: $size;
height: $size;
background-color: $bg;
position: absolute;
left: 0;
bottom: calc(-1 * $size / 2);
transform: rotateZ(45deg);
box-shadow: $box-shadow;
}
}
@include e(arrow-up) {
transform: translateX(-50%);
top: 0;
&:after {
content: "";
width: $size;
height: $size;
background-color: $bg;
position: absolute;
left: 0;
top: calc(-1 * $size / 2);
transform: rotateZ(45deg);
box-shadow: $box-shadow;
}
}
@include e(arrow-left) {
transform: translateY(-50%);
left: 0;
&:after {
content: "";
width: $size;
height: $size;
background-color: $bg;
position: absolute;
left: calc(-1 * $size / 2);
top: 0;
transform: rotateZ(45deg);
box-shadow: $box-shadow;
}
}
@include e(arrow-right) {
transform: translateY(-50%);
right: 0;
&:after {
content: "";
width: $size;
height: $size;
background-color: $bg;
position: absolute;
right: calc(-1 * $size / 2);
top: 0;
transform: rotateZ(45deg);
box-shadow: $box-shadow;
}
}
}

View File

@ -0,0 +1,968 @@
@import './function';
/**
* UI规范基础变量
*/
/*----------------------------------------- Theme color. start ----------------------------------------*/
/* 主题颜色 */
$-color-theme: var(--wot-color-theme, $default-theme) !default; // 主题色
$-color-white: var(--wot-color-white, rgb(255, 255, 255)) !default; // 用于mix的白色
$-color-black: var(--wot-color-black, rgb(0, 0, 0)) !default; // 用于mix的黑色
/* 辅助色 */
$-color-success: var(--wot-color-success, #34d19d) !default; // 成功色
$-color-warning: var(--wot-color-warning, #f0883a) !default; // 警告色
$-color-danger: var(--wot-color-danger, #fa4350) !default; // 危险出错色
$-color-purple: var(--wot-color-purple, #8268de) !default; // 紫色
$-color-yellow: var(--wot-color-yellow, #f0cd1d) !default; // 黄色
$-color-blue: var(--wot-color-blue, #2bb3ed) !default; // 蓝色
$-color-info: var(--wot-color-info, #909399) !default;
$-color-gray-1: var(--wot-color-gray-1, #f7f8fa) !default;
$-color-gray-2: var(--wot-color-gray-2, #f2f3f5) !default;
$-color-gray-3: var(--wot-color-gray-3, #ebedf0) !default;
$-color-gray-4: var(--wot-color-gray-4, #dcdee0) !default;
$-color-gray-5: var(--wot-color-gray-5, #c8c9cc) !default;
$-color-gray-6: var(--wot-color-gray-6, #969799) !default;
$-color-gray-7: var(--wot-color-gray-7, #646566) !default;
$-color-gray-8: var(--wot-color-gray-8, #323233) !default;
$-font-gray-1: var(--wot-font-gray-1, rgba(0, 0, 0, 0.9));
$-font-gray-2: var(--wot-font-gray-2, rgba(0, 0, 0, 0.6));
$-font-gray-3: var(--wot-font-gray-3, rgba(0, 0, 0, 0.4));
$-font-gray-4: var(--wot-font-gray-4, rgba(0, 0, 0, 0.26));
$-font-white-1: var(--wot-font-white-1, rgba(255, 255, 255, 1));
$-font-white-2: var(--wot-font-white-2, rgba(255, 255, 255, 0.55));
$-font-white-3: var(--wot-font-white-3, rgba(255, 255, 255, 0.35));
$-font-white-4: var(--wot-font-white-4, rgba(255, 255, 255, 0.22));
/* 文字颜色(默认浅色背景下 */
$-color-title: var(--wot-color-title, $-color-black) !default; // 模块标题/重要正文 000
$-color-content: var(--wot-color-content, #262626) !default; // 普通正文 262626
$-color-secondary: var(--wot-color-secondary, #595959) !default; // 次要信息注释/补充/正文 595959
$-color-aid: var(--wot-color-aid, #8c8c8c) !default; // 辅助文字字号弱化信息引导性/不可点文字 8c8c8c
$-color-tip: var(--wot-color-tip, #bfbfbf) !default; // 失效默认提示文字 bfbfbf
$-color-border: var(--wot-color-border, #d9d9d9) !default; // 控件边框线 d9d9d9
$-color-border-light: var(--wot-color-border-light, #e8e8e8) !default; // 分割线颜色 e8e8e8
$-color-bg: var(--wot-color-bg, #f5f5f5) !default; // 背景色禁用填充色 f5f5f5
/* 暗黑模式 */
$-dark-background: var(--wot-dark-background, #131313) !default;
$-dark-background2: var(--wot-dark-background2, #1b1b1b) !default;
$-dark-background3: var(--wot-dark-background3, #141414) !default;
$-dark-background4: var(--wot-dark-background4, #323233) !default;
$-dark-background5: var(--wot-dark-background5, #646566) !default;
$-dark-background6: var(--wot-dark-background6, #380e08) !default;
$-dark-background7: var(--wot-dark-background7, #707070) !default;
$-dark-color: var(--wot-dark-color, $-color-white) !default;
$-dark-color2: var(--wot-dark-color2, #f2270c) !default;
$-dark-color3: var(--wot-dark-color3, rgba(232, 230, 227, 0.8)) !default;
$-dark-color-gray: var(--wot-dark-color-gray, $-color-secondary) !default;
$-dark-border-color: var(--wot-dark-border-color, #3a3a3c) !default;
/* 图形颜色 */
$-color-icon: var(--wot-color-icon, #d9d9d9) !default; // icon颜色
$-color-icon-active: var(--wot-color-icon-active, #eee) !default; // icon颜色hover
$-color-icon-disabled: var(--wot-color-icon-disabled, #a7a7a7) !default; // icon颜色disabled
/*----------------------------------------- Theme color. end -------------------------------------------*/
/*-------------------------------- Theme color application size. start --------------------------------*/
/* 文字字号 */
$-fs-big: var(--wot-fs-big, 24px) !default; // 大型标题
$-fs-important: var(--wot-fs-important, 19px) !default; // 重要数据
$-fs-title: var(--wot-fs-title, 16px) !default; // 标题字号/重要正文字号
$-fs-content: var(--wot-fs-content, 14px) !default; // 普通正文
$-fs-secondary: var(--wot-fs-secondary, 12px) !default; // 次要信息注释/补充/正文
$-fs-aid: var(--wot-fs-aid, 10px) !default; // 辅助文字字号弱化信息引导性/不可点文字
/* 文字字重 */
$-fw-medium: var(--wot-fw-medium, 500) !default; // PingFangSC-Medium
$-fw-semibold: var(--wot-fw-semibold, 600) !default; // PingFangSC-Semibold
/* 尺寸 */
$-size-side-padding: var(--wot-size-side-padding, 15px) !default; // 屏幕两边留白
/*-------------------------------- Theme color application size. end --------------------------------*/
/* component var */
/* action-sheet */
$-action-sheet-weight: var(--wot-action-sheet-weight, 500) !default; // 面板字重
$-action-sheet-radius: var(--wot-action-sheet-radius, 16px) !default; // 面板圆角大小
$-action-sheet-loading-size: var(--wot-action-sheet-loading-size, 20px) !default; // loading动画尺寸
$-action-sheet-action-height: var(--wot-action-sheet-action-height, 48px) !default; // 单条菜单高度
$-action-sheet-color: var(--wot-action-sheet-color, rgba(0, 0, 0, 0.85)) !default; // 选项名称颜色
$-action-sheet-fs: var(--wot-action-sheet-fs, $-fs-title) !default; // 选项名称字号
$-action-sheet-active-color: var(--wot-action-sheet-active-color, $-color-bg) !default; // 点击高亮颜色
$-action-sheet-subname-fs: var(--wot-action-sheet-subname-fs, $-fs-secondary) !default; // 描述信息字号
$-action-sheet-subname-color: var(--wot-action-sheet-subname-color, rgba(0, 0, 0, 0.45)) !default; // 描述信息颜色
$-action-sheet-disabled-color: var(--wot-action-sheet-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 禁用颜色
$-action-sheet-bg: var(--wot-action-sheet-bg, $-color-white) !default; // 菜单容器颜色取消按钮上方的颜色
$-action-sheet-title-height: var(--wot-action-sheet-title-height, 64px) !default; // 标题高度
$-action-sheet-title-fs: var(--wot-action-sheet-title-fs, $-fs-title) !default; // 标题字号
$-action-sheet-close-fs: var(--wot-action-sheet-close-fs, $-fs-title) !default; // 关闭按钮大小
$-action-sheet-close-color: var(--wot-action-sheet-close-color, rgba(0, 0, 0, 0.65)) !default; // 关闭按钮颜色
$-action-sheet-close-top: var(--wot-action-sheet-close-top, 25px) !default; // 关闭按钮距离标题顶部距离
$-action-sheet-close-right: var(--wot-action-sheet-close-right, 15px) !default; // 关闭按钮距离标题右侧距离
$-action-sheet-cancel-color: var(--wot-action-sheet-cancel-color, #131415) !default; // 取消按钮颜色
$-action-sheet-cancel-height: var(--wot-action-sheet-cancel-height, 44px) !default; // 取消按钮高度
$-action-sheet-cancel-bg: var(--wot-action-sheet-cancel-bg, rgba(240, 240, 240, 1)) !default; // 取消按钮背景色
$-action-sheet-cancel-radius: var(--wot-action-sheet-cancel-radius, 22px) !default; // 取消按钮圆角大小
$-action-sheet-panel-padding: var(--wot-action-sheet-panel-padding, 12px 0 11px) !default; // 自定义面板内边距大小
$-action-sheet-panel-img-fs: var(--wot-action-sheet-panel-img-fs, 40px) !default; // 自定义面板图片大小
$-action-sheet-panel-img-radius: var(--wot-action-sheet-panel-img-radius, 4px) !default; // 自定义面板图片圆角大小
/* badge */
$-badge-bg: var(--wot-badge-bg, $-color-danger) !default; // 背景填充颜色
$-badge-color: var(--wot-badge-color, #fff) !default; // 文字颜色
$-badge-fs: var(--wot-badge-fs, 12px) !default; // 文字字号
$-badge-padding: var(--wot-badge-padding, 0 5px) !default; // padding
$-badge-height: var(--wot-badge-height, 16px) !default; // 高度
$-badge-primary: var(--wot-badge-primary, $-color-theme) !default;
$-badge-success: var(--wot-badge-success, $-color-success) !default;
$-badge-warning: var(--wot-badge-warning, $-color-warning) !default;
$-badge-danger: var(--wot-badge-danger, $-color-danger) !default;
$-badge-info: var(--wot-badge-info, $-color-info) !default;
$-badge-dot-size: var(--wot-badge-dot-size, 6px) !default; // dot 类型大小
$-badge-border: var(--wot-badge-border, 2px solid $-badge-color) !default; // 边框样式
/* button */
$-button-disabled-opacity: var(--wot-button-disabled-opacity, 0.6) !default; // button禁用透明度
$-button-small-height: var(--wot-button-small-height, 28px) !default; // 小型按钮高度
$-button-small-padding: var(--wot-button-small-padding, 0 12px) !default; // 小型按钮padding
$-button-small-fs: var(--wot-button-small-fs, $-fs-secondary) !default; // 小型按钮字号
$-button-small-radius: var(--wot-button-small-radius, 2px) !default; // 小型按钮圆角大小
$-button-small-loading: var(--wot-button-small-loading, 14px) !default; // 小型按钮loading图标大小
$-button-medium-height: var(--wot-button-medium-height, 36px) !default; // 中型按钮高度
$-button-medium-padding: var(--wot-button-medium-padding, 0 16px) !default; // 中型按钮padding
$-button-medium-fs: var(--wot-button-medium-fs, $-fs-content) !default; // 中型按钮字号
$-button-medium-radius: var(--wot-button-medium-radius, 4px) !default; // 中型按钮圆角大小
$-button-medium-loading: var(--wot-button-medium-loading, 18px) !default; // 中型按钮loading图标大小
$-button-medium-box-shadow-size: var(--wot-button-medium-box-shadow-size, 0px 2px 4px 0px) !default; // 中尺寸阴影尺寸
$-button-large-height: var(--wot-button-large-height, 44px) !default; // 大型按钮高度
$-button-large-padding: var(--wot-button-large-padding, 0 36px) !default; // 大型按钮padding
$-button-large-fs: var(--wot-button-large-fs, $-fs-title) !default; // 大型按钮字号
$-button-large-radius: var(--wot-button-large-radius, 8px) !default; // 大型按钮圆角大小
$-button-large-loading: var(--wot-button-large-loading, 24px) !default; // 大小按钮loading图标大小
$-button-large-box-shadow-size: var(--wot-button-large-box-shadow-size, 0px 4px 8px 0px) !default; // 大尺寸阴影尺寸
$-button-icon-fs: var(--wot-button-icon-fs, 1.18em) !default; // 带图标的按钮的图标大小
$-button-icon-size: var(--wot-button-icon-size, 40px) !default; // icon 类型按钮尺寸
$-button-icon-color: var(--wot-button-icon-color, rgba(0, 0, 0, 0.65)) !default; // icon 类型按钮颜色
$-button-icon-disabled-color: var(--wot-button-icon-disabled-color, $-color-icon-disabled) !default; // icon 类型按钮禁用颜色
$-button-normal-color: var(--wot-button-normal-color, $-color-title) !default; // 文字颜色
$-button-normal-disabled-color: var(--wot-button-normal-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 默认按钮禁用文字色
$-button-plain-bg-color: var(--wot-button-plain-bg-color, $-color-white) !default; // 幽灵按钮背景色
$-button-primary-color: var(--wot-button-primary-color, $-color-white) !default; // 主要按钮颜色
$-button-primary-bg-color: var(--wot-button-primary-bg-color, $-color-theme) !default; // 主要按钮背景颜色
$-button-success-color: var(--wot-button-success-color, $-color-white) !default; // 成功按钮文字颜色
$-button-success-bg-color: var(--wot-button-success-bg-color, $-color-success) !default; // 成功按钮颜色
$-button-info-color: var(--wot-button-info-color, $-color-title) !default; // 信息按钮颜色
$-button-info-bg-color: var(--wot-button-info-bg-color, #f0f0f0) !default; // 信息按钮背景颜色
$-button-info-plain-border-color: var(--wot-button-info-plain-border-color, rgba(0, 0, 0, 0.45)) !default; // 信息按钮禁用颜色
$-button-info-plain-normal-color: var(--wot-button-info-plain-normal-color, rgba(0, 0, 0, 0.85)) !default; // 信息幽灵按钮默认颜色
$-button-warning-color: var(--wot-button-warning-color, $-color-white) !default; // 警告按钮字体颜色
$-button-warning-bg-color: var(--wot-button-warning-bg-color, $-color-warning) !default; // 警告按钮背景颜色
$-button-error-color: var(--wot-button-error-color, $-color-white) !default; // 错误按钮颜色
$-button-error-bg-color: var(--wot-button-error-bg-color, $-color-danger) !default; // 错误按钮背景颜色
$-button-text-hover-opacity: var(--wot-button-text-hover-opacity, 0.7) !default; // 文字button激活时透明度
/* cell */
$-cell-padding: var(--wot-cell-padding, $-size-side-padding) !default; // cell 左右padding距离
$-cell-line-height: var(--wot-cell-line-height, 24px) !default; // 行高
$-cell-group-title-fs: var(--wot-cell-group-title-fs, $-fs-title) !default; // 组标题字号
$-cell-group-padding: var(--wot-cell-group-padding, 13px $-cell-padding) !default; // 组padding
$-cell-group-title-color: var(--wot-cell-group-title-color, rgba(0, 0, 0, 0.85)) !default; // 组标题文字颜色
$-cell-group-value-fs: var(--wot-cell-group-value-fs, $-fs-content) !default; // 组值字号
$-cell-group-value-color: var(--wot-cell-group-value-color, $-color-content) !default; // 组值文字颜色
$-cell-wrapper-padding: var(--wot-cell-wrapper-padding, 10px) !default; // cell 容器padding
$-cell-wrapper-padding-large: var(--wot-cell-wrapper-padding-large, 12px) !default; // large类型cell容器padding
$-cell-wrapper-padding-with-label: var(--wot-cell-wrapper-padding-with-label, 16px) !default; // cell 容器上下padding有label情况下
$-cell-icon-right: var(--wot-cell-icon-right, 4px) !default; // 图标距离右边缘
$-cell-icon-size: var(--wot-cell-icon-size, 16px) !default; // 图标大小
$-cell-title-fs: var(--wot-cell-title-fs, 14px) !default; // 标题字号
$-cell-title-color: var(--wot-cell-title-color, rgba(0, 0, 0, 0.85)) !default; // 标题文字颜色
$-cell-label-fs: var(--wot-cell-label-fs, 12px) !default; // 描述信息字号
$-cell-label-color: var(--wot-cell-label-color, rgba(0, 0, 0, 0.45)) !default; // 描述信息文字颜色
$-cell-value-fs: var(--wot-cell-value-fs, 14px) !default; // 右侧内容字号
$-cell-value-color: var(--wot-cell-value-color, rgba(0, 0, 0, 0.85)) !default; // 右侧内容文字颜色
$-cell-arrow-size: var(--wot-cell-arrow-size, 18px) !default; // 右箭头大小
$-cell-arrow-color: var(--wot-cell-arrow-color, rgba(0, 0, 0, 0.25)) !default; // 右箭头颜色
$-cell-clear-color: var(--wot-cell-clear-color, #585858) !default; // 清空按钮颜色
$-cell-tap-bg: var(--wot-cell-tap-bg, rgba(0, 0, 0, 0.06)) !default; // 点击态背景色
$-cell-title-fs-large: var(--wot-cell-title-fs-large, 16px) !default; // 大尺寸标题字号
$-cell-label-fs-large: var(--wot-cell-label-fs-large, 14px) !default; // 描述信息字号
$-cell-icon-size-large: var(--wot-cell-icon-size-large, 18px) !default; // 图标大小
$-cell-required-color: var(--wot-cell-required-color, $-color-danger) !default; // 要求必填*颜色
$-cell-required-size: var(--wot-cell-required-size, 18px) !default; // 必填*字号
$-cell-vertical-top: var(--wot-cell-vertical-top, 16px) !default; // 表单类型-上下结构的间距
/* calendar */
$-calendar-fs: var(--wot-calendar-fs, 16px) !default;
$-calendar-panel-padding: var(--wot-calendar-panel-padding, 0 12px) !default;
$-calendar-panel-title-fs: var(--wot-calendar-panel-title-fs, 14px) !default;
$-calendar-panel-title-color: var(--wot-calendar-panel-title-color, rgba(0, 0, 0, 0.85)) !default;
$-calendar-week-color: var(--wot-calendar-week-color, rgba(0, 0, 0, 0.85)) !default;
$-calendar-week-height: var(--wot-calendar-week-height, 36px) !default;
$-calendar-week-fs: var(--wot-calendar-week-fs, 12px) !default;
$-calendar-day-fs: var(--wot-calendar-day-fs, 16px) !default;
$-calendar-day-color: var(--wot-calendar-day-color, rgba(0, 0, 0, 0.85)) !default;
$-calendar-day-fw: var(--wot-calendar-day-fw, 500) !default;
$-calendar-day-height: var(--wot-calendar-day-height, 64px) !default;
$-calendar-month-width: var(--wot-calendar-month-width, 50px) !default;
$-calendar-active-color: var(--wot-calendar-active-color, $-color-theme) !default;
$-calendar-selected-color: var(--wot-calendar-selected-color, $-color-white) !default;
$-calendar-disabled-color: var(--wot-calendar-disabled-color, rgba(0, 0, 0, 0.25)) !default;
$-calendar-range-color: var(--wot-calendar-range-color, rgba(#4d80f0, 0.09)) !default;
$-calendar-active-border: var(--wot-calendar-active-border, 8px) !default;
$-calendar-info-fs: var(--wot-calendar-info-fs, 10px) !default;
$-calendar-item-margin-bottom: var(--wot-calendar-item-margin-bottom, 4px) !default;
/* checkbox */
$-checkbox-margin: var(--wot-checkbox-margin, 10px) !default; // 多个复选框距离
$-checkbox-bg: var(--wot-checkbox-bg, $-color-white) !default; // 多个复选框距离
$-checkbox-label-margin: var(--wot-checkbox-label-margin, 9px) !default; // 右侧文字与左侧图标距离
$-checkbox-size: var(--wot-checkbox-size, 16px) !default; // 左侧图标尺寸
$-checkbox-icon-size: var(--wot-checkbox-icon-size, 14px) !default; // 左侧图标尺寸
$-checkbox-border-color: var(--wot-checkbox-border-color, #dcdcdc) !default; // 左侧图标边框颜色
$-checkbox-check-color: var(--wot-checkbox-check-color, $-color-white) !default; // 左侧图标边框颜色
$-checkbox-label-fs: var(--wot-checkbox-label-fs, 14px) !default; // 右侧文字字号
$-checkbox-label-color: var(--wot-checkbox-label-color, rgba(0, 0, 0, 0.85)) !default; // 右侧文字颜色
$-checkbox-checked-color: var(--wot-checkbox-checked-color, $-color-theme) !default; // 选中颜色
$-checkbox-disabled-color: var(--wot-checkbox-disabled-color, rgba(0, 0, 0, 0.04)) !default; // 禁用背景颜色
$-checkbox-disabled-label-color: var(--wot-checkbox-disabled-label-color, rgba(0, 0, 0, 0.25)) !default; // 禁用文字颜色
$-checkbox-disabled-check-color: var(--wot-checkbox-disabled-check-color, rgba(0, 0, 0, 0.15)) !default; // 禁用图标颜色
$-checkbox-disabled-check-bg: var(--wot-checkbox-disabled-check-bg, rgba(0, 0, 0, 0.15)) !default; // 禁用边框背景颜色
$-checkbox-square-radius: var(--wot-checkbox-square-radius, 4px) !default; // 方型圆角大小
$-checkbox-large-size: var(--wot-checkbox-large-size, 18px) !default; // 左侧图标尺寸
$-checkbox-large-label-fs: var(--wot-checkbox-large-label-fs, 16px) !default; // 右侧文字字号
$-checkbox-button-height: var(--wot-checkbox-button-height, 32px) !default; // 按钮模式复选框高
$-checkbox-button-min-width: var(--wot-checkbox-button-min-width, 78px) !default; // 按钮模式最小宽
$-checkbox-button-radius: var(--wot-checkbox-button-radius, 16px) !default; // 按钮圆角大小
$-checkbox-button-bg: var(--wot-checkbox-button-bg, rgba(0, 0, 0, 0.04)) !default; // 按钮模式背景颜色
$-checkbox-button-font-size: var(--wot-checkbox-button-font-size, 14px) !default; // 按钮模式字号
$-checkbox-button-border: var(--wot-checkbox-button-border, #f5f5f5) !default; // 按钮边框颜色
$-checkbox-button-disabled-border: var(--wot-checkbox-button-disabled-border, rgba(0, 0, 0, 0.15)) !default; // 按钮禁用边框颜色
/* collapse */
$-collapse-side-padding: var(--wot-collapse-side-padding, $-size-side-padding) !default; // 左右间距
$-collapse-body-padding: var(--wot-collapse-body-padding, 14px $-size-side-padding) !default; // body padding
$-collapse-header-padding: var(--wot-collapse-header-padding, 13px $-size-side-padding) !default; // 头部padding
$-collapse-title-color: var(--wot-collapse-title-color, rgba(0, 0, 0, 0.85)) !default; // 标题颜色
$-collapse-title-fs: var(--wot-collapse-title-fs, 16px) !default; // 标题字号
$-collapse-arrow-size: var(--wot-collapse-arrow-size, 18px) !default; // 箭头大小
$-collapse-arrow-color: var(--wot-collapse-arrow-color, #d8d8d8) !default; // 箭头颜色
$-collapse-body-fs: var(--wot-collapse-body-fs, 14px) !default; // 内容字号
$-collapse-body-color: var(--wot-collapse-body-color, rgba(0, 0, 0, 0.65)) !default; // 内容颜色
$-collapse-disabled-color: var(--wot-collapse-disabled-color, rgba(0, 0, 0, 0.15)) !default; // 禁用颜色
$-collapse-retract-fs: var(--wot-collapse-retract-fs, 14px) !default; // 更多 字号
$-collapse-more-color: var(--wot-collapse-more-color, $-color-theme) !default; // 更多 颜色
/* divider */
$-divider-padding: var(--wot-divider-padding, 0 $-size-side-padding) !default; // 两边间距
$-divider-margin: var(--wot-divider-margin, 16px 0) !default; // 上下间距
$-divider-color: var(--wot-divider-color, rgba(0, 0, 0, 0.45)) !default; // 字体颜色
$-divider-line-color: var(--wot-divider-line-color, currentColor) !default; // 线条颜色
$-divider-line-height: var(--wot-divider-line-height, 1px) !default; // 线条高度
$-divider-fs: var(--wot-divider-fs, 14px) !default; // 字体大小
$-divider-content-left-width: var(--wot-divider-content-left-width, 10%) !default; // 左侧内容宽度
$-divider-content-left-margin: var(--wot-divider-content-left-margin, 12px) !default; // 左侧内容距离线距离
$-divider-content-right-margin: var(--wot-divider-content-right-margin, 12px) !default; // 右侧内容距离线距离
$-divider-content-right-width: var(--wot-divider-content-right-width, 10%) !default; // 右侧内容宽度
$-divider-vertical-height: var(--wot-divider-vertical-height, 16px) !default; // 垂直分割线高度
$-divider-vertical-content-margin: var(--wot-divider-vertical-content-margin, 0 8px) !default; // 垂直分割线内容间距
$-divider-vertical-line-width: var(--wot-divider-vertical-line-width, 1px) !default; // 线条高度
/* drop-menu */
$-drop-menu-height: var(--wot-drop-menu-height, 48px) !default; // 展示选中项的高度
$-drop-menu-color: var(--wot-drop-menu-color, $-color-content) !default; // 展示选中项的颜色
$-drop-menu-fs: var(--wot-drop-menu-fs, $-fs-content) !default; // 展示选中项的字号
$-drop-menu-arrow-fs: var(--wot-drop-menu-arrow-fs, $-fs-content) !default; // 箭头图标大小
$-drop-menu-side-padding: var(--wot-drop-menu-side-padding, $-size-side-padding) !default; // 两边留白间距
$-drop-menu-disabled-color: var(--wot-drop-menu-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 禁用颜色
$-drop-menu-item-height: var(--wot-drop-menu-item-height, 48px) !default; // 选项高度
$-drop-menu-item-color: var(--wot-drop-menu-item-color, $-color-content) !default; // 选项颜色
$-drop-menu-item-fs: var(--wot-drop-menu-item-fs, $-fs-content) !default; // 选项字号
$-drop-menu-item-color-active: var(--wot-drop-menu-item-color-active, $-color-theme) !default; // 选中颜色
$-drop-menu-item-color-tip: var(--wot-drop-menu-item-color-tip, rgba(0, 0, 0, 0.45)) !default; // 提示文字颜色
$-drop-menu-item-fs-tip: var(--wot-drop-menu-item-fs-tip, $-fs-secondary) !default; // 提示文字字号
$-drop-menu-option-check-size: var(--wot-drop-menu-option-check-size, 20px) !default; // check 图标大小
$-drop-menu-line-color: var(--wot-drop-menu-line-color, $-color-theme) !default; // 下划线颜色
$-drop-menu-line-height: var(--wot-drop-menu-line-height, 3px) !default; // 下划线高度
/* input-number */
$-input-number-color: var(--wot-input-number-color, #262626) !default; // 文字颜色
$-input-number-border-color: var(--wot-input-number-border-color, #e8e8e8) !default; // 边框颜色
$-input-number-disabled-color: var(--wot-input-number-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 禁用颜色
$-input-number-height: var(--wot-input-number-height, 24px) !default; // 加减号按钮高度
$-input-number-btn-width: var(--wot-input-number-btn-width, 26px) !default; // 加减号按钮宽度
$-input-number-input-width: var(--wot-input-number-input-width, 36px) !default; // 输入框宽度
$-input-number-radius: var(--wot-input-number-radius, 4px) !default; // 加减号按钮圆角大小
$-input-number-fs: var(--wot-input-number-fs, 12px) !default; // 输入框字号
$-input-number-icon-size: var(--wot-input-number-icon-size, 14px) !default; // 加减号图标大小
$-input-number-icon-color: var(--wot-input-number-icon-color, rgba(0, 0, 0, 0.65)) !default; // icon颜色
/* input */
$-input-padding: var(--wot-input-padding, $-size-side-padding) !default; // input 左右padding距离
$-input-border-color: var(--wot-input-border-color, #dadada) !default; // 无label边框颜色
$-input-not-empty-border-color: var(--wot-input-not-empty-border-color, #262626) !default; // 输入框有值时 无label边框颜色
$-input-fs: var(--wot-input-fs, $-cell-title-fs) !default; // 字号
$-input-fs-large: var(--wot-input-fs-large, $-cell-title-fs-large) !default; // 大尺寸字号
$-input-icon-margin: var(--wot-input-icon-margin, 8px) !default; // 图标距离
$-input-color: var(--wot-input-color, #262626) !default; // 文字颜色
$-input-placeholder-color: var(--wot-input-placeholder-color, #bfbfbf) !default; // 占位符颜色
$-input-disabled-color: var(--wot-input-disabled-color, #d9d9d9) !default; // 输入框禁用颜色
$-input-error-color: var(--wot-input-error-color, $-color-danger) !default; // 输入框错误颜色
$-input-icon-color: var(--wot-input-icon-color, #bfbfbf) !default; // 图标颜色
$-input-clear-color: var(--wot-input-clear-color, #585858) !default; // 关闭按钮颜色
$-input-count-color: var(--wot-input-count-color, #bfbfbf) !default; // 计数文字颜色
$-input-count-current-color: var(--wot-input-count-current-color, #262626) !default; // 当前长度颜色
$-input-bg: var(--wot-input-bg, $-color-white) !default; // 默认背景颜色
$-input-cell-bg: var(--wot-input-cell-bg, $-color-white) !default; // cell 类型背景色
$-input-cell-border-color: var(--wot-input-cell-border-color, $-color-border-light) !default; // cell 类型边框颜色
$-input-cell-padding: var(--wot-input-cell-padding, 10px) !default; // cell 容器padding
$-input-cell-padding-large: var(--wot-input-cell-padding-large, 12px) !default; // large类型cell容器padding
$-input-cell-height: var(--wot-input-cell-height, 24px) !default; // cell 高度
$-input-cell-label-width: var(--wot-input-cell-label-width, 33%) !default; // cell label 的宽度
$-input-inner-height: var(--wot-input-inner-height, 34px) !default; // 非cell和textarea下的高度
$-input-inner-height-no-border: var(--wot-input-inner-height-no-border, 24px) !default; // 无边框下的高度
$-input-count-fs: var(--wot-input-count-fs, 14px) !default; // 计数字号
$-input-count-fs-large: var(--wot-input-count-fs-large, 14px) !default; // 大尺寸计数字号
$-input-icon-size: var(--wot-input-icon-size, 16px) !default; // 图标大小
$-input-icon-size-large: var(--wot-input-icon-size-large, 18px) !default; // 大尺寸图标大小
/* textarea */
$-textarea-padding: var(--wot-textarea-padding, $-size-side-padding) !default; // textarea 左右padding距离
$-textarea-border-color: var(--wot-textarea-border-color, #dadada) !default; // 无label边框颜色
$-textarea-not-empty-border-color: var(--wot-textarea-not-empty-border-color, #262626) !default; // 输入框有值时 无label边框颜色
$-textarea-fs: var(--wot-textarea-fs, $-cell-title-fs) !default; // 字号
$-textarea-fs-large: var(--wot-textarea-fs-large, $-cell-title-fs-large) !default; // 大尺寸字号
$-textarea-icon-margin: var(--wot-textarea-icon-margin, 8px) !default; // 图标距离
$-textarea-color: var(--wot-textarea-color, #262626) !default; // 文字颜色
$-textarea-icon-color: var(--wot-textarea-icon-color, #bfbfbf) !default; // 图标颜色
$-textarea-clear-color: var(--wot-textarea-clear-color, #585858) !default; // 关闭按钮颜色
$-textarea-count-color: var(--wot-textarea-count-color, #bfbfbf) !default; // 计数文字颜色
$-textarea-count-current-color: var(--wot-textarea-count-current-color, #262626) !default; // 当前长度颜色
$-textarea-bg: var(--wot-textarea-bg, $-color-white) !default; // 默认背景颜色
$-textarea-cell-border-color: var(--wot-textarea-cell-border-color, $-color-border-light) !default; // cell 类型边框颜色
$-textarea-cell-padding: var(--wot-textarea-cell-padding, 10px) !default; // cell 容器padding
$-textarea-cell-padding-large: var(--wot-textarea-cell-padding-large, 12px) !default; // large类型cell容器padding
$-textarea-cell-height: var(--wot-textarea-cell-height, 24px) !default; // cell 高度
$-textarea-count-fs: var(--wot-textarea-count-fs, 14px) !default; // 计数字号
$-textarea-count-fs-large: var(--wot-textarea-count-fs-large, 14px) !default; // 大尺寸计数字号
$-textarea-icon-size: var(--wot-textarea-icon-size, 16px) !default; // 图标大小
$-textarea-icon-size-large: var(--wot-textarea-icon-size-large, 18px) !default; // 大尺寸图标大小
/* loadmore */
$-loadmore-height: var(--wot-loadmore-height, 48px) !default; // 高度
$-loadmore-color: var(--wot-loadmore-color, rgba(0, 0, 0, 0.45)) !default; // 颜色
$-loadmore-fs: var(--wot-loadmore-fs, 14px) !default; // 字号
$-loadmore-error-color: var(--wot-loadmore-error-color, $-color-theme) !default; // 点击重试颜色
$-loadmore-refresh-fs: var(--wot-loadmore-refresh-fs, $-fs-title) !default; // refresh图标字号
$-loadmore-loading-size: var(--wot-loadmore-loading-size, $-fs-title) !default; // loading尺寸
/* message-box */
$-message-box-width: var(--wot-message-box-width, 300px) !default; // 宽度
$-message-box-bg: var(--wot-message-box-bg, $-color-white) !default; // 默认背景颜色
$-message-box-radius: var(--wot-message-box-radius, 16px) !default; // 圆角大小
$-message-box-padding: var(--wot-message-box-padding, 25px 24px 0) !default; // 主体内容padding
$-message-box-title-fs: var(--wot-message-box-title-fs, 16px) !default; // 标题字号
$-message-box-title-color: var(--wot-message-box-title-color, rgba(0, 0, 0, 0.85)) !default; // 标题颜色
$-message-box-content-fs: var(--wot-message-box-content-fs, 14px) !default; // 内容字号
$-message-box-content-color: var(--wot-message-box-content-color, #666666) !default; // 内容颜色
$-message-box-content-max-height: var(--wot-message-box-content-max-height, 264px) !default; // 内容最大高度
$-message-box-content-scrollbar-width: var(--wot-message-box-content-scrollbar-width, 4px) !default; // 内容滚动条宽度
$-message-box-content-scrollbar-color: var(--wot-message-box-content-scrollbar-color, rgba(0, 0, 0, 0.1)) !default; // 内容滚动条颜色
$-message-box-input-error-color: var(--wot-message-box-input-error-color, $-input-error-color) !default; // 输入框错误颜色
/* notice-bar */
$-notice-bar-fs: var(--wot-notice-bar-fs, 12px) !default; // 字号
$-notice-bar-line-height: var(--wot-notice-bar-line-height, 18px) !default; // 行高
$-notice-bar-border-radius: var(--wot-notice-bar-border-radius, 8px) !default; // 圆角
$-notice-bar-padding: var(--wot-notice-bar-padding, 9px 20px 9px 15px) !default; // 非换行下的padding
$-notice-bar-warning-bg: var(--wot-notice-bar-warning-bg, #fff6c8) !default; // 背景色
$-notice-bar-info-bg: var(--wot-notice-bar-info-bg, #f4f9ff) !default; // 背景色
$-notice-bar-danger-bg: var(--wot-notice-bar-danger-bg, #feeced) !default; // 背景色
$-notice-bar-warning-color: var(--wot-notice-bar-warning-color, $-color-warning) !default; // 文字和图标颜色
$-notice-bar-info-color: var(--wot-notice-bar-info-color, $-color-theme) !default; // 文字和图标颜色
$-notice-bar-danger-color: var(--wot-notice-bar-danger-color, $-color-danger) !default; // 文字和图标颜色
$-notice-bar-prefix-size: var(--wot-notice-bar-prefix-size, 18px) !default; // 图标大小
$-notice-bar-close-bg: var(--wot-notice-bar-close-bg, rgba(0, 0, 0, 0.15)) !default; // 右侧关闭按钮背景颜色
$-notice-bar-close-size: var(--wot-notice-bar-close-size, 18px) !default; // 右侧关闭按钮背景颜色
$-notice-bar-close-color: var(--wot-notice-bar-close-color, $-color-white) !default; // 右侧关闭按钮颜色
$-notice-bar-wrap-padding: var(--wot-notice-bar-wrap-padding, 14px $-size-side-padding) !default; // 换行下的padding
/* pagination */
$-pagination-content-padding: var(--wot-pagination-content-padding, 10px 15px) !default;
$-pagination-message-padding: var(--wot-pagination-message-padding, 1px 0 16px 0) !default;
$-pagination-message-fs: var(--wot-pagination-message-fs, 12px) !default;
$-pagination-message-color: var(--wot-pagination-message-color, rgba(0, 0, 0, 0.69)) !default;
$-pagination-nav-border: var(--wot-pagination-nav-border, 1px solid rgba(0, 0, 0, 0.45)) !default;
$-pagination-nav-border-radius: var(--wot-pagination-nav-border-radius, 16px) !default;
$-pagination-nav-fs: var(--wot-pagination-nav-fs, 12px) !default;
$-pagination-nav-width: var(--wot-pagination-nav-width, 60px) !default;
$-pagination-nav-color: var(--wot-pagination-nav-color, rgba(0, 0, 0, 0.85)) !default;
$-pagination-nav-content-fs: var(--wot-pagination-nav-content-fs, 12px) !default;
$-pagination-nav-sepatator-padding: var(--wot-pagination-nav-sepatator-padding, 0 4px) !default;
$-pagination-nav-current-color: var(--wot-pagination-nav-current-color, $-color-theme) !default;
$-pagination-icon-size: var(--wot-pagination-icon-size, $-fs-content) !default;
/* picker */
$-picker-toolbar-height: var(--wot-picker-toolbar-height, 54px) !default; // toolbar 操作条的高度
$-picker-action-height: var(--wot-picker-action-height, 16px) !default; // toolbar 操作条的高度
$-picker-toolbar-finish-color: var(--wot-picker-toolbar-finish-color, $-color-theme) !default; // toolbar 操作条完成按钮的颜色
$-picker-toolbar-cancel-color: var(--wot-picker-toolbar-cancel-color, #666666) !default; // toolbar 操作条的边框颜色
$-picker-toolbar-fs: var(--wot-picker-toolbar-fs, $-fs-title) !default; // toolbar 操作条的字号
$-picker-toolbar-title-color: var(--wot-picker-toolbar-title-color, rgba(0, 0, 0, 0.85)) !default; // toolbar 操作台的标题颜色
$-picker-column-fs: var(--wot-picker-column-fs, 16px) !default; // 选择器选项的字号
$-picker-bg: var(--wot-picker-bg, $-color-white) !default; // 选择器选项的字号
$-picker-column-active-fs: var(--wot-picker-column-active-fs, 18px) !default; // 选择器选项被选中的字号
$-picker-column-color: var(--wot-picker-column-color, rgba(0, 0, 0, 0.85)) !default; // 选择器选项的颜色
$-picker-column-height: var(--wot-picker-column-height, 210px) !default; // 列高 滚筒外部的高度
$-picker-column-item-height: var(--wot-picker-column-item-height, 35px) !default; // 列高 滚筒外部的高度
$-picker-column-select-bg: var(--wot-picker-column-select-bg, #f5f5f5) !default;
$-picker-loading-button-color: var(--wot-picker-loading-button-color, rgba(0, 0, 0, 0.25)) !default; // loading 背景颜色
$-picker-column-padding: var(--wot-picker-column-padding, 0 $-size-side-padding) !default; // 选项内间距
$-picker-column-disabled-color: var(--wot-picker-column-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 选择器选项禁用的颜色
$-picker-mask: var(--wot-picker-mask, linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.25)))
linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.25)) !default; // 上下阴影
$-picker-loading-bg: var(--wot-picker-loading-bg, rgba($-color-white, 0.8)) !default; // loading 背景颜色
$-picker-region-separator-color: var(--wot-picker-region-separator-color, rgba(0, 0, 0, 0.65)) !default; // 区域选择文字颜色
$-picker-cell-arrow-size-large: var(--wot-picker-cell-arrow-size-large, $-cell-icon-size) !default; // cell 类型的大尺寸 右侧icon尺寸
$-picker-region-color: var(--wot-picker-region-color, rgba(0, 0, 0, 0.45)) !default; // 区域选择文字颜色
$-picker-region-bg-active-color: var(--wot-picker-region-bg-active-color, $-color-theme) !default; // 区域选择激活选中背景颜色
$-picker-region-fs: var(--wot-picker-region-fs, 14px) !default; // 区域选择文字字号
/* col-picker */
$-col-picker-selected-height: var(--wot-col-picker-selected-height, 44px) !default; // 弹框顶部值高度
$-col-picker-selected-padding: var(--wot-col-picker-selected-padding, 0 16px) !default; // 弹框顶部值左右间距
$-col-picker-selected-fs: var(--wot-col-picker-selected-fs, 14px) !default; // 弹框顶部值字号
$-col-picker-selected-color: var(--wot-col-picker-selected-color, rgba(0, 0, 0, 0.85)) !default; // 弹框顶部值文字颜色
$-col-picker-selected-fw: var(--wot-col-picker-selected-fw, 700) !default; // 弹框顶部值高亮字重
$-col-picker-line-width: var(--wot-col-picker-line-width, 16px) !default; // 弹框顶部值高亮线条宽度
$-col-picker-line-height: var(--wot-col-picker-line-height, 3px) !default; // 弹框顶部值高亮线条高度
$-col-picker-line-color: var(
--wot-col-picker-line-color,
linear-gradient(315deg, rgba(81, 124, 240, 1), rgba(118, 158, 245, 1))
) !default; // 弹框顶部值高亮线条颜色
$-col-picker-line-box-shadow: var(--wot-col-picker-line-box-shadow, 0px 1px 2px 0px rgba(1, 87, 255, 0.2)) !default; // 弹框顶部值高亮线条阴影
$-col-picker-list-height: var(--wot-col-picker-list-height, 53vh) !default; // 弹框列表高度
$-col-picker-list-padding-bottom: var(--wot-col-picker-list-padding-bottom, 30px) !default; // 弹框列表底部间距
$-col-picker-list-color: var(--wot-col-picker-list-color, rgba(0, 0, 0, 0.85)) !default; // 弹框列表文字颜色
$-col-picker-list-color-disabled: var(--wot-col-picker-list-color-disabled, rgba(0, 0, 0, 0.15)) !default; // 弹框列表文字禁用颜色
$-col-picker-list-color-tip: var(--wot-col-picker-list-color-tip, rgba(0, 0, 0, 0.45)) !default; // 弹框列表提示文字颜色
$-col-picker-list-fs: var(--wot-col-picker-list-fs, 14px) !default; // 弹框列表文字字号
$-col-picker-list-fs-tip: var(--wot-col-picker-list-fs-tip, 12px) !default; // 弹框列表提示文字字号
$-col-picker-list-item-padding: var(--wot-col-picker-list-item-padding, 12px 15px) !default; // 弹框列表选项间距
$-col-picker-list-checked-icon-size: var(--wot-col-picker-list-checked-icon-size, 18px) !default; // 弹框列表选中箭头大小
$-col-picker-list-color-checked: var(--wot-col-picker-list-color-checked, $-color-theme) !default; // 弹框列表选中选项颜色
/* overlay */
$-overlay-bg: var(--wot-overlay-bg, rgba(0, 0, 0, 0.65)) !default;
$-overlay-bg-dark: var(--wot-overlay-bg-dark, rgba(0, 0, 0, 0.75)) !default;
/* popup */
$-popup-close-size: var(--wot-popup-close-size, 24px) !default; // 关闭按钮尺寸
$-popup-close-color: var(--wot-popup-close-color, #666) !default; // 关闭按钮颜色
/* progress */
$-progress-padding: var(--wot-progress-padding, 9px 0 8px) !default; // 进度条内边距
$-progress-bg: var(--wot-progress-bg, rgba(229, 229, 229, 1)) !default; // 进度条底色
$-progress-danger-color: var(--wot-progress-danger-color, $-color-danger) !default; // 进度条danger颜色
$-progress-success-color: var(--wot-progress-success-color, $-color-success) !default; // 进度条success进度条颜色
$-progress-warning-color: var(--wot-progress-warning-color, $-color-warning) !default; // 进度条warning进度条颜色
$-progress-color: var(--wot-progress-color, $-color-theme) !default; // 进度条颜色
$-progress-height: var(--wot-progress-height, 3px) !default; // 进度条高度
$-progress-label-color: var(--wot-progress-label-color, #333) !default; // 文字颜色
$-progress-label-fs: var(--wot-progress-label-fs, 14px) !default; // 文字字号
$-progress-icon-fs: var(--wot-progress-icon-fs, 18px) !default; // 图标字号
/* radio */
$-radio-margin: var(--wot-radio-margin, $-checkbox-margin) !default; // 多个单选框距离
$-radio-label-margin: var(--wot-radio-label-margin, $-checkbox-label-margin) !default; // 右侧文字与左侧图标距离
$-radio-size: var(--wot-radio-size, 16px) !default; // 左侧图标尺寸
$-radio-bg: var(--wot-radio-bg, $-color-white) !default; // 左侧图标尺寸
$-radio-label-fs: var(--wot-radio-label-fs, $-checkbox-label-fs) !default; // 右侧文字字号
$-radio-label-color: var(--wot-radio-label-color, $-checkbox-label-color) !default; // 右侧文字颜色
$-radio-checked-color: var(--wot-radio-checked-color, $-checkbox-checked-color) !default; // 选中颜色
$-radio-disabled-color: var(--wot-radio-disabled-color, $-checkbox-disabled-color) !default; // 禁用颜色
$-radio-disabled-label-color: var(--wot-radio-disabled-label-color, $-checkbox-disabled-label-color) !default; // 禁用文字颜色
$-radio-large-size: var(--wot-radio-large-size, $-checkbox-large-size) !default; // 左侧图标尺寸
$-radio-large-label-fs: var(--wot-radio-large-label-fs, $-checkbox-large-label-fs) !default; // 右侧文字字号
$-radio-button-height: var(--wot-radio-button-height, $-checkbox-button-height) !default; // 按钮模式复选框高
$-radio-button-min-width: var(--wot-radio-button-min-width, 60px) !default; // 按钮模式最小宽
$-radio-button-max-width: var(--wot-radio-button-max-width, 144px) !default; // 按钮模式最大宽
$-radio-button-radius: var(--wot-radio-button-radius, $-checkbox-button-radius) !default; // 按钮圆角大小
$-radio-button-bg: var(--wot-radio-button-bg, $-checkbox-button-bg) !default; // 按钮模式背景颜色
$-radio-button-fs: var(--wot-radio-button-fs, $-checkbox-button-font-size) !default; // 按钮模式字号
$-radio-button-border: var(--wot-radio-button-border, $-checkbox-button-border) !default; // 按钮边框颜色
$-radio-button-disabled-border: var(--wot-radio-button-disabled-border, $-checkbox-button-disabled-border) !default; // 按钮禁用边框颜色
$-radio-dot-size: var(--wot-radio-dot-size, 8px) !default; // 单选dot模式圆点尺寸
$-radio-dot-large-size: var(--wot-radio-dot-large-size, 10px) !default; // 单选dot模式大尺寸圆点尺寸
$-radio-dot-checked-bg: var(--wot-radio-dot-checked-bg, $-color-theme) !default; // 单选dot模式选中背景色
$-radio-dot-checked-border-color: var(--wot-radio-dot-checked-border-color, $-color-theme) !default; // 单选dot模式选中边框色
$-radio-dot-border-color: var(--wot-radio-dot-border-color, #dcdcdc) !default; // 单选dot模式边框色
$-radio-dot-disabled-border: var(--wot-radio-dot-disabled-border, #d9d9d9) !default; // 单选dot模式禁用边框颜色
$-radio-dot-disabled-bg: var(--wot-radio-dot-disabled-bg, #d9d9d9) !default; // 单选dot模式禁用背景颜色
/* search */
$-search-side-padding: var(--wot-search-side-padding, $-size-side-padding) !default; // 左右间距
$-search-padding: var(--wot-search-padding, 10px 0 10px $-search-side-padding) !default; // 不包含取消按钮的间距
$-search-input-radius: var(--wot-search-input-radius, 15px) !default; // 输入框圆角大小
$-search-input-bg: var(--wot-search-input-bg, $-color-bg) !default; // 输入框背景色
$-search-input-height: var(--wot-search-input-height, 30px) !default; // 输入框高度
$-search-input-padding: var(--wot-search-input-padding, 0 32px 0 42px) !default; // 输入框间距
$-search-input-fs: var(--wot-search-input-fs, $-fs-content) !default; // 输入框字号
$-search-input-color: var(--wot-search-input-color, #262626) !default; // 输入框文字颜色
$-search-icon-color: var(--wot-search-icon-color, $-color-icon) !default; // 图标颜色
$-search-icon-size: var(--wot-search-icon-size, 18px) !default; // 图标大小
$-search-clear-icon-size: var(--wot-search-clear-icon-size, $-fs-title) !default; // 清除图标大小
$-search-placeholder-color: var(--wot-search-placeholder-color, #bfbfbf) !default; // placeholder 颜色
$-search-cancel-padding: var(--wot-search-cancel-padding, 0 $-search-side-padding 0 10px) !default; // 取消按钮间距
$-search-cancel-fs: var(--wot-search-cancel-fs, $-fs-title) !default; // 取消按钮字号
$-search-cancel-color: var(--wot-search-cancel-color, rgba(0, 0, 0, 0.65)) !default; // 取消按钮颜色
$-search-light-bg: var(--wot-search-light-bg, $-color-bg) !default; // light 类型的容器背景色
/* slider */
$-slider-fs: var(--wot-slider-fs, $-fs-content) !default; // 字体大小
$-slider-handle-radius: var(--wot-slider-handle-radius, 12px) !default; // 滑块半径
$-slider-handle-bg: var(--wot-slider-handle-bg, resultColor(139deg, $-color-theme, 'dark' 'light', #ffffff #f7f7f7, 0% 100%)) !default; // 滑块背景
$-slider-axie-height: var(--wot-slider-axie-height, 3px) !default; // 滑轴高度
$-slider-color: var(--wot-slider-color, #333) !default; // 字体颜色
$-slider-axie-bg: var(--wot-slider-axie-bg, #e5e5e5) !default; // 滑轴的默认背景色
$-slider-line-color: var(
--wot-slider-line-color,
resultColor(315deg, $-color-theme, 'dark' 'light', #517cf0 #769ef5, 0% 100%)
) !default; // 进度条颜色
$-slider-disabled-color: var(--wot-slider-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 禁用状态下字体颜色
/* sort-button */
$-sort-button-fs: var(--wot-sort-button-fs, $-fs-content) !default; // 字号
$-sort-button-color: var(--wot-sort-button-color, $-color-content) !default; // 颜色
$-sort-button-height: var(--wot-sort-button-height, 48px) !default; // 高度
$-sort-button-line-height: var(--wot-sort-button-line-height, 3px) !default; // 下划线高度
$-sort-button-line-color: var(--wot-sort-button-line-color, $-color-theme) !default; // 下划线颜色
/* steps */
$-steps-icon-size: var(--wot-steps-icon-size, 22px) !default; // 图标尺寸
$-steps-inactive-color: var(--wot-steps-inactive-color, rgba(0, 0, 0, 0.25)) !default; // 等待状态文字颜色
$-steps-finished-color: var(--wot-steps-finished-color, $-color-theme) !default; // 完成文字颜色
$-steps-icon-text-fs: var(--wot-steps-icon-text-fs, $-fs-content) !default; // 数字图标文字字号
$-steps-error-color: var(--wot-steps-error-color, $-color-danger) !default; // 异常颜色
$-steps-title-fs: var(--wot-steps-title-fs, $-fs-content) !default; // 标题字号
$-steps-title-fw: var(--wot-steps-title-fw, $-fw-medium) !default; // 标题字重
$-steps-label-fs: var(--wot-steps-label-fs, $-fs-secondary) !default; // 描述信息字号
$-steps-description-color: var(--wot-steps-description-color, rgba(0, 0, 0, 0.45)) !default; // 描述信息颜色
$-steps-is-icon-width: var(--wot-steps-is-icon-width, 30px) !default; // 自定义图标的宽度给左右留白
$-steps-line-color: var(--wot-steps-line-color, rgba(0, 0, 0, 0.15)) !default; // 线条颜色
$-steps-dot-size: var(--wot-steps-dot-size, 7px) !default; // 点状大小
$-steps-dot-active-size: var(--wot-steps-dot-active-size, 9px) !default; // 点状高亮大小
/* switch */
$-switch-size: var(--wot-switch-size, 28px) !default; // switch大小
$-switch-width: var(--wot-switch-width, calc(1.8em + 4px)) !default; // 宽度
$-switch-height: var(--wot-switch-height, calc(1em + 4px)) !default; // 高度
$-switch-circle-size: var(--wot-switch-circle-size, 1em) !default; // 圆点大小
$-switch-border-color: var(--wot-switch-border-color, #e5e5e5) !default; // 边框颜色选中状态背景颜色
$-switch-active-color: var(--wot-switch-active-color, $-color-theme) !default; // 选中状态背景
$-switch-active-shadow-color: var(--wot-switch-active-shadow-color, rgba(0, 83, 162, 0.5)) !default; // 选中状态shadow颜色
$-switch-inactive-color: var(--wot-switch-inactive-color, #eaeaea) !default; // 非选中背景颜色
$-switch-inactive-shadow-color: var(--wot-switch-inactive-shadow-color, rgba(155, 155, 155, 0.5)) !default; // 非选中状态shadow颜色
/* tabs */
$-tabs-nav-arrow-fs: var(--wot-tabs-nav-arrow-fs, 18px) !default; // 全部Icon字号
$-tabs-nav-arrow-open-fs: var(--wot-tabs-nav-arrow-open-fs, 14px) !default; // 展开Icon字号
$-tabs-nav-width: var(--wot-tabs-nav-width, 100vw) !default; // tabs 头部切换宽度
$-tabs-nav-height: var(--wot-tabs-nav-height, 42px) !default; // 头部切换高度
$-tabs-nav-fs: var(--wot-tabs-nav-fs, $-fs-content) !default; // 头部切换文字大小
$-tabs-nav-color: var(--wot-tabs-nav-color, rgba(0, 0, 0, 0.85)) !default; // 头部切换文字颜色
$-tabs-nav-bg: var(--wot-tabs-nav-bg, $-color-white) !default; // 背景颜色
$-tabs-nav-active-color: var(--wot-tabs-nav-active-color, $-color-theme) !default; // 头部高亮颜色
$-tabs-nav-disabled-color: var(--wot-tabs-nav-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 头部禁用颜色
$-tabs-nav-line-height: var(--wot-tabs-nav-line-height, 3px) !default; // 高亮边框高度
$-tabs-nav-line-width: var(--wot-tabs-nav-line-width, 19px) !default; // 高亮边框宽度
$-tabs-nav-line-bg-color: var(--wot-tabs-nav-line-bg-color, $-color-theme) !default; // 底部条颜色
$-tabs-nav-map-fs: var(--wot-tabs-nav-map-fs, $-fs-content) !default; // map 类型按钮字号
$-tabs-nav-map-color: var(--wot-tabs-nav-map-color, rgba(0, 0, 0, 0.85)) !default; // map 类型按钮文字颜色
$-tabs-nav-map-arrow-color: var(--wot-tabs-nav-map-arrow-color, rgba(0, 0, 0, 0.65)) !default; // map 类型箭头颜色
$-tabs-nav-map-btn-before-bg: var(
--wot-tabs-nav-map-btn-before-bg,
linear-gradient(270deg, rgba(255, 255, 255, 1) 1%, rgba(255, 255, 255, 0) 100%)
) !default; // 左侧map遮罩阴影
$-tabs-nav-map-button-back-color: var(--wot-tabs-nav-map-button-back-color, rgba(0, 0, 0, 0.04)) !default; // map 类型按钮边框颜色
$-tabs-nav-map-button-radius: var(--wot-tabs-nav-map-button-radius, 16px) !default; // map 类型按钮圆角大小
$-tabs-nav-map-modal-bg: var(--wot-tabs-nav-map-modal-bg, $-overlay-bg) !default; // map 类型蒙层背景色
/* tag */
$-tag-fs: var(--wot-tag-fs, $-fs-secondary) !default; // 字号
$-tag-color: var(--wot-tag-color, $-color-white) !default; // 字体颜色
$-tag-small-fs: var(--wot-tag-small-fs, $-fs-aid) !default; // 小尺寸字号
$-tag-info-color: var(--wot-tag-info-color, #585858) !default; // info 颜色
$-tag-primary-color: var(--wot-tag-primary-color, $-color-theme) !default; // 主颜色
$-tag-danger-color: var(--wot-tag-danger-color, $-color-danger) !default; // danger 颜色
$-tag-warning-color: var(--wot-tag-warning-color, $-color-warning) !default; // warning 颜色
$-tag-success-color: var(--wot-tag-success-color, $-color-success) !default; // success 颜色
$-tag-info-bg: var(--wot-tag-info-bg, resultColor(49deg, $-color-black, 'dark' 'light', #808080 #999999, 0% 100%)) !default; // info 背景颜色
$-tag-primary-bg: var(--wot-tag-primary-bg, $-color-theme) !default; // 主背景颜色
$-tag-danger-bg: var(--wot-tag-danger-bg, $-color-danger) !default; // danger 背景颜色
$-tag-warning-bg: var(--wot-tag-warning-bg, $-color-warning) !default; // warning 背景颜色
$-tag-success-bg: var(--wot-tag-success-bg, $-color-success) !default; // success 背景颜色
$-tag-round-color: var(--wot-tag-round-color, rgba(102, 102, 102, 1)) !default; // round 字体颜色
$-tag-round-border-color: var(--wot-tag-round-border-color, rgba(225, 225, 225, 1)) !default; // round 边框颜色
$-tag-round-radius: var(--wot-tag-round-radius, 12px) !default; // round 圆角大小
$-tag-mark-radius: var(--wot-tag-mark-radius, 6px 2px 6px 2px) !default; // mark 圆角大小
$-tag-close-size: var(--wot-tag-close-size, 14px) !default; // 关闭按钮字号
$-tag-close-color: var(--wot-tag-close-color, $-tag-info-color) !default; // 关闭按钮颜色
$-tag-close-active-color: var(--wot-tag-close-active-color, rgba(0, 0, 0, 0.45)) !default; // 关闭按钮 active 颜色
/* toast */
$-toast-padding: var(--wot-toast-padding, 16px 24px) !default; // padding
$-toast-max-width: var(--wot-toast-max-width, 300px) !default; // 最大宽度
$-toast-radius: var(--wot-toast-radius, 8px) !default; // 圆角大小
$-toast-bg: var(--wot-toast-bg, $-overlay-bg) !default; // 背景色
$-toast-fs: var(--wot-toast-fs, $-fs-content) !default; // 字号
$-toast-with-icon-min-width: var(--wot-toast-with-icon-min-width, 150px) !default; // 有图标的情况下最小宽度
$-toast-icon-size: var(--wot-toast-icon-size, 32px) !default; // 图标大小
$-toast-icon-margin-right: var(--wot-toast-icon-margin-right, 12px) !default; // 图标右边距
$-toast-icon-margin-bottom: var(--wot-toast-icon-margin-bottom, 12px) !default; // 图标下边距
$-toast-loading-padding: var(--wot-toast-loading-padding, 10px) !default; // loading 下的padding
$-toast-box-shadow: var(--wot-toast-box-shadow, 0px 6px 16px 0px rgba(0, 0, 0, 0.08)) !default; // 外部阴影
/* loading */
$-loading-size: var(--wot-loading-size, 32px) !default; // loading 大小
/* tooltip */
$-tooltip-bg: var(--wot-tooltip-bg, rgba(38, 39, 40, 0.8)) !default; // 背景色
$-tooltip-color: var(--wot-tooltip-color, $-color-white) !default; // 文字颜色
$-tooltip-radius: var(--wot-tooltip-radius, 8px) !default; // 圆角大小
$-tooltip-arrow-size: var(--wot-tooltip-arrow-size, 5px) !default; // 箭头大小
$-tooltip-fs: var(--wot-tooltip-fs, $-fs-content) !default; // 字号
$-tooltip-blur: var(--wot-tooltip-blur, 10px) !default; // 背景高斯模糊效果
$-tooltip-padding: var(--wot-tooltip-padding, 9px 20px) !default; // 间距
$-tooltip-close-size: var(--wot-tooltip-close-size, 6px) !default; // 背景高斯模糊效果
$-tooltip-z-index: var(--wot-tooltip-z-index, 500) !default;
$-tooltip-line-height: var(--wot-tooltip-line-height, 18px) !default; // 行高
/* popover */
$-popover-bg: var(--wot-popover-bg, $-color-white) !default; // 背景色
$-popover-color: var(--wot-popover-color, rgba(0, 0, 0, 0.85)) !default; // 文字颜色
$-popover-box-shadow: var(--wot-popover-box-shadow, 0px 2px 10px 0px rgba(0, 0, 0, 0.1)) !default; // 阴影颜色
$-popover-arrow-box-shadow: var(--wot-popover-arrow-box-shadow, 0px 2px 10px 0px rgba(0, 0, 0, 0.2)) !default; // 阴影颜色
$-popover-border-color: var(--wot-popover-border-color, rgba(0, 0, 0, 0.09)) !default; // 阴影颜色
$-popover-radius: var(--wot-popover-radius, 4px) !default; // 圆角大小
$-popover-arrow-size: var(--wot-popover-arrow-size, 6px) !default; // 箭头大小
$-popover-fs: var(--wot-popover-fs, $-fs-content) !default; // 字号
$-popover-padding: var(--wot-popover-padding, 15px) !default; // 间距
$-popover-line-height: var(--wot-popover-line-height, 18px) !default; // 行高
$-popover-z-index: var(--wot-popover-z-index, $-tooltip-z-index) !default;
/* grid-item */
$-grid-item-fs: var(--wot-grid-item-fs, 12px) !default; // 字号
$-grid-item-bg: var(--wot-grid-item-bg, $-color-white) !default; // 字号
$-grid-item-padding: var(--wot-grid-item-padding, 14px 0px) !default; // 内容的 padding
$-grid-item-border-color: var(--wot-grid-item-border-color, $-color-border-light) !default; // 边框颜色
$-grid-item-hover-bg: var(--wot-grid-item-hover-bg, $-color-gray-3) !default; // hover背景色
$-grid-item-hover-bg-dark: var(--wot-grid-item-hover-bg-dark, $-color-gray-7) !default; // 暗黑模式hover背景色
/* statustip */
$-statustip-fs: var(--wot-statustip-fs, $-fs-content) !default; // 字号
$-statustip-color: var(--wot-statustip-color, rgba(0, 0, 0, 0.45)) !default; // 文字颜色
$-statustip-line-height: var(--wot-statustip-line-height, 16px) !default; // 文字行高
$-statustip-padding: var(--wot-statustip-padding, 5px 10px) !default; // 间距
/* card */
$-card-bg: var(--wot-card-bg, $-color-white) !default; // 背景色
$-card-fs: var(--wot-card-fs, $-fs-content) !default; // 卡片字号
$-card-padding: var(--wot-card-padding, 0 $-size-side-padding) !default; // 内边距
$-card-footer-padding: var(--wot-card-footer-padding, 12px 0 16px) !default; // 底部内边距
$-card-shadow-color: var(--wot-card-shadow-color, 0px 4px 8px 0px rgba(0, 0, 0, 0.02)) !default; // 阴影
$-card-radius: var(--wot-card-radius, 8px) !default; // 圆角大小
$-card-line-height: var(--wot-card-line-height, 1.1) !default; // 行高
$-card-margin: var(--wot-card-margin, 0 $-size-side-padding) !default; // 外边距
$-card-title-color: var(--wot-card-title-color, rgba(0, 0, 0, 0.85)) !default; // 标题颜色
$-card-title-fs: var(--wot-card-title-fs, $-fs-title) !default; // 矩形卡片标题字号
$-card-content-border-color: var(--wot-card-content-border-color, rgba(0, 0, 0, 0.09)) !default; // 内容边框
$-card-rectangle-title-padding: var(--wot-card-rectangle-title-padding, 15px 15px 12px) !default; // 矩形卡片头部内边距
$-card-rectangle-content-padding: var(--wot-card-rectangle-content-padding, 16px 0) !default; // 矩形卡片内容内边距
$-card-rectangle-footer-padding: var(--wot-card-rectangle-footer-padding, 12px 0) !default; // 矩形卡片底部内边距
$-card-content-color: var(--wot-card-content-color, rgba(0, 0, 0, 0.45)) !default; // 文本内容颜色
$-card-content-line-height: var(--wot-card-content-line-height, 1.428) !default; // 文本内容行高
$-card-content-margin: var(--wot-card-content-margin, 13px 0 12px) !default; // 内容外边距
$-card-content-rectangle-margin: var(--wot-card-content-rectangle-margin, 14px 0 12px) !default; // 矩形卡片内容外边距
/* upload */
$-upload-size: var(--wot-upload-size, 80px) !default; // upload的外边框默认尺寸
$-upload-evoke-icon-size: var(--wot-upload-evoke-icon-size, 32px) !default; // 唤起项的图标大小
$-upload-evoke-bg: var(--wot-upload-evoke-bg, rgba(0, 0, 0, 0.04)) !default; // 唤起项的背景色
$-upload-evoke-color: var(--wot-upload-evoke-color, rgba(0, 0, 0, 0.25)) !default; // 唤起项的图标颜色
$-upload-evoke-disabled-color: var(--wot-upload-evoke-disabled-color, rgba(0, 0, 0, 0.09)) !default; // 唤起项禁用颜色
$-upload-close-icon-size: var(--wot-upload-close-icon-size, 16px) !default; // 移除按钮尺寸
$-upload-close-icon-color: var(--wot-upload-close-icon-color, rgba(0, 0, 0, 0.65)) !default; // 移除按钮颜色
$-upload-progress-fs: var(--wot-upload-progress-fs, 14px) !default; // 进度文字字号
$-upload-file-fs: var(--wot-upload-file-fs, 12px) !default; // 文件名字号
$-upload-file-color: var(--wot-upload-file-color, $-color-secondary) !default; // 文件名字颜色
$-upload-preview-name-fs: var(--wot-upload-preview-name-fs, 12px) !default; // 预览图片名字号
$-upload-preview-icon-size: var(--wot-upload-preview-icon-size, 24px) !default; // 预览内部图标尺寸
$-upload-preview-name-bg: var(--wot-upload-preview-name-bg, rgba(0, 0, 0, 0.6)) !default; // 预览文件名背景色
$-upload-preview-name-height: var(--wot-upload-preview-name-height, 22px) !default; // 预览文件名背景高度
$-upload-cover-icon-size: var(--wot-upload-cover-icon-size, 22px) !default; // 视频/文件图标尺寸
/* curtain */
$-curtain-content-radius: var(--wot-curtain-content-radius, 24px) !default; // 内容圆角
$-curtain-content-close-color: var(--wot-curtain-content-close-color, $-color-white) !default; // 关闭按钮颜色
$-curtain-content-close-fs: var(--wot-curtain-content-close-fs, $-fs-big) !default; // 关闭按钮大小
/* notify */
$-notify-text-color: var(--wot-notify-text-color, $-color-white) !default;
$-notify-padding: var(--wot-notify-padding, 8px 16px) !default;
$-notify-font-size: var(--wot-notify-font-size, $-fs-content) !default;
$-notify-line-height: var(--wot-notify-line-height, 20px) !default;
$-notify-primary-background: var(--wot-notify-primary-background, $-color-theme) !default;
$-notify-success-background: var(--wot-notify-success-background, $-color-success) !default;
$-notify-danger-background: var(--wot-notify-danger-background, $-color-danger) !default;
$-notify-warning-background: var(--wot-notify-warning-background, $-color-warning) !default;
/* skeleton */
$-skeleton-background-color: var(--wot-skeleton-background-color, #eee) !default;
$-skeleton-animation-gradient: var(--wot-skeleton-animation-gradient, rgba(0, 0, 0, 0.04)) !default;
$-skeleton-animation-flashed: var(--wot-skeleton-animation-flashed, rgba(230, 230, 230, 0.3)) !default;
$-skeleton-text-height-default: var(--wot-skeleton-text-height-default, 16px) !default;
$-skeleton-rect-height-default: var(--wot-skeleton-rect-height-default, 16px) !default;
$-skeleton-circle-height-default: var(--wot-skeleton-circle-height-default, 48px) !default;
$-skeleton-row-margin-bottom: var(--wot-skeleton-row-margin-bottom, 16px) !default;
$-skeleton-border-radius-text: var(--wot-skeleton-border-radius-text, 2px) !default;
$-skeleton-border-radius-rect: var(--wot-skeleton-border-radius-rect, 4px) !default;
$-skeleton-border-radius-circle: var(--wot-skeleton-border-radius-circle, 50%) !default;
/* circle */
$-circle-text-color: var(--wot-circle-text-color, $-color-content) !default; // circle文字颜色
/* swiper */
$-swiper-radius: var(--wot-swiper-radius, 8px);
$-swiper-item-padding: var(--wot-swiper-item-padding, 0);
$-swiper-item-text-color: var(--wot-swiper-item-text-color, #ffffff);
$-swiper-item-text-fs: var(--wot-swiper-item-text-fs, $-fs-title);
/* swiper-nav */
// dot & dots-bar
$-swiper-nav-dot-color: var(--wot-swiper-nav-dot-color, $-font-white-2) !default;
$-swiper-nav-dot-active-color: var(--wot-swiper-nav-dot-active-color, $-font-white-1) !default;
$-swiper-nav-dot-size: var(--wot-swiper-nav-dot-size, 12rpx) !default;
$-swiper-nav-dots-bar-active-width: var(--wot-swiper-nav-dots-bar-active-width, 40rpx) !default;
// fraction
$-swiper-nav-fraction-color: var(--wot-swiper-nav-fraction-color, $-font-white-1) !default;
$-swiper-nav-fraction-bg-color: var(--wot-swiper-nav-fraction-bg-color, $-font-gray-3) !default;
$-swiper-nav-fraction-height: var(--wot-swiper-nav-fraction-height, 48rpx) !default;
$-swiper-nav-fraction-font-size: var(--wot-swiper-nav-fraction-font-size, 24rpx) !default;
// button
$-swiper-nav-btn-color: var(--wot-swiper-nav-btn-color, $-font-white-1) !default;
$-swiper-nav-btn-bg-color: var(--wot-swiper-nav-btn-bg-color, $-font-gray-3) !default;
$-swiper-nav-btn-size: var(--wot-swiper-nav-btn-size, 48rpx) !default;
/* segmented */
$-segmented-padding: var(--wot-segmented-padding, 4px) !default; // 分段器padding
$-segmented-item-bg-color: var(--wot-segmented-item-bg-color, #eeeeee) !default;
$-segmented-item-color: var(--wot-segmented-item-color, rgba(0, 0, 0, 0.85)) !default; // 标题文字颜色
$-segmented-item-acitve-bg: var(--wot-segmented-item-acitve-bg, #ffffff) !default; // 标题文字颜色
$-segmented-item-disabled-color: var(--wot-segmented-item-disabled-color, rgba(0, 0, 0, 0.25)) !default; // 标题文字禁用颜色
/* tabbar */
$-tabbar-height: var(--wot-tabbar-height, 50px) !default;
$-tabbar-box-shadow: var(
--wot-tabbar-box-shadow,
0 6px 30px 5px rgba(0, 0, 0, 0.05),
0 16px 24px 2px rgba(0, 0, 0, 0.04),
0 8px 10px -5px rgba(0, 0, 0, 0.08)
) !default; // round类型tabbar阴影
/* tabbar-item */
$-tabbar-item-title-font-size: var(--wot-tabbar-item-title-font-size, 10px) !default; // tabbar选项文字大小
$-tabbar-item-title-line-height: var(--wot-tabbar-item-title-line-height, initial) !default; // tabbar选项标题文字行高
$-tabbar-inactive-color: var(--wot-tabbar-inactive-color, $-color-title) !default; // 标题文字和图标颜色
$-tabbar-active-color: var(--wot-tabbar-active-color, $-color-theme) !default; // 选中文字和图标颜色
$-tabbar-item-icon-size: var(--wot-tabbar-item-icon-size, 20px) !default; // tabbar选项图标大小
/* navbar */
$-navbar-height: var(--wot-navbar-height, 44px) !default; // navbar高度
$-navbar-color: var(--wot-navbar-color, $-font-gray-1) !default; // navbar字体颜色
$-navbar-background: var(--wot-navbar-background, $-color-white) !default; // navbar背景颜色
$-navbar-arrow-size: var(--wot-navbar-arrow-size, 24px) !default; // navbar左箭头图标大小
$-navbar-desc-font-size: var(--wot-navbar-desc-font-size, 16px); // navbar 左箭头字体大小
$-navbar-desc-font-color: var(--wot-navbar-desc-font-color, $-font-gray-1) !default; // navbar左右两侧字体颜色
$-navbar-title-font-size: var(--wot-navbar-title-font-size, 18px); // navbar title字体大小
$-navbar-title-font-weight: var(--wot-navbar-title-font-weight, 600); // navbar title字重
$-navbar-disabled-opacity: var(--wot-navbar-disabled-opacity, 0.6) !default; // navbar左右两侧字体禁用
$-navbar-hover-color: var(--wot-navbar-hover-color, #eee) !default; // navbar hover样式
/* navbar-capsule */
$-navbar-capsule-border-color: var(--wot-navbar-capsule-border-color, #e7e7e7) !default;
$-navbar-capsule-border-radius: var(--wot-navbar-capsule-border-radius, 16px) !default;
$-navbar-capsule-width: var(--wot-navbar-capsule-width, 88px) !default;
$-navbar-capsule-height: var(--wot-navbar-capsule-height, 32px) !default;
$-navbar-capsule-icon-size: var(--wot-navbar-capsule-icon-size, 20px) !default; // navbar capsule图标大小
/* table */
$-table-color: var(--wot-table-color, $-font-gray-1) !default; // 表格字体颜色
$-table-bg: var(--wot-table-bg, #ffffff) !default; // 表格背景颜色
$-table-stripe-bg: var(--wot-table-stripe-bg, #f3f3f3) !default; // 表格背景颜色
$-table-border-color: var(--wot-table-border-color, #ececec) !default; // 表格边框颜色
$-table-font-size: var(--wot-table-font-size, 13px) !default; // 表格字体大小
/* sidebar */
$-sidebar-bg: var(--wot-sidebar-bg, $-color-gray-1) !default; // 侧边栏背景色
$-sidebar-width: var(--wot-sidebar-width, 104px) !default; // 侧边栏宽度
$-sidebar-height: var(--wot-sidebar-height, 100%) !default; // 侧边栏高度
/* sidebar-item */
$-sidebar-color: var(--wot-sidebar-color, $-font-gray-1) !default;
$-sidebar-item-height: var(--wot-sidebar-item-height, 56px) !default;
$-sidebar-item-line-height: var(--wot-sidebar-item-line-height, 24px) !default;
$-sidebar-disabled-color: var(--wot-side-bar-disabled-color, $-font-gray-4) !default;
$-sidebar-active-color: var(--wot-sidebar-active-color, $-color-theme) !default; // 激活项字体颜色
$-sidebar-active-bg: var(--wot-sidebar-active-bg, $-color-white) !default; // 激活项背景颜色
$-sidebar-hover-bg: var(--wot-sidebar-hover-bg, $-color-gray-2) !default; // 激活项点击背景颜色
$-sidebar-border-radius: var(--wot-sidebar-border-radius, 8px) !default;
$-sidebar-font-size: var(--wot-sidebar-font-size, 16px) !default;
$-sidebar-icon-size: var(--wot-sidebar-icon-size, 20px) !default;
$-sidebar-active-border-width: var(--wot-sidebar-active-border-width, 4px) !default;
$-sidebar-active-border-height: var(--wot-sidebar-active-border-height, 16px) !default;
/* fab */
$-fab-trigger-height: var(--wot-fab-trigger-height, 56px) !default;
$-fab-trigger-width: var(--wot-fab-trigger-width, 56px) !default;
$-fab-actions-padding: var(--wot-actions-padding, 12px) !default;
$-fab-icon-fs: var(--wot-fab-icon-fs, 20px) !default;
/* count-down */
$-count-down-text-color: var(--wot-count-down-text-color, $-color-gray-8) !default;
$-count-down-font-size: var(--wot-count-down-font-size, $-fs-content) !default;
$-count-down-line-height: var(--wot-count-down-line-height, 20px) !default;
/* keyboard */
$-keyboard-key-height: var(--wot-keyboard-key-height, 48px) !default;
$-keyboard-key-font-size: var(--wot-keyboard-key-font-size, 28px) !default;
$-keyboard-key-background: var(--wot-keyboard-key-background, $-color-white) !default;
$-keyboard-key-border-radius: var(--wot-keyboard-key-border-radius, 8px) !default;
$-keyboard-delete-font-size: var(--wot-keyboard-delete-font-size, 16px) !default;
$-keyboard-key-active-color: var(--wot-keyboard-key-active-color, $-color-gray-3) !default;
$-keyboard-button-text-color: var(--wot-keyboard-button-text-color, $-color-white) !default;
$-keyboard-button-background: var(--wot-keyboard--button-background, $-color-theme) !default;
$-keyboard-button-active-opacity: var(--wot-keyboard-button-active-opacity, 0.6) !default;
$-keyboard-background: var(--wot-keyboard-background, $-color-gray-2) !default;
$-keyboard-title-height: var(--wot-keyboard-title-height, 34px) !default;
$-keyboard-title-color: var(--wot-keyboard-title-color, $-color-gray-7) !default;
$-keyboard-title-font-size: var(--wot-keyboard-title-font-size, 16px) !default;
$-keyboard-close-padding: var(--wot-keyboard-title-font-size, 0 16px) !default;
$-keyboard-close-color: var(--wot-keyboard-close-color, $-color-theme) !default;
$-keyboard-close-font-size: var(--wot-keyboard-close-font-size, 14px) !default;
$-keyboard-icon-size: var(--wot-keyboard-icon-size, 22px) !default;
/* number-keyboard */
$-number-keyboard-key-height: var(--wot-number-keyboard-key-height, 48px) !default;
$-number-keyboard-key-font-size: var(--wot-number-keyboard-key-font-size, 28px) !default;
$-number-keyboard-key-background: var(--wot-number-keyboard-key-background, $-color-white) !default;
$-number-keyboard-key-border-radius: var(--wot-number-keyboard-key-border-radius, 8px) !default;
$-number-keyboard-delete-font-size: var(--wot-number-keyboard-delete-font-size, 16px) !default;
$-number-keyboard-key-active-color: var(--wot-number-keyboard-key-active-color, $-color-gray-3) !default;
$-number-keyboard-button-text-color: var(--wot-number-keyboard-button-text-color, $-color-white) !default;
$-number-keyboard-button-background: var(--wot-number-keyboard--button-background, $-color-theme) !default;
$-number-keyboard-button-active-opacity: var(--wot-number-keyboard-button-active-opacity, 0.6) !default;
$-number-keyboard-background: var(--wot-number-keyboard-background, $-color-gray-2) !default;
$-number-keyboard-title-height: var(--wot-number-keyboard-title-height, 34px) !default;
$-number-keyboard-title-color: var(--wot-number-keyboard-title-color, $-color-gray-7) !default;
$-number-keyboard-title-font-size: var(--wot-number-keyboard-title-font-size, 16px) !default;
$-number-keyboard-close-padding: var(--wot-number-keyboard-title-font-size, 0 16px) !default;
$-number-keyboard-close-color: var(--wot-number-keyboard-close-color, $-color-theme) !default;
$-number-keyboard-close-font-size: var(--wot-number-keyboard-close-font-size, 14px) !default;
$-number-keyboard-icon-size: var(--wot-number-keyboard-icon-size, 22px) !default;
/* passwod-input */
$-password-input-height: var(--wot-password-input-height, 50px);
$-password-input-margin: var(--wot-password-input-margin, 16px);
$-password-input-font-size: var(--wot-password-input-margin, 20px);
$-password-input-radius: var(--wot-password-input-radius, 6px);
$-password-input-background: var(--wot-password-input-background, #fff);
$-password-input-info-color: var(--wot-password-input-info-color, $-color-info);
$-password-input-info-font-size: var(--wot-password-input-info-font-size, $-fs-content);
$-password-input-border-color: var(--wot-password-border-color, #ebedf0);
$-password-input-error-info-color: var(--wot-password-input-error-info-color, $-color-danger);
$-password-input-dot-size: var(--wot-password-input-dot-size, 10px);
$-password-input-dot-color: var(--wot-password-input-dot-color, $-color-gray-8);
$-password-input-text-color: var(--wot-password-input-text-color, $-color-gray-8);
$-password-input-cursor-color: var(--wot-password-input-cursor-color, $-color-gray-8);
$-password-input-cursor-width: var(--wot-password-input-cursor-width, 1px);
$-password-input-cursor-height: var(--wot-password-input-cursor-height, 40%);
$-password-input-cursor-duration: var(--wot-password-input-cursor-duration, 1s);
/* form-item */
$-form-item-error-message-color: var(--wot-form-item-error-message-color, $-color-danger) !default;
$-form-item-error-message-font-size: var(--wot-form-item-error-message-font-size, $-fs-secondary) !default;
$-form-item-error-message-line-height: var(--wot-form-item-error-message-line-height, 24px) !default;
/* backtop */
$-backtop-bg: var(--wot-backtop-bg, #e1e1e1) !default;
$-backtop-icon-size: var(--wot-backtop-icon-size, 20px) !default;
/* index-bar */
$-index-bar-index-font-size: var(--wot-index-bar-index-font-size, $-fs-aid) !default;
/* text */
$-text-info-color: var(--wot-text-info-color, $-color-info) !default;
$-text-primary-color: var(--wot-text-primary-color, $-color-theme) !default;
$-text-error-color: var(--wot-text-error-color, $-color-danger) !default;
$-text-warning-color: var(--wot-text-warning-color, $-color-warning) !default;
$-text-success-color: var(--wot-text-success-color, $-color-success) !default;
/* video-preview */
$-video-preview-bg: var(--wot-video-preview-bg, rgba(0, 0, 0, 0.8)) !default; // 背景色
$-video-preview-close-color: var(--wot-video-preview-close-color, #fff) !default; // 图标颜色
$-video-preview-close-font-size: var(--wot-video-preview-close-font-size, 20px) !default; // 图标大小
/* img-cropper */
$-img-cropper-icon-size: var(--wot-img-cropper-icon-size, $-fs-big) !default; // 图标大小
$-img-cropper-icon-color: var(--wot-img-cropper-icon-color, #fff) !default; // 图标颜色
/* floating-panel */
$-floating-panel-bg: var(--wot-floating-panel-bg, $-color-white) !default; // 背景色
$-floating-panel-radius: var(--wot-floating-panel-radius, 16px) !default; // 圆角
$-floating-panel-z-index: var(--wot-floating-panel-z-index, 99) !default; // 层级
$-floating-panel-header-height: var(--wot-floating-panel-header-height, 30px) !default; // 头部高度
$-floating-panel-bar-width: var(--wot-floating-panel-bar-width, 20px) !default; // bar 宽度
$-floating-panel-bar-height: var(--wot-floating-panel-bar-height, 3px) !default; // bar 高度
$-floating-panel-bar-bg: var(--wot-floating-panel-bar-bg, $-color-gray-5) !default; // bar 背景色
$-floating-panel-bar-radius: var(--wot-floating-panel-bar-radius, 4px) !default; // bar 圆角
$-floating-panel-content-bg: var(--wot-floating-panel-content-bg, $-color-white) !default; // 内容背景色
/* signature */
$-signature-bg: var(--wot-signature-bg, $-color-white) !default; // 背景色
$-signature-radius: var(--wot-signature-radius, 4px) !default; // 圆角
$-signature-border: var(--wot-signature-border, 1px solid $-color-gray-5) !default; // 边框圆角
$-signature-footer-margin-top: var(--wot-signature-footer-margin-top, 8px) !default; // 底部按钮上边距
$-signature-button-margin-left: var(--wot-signature-button-margin-left, 8px) !default; // 底部按钮左边距

View File

@ -0,0 +1,29 @@
const _b64chars: string[] = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']
const _mkUriSafe = (src: string): string => src.replace(/[+/]/g, (m0: string) => (m0 === '+' ? '-' : '_')).replace(/=+\$/m, '')
const fromUint8Array = (src: Uint8Array, rfc4648 = false): string => {
let b64 = ''
for (let i = 0, l = src.length; i < l; i += 3) {
const [a0, a1, a2] = [src[i], src[i + 1], src[i + 2]]
const ord = (a0 << 16) | (a1 << 8) | a2
b64 += _b64chars[ord >>> 18]
b64 += _b64chars[(ord >>> 12) & 63]
b64 += typeof a1 !== 'undefined' ? _b64chars[(ord >>> 6) & 63] : '='
b64 += typeof a2 !== 'undefined' ? _b64chars[ord & 63] : '='
}
return rfc4648 ? _mkUriSafe(b64) : b64
}
const _btoa: (s: string) => string =
typeof btoa === 'function'
? (s: string) => btoa(s)
: (s: string) => {
if (s.charCodeAt(0) > 255) {
throw new RangeError('The string contains invalid characters.')
}
return fromUint8Array(Uint8Array.from(s, (c: string) => c.charCodeAt(0)))
}
const utob = (src: string): string => unescape(encodeURIComponent(src))
export default function encode(src: string, rfc4648 = false): string {
const b64 = _btoa(utob(src))
return rfc4648 ? _mkUriSafe(b64) : b64
}

View File

@ -0,0 +1,49 @@
/**
* 适配 canvas 2d 上下文
* @param ctx canvas 2d 上下文
* @returns
*/
export function canvas2dAdapter(ctx: CanvasRenderingContext2D): UniApp.CanvasContext {
return Object.assign(ctx, {
setFillStyle(color: string | CanvasGradient) {
ctx.fillStyle = color
},
setStrokeStyle(color: string | CanvasGradient | CanvasPattern) {
ctx.strokeStyle = color
},
setLineWidth(lineWidth: number) {
ctx.lineWidth = lineWidth
},
setLineCap(lineCap: 'butt' | 'round' | 'square') {
ctx.lineCap = lineCap
},
setFontSize(font: string) {
ctx.font = font
},
setGlobalAlpha(alpha: number) {
ctx.globalAlpha = alpha
},
setLineJoin(lineJoin: 'bevel' | 'round' | 'miter') {
ctx.lineJoin = lineJoin
},
setTextAlign(align: 'left' | 'center' | 'right') {
ctx.textAlign = align
},
setMiterLimit(miterLimit: number) {
ctx.miterLimit = miterLimit
},
setShadow(offsetX: number, offsetY: number, blur: number, color: string) {
ctx.shadowOffsetX = offsetX
ctx.shadowOffsetY = offsetY
ctx.shadowBlur = blur
ctx.shadowColor = color
},
setTextBaseline(textBaseline: 'top' | 'bottom' | 'middle') {
ctx.textBaseline = textBaseline
},
createCircularGradient() {},
draw() {},
addColorStop() {}
}) as unknown as UniApp.CanvasContext
}

View File

@ -0,0 +1,34 @@
/*
* @Author: weisheng
* @Date: 2023-07-02 22:51:06
* @LastEditTime: 2024-03-16 19:59:07
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/common/clickoutside.ts
* 记得注释
*/
let queue: any[] = []
export function pushToQueue(comp: any) {
queue.push(comp)
}
export function removeFromQueue(comp: any) {
queue = queue.filter((item) => {
return item.$.uid !== comp.$.uid
})
}
export function closeOther(comp: any) {
queue.forEach((item) => {
if (item.$.uid !== comp.$.uid) {
item.$.exposed.close()
}
})
}
export function closeOutside() {
queue.forEach((item) => {
item.$.exposed.close()
})
}

View File

@ -0,0 +1,148 @@
import { isDate } from './util'
/* eslint-disable */
class Dayjs {
utc: boolean
date: Date
timeZone: number
timeZoneString: any
mYear: any
mMonth: any
mDay: any
mWeek: any
mHour: any
mMinute: any
mSecond: any
constructor(dateStr?: string | number | Date) {
this.utc = false
const parsedDate = this.parseConfig(dateStr)
this.date = new Date(parsedDate)
this.timeZone = this.date.getTimezoneOffset() / 60
this.timeZoneString = this.padNumber(String(-1 * this.timeZone).replace(/^(.)?(\d)/, '$10$200'), 5, '+')
this.mYear = this.date.getFullYear()
this.mMonth = this.date.getMonth()
this.mDay = this.date.getDate()
this.mWeek = this.date.getDay()
this.mHour = this.date.getHours()
this.mMinute = this.date.getMinutes()
this.mSecond = this.date.getSeconds()
}
parseConfig(dateStr?:string | number | Date) {
if (!dateStr) return new Date()
if (isDate(dateStr)) return dateStr
if (/^(\d){8}$/.test(dateStr as string)) {
this.utc = true
return `${(dateStr as string).substr(0, 4)}-${(dateStr as string).substr(4, 2)}-${(dateStr as string).substr(6, 2)}`
}
return dateStr
}
padNumber(num:string, length:number, padChar:string) {
return !num || num.length >= length ? num : `${Array(length + 1 - num.length).join(padChar)}${num}`
}
year() {
return this.mYear
}
month() {
return this.mMonth
}
unix() {
const timeZoneOffset = this.utc ? 60 * this.timeZone * 60 * 1000 : 0
return Math.floor((this.date.getTime() + timeZoneOffset) / 1000)
}
toString() {
return this.date.toUTCString()
}
startOf(unit:string) {
switch (unit) {
case 'year':
return new Dayjs(new Date(this.year(), 0, 1))
case 'month':
return new Dayjs(new Date(this.year(), this.month(), 1))
default:
return this
}
}
add(amount:number, unit:string) {
let interval
switch (unit) {
case 'm':
case 'minutes':
interval = 60
break
case 'h':
case 'hours':
interval = 60 * 60
break
case 'd':
case 'days':
interval = 24 * 60 * 60
break
case 'w':
case 'weeks':
interval = 7 * 24 * 60 * 60
break
default:
interval = 1
}
const newUnixTime = this.unix() + amount * interval
return new Dayjs(1000 * newUnixTime)
}
subtract(amount:number, unit:string) {
return this.add(-1 * amount, unit)
}
format(formatStr = 'YYYY-MM-DDTHH:mm:ssZ') {
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
return formatStr.replace(/Y{2,4}|M{1,2}|D{1,2}|d{1,4}|H{1,2}|m{1,2}|s{1,2}|Z{1,2}/g, (match) => {
switch (match) {
case 'YY':
return String(this.mYear).slice(-2)
case 'YYYY':
return String(this.mYear)
case 'M':
return String(this.mMonth + 1)
case 'MM':
return this.padNumber(String(this.mMonth + 1), 2, '0')
case 'D':
return String(this.mDay)
case 'DD':
return this.padNumber(String(this.mDay), 2, '0')
case 'd':
return String(this.mWeek)
case 'dddd':
return weekdays[this.mWeek]
case 'H':
return String(this.mHour)
case 'HH':
return this.padNumber(String(this.mHour), 2, '0')
case 'm':
return String(this.mMinute)
case 'mm':
return this.padNumber(String(this.mMinute), 2, '0')
case 's':
return String(this.mSecond)
case 'ss':
return this.padNumber(String(this.mSecond), 2, '0')
case 'Z':
return `${this.timeZoneString.slice(0, -2)}:00`
case 'ZZ':
return this.timeZoneString
default:
return match
}
})
}
}
export function dayjs(dateStr?: string | number | Date) {
return new Dayjs(dateStr)
}

View File

@ -0,0 +1,8 @@
export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const CHANGE_EVENT = 'change'
export const INPUT_EVENT = 'input'
export const CLICK_EVENT = 'click'
export const CLOSE_EVENT = 'close'
export const OPEN_EVENT = 'open'
export const CONFIRM_EVENT = 'confirm'
export const CANCEL_EVENT = 'cancel'

View File

@ -0,0 +1,43 @@
import { isPromise } from './util'
function noop() {}
export type Interceptor = (...args: any[]) => Promise<boolean> | boolean | undefined | void
export function callInterceptor(
interceptor: Interceptor | undefined,
{
args = [],
done,
canceled,
error
}: {
args?: unknown[]
done: () => void
canceled?: () => void
error?: () => void
}
) {
if (interceptor) {
// eslint-disable-next-line prefer-spread
const returnVal = interceptor.apply(null, args)
if (isPromise(returnVal)) {
returnVal
.then((value) => {
if (value) {
done()
} else if (canceled) {
canceled()
}
})
.catch(error || noop)
} else if (returnVal) {
done()
} else if (canceled) {
canceled()
}
} else {
done()
}
}

View File

@ -0,0 +1,51 @@
import type { PropType } from 'vue'
export const unknownProp = null as unknown as PropType<unknown>
export const numericProp = [Number, String]
export const truthProp = {
type: Boolean,
default: true as const
}
export const makeRequiredProp = <T>(type: T) => ({
type,
required: true as const
})
export const makeArrayProp = <T>() => ({
type: Array as PropType<T[]>,
default: () => []
})
export const makeBooleanProp = <T>(defaultVal: T) => ({
type: Boolean,
default: defaultVal
})
export const makeNumberProp = <T>(defaultVal: T) => ({
type: Number,
default: defaultVal
})
export const makeNumericProp = <T>(defaultVal: T) => ({
type: numericProp,
default: defaultVal
})
export const makeStringProp = <T>(defaultVal: T) => ({
type: String as unknown as PropType<T>,
default: defaultVal
})
export const baseProps = {
/**
* 自定义根节点样式
*/
customStyle: makeStringProp(''),
/**
* 自定义根节点样式类
*/
customClass: makeStringProp('')
}

View File

@ -0,0 +1,778 @@
import { AbortablePromise } from './AbortablePromise'
type NotUndefined<T> = T extends undefined ? never : T
/**
* 生成uuid
* @returns string
*/
export function uuid() {
return s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4()
}
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
/**
* @description 对num自动填充px
* @param {Number} num
* @return {string} num+px
*/
export function addUnit(num: number | string) {
return Number.isNaN(Number(num)) ? `${num}` : `${num}px`
}
/**
* @description 判断target是否对象
* @param value
* @return {boolean}
*/
export function isObj(value: any): value is object {
return Object.prototype.toString.call(value) === '[object Object]' || typeof value === 'object'
}
/**
* 获取目标原始类型
* @param target 任意类型
* @returns {string} type 数据类型
*/
export function getType(target: unknown): string {
//
const typeStr = Object.prototype.toString.call(target)
//
const match = typeStr.match(/\[object (\w+)\]/)
const type = match && match.length ? match[1].toLowerCase() : ''
//
return type
}
/**
* @description 默认的外部格式化函数 - picker 组件
* @param items - 要格式化的数据项数组或单个数据项
* @param kv - 配置对象包含 labelKey 作为键值
* @returns 格式化后的字符串
*/
export const defaultDisplayFormat = function (items: any[] | Record<string, any>, kv?: { labelKey?: string }): string {
const labelKey: string = kv?.labelKey || 'value'
if (Array.isArray(items)) {
return items.map((item) => item[labelKey]).join(', ')
} else {
return items[labelKey]
}
}
/**
* @description 默认函数占位符 - pickerView组件
* @param value
* @return value
*/
export const defaultFunction = <T>(value: T): T => value
/**
* @description 检查值是否不为空
* @param value
* @return {Boolean} 是否不为空
*/
export const isDef = <T>(value: T): value is NonNullable<T> => value !== undefined && value !== null
/**
* @description 防止数字小于零
* @param {number} num
* @param {string} label 标签
*/
export const checkNumRange = (num: number, label: string = 'value'): void => {
if (num < 0) {
throw new Error(`${label} shouldn't be less than zero`)
}
}
/**
* @description 防止 pixel 无意义
* @param {number} num
* @param {string} label 标签
*/
export const checkPixelRange = (num: number, label: string = 'value'): void => {
if (num <= 0) {
throw new Error(`${label} should be greater than zero`)
}
}
/**
* RGB 值转换为十六进制颜色代码
* @param {number} r - 红色分量 (0-255)
* @param {number} g - 绿色分量 (0-255)
* @param {number} b - 蓝色分量 (0-255)
* @returns {string} 十六进制颜色代码 (#RRGGBB)
*/
export function rgbToHex(r: number, g: number, b: number): string {
// RGB
const hex = ((r << 16) | (g << 8) | b).toString(16)
// 使 6 RGB
const paddedHex = '#' + '0'.repeat(Math.max(0, 6 - hex.length)) + hex
return paddedHex
}
/**
* 将十六进制颜色代码转换为 RGB 颜色数组
* @param hex 十六进制颜色代码例如'#RRGGBB'
* @returns 包含红绿蓝三个颜色分量的数组
*/
export function hexToRgb(hex: string): number[] {
const rgb: number[] = []
//
for (let i = 1; i < 7; i += 2) {
// rgb
rgb.push(parseInt('0x' + hex.slice(i, i + 2), 16))
}
return rgb
}
/**
* 计算渐变色的中间变量数组
* @param {string} startColor 开始颜色
* @param {string} endColor 结束颜色
* @param {number} step 获取渲染位置默认为中间位置
* @returns {string[]} 渐变色中间颜色变量数组
*/
export const gradient = (startColor: string, endColor: string, step: number = 2): string[] => {
// hexrgb
const sColor: number[] = hexToRgb(startColor)
const eColor: number[] = hexToRgb(endColor)
// R\G\B
const rStep: number = (eColor[0] - sColor[0]) / step
const gStep: number = (eColor[1] - sColor[1]) / step
const bStep: number = (eColor[2] - sColor[2]) / step
const gradientColorArr: string[] = []
for (let i = 0; i < step; i++) {
// hex
gradientColorArr.push(
rgbToHex(parseInt(String(rStep * i + sColor[0])), parseInt(String(gStep * i + sColor[1])), parseInt(String(bStep * i + sColor[2])))
)
}
return gradientColorArr
}
/**
* 确保数值不超出指定范围
* @param {number} num 要限制范围的数值
* @param {number} min 最小范围
* @param {number} max 最大范围
* @returns {number} 在指定范围内的数值
*/
export const range = (num: number, min: number, max: number): number => {
// 使 Math.min Math.max num
return Math.min(Math.max(num, min), max)
}
/**
* 比较两个值是否相等
* @param {any} value1 第一个值
* @param {any} value2 第二个值
* @returns {boolean} 如果值相等则为 true否则为 false
*/
export const isEqual = (value1: any, value2: any): boolean => {
// 使
if (value1 === value2) {
return true
}
//
if (!Array.isArray(value1) || !Array.isArray(value2)) {
return false
}
//
if (value1.length !== value2.length) {
return false
}
//
for (let i = 0; i < value1.length; ++i) {
if (value1[i] !== value2[i]) {
return false
}
}
//
return true
}
/**
* 在数字前补零使其达到指定长度
* @param {number | string} number 要补零的数字
* @param {number} length 目标长度默认为 2
* @returns {string} 补零后的结果
*/
export const padZero = (number: number | string, length: number = 2): string => {
//
let numStr: string = number.toString()
//
while (numStr.length < length) {
numStr = '0' + numStr
}
return numStr
}
/** @description 全局变量id */
export const context = {
id: 1000
}
export type RectResultType<T extends boolean> = T extends true ? UniApp.NodeInfo[] : UniApp.NodeInfo
/**
* 获取节点信息
* @param selector 节点选择器 #id,.class
* @param all 是否返回所有 selector 对应的节点
* @param scope 作用域支付宝小程序无效
* @param useFields 是否使用 fields 方法获取节点信息
* @returns 节点信息或节点信息数组
*/
export function getRect<T extends boolean>(selector: string, all: T, scope?: any, useFields?: boolean): Promise<RectResultType<T>> {
return new Promise<RectResultType<T>>((resolve, reject) => {
let query: UniNamespace.SelectorQuery | null = null
if (scope) {
query = uni.createSelectorQuery().in(scope)
} else {
query = uni.createSelectorQuery()
}
const method = all ? 'selectAll' : 'select'
const callback = (rect: UniApp.NodeInfo | UniApp.NodeInfo[]) => {
if (all && isArray(rect) && rect.length > 0) {
resolve(rect as RectResultType<T>)
} else if (!all && rect) {
resolve(rect as RectResultType<T>)
} else {
reject(new Error('No nodes found'))
}
}
if (useFields) {
query[method](selector).fields({ size: true, node: true }, callback).exec()
} else {
query[method](selector).boundingClientRect(callback).exec()
}
})
}
/**
* 将驼峰命名转换为短横线命名
* @param {string} word 待转换的词条
* @returns {string} 转换后的结果
*/
export function kebabCase(word: string): string {
// 使线
const newWord: string = word
.replace(/[A-Z]/g, function (match) {
return '-' + match
})
.toLowerCase()
return newWord
}
/**
* 将短横线链接转换为驼峰命名
* @param word 需要转换的短横线链接
* @returns 转换后的驼峰命名字符串
*/
export function camelCase(word: string): string {
return word.replace(/-(\w)/g, (_, c) => c.toUpperCase())
}
/**
* 检查给定值是否为数组
* @param {any} value 要检查的值
* @returns {boolean} 如果是数组则返回 true否则返回 false
*/
export function isArray(value: any): value is Array<any> {
// Array.isArray 使
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
// 使 toString
return Object.prototype.toString.call(value) === '[object Array]'
}
/**
* 检查给定值是否为函数
* @param {any} value 要检查的值
* @returns {boolean} 如果是函数则返回 true否则返回 false
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function isFunction<T extends Function>(value: any): value is T {
return getType(value) === 'function' || getType(value) === 'asyncfunction'
}
/**
* 检查给定值是否为字符串
* @param {unknown} value 要检查的值
* @returns {value is string} 如果是字符串则返回 true否则返回 false
*/
export function isString(value: unknown): value is string {
return getType(value) === 'string'
}
/**
* 否是数值
* @param {*} value
*/
export function isNumber(value: any): value is number {
return getType(value) === 'number'
}
/**
* 检查给定值是否为 Promise 对象
* @param {unknown} value 要检查的值
* @returns {value is Promise<any>} 如果是 Promise 对象则返回 true否则返回 false
*/
export function isPromise(value: unknown): value is Promise<any> {
// value object
if (isObj(value) && isDef(value)) {
// value then catch
return isFunction((value as Promise<any>).then) && isFunction((value as Promise<any>).catch)
}
return false // value Promise
}
/**
* 检查给定的值是否为布尔类型
* @param value 要检查的值
* @returns 如果值为布尔类型则返回true否则返回false
*/
export function isBoolean(value: any): value is boolean {
return typeof value === 'boolean'
}
export function isUndefined(value: any): value is undefined {
return typeof value === 'undefined'
}
export function isNotUndefined<T>(value: T): value is NotUndefined<T> {
return !isUndefined(value)
}
/**
* 检查给定的值是否为奇数
* @param value 要检查的值
* @returns
*/
export function isOdd(value: number): boolean {
if (typeof value !== 'number') {
throw new Error('输入必须为数字')
}
// 使
// number 2 1
//
return value % 2 === 1
}
/**
* 是否为base64图片
* @param {string} url
* @return
*/
export function isBase64Image(url: string) {
// 使URL"data:image"Base64
return /^data:image\/(png|jpg|jpeg|gif|bmp);base64,/.test(url)
}
/**
* 将外部传入的样式格式化为可读的 CSS 样式
* @param {object | object[]} styles 外部传入的样式对象或数组
* @returns {string} 格式化后的 CSS 样式字符串
*/
export function objToStyle(styles: Record<string, any> | Record<string, any>[]): string {
// styles
if (isArray(styles)) {
// 使 null
// objToStyle
const result = styles
.filter(function (item) {
return item != null && item !== ''
})
.map(function (item) {
return objToStyle(item)
})
.join(';')
//
return result ? (result.endsWith(';') ? result : result + ';') : ''
}
if (isString(styles)) {
//
return styles ? (styles.endsWith(';') ? styles : styles + ';') : ''
}
// styles
if (isObj(styles)) {
// 使 Object.keys
// 使 null
//
const result = Object.keys(styles)
.filter(function (key) {
return styles[key] != null && styles[key] !== ''
})
.map(function (key) {
// 使 kebabCase kebab-case
// CSS
return [kebabCase(key), styles[key]].join(':')
})
.join(';')
//
return result ? (result.endsWith(';') ? result : result + ';') : ''
}
// styles
return ''
}
/**
* 判断一个对象是否包含任何字段
* @param obj 要检查的对象
* @returns {boolean} 如果对象为空不包含任何字段则返回 true否则返回 false
*/
export function hasFields(obj: unknown): boolean {
// null
if (!isObj(obj) || obj === null) {
return false
}
// 使 Object.keys
return Object.keys(obj).length > 0
}
/**
* 判断一个对象是否为空对象不包含任何字段
* @param obj 要检查的对象
* @returns {boolean} 如果对象为空不包含任何字段则返回 true否则返回 false
*/
export function isEmptyObj(obj: unknown): boolean {
return !hasFields(obj)
}
export const requestAnimationFrame = (cb = () => {}) => {
return new AbortablePromise((resolve) => {
const timer = setInterval(() => {
clearInterval(timer)
resolve(true)
cb()
}, 1000 / 30)
})
}
/**
* 暂停指定时间函数
* @param ms 延迟时间
* @returns
*/
export const pause = (ms: number = 1000 / 30) => {
return new AbortablePromise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer)
resolve(true)
}, ms)
})
}
/**
* 深拷贝函数用于将对象进行完整复制
* @param obj 要深拷贝的对象
* @param cache 用于缓存已复制的对象防止循环引用
* @returns 深拷贝后的对象副本
*/
export function deepClone<T>(obj: T, cache: Map<any, any> = new Map()): T {
// null
if (obj === null || typeof obj !== 'object') {
return obj
}
//
if (isDate(obj)) {
return new Date(obj.getTime()) as any
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as any
}
if (obj instanceof Error) {
const errorCopy = new Error(obj.message) as any
errorCopy.stack = obj.stack
return errorCopy
}
//
if (cache.has(obj)) {
return cache.get(obj)
}
//
const copy: any = Array.isArray(obj) ? [] : {}
//
cache.set(obj, copy)
//
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = deepClone(obj[key], cache)
}
}
return copy as T
}
/**
* 深度合并两个对象
* @param target 目标对象将合并的结果存放在此对象中
* @param source 源对象要合并到目标对象的对象
* @returns 合并后的目标对象
*/
export function deepMerge<T extends Record<string, any>>(target: T, source: Record<string, any>): T {
//
target = deepClone(target)
//
if (typeof target !== 'object' || typeof source !== 'object') {
throw new Error('Both target and source must be objects.')
}
//
for (const prop in source) {
// eslint-disable-next-line no-prototype-builtins
if (!source.hasOwnProperty(prop))
continue
// 使 TypeScript
;(target as Record<string, any>)[prop] = source[prop]
}
return target
}
/**
* 深度合并两个对象
* @param target
* @param source
* @returns
*/
export function deepAssign(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
Object.keys(source).forEach((key) => {
const targetValue = target[key]
const newObjValue = source[key]
if (isObj(targetValue) && isObj(newObjValue)) {
deepAssign(targetValue, newObjValue)
} else {
target[key] = newObjValue
}
})
return target
}
/**
* 构建带参数的URL
* @param baseUrl 基础URL
* @param params 参数对象键值对表示要添加到URL的参数
* @returns 返回带有参数的URL
*/
export function buildUrlWithParams(baseUrl: string, params: Record<string, string>) {
//
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
// URL
const separator = baseUrl.includes('?') ? '&' : '?'
// URL
return `${baseUrl}${separator}${queryString}`
}
type DebounceOptions = {
leading?: boolean //
trailing?: boolean //
}
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number, options: DebounceOptions = {}): T {
let timeoutId: ReturnType<typeof setTimeout> | null = null
let lastArgs: any[] | undefined
let lastThis: any
let result: ReturnType<T> | undefined
const leading = isDef(options.leading) ? options.leading : false
const trailing = isDef(options.trailing) ? options.trailing : true
function invokeFunc() {
if (lastArgs !== undefined) {
result = func.apply(lastThis, lastArgs)
lastArgs = undefined
}
}
function startTimer() {
timeoutId = setTimeout(() => {
timeoutId = null
if (trailing) {
invokeFunc()
}
}, wait)
}
function cancelTimer() {
if (timeoutId !== null) {
clearTimeout(timeoutId)
timeoutId = null
}
}
function debounced(this: any, ...args: Parameters<T>): ReturnType<T> | undefined {
lastArgs = args
lastThis = this
if (timeoutId === null) {
if (leading) {
invokeFunc()
}
startTimer()
} else if (trailing) {
cancelTimer()
startTimer()
}
return result
}
return debounced as T
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function throttle(func: Function, wait: number): Function {
let timeout: ReturnType<typeof setTimeout> | null = null
let previous: number = 0
const throttled = function (this: any, ...args: any[]) {
const now = Date.now()
const remaining = wait - (now - previous)
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(this, args)
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now()
timeout = null
func.apply(this, args)
}, remaining)
}
}
return throttled
}
/**
* 根据属性路径获取对象中的属性值
* @param obj 目标对象
* @param path 属性路径可以是字符串或字符串数组
* @returns 属性值如果属性不存在或中间的属性为 null undefined则返回 undefined
*/
export const getPropByPath = (obj: any, path: string): any => {
const keys: string[] = path.split('.')
try {
return keys.reduce((acc: any, key: string) => (acc !== undefined && acc !== null ? acc[key] : undefined), obj)
} catch (error) {
return undefined
}
}
/**
* 检查一个值是否为Date类型
* @param val 要检查的值
* @returns 如果值是Date类型则返回true否则返回false
*/
export const isDate = (val: unknown): val is Date => Object.prototype.toString.call(val) === '[object Date]' && !Number.isNaN((val as Date).getTime())
/**
* 检查提供的URL是否为视频链接
* @param url 需要检查的URL字符串
* @returns 返回一个布尔值如果URL是视频链接则为true否则为false
*/
export function isVideoUrl(url: string): boolean {
// 使URL
const videoRegex = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|video)/i
return videoRegex.test(url)
}
/**
* 检查提供的URL是否为图片URL
* @param url 待检查的URL字符串
* @returns 返回一个布尔值如果URL是图片格式则为true否则为false
*/
export function isImageUrl(url: string): boolean {
// 使URL
const imageRegex = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg|image)/i
return imageRegex.test(url)
}
/**
* 判断环境是否是H5
*/
export const isH5 = (() => {
let isH5 = false
// #ifdef H5
isH5 = true
// #endif
return isH5
})()
/**
* 剔除对象中的某些属性
* @param obj
* @param predicate
* @returns
*/
export function omitBy<O extends Record<string, any>>(obj: O, predicate: (value: any, key: keyof O) => boolean): Partial<O> {
const newObj = deepClone(obj)
Object.keys(newObj).forEach((key) => predicate(newObj[key], key) && delete newObj[key]) // predicate
return newObj
}
/**
* 缓动函数用于在动画或过渡效果中根据时间参数计算当前值
* @param t 当前时间通常是从动画开始经过的时间
* @param b 初始值动画属性的初始值
* @param c 变化量动画属性的目标值与初始值的差值
* @param d 持续时间动画持续的总时间长度
* @returns 计算出的当前值
*/
export function easingFn(t: number = 0, b: number = 0, c: number = 0, d: number = 0): number {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
}
/**
* 从数组中寻找最接近目标值的元素
*
* @param arr 数组
* @param target 目标值
* @returns 最接近目标值的元素
*/
export function closest(arr: number[], target: number) {
return arr.reduce((prev, curr) => (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev))
}

View File

@ -0,0 +1,13 @@
import { computed } from 'vue'
import { useParent } from './useParent'
import { CELL_GROUP_KEY } from '../wd-cell-group/types'
export function useCell() {
const { parent: cellGroup, index } = useParent(CELL_GROUP_KEY)
const border = computed(() => {
return cellGroup && cellGroup.props.border && index.value
})
return { border }
}

View File

@ -0,0 +1,113 @@
import {
provide,
reactive,
getCurrentInstance,
type VNode,
type InjectionKey,
type VNodeNormalizedChildren,
type ComponentPublicInstance,
type ComponentInternalInstance
} from 'vue'
// vueisVNodeuni-mp-vue
function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}
export function flattenVNodes(children: VNodeNormalizedChildren) {
const result: VNode[] = []
const traverse = (children: VNodeNormalizedChildren) => {
if (Array.isArray(children)) {
children.forEach((child) => {
if (isVNode(child)) {
result.push(child)
if (child.component?.subTree) {
result.push(child.component.subTree)
traverse(child.component.subTree.children)
}
if (child.children) {
traverse(child.children)
}
}
})
}
}
traverse(children)
return result
}
const findVNodeIndex = (vnodes: VNode[], vnode: VNode) => {
const index = vnodes.indexOf(vnode)
if (index === -1) {
return vnodes.findIndex((item) => vnode.key !== undefined && vnode.key !== null && item.type === vnode.type && item.key === vnode.key)
}
return index
}
// sort children instances by vnodes order
export function sortChildren(
parent: ComponentInternalInstance,
publicChildren: ComponentPublicInstance[],
internalChildren: ComponentInternalInstance[]
) {
const vnodes = parent && parent.subTree && parent.subTree.children ? flattenVNodes(parent.subTree.children) : []
internalChildren.sort((a, b) => findVNodeIndex(vnodes, a.vnode) - findVNodeIndex(vnodes, b.vnode))
const orderedPublicChildren = internalChildren.map((item) => item.proxy!)
publicChildren.sort((a, b) => {
const indexA = orderedPublicChildren.indexOf(a)
const indexB = orderedPublicChildren.indexOf(b)
return indexA - indexB
})
}
export function useChildren<
// eslint-disable-next-line
Child extends ComponentPublicInstance = ComponentPublicInstance<{}, any>,
ProvideValue = never
>(key: InjectionKey<ProvideValue>) {
const publicChildren: Child[] = reactive([])
const internalChildren: ComponentInternalInstance[] = reactive([])
const parent = getCurrentInstance()!
const linkChildren = (value?: ProvideValue) => {
const link = (child: ComponentInternalInstance) => {
if (child.proxy) {
internalChildren.push(child)
publicChildren.push(child.proxy as Child)
sortChildren(parent, publicChildren, internalChildren)
}
}
const unlink = (child: ComponentInternalInstance) => {
const index = internalChildren.indexOf(child)
publicChildren.splice(index, 1)
internalChildren.splice(index, 1)
}
provide(
key,
Object.assign(
{
link,
unlink,
children: publicChildren,
internalChildren
},
value
)
)
}
return {
children: publicChildren,
linkChildren
}
}

View File

@ -0,0 +1,138 @@
import { ref, computed, onBeforeUnmount } from 'vue'
import { isDef } from '../common/util'
import { useRaf } from './useRaf'
//
export type CurrentTime = {
days: number
hours: number
total: number
minutes: number
seconds: number
milliseconds: number
}
//
export type UseCountDownOptions = {
time: number //
millisecond?: boolean // false
onChange?: (current: CurrentTime) => void //
onFinish?: () => void //
}
//
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
//
function parseTime(time: number): CurrentTime {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
total: time,
days,
hours,
minutes,
seconds,
milliseconds
}
}
//
function isSameSecond(time1: number, time2: number): boolean {
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
}
// useCountDown
export function useCountDown(options: UseCountDownOptions) {
let endTime: number //
let counting: boolean //
const { start: startRaf, cancel: cancelRaf } = useRaf(tick)
const remain = ref(options.time) //
const current = computed(() => parseTime(remain.value)) //
//
const pause = () => {
counting = false
cancelRaf()
}
//
const getCurrentRemain = () => Math.max(endTime - Date.now(), 0)
//
const setRemain = (value: number) => {
remain.value = value
isDef(options.onChange) && options.onChange(current.value)
if (value === 0) {
pause()
isDef(options.onFinish) && options.onFinish()
}
}
//
const microTick = () => {
if (counting) {
setRemain(getCurrentRemain())
if (remain.value > 0) {
startRaf()
}
}
}
//
const macroTick = () => {
if (counting) {
const remainRemain = getCurrentRemain()
if (!isSameSecond(remainRemain, remain.value) || remainRemain === 0) {
setRemain(remainRemain)
}
if (remain.value > 0) {
startRaf()
}
}
}
//
function tick() {
if (options.millisecond) {
microTick()
} else {
macroTick()
}
}
//
const start = () => {
if (!counting) {
endTime = Date.now() + remain.value
counting = true
startRaf()
}
}
//
const reset = (totalTime: number = options.time) => {
pause()
remain.value = totalTime
}
//
onBeforeUnmount(pause)
return {
start,
pause,
reset,
current
}
}

View File

@ -0,0 +1,39 @@
import { onBeforeUnmount, onDeactivated, ref, watch } from 'vue'
function useLockScroll(shouldLock: () => boolean) {
const scrollLockCount = ref(0)
const lock = () => {
if (scrollLockCount.value === 0) {
document.getElementsByTagName('body')[0].style.overflow = 'hidden'
}
scrollLockCount.value++
}
const unlock = () => {
if (scrollLockCount.value > 0) {
scrollLockCount.value--
if (scrollLockCount.value === 0) {
document.getElementsByTagName('body')[0].style.overflow = ''
}
}
}
const destroy = () => {
shouldLock() && unlock()
}
watch(shouldLock, (value) => {
value ? lock() : unlock()
})
onDeactivated(destroy)
onBeforeUnmount(destroy)
return {
lock,
unlock
}
}
export default useLockScroll

View File

@ -0,0 +1,41 @@
import {
ref,
inject,
computed,
onUnmounted,
type InjectionKey,
getCurrentInstance,
type ComponentPublicInstance,
type ComponentInternalInstance
} from 'vue'
type ParentProvide<T> = T & {
link(child: ComponentInternalInstance): void
unlink(child: ComponentInternalInstance): void
children: ComponentPublicInstance[]
internalChildren: ComponentInternalInstance[]
}
export function useParent<T>(key: InjectionKey<ParentProvide<T>>) {
const parent = inject(key, null)
if (parent) {
const instance = getCurrentInstance()!
const { link, unlink, internalChildren } = parent
link(instance)
onUnmounted(() => unlink(instance))
const index = computed(() => internalChildren.indexOf(instance))
return {
parent,
index
}
}
return {
parent: null,
index: ref(-1)
}
}

View File

@ -0,0 +1,176 @@
import { getCurrentInstance, ref } from 'vue'
import { getRect, isObj } from '../common/util'
export function usePopover(visibleArrow = true) {
const { proxy } = getCurrentInstance() as any
const popStyle = ref<string>('')
const arrowStyle = ref<string>('')
const showStyle = ref<string>('')
const arrowClass = ref<string>('')
const popWidth = ref<number>(0)
const popHeight = ref<number>(0)
const left = ref<number>(0)
const bottom = ref<number>(0)
const width = ref<number>(0)
const height = ref<number>(0)
const top = ref<number>(0)
function noop() {}
function init(
placement:
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end',
visibleArrow: boolean,
selector: string
) {
// class
if (visibleArrow) {
const arrowClassArr = [
`wd-${selector}__arrow`,
placement === 'bottom' || placement === 'bottom-start' || placement === 'bottom-end' ? `wd-${selector}__arrow-up` : '',
placement === 'left' || placement === 'left-start' || placement === 'left-end' ? `wd-${selector}__arrow-right` : '',
placement === 'right' || placement === 'right-start' || placement === 'right-end' ? `wd-${selector}__arrow-left` : '',
placement === 'top' || placement === 'top-start' || placement === 'top-end' ? `wd-${selector}__arrow-down` : ''
]
arrowClass.value = arrowClassArr.join(' ')
}
//
getRect('#target', false, proxy).then((rect) => {
if (!rect) return
left.value = rect.left as number
bottom.value = rect.bottom as number
width.value = rect.width as number
height.value = rect.height as number
top.value = rect.top as number
})
// pop
getRect('#pos', false, proxy).then((rect) => {
if (!rect) return
popWidth.value = rect.width as number
popHeight.value = rect.height as number
})
}
function control(
placement:
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end',
offset: number | number[] | Record<'x' | 'y', number>
) {
// arrow size
const arrowSize = visibleArrow ? 9 : 0
//
const verticalX = width.value / 2
//
const verticalY = arrowSize + height.value + 5
//
const horizontalX = width.value + arrowSize + 5
//
const horizontalY = height.value / 2
let offsetX = 0
let offsetY = 0
if (Array.isArray(offset)) {
offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25) + offset[0]
offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25) + (offset[1] ? offset[1] : offset[0])
} else if (isObj(offset)) {
offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25) + offset.x
offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25) + offset.y
} else {
offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25) + offset
offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25) + offset
}
// const offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25) + offset
// const offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25) + offset
const placements = new Map([
//
['top', [`left: ${verticalX}px; bottom: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%;']],
[
'top-start',
[
`left: ${offsetX}px; bottom: ${verticalY}px;`,
`left: ${(popWidth.value >= width.value ? width.value / 2 : popWidth.value - 25) - offsetX}px;`
]
],
[
'top-end',
[
`right: ${offsetX}px; bottom: ${verticalY}px;`,
`right: ${(popWidth.value >= width.value ? width.value / 2 : popWidth.value - 25) - offsetX}px; transform: translateX(50%);`
]
],
//
['bottom', [`left: ${verticalX}px; top: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%;']],
[
'bottom-start',
[`left: ${offsetX}px; top: ${verticalY}px;`, `left: ${(popWidth.value >= width.value ? width.value / 2 : popWidth.value - 25) - offsetX}px;`]
],
[
'bottom-end',
[
`right: ${offsetX}px; top: ${verticalY}px;`,
`right: ${(popWidth.value >= width.value ? width.value / 2 : popWidth.value - 25) - offsetX}px; transform: translateX(50%);`
]
],
//
['left', [`right: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%']],
[
'left-start',
[
`right: ${horizontalX}px; top: ${offsetY}px;`,
`top: ${(popHeight.value >= height.value ? height.value / 2 : popHeight.value - 20) - offsetY}px;`
]
],
[
'left-end',
[
`right: ${horizontalX}px; bottom: ${offsetY}px;`,
`bottom: ${(popHeight.value >= height.value ? height.value / 2 : popHeight.value - 20) - offsetY}px; transform: translateY(50%);`
]
],
//
['right', [`left: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%']],
[
'right-start',
[
`left: ${horizontalX}px; top: ${offsetY}px;`,
`top: ${(popHeight.value >= height.value ? height.value / 2 : popHeight.value - 20) - offsetY}px;`
]
],
[
'right-end',
[
`left: ${horizontalX}px; bottom: ${offsetY}px;`,
`bottom: ${(popHeight.value >= height.value ? height.value / 2 : popHeight.value - 20) - offsetY}px; transform: translateY(50%);`
]
]
])
popStyle.value = placements.get(placement)![0]
arrowStyle.value = placements.get(placement)![1]
}
return { popStyle, arrowStyle, showStyle, arrowClass, init, control, noop }
}

View File

@ -0,0 +1,52 @@
import { type Ref, provide, ref } from 'vue'
export const queueKey = '__QUEUE_KEY__'
export interface Queue {
queue: Ref<any[]>
pushToQueue: (comp: any) => void
removeFromQueue: (comp: any) => void
closeOther: (comp: any) => void
closeOutside: () => void
}
export function useQueue() {
const queue = ref<any[]>([])
function pushToQueue(comp: any) {
queue.value.push(comp)
}
function removeFromQueue(comp: any) {
queue.value = queue.value.filter((item) => {
return item.$.uid !== comp.$.uid
})
}
function closeOther(comp: any) {
queue.value.forEach((item) => {
if (item.$.uid !== comp.$.uid) {
item.$.exposed.close()
}
})
}
function closeOutside() {
queue.value.forEach((item) => {
item.$.exposed.close()
})
}
provide(queueKey, {
queue,
pushToQueue,
removeFromQueue,
closeOther,
closeOutside
})
return {
closeOther,
closeOutside
}
}

View File

@ -0,0 +1,37 @@
import { ref, onUnmounted } from 'vue'
import { isDef, isH5, isNumber } from '../common/util'
//
type RafCallback = (time: number) => void
export function useRaf(callback: RafCallback) {
const requestRef = ref<number | null | ReturnType<typeof setTimeout>>(null)
//
const start = () => {
const handle = (time: number) => {
callback(time)
}
if (isH5) {
requestRef.value = requestAnimationFrame(handle)
} else {
requestRef.value = setTimeout(() => handle(Date.now()), 1000 / 30)
}
}
//
const cancel = () => {
if (isH5 && isNumber(requestRef.value)) {
cancelAnimationFrame(requestRef.value!)
} else if (isDef(requestRef.value)) {
clearTimeout(requestRef.value)
}
}
onUnmounted(() => {
cancel()
})
return { start, cancel }
}

View File

@ -0,0 +1,43 @@
import { ref } from 'vue'
export function useTouch() {
const direction = ref<string>('')
const deltaX = ref<number>(0)
const deltaY = ref<number>(0)
const offsetX = ref<number>(0)
const offsetY = ref<number>(0)
const startX = ref<number>(0)
const startY = ref<number>(0)
function touchStart(event: any) {
const touch = event.touches[0]
direction.value = ''
deltaX.value = 0
deltaY.value = 0
offsetX.value = 0
offsetY.value = 0
startX.value = touch.clientX
startY.value = touch.clientY
}
function touchMove(event: any) {
const touch = event.touches[0]
deltaX.value = touch.clientX - startX.value
deltaY.value = touch.clientY - startY.value
offsetX.value = Math.abs(deltaX.value)
offsetY.value = Math.abs(deltaY.value)
direction.value = offsetX.value > offsetY.value ? 'horizontal' : offsetX.value < offsetY.value ? 'vertical' : ''
}
return {
touchStart,
touchMove,
direction,
deltaX,
deltaY,
offsetX,
offsetY,
startX,
startY
}
}

View File

@ -0,0 +1,12 @@
import { camelCase, getPropByPath, isDef, isFunction } from '../common/util'
import Locale from '../../locale'
export const useTranslate = (name?: string) => {
const prefix = name ? camelCase(name) + '.' : ''
const translate = (key: string, ...args: unknown[]) => {
const currentMessages = Locale.messages()
const message = getPropByPath(currentMessages, prefix + key)
return isFunction(message) ? message(...args) : isDef(message) ? message : `${prefix}${key}`
}
return { translate }
}

View File

@ -0,0 +1,326 @@
import { isArray, isDef, isFunction } from '../common/util'
import type { ChooseFile, ChooseFileOption, UploadFileItem, UploadMethod, UploadStatusType } from '../wd-upload/types'
export const UPLOAD_STATUS: Record<string, UploadStatusType> = {
PENDING: 'pending',
LOADING: 'loading',
SUCCESS: 'success',
FAIL: 'fail'
}
export interface UseUploadReturn {
//
startUpload: (file: UploadFileItem, options: UseUploadOptions) => UniApp.UploadTask | void | Promise<void>
//
abort: (task?: UniApp.UploadTask) => void
//
UPLOAD_STATUS: Record<string, UploadStatusType>
//
chooseFile: (options: ChooseFileOption) => Promise<ChooseFile[]>
}
export interface UseUploadOptions {
//
action: string
//
header?: Record<string, any>
// key
name?: string
//
formData?: Record<string, any>
//
fileType?: 'image' | 'video' | 'audio'
//
statusCode?: number
// key
statusKey?: string
//
uploadMethod?: UploadMethod
//
onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult, file: UploadFileItem, formData: Record<string, any>) => void
//
onError?: (res: UniApp.GeneralCallbackResult, file: UploadFileItem, formData: Record<string, any>) => void
//
onProgress?: (res: UniApp.OnProgressUpdateResult, file: UploadFileItem) => void
//
abortPrevious?: boolean
// (H5,allfile,)
extension?: string[]
}
export function useUpload(): UseUploadReturn {
let currentTask: UniApp.UploadTask | null = null
//
const abort = (task?: UniApp.UploadTask) => {
if (task) {
task.abort()
} else if (currentTask) {
currentTask.abort()
currentTask = null
}
}
/**
* 默认上传方法
*/
const defaultUpload: UploadMethod = (file, formData, options) => {
// ,
if (options.abortPrevious) {
abort()
}
const uploadTask = uni.uploadFile({
url: options.action,
header: options.header,
name: options.name,
fileName: options.name,
fileType: options.fileType,
formData,
filePath: file.url,
success(res) {
if (res.statusCode === options.statusCode) {
//
options.onSuccess(res, file, formData)
} else {
//
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
}
},
fail(err) {
//
options.onError(err, file, formData)
}
})
currentTask = uploadTask
//
uploadTask.onProgressUpdate((res) => {
options.onProgress(res, file)
})
// ,
return uploadTask
}
/**
* 开始上传文件
*/
const startUpload = (file: UploadFileItem, options: UseUploadOptions) => {
const {
uploadMethod,
formData = {},
action,
name = 'file',
header = {},
fileType = 'image',
statusCode = 200,
statusKey = 'status',
abortPrevious = false
} = options
//
file[statusKey] = UPLOAD_STATUS.LOADING
const uploadOptions = {
action,
header,
name,
fileName: name,
fileType,
statusCode,
abortPrevious,
onSuccess: (res: UniApp.UploadFileSuccessCallbackResult, file: UploadFileItem, formData: Record<string, any>) => {
//
file[statusKey] = UPLOAD_STATUS.SUCCESS
currentTask = null
options.onSuccess?.(res, file, formData)
},
onError: (error: UniApp.GeneralCallbackResult, file: UploadFileItem, formData: Record<string, any>) => {
//
file[statusKey] = UPLOAD_STATUS.FAIL
file.error = error.errMsg
currentTask = null
options.onError?.(error, file, formData)
},
onProgress: (res: UniApp.OnProgressUpdateResult, file: UploadFileItem) => {
//
file.percent = res.progress
options.onProgress?.(res, file)
}
}
// ,uploadTask
if (isFunction(uploadMethod)) {
return uploadMethod(file, formData, uploadOptions)
} else {
return defaultUpload(file, formData, uploadOptions)
}
}
/**
* 格式化图片信息
*/
function formatImage(res: UniApp.ChooseImageSuccessCallbackResult): ChooseFile[] {
// #ifdef MP-DINGTALK
// files
res.tempFiles = isDef((res as any).files) ? (res as any).files : res.tempFiles
// #endif
if (isArray(res.tempFiles)) {
return res.tempFiles.map((item: any) => ({
path: item.path || '',
name: item.name || '',
size: item.size,
type: 'image',
thumb: item.path || ''
}))
}
return [
{
path: (res.tempFiles as any).path || '',
name: (res.tempFiles as any).name || '',
size: (res.tempFiles as any).size,
type: 'image',
thumb: (res.tempFiles as any).path || ''
}
]
}
/**
* 格式化视频信息
*/
function formatVideo(res: UniApp.ChooseVideoSuccess): ChooseFile[] {
return [
{
path: res.tempFilePath || (res as any).filePath || '',
name: res.name || '',
size: res.size,
type: 'video',
thumb: (res as any).thumbTempFilePath || '',
duration: res.duration
}
]
}
/**
* 格式化媒体信息
*/
function formatMedia(res: UniApp.ChooseMediaSuccessCallbackResult): ChooseFile[] {
return res.tempFiles.map((item) => ({
type: item.fileType,
path: item.tempFilePath,
thumb: item.fileType === 'video' ? item.thumbTempFilePath : item.tempFilePath,
size: item.size,
duration: item.duration
}))
}
/**
* 选择文件
*/
function chooseFile({
multiple,
sizeType,
sourceType,
maxCount,
accept,
compressed,
maxDuration,
camera,
extension
}: ChooseFileOption): Promise<ChooseFile[]> {
return new Promise((resolve, reject) => {
switch (accept) {
case 'image':
uni.chooseImage({
count: multiple ? Math.min(maxCount || 9, 9) : 1, // 9,9
sizeType,
sourceType,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatImage(res)),
fail: reject
})
break
case 'video':
uni.chooseVideo({
sourceType,
compressed,
maxDuration,
camera,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatVideo(res)),
fail: reject
})
break
// #ifdef MP-WEIXIN
case 'media':
uni.chooseMedia({
count: multiple ? Math.min(maxCount || 9, 9) : 1, // 9,9
sourceType,
sizeType,
camera,
maxDuration,
success: (res) => resolve(formatMedia(res)),
fail: reject
})
break
case 'file':
uni.chooseMessageFile({
count: multiple ? Math.min(maxCount || 100, 100) : 1, // 100,100
type: accept,
extension,
success: (res) => resolve(res.tempFiles),
fail: reject
})
break
// #endif
case 'all':
// #ifdef H5
uni.chooseFile({
count: multiple ? Math.min(maxCount || 100, 100) : 1, // 100,100
type: accept,
extension,
success: (res) => resolve(res.tempFiles as ChooseFile[]),
fail: reject
})
// #endif
// #ifdef MP-WEIXIN
uni.chooseMessageFile({
count: multiple ? Math.min(maxCount || 100, 100) : 1, // 100,100
type: accept,
extension,
success: (res) => resolve(res.tempFiles),
fail: reject
})
// #endif
break
default:
//
uni.chooseImage({
count: multiple ? Math.min(maxCount || 9, 9) : 1, // 9,9
sizeType,
sourceType,
// #ifdef H5
extension,
// #endif
success: (res) => resolve(formatImage(res)),
fail: reject
})
break
}
})
}
return {
startUpload,
abort,
UPLOAD_STATUS,
chooseFile
}
}

View File

@ -0,0 +1,204 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(action-sheet) {
background-color: $-dark-background2;
color: $-dark-color;
@include e(action) {
color: $-dark-color;
background: $-dark-background2;
&:not(.wd-action-sheet__action--disabled):not(.wd-action-sheet__action--loading):active {
background: $-dark-background4;
}
@include m(disabled) {
color: $-dark-color-gray;
}
}
@include e(subname) {
color: $-dark-color3;
}
@include e(cancel) {
color: $-dark-color;
background: $-dark-background4;
&:active {
background: $-dark-background5;
}
}
:deep(.wd-action-sheet__close) {
color: $-dark-color3;
}
@include e(panel-title) {
color: $-dark-color;
}
@include e(header) {
color: $-dark-color;
}
}
}
:deep(.wd-action-sheet__popup) {
border-radius: $-action-sheet-radius $-action-sheet-radius 0 0;
}
@include b(action-sheet) {
background-color: $-color-white;
padding-bottom: 1px;
@include edeep(popup) {
border-radius: $-action-sheet-radius $-action-sheet-radius 0 0;
}
@include e(actions) {
padding: 8px 0;
max-height: 50vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@include e(action) {
position: relative;
display: block;
width: 100%;
height: $-action-sheet-action-height;
line-height: $-action-sheet-action-height;
color: $-action-sheet-color;
font-size: $-action-sheet-fs;
text-align: center;
border: none;
background: $-action-sheet-bg;
outline: none;
&:after {
display: none;
}
&:not(&--disabled):not(&--loading):active {
background: $-action-sheet-active-color;
}
@include m(disabled) {
color: $-action-sheet-disabled-color;
cursor: not-allowed;
}
@include m(loading) {
display: flex;
align-items: center;
justify-content: center;
line-height: initial;
}
}
@include edeep(action-loading){
width: $-action-sheet-loading-size;
height: $-action-sheet-loading-size;
}
@include e(name) {
display: inline-block;
}
@include e(subname) {
display: inline-block;
margin-left: 4px;
font-size: $-action-sheet-subname-fs;
color: $-action-sheet-subname-color;
}
@include e(cancel) {
display: block;
width: calc(100% - 48px);
line-height: $-action-sheet-cancel-height;
padding: 0;
color: $-action-sheet-cancel-color;
font-size: $-action-sheet-fs;
text-align: center;
border-radius: $-action-sheet-cancel-radius;
border: none;
background: $-action-sheet-cancel-bg;
outline: none;
margin: 0 auto 24px;
font-weight: $-action-sheet-weight;
&:active {
background: $-action-sheet-active-color;
}
&:after {
display: none;
}
}
@include e(header) {
color: $-action-sheet-color;
position: relative;
height: $-action-sheet-title-height;
line-height: $-action-sheet-title-height;
text-align: center;
font-size: $-action-sheet-title-fs;
font-weight: $-action-sheet-weight;
}
@include edeep(close) {
position: absolute;
top: $-action-sheet-close-top;
right: $-action-sheet-close-right;
color: $-action-sheet-close-color;
font-size: $-action-sheet-close-fs;
transform: rotate(-45deg);
line-height: 1.1;
}
@include e(panels) {
height: 84px;
overflow-y: hidden;
&:first-of-type {
margin-top: 20px;
}
&:last-of-type {
margin-bottom: 12px;
}
}
@include e(panels-content) {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@include e(panel) {
width: 88px;
flex: 0 0 auto;
display: inline-block;
padding: $-action-sheet-panel-padding;
}
@include e(panel-img) {
display: block;
width: $-action-sheet-panel-img-fs;
height: $-action-sheet-panel-img-fs;
margin: 0 auto;
margin-bottom: 7px;
border-radius: $-action-sheet-panel-img-radius;
}
@include e(panel-title) {
font-size: $-action-sheet-subname-fs;
line-height: 1.2;
text-align: center;
color: $-action-sheet-color;
@include lineEllipsis;
}
}

View File

@ -0,0 +1,121 @@
/*
* @Author: weisheng
* @Date: 2024-03-18 11:22:03
* @LastEditTime: 2024-04-04 22:35:25
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-action-sheet/types.ts
* 记得注释
*/
import type { ExtractPropTypes } from 'vue'
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeRequiredProp, makeStringProp } from '../common/props'
export type Action = {
/**
* 选项名称
*/
name: string
/**
* 描述信息
*/
subname?: string
/**
* 颜色
*/
color?: string
/**
* 禁用
*/
disabled?: boolean
/**
* 加载中状态
*/
loading?: boolean
}
export type Panel = {
/**
* 图片地址
*/
iconUrl: string
/**
* 标题内容
*/
title: string
}
export const actionSheetProps = {
...baseProps,
/**
* header 头部样式
* @default ''
* @type {string}
*/
customHeaderClass: makeStringProp(''),
/**
* 设置菜单显示隐藏
* @default false
* @type {boolean}
*/
modelValue: { ...makeBooleanProp(false), ...makeRequiredProp(Boolean) },
/**
* 菜单选项
* @default []
* @type {Action[]}
*/
actions: makeArrayProp<Action>(),
/**
* 自定义面板项,可以为字符串数组也可以为对象数组如果为二维数组则为多行展示
* @default []
* @type {Array<Panel | Panel[]>}
*/
panels: makeArrayProp<Panel | Panel[]>(),
/**
* 标题
* @type {string}
*/
title: String,
/**
* 取消按钮文案
* @type {string}
*/
cancelText: String,
/**
* 点击选项后是否关闭菜单
* @default true
* @type {boolean}
*/
closeOnClickAction: makeBooleanProp(true),
/**
* 点击遮罩是否关闭
* @default true
* @type {boolean}
*/
closeOnClickModal: makeBooleanProp(true),
/**
* 弹框动画持续时间
* @default 200
* @type {number}
*/
duration: makeNumberProp(200),
/**
* 菜单层级
* @default 10
* @type {number}
*/
zIndex: makeNumberProp(10),
/**
* 弹层内容懒渲染触发展示时才渲染内容
* @default true
* @type {boolean}
*/
lazyRender: makeBooleanProp(true),
/**
* 弹出面板是否设置底部安全距离iphone X 类型的机型
* @default true
* @type {boolean}
*/
safeAreaInsetBottom: makeBooleanProp(true)
}
export type ActionSheetProps = ExtractPropTypes<typeof actionSheetProps>

View File

@ -0,0 +1,154 @@
<template>
<view>
<wd-popup
custom-class="wd-action-sheet__popup"
:custom-style="`${(actions && actions.length) || (panels && panels.length) ? 'background: transparent;' : ''}`"
v-model="showPopup"
:duration="duration"
position="bottom"
:close-on-click-modal="closeOnClickModal"
:safe-area-inset-bottom="safeAreaInsetBottom"
:lazy-render="lazyRender"
@enter="handleOpen"
@close="close"
@after-enter="handleOpened"
@after-leave="handleClosed"
@click-modal="handleClickModal"
:z-index="zIndex"
>
<view
:class="`wd-action-sheet ${customClass}`"
:style="`${
(actions && actions.length) || (panels && panels.length)
? 'margin: 0 10px calc(var(--window-bottom) + 10px) 10px; border-radius: 16px;'
: 'margin-bottom: var(--window-bottom);'
} ${customStyle}`"
>
<view v-if="title" :class="`wd-action-sheet__header ${customHeaderClass}`">
{{ title }}
<wd-icon custom-class="wd-action-sheet__close" name="add" @click="close" />
</view>
<view class="wd-action-sheet__actions" v-if="actions && actions.length">
<button
v-for="(action, rowIndex) in actions"
:key="rowIndex"
:class="`wd-action-sheet__action ${action.disabled ? 'wd-action-sheet__action--disabled' : ''} ${
action.loading ? 'wd-action-sheet__action--loading' : ''
}`"
:style="`color: ${action.color}`"
@click="select(rowIndex, 'action')"
>
<wd-loading custom-class="`wd-action-sheet__action-loading" v-if="action.loading" />
<view v-else class="wd-action-sheet__name">{{ action.name }}</view>
<view v-if="!action.loading && action.subname" class="wd-action-sheet__subname">{{ action.subname }}</view>
</button>
</view>
<view v-if="formatPanels && formatPanels.length">
<view v-for="(panel, rowIndex) in formatPanels" :key="rowIndex" class="wd-action-sheet__panels">
<view class="wd-action-sheet__panels-content">
<view v-for="(col, colIndex) in panel" :key="colIndex" class="wd-action-sheet__panel" @click="select(rowIndex, 'panels', colIndex)">
<image class="wd-action-sheet__panel-img" :src="(col as any).iconUrl" />
<view class="wd-action-sheet__panel-title">{{ (col as any).title }}</view>
</view>
</view>
</view>
</view>
<slot />
<button v-if="cancelText" class="wd-action-sheet__cancel" @click="handleCancel">{{ cancelText }}</button>
</view>
</wd-popup>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-action-sheet',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdPopup from '../wd-popup/wd-popup.vue'
import wdIcon from '../wd-icon/wd-icon.vue'
import wdLoading from '../wd-loading/wd-loading.vue'
import { watch, ref } from 'vue'
import { actionSheetProps, type Panel } from './types'
import { isArray } from '../common/util'
const props = defineProps(actionSheetProps)
const emit = defineEmits(['select', 'click-modal', 'cancel', 'closed', 'close', 'open', 'opened', 'update:modelValue'])
const formatPanels = ref<Array<Panel> | Array<Panel[]>>([])
const showPopup = ref<boolean>(false)
watch(() => props.panels, computedValue, { deep: true, immediate: true })
watch(
() => props.modelValue,
(newValue) => {
showPopup.value = newValue
},
{ deep: true, immediate: true }
)
function isPanelArray() {
return props.panels.length && !isArray(props.panels[0])
}
function computedValue() {
formatPanels.value = isPanelArray() ? [props.panels as Panel[]] : (props.panels as Panel[][])
}
function select(rowIndex: number, type: 'action' | 'panels', colIndex?: number) {
if (type === 'action') {
if (props.actions[rowIndex].disabled || props.actions[rowIndex].loading) {
return
}
emit('select', {
item: props.actions[rowIndex],
index: rowIndex
})
} else if (isPanelArray()) {
emit('select', {
item: props.panels[Number(colIndex)],
index: colIndex
})
} else {
emit('select', {
item: (props.panels as Panel[][])[rowIndex][Number(colIndex)],
rowIndex,
colIndex
})
}
if (props.closeOnClickAction) {
close()
}
}
function handleClickModal() {
emit('click-modal')
}
function handleCancel() {
emit('cancel')
close()
}
function close() {
emit('update:modelValue', false)
emit('close')
}
function handleOpen() {
emit('open')
}
function handleOpened() {
emit('opened')
}
function handleClosed() {
emit('closed')
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,25 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
@include b(backtop) {
position: fixed;
background-color: $-backtop-bg;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
color: $-color-gray-8;
@include edeep(backicon) {
font-size: $-backtop-icon-size;
}
@include when(circle) {
border-radius: 50%;
}
@include when(square) {
border-radius: 4px;
}
}

View File

@ -0,0 +1,37 @@
import { baseProps, makeNumberProp, makeRequiredProp, makeStringProp } from '../common/props'
export const backtopProps = {
...baseProps,
/**
* 页面滚动距离
*/
scrollTop: makeRequiredProp(Number),
/**
* 距离顶部多少距离时显示
*/
top: makeNumberProp(300),
/**
* 返回顶部滚动时间
*/
duration: makeNumberProp(100),
/**
* 层级
*/
zIndex: makeNumberProp(10),
/**
* icon样式
*/
iconStyle: makeStringProp(''),
/**
* 形状
*/
shape: makeStringProp('circle'),
/**
* 距离屏幕底部距离
*/
bottom: makeNumberProp(100),
/**
* 距离屏幕右边距离
*/
right: makeNumberProp(20)
}

View File

@ -0,0 +1,45 @@
<template>
<wd-transition :show="show" name="fade">
<view
:class="`wd-backtop ${customClass} is-${shape}`"
:style="`z-index: ${zIndex}; bottom: ${bottom}px; right: ${right}px; ${customStyle}`"
@click="handleBacktop"
>
<slot v-if="$slots.default"></slot>
<wd-icon v-else custom-class="wd-backtop__backicon" name="backtop" :custom-style="iconStyle" />
</view>
</wd-transition>
</template>
<script lang="ts">
export default {
name: 'wd-backtop',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdTransition from '../wd-transition/wd-transition.vue'
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed } from 'vue'
import { backtopProps } from './types'
const props = defineProps(backtopProps)
const show = computed(() => props.scrollTop > props.top)
function handleBacktop() {
uni.pageScrollTo({
scrollTop: 0,
duration: props.duration
})
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,63 @@
@import './../common/abstracts/_mixin.scss';
@import './../common/abstracts/variable.scss';
.wot-theme-dark {
@include b(badge) {
@include e(content) {
border-color: $-dark-background2;
}
}
}
@include b(badge) {
position: relative;
vertical-align: middle;
display: inline-block;
@include e(content) {
display: inline-block;
box-sizing: content-box;
height: $-badge-height;
line-height: $-badge-height;
padding: $-badge-padding;
background-color: $-badge-bg;
border-radius: calc($-badge-height / 2 + 2px);
color: $-badge-color;
font-size: $-badge-fs;
text-align: center;
white-space: nowrap;
border: $-badge-border;
font-weight: 500;
@include when(fixed) {
position: absolute;
top: 0px;
right: 0px;
transform: translateY(-50%) translateX(50%);
}
@include when(dot) {
height: $-badge-dot-size;
width: $-badge-dot-size;
padding: 0;
border-radius: 50%;
}
@each $type in (primary, success, warning, info, danger) {
@include m($type) {
@if $type == primary {
background-color: $-badge-primary;
} @else if $type == success {
background-color: $-badge-success;
} @else if $type == warning {
background-color: $-badge-warning;
} @else if $type == info {
background-color: $-badge-info;
} @else {
background-color: $-badge-danger;
}
}
}
}
}

View File

@ -0,0 +1,50 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 11:36:12
* @LastEditTime: 2024-11-20 20:29:03
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-badge/types.ts
* 记得注释
*/
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeStringProp, numericProp } from '../common/props'
export type BadgeType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
export const badgeProps = {
...baseProps,
/**
* 显示值
*/
modelValue: numericProp,
/** 当数值为 0 时,是否展示徽标 */
showZero: makeBooleanProp(false),
bgColor: String,
/**
* 最大值超过最大值会显示 '{max}+'要求 value Number 类型
*/
max: Number,
/**
* 是否为红色点状标注
*/
isDot: Boolean,
/**
* 是否隐藏 badge
*/
hidden: Boolean,
/**
* badge类型可选值primary / success / warning / danger / info
*/
type: makeStringProp<BadgeType | undefined>(undefined),
/**
* 为正时角标向下偏移对应的像素
*/
top: numericProp,
/**
* 为正时角标向左偏移对应的像素
*/
right: numericProp
}
export type BadgeProps = ExtractPropTypes<typeof badgeProps>

View File

@ -0,0 +1,61 @@
<template>
<view :class="['wd-badge', customClass]" :style="customStyle">
<slot></slot>
<view
v-if="shouldShowBadge"
:class="['wd-badge__content', 'is-fixed', type ? 'wd-badge__content--' + type : '', isDot ? 'is-dot' : '']"
:style="contentStyle"
>
{{ content }}
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-badge',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, type CSSProperties } from 'vue'
import { badgeProps } from './types'
import { addUnit, isDef, isNumber, objToStyle } from '../common/util'
const props = defineProps(badgeProps)
const content = computed(() => {
const { modelValue, max, isDot } = props
if (isDot) return ''
let value = modelValue
if (value && max && isNumber(value) && !Number.isNaN(value) && !Number.isNaN(max)) {
value = max < value ? `${max}+` : value
}
return value
})
const contentStyle = computed(() => {
const style: CSSProperties = {}
if (isDef(props.bgColor)) {
style.backgroundColor = props.bgColor
}
if (isDef(props.top)) {
style.top = addUnit(props.top)
}
if (isDef(props.right)) {
style.right = addUnit(props.right)
}
return objToStyle(style)
})
//
const shouldShowBadge = computed(() => !props.hidden && (content.value || (content.value === 0 && props.showZero) || props.isDot))
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,336 @@
@import './../common/abstracts/_mixin.scss';
@import './../common/abstracts/variable.scss';
.wot-theme-dark {
@include b(button) {
@include when(info) {
background: $-dark-background4;
color: $-dark-color3;
}
@include when(plain) {
background: transparent;
@include when(info) {
color: $-dark-color;
&::after {
border-color: $-dark-background5;
}
}
}
@include when(text) {
@include when(disabled) {
color: $-dark-color-gray;
background: transparent;
}
}
@include when(icon) {
color: $-dark-color;
@include when(disabled) {
color: $-dark-color-gray;
background: transparent;
}
}
}
}
@include b(button) {
margin-left: initial;
margin-right: initial;
position: relative;
display: inline-block;
outline: none;
-webkit-appearance: none;
outline: none;
background: transparent;
box-sizing: border-box;
border: none;
border-radius: 0;
color: $-button-normal-color;
transition: opacity 0.2s;
user-select: none;
font-weight: normal;
&::before {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: $-color-black;
border: inherit;
border-color: $-color-black;
border-radius: inherit;
transform: translate(-50%, -50%);
opacity: 0;
content: ' ';
}
&::after {
border: none;
border-radius: 0;
}
@include e(content) {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
@include m(active) {
&:active::before {
opacity: 0.15;
}
}
@include when(disabled) {
opacity: $-button-disabled-opacity;
}
@include e(loading) {
margin-right: 5px;
animation: wd-rotate 0.8s linear infinite;
animation-duration: 2s;
}
@include e(loading-svg) {
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
}
@include when(loading) {}
@include when(primary) {
background: $-button-primary-bg-color;
color: $-button-primary-color;
}
@include when(success) {
background: $-button-success-bg-color;
color: $-button-success-color;
}
@include when(info) {
background: $-button-info-bg-color;
color: $-button-info-color;
}
@include when(warning) {
background: $-button-warning-bg-color;
color: $-button-warning-color;
}
@include when(error) {
background: $-button-error-bg-color;
color: $-button-error-color;
}
@include when(small) {
height: $-button-small-height;
padding: $-button-small-padding;
border-radius: $-button-small-radius;
font-size: $-button-small-fs;
font-weight: normal;
.wd-button__loading {
width: $-button-small-loading;
height: $-button-small-loading;
}
}
@include when(medium) {
height: $-button-medium-height;
padding: $-button-medium-padding;
border-radius: $-button-medium-radius;
font-size: $-button-medium-fs;
min-width: 120px;
@include when(round) {
@include when(icon) {
min-width: 0;
border-radius: 50%;
}
@include when(text) {
border-radius: 0;
min-width: 0;
}
}
.wd-button__loading {
width: $-button-medium-loading;
height: $-button-medium-loading;
}
}
@include when(large) {
height: $-button-large-height;
padding: $-button-large-padding;
border-radius: $-button-large-radius;
font-size: $-button-large-fs;
&::after {
border-radius: $-button-large-radius;
}
.wd-button__loading {
width: $-button-large-loading;
height: $-button-large-loading;
}
}
@include when(round) {
border-radius: 999px;
}
@include when(text) {
color: $-button-primary-bg-color;
min-width: 0;
padding: 4px 0;
&::after {
display: none;
}
&.wd-button--active {
opacity: $-button-text-hover-opacity;
&:active::before {
display: none;
}
}
@include when(disabled) {
color: $-button-normal-disabled-color;
background: transparent;
}
}
@include when(plain) {
background: $-button-plain-bg-color;
border: 1px solid currentColor;
@include when(primary) {
color: $-button-primary-bg-color;
}
@include when(success) {
color: $-button-success-bg-color;
}
@include when(info) {
color: $-button-info-plain-normal-color;
border-color: $-button-info-plain-border-color;
}
@include when(warning) {
color: $-button-warning-bg-color;
}
@include when(error) {
color: $-button-error-bg-color;
}
}
@include when(hairline) {
border-width: 0;
&.is-plain {
@include halfPixelBorderSurround();
&::before {
border-radius: inherit;
}
&::after {
border-color: inherit;
}
&.is-round {
&::after {
border-radius: inherit !important;
}
}
&.is-large {
&::after {
border-radius: calc(2 * $-button-large-radius);
}
}
&.is-medium {
&::after {
border-radius: calc(2 * $-button-medium-radius);
}
}
&.is-small {
&::after {
border-radius: calc(2 * $-button-small-radius);
}
}
}
}
@include when(block) {
display: block;
}
@include when(icon) {
width: $-button-icon-size;
height: $-button-icon-size;
padding: 0;
border-radius: 50%;
color: $-button-icon-color;
&::after {
display: none;
}
:deep(.wd-button__icon) {
margin-right: 0;
}
@include when(disabled) {
color: $-button-icon-disabled-color;
background: transparent;
}
}
@include edeep(icon) {
display: block;
margin-right: 6px;
font-size: $-button-icon-fs;
vertical-align: middle;
}
@include e(text) {
user-select: none;
white-space: nowrap;
}
}
@keyframes wd-rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,141 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 11:36:12
* @LastEditTime: 2024-11-04 21:33:52
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-button\types.ts
* 记得注释
*/
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeStringProp } from '../common/props'
export type ButtonType = 'primary' | 'success' | 'info' | 'warning' | 'error' | 'default' | 'text' | 'icon'
export type ButtonSize = 'small' | 'medium' | 'large'
export type ButtonLang = 'zh_CN' | 'zh_TW' | 'en'
export type ButtonOpenType =
| 'feedback'
| 'share'
| 'getUserInfo'
| 'contact'
| 'getPhoneNumber'
| 'launchApp'
| 'openSetting'
| 'chooseAvatar'
| 'getAuthorize'
| 'lifestyle'
| 'contactShare'
| 'openGroupProfile'
| 'openGuildProfile'
| 'openPublicProfile'
| 'shareMessageToFriend'
| 'addFriend'
| 'addColorSign'
| 'addGroupApp'
| 'addToFavorites'
| 'chooseAddress'
| 'chooseInvoiceTitle'
| 'login'
| 'subscribe'
| 'favorite'
| 'watchLater'
| 'openProfile'
| 'agreePrivacyAuthorization'
export type ButtonScope = 'phoneNumber' | 'userInfo'
export const buttonProps = {
...baseProps,
/**
* 幽灵按钮
*/
plain: makeBooleanProp(false),
/**
* 圆角按钮
*/
round: makeBooleanProp(true),
/**
* 禁用按钮
*/
disabled: makeBooleanProp(false),
/**
* 是否细边框
*/
hairline: makeBooleanProp(false),
/**
* 块状按钮
*/
block: makeBooleanProp(false),
/**
* 按钮类型可选值primary / success / info / warning / error / text / icon
*/
type: makeStringProp<ButtonType>('primary'),
/**
* 按钮尺寸可选值small / medium / large
*/
size: makeStringProp<ButtonSize>('medium'),
/**
* 图标类名
*/
icon: String,
/**
* 类名前缀用于使用自定义图标用法参考Icon组件
*/
classPrefix: makeStringProp('wd-icon'),
/**
* 加载中按钮
*/
loading: makeBooleanProp(false),
/**
* 加载图标颜色
*/
loadingColor: String,
/**
* 开放能力
*/
openType: String as PropType<ButtonOpenType>,
/**
* 指定是否阻止本节点的祖先节点出现点击态
*/
hoverStopPropagation: Boolean,
/**
* 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文
*/
lang: String as PropType<ButtonLang>,
/**
* 会话来源open-type="contact"时有效
*/
sessionFrom: String,
/**
* 会话内消息卡片标题open-type="contact"时有效
*/
sendMessageTitle: String,
/**
* 会话内消息卡片点击跳转小程序路径open-type="contact"时有效
*/
sendMessagePath: String,
/**
* 会话内消息卡片图片open-type="contact"时有效
*/
sendMessageImg: String,
/**
* 打开 APP APP 传递的参数open-type=launchApp时有效
*/
appParameter: String,
/**
* 是否显示会话内消息卡片设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息open-type="contact"时有效
*/
showMessageCard: Boolean,
/**
* 按钮的唯一标识可用于设置隐私同意授权按钮的id
*/
buttonId: String,
/**
* 支付宝小程序 open-type getAuthorize 时有效
* 可选值'phoneNumber' | 'userInfo'
*/
scope: String as PropType<ButtonScope>
}
export type ButtonProps = ExtractPropTypes<typeof buttonProps>

View File

@ -0,0 +1,189 @@
<template>
<button
:id="buttonId"
:hover-class="`${disabled || loading ? '' : 'wd-button--active'}`"
:style="customStyle"
:class="[
'wd-button',
'is-' + type,
'is-' + size,
round ? 'is-round' : '',
hairline ? 'is-hairline' : '',
plain ? 'is-plain' : '',
disabled ? 'is-disabled' : '',
block ? 'is-block' : '',
loading ? 'is-loading' : '',
customClass
]"
:hover-start-time="hoverStartTime"
:hover-stay-time="hoverStayTime"
:open-type="disabled || loading ? undefined : openType"
:send-message-title="sendMessageTitle"
:send-message-path="sendMessagePath"
:send-message-img="sendMessageImg"
:app-parameter="appParameter"
:show-message-card="showMessageCard"
:session-from="sessionFrom"
:lang="lang"
:hover-stop-propagation="hoverStopPropagation"
:scope="scope"
@click="handleClick"
@getAuthorize="handleGetAuthorize"
@getuserinfo="handleGetuserinfo"
@contact="handleConcat"
@getphonenumber="handleGetphonenumber"
@error="handleError"
@launchapp="handleLaunchapp"
@opensetting="handleOpensetting"
@chooseavatar="handleChooseavatar"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
<view class="wd-button__content">
<view v-if="loading" class="wd-button__loading">
<view class="wd-button__loading-svg" :style="loadingStyle"></view>
</view>
<wd-icon v-else-if="icon" custom-class="wd-button__icon" :name="icon" :classPrefix="classPrefix"></wd-icon>
<view class="wd-button__text"><slot /></view>
</view>
</button>
</template>
<script lang="ts">
export default {
name: 'wd-button',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed, watch } from 'vue'
import { ref } from 'vue'
import base64 from '../common/base64'
import { buttonProps } from './types'
const loadingIcon = (color = '#4D80F0', reverse = true) => {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42 42"><defs><linearGradient x1="100%" y1="0%" x2="0%" y2="0%" id="a"><stop stop-color="${
reverse ? color : '#fff'
}" offset="0%" stop-opacity="0"/><stop stop-color="${
reverse ? color : '#fff'
}" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path d="M21 1c11.046 0 20 8.954 20 20s-8.954 20-20 20S1 32.046 1 21 9.954 1 21 1zm0 7C13.82 8 8 13.82 8 21s5.82 13 13 13 13-5.82 13-13S28.18 8 21 8z" fill="${
reverse ? '#fff' : color
}"/><path d="M4.599 21c0 9.044 7.332 16.376 16.376 16.376 9.045 0 16.376-7.332 16.376-16.376" stroke="url(#a)" stroke-width="3.5" stroke-linecap="round"/></g></svg>`
}
const props = defineProps(buttonProps)
const emit = defineEmits([
'click',
'getuserinfo',
'contact',
'getphonenumber',
'error',
'launchapp',
'opensetting',
'chooseavatar',
'agreeprivacyauthorization'
])
const hoverStartTime = ref<number>(20)
const hoverStayTime = ref<number>(70)
const loadingIconSvg = ref<string>('')
const loadingStyle = computed(() => {
return `background-image: url(${loadingIconSvg.value});`
})
watch(
() => props.loading,
() => {
buildLoadingSvg()
},
{ deep: true, immediate: true }
)
function handleClick(event: any) {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
/**
* 支付宝小程序授权
* @param event
*/
function handleGetAuthorize(event: any) {
if (props.scope === 'phoneNumber') {
handleGetphonenumber(event)
} else if (props.scope === 'userInfo') {
handleGetuserinfo(event)
}
}
function handleGetuserinfo(event: any) {
emit('getuserinfo', event.detail)
}
function handleConcat(event: any) {
emit('contact', event.detail)
}
function handleGetphonenumber(event: any) {
emit('getphonenumber', event.detail)
}
function handleError(event: any) {
emit('error', event.detail)
}
function handleLaunchapp(event: any) {
emit('launchapp', event.detail)
}
function handleOpensetting(event: any) {
emit('opensetting', event.detail)
}
function handleChooseavatar(event: any) {
emit('chooseavatar', event.detail)
}
function handleAgreePrivacyAuthorization(event: any) {
emit('agreeprivacyauthorization', event.detail)
}
function buildLoadingSvg() {
const { loadingColor, type, plain } = props
let color = loadingColor
if (!color) {
switch (type) {
case 'primary':
color = '#4D80F0'
break
case 'success':
color = '#34d19d'
break
case 'info':
color = '#333'
break
case 'warning':
color = '#f0883a'
break
case 'error':
color = '#fa4350'
break
case 'default':
color = '#333'
break
}
}
const svg = loadingIcon(color, !plain)
loadingIconSvg.value = `"data:image/svg+xml;base64,${base64(svg)}"`
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,9 @@
/*
* @Author: weisheng
* @Date: 2023-06-12 10:04:19
* @LastEditTime: 2023-07-15 16:16:34
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-calendar-view\index.scss
* 记得注释
*/

View File

@ -0,0 +1,162 @@
@import '../../common/abstracts/variable';
@import '../../common/abstracts/mixin';
.wot-theme-dark {
@include b(month) {
@include e(title) {
color: $-dark-color;
}
@include e(days) {
color: $-dark-color;
}
@include e(day) {
@include when(disabled) {
.wd-month__day-text {
color: $-dark-color-gray;
}
}
}
}
}
@include b(month) {
@include e(title) {
display: flex;
align-items: center;
justify-content: center;
height: 45px;
font-size: $-calendar-panel-title-fs;
color: $-calendar-panel-title-color;
}
@include e(days) {
display: flex;
flex-wrap: wrap;
font-size: $-calendar-day-fs;
color: $-calendar-day-color;
}
@include e(day) {
position: relative;
width: 14.285%;
height: $-calendar-day-height;
line-height: $-calendar-day-height;
text-align: center;
margin-bottom: $-calendar-item-margin-bottom;
@include when(disabled) {
.wd-month__day-text {
color: $-calendar-disabled-color;
}
}
@include when(current) {
color: $-calendar-active-color;
}
@include when(selected, multiple-selected) {
.wd-month__day-container {
border-radius: $-calendar-active-border;
background: $-calendar-active-color;
color: $-calendar-selected-color;
}
}
@include when(middle) {
.wd-month__day-container {
background: $-calendar-range-color;
}
}
@include when(multiple-middle) {
.wd-month__day-container {
background: $-calendar-active-color;
color: $-calendar-selected-color;
}
}
@include when(start) {
&::after {
position: absolute;
content: '';
height: $-calendar-day-height;
top: 0;
right: 0;
left: 50%;
background: $-calendar-range-color;
z-index: 1;
}
&.is-without-end::after {
display: none;
}
.wd-month__day-container {
background: $-calendar-active-color;
color: $-calendar-selected-color;
border-radius: $-calendar-active-border 0 0 $-calendar-active-border;
}
}
@include when(end) {
&::after {
position: absolute;
content: '';
height: $-calendar-day-height;
top: 0;
left: 0;
right: 50%;
background: $-calendar-range-color;
z-index: 1;
}
.wd-month__day-container {
background: $-calendar-active-color;
color: $-calendar-selected-color;
border-radius: 0 $-calendar-active-border $-calendar-active-border 0;
}
}
@include when(same) {
.wd-month__day-container {
background: $-calendar-active-color;
color: $-calendar-selected-color;
border-radius: $-calendar-active-border;
}
}
@include when(last-row){
margin-bottom: 0;
}
}
@include e(day-container) {
position: relative;
z-index: 2;
}
@include e(day-text) {
font-weight: $-calendar-day-fw;
}
@include e(day-top) {
position: absolute;
top: 10px;
left: 0;
right: 0;
line-height: 1.1;
font-size: $-calendar-info-fs;
text-align: center;
}
@include e(day-bottom) {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
line-height: 1.1;
font-size: $-calendar-info-fs;
text-align: center;
}
}

View File

@ -0,0 +1,389 @@
<template>
<view>
<wd-toast selector="wd-month" />
<view class="month">
<view class="wd-month">
<view class="wd-month__title" v-if="showTitle">{{ monthTitle(date) }}</view>
<view class="wd-month__days">
<view
v-for="(item, index) in days"
:key="index"
:class="`wd-month__day ${item.disabled ? 'is-disabled' : ''} ${item.isLastRow ? 'is-last-row' : ''} ${
item.type ? dayTypeClass(item.type) : ''
}`"
:style="index === 0 ? firstDayStyle : ''"
@click="handleDateClick(index)"
>
<view class="wd-month__day-container">
<view class="wd-month__day-top">{{ item.topInfo }}</view>
<view class="wd-month__day-text">
{{ item.text }}
</view>
<view class="wd-month__day-bottom">{{ item.bottomInfo }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdToast from '../../wd-toast/wd-toast.vue'
import { computed, ref, watch, type CSSProperties } from 'vue'
import {
compareDate,
formatMonthTitle,
getDateByDefaultTime,
getDayByOffset,
getDayOffset,
getItemClass,
getMonthEndDay,
getNextDay,
getPrevDay,
getWeekRange
} from '../utils'
import { useToast } from '../../wd-toast'
import { deepClone, isArray, isFunction, objToStyle } from '../../common/util'
import { useTranslate } from '../../composables/useTranslate'
import type { CalendarDayItem, CalendarDayType } from '../types'
import { monthProps } from './types'
const props = defineProps(monthProps)
const emit = defineEmits(['change'])
const { translate } = useTranslate('calendar-view')
const days = ref<Array<CalendarDayItem>>([])
const toast = useToast('wd-month')
const offset = computed(() => {
const firstDayOfWeek = props.firstDayOfWeek >= 7 ? props.firstDayOfWeek % 7 : props.firstDayOfWeek
const offset = (7 + new Date(props.date).getDay() - firstDayOfWeek) % 7
return offset
})
const dayTypeClass = computed(() => {
return (monthType: CalendarDayType) => {
return getItemClass(monthType, props.value, props.type)
}
})
const monthTitle = computed(() => {
return (date: number) => {
return formatMonthTitle(date)
}
})
const firstDayStyle = computed(() => {
const dayStyle: CSSProperties = {}
dayStyle.marginLeft = `${(100 / 7) * offset.value}%`
return objToStyle(dayStyle)
})
const isLastRow = (date: number) => {
const currentDate = new Date(date)
const currentDay = currentDate.getDate()
const daysInMonth = getMonthEndDay(currentDate.getFullYear(), currentDate.getMonth() + 1)
const totalDaysShown = offset.value + daysInMonth
const totalRows = Math.ceil(totalDaysShown / 7)
return Math.ceil((offset.value + currentDay) / 7) === totalRows
}
watch(
[() => props.type, () => props.date, () => props.value, () => props.minDate, () => props.maxDate, () => props.formatter],
() => {
setDays()
},
{
deep: true,
immediate: true
}
)
function setDays() {
const dayList: Array<CalendarDayItem> = []
const date = new Date(props.date)
const year = date.getFullYear()
const month = date.getMonth()
const totalDay = getMonthEndDay(year, month + 1)
let value = props.value
if ((props.type === 'week' || props.type === 'weekrange') && value) {
value = getWeekValue()
}
for (let day = 1; day <= totalDay; day++) {
const date = new Date(year, month, day).getTime()
let type: CalendarDayType = getDayType(date, value as number | number[] | null)
if (!type && compareDate(date, Date.now()) === 0) {
type = 'current'
}
const dayObj = getFormatterDate(date, day, type)
dayList.push(dayObj)
}
days.value = dayList
}
function getDayType(date: number, value: number | number[] | null): CalendarDayType {
switch (props.type) {
case 'date':
case 'datetime':
return getDateType(date)
case 'dates':
return getDatesType(date)
case 'daterange':
case 'datetimerange':
return getDatetimeType(date, value)
case 'week':
return getWeektimeType(date, value)
case 'weekrange':
return getWeektimeType(date, value)
default:
return getDateType(date)
}
}
function getDateType(date: number): CalendarDayType {
if (props.value && compareDate(date, props.value as number) === 0) {
return 'selected'
}
return ''
}
function getDatesType(date: number): CalendarDayType {
const { value } = props
let type: CalendarDayType = ''
if (!isArray(value)) return type
const isSelected = (day: number) => {
return value.some((item) => compareDate(day, item) === 0)
}
if (isSelected(date)) {
const prevDay = getPrevDay(date)
const nextDay = getNextDay(date)
const prevSelected = isSelected(prevDay)
const nextSelected = isSelected(nextDay)
if (prevSelected && nextSelected) {
type = 'multiple-middle'
} else if (prevSelected) {
type = 'end'
} else if (nextSelected) {
type = 'start'
} else {
type = 'multiple-selected'
}
}
return type
}
function getDatetimeType(date: number, value: number | number[] | null) {
const [startDate, endDate] = isArray(value) ? value : []
if (startDate && compareDate(date, startDate) === 0) {
if (props.allowSameDay && endDate && compareDate(startDate, endDate) === 0) {
return 'same'
}
return 'start'
} else if (endDate && compareDate(date, endDate) === 0) {
return 'end'
} else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
return 'middle'
} else {
return ''
}
}
function getWeektimeType(date: number, value: number | number[] | null) {
const [startDate, endDate] = isArray(value) ? value : []
if (startDate && compareDate(date, startDate) === 0) {
return 'start'
} else if (endDate && compareDate(date, endDate) === 0) {
return 'end'
} else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
return 'middle'
} else {
return ''
}
}
function getWeekValue() {
if (props.type === 'week') {
return getWeekRange(props.value as number, props.firstDayOfWeek)
} else {
const [startDate, endDate] = (props.value as any) || []
if (startDate) {
const firstWeekRange = getWeekRange(startDate, props.firstDayOfWeek)
if (endDate) {
const endWeekRange = getWeekRange(endDate, props.firstDayOfWeek)
return [firstWeekRange[0], endWeekRange[1]]
} else {
return firstWeekRange
}
}
return []
}
}
function handleDateClick(index: number) {
const date = days.value[index]
switch (props.type) {
case 'date':
case 'datetime':
handleDateChange(date)
break
case 'dates':
handleDatesChange(date)
break
case 'daterange':
case 'datetimerange':
handleDateRangeChange(date)
break
case 'week':
handleWeekChange(date)
break
case 'weekrange':
handleWeekRangeChange(date)
break
default:
handleDateChange(date)
}
}
function getDate(date: number, isEnd: boolean = false) {
date = props.defaultTime && props.defaultTime.length > 0 ? getDateByDefaultTime(date, isEnd ? props.defaultTime[1] : props.defaultTime[0]) : date
if (date < props.minDate) return props.minDate
if (date > props.maxDate) return props.maxDate
return date
}
function handleDateChange(date: CalendarDayItem) {
if (date.disabled) return
if (date.type !== 'selected') {
emit('change', {
value: getDate(date.date),
type: 'start'
})
}
}
function handleDatesChange(date: CalendarDayItem) {
if (date.disabled) return
const currentValue = deepClone(isArray(props.value) ? props.value : [])
const dateIndex = currentValue.findIndex((item) => item && compareDate(item, date.date) === 0)
const value = dateIndex === -1 ? [...currentValue, getDate(date.date)] : currentValue.filter((_, index) => index !== dateIndex)
emit('change', { value })
}
function handleDateRangeChange(date: CalendarDayItem) {
if (date.disabled) return
let value: (number | null)[] = []
let type: CalendarDayType = ''
const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
const compare = compareDate(date.date, startDate)
//
if (!props.allowSameDay && compare === 0 && (props.type === 'daterange' || props.type === 'datetimerange') && !endDate) {
return
}
if (startDate && !endDate && compare > -1) {
//
if (props.maxRange && getDayOffset(date.date, startDate) > props.maxRange) {
const maxEndDate = getDayByOffset(startDate, props.maxRange - 1)
value = [startDate, getDate(maxEndDate, true)]
toast.show({
msg: props.rangePrompt || translate('rangePrompt', props.maxRange)
})
} else {
value = [startDate, getDate(date.date, true)]
}
} else if (props.type === 'datetimerange' && startDate && endDate) {
//
if (compare === 0) {
type = 'start'
value = props.value as number[]
} else if (compareDate(date.date, endDate) === 0) {
type = 'end'
value = props.value as number[]
} else {
value = [getDate(date.date), null]
}
} else {
value = [getDate(date.date), null]
}
emit('change', {
value,
type: type || (value[1] ? 'end' : 'start')
})
}
function handleWeekChange(date: CalendarDayItem) {
const [weekStart] = getWeekRange(date.date, props.firstDayOfWeek)
//
if (getFormatterDate(weekStart, new Date(weekStart).getDate()).disabled) return
emit('change', {
value: getDate(weekStart) + 24 * 60 * 60 * 1000
})
}
function handleWeekRangeChange(date: CalendarDayItem) {
const [weekStartDate] = getWeekRange(date.date, props.firstDayOfWeek)
//
if (getFormatterDate(weekStartDate, new Date(weekStartDate).getDate()).disabled) return
let value: (number | null)[] = []
const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
const [startWeekStartDate] = startDate ? getWeekRange(startDate, props.firstDayOfWeek) : []
const compare = compareDate(weekStartDate, startWeekStartDate)
if (startDate && !endDate && compare > -1) {
if (!props.allowSameDay && compare === 0) return
value = [getDate(startWeekStartDate) + 24 * 60 * 60 * 1000, getDate(weekStartDate) + 24 * 60 * 60 * 1000]
} else {
value = [getDate(weekStartDate) + 24 * 60 * 60 * 1000, null]
}
emit('change', {
value
})
}
function getFormatterDate(date: number, day: string | number, type?: CalendarDayType) {
let dayObj: CalendarDayItem = {
date: date,
text: day,
topInfo: '',
bottomInfo: '',
type,
disabled: compareDate(date, props.minDate) === -1 || compareDate(date, props.maxDate) === 1,
isLastRow: isLastRow(date)
}
if (props.formatter) {
if (isFunction(props.formatter)) {
dayObj = props.formatter(dayObj)
} else {
console.error('[wot-design] error(wd-calendar-view): the formatter prop of wd-calendar-view should be a function')
}
}
return dayObj
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,20 @@
import type { PropType } from 'vue'
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
import type { CalendarFormatter, CalendarType } from '../types'
export const monthProps = {
type: makeRequiredProp(String as PropType<CalendarType>),
date: makeRequiredProp(Number),
value: makeRequiredProp([Number, Array, null] as PropType<number | (number | null)[] | null>),
minDate: makeRequiredProp(Number),
maxDate: makeRequiredProp(Number),
firstDayOfWeek: makeRequiredProp(Number),
formatter: Function as PropType<CalendarFormatter>,
maxRange: Number,
rangePrompt: String,
allowSameDay: makeBooleanProp(false),
defaultTime: {
type: [Array] as PropType<Array<number[]>>
},
showTitle: makeBooleanProp(true)
}

View File

@ -0,0 +1,89 @@
@import '../../common/abstracts/variable';
@import '../../common/abstracts/mixin';
.wot-theme-dark {
@include b(month-panel) {
@include e(title) {
color: $-dark-color;
}
@include e(weeks) {
box-shadow: 0px 4px 8px 0 rgba(255, 255, 255, 0.02);
color: $-dark-color;
}
@include e(time-label) {
color: $-dark-color;
&::after{
background: $-dark-background4;
}
}
}
}
@include b(month-panel) {
font-size: $-calendar-fs;
@include e(title) {
padding: 5px 0;
text-align: center;
font-size: $-calendar-panel-title-fs;
color: $-calendar-panel-title-color;
padding: $-calendar-panel-padding;
}
@include e(weeks) {
display: flex;
height: $-calendar-week-height;
line-height: $-calendar-week-height;
box-shadow: 0px 4px 8px 0 rgba(0, 0, 0, 0.02);
color: $-calendar-week-color;
font-size: $-calendar-week-fs;
padding: $-calendar-panel-padding;
}
@include e(week) {
flex: 1;
text-align: center;
}
@include e(container) {
padding: $-calendar-panel-padding;
box-sizing: border-box;
}
@include e(time) {
display: flex;
box-shadow: 0px -4px 8px 0px rgba(0, 0, 0, 0.02);
}
@include e(time-label) {
position: relative;
flex: 1;
font-size: $-picker-column-fs;
text-align: center;
line-height: 125px;
color: $-picker-column-color;
&::after {
position: absolute;
content: '';
height: 35px;
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
background: $-picker-column-select-bg;
z-index: 0;
}
}
@include e(time-text) {
position: relative;
z-index: 1;
}
@include e(time-picker) {
flex: 3;
}
}

View File

@ -0,0 +1,374 @@
<template>
<view class="wd-month-panel">
<view v-if="showPanelTitle" class="wd-month-panel__title">
{{ title }}
</view>
<view class="wd-month-panel__weeks">
<view v-for="item in 7" :key="item" class="wd-month-panel__week">{{ weekLabel(item + firstDayOfWeek) }}</view>
</view>
<scroll-view
:class="`wd-month-panel__container ${!!timeType ? 'wd-month-panel__container--time' : ''}`"
:style="`height: ${scrollHeight}px`"
scroll-y
@scroll="monthScroll"
:scroll-top="scrollTop"
>
<view v-for="(item, index) in months" :key="index" :id="`month${index}`">
<month
:type="type"
:date="item.date"
:value="value"
:min-date="minDate"
:max-date="maxDate"
:first-day-of-week="firstDayOfWeek"
:formatter="formatter"
:max-range="maxRange"
:range-prompt="rangePrompt"
:allow-same-day="allowSameDay"
:default-time="defaultTime"
:showTitle="index !== 0"
@change="handleDateChange"
/>
</view>
</scroll-view>
<view v-if="timeType" class="wd-month-panel__time">
<view v-if="type === 'datetimerange'" class="wd-month-panel__time-label">
<view class="wd-month-panel__time-text">{{ timeType === 'start' ? translate('startTime') : translate('endTime') }}</view>
</view>
<view class="wd-month-panel__time-picker">
<wd-picker-view
v-if="timeData.length"
v-model="timeValue"
:columns="timeData"
:columns-height="125"
:immediate-change="immediateChange"
@change="handleTimeChange"
@pickstart="handlePickStart"
@pickend="handlePickEnd"
/>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdPickerView from '../../wd-picker-view/wd-picker-view.vue'
import { computed, ref, watch, onMounted } from 'vue'
import { debounce, isArray, isEqual, isNumber, pause } from '../../common/util'
import { compareMonth, formatMonthTitle, getMonthEndDay, getMonths, getTimeData, getWeekLabel } from '../utils'
import Month from '../month/month.vue'
import { monthPanelProps, type MonthInfo, type MonthPanelTimeType, type MonthPanelExpose } from './types'
import { useTranslate } from '../../composables/useTranslate'
import type { CalendarItem } from '../types'
const props = defineProps(monthPanelProps)
const emit = defineEmits(['change', 'pickstart', 'pickend'])
const { translate } = useTranslate('calendar-view')
const scrollTop = ref<number>(0) //
const scrollIndex = ref<number>(0) //
const timeValue = ref<number[]>([]) //
const timeType = ref<MonthPanelTimeType>('') //
const innerValue = ref<string | number | (number | null)[]>('') //
const handleChange = debounce((value) => {
emit('change', {
value
})
}, 50)
// picker
const timeData = computed<Array<CalendarItem[]>>(() => {
let timeColumns: Array<CalendarItem[]> = []
if (props.type === 'datetime' && isNumber(props.value)) {
const date = new Date(props.value)
date.setHours(timeValue.value[0])
date.setMinutes(timeValue.value[1])
date.setSeconds(props.hideSecond ? 0 : timeValue.value[2])
const dateTime = date.getTime()
timeColumns = getTime(dateTime) || []
} else if (isArray(props.value) && props.type === 'datetimerange') {
const [start, end] = props.value!
const dataValue = timeType.value === 'start' ? start : end
const date = new Date(dataValue || '')
date.setHours(timeValue.value[0])
date.setMinutes(timeValue.value[1])
date.setSeconds(props.hideSecond ? 0 : timeValue.value[2])
const dateTime = date.getTime()
const finalValue = [start, end]
if (timeType.value === 'start') {
finalValue[0] = dateTime
} else {
finalValue[1] = dateTime
}
timeColumns = getTime(finalValue, timeType.value) || []
}
return timeColumns
})
//
const title = computed(() => {
return formatMonthTitle(months.value[scrollIndex.value].date)
})
//
const weekLabel = computed(() => {
return (index: number) => {
return getWeekLabel(index - 1)
}
})
//
const scrollHeight = computed(() => {
const scrollHeight: number = timeType.value ? props.panelHeight - 125 : props.panelHeight
return scrollHeight
})
//
const months = computed<MonthInfo[]>(() => {
return getMonths(props.minDate, props.maxDate).map((month, index) => {
const offset = (7 + new Date(month).getDay() - props.firstDayOfWeek) % 7
const totalDay = getMonthEndDay(new Date(month).getFullYear(), new Date(month).getMonth() + 1)
const rows = Math.ceil((offset + totalDay) / 7)
return {
height: rows * 64 + (rows - 1) * 4 + (index === 0 ? 0 : 45), // 64px,4px margin,45px
date: month
}
})
})
watch(
() => props.type,
(val) => {
if (
(val === 'datetime' && props.value) ||
(val === 'datetimerange' && isArray(props.value) && props.value && props.value.length > 0 && props.value[0])
) {
setTime(props.value, 'start')
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.value,
(val) => {
if (isEqual(val, innerValue.value)) return
if ((props.type === 'datetime' && val) || (props.type === 'datetimerange' && val && isArray(val) && val.length > 0 && val[0])) {
setTime(val, 'start')
}
},
{
deep: true,
immediate: true
}
)
onMounted(() => {
scrollIntoView()
})
/**
* 使当前日期或者选中日期滚动到可视区域
*/
async function scrollIntoView() {
//
await pause()
let activeDate: number | null = 0
if (isArray(props.value)) {
// ,
const sortedValue = [...props.value].sort((a, b) => (a || 0) - (b || 0))
activeDate = sortedValue[0]
} else if (isNumber(props.value)) {
activeDate = props.value
}
if (!activeDate) {
activeDate = Date.now()
}
let top: number = 0
let activeMonthIndex = -1
for (let index = 0; index < months.value.length; index++) {
if (compareMonth(months.value[index].date, activeDate) === 0) {
activeMonthIndex = index
// ,
const date = new Date(activeDate)
const day = date.getDate()
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1)
const offset = (7 + firstDay.getDay() - props.firstDayOfWeek) % 7
const row = Math.floor((offset + day - 1) / 7)
// 64px,4px margin
top += row * 64 + row * 4
break
}
top += months.value[index] ? Number(months.value[index].height) : 0
}
scrollTop.value = 0
if (top > 0) {
await pause()
// 45
scrollTop.value = top + (activeMonthIndex > 0 ? 45 : 0)
}
}
/**
* 获取时间 picker 的数据
* @param {timestamp|array} value 当前时间
* @param {string} type 类型是开始还是结束
*/
function getTime(value: number | (number | null)[], type?: string) {
if (props.type === 'datetime') {
return getTimeData({
date: value as number,
minDate: props.minDate,
maxDate: props.maxDate,
filter: props.timeFilter,
isHideSecond: props.hideSecond
})
} else {
if (type === 'start' && isArray(props.value)) {
return getTimeData({
date: (value as Array<number>)[0],
minDate: props.minDate,
maxDate: props.value[1] ? props.value[1] : props.maxDate,
filter: props.timeFilter,
isHideSecond: props.hideSecond
})
} else {
return getTimeData({
date: (value as Array<number>)[1],
minDate: (value as Array<number>)[0],
maxDate: props.maxDate,
filter: props.timeFilter,
isHideSecond: props.hideSecond
})
}
}
}
/**
* 获取 date 的时分秒
* @param {timestamp} date 时间
* @param {string} type 类型是开始还是结束
*/
function getTimeValue(date: number | (number | null)[], type: MonthPanelTimeType) {
let dateValue: Date = new Date()
if (props.type === 'datetime') {
dateValue = new Date(date as number)
} else if (isArray(date)) {
if (type === 'start') {
dateValue = new Date(date[0] || '')
} else {
dateValue = new Date(date[1] || '')
}
}
const hour = dateValue.getHours()
const minute = dateValue.getMinutes()
const second = dateValue.getSeconds()
return props.hideSecond ? [hour, minute] : [hour, minute, second]
}
function setTime(value: number | (number | null)[], type?: MonthPanelTimeType) {
if (isArray(value) && value[0] && value[1] && type === 'start' && timeType.value === 'start') {
type = 'end'
}
timeType.value = type || ''
timeValue.value = getTimeValue(value, type || '')
}
function handleDateChange({ value, type }: { value: number | (number | null)[]; type?: MonthPanelTimeType }) {
if (!isEqual(value, props.value)) {
//
innerValue.value = value
handleChange(value)
}
// datetime datetimerange timeData
if (props.type.indexOf('time') > -1) {
setTime(value, type)
}
}
function handleTimeChange({ value }: { value: any[] }) {
if (!props.value) {
return
}
if (props.type === 'datetime' && isNumber(props.value)) {
const date = new Date(props.value)
date.setHours(value[0])
date.setMinutes(value[1])
date.setSeconds(props.hideSecond ? 0 : value[2])
const dateTime = date.getTime()
handleChange(dateTime)
} else if (isArray(props.value) && props.type === 'datetimerange') {
const [start, end] = props.value!
const dataValue = timeType.value === 'start' ? start : end
const date = new Date(dataValue || '')
date.setHours(value[0])
date.setMinutes(value[1])
date.setSeconds(props.hideSecond ? 0 : value[2])
const dateTime = date.getTime()
if (dateTime === dataValue) return
const finalValue = [start, end]
if (timeType.value === 'start') {
finalValue[0] = dateTime
} else {
finalValue[1] = dateTime
}
innerValue.value = finalValue //
handleChange(finalValue)
}
}
function handlePickStart() {
emit('pickstart')
}
function handlePickEnd() {
emit('pickend')
}
const monthScroll = (event: { detail: { scrollTop: number } }) => {
if (months.value.length <= 1) {
return
}
const scrollTop = Math.max(0, event.detail.scrollTop)
doSetSubtitle(scrollTop)
}
/**
* 设置小标题
* scrollTop 滚动条位置
*/
function doSetSubtitle(scrollTop: number) {
let height: number = 0 //
for (let index = 0; index < months.value.length; index++) {
height = height + months.value[index].height
if (scrollTop < height) {
scrollIndex.value = index
return
}
}
}
defineExpose<MonthPanelExpose>({
scrollIntoView
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,48 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
import type { CalendarFormatter, CalendarTimeFilter, CalendarType } from '../types'
/**
* 月份信息
*/
export interface MonthInfo {
date: number
height: number
}
export const monthPanelProps = {
type: makeRequiredProp(String as PropType<CalendarType>),
value: makeRequiredProp([Number, Array, null] as PropType<number | (number | null)[] | null>),
minDate: makeRequiredProp(Number),
maxDate: makeRequiredProp(Number),
firstDayOfWeek: makeRequiredProp(Number),
formatter: Function as PropType<CalendarFormatter>,
maxRange: Number,
rangePrompt: String,
allowSameDay: makeBooleanProp(false),
showPanelTitle: makeBooleanProp(false),
defaultTime: {
type: [Array] as PropType<Array<number[]>>
},
panelHeight: makeRequiredProp(Number),
// type 'datetime' 'datetimerange'
timeFilter: Function as PropType<CalendarTimeFilter>,
hideSecond: makeBooleanProp(false),
/**
* 是否在手指松开时立即触发picker-view的 change 事件若不开启则会在滚动动画结束后触发 change 事件1.2.25版本起提供仅微信小程序和支付宝小程序支持
*/
immediateChange: makeBooleanProp(false)
}
export type MonthPanelProps = ExtractPropTypes<typeof monthPanelProps>
export type MonthPanelTimeType = 'start' | 'end' | ''
export type MonthPanelExpose = {
/**
* 使当前日期或者选中日期滚动到可视区域
*/
scrollIntoView: () => void
}
export type MonthPanelInstance = ComponentPublicInstance<MonthPanelProps, MonthPanelExpose>

View File

@ -0,0 +1,109 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeRequiredProp, makeStringProp } from '../common/props'
export type CalendarType = 'date' | 'dates' | 'datetime' | 'week' | 'month' | 'daterange' | 'datetimerange' | 'weekrange' | 'monthrange'
export const calendarViewProps = {
...baseProps,
/**
* 选中值 13 位时间戳或时间戳数组
*/
modelValue: makeRequiredProp([Number, Array, null] as PropType<number | number[] | null>),
/**
* 日期类型
*/
type: makeStringProp<CalendarType>('date'),
/**
* 最小日期 13 位时间戳
*/
minDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() - 6, new Date().getDate()).getTime()),
/**
* 最大日期 13 位时间戳
*/
maxDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() + 6, new Date().getDate(), 23, 59, 59).getTime()),
/**
* 周起始天
*/
firstDayOfWeek: makeNumberProp(0),
/**
* 日期格式化函数
*/
formatter: Function as PropType<CalendarFormatter>,
/**
* type 为范围选择时有效最大日期范围
*/
maxRange: Number,
/**
* type 为范围选择时有效选择超出最大日期范围时的错误提示文案
*/
rangePrompt: String,
/**
* type 为范围选择时有效是否允许选择同一天
*/
allowSameDay: makeBooleanProp(false),
//
showPanelTitle: makeBooleanProp(true),
/**
* 选中日期所使用的当日内具体时刻
*/
defaultTime: {
type: [String, Array] as PropType<string | string[]>,
default: '00:00:00'
},
/**
* 可滚动面板的高度
*/
panelHeight: makeNumberProp(378),
/**
* type 'datetime' 'datetimerange' 时有效用于过滤时间选择器的数据
*/
timeFilter: Function as PropType<CalendarTimeFilter>,
/**
* type 'datetime' 'datetimerange' 时有效是否不展示秒修改
*/
hideSecond: makeBooleanProp(false),
/**
* 是否在手指松开时立即触发picker-view的 change 事件若不开启则会在滚动动画结束后触发 change 事件1.2.25版本起提供仅微信小程序和支付宝小程序支持
*/
immediateChange: makeBooleanProp(false)
}
export type CalendarViewProps = ExtractPropTypes<typeof calendarViewProps>
export type CalendarDayType = '' | 'start' | 'middle' | 'end' | 'selected' | 'same' | 'current' | 'multiple-middle' | 'multiple-selected'
export type CalendarDayItem = {
date: number
text?: number | string
topInfo?: string
bottomInfo?: string
type?: CalendarDayType
disabled?: boolean
isLastRow?: boolean
}
export type CalendarFormatter = (day: CalendarDayItem) => CalendarDayItem
export type CalendarTimeFilterOptionType = 'hour' | 'minute' | 'second'
export type CalendarTimeFilterOption = {
type: CalendarTimeFilterOptionType
values: CalendarItem[]
}
export type CalendarTimeFilter = (option: CalendarTimeFilterOption) => CalendarItem[]
export type CalendarItem = {
label: string
value: number
disabled: boolean
}
export type CalendarViewExpose = {
/**
* 使当前日期或者选中日期滚动到可视区域
*/
scrollIntoView: () => void
}
export type CalendarViewInstance = ComponentPublicInstance<CalendarViewExpose, CalendarViewProps>

View File

@ -0,0 +1,429 @@
import { computed } from 'vue'
import { dayjs } from '../common/dayjs'
import { isArray, isFunction, padZero } from '../common/util'
import { useTranslate } from '../composables/useTranslate'
import type { CalendarDayType, CalendarItem, CalendarTimeFilter, CalendarType } from './types'
const { translate } = useTranslate('calendar-view')
const weeks = computed(() => {
return [
translate('weeks.sun'),
translate('weeks.mon'),
translate('weeks.tue'),
translate('weeks.wed'),
translate('weeks.thu'),
translate('weeks.fri'),
translate('weeks.sat')
]
})
/**
* 比较两个时间的日期是否相等
* @param {timestamp} date1
* @param {timestamp} date2
*/
export function compareDate(date1: number, date2: number | null) {
const dateValue1 = new Date(date1)
const dateValue2 = new Date(date2 || '')
const year1 = dateValue1.getFullYear()
const year2 = dateValue2.getFullYear()
const month1 = dateValue1.getMonth()
const month2 = dateValue2.getMonth()
const day1 = dateValue1.getDate()
const day2 = dateValue2.getDate()
if (year1 === year2) {
if (month1 === month2) {
return day1 === day2 ? 0 : day1 > day2 ? 1 : -1
}
return month1 === month2 ? 0 : month1 > month2 ? 1 : -1
}
return year1 > year2 ? 1 : -1
}
/**
* 判断是否是范围选择
* @param {string} type
*/
export function isRange(type: CalendarType) {
return type.indexOf('range') > -1
}
/**
* 比较两个日期的月份是否相等
* @param {timestamp} date1
* @param {timestamp} date2
*/
export function compareMonth(date1: number, date2: number) {
const dateValue1 = new Date(date1)
const dateValue2 = new Date(date2)
const year1 = dateValue1.getFullYear()
const year2 = dateValue2.getFullYear()
const month1 = dateValue1.getMonth()
const month2 = dateValue2.getMonth()
if (year1 === year2) {
return month1 === month2 ? 0 : month1 > month2 ? 1 : -1
}
return year1 > year2 ? 1 : -1
}
/**
* 比较两个日期的年份是否一致
* @param {timestamp} date1
* @param {timestamp} date2
*/
export function compareYear(date1: number, date2: number) {
const dateValue1 = new Date(date1)
const dateValue2 = new Date(date2)
const year1 = dateValue1.getFullYear()
const year2 = dateValue2.getFullYear()
return year1 === year2 ? 0 : year1 > year2 ? 1 : -1
}
/**
* 获取一个月的最后一天
* @param {number} year
* @param {number} month
*/
export function getMonthEndDay(year: number, month: number) {
return 32 - new Date(year, month - 1, 32).getDate()
}
/**
* 格式化年月
* @param {timestamp} date
*/
export function formatMonthTitle(date: number) {
return dayjs(date).format(translate('monthTitle'))
}
/**
* 根据下标获取星期
* @param {number} index
*/
export function getWeekLabel(index: number) {
if (index >= 7) {
index = index % 7
}
return weeks.value[index]
}
/**
* 格式化年份
* @param {timestamp} date
*/
export function formatYearTitle(date: number) {
return dayjs(date).format(translate('yearTitle'))
}
/**
* 根据最小日期和最大日期获取这之间总共有几个月份
* @param {timestamp} minDate
* @param {timestamp} maxDate
*/
export function getMonths(minDate: number, maxDate: number) {
const months: number[] = []
const month = new Date(minDate)
month.setDate(1)
while (compareMonth(month.getTime(), maxDate) < 1) {
months.push(month.getTime())
month.setMonth(month.getMonth() + 1)
}
return months
}
/**
* 根据最小日期和最大日期获取这之间总共有几年
* @param {timestamp} minDate
* @param {timestamp} maxDate
*/
export function getYears(minDate: number, maxDate: number) {
const years: number[] = []
const year = new Date(minDate)
year.setMonth(0)
year.setDate(1)
while (compareYear(year.getTime(), maxDate) < 1) {
years.push(year.getTime())
year.setFullYear(year.getFullYear() + 1)
}
return years
}
/**
* 获取一个日期所在周的第一天和最后一天
* @param {timestamp} date
*/
export function getWeekRange(date: number, firstDayOfWeek: number) {
if (firstDayOfWeek >= 7) {
firstDayOfWeek = firstDayOfWeek % 7
}
const dateValue = new Date(date)
dateValue.setHours(0, 0, 0, 0)
const year = dateValue.getFullYear()
const month = dateValue.getMonth()
const day = dateValue.getDate()
const week = dateValue.getDay()
const weekStart = new Date(year, month, day - ((7 + week - firstDayOfWeek) % 7))
const weekEnd = new Date(year, month, day + 6 - ((7 + week - firstDayOfWeek) % 7))
return [weekStart.getTime(), weekEnd.getTime()]
}
/**
* 获取日期偏移量
* @param {timestamp} date1
* @param {timestamp} date2
*/
export function getDayOffset(date1: number, date2: number) {
return (date1 - date2) / (24 * 60 * 60 * 1000) + 1
}
/**
* 获取偏移日期
* @param {timestamp} date
* @param {number} offset
*/
export function getDayByOffset(date: number, offset: number) {
const dateValue = new Date(date)
dateValue.setDate(dateValue.getDate() + offset)
return dateValue.getTime()
}
export const getPrevDay = (date: number) => getDayByOffset(date, -1)
export const getNextDay = (date: number) => getDayByOffset(date, 1)
/**
* 获取月份偏移量
* @param {timestamp} date1
* @param {timestamp} date2
*/
export function getMonthOffset(date1: number, date2: number) {
const dateValue1 = new Date(date1)
const dateValue2 = new Date(date2)
const year1 = dateValue1.getFullYear()
const year2 = dateValue2.getFullYear()
let month1 = dateValue1.getMonth()
const month2 = dateValue2.getMonth()
month1 = (year1 - year2) * 12 + month1
return month1 - month2 + 1
}
/**
* 获取偏移月份
* @param {timestamp} date
* @param {number} offset
*/
export function getMonthByOffset(date: number, offset: number) {
const dateValue = new Date(date)
dateValue.setMonth(dateValue.getMonth() + offset)
return dateValue.getTime()
}
/**
* 获取默认时间格式化为数组
* @param {array|string|null} defaultTime
*/
export function getDefaultTime(defaultTime: string[] | string | null) {
if (isArray(defaultTime)) {
const startTime = (defaultTime[0] || '00:00:00').split(':').map((item: string) => {
return parseInt(item)
})
const endTime = (defaultTime[1] || '00:00:00').split(':').map((item) => {
return parseInt(item)
})
return [startTime, endTime]
} else {
const time = (defaultTime || '00:00:00').split(':').map((item) => {
return parseInt(item)
})
return [time, time]
}
}
/**
* 根据默认时间获取日期
* @param {timestamp} date
* @param {array} defaultTime
*/
export function getDateByDefaultTime(date: number, defaultTime: number[]) {
const dateValue = new Date(date)
dateValue.setHours(defaultTime[0])
dateValue.setMinutes(defaultTime[1])
dateValue.setSeconds(defaultTime[2])
return dateValue.getTime()
}
/**
* 获取经过 iteratee 格式化后的长度为 n 的数组
* @param {number} n
* @param {function} iteratee
*/
const times = (n: number, iteratee: (index: number) => CalendarItem) => {
let index: number = -1
const result: CalendarItem[] = Array(n < 0 ? 0 : n)
while (++index < n) {
result[index] = iteratee(index)
}
return result
}
/**
* 获取时分秒
* @param {timestamp}} date
*/
const getTime = (date: number) => {
const dateValue = new Date(date)
return [dateValue.getHours(), dateValue.getMinutes(), dateValue.getSeconds()]
}
/**
* 根据最小最大日期获取时间数据用于填入picker
* @param {*} param0
*/
export function getTimeData({
date,
minDate,
maxDate,
isHideSecond,
filter
}: {
date: number
minDate: number
maxDate: number
isHideSecond: boolean
filter?: CalendarTimeFilter
}) {
const compareMin = compareDate(date, minDate)
const compareMax = compareDate(date, maxDate)
let minHour = 0
let maxHour = 23
let minMinute = 0
let maxMinute = 59
let minSecond = 0
let maxSecond = 59
if (compareMin === 0) {
const minTime = getTime(minDate)
const currentTime = getTime(date)
minHour = minTime[0]
if (minTime[0] === currentTime[0]) {
minMinute = minTime[1]
if (minTime[1] === currentTime[1]) {
minSecond = minTime[2]
}
}
}
if (compareMax === 0) {
const maxTime = getTime(maxDate)
const currentTime = getTime(date)
maxHour = maxTime[0]
if (maxTime[0] === currentTime[0]) {
maxMinute = maxTime[1]
if (maxTime[1] === currentTime[1]) {
maxSecond = maxTime[2]
}
}
}
let columns: CalendarItem[][] = []
let hours = times(24, (index) => {
return {
label: translate('hour', padZero(index)),
value: index,
disabled: index < minHour || index > maxHour
}
})
let minutes = times(60, (index) => {
return {
label: translate('minute', padZero(index)),
value: index,
disabled: index < minMinute || index > maxMinute
}
})
let seconds: CalendarItem[] = []
if (filter && isFunction(filter)) {
hours = filter({
type: 'hour',
values: hours
})
minutes = filter({
type: 'minute',
values: minutes
})
}
if (!isHideSecond) {
seconds = times(60, (index) => {
return {
label: translate('second', padZero(index)),
value: index,
disabled: index < minSecond || index > maxSecond
}
})
if (filter && isFunction(filter)) {
seconds = filter({
type: 'second',
values: seconds
})
}
}
columns = isHideSecond ? [hours, minutes] : [hours, minutes, seconds]
return columns
}
/**
* 获取当前是第几周
* @param {timestamp} date
*/
export function getWeekNumber(date: number | Date) {
date = new Date(date)
date.setHours(0, 0, 0, 0)
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7))
// January 4 is always in week 1.
const week = new Date(date.getFullYear(), 0, 4)
// Adjust to Thursday in week 1 and count number of weeks from date to week 1.
// Rounding should be fine for Daylight Saving Time. Its shift should never be more than 12 hours.
return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + ((week.getDay() + 6) % 7)) / 7)
}
export function getItemClass(monthType: CalendarDayType, value: number | null | (number | null)[], type: CalendarType) {
const classList = ['is-' + monthType]
if (type.indexOf('range') > -1 && isArray(value)) {
if (!value || !value[1]) {
classList.push('is-without-end')
}
}
return classList.join(' ')
}

View File

@ -0,0 +1,111 @@
<template>
<view :class="`wd-calendar-view ${customClass}`">
<year-panel
v-if="type === 'month' || type === 'monthrange'"
ref="yearPanelRef"
:type="type"
:value="modelValue"
:min-date="minDate"
:max-date="maxDate"
:formatter="formatter"
:max-range="maxRange"
:range-prompt="rangePrompt"
:allow-same-day="allowSameDay"
:show-panel-title="showPanelTitle"
:default-time="formatDefauleTime"
:panel-height="panelHeight"
@change="handleChange"
/>
<month-panel
v-else
ref="monthPanelRef"
:type="type"
:value="modelValue"
:min-date="minDate"
:max-date="maxDate"
:first-day-of-week="firstDayOfWeek"
:formatter="formatter"
:max-range="maxRange"
:range-prompt="rangePrompt"
:allow-same-day="allowSameDay"
:show-panel-title="showPanelTitle"
:default-time="formatDefauleTime"
:panel-height="panelHeight"
:immediate-change="immediateChange"
:time-filter="timeFilter"
:hide-second="hideSecond"
@change="handleChange"
@pickstart="handlePickStart"
@pickend="handlePickEnd"
/>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-calendar-view',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { getDefaultTime } from './utils'
import yearPanel from './yearPanel/year-panel.vue'
import MonthPanel from './monthPanel/month-panel.vue'
import { calendarViewProps, type CalendarViewExpose } from './types'
const props = defineProps(calendarViewProps)
const emit = defineEmits(['change', 'update:modelValue', 'pickstart', 'pickend'])
const formatDefauleTime = ref<number[][]>([])
const yearPanelRef = ref()
const monthPanelRef = ref()
watch(
() => props.defaultTime,
(newValue) => {
formatDefauleTime.value = getDefaultTime(newValue)
},
{
deep: true,
immediate: true
}
)
/**
* 使当前日期或者选中日期滚动到可视区域
*/
function scrollIntoView() {
const panel = getPanel()
panel.scrollIntoView && panel.scrollIntoView()
}
function getPanel() {
return props.type.indexOf('month') > -1 ? yearPanelRef.value : monthPanelRef.value
}
function handleChange({ value }: { value: number | number[] | null }) {
emit('update:modelValue', value)
emit('change', {
value
})
}
function handlePickStart() {
emit('pickstart')
}
function handlePickEnd() {
emit('pickend')
}
defineExpose<CalendarViewExpose>({
scrollIntoView
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,153 @@
@import '../../common/abstracts/variable';
@import '../../common/abstracts/mixin';
.wot-theme-dark {
@include b(year) {
@include e(title) {
color: $-dark-color;
}
@include e(months) {
color: $-dark-color;
}
@include e(month) {
@include when(disabled) {
.wd-year__month-text {
color: $-dark-color-gray;
}
}
}
}
}
@include b(year) {
@include e(title) {
display: flex;
align-items: center;
justify-content: center;
height: 45px;
font-size: $-calendar-panel-title-fs;
color: $-calendar-panel-title-color;
}
@include e(months) {
display: flex;
flex-wrap: wrap;
font-size: $-calendar-day-fs;
color: $-calendar-day-color;
}
@include e(month) {
position: relative;
width: 25%;
height: $-calendar-day-height;
line-height: $-calendar-day-height;
text-align: center;
margin-bottom: $-calendar-item-margin-bottom;
@include when(disabled) {
.wd-year__month-text {
color: $-calendar-disabled-color;
}
}
@include when(current) {
color: $-calendar-active-color;
}
@include when(selected) {
color: #fff;
.wd-year__month-text {
border-radius: $-calendar-active-border;
background: $-calendar-active-color;
}
}
@include when(middle) {
background: $-calendar-range-color;
}
@include when(start) {
color: $-calendar-selected-color;
&::after {
position: absolute;
top: 0;
right: 0;
left: 50%;
bottom: 0;
content: '';
background: $-calendar-range-color;
}
.wd-year__month-text {
background: $-calendar-active-color;
border-radius: $-calendar-active-border 0 0 $-calendar-active-border;
}
&.is-without-end::after {
display: none;
}
}
@include when(end) {
color: $-calendar-selected-color;
&::after {
position: absolute;
top: 0;
left: 0;
right: 50%;
bottom: 0;
content: '';
background: $-calendar-range-color;
}
.wd-year__month-text {
background: $-calendar-active-color;
border-radius: 0 $-calendar-active-border $-calendar-active-border 0;
}
}
@include when(same) {
color: $-calendar-selected-color;
.wd-year__month-text {
background: $-calendar-active-color;
border-radius: $-calendar-active-border;
}
}
@include when(last-row){
margin-bottom: 0;
}
}
@include e(month-text) {
width: $-calendar-month-width;
margin: 0 auto;
text-align: center;
}
@include e(month-top) {
position: absolute;
top: 10px;
left: 0;
right: 0;
line-height: 1.1;
font-size: $-calendar-info-fs;
text-align: center;
}
@include e(month-bottom) {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
line-height: 1.1;
font-size: $-calendar-info-fs;
text-align: center;
}
}

View File

@ -0,0 +1,20 @@
import type { PropType } from 'vue'
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
import type { CalendarFormatter, CalendarType } from '../types'
export const yearProps = {
type: makeRequiredProp(String as PropType<CalendarType>),
date: makeRequiredProp(Number),
value: makeRequiredProp([Number, Array] as PropType<number | (number | null)[] | null>),
minDate: makeRequiredProp(Number),
maxDate: makeRequiredProp(Number),
//
formatter: Function as PropType<CalendarFormatter>,
maxRange: Number,
rangePrompt: String,
allowSameDay: makeBooleanProp(false),
defaultTime: {
type: [Array] as PropType<Array<number[]>>
},
showTitle: makeBooleanProp(true)
}

View File

@ -0,0 +1,202 @@
<template>
<wd-toast selector="wd-year" />
<view class="wd-year year">
<view class="wd-year__title" v-if="showTitle">{{ yearTitle(date) }}</view>
<view class="wd-year__months">
<view
v-for="(item, index) in months"
:key="index"
:class="`wd-year__month ${item.disabled ? 'is-disabled' : ''} ${item.isLastRow ? 'is-last-row' : ''} ${
item.type ? monthTypeClass(item.type) : ''
}`"
@click="handleDateClick(index)"
>
<view class="wd-year__month-top">{{ item.topInfo }}</view>
<view class="wd-year__month-text">{{ getMonthLabel(item.date) }}</view>
<view class="wd-year__month-bottom">{{ item.bottomInfo }}</view>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdToast from '../../wd-toast/wd-toast.vue'
import { computed, ref, watch } from 'vue'
import { deepClone, isArray, isFunction } from '../../common/util'
import { compareMonth, formatYearTitle, getDateByDefaultTime, getItemClass, getMonthByOffset, getMonthOffset } from '../utils'
import { useToast } from '../../wd-toast'
import { useTranslate } from '../../composables/useTranslate'
import { dayjs } from '../../common/dayjs'
import { yearProps } from './types'
import type { CalendarDayItem, CalendarDayType } from '../types'
const props = defineProps(yearProps)
const emit = defineEmits(['change'])
const toast = useToast('wd-year')
const { translate } = useTranslate('calendar-view')
const months = ref<CalendarDayItem[]>([])
const monthTypeClass = computed(() => {
return (monthType: CalendarDayType) => {
return getItemClass(monthType, props.value, props.type)
}
})
const yearTitle = computed(() => {
return (date: number) => {
return formatYearTitle(date)
}
})
watch(
[() => props.type, () => props.date, () => props.value, () => props.minDate, () => props.maxDate, () => props.formatter],
() => {
setMonths()
},
{
deep: true,
immediate: true
}
)
function getMonthLabel(date: number) {
return dayjs(date).format(translate('month', date))
}
function setMonths() {
const monthList: CalendarDayItem[] = []
const date = new Date(props.date)
const year = date.getFullYear()
const value = props.value
if (props.type.indexOf('range') > -1 && value && !isArray(value)) {
console.error('[wot-design] value should be array when type is range')
return
}
for (let month = 0; month < 12; month++) {
const date = new Date(year, month, 1).getTime()
let type: CalendarDayType = getMonthType(date)
if (!type && compareMonth(date, Date.now()) === 0) {
type = 'current'
}
const monthObj = getFormatterDate(date, month, type)
monthList.push(monthObj)
}
months.value = deepClone(monthList)
}
function getMonthType(date: number) {
if (props.type === 'monthrange' && isArray(props.value)) {
const [startDate, endDate] = props.value || []
if (startDate && compareMonth(date, startDate) === 0) {
if (endDate && compareMonth(startDate, endDate) === 0) {
return 'same'
}
return 'start'
} else if (endDate && compareMonth(date, endDate) === 0) {
return 'end'
} else if (startDate && endDate && compareMonth(date, startDate) === 1 && compareMonth(date, endDate) === -1) {
return 'middle'
} else {
return ''
}
} else {
if (props.value && compareMonth(date, props.value as number) === 0) {
return 'selected'
} else {
return ''
}
}
}
function handleDateClick(index: number) {
const date = months.value[index]
if (date.disabled) return
switch (props.type) {
case 'month':
handleMonthChange(date)
break
case 'monthrange':
handleMonthRangeChange(date)
break
default:
handleMonthChange(date)
}
}
function getDate(date: number) {
return props.defaultTime && props.defaultTime.length > 0 ? getDateByDefaultTime(date, props.defaultTime[0]) : date
}
function handleMonthChange(date: CalendarDayItem) {
if (date.type !== 'selected') {
emit('change', {
value: getDate(date.date)
})
}
}
function handleMonthRangeChange(date: CalendarDayItem) {
let value: (number | null)[] = []
const [startDate, endDate] = isArray(props.value) ? props.value || [] : []
const compare = compareMonth(date.date, startDate!)
//
if (!props.allowSameDay && !endDate && compare === 0) return
if (startDate && !endDate && compare > -1) {
if (props.maxRange && getMonthOffset(date.date, startDate) > props.maxRange) {
const maxEndDate = getMonthByOffset(startDate, props.maxRange - 1)
value = [startDate, getDate(maxEndDate)]
toast.show({
msg: props.rangePrompt || translate('rangePromptMonth', props.maxRange)
})
} else {
value = [startDate, getDate(date.date)]
}
} else {
value = [getDate(date.date), null]
}
emit('change', {
value
})
}
function getFormatterDate(date: number, month: number, type?: CalendarDayType) {
let monthObj: CalendarDayItem = {
date: date,
text: month + 1,
topInfo: '',
bottomInfo: '',
type,
disabled: compareMonth(date, props.minDate) === -1 || compareMonth(date, props.maxDate) === 1,
isLastRow: month >= 8
}
if (props.formatter) {
if (isFunction(props.formatter)) {
monthObj = props.formatter(monthObj)
} else {
console.error('[wot-design] error(wd-calendar-view): the formatter prop of wd-calendar-view should be a function')
}
}
return monthObj
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,24 @@
@import '../../common/abstracts/variable';
@import '../../common/abstracts/mixin';
.wot-theme-dark {
@include b(year-panel) {
@include e(title) {
color: $-dark-color;
box-shadow: 0px 4px 8px 0 rgba(255, 255,255, 0.02);
}
}
}
@include b(year-panel) {
font-size: $-calendar-fs;
padding: $-calendar-panel-padding;
@include e(title) {
padding: 5px 0;
text-align: center;
font-size: $-calendar-panel-title-fs;
color: $-calendar-panel-title-color;
box-shadow: 0px 4px 8px 0 rgba(0, 0, 0, 0.02);
}
}

View File

@ -0,0 +1,38 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
import type { CalendarFormatter, CalendarType } from '../types'
/**
* 月份信息
*/
export interface YearInfo {
date: number
height: number
}
export const yearPanelProps = {
type: makeRequiredProp(String as PropType<CalendarType>),
value: makeRequiredProp([Number, Array] as PropType<number | (number | null)[] | null>),
minDate: makeRequiredProp(Number),
maxDate: makeRequiredProp(Number),
formatter: Function as PropType<CalendarFormatter>,
maxRange: Number,
rangePrompt: String,
allowSameDay: makeBooleanProp(false),
showPanelTitle: makeBooleanProp(false),
defaultTime: {
type: [Array] as PropType<Array<number[]>>
},
panelHeight: makeRequiredProp(Number)
}
export type YearPanelProps = ExtractPropTypes<typeof yearPanelProps>
export type YearPanelExpose = {
/**
* 使当前日期或者选中日期滚动到可视区域
*/
scrollIntoView: () => void
}
export type YearPanelInstance = ComponentPublicInstance<YearPanelProps, YearPanelExpose>

View File

@ -0,0 +1,135 @@
<template>
<view class="wd-year-panel">
<view v-if="showPanelTitle" class="wd-year-panel__title">{{ title }}</view>
<scroll-view class="wd-year-panel__container" :style="`height: ${scrollHeight}px`" scroll-y @scroll="yearScroll" :scroll-top="scrollTop">
<view v-for="(item, index) in years" :key="index" :id="`year${index}`">
<year
:type="type"
:date="item.date"
:value="value"
:min-date="minDate"
:max-date="maxDate"
:max-range="maxRange"
:formatter="formatter"
:range-prompt="rangePrompt"
:allow-same-day="allowSameDay"
:default-time="defaultTime"
:showTitle="index !== 0"
@change="handleDateChange"
/>
</view>
</scroll-view>
</view>
</template>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, ref, onMounted } from 'vue'
import { compareYear, formatYearTitle, getYears } from '../utils'
import { isArray, isNumber, pause } from '../../common/util'
import Year from '../year/year.vue'
import { yearPanelProps, type YearInfo, type YearPanelExpose } from './types'
const props = defineProps(yearPanelProps)
const emit = defineEmits(['change'])
const scrollTop = ref<number>(0) //
const scrollIndex = ref<number>(0) //
//
const scrollHeight = computed(() => {
const scrollHeight: number = props.panelHeight + (props.showPanelTitle ? 26 : 16)
return scrollHeight
})
//
const years = computed<YearInfo[]>(() => {
return getYears(props.minDate, props.maxDate).map((year, index) => {
return {
date: year,
height: index === 0 ? 200 : 245
}
})
})
//
const title = computed(() => {
return formatYearTitle(years.value[scrollIndex.value].date)
})
onMounted(() => {
scrollIntoView()
})
async function scrollIntoView() {
await pause()
let activeDate: number | null = null
if (isArray(props.value)) {
activeDate = props.value![0]
} else if (isNumber(props.value)) {
activeDate = props.value
}
if (!activeDate) {
activeDate = Date.now()
}
let top: number = 0
for (let index = 0; index < years.value.length; index++) {
if (compareYear(years.value[index].date, activeDate) === 0) {
break
}
top += years.value[index] ? Number(years.value[index].height) : 0
}
scrollTop.value = 0
if (top > 0) {
await pause()
scrollTop.value = top + 45
}
}
const yearScroll = (event: { detail: { scrollTop: number } }) => {
if (years.value.length <= 1) {
return
}
const scrollTop = Math.max(0, event.detail.scrollTop)
doSetSubtitle(scrollTop)
}
/**
* 设置小标题
* scrollTop 滚动条位置
*/
function doSetSubtitle(scrollTop: number) {
let height: number = 0 //
for (let index = 0; index < years.value.length; index++) {
height = height + years.value[index].height
if (scrollTop < height) {
scrollIndex.value = index
return
}
}
}
function handleDateChange({ value }: { value: number[] }) {
emit('change', {
value
})
}
defineExpose<YearPanelExpose>({
scrollIntoView
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,245 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(calendar) {
@include e(cell) {
background-color: $-dark-background2;
color: $-dark-color;
}
@include e(label) {
color: $-dark-color;
}
@include e(value) {
color: $-dark-color;
}
@include e(title) {
color: $-dark-color;
}
:deep(.wd-calendar__arrow),
:deep(.wd-calendar__close) {
color: $-dark-color;
}
@include when(border) {
.wd-calendar__cell {
@include halfPixelBorder('top', $-cell-padding, $-dark-border-color);
}
}
@include e(range-label-item) {
color: $-dark-color;
@include when(placeholder) {
color: $-dark-color-gray;
}
}
@include e(range-sperator) {
color: $-dark-color-gray;
}
}
}
@include b(calendar) {
@include when(border) {
.wd-calendar__cell {
@include halfPixelBorder('top', $-cell-padding);
}
}
@include e(cell) {
position: relative;
display: flex;
padding: $-cell-wrapper-padding $-cell-padding;
align-items: flex-start;
background-color: $-color-white;
text-decoration: none;
color: $-cell-title-color;
font-size: $-cell-title-fs;
overflow: hidden;
line-height: $-cell-line-height;
}
@include e(cell) {
@include when(disabled) {
.wd-calendar__value {
color: $-input-disabled-color;
}
}
@include when(align-right) {
.wd-calendar__value {
text-align: right;
}
}
@include when(error) {
.wd-calendar__value {
color: $-input-error-color;
}
:deep(.wd-calendar__arrow) {
color: $-input-error-color;
}
}
@include when(large) {
font-size: $-cell-title-fs-large;
:deep(.wd-calendar__arrow) {
font-size: $-cell-icon-size-large;
}
}
@include when(center) {
align-items: center;
:deep(.wd-calendar__arrow) {
margin-top: 0;
}
}
}
@include e(error-message){
color: $-form-item-error-message-color;
font-size: $-form-item-error-message-font-size;
line-height: $-form-item-error-message-line-height;
text-align: left;
vertical-align: middle;
}
@include e(label) {
position: relative;
width: $-input-cell-label-width;
margin-right: $-cell-padding;
color: $-cell-title-color;
box-sizing: border-box;
@include when(required) {
padding-left: 12px;
&::after {
position: absolute;
left: 0;
top: 2px;
content: '*';
font-size: $-cell-required-size;
line-height: 1.1;
color: $-cell-required-color;
}
}
}
@include e(value-wraper) {
display: flex;
}
@include e(value) {
flex: 1;
margin-right: 10px;
color: $-cell-value-color;
@include when(ellipsis) {
@include lineEllipsis;
}
@include m(placeholder) {
color: $-input-placeholder-color;
}
}
@include e(body) {
flex: 1;
}
@include edeep(arrow) {
display: block;
font-size: $-cell-icon-size;
color: $-cell-arrow-color;
line-height: $-cell-line-height;
}
@include e(header) {
position: relative;
overflow: hidden;
}
@include e(title) {
color: $-action-sheet-color;
height: $-action-sheet-title-height;
line-height: $-action-sheet-title-height;
text-align: center;
font-size: $-action-sheet-title-fs;
font-weight: $-action-sheet-weight;
}
@include edeep(close) {
position: absolute;
top: $-action-sheet-close-top;
right: $-action-sheet-close-right;
color: $-action-sheet-close-color;
font-size: $-action-sheet-close-fs;
transform: rotate(-45deg);
line-height: 1.1;
}
@include e(tabs) {
width: 222px;
margin: 10px auto 12px;
}
@include e(shortcuts) {
padding: 20px 0;
text-align: center;
}
@include edeep(tag) {
margin-right: 8px;
}
@include e(view) {
@include when(show-confirm) {
height: 394px;
@include when(range) {
height: 384px;
}
}
}
@include e(range-label) {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
@include when(monthrange) {
padding-bottom: 10px;
box-shadow: 0px 4px 8px 0 rgba(0, 0, 0, 0.02);
}
}
@include e(range-label-item) {
flex: 1;
color: rgba(0, 0, 0, 0.85);
@include when(placeholder) {
color: rgba(0, 0, 0, 0.25);
}
}
@include e(range-sperator) {
margin: 0 24px;
color: rgba(0, 0, 0, 0.25);
}
@include e(confirm) {
padding: 12px 25px 14px;
}
}

View File

@ -0,0 +1,214 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 20:40:34
* @LastEditTime: 2024-12-08 19:13:33
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-calendar/types.ts
* 记得注释
*/
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeRequiredProp, makeStringProp } from '../common/props'
import type { CalendarFormatter, CalendarTimeFilter, CalendarType } from '../wd-calendar-view/types'
import type { FormItemRule } from '../wd-form/types'
export const calendarProps = {
...baseProps,
/**
* 选中值 13 位时间戳或时间戳数组
*/
modelValue: makeRequiredProp([Number, Array, null] as PropType<number | number[] | null>),
/**
* 日期类型可选值date / dates / datetime / week / month / daterange / datetimerange / weekrange / monthrange
*/
type: makeStringProp<CalendarType>('date'),
/**
* 最小日期 13 位时间戳
*/
minDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() - 6, new Date().getDate()).getTime()),
/**
* 最大日期 13 位时间戳
*/
maxDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() + 6, new Date().getDate(), 23, 59, 59).getTime()),
/**
* 周起始天
*/
firstDayOfWeek: makeNumberProp(0),
/**
* 日期格式化函数
*/
formatter: Function as PropType<CalendarFormatter>,
/**
* type 为范围选择时有效最大日期范围
*/
maxRange: Number,
/**
* type 为范围选择时有效选择超出最大日期范围时的错误提示文案
*/
rangePrompt: String,
/**
* type 为范围选择时有效是否允许选择同一天
*/
allowSameDay: makeBooleanProp(false),
/**
* 选中日期所使用的当日内具体时刻
*/
defaultTime: {
type: [String, Array] as PropType<string | string[]>
},
/**
* type 'datetime' 'datetimerange' 时有效用于过滤时间选择器的数据
*/
timeFilter: Function as PropType<CalendarTimeFilter>,
/**
* type 'datetime' 'datetimerange' 时有效是否不展示秒修改
*/
hideSecond: makeBooleanProp(false),
/**
* 选择器左侧文案
*/
label: String,
/**
* 设置左侧标题宽度
*/
labelWidth: String,
/**
* 禁用
*/
disabled: makeBooleanProp(false),
/**
* 只读
*/
readonly: makeBooleanProp(false),
/**
* 选择器占位符
*/
placeholder: String,
/**
* 弹出层标题
*/
title: String,
/**
* 选择器的值靠右展示
*/
alignRight: makeBooleanProp(false),
/**
* 是否为错误状态错误状态时右侧内容为红色
*/
error: makeBooleanProp(false),
/**
* 是否必填
*/
required: makeBooleanProp(false),
/**
* 设置选择器大小可选值large
*/
size: String,
/**
* 是否垂直居中
*/
center: makeBooleanProp(false),
/**
* 点击遮罩是否关闭
*/
closeOnClickModal: makeBooleanProp(true),
/**
* 弹框层级
*/
zIndex: makeNumberProp(15),
/**
* 是否显示确定按钮
*/
showConfirm: makeBooleanProp(true),
/**
* 确定按钮文字
*/
confirmText: String,
/**
* 自定义展示文案的格式化函数返回一个字符串
*/
displayFormat: Function as PropType<CalendarDisplayFormat>,
/**
* 自定义范围选择类型的面板内部回显返回一个字符串
*/
innerDisplayFormat: Function as PropType<CalendarInnerDisplayFormat>,
/**
* 是否超出隐藏
*/
ellipsis: makeBooleanProp(false),
/**
* 是否显示类型切换功能
*/
showTypeSwitch: makeBooleanProp(false),
/**
* 快捷选项为对象数组其中对象的 text 必传
*/
shortcuts: makeArrayProp<Record<string, any>>(),
/**
* 快捷操作点击回调
*/
onShortcutsClick: Function as PropType<CalendarOnShortcutsClick>,
/**
* 弹出面板是否设置底部安全距离iphone X 类型的机型
*/
safeAreaInsetBottom: makeBooleanProp(true),
/**
* 确定前校验函数接收 { value, resolve } 参数通过 resolve 继续执行resolve 接收 1 boolean 参数
*/
beforeConfirm: Function as PropType<CalendarBeforeConfirm>,
/**
* 表单域 model 字段名在使用表单校验功能的情况下该属性是必填的
*/
prop: String,
/**
* 表单验证规则结合wd-form组件使用
*/
rules: makeArrayProp<FormItemRule>(),
customViewClass: makeStringProp(''),
/**
* label 外部自定义样式
*/
customLabelClass: makeStringProp(''),
/**
* value 外部自定义样式
*/
customValueClass: makeStringProp(''),
/**
* 是否在手指松开时立即触发picker-view的 change 事件若不开启则会在滚动动画结束后触发 change 事件1.2.25版本起提供仅微信小程序和支付宝小程序支持
*/
immediateChange: makeBooleanProp(false),
/**
* 是否使用内置单元格
* 默认为 true使用内置单元格
*/
withCell: makeBooleanProp(true)
}
export type CalendarDisplayFormat = (value: number | number[], type: CalendarType) => string
export type CalendarInnerDisplayFormat = (value: number, rangeType: 'start' | 'end', type: CalendarType) => string
export type CalendarBeforeConfirmOption = {
value: number | number[] | null
resolve: (isPass: boolean) => void
}
export type CalendarBeforeConfirm = (option: CalendarBeforeConfirmOption) => void
export type CalendarOnShortcutsClickOption = {
item: Record<string, any>
index: number
}
export type CalendarOnShortcutsClick = (option: CalendarOnShortcutsClickOption) => number | number[]
export type CalendarExpose = {
/** 关闭时间选择器弹窗 */
close: () => void
/** 打开时间选择器弹窗 */
open: () => void
}
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
export type CalendarInstance = ComponentPublicInstance<CalendarExpose, CalendarProps>

View File

@ -0,0 +1,443 @@
<template>
<view :class="`wd-calendar ${cell.border.value ? 'is-border' : ''} ${customClass}`">
<view class="wd-calendar__field" @click="open" v-if="withCell">
<slot v-if="$slots.default"></slot>
<view
v-else
:class="`wd-calendar__cell ${disabled ? 'is-disabled' : ''} ${props.readonly ? 'is-readonly' : ''} ${alignRight ? 'is-align-right' : ''} ${
error ? 'is-error' : ''
} ${size ? 'is-' + size : ''} ${center ? 'is-center' : ''}`"
>
<view
v-if="label || $slots.label"
:class="`wd-calendar__label ${isRequired ? 'is-required' : ''} ${customLabelClass}`"
:style="labelWidth ? 'min-width:' + labelWidth + ';max-width:' + labelWidth + ';' : ''"
>
<slot name="label">{{ label }}</slot>
</view>
<view class="wd-calendar__body">
<view class="wd-calendar__value-wraper">
<view
:class="`wd-calendar__value ${ellipsis ? 'is-ellipsis' : ''} ${customValueClass} ${showValue ? '' : 'wd-calendar__value--placeholder'}`"
>
{{ showValue || placeholder || translate('placeholder') }}
</view>
<wd-icon v-if="!disabled && !readonly" custom-class="wd-calendar__arrow" name="arrow-right" />
</view>
<view v-if="errorMessage" class="wd-calendar__error-message">{{ errorMessage }}</view>
</view>
</view>
</view>
<wd-action-sheet
v-model="pickerShow"
:duration="250"
:close-on-click-modal="closeOnClickModal"
:safe-area-inset-bottom="safeAreaInsetBottom"
:z-index="zIndex"
@close="close"
>
<view class="wd-calendar__header">
<view v-if="!showTypeSwitch && shortcuts.length === 0" class="wd-calendar__title">{{ title || translate('title') }}</view>
<view v-if="showTypeSwitch" class="wd-calendar__tabs">
<wd-tabs ref="calendarTabs" v-model="currentTab" @change="handleTypeChange">
<wd-tab :title="translate('day')" :name="translate('day')" />
<wd-tab :title="translate('week')" :name="translate('week')" />
<wd-tab :title="translate('month')" :name="translate('month')" />
</wd-tabs>
</view>
<view v-if="shortcuts.length > 0" class="wd-calendar__shortcuts">
<wd-tag
v-for="(item, index) in shortcuts"
:key="index"
custom-class="wd-calendar__tag"
type="primary"
plain
round
@click="handleShortcutClick(index)"
>
{{ item.text }}
</wd-tag>
</view>
<wd-icon custom-class="wd-calendar__close" name="add" @click="close" />
</view>
<view
v-if="inited"
:class="`wd-calendar__view ${currentType.indexOf('range') > -1 ? 'is-range' : ''} ${showConfirm ? 'is-show-confirm' : ''}`"
>
<view v-if="range(type)" :class="`wd-calendar__range-label ${type === 'monthrange' ? 'is-monthrange' : ''}`">
<view
:class="`wd-calendar__range-label-item ${!calendarValue || !isArray(calendarValue) || !calendarValue[0] ? 'is-placeholder' : ''}`"
style="text-align: right"
>
{{ rangeLabel[0] }}
</view>
<view class="wd-calendar__range-sperator">/</view>
<view :class="`wd-calendar__range-label-item ${!calendarValue || !isArray(calendarValue) || !calendarValue[1] ? 'is-placeholder' : ''}`">
{{ rangeLabel[1] }}
</view>
</view>
<wd-calendar-view
ref="calendarView"
v-model="calendarValue"
:type="currentType"
:min-date="minDate"
:max-date="maxDate"
:first-day-of-week="firstDayOfWeek"
:formatter="formatter"
:panel-height="panelHeight"
:max-range="maxRange"
:range-prompt="rangePrompt"
:allow-same-day="allowSameDay"
:default-time="defaultTime"
:time-filter="timeFilter"
:hide-second="hideSecond"
:show-panel-title="!range(type)"
:immediate-change="immediateChange"
@change="handleChange"
/>
</view>
<view v-if="showConfirm" class="wd-calendar__confirm">
<wd-button block :disabled="confirmBtnDisabled" @click="handleConfirm">{{ confirmText || translate('confirm') }}</wd-button>
</view>
</wd-action-sheet>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-calendar',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import wdCalendarView from '../wd-calendar-view/wd-calendar-view.vue'
import wdActionSheet from '../wd-action-sheet/wd-action-sheet.vue'
import wdButton from '../wd-button/wd-button.vue'
import { ref, computed, watch } from 'vue'
import { dayjs } from '../common/dayjs'
import { deepClone, isArray, isEqual, padZero, pause } from '../common/util'
import { getWeekNumber, isRange } from '../wd-calendar-view/utils'
import { useCell } from '../composables/useCell'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
import { useParent } from '../composables/useParent'
import { useTranslate } from '../composables/useTranslate'
import { calendarProps, type CalendarExpose } from './types'
import type { CalendarType } from '../wd-calendar-view/types'
const { translate } = useTranslate('calendar')
const defaultDisplayFormat = (value: number | number[], type: CalendarType): string => {
switch (type) {
case 'date':
return dayjs(value as number).format('YYYY-MM-DD')
case 'dates':
return (value as number[])
.map((item) => {
return dayjs(item).format('YYYY-MM-DD')
})
.join(', ')
case 'daterange':
return `${(value as number[])[0] ? dayjs((value as number[])[0]).format('YYYY-MM-DD') : translate('startTime')} ${translate('to')} ${
(value as number[])[1] ? dayjs((value as number[])[1]).format('YYYY-MM-DD') : translate('endTime')
}`
case 'datetime':
return dayjs(value as number).format('YYYY-MM-DD HH:mm:ss')
case 'datetimerange':
return `${(value as number[])[0] ? dayjs((value as number[])[0]).format(translate('timeFormat')) : translate('startTime')} ${translate(
'to'
)}\n${(value as number[])[1] ? dayjs((value as number[])[1]).format(translate('timeFormat')) : translate('endTime')}`
case 'week': {
const date = new Date(value as number)
const year = date.getFullYear()
const week = getWeekNumber(value as number)
const weekStart = new Date(date)
weekStart.setDate(date.getDate() - date.getDay() + 1)
const weekEnd = new Date(date)
weekEnd.setDate(date.getDate() + (7 - date.getDay()))
const adjustedYear = weekEnd.getFullYear() > year ? weekEnd.getFullYear() : year
return translate('weekFormat', adjustedYear, padZero(week))
}
case 'weekrange': {
const date1 = new Date((value as number[])[0])
const date2 = new Date((value as number[])[1])
const year1 = date1.getFullYear()
const year2 = date2.getFullYear()
const week1 = getWeekNumber((value as number[])[0])
const week2 = getWeekNumber((value as number[])[1])
const weekStart1 = new Date(date1)
weekStart1.setDate(date1.getDate() - date1.getDay() + 1)
const weekEnd1 = new Date(date1)
weekEnd1.setDate(date1.getDate() + (7 - date1.getDay()))
const weekStart2 = new Date(date2)
weekStart2.setDate(date2.getDate() - date2.getDay() + 1)
const weekEnd2 = new Date(date2)
weekEnd2.setDate(date2.getDate() + (7 - date2.getDay()))
const adjustedYear1 = weekEnd1.getFullYear() > year1 ? weekEnd1.getFullYear() : year1
const adjustedYear2 = weekEnd2.getFullYear() > year2 ? weekEnd2.getFullYear() : year2
return `${(value as number[])[0] ? translate('weekFormat', adjustedYear1, padZero(week1)) : translate('startWeek')} - ${
(value as number[])[1] ? translate('weekFormat', adjustedYear2, padZero(week2)) : translate('endWeek')
}`
}
case 'month':
return dayjs(value as number).format('YYYY / MM')
case 'monthrange':
return `${(value as number[])[0] ? dayjs((value as number[])[0]).format('YYYY / MM') : translate('startMonth')} ${translate('to')} ${
(value as number[])[1] ? dayjs((value as number[])[1]).format('YYYY / MM') : translate('endMonth')
}`
}
}
const formatRange = (value: number, rangeType: 'start' | 'end', type: CalendarType) => {
switch (type) {
case 'daterange':
if (!value) {
return rangeType === 'end' ? translate('endTime') : translate('startTime')
}
return dayjs(value).format(translate('dateFormat'))
case 'datetimerange':
if (!value) {
return rangeType === 'end' ? translate('endTime') : translate('startTime')
}
return dayjs(value).format(translate('timeFormat'))
case 'weekrange': {
if (!value) {
return rangeType === 'end' ? translate('endWeek') : translate('startWeek')
}
const date = new Date(value)
const year = date.getFullYear()
const week = getWeekNumber(value)
return translate('weekFormat', year, padZero(week))
}
case 'monthrange':
if (!value) {
return rangeType === 'end' ? translate('endMonth') : translate('startMonth')
}
return dayjs(value).format(translate('monthFormat'))
}
}
const props = defineProps(calendarProps)
const emit = defineEmits(['cancel', 'change', 'update:modelValue', 'confirm', 'open'])
const pickerShow = ref<boolean>(false)
const calendarValue = ref<null | number | number[]>(null)
const lastCalendarValue = ref<null | number | number[]>(null)
const panelHeight = ref<number>(338)
const confirmBtnDisabled = ref<boolean>(true)
const currentTab = ref<number>(0)
const lastTab = ref<number>(0)
const currentType = ref<CalendarType>('date')
const lastCurrentType = ref<CalendarType>()
const inited = ref<boolean>(false)
const cell = useCell()
const calendarView = ref()
const calendarTabs = ref()
const rangeLabel = computed(() => {
const [start, end] = deepClone(isArray(calendarValue.value) ? calendarValue.value : [])
return [start, end].map((item, index) => {
return (props.innerDisplayFormat || formatRange)(item, index === 0 ? 'start' : 'end', currentType.value)
})
})
const showValue = computed(() => {
if ((!isArray(props.modelValue) && props.modelValue) || (isArray(props.modelValue) && props.modelValue.length)) {
return (props.displayFormat || defaultDisplayFormat)(props.modelValue, lastCurrentType.value || currentType.value)
} else {
return ''
}
})
watch(
() => props.modelValue,
(val, oldVal) => {
if (isEqual(val, oldVal)) return
calendarValue.value = deepClone(val)
confirmBtnDisabled.value = getConfirmBtnStatus(val)
},
{
immediate: true
}
)
watch(
() => props.type,
(newValue, oldValue) => {
if (props.showTypeSwitch) {
const tabs = ['date', 'week', 'month']
const rangeTabs = ['daterange', 'weekrange', 'monthrange']
const index = newValue.indexOf('range') > -1 ? rangeTabs.indexOf(newValue) || 0 : tabs.indexOf(newValue)
currentTab.value = index
}
panelHeight.value = props.showConfirm ? 338 : 400
currentType.value = deepClone(newValue)
},
{
deep: true,
immediate: true
}
)
watch(
() => props.showConfirm,
(val) => {
panelHeight.value = val ? 338 : 400
},
{
deep: true,
immediate: true
}
)
const { parent: form } = useParent(FORM_KEY)
//
const errorMessage = computed(() => {
if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
return form.errorMessages[props.prop]
} else {
return ''
}
})
//
const isRequired = computed(() => {
let formRequired = false
if (form && form.props.rules) {
const rules = form.props.rules
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
formRequired = rules[key].some((rule: FormItemRule) => rule.required)
}
}
}
return props.required || props.rules.some((rule) => rule.required) || formRequired
})
const range = computed(() => {
return (type: CalendarType) => {
return isRange(type)
}
})
function scrollIntoView() {
calendarView.value && calendarView.value && calendarView.value.$.exposed.scrollIntoView()
}
//
async function open() {
const { disabled, readonly } = props
if (disabled || readonly) return
inited.value = true
pickerShow.value = true
lastCalendarValue.value = deepClone(calendarValue.value)
lastTab.value = currentTab.value
lastCurrentType.value = currentType.value
//
await pause()
scrollIntoView()
setTimeout(() => {
if (props.showTypeSwitch) {
calendarTabs.value.scrollIntoView()
calendarTabs.value.updateLineStyle(false)
}
}, 250)
emit('open')
}
//
function close() {
pickerShow.value = false
setTimeout(() => {
calendarValue.value = deepClone(lastCalendarValue.value)
currentTab.value = lastTab.value
currentType.value = lastCurrentType.value || 'date'
confirmBtnDisabled.value = getConfirmBtnStatus(lastCalendarValue.value)
}, 250)
emit('cancel')
}
function handleTypeChange({ index }: { index: number }) {
const tabs = ['date', 'week', 'month']
const rangeTabs = ['daterange', 'weekrange', 'monthrange']
const type = props.type.indexOf('range') > -1 ? rangeTabs[index] : tabs[index]
currentTab.value = index
currentType.value = type as CalendarType
}
function getConfirmBtnStatus(value: number | number[] | null) {
let confirmBtnDisabled = false
//
if (
(props.type.indexOf('range') > -1 && (!isArray(value) || !value[0] || !value[1] || !value)) ||
(props.type === 'dates' && (!isArray(value) || value.length === 0 || !value)) ||
!value
) {
confirmBtnDisabled = true
}
return confirmBtnDisabled
}
function handleChange({ value }: { value: number | number[] | null }) {
calendarValue.value = deepClone(value)
confirmBtnDisabled.value = getConfirmBtnStatus(value)
emit('change', {
value
})
if (!props.showConfirm && !confirmBtnDisabled.value) {
handleConfirm()
}
}
function handleConfirm() {
if (props.beforeConfirm) {
props.beforeConfirm({
value: calendarValue.value,
resolve: (isPass: boolean) => {
isPass && onConfirm()
}
})
} else {
onConfirm()
}
}
function onConfirm() {
pickerShow.value = false
lastCurrentType.value = currentType.value
emit('update:modelValue', calendarValue.value)
emit('confirm', {
value: calendarValue.value,
type: currentType.value
})
}
function handleShortcutClick(index: number) {
if (props.onShortcutsClick && typeof props.onShortcutsClick === 'function') {
calendarValue.value = deepClone(
props.onShortcutsClick({
item: props.shortcuts[index],
index
})
)
confirmBtnDisabled.value = getConfirmBtnStatus(calendarValue.value)
}
if (!props.showConfirm) {
handleConfirm()
}
}
defineExpose<CalendarExpose>({
close,
open
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,71 @@
@import "../common/abstracts/variable.scss";
@import "../common/abstracts/_mixin.scss";
.wot-theme-dark {
@include b(card) {
background-color: $-dark-background2;
@include when(rectangle) {
.wd-card__content {
@include halfPixelBorder('top', 0, $-dark-border-color);
}
.wd-card__footer {
@include halfPixelBorder('top', 0, $-dark-border-color);
}
}
@include e(title-content) {
color: $-dark-color;
}
@include e(content) {
color: $-dark-color3;
}
}
}
@include b(card) {
padding: $-card-padding;
background-color: $-card-bg;
line-height: $-card-line-height;
margin: $-card-margin;
border-radius: $-card-radius;
box-shadow: $-card-shadow-color;
font-size: $-card-fs;
margin-bottom: 12px;
@include when(rectangle) {
margin-left: 0;
margin-right: 0;
border-radius: 0;
box-shadow: none;
.wd-card__title-content {
font-size: $-card-fs;
}
.wd-card__content {
position: relative;
padding: $-card-rectangle-content-padding;
@include halfPixelBorder('top', 0, $-card-content-border-color);
}
.wd-card__footer {
position: relative;
padding: $-card-rectangle-footer-padding;
@include halfPixelBorder('top', 0, $-card-content-border-color);
}
}
@include e(title-content) {
padding: 16px 0;
color: $-card-title-color;
font-size: $-card-title-fs;
}
@include e(content) {
color: $-card-content-color;
line-height: $-card-content-line-height;
}
@include e(footer) {
padding: $-card-footer-padding;
text-align: right;
}
}

View File

@ -0,0 +1,30 @@
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeStringProp } from '../common/props'
export type CardType = 'rectangle'
export const cardProps = {
...baseProps,
/**
* 卡片类型
*/
type: String as PropType<CardType>,
/**
* 卡片标题
*/
title: String,
/**
* 标题自定义样式
*/
customTitleClass: makeStringProp(''),
/**
* 内容自定义样式
*/
customContentClass: makeStringProp(''),
/**
* 底部自定义样式
*/
customFooterClass: makeStringProp('')
}
export type CardProps = ExtractPropTypes<typeof cardProps>

View File

@ -0,0 +1,37 @@
<template>
<view :class="['wd-card', type == 'rectangle' ? 'is-rectangle' : '', customClass]" :style="customStyle">
<view :class="['wd-card__title-content', customTitleClass]" v-if="title || $slots.title">
<view class="wd-card__title">
<text v-if="title">{{ title }}</text>
<slot v-else name="title"></slot>
</view>
</view>
<view :class="`wd-card__content ${customContentClass}`">
<slot></slot>
</view>
<view :class="`wd-card__footer ${customFooterClass}`" v-if="$slots.footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-card',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { cardProps } from './types'
defineProps(cardProps)
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,56 @@
@import '../common/abstracts/variable.scss';
@import '../common/abstracts/_mixin.scss';
.wot-theme-dark {
@include b(cell-group) {
background-color: $-dark-background2;
@include when(border) {
.wd-cell-group__title {
@include halfPixelBorder('bottom', 0, $-dark-border-color);
}
}
@include e(title) {
background: $-dark-background2;
color: $-dark-color;
}
@include e(right) {
color: $-dark-color3;
}
@include e(body) {
background: $-dark-background2;
}
}
}
@include b(cell-group) {
background-color: $-color-white;
@include when(border) {
.wd-cell-group__title {
@include halfPixelBorder;
}
}
@include e(title) {
position: relative;
display: flex;
justify-content: space-between;
padding: $-cell-group-padding;
background: $-color-white;
font-size: $-cell-group-title-fs;
color: $-cell-group-title-color;
font-weight: $-fw-medium;
line-height: 1.43;
}
@include e(right) {
color: $-cell-group-value-color;
font-size: $-cell-group-value-fs;
}
@include e(body) {
background: $-color-white;
}
}

View File

@ -0,0 +1,41 @@
/*
* @Author: weisheng
* @Date: 2023-12-14 11:21:58
* @LastEditTime: 2024-03-18 13:57:14
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-cell-group\types.ts
* 记得注释
*/
import { type ExtractPropTypes, type InjectionKey } from 'vue'
import { baseProps, makeBooleanProp } from '../common/props'
export type CelllGroupProvide = {
props: {
border?: boolean
}
}
export const CELL_GROUP_KEY: InjectionKey<CelllGroupProvide> = Symbol('wd-cell-group')
export const cellGroupProps = {
...baseProps,
/**
* 分组标题
*/
title: String,
/**
* 分组右侧内容
*/
value: String,
/**
* 分组启用插槽
*/
useSlot: makeBooleanProp(false),
/**
* 是否展示边框线
*/
border: makeBooleanProp(false)
}
export type CellGroupProps = ExtractPropTypes<typeof cellGroupProps>

View File

@ -0,0 +1,45 @@
<template>
<view :class="['wd-cell-group', border ? 'is-border' : '', customClass]" :style="customStyle">
<view v-if="title || value || useSlot" class="wd-cell-group__title">
<!--左侧标题-->
<view class="wd-cell-group__left">
<text v-if="!$slots.title">{{ title }}</text>
<slot v-else name="title"></slot>
</view>
<!--右侧标题-->
<view class="wd-cell-group__right">
<text v-if="!$slots.value">{{ value }}</text>
<slot v-else name="value"></slot>
</view>
</view>
<view class="wd-cell-group__body">
<slot></slot>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-cell-group',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { useChildren } from '../composables/useChildren'
import { CELL_GROUP_KEY, cellGroupProps } from './types'
const props = defineProps(cellGroupProps)
const { linkChildren } = useChildren(CELL_GROUP_KEY)
linkChildren({ props })
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,188 @@
@import '../common/abstracts/variable.scss';
@import '../common/abstracts/_mixin.scss';
.wot-theme-dark {
@include b(cell) {
background-color: $-dark-background2;
color: $-dark-color;
@include e(value) {
color: $-dark-color3;
}
@include e(label) {
color: $-dark-color3;
}
@include when(hover) {
background-color: $-dark-background4;
}
@include when(border) {
.wd-cell__wrapper {
@include halfPixelBorder('top', 0, $-dark-border-color);
}
}
:deep(.wd-cell__arrow-right) {
color: $-dark-color;
}
}
}
@include b(cell) {
position: relative;
padding-left: $-cell-padding;
background-color: $-color-white;
text-decoration: none;
color: $-cell-title-color;
line-height: $-cell-line-height;
-webkit-tap-highlight-color: transparent;
@include when(border) {
.wd-cell__wrapper {
@include halfPixelBorder('top');
}
}
@include e(wrapper) {
position: relative;
display: flex;
padding: $-cell-wrapper-padding $-cell-padding $-cell-wrapper-padding 0;
justify-content: space-between;
align-items: flex-start;
@include when(vertical) {
display: block;
.wd-cell__right {
margin-top: $-cell-vertical-top;
}
.wd-cell__value {
text-align: left;
}
.wd-cell__left {
margin-right: 0;
}
}
@include when(label) {
padding: $-cell-wrapper-padding-with-label $-cell-padding $-cell-wrapper-padding-with-label 0;
}
}
@include e(left) {
position: relative;
flex: 1;
display: flex;
text-align: left;
font-size: $-cell-title-fs;
box-sizing: border-box;
margin-right: $-cell-padding;
@include when(required) {
padding-left: 12px;
&::after {
position: absolute;
content: '*';
top: 0;
left: 0;
font-size: $-cell-required-size;
color: $-cell-required-color;
}
}
}
@include e(right) {
position: relative;
flex: 1;
}
@include e(title) {
flex: 1;
width: 100%;
font-size: $-cell-title-fs;
}
@include e(label) {
margin-top: 2px;
font-size: $-cell-label-fs;
color: $-cell-label-color;
}
@include edeep(icon) {
display: block;
position: relative;
margin-right: $-cell-icon-right;
font-size: $-cell-icon-size;
height: $-cell-line-height;
line-height: $-cell-line-height;
}
@include e(body){
display: flex;
}
@include e(value) {
position: relative;
flex: 1;
font-size: $-cell-value-fs;
color: $-cell-value-color;
text-align: right;
vertical-align: middle;
}
@include edeep(arrow-right) {
display: block;
margin-left: 8px;
width: $-cell-arrow-size;
font-size: $-cell-arrow-size;
color: $-cell-arrow-color;
height: $-cell-line-height;
line-height: $-cell-line-height;
}
@include e(error-message){
color: $-form-item-error-message-color;
font-size: $-form-item-error-message-font-size;
line-height: $-form-item-error-message-line-height;
text-align: left;
vertical-align: middle;
}
@include when(link) {
-webkit-tap-highlight-color: $-cell-tap-bg;
}
@include when(hover) {
background-color: $-cell-tap-bg;
}
@include when(large) {
.wd-cell__title {
font-size: $-cell-title-fs-large;
}
.wd-cell__wrapper {
padding-top: $-cell-wrapper-padding-large;
padding-bottom: $-cell-wrapper-padding-large;
}
.wd-cell__label {
font-size: $-cell-label-fs-large;
}
:deep(.wd-cell__icon) {
font-size: $-cell-icon-size-large;
}
}
@include when(center) {
.wd-cell__wrapper {
align-items: center;
}
}
}

View File

@ -0,0 +1,90 @@
import type { ExtractPropTypes } from 'vue'
import { baseProps, makeArrayProp, makeBooleanProp, makeStringProp, makeNumericProp } from '../common/props'
import { type FormItemRule } from '../wd-form/types'
export const cellProps = {
...baseProps,
/**
* 标题
*/
title: String,
/**
* 右侧内容
*/
value: makeNumericProp(''),
/**
* 图标类名
*/
icon: String,
/**
* 描述信息
*/
label: String,
/**
* 是否为跳转链接
*/
isLink: makeBooleanProp(false),
/**
* 跳转地址
*/
to: String,
/**
* 跳转时是否替换栈顶页面
*/
replace: makeBooleanProp(false),
/**
* 开启点击反馈is-link 默认开启
*/
clickable: makeBooleanProp(false),
/**
* 设置单元格大小可选值large
*/
size: String,
/**
* 是否展示边框线
*/
border: makeBooleanProp(void 0),
/**
* 设置左侧标题宽度
*/
titleWidth: String,
/**
* 是否垂直居中默认顶部居中
*/
center: makeBooleanProp(false),
/**
* 是否必填
*/
required: makeBooleanProp(false),
/**
* 表单属性上下结构
*/
vertical: makeBooleanProp(false),
/**
* 表单域 model 字段名在使用表单校验功能的情况下该属性是必填的
*/
prop: String,
/**
* 表单验证规则结合wd-form组件使用
*/
rules: makeArrayProp<FormItemRule>(),
/**
* icon 使用 slot 时的自定义样式
*/
customIconClass: makeStringProp(''),
/**
* label 使用 slot 时的自定义样式
*/
customLabelClass: makeStringProp(''),
/**
* value 使用 slot 时的自定义样式
*/
customValueClass: makeStringProp(''),
/**
* title 使用 slot 时的自定义样式
*/
customTitleClass: makeStringProp('')
}
export type CellProps = ExtractPropTypes<typeof cellProps>

View File

@ -0,0 +1,122 @@
<template>
<view
:class="['wd-cell', isBorder ? 'is-border' : '', size ? 'is-' + size : '', center ? 'is-center' : '', customClass]"
:style="customStyle"
:hover-class="isLink || clickable ? 'is-hover' : 'none'"
:hover-stay-time="70"
@click="onClick"
>
<view :class="['wd-cell__wrapper', vertical ? 'is-vertical' : '']">
<view
:class="['wd-cell__left', isRequired ? 'is-required' : '']"
:style="titleWidth ? 'min-width:' + titleWidth + ';max-width:' + titleWidth + ';' : ''"
>
<!--左侧icon部位-->
<wd-icon v-if="icon" :name="icon" :custom-class="`wd-cell__icon ${customIconClass}`"></wd-icon>
<slot v-else name="icon" />
<view class="wd-cell__title">
<!--title BEGIN-->
<view v-if="title" :class="customTitleClass">{{ title }}</view>
<slot v-else name="title"></slot>
<!--title END-->
<!--label BEGIN-->
<view v-if="label" :class="`wd-cell__label ${customLabelClass}`">{{ label }}</view>
<slot v-else name="label" />
<!--label END-->
</view>
</view>
<!--right content BEGIN-->
<view class="wd-cell__right">
<view class="wd-cell__body">
<!--文案内容-->
<view :class="`wd-cell__value ${customValueClass}`">
<slot>{{ value }}</slot>
</view>
<!--箭头-->
<wd-icon v-if="isLink" custom-class="wd-cell__arrow-right" name="arrow-right" />
<slot v-else name="right-icon" />
</view>
<view v-if="errorMessage" class="wd-cell__error-message">{{ errorMessage }}</view>
</view>
<!--right content END-->
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-cell',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed } from 'vue'
import { useCell } from '../composables/useCell'
import { useParent } from '../composables/useParent'
import { FORM_KEY } from '../wd-form/types'
import { cellProps } from './types'
import { isDef } from '../common/util'
const props = defineProps(cellProps)
const emit = defineEmits(['click'])
const cell = useCell()
const isBorder = computed(() => {
return Boolean(isDef(props.border) ? props.border : cell.border.value)
})
const { parent: form } = useParent(FORM_KEY)
const errorMessage = computed(() => {
if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
return form.errorMessages[props.prop]
} else {
return ''
}
})
//
const isRequired = computed(() => {
let formRequired = false
if (form && form.props.rules) {
const rules = form.props.rules
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
formRequired = rules[key].some((rule) => rule.required)
}
}
}
return props.required || props.rules.some((rule) => rule.required) || formRequired
})
/**
* @description 点击cell的handle
*/
function onClick() {
const url = props.to
if (props.clickable || props.isLink) {
emit('click')
}
if (url && props.isLink) {
if (props.replace) {
uni.redirectTo({ url })
} else {
uni.navigateTo({ url })
}
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,20 @@
@import "./../common/abstracts/_mixin.scss";
@import "./../common/abstracts/variable.scss";
.wot-theme-dark {
@include b(checkbox-group) {
background-color: $-dark-background2;
}
}
@include b(checkbox-group) {
background-color: $-checkbox-bg;
// 上下20px 左右15px 内部间隔12px
@include when(button) {
width: 100%;
padding: 8px 3px 20px 15px;
box-sizing: border-box;
overflow: hidden;
height: auto;
}
}

View File

@ -0,0 +1,59 @@
import { type ExtractPropTypes, type InjectionKey, type PropType } from 'vue'
import type { CheckShape } from '../wd-checkbox/types'
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
export type RequiredModelValue = {
modelValue: Array<string | number | boolean>
}
export type checkboxGroupProvide = {
props: Partial<Omit<CheckboxGroupProps, 'modelValue'>> & RequiredModelValue
changeSelectState: (value: string | number | boolean) => void
}
export const CHECKBOX_GROUP_KEY: InjectionKey<checkboxGroupProvide> = Symbol('wd-checkbox-group')
export const checkboxGroupProps = {
...baseProps,
/**
* 绑定值
*/
modelValue: {
type: Array as PropType<Array<string | number | boolean>>,
default: () => []
},
/**
* 表单模式
*/
cell: makeBooleanProp(false),
/**
* 单选框形状可选值circle / square / button
*/
shape: makeStringProp<CheckShape>('circle'),
/**
* 选中的颜色
*/
checkedColor: String,
/**
* 禁用
*/
disabled: makeBooleanProp(false),
/**
* 最小选中的数量
*/
min: makeNumberProp(0),
/**
* 最大选中的数量0 为无限数量默认为 0
*/
max: makeNumberProp(0),
/**
* 同行展示
*/
inline: makeBooleanProp(false),
/**
* 设置大小可选值large
*/
size: String
}
export type CheckboxGroupProps = ExtractPropTypes<typeof checkboxGroupProps>

View File

@ -0,0 +1,100 @@
<template>
<view :class="`wd-checkbox-group ${shape === 'button' && cell ? 'is-button' : ''} ${customClass}`" :style="customStyle">
<slot />
</view>
</template>
<script lang="ts">
export default {
name: 'wd-checkbox-group',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { watch } from 'vue'
import { checkNumRange, deepClone } from '../common/util'
import { useChildren } from '../composables/useChildren'
import { CHECKBOX_GROUP_KEY, checkboxGroupProps } from './types'
const props = defineProps(checkboxGroupProps)
const emit = defineEmits(['change', 'update:modelValue'])
const { linkChildren } = useChildren(CHECKBOX_GROUP_KEY)
linkChildren({ props, changeSelectState })
watch(
() => props.modelValue,
(newValue) => {
// value
if (new Set(newValue).size !== newValue.length) {
// eslint-disable-next-line quotes
console.error("checkboxGroup's bound value includes same value")
}
if (newValue.length < props.min) {
// eslint-disable-next-line quotes
console.error("checkboxGroup's bound value's length can't be less than min")
}
if (props.max !== 0 && newValue.length > props.max) {
// eslint-disable-next-line quotes
console.error("checkboxGroup's bound value's length can't be large than max")
}
// value
},
{ deep: true, immediate: true }
)
watch(
() => props.shape,
(newValue) => {
const type = ['circle', 'square', 'button']
if (type.indexOf(newValue) === -1) console.error(`shape must be one of ${type.toString()}`)
},
{ deep: true, immediate: true }
)
watch(
() => props.min,
(newValue) => {
checkNumRange(newValue, 'min')
},
{ deep: true, immediate: true }
)
watch(
() => props.max,
(newValue) => {
checkNumRange(newValue, 'max')
},
{ deep: true, immediate: true }
)
/**
* @description 子节点通知父节点修改子节点选中状态
* @param {any} value 子组件的标识符
*/
function changeSelectState(value: string | number | boolean) {
const temp: (string | number | boolean)[] = deepClone(props.modelValue)
const index = temp.indexOf(value)
if (index > -1) {
// value
temp.splice(index, 1)
} else {
// value
temp.push(value)
}
emit('update:modelValue', temp)
emit('change', {
value: temp
})
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,285 @@
@import "./../common/abstracts/_mixin.scss";
@import "./../common/abstracts/variable.scss";
.wot-theme-dark {
@include b(checkbox) {
@include e(shape) {
background: transparent;
border-color: $-checkbox-border-color;
color: $-checkbox-check-color;
}
@include e(label) {
color: $-dark-color;
}
@include when(disabled) {
.wd-checkbox__shape {
border-color: $-dark-color-gray;
background: $-dark-background4;
}
.wd-checkbox__label {
color: $-dark-color-gray;
}
:deep(.wd-checkbox__check) {
color: $-dark-color-gray;
}
@include when(checked) {
.wd-checkbox__shape {
color: $-dark-color-gray;
}
.wd-checkbox__label {
color: $-dark-color-gray;
}
}
@include when(button) {
.wd-checkbox__label {
border-color: #c8c9cc;
background: #3a3a3c;
color: $-dark-color-gray;
}
@include when(checked) {
.wd-checkbox__label {
border-color: #c8c9cc;
background: #3a3a3c;
color: #c8c9cc;
}
}
}
}
@include when(button) {
.wd-checkbox__label {
background-color: $-dark-background;
}
@include when(checked) {
.wd-checkbox__label {
background-color: $-dark-background2;
}
}
}
}
}
@include b(checkbox) {
display: block;
margin-bottom: $-checkbox-margin;
font-size: 0;
-webkit-tap-highlight-color: transparent;
line-height: 1.2;
@include when(last-child) {
margin-bottom: 0;
}
@include e(shape) {
position: relative;
display: inline-block;
width: $-checkbox-size;
height: $-checkbox-size;
border: 2px solid $-checkbox-border-color;
border-radius: 50%;
color: $-checkbox-check-color;
background: $-checkbox-bg;
vertical-align: middle;
transition: background 0.2s;
box-sizing: border-box;
@include when(square) {
border-radius: $-checkbox-square-radius;
}
}
@include e(input) {
position: absolute;
width: 0;
height: 0;
margin: 0;
opacity: 0;
}
@include edeep(btn-check) {
display: inline-block;
font-size: $-checkbox-icon-size;
margin-right: 4px;
vertical-align: middle;
}
@include e(txt) {
display: inline-block;
vertical-align: middle;
line-height: 20px;
@include lineEllipsis;
}
@include e(label) {
position: relative;
display: inline-block;
margin-left: $-checkbox-label-margin;
vertical-align: middle;
font-size: $-checkbox-label-fs;
color: $-checkbox-label-color;
}
@include edeep(check) {
color: $-checkbox-check-color;
font-size: $-checkbox-icon-size;
opacity: 0;
transition: opacity 0.2s;
}
@include when(checked) {
.wd-checkbox__shape {
color: $-checkbox-checked-color;
background: currentColor;
border-color: currentColor;
}
:deep(.wd-checkbox__check) {
opacity: 1;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
@include when(button) {
display: inline-block;
margin-bottom: 0;
margin-right: $-checkbox-margin;
vertical-align: top;
font-size: $-checkbox-button-font-size;
@include when(last-child) {
margin-right: 0;
}
.wd-checkbox__shape {
width: 0;
height: 0;
overflow: hidden;
opacity: 0;
border: none;
}
.wd-checkbox__label {
display: inline-flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: $-checkbox-button-min-width;
height: $-checkbox-button-height;
font-size: $-checkbox-button-font-size;
margin-left: 0;
padding: 5px 15px;
border: 1px solid $-checkbox-button-border;
background-color: $-checkbox-button-bg;
border-radius: $-checkbox-button-radius;
transition: color 0.2s, border 0.2s;
box-sizing: border-box;
}
@include when(checked) {
.wd-checkbox__label {
color: $-checkbox-checked-color;
background-color: $-checkbox-bg;
border-color: $-checkbox-checked-color;
border-color: currentColor;
}
}
}
@include when(inline) {
display: inline-block;
margin-bottom: 0;
margin-right: $-checkbox-margin;
@include when(last-child) {
margin-right: 0;
}
}
@include when(disabled) {
.wd-checkbox__shape {
border-color: $-checkbox-border-color;
background: $-checkbox-disabled-check-bg;
}
.wd-checkbox__label {
color: $-checkbox-disabled-label-color;
}
@include when(checked) {
.wd-checkbox__shape {
color: $-checkbox-disabled-check-color;
}
.wd-checkbox__label {
color: $-checkbox-disabled-label-color;
}
}
@include when(button) {
.wd-checkbox__label {
background: $-checkbox-disabled-color;
border-color: $-checkbox-button-border;
color: $-checkbox-disabled-label-color;
}
@include when(checked) {
.wd-checkbox__label {
border-color: $-checkbox-button-disabled-border;
}
}
}
}
// 以下内容用于解决父子组件样式隔离的问题 START
@include when(cell-box) {
padding: 13px 15px;
margin: 0;
@include when(large) {
padding: 14px 15px;
}
}
@include when(button-box) {
display: inline-flex;
width: 33.3333%;
padding: 12px 12px 0 0;
box-sizing: border-box;
.wd-checkbox__label {
width: 100%;
}
&:last-child::after {
content: "";
display: table;
clear: both;
}
}
@include when(large) {
.wd-checkbox__shape {
width: $-checkbox-large-size;
height: $-checkbox-large-size;
font-size: $-checkbox-large-size;
}
.wd-checkbox__label {
font-size: $-checkbox-large-label-fs;
}
}
}

View File

@ -0,0 +1,68 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeStringProp } from '../common/props'
export type CheckShape = 'circle' | 'square' | 'button'
export const checkboxProps = {
...baseProps,
customLabelClass: makeStringProp(''),
customShapeClass: makeStringProp(''),
/**
* 单选框选中时的值
*/
modelValue: {
type: [String, Number, Boolean],
required: true,
default: false
},
/**
* 单选框形状可选值circle / square / button
*/
shape: {
type: String as PropType<CheckShape>
},
/**
* 选中的颜色
*/
checkedColor: String,
/**
* 禁用
*/
disabled: {
type: [Boolean, null] as PropType<boolean | null>,
default: null
},
/**
* 选中值 checkbox-group 中使用无效需同 false-value 一块使用
*/
trueValue: {
type: [String, Number, Boolean],
default: true
},
/**
* 非选中时的值 checkbox-group 中使用无效需同 true-value 一块使用
*/
falseValue: {
type: [String, Number, Boolean],
default: false
},
/**
* 设置大小可选值large
*/
size: String,
/**
* 文字位置最大宽度
*/
maxWidth: String
}
export type CheckboxProps = ExtractPropTypes<typeof checkboxProps>
export type CheckboxExpose = {
/**
* 切换当前选中状态
*/
toggle: () => void
}
export type CheckboxInstance = ComponentPublicInstance<CheckboxProps, CheckboxExpose>

View File

@ -0,0 +1,177 @@
<template>
<view
:class="`wd-checkbox ${innerCell ? 'is-cell-box' : ''} ${innerShape === 'button' ? 'is-button-box' : ''} ${isChecked ? 'is-checked' : ''} ${
isFirst ? 'is-first-child' : ''
} ${isLast ? 'is-last-child' : ''} ${innerInline ? 'is-inline' : ''} ${innerShape === 'button' ? 'is-button' : ''} ${
innerDisabled ? 'is-disabled' : ''
} ${innerSize ? 'is-' + innerSize : ''} ${customClass}`"
:style="customStyle"
@click="toggle"
>
<!--shape为button时移除wd-checkbox__shape只保留wd-checkbox__label-->
<view
v-if="innerShape !== 'button'"
:class="`wd-checkbox__shape ${innerShape === 'square' ? 'is-square' : ''} ${customShapeClass}`"
:style="isChecked && !innerDisabled && innerCheckedColor ? 'color :' + innerCheckedColor : ''"
>
<wd-icon custom-class="wd-checkbox__check" name="check-bold" />
</view>
<!--shape为button时只保留wd-checkbox__label-->
<view
:class="`wd-checkbox__label ${customLabelClass}`"
:style="isChecked && innerShape === 'button' && !innerDisabled && innerCheckedColor ? 'color:' + innerCheckedColor : ''"
>
<!--button选中时展示的icon-->
<wd-icon v-if="innerShape === 'button' && isChecked" custom-class="wd-checkbox__btn-check" name="check-bold" />
<!--文案-->
<view class="wd-checkbox__txt" :style="maxWidth ? 'max-width:' + maxWidth : ''">
<slot></slot>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-checkbox',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed, getCurrentInstance, onBeforeMount, watch } from 'vue'
import { useParent } from '../composables/useParent'
import { CHECKBOX_GROUP_KEY } from '../wd-checkbox-group/types'
import { getPropByPath, isDef } from '../common/util'
import { checkboxProps, type CheckboxExpose } from './types'
const props = defineProps(checkboxProps)
const emit = defineEmits(['change', 'update:modelValue'])
defineExpose<CheckboxExpose>({
toggle
})
const { parent: checkboxGroup, index } = useParent(CHECKBOX_GROUP_KEY)
const isChecked = computed(() => {
if (checkboxGroup) {
return checkboxGroup.props.modelValue.indexOf(props.modelValue) > -1
} else {
return props.modelValue === props.trueValue
}
}) //
const isFirst = computed(() => {
return index.value === 0
})
const isLast = computed(() => {
const children = isDef(checkboxGroup) ? checkboxGroup.children : []
return index.value === children.length - 1
})
const { proxy } = getCurrentInstance() as any
watch(
() => props.modelValue,
() => {
// 使
if (checkboxGroup) {
checkName()
}
}
)
watch(
() => props.shape,
(newValue) => {
const type = ['circle', 'square', 'button']
if (isDef(newValue) && type.indexOf(newValue) === -1) console.error(`shape must be one of ${type.toString()}`)
}
)
const innerShape = computed(() => {
return props.shape || getPropByPath(checkboxGroup, 'props.shape') || 'circle'
})
const innerCheckedColor = computed(() => {
return props.checkedColor || getPropByPath(checkboxGroup, 'props.checkedColor')
})
const innerDisabled = computed(() => {
if (!checkboxGroup) {
return props.disabled
}
const { max, min, modelValue, disabled } = checkboxGroup.props
if (
(max && modelValue.length >= max && !isChecked.value) ||
(min && modelValue.length <= min && isChecked.value) ||
props.disabled === true ||
(disabled && props.disabled === null)
) {
return true
}
return props.disabled
})
const innerInline = computed(() => {
return getPropByPath(checkboxGroup, 'props.inline') || false
})
const innerCell = computed(() => {
return getPropByPath(checkboxGroup, 'props.cell') || false
})
const innerSize = computed(() => {
return props.size || getPropByPath(checkboxGroup, 'props.size')
})
onBeforeMount(() => {
// eslint-disable-next-line quotes
if (props.modelValue === null) console.error("checkbox's value must be set")
})
/**
* @description 检测checkbox绑定的value是否和其它checkbox的value冲突
* @param {Object} self 自身
* @param myName 自己的标识符
*/
function checkName() {
checkboxGroup &&
checkboxGroup.children &&
checkboxGroup.children.forEach((child: any) => {
if (child.$.uid !== proxy.$.uid && child.modelValue === props.modelValue) {
console.error(`The checkbox's bound value: ${props.modelValue} has been used`)
}
})
}
/**
* @description 点击checkbox的Event handle
*/
function toggle() {
if (innerDisabled.value) return
// 使checkboxchange
if (checkboxGroup) {
emit('change', {
value: !isChecked.value
})
checkboxGroup.changeSelectState(props.modelValue)
} else {
const newVal = props.modelValue === props.trueValue ? props.falseValue : props.trueValue
emit('update:modelValue', newVal)
emit('change', {
value: newVal
})
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,18 @@
@import "../common/abstracts/variable.scss";
@import "../common/abstracts/_mixin.scss";
@include b(circle) {
position: relative;
display: inline-block;
text-align: center;
@include e(text) {
position: absolute;
z-index: 1;
top: 50%;
left: 0;
width: 100%;
transform: translateY(-50%);
color: $-circle-text-color;
}
}

View File

@ -0,0 +1,54 @@
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
// "butt" | "round" | "square"
export type StrokeLinecapType = 'butt' | 'round' | 'square'
export const circleProps = {
...baseProps,
/**
* 当前进度
*/
modelValue: makeNumberProp(0),
/**
* 圆环直径默认单位为 px
*/
size: makeNumberProp(100),
/**
* 进度条颜色传入对象格式可以定义渐变色
*/
color: {
type: [String, Object] as PropType<string | Record<string, string>>,
default: '#4d80f0'
},
/**
* 轨道颜色
*/
layerColor: makeStringProp('#EBEEF5'),
/**
* 填充颜色
*/
fill: String,
/**
* 动画速度单位为 rate/s
*/
speed: makeNumberProp(50),
/**
* 文字
*/
text: String,
/**
* 进度条宽度 单位px
*/
strokeWidth: makeNumberProp(10),
/**
* 进度条端点的形状可选值为 "butt" | "round" | "square"
*/
strokeLinecap: makeStringProp<StrokeLinecapType>('round'),
/**
* 是否顺时针增加
*/
clockwise: makeBooleanProp(true)
}
export type CircleProps = ExtractPropTypes<typeof circleProps>

View File

@ -0,0 +1,296 @@
<template>
<view :class="`wd-circle ${customClass}`" :style="customStyle">
<!-- #ifdef MP-WEIXIN -->
<canvas :style="canvasStyle" :id="canvasId" :canvas-id="canvasId" type="2d"></canvas>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas :width="canvasSize" :height="canvasSize" :style="canvasStyle" :id="canvasId" :canvas-id="canvasId"></canvas>
<!-- #endif -->
<view v-if="!text" class="wd-circle__text">
<!-- 自定义提示内容 -->
<slot></slot>
</view>
<text v-else class="wd-circle__text">
{{ text }}
</text>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-circle',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
import { addUnit, isObj, objToStyle, uuid } from '../common/util'
import { circleProps } from './types'
// #ifdef MP-WEIXIN
import { canvas2dAdapter } from '../common/canvasHelper'
// #endif
// 0100
function format(rate: number) {
return Math.min(Math.max(rate, 0), 100)
}
//
const PERIMETER = 2 * Math.PI
//
const BEGIN_ANGLE = -Math.PI / 2
const STEP = 1
const props = defineProps(circleProps)
const { proxy } = getCurrentInstance() as any
const progressColor = ref<string | CanvasGradient>('') //
const currentValue = ref<number>(0) //
const interval = ref<any>(null) //
const pixelRatio = ref<number>(1) //
const canvasId = ref<string>(`wd-circle${uuid()}`) // canvasId
let ctx: UniApp.CanvasContext | null = null
// canvas
const canvasSize = computed(() => {
let size = props.size
// #ifdef MP-ALIPAY
size = size * pixelRatio.value
// #endif
return size
})
//
const sWidth = computed(() => {
let sWidth = props.strokeWidth
// #ifdef MP-ALIPAY
sWidth = sWidth * pixelRatio.value
// #endif
return sWidth
})
// Circle
const canvasStyle = computed(() => {
const style = {
width: addUnit(props.size),
height: addUnit(props.size)
}
return `${objToStyle(style)}`
})
//
watch(
() => props.modelValue,
() => {
reRender()
},
{ immediate: true }
)
// Circle
watch(
() => props.size,
() => {
let timer = setTimeout(() => {
drawCircle(currentValue.value)
clearTimeout(timer)
}, 50)
},
{ immediate: false }
)
//
watch(
() => props.color,
() => {
drawCircle(currentValue.value)
},
{ immediate: false, deep: true }
)
onBeforeMount(() => {
pixelRatio.value = uni.getSystemInfoSync().pixelRatio
})
onMounted(() => {
currentValue.value = props.modelValue
drawCircle(currentValue.value)
})
onUnmounted(() => {
clearTimeInterval()
})
/**
* 获取canvas上下文
*/
function getContext() {
return new Promise<UniApp.CanvasContext>((resolve) => {
if (ctx) {
return resolve(ctx)
}
// #ifndef MP-WEIXIN
ctx = uni.createCanvasContext(canvasId.value, proxy)
resolve(ctx)
// #endif
// #ifdef MP-WEIXIN
uni
.createSelectorQuery()
.in(proxy)
.select(`#${canvasId.value}`)
.node((res) => {
if (res && res.node) {
const canvas = res.node
ctx = canvas2dAdapter(canvas.getContext('2d') as CanvasRenderingContext2D)
canvas.width = props.size * pixelRatio.value
canvas.height = props.size * pixelRatio.value
ctx.scale(pixelRatio.value, pixelRatio.value)
resolve(ctx)
}
})
.exec()
// #endif
})
}
/**
* 设置canvas
*/
function presetCanvas(context: any, strokeStyle: string | CanvasGradient, beginAngle: number, endAngle: number, fill?: string) {
let width = sWidth.value
const position = canvasSize.value / 2
if (!fill) {
width = width / 2
}
const radius = position - width / 2
context.strokeStyle = strokeStyle
context.setStrokeStyle(strokeStyle)
context.setLineWidth(width)
context.setLineCap(props.strokeLinecap)
context.beginPath()
context.arc(position, position, radius, beginAngle, endAngle, !props.clockwise)
context.stroke()
if (fill) {
context.setLineWidth(width)
context.setFillStyle(fill)
context.fill()
}
}
/**
* 渲染管道
*/
function renderLayerCircle(context: UniApp.CanvasContext) {
presetCanvas(context, props.layerColor, 0, PERIMETER, props.fill)
}
/**
* 渲染进度条
*/
function renderHoverCircle(context: UniApp.CanvasContext, formatValue: number) {
//
const progress = PERIMETER * (formatValue / 100)
const endAngle = props.clockwise ? BEGIN_ANGLE + progress : 3 * Math.PI - (BEGIN_ANGLE + progress)
//
if (isObj(props.color)) {
const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
progressColor.value = LinearColor
} else {
progressColor.value = props.color
}
presetCanvas(context, progressColor.value, BEGIN_ANGLE, endAngle)
}
/**
* 渲染圆点
* 进度值为0时渲染一个圆点
*/
function renderDot(context: UniApp.CanvasContext) {
const strokeWidth = sWidth.value // =
const position = canvasSize.value / 2 //
//
if (isObj(props.color)) {
const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
progressColor.value = LinearColor
} else {
progressColor.value = props.color
}
context.beginPath()
context.arc(position, strokeWidth / 4, strokeWidth / 4, 0, PERIMETER)
context.setFillStyle(progressColor.value)
context.fill()
}
/**
* 画圆
*/
function drawCircle(currentValue: number) {
getContext().then((context) => {
context.clearRect(0, 0, canvasSize.value, canvasSize.value)
renderLayerCircle(context)
const formatValue = format(currentValue)
if (formatValue !== 0) {
renderHoverCircle(context, formatValue)
} else {
renderDot(context)
}
context.draw()
})
}
/**
* Circle组件渲染
* 当前进度值变化时重新渲染Circle组件
*/
function reRender() {
//
if (props.speed <= 0 || props.speed > 1000) {
drawCircle(props.modelValue)
return
}
clearTimeInterval()
currentValue.value = currentValue.value || 0
const run = () => {
interval.value = setTimeout(() => {
if (currentValue.value !== props.modelValue) {
if (Math.abs(currentValue.value - props.modelValue) < STEP) {
currentValue.value = props.modelValue
} else if (currentValue.value < props.modelValue) {
currentValue.value += STEP
} else {
currentValue.value -= STEP
}
drawCircle(currentValue.value)
run()
} else {
clearTimeInterval()
}
}, 1000 / props.speed)
}
run()
}
/**
* 清除定时器
*/
function clearTimeInterval() {
interval.value && clearTimeout(interval.value)
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,243 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(col-picker) {
@include when(border) {
.wd-col-picker__cell {
@include halfPixelBorder('top', $-cell-padding, $-dark-border-color);
}
}
@include e(label) {
color: $-dark-color;
}
@include e(cell) {
background-color: $-dark-background2;
color: $-dark-color;
@include when(disabled) {
.wd-col-picker__value {
color: $-dark-color3;
}
}
}
@include e(list-item) {
@include when(disabled) {
color: $-dark-color3;
}
}
@include e(list-item-tip) {
color: $-dark-color-gray;
}
@include e(value) {
color: $-dark-color;
@include m(placeholder) {
color: $-dark-color-gray;
}
}
:deep(.wd-col-picker__arrow) {
color: $-dark-color;
}
@include e(list) {
color: $-dark-color;
}
@include e(selected) {
color: $-dark-color;
}
}
}
@include b(col-picker) {
@include when(border) {
.wd-col-picker__cell {
@include halfPixelBorder('top', $-cell-padding);
}
}
@include e(cell) {
position: relative;
display: flex;
padding: $-cell-wrapper-padding $-cell-padding;
align-items: flex-start;
background-color: $-color-white;
text-decoration: none;
color: $-cell-title-color;
font-size: $-cell-title-fs;
overflow: hidden;
line-height: $-cell-line-height;
}
@include e(cell) {
@include when(disabled) {
.wd-col-picker__value {
color: $-input-disabled-color;
}
}
@include when(align-right) {
.wd-col-picker__value {
text-align: right;
}
}
@include when(error) {
.wd-col-picker__value {
color: $-input-error-color;
}
:deep(.wd-col-picker__arrow) {
color: $-input-error-color;
}
}
@include when(large) {
font-size: $-cell-title-fs-large;
:deep(.wd-col-picker__arrow) {
font-size: $-cell-icon-size-large;
}
}
}
@include e(error-message){
color: $-form-item-error-message-color;
font-size: $-form-item-error-message-font-size;
line-height: $-form-item-error-message-line-height;
text-align: left;
vertical-align: middle;
}
@include e(label) {
position: relative;
width: $-input-cell-label-width;
margin-right: $-cell-padding;
color: $-cell-title-color;
box-sizing: border-box;
@include when(required) {
padding-left: 12px;
&::after {
position: absolute;
left: 0;
top: 2px;
content: '*';
font-size: $-cell-required-size;
line-height: 1.1;
color: $-cell-required-color;
}
}
}
@include e(value-wraper) {
display: flex;
}
@include e(value) {
flex: 1;
margin-right: 10px;
color: $-cell-value-color;
@include when(ellipsis) {
@include lineEllipsis;
}
@include m(placeholder) {
color: $-input-placeholder-color;
}
}
@include e(body) {
flex: 1;
}
@include edeep(arrow) {
display: block;
font-size: $-cell-icon-size;
color: $-cell-arrow-color;
line-height: $-cell-line-height;
}
@include e(selected) {
height: $-col-picker-selected-height;
font-size: $-col-picker-selected-fs;
color: $-col-picker-selected-color;
overflow: hidden;
}
@include e(selected-container){
position: relative;
display: flex;
user-select: none;
}
@include e(selected-item) {
flex: 0 0 auto;
height: $-col-picker-selected-height;
line-height: $-col-picker-selected-height;
padding: $-col-picker-selected-padding;
@include when(selected) {
font-weight: $-col-picker-selected-fw;
}
}
@include e(selected-line) {
position: absolute;
bottom: 5px;
width: $-col-picker-line-width;
left: 0;
height: $-col-picker-line-height;
background: $-col-picker-line-color;
z-index: 1;
border-radius: calc($-col-picker-line-height / 2);
box-shadow: $-col-picker-line-box-shadow;
}
@include e(list-container){
position: relative;
}
@include e(list) {
height: $-col-picker-list-height;
padding-bottom: $-col-picker-list-padding-bottom;
box-sizing: border-box;
overflow: auto;
color: $-col-picker-list-color;
font-size: $-col-picker-list-fs;
-webkit-overflow-scrolling: touch;
}
@include e(list-item) {
display: flex;
padding: $-col-picker-list-item-padding;
align-items: flex-start;
@include when(selected) {
color: $-col-picker-list-color-checked;
:deep(.wd-col-picker__checked) {
opacity: 1;
}
}
@include when(disabled) {
color: $-col-picker-list-color-disabled;
}
}
@include e(list-item-label) {
line-height: 1.285;
}
@include e(list-item-tip) {
margin-top: 2px;
font-size: $-col-picker-list-fs-tip;
color: $-col-picker-list-color-tip;
}
@include edeep(checked) {
display: block;
margin-left: 4px;
font-size: $-col-picker-list-checked-icon-size;
color: $-col-picker-list-color-checked;
opacity: 0;
}
@include e(loading) {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
}

View File

@ -0,0 +1,158 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeRequiredProp, makeStringProp, numericProp } from '../common/props'
import type { FormItemRule } from '../wd-form/types'
export const colPickerProps = {
...baseProps,
/**
* 选中项
*/
modelValue: makeRequiredProp(Array as PropType<Array<string | number>>),
/**
* 选择器数据二维数组
*/
columns: makeArrayProp<Record<string, any>[]>(),
/**
* 选择器左侧文案
*/
label: String,
/**
* 设置左侧标题宽度
*/
labelWidth: makeStringProp('33%'),
/**
* 使用 label 插槽时设置该选项
*/
useLabelSlot: makeBooleanProp(false),
/**
* 使用默认插槽时设置该选项
*/
useDefaultSlot: makeBooleanProp(false),
/**
* 禁用
*/
disabled: makeBooleanProp(false),
/**
* 只读
*/
readonly: makeBooleanProp(false),
/**
* 选择器占位符
*/
placeholder: String,
/**
* 弹出层标题
*/
title: String,
/**
* 接收当前列的选中项 item当前列下标当前列选中项下标下一列数据处理函数 resolve结束选择 finish
*/
columnChange: Function as PropType<ColPickerColumnChange>,
/**
* 自定义展示文案的格式化函数返回一个字符串
*/
displayFormat: Function as PropType<ColPickerDisplayFormat>,
/**
* 确定前校验函数接收 (value, resolve) 参数通过 resolve 继续执行 pickerresolve 接收 1 boolean 参数
*/
beforeConfirm: Function as PropType<ColPickerBeforeConfirm>,
/**
* 选择器的值靠右展示
*/
alignRight: makeBooleanProp(false),
/**
* 是否为错误状态错误状态时右侧内容为红色
*/
error: makeBooleanProp(false),
/**
* 是否必填
*/
required: makeBooleanProp(false),
/**
* 设置选择器大小可选值large
*/
size: String,
/**
* 选项对象中value 对应的 key
*/
valueKey: makeStringProp('value'),
/**
* 选项对象中展示的文本对应的 key
*/
labelKey: makeStringProp('label'),
/**
* 选项对象中提示文案对应的 key
*/
tipKey: makeStringProp('tip'),
/**
* loading 图标的颜色
*/
loadingColor: makeStringProp('#4D80F0'),
/**
* 点击遮罩是否关闭
*/
closeOnClickModal: makeBooleanProp(true),
/**
* 自动触发 column-change 事件来补全数据 columns 为空数组或者 columns 数组长度小于 value 数组长度时会自动触发 column-change
*/
autoComplete: makeBooleanProp(false),
/**
* 弹窗层级
*/
zIndex: makeNumberProp(15),
/**
* 弹出面板是否设置底部安全距离iphone X 类型的机型
*/
safeAreaInsetBottom: makeBooleanProp(true),
/**
* 是否超出隐藏
*/
ellipsis: makeBooleanProp(false),
/**
* 表单域 model 字段名在使用表单校验功能的情况下该属性是必填的
*/
prop: String,
/**
* 表单验证规则结合wd-form组件使用
*/
rules: makeArrayProp<FormItemRule>(),
/**
* 底部条宽度单位像素
*/
lineWidth: numericProp,
/**
* 底部条高度单位像素
*/
lineHeight: numericProp,
/**
* label 外部自定义样式
*/
customViewClass: makeStringProp(''),
/**
* value 外部自定义样式
*/
customLabelClass: makeStringProp(''),
customValueClass: makeStringProp('')
}
export type ColPickerProps = ExtractPropTypes<typeof colPickerProps>
export type ColPickerColumnChangeOption = {
selectedItem: Record<string, any>
index: number
rowIndex: number
resolve: (nextColumn: Record<string, any>[]) => void
finish: (isOk?: boolean) => void
}
export type ColPickerColumnChange = (option: ColPickerColumnChangeOption) => void
export type ColPickerDisplayFormat = (selectedItems: Record<string, any>[]) => string
export type ColPickerBeforeConfirm = (value: (string | number)[], selectedItems: Record<string, any>[], resolve: (isPass: boolean) => void) => void
export type ColPickerExpose = {
// picker
close: () => void
// picker
open: () => void
}
export type ColPickerInstance = ComponentPublicInstance<ColPickerExpose, ColPickerProps>

View File

@ -0,0 +1,511 @@
<template>
<view :class="`wd-col-picker ${cell.border.value ? 'is-border' : ''} ${customClass}`" :style="customStyle">
<view class="wd-col-picker__field" @click="showPicker">
<slot v-if="useDefaultSlot"></slot>
<view
v-else
:class="`wd-col-picker__cell ${disabled && 'is-disabled'} ${props.readonly && 'is-readonly'} ${alignRight && 'is-align-right'} ${
error && 'is-error'
} ${size && 'is-' + size}`"
>
<view
v-if="label || useLabelSlot"
:class="`wd-col-picker__label ${isRequired && 'is-required'} ${customLabelClass}`"
:style="labelWidth ? 'min-width:' + labelWidth + ';max-width:' + labelWidth + ';' : ''"
>
<block v-if="label">{{ label }}</block>
<slot v-else name="label"></slot>
</view>
<view class="wd-col-picker__body">
<view class="wd-col-picker__value-wraper">
<view
:class="`wd-col-picker__value ${ellipsis && 'is-ellipsis'} ${customValueClass} ${showValue ? '' : 'wd-col-picker__value--placeholder'}`"
>
{{ showValue || placeholder || translate('placeholder') }}
</view>
<wd-icon v-if="!disabled && !readonly" custom-class="wd-col-picker__arrow" name="arrow-right" />
</view>
<view v-if="errorMessage" class="wd-col-picker__error-message">{{ errorMessage }}</view>
</view>
</view>
</view>
<wd-action-sheet
v-model="pickerShow"
:duration="250"
:title="title || translate('title')"
:close-on-click-modal="closeOnClickModal"
:z-index="zIndex"
:safe-area-inset-bottom="safeAreaInsetBottom"
@open="handlePickerOpend"
@close="handlePickerClose"
@closed="handlePickerClosed"
>
<view class="wd-col-picker__selected">
<scroll-view :scroll-x="true" scroll-with-animation :scroll-left="scrollLeft">
<view class="wd-col-picker__selected-container">
<view
v-for="(_, colIndex) in selectList"
:key="colIndex"
:class="`wd-col-picker__selected-item ${colIndex === currentCol && 'is-selected'}`"
@click="handleColClick(colIndex)"
>
{{ selectShowList[colIndex] || translate('select') }}
</view>
<view class="wd-col-picker__selected-line" :style="state.lineStyle"></view>
</view>
</scroll-view>
</view>
<view class="wd-col-picker__list-container">
<view
v-for="(col, colIndex) in selectList"
:key="colIndex"
class="wd-col-picker__list"
:style="colIndex === currentCol ? 'display: block;' : 'display: none;'"
>
<view
v-for="(item, index) in col"
:key="index"
:class="`wd-col-picker__list-item ${pickerColSelected[colIndex] && item[valueKey] === pickerColSelected[colIndex] && 'is-selected'} ${
item.disabled && 'is-disabled'
}`"
@click="chooseItem(colIndex, index)"
>
<view>
<view class="wd-col-picker__list-item-label">{{ item[labelKey] }}</view>
<view v-if="item[tipKey]" class="wd-col-picker__list-item-tip">{{ item[tipKey] }}</view>
</view>
<wd-icon custom-class="wd-col-picker__checked" name="check"></wd-icon>
</view>
<view v-if="loading" class="wd-col-picker__loading">
<wd-loading :color="loadingColor" />
</view>
</view>
</view>
</wd-action-sheet>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-col-picker',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import wdLoading from '../wd-loading/wd-loading.vue'
import wdActionSheet from '../wd-action-sheet/wd-action-sheet.vue'
import { computed, getCurrentInstance, onMounted, ref, watch, type CSSProperties, reactive, nextTick } from 'vue'
import { addUnit, debounce, getRect, isArray, isBoolean, isDef, isFunction, objToStyle } from '../common/util'
import { useCell } from '../composables/useCell'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
import { useParent } from '../composables/useParent'
import { useTranslate } from '../composables/useTranslate'
import { colPickerProps, type ColPickerExpose } from './types'
const { translate } = useTranslate('col-picker')
const $container = '.wd-col-picker__selected-container'
const $item = '.wd-col-picker__selected-item'
const props = defineProps(colPickerProps)
const emit = defineEmits(['close', 'update:modelValue', 'confirm'])
const pickerShow = ref<boolean>(false)
const currentCol = ref<number>(0)
const selectList = ref<Record<string, any>[][]>([])
const pickerColSelected = ref<(string | number)[]>([])
const selectShowList = ref<Record<string, any>[]>([])
const loading = ref<boolean>(false)
const isChange = ref<boolean>(false)
const lastSelectList = ref<Record<string, any>[][]>([])
const lastPickerColSelected = ref<(string | number)[]>([])
const scrollLeft = ref<number>(0)
const inited = ref<boolean>(false)
const isCompleting = ref<boolean>(false)
const state = reactive({
lineStyle: 'display:none;' // 线
})
const { proxy } = getCurrentInstance() as any
const cell = useCell()
const updateLineAndScroll = debounce(function (animation = true) {
setLineStyle(animation)
lineScrollIntoView()
}, 50)
const showValue = computed(() => {
const selectedItems = (props.modelValue || []).map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)
})
if (props.displayFormat) {
return props.displayFormat(selectedItems)
} else {
return selectedItems
.map((item) => {
return item[props.labelKey]
})
.join('')
}
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue === pickerColSelected.value) return
pickerColSelected.value = newValue
newValue.map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
})
handleAutoComplete()
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columns,
(newValue, oldValue) => {
if (newValue.length && !isArray(newValue[0])) {
console.error('[wot design] error(wd-col-picker): the columns props of wd-col-picker should be a two-dimensional array')
return
}
if (newValue.length === 0 && !oldValue) return
const newSelectedList = newValue.slice(0)
selectList.value = newSelectedList
selectShowList.value = pickerColSelected.value.map((item, colIndex) => {
return getSelectedItem(item, colIndex, newSelectedList)[props.labelKey]
})
lastSelectList.value = newSelectedList
if (newSelectedList.length > 0) {
currentCol.value = newSelectedList.length - 1
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columnChange,
(fn) => {
if (fn && !isFunction(fn)) {
console.error('The type of columnChange must be Function')
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.displayFormat,
(fn) => {
if (fn && !isFunction(fn)) {
console.error('The type of displayFormat must be Function')
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.beforeConfirm,
(fn) => {
if (fn && !isFunction(fn)) {
console.error('The type of beforeConfirm must be Function')
}
},
{
deep: true,
immediate: true
}
)
const { parent: form } = useParent(FORM_KEY)
//
const errorMessage = computed(() => {
if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
return form.errorMessages[props.prop]
} else {
return ''
}
})
//
const isRequired = computed(() => {
let formRequired = false
if (form && form.props.rules) {
const rules = form.props.rules
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
formRequired = rules[key].some((rule: FormItemRule) => rule.required)
}
}
}
return props.required || props.rules.some((rule) => rule.required) || formRequired
})
onMounted(() => {
inited.value = true
})
//
function open() {
showPicker()
}
//
function close() {
handlePickerClose()
}
function handlePickerOpend() {
updateLineAndScroll(false)
}
function handlePickerClose() {
pickerShow.value = false
emit('close')
}
function handlePickerClosed() {
if (isChange.value) {
setTimeout(() => {
selectList.value = lastSelectList.value.slice(0)
pickerColSelected.value = lastPickerColSelected.value.slice(0)
selectShowList.value = lastPickerColSelected.value.map((item, colIndex) => {
return getSelectedItem(item, colIndex, lastSelectList.value)[props.labelKey]
})
currentCol.value = lastSelectList.value.length - 1
isChange.value = false
}, 250)
}
}
function showPicker() {
const { disabled, readonly } = props
if (disabled || readonly) return
pickerShow.value = true
lastPickerColSelected.value = pickerColSelected.value.slice(0)
lastSelectList.value = selectList.value.slice(0)
}
function getSelectedItem(value: string | number, colIndex: number, selectList: Record<string, any>[][]) {
const { valueKey, labelKey } = props
if (selectList[colIndex]) {
const selecteds = selectList[colIndex].filter((item) => {
return item[valueKey] === value
})
if (selecteds.length > 0) {
return selecteds[0]
}
}
return {
[valueKey]: value,
[labelKey]: ''
}
}
function chooseItem(colIndex: number, index: number) {
const item = selectList.value[colIndex][index]
if (item.disabled) return
const newPickerColSelected = pickerColSelected.value.slice(0, colIndex)
newPickerColSelected.push(item[props.valueKey])
isChange.value = true
pickerColSelected.value = newPickerColSelected
selectList.value = selectList.value.slice(0, colIndex + 1)
selectShowList.value = newPickerColSelected.map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
})
if (selectShowList.value[colIndex] && colIndex === currentCol.value) {
updateLineAndScroll(true)
}
handleColChange(colIndex, item, index)
}
function handleColChange(colIndex: number, item: Record<string, any>, index: number, callback?: () => void) {
loading.value = true
const { columnChange, beforeConfirm } = props
columnChange &&
columnChange({
selectedItem: item,
index: colIndex,
rowIndex: index,
resolve: (nextColumn: Record<string, any>[]) => {
if (!isArray(nextColumn)) {
console.error('[wot design] error(wd-col-picker): the data of each column of wd-col-picker should be an array')
return
}
const newSelectList = selectList.value.slice(0)
newSelectList[colIndex + 1] = nextColumn
selectList.value = newSelectList
loading.value = false
currentCol.value = colIndex + 1
updateLineAndScroll(true)
if (typeof callback === 'function') {
isCompleting.value = false
selectShowList.value = pickerColSelected.value.map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
})
callback()
}
},
finish: (isOk?: boolean) => {
//
if (typeof callback === 'function') {
loading.value = false
isCompleting.value = false
return
}
if (isBoolean(isOk) && !isOk) {
loading.value = false
return
}
if (beforeConfirm) {
beforeConfirm(
pickerColSelected.value,
pickerColSelected.value.map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)
}),
(isPass: boolean) => {
if (isPass) {
onConfirm()
} else {
loading.value = false
}
}
)
} else {
onConfirm()
}
}
})
}
function onConfirm() {
isChange.value = false
loading.value = false
pickerShow.value = false
emit('update:modelValue', pickerColSelected.value)
emit('confirm', {
value: pickerColSelected.value,
selectedItems: pickerColSelected.value.map((item, colIndex) => {
return getSelectedItem(item, colIndex, selectList.value)
})
})
}
function handleColClick(index: number) {
isChange.value = true
currentCol.value = index
updateLineAndScroll(true)
}
/**
* @description 更新navBar underline的偏移量
* @param {Boolean} animation 是否伴随动画
*/
function setLineStyle(animation: boolean = true) {
if (!inited.value) return
const { lineWidth, lineHeight } = props
getRect($item, true, proxy)
.then((rects) => {
const lineStyle: CSSProperties = {}
if (isDef(lineWidth)) {
lineStyle.width = addUnit(lineWidth)
}
if (isDef(lineHeight)) {
lineStyle.height = addUnit(lineHeight)
lineStyle.borderRadius = `calc(${addUnit(lineHeight)} / 2)`
}
const rect = rects[currentCol.value]
let left = rects.slice(0, currentCol.value).reduce((prev, curr) => prev + Number(curr.width), 0) + Number(rect.width) / 2
lineStyle.transform = `translateX(${left}px) translateX(-50%)`
if (animation) {
lineStyle.transition = 'width 300ms ease, transform 300ms ease'
}
state.lineStyle = objToStyle(lineStyle)
})
.catch(() => {})
}
/**
* @description scroll-view滑动到active的tab_nav
*/
function lineScrollIntoView() {
if (!inited.value) return
Promise.all([getRect($item, true, proxy), getRect($container, false, proxy)])
.then(([navItemsRects, navRect]) => {
if (!isArray(navItemsRects) || navItemsRects.length === 0) return
//
const selectItem = navItemsRects[currentCol.value]
//
const offsetLeft = navItemsRects.slice(0, currentCol.value).reduce((prev, curr) => prev + Number(curr.width), 0)
// scroll-viewselectItem
scrollLeft.value = offsetLeft - ((navRect as any).width - Number(selectItem.width)) / 2
})
.catch(() => {})
}
//
function diffColumns(colIndex: number) {
// colIndex -1 item >=0 value
const item = colIndex === -1 ? {} : { [props.valueKey]: props.modelValue[colIndex] }
handleColChange(colIndex, item, -1, () => {
// columns value colIndex + 1
if (selectList.value.length < props.modelValue.length) {
diffColumns(colIndex + 1)
}
})
}
function handleAutoComplete() {
if (props.autoComplete) {
// columns value columnChange
if (selectList.value.length < props.modelValue.length || selectList.value.length === 0) {
// isCompleting
if (!isCompleting.value) {
// columns colIndex -1
const colIndex = selectList.value.length === 0 ? -1 : selectList.value.length - 1
diffColumns(colIndex)
}
isCompleting.value = true
}
}
}
defineExpose<ColPickerExpose>({
close,
open
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,19 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
$i: 1;
@include b(col) {
float: left;
box-sizing: border-box;
}
@while $i <= 24 {
.wd-col__#{$i} {
width: calc(100% / 24 * $i);
}
.wd-col__offset-#{$i} {
margin-left: calc(100% / 24 * $i);
}
$i: $i + 1;
}

View File

@ -0,0 +1,15 @@
import type { ExtractPropTypes } from 'vue'
import { baseProps, makeNumberProp } from '../common/props'
export const colProps = {
...baseProps,
/**
* 列元素宽度
*/
span: makeNumberProp(24),
/**
* 列元素偏移距离
*/
offset: makeNumberProp(0)
}
export type ColProps = ExtractPropTypes<typeof colProps>

View File

@ -0,0 +1,49 @@
<template>
<view :class="['wd-col', span && 'wd-col__' + span, offset && 'wd-col__offset-' + offset, customClass]" :style="rootStyle">
<!-- 每一列 -->
<slot />
</view>
</template>
<script lang="ts">
export default {
name: 'wd-col',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, watch } from 'vue'
import { useParent } from '../composables/useParent'
import { ROW_KEY } from '../wd-row/types'
import { colProps } from './types'
import { isDef } from '../common/util'
const props = defineProps(colProps)
const { parent: row } = useParent(ROW_KEY)
const rootStyle = computed(() => {
const gutter = isDef(row) ? row.props.gutter || 0 : 0
const padding = `${gutter / 2}px`
const style = gutter > 0 ? `padding-left: ${padding}; padding-right: ${padding};background-clip: content-box;` : ''
return `${style}${props.customStyle}`
})
watch([() => props.span, () => props.offset], () => {
check()
})
function check() {
const { span, offset } = props
if (span < 0 || offset < 0) {
console.error('[wot-design] warning(wd-col): attribute span/offset must be greater than or equal to 0')
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,90 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(collapse-item) {
@include halfPixelBorder('top', 0, $-dark-border-color);
@include e(title) {
color: $-dark-color;
}
@include e(body) {
color: $-dark-color3;
}
@include when(disabled) {
.wd-collapse-item__title {
color: $-dark-color-gray;
}
.wd-collapse-item__arrow {
color: $-dark-color-gray;
}
}
}
}
@include b(collapse-item) {
position: relative;
@include halfPixelBorder('top');
@include e(header) {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding: $-collapse-header-padding;
overflow: hidden;
user-select: none;
@include when(expanded) {
@include halfPixelBorder('bottom');
}
@include when(custom) {
display: block;
}
}
@include e(title) {
color: $-collapse-title-color;
font-weight: $-fw-medium;
font-size: $-collapse-title-fs;
}
@include edeep(arrow) {
display: block;
font-size: $-collapse-arrow-size;
color: $-collapse-arrow-color;
transition: transform 0.3s;
@include when(retract) {
transform: rotate(-180deg);
}
}
@include e(wrapper) {
position: relative;
overflow: hidden;
will-change: height;
}
@include e(body) {
color: $-collapse-body-color;
font-size: $-collapse-body-fs;
padding: $-collapse-body-padding;
line-height: 1.43;
}
@include when(disabled) {
.wd-collapse-item__title {
color: $-collapse-disabled-color;
}
.wd-collapse-item__arrow {
color: $-collapse-disabled-color;
}
}
}

View File

@ -0,0 +1,48 @@
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeRequiredProp, makeStringProp } from '../common/props'
export type CollapseItemBeforeExpand = (name: string) => boolean | Promise<unknown>
export const collapseItemProps = {
...baseProps,
/**
* 自定义折叠栏内容容器样式类名
*/
customBodyClass: makeStringProp(''),
/**
* 自定义折叠栏内容容器样式
*/
customBodyStyle: makeStringProp(''),
/**
* 折叠栏的标题, 可通过 slot 传递自定义内容
*/
title: makeStringProp(''),
/**
* 禁用折叠栏
*/
disabled: makeBooleanProp(false),
/**
* 折叠栏的标识符
*/
name: makeRequiredProp(String),
/**
* 打开前的回调函数返回 false 可以阻止打开支持返回 Promise
*/
beforeExpend: Function as PropType<CollapseItemBeforeExpand>
}
export type CollapseItemProps = ExtractPropTypes<typeof collapseItemProps>
export type CollapseItemExpose = {
/**
* 获取展开状态
* @returns boolean
*/
getExpanded: () => boolean
/**
* 更新展开状态
*/
updateExpand: () => Promise<void>
}
export type CollapseItemInstance = ComponentPublicInstance<CollapseItemProps, CollapseItemExpose>

View File

@ -0,0 +1,171 @@
<template>
<view :class="`wd-collapse-item ${disabled ? 'is-disabled' : ''} is-border ${customClass}`" :style="customStyle">
<view
:class="`wd-collapse-item__header ${expanded ? 'is-expanded' : ''} ${isFirst ? 'wd-collapse-item__header-first' : ''} ${
$slots.title ? 'is-custom' : ''
}`"
@click="handleClick"
>
<slot name="title" :expanded="expanded" :disabled="disabled" :isFirst="isFirst">
<text class="wd-collapse-item__title">{{ title }}</text>
<wd-icon name="arrow-down" :custom-class="`wd-collapse-item__arrow ${expanded ? 'is-retract' : ''}`" />
</slot>
</view>
<view class="wd-collapse-item__wrapper" :style="contentStyle" @transitionend="handleTransitionEnd">
<view class="wd-collapse-item__body" :class="customBodyClass" :style="customBodyStyle" :id="collapseId">
<slot />
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-collapse-item',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed, getCurrentInstance, onMounted, ref, watch, type CSSProperties } from 'vue'
import { addUnit, getRect, isArray, isDef, isPromise, isString, objToStyle, pause, uuid } from '../common/util'
import { useParent } from '../composables/useParent'
import { COLLAPSE_KEY } from '../wd-collapse/types'
import { collapseItemProps, type CollapseItemExpose } from './types'
const collapseId = ref<string>(`collapseId${uuid()}`)
const props = defineProps(collapseItemProps)
const { parent: collapse, index } = useParent(COLLAPSE_KEY)
const height = ref<string | number>('')
const inited = ref<boolean>(false)
const expanded = ref<boolean>(false)
const { proxy } = getCurrentInstance() as any
/**
* 容器样式(动画)
*/
const isFirst = computed(() => {
return index.value === 0
})
/**
* 容器样式(动画)
*/
const contentStyle = computed(() => {
const style: CSSProperties = {}
if (inited.value) {
style.transition = 'height 0.3s ease-in-out'
}
if (!expanded.value) {
style.height = '0px'
} else if (height.value) {
style.height = addUnit(height.value)
}
return objToStyle(style)
})
/**
* 是否选中
*/
const isSelected = computed(() => {
const modelValue = collapse ? collapse?.props.modelValue || [] : []
const { name } = props
return (isString(modelValue) && modelValue === name) || (isArray(modelValue) && modelValue.indexOf(name as string) >= 0)
})
watch(
() => isSelected.value,
(newVal) => {
updateExpand(newVal)
}
)
onMounted(() => {
updateExpand(isSelected.value)
})
async function updateExpand(useBeforeExpand: boolean = true) {
try {
if (useBeforeExpand) {
await handleBeforeExpand()
}
initRect()
} catch (error) {
/* empty */
}
}
function initRect() {
getRect(`#${collapseId.value}`, false, proxy).then(async (rect) => {
const { height: rectHeight } = rect
height.value = isDef(rectHeight) ? Number(rectHeight) : ''
await pause()
if (isSelected.value) {
expanded.value = true
} else {
expanded.value = false
}
if (!inited.value) {
inited.value = true
}
})
}
function handleTransitionEnd() {
if (expanded.value) {
height.value = ''
}
}
//
async function handleClick() {
if (props.disabled) return
try {
await updateExpand()
const { name } = props
collapse && collapse.toggle(name, !expanded.value)
} catch (error) {
/* empty */
}
}
/**
* 展开前钩子
*/
function handleBeforeExpand() {
return new Promise<void>((resolve, reject) => {
const { name } = props
const nextexpanded = !expanded.value
if (nextexpanded && props.beforeExpend) {
const response = props.beforeExpend(name)
if (!response) {
reject()
}
if (isPromise(response)) {
response.then(() => resolve()).catch(reject)
} else {
resolve()
}
} else {
resolve()
}
})
}
function getExpanded() {
return expanded.value
}
defineExpose<CollapseItemExpose>({ getExpanded, updateExpand })
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

View File

@ -0,0 +1,55 @@
@import "../common/abstracts/variable";
@import "../common/abstracts/mixin";
.wot-theme-dark {
@include b(collapse) {
background: $-dark-background2;
@include e(content) {
color: $-dark-color3;
}
}
}
@include b(collapse) {
background: $-color-white;
@include when(viewmore) {
padding: $-collapse-side-padding;
}
@include e(content) {
font-size: $-collapse-body-fs;
color: $-collapse-body-color;
@include when(retract) {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: $-collapse-retract-fs;
}
}
@include e(more) {
display: inline-block;
font-size: $-collapse-retract-fs;
margin-top: 8px;
color: $-collapse-more-color;
user-select: none;
}
@include e(more-txt) {
display: inline-block;
vertical-align: middle;
margin-right: 4px;
}
@include e(arrow) {
display: inline-block;
vertical-align: middle;
transition: transform 0.1s;
font-size: $-collapse-arrow-size;
height: $-collapse-arrow-size;
line-height: $-collapse-arrow-size;
@include when(retract) {
transform: rotate(-180deg);
}
}
}

View File

@ -0,0 +1,58 @@
import { type ComponentPublicInstance, type ExtractPropTypes, type InjectionKey, type PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
export type CollapseToggleAllOptions =
| boolean
| {
expanded?: boolean
skipDisabled?: boolean
}
export type CollapseProvide = {
props: Partial<CollapseProps>
toggle: (name: string, expanded: boolean) => void
}
export const COLLAPSE_KEY: InjectionKey<CollapseProvide> = Symbol('wd-collapse')
export const collapseProps = {
...baseProps,
/**
* 查看更多模式下的插槽外部自定义样式
*/
customMoreSlotClass: makeStringProp(''),
/**
* 绑定值
*/
modelValue: {
type: [String, Array, Boolean] as PropType<string | Array<string> | boolean>
},
/**
* 手风琴模式
*/
accordion: makeBooleanProp(false),
/**
* 查看更多的折叠面板
*/
viewmore: makeBooleanProp(false),
/**
* 查看更多的自定义插槽使用标志
*/
useMoreSlot: makeBooleanProp(false),
/**
* 查看更多的折叠面板收起时的显示行数
*/
lineNum: makeNumberProp(2)
}
export type CollapseProps = ExtractPropTypes<typeof collapseProps>
export type CollapseExpose = {
/**
* 切换所有面板展开状态 true 为全部展开false 为全部收起不传参为全部切换
* @param options 面板状态
*/
toggleAll: (options?: CollapseToggleAllOptions) => void
}
export type CollapseInstance = ComponentPublicInstance<CollapseProps, CollapseExpose>

View File

@ -0,0 +1,151 @@
<template>
<view :class="`wd-collapse ${viewmore ? 'is-viewmore' : ''} ${customClass}`" :style="customStyle">
<!-- 普通或手风琴 -->
<block v-if="!viewmore">
<slot></slot>
</block>
<!-- 查看更多模式 -->
<view v-else>
<view
:class="`wd-collapse__content ${!modelValue ? 'is-retract' : ''} `"
:style="`-webkit-line-clamp: ${contentLineNum}; -webkit-box-orient: vertical`"
>
<slot></slot>
</view>
<view class="wd-collapse__more" @click="handleMore">
<!-- 自定义展开按钮 -->
<view v-if="useMoreSlot" :class="customMoreSlotClass">
<slot name="more"></slot>
</view>
<!-- 显示展开或折叠按钮 -->
<block v-else>
<span class="wd-collapse__more-txt">{{ !modelValue ? translate('expand') : translate('retract') }}</span>
<view :class="`wd-collapse__arrow ${modelValue ? 'is-retract' : ''}`">
<wd-icon name="arrow-down"></wd-icon>
</view>
</block>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-collapse',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { onBeforeMount, ref, watch } from 'vue'
import { COLLAPSE_KEY, collapseProps, type CollapseExpose, type CollapseToggleAllOptions } from './types'
import { useChildren } from '../composables/useChildren'
import { isArray, isBoolean, isDef } from '../common/util'
import { useTranslate } from '../composables/useTranslate'
const props = defineProps(collapseProps)
const emit = defineEmits(['change', 'update:modelValue'])
const { translate } = useTranslate('collapse')
const contentLineNum = ref<number>(0) //
const { linkChildren, children } = useChildren(COLLAPSE_KEY)
linkChildren({ props, toggle })
watch(
() => props.modelValue,
(newVal) => {
const { viewmore, accordion } = props
// value string
if (accordion && typeof newVal !== 'string') {
console.error('accordion value must be string')
} else if (!accordion && !viewmore && !isArray(newVal)) {
console.error('value must be Array')
}
},
{ deep: true }
)
watch(
() => props.lineNum,
(newVal) => {
if (newVal <= 0) {
console.error('lineNum must greater than 0')
}
},
{ deep: true, immediate: true }
)
onBeforeMount(() => {
const { lineNum, viewmore, modelValue } = props
contentLineNum.value = viewmore && !modelValue ? lineNum : 0
})
function updateChange(activeNames: string | string[] | boolean) {
emit('update:modelValue', activeNames)
emit('change', {
value: activeNames
})
}
function toggle(name: string, expanded: boolean) {
const { accordion, modelValue } = props
if (accordion) {
updateChange(name === modelValue ? '' : name)
} else if (expanded) {
updateChange((modelValue as string[]).concat(name))
} else {
updateChange((modelValue as string[]).filter((activeName) => activeName !== name))
}
}
/**
* 切换所有面板展开状态 true 为全部展开false 为全部收起不传参为全部切换
* @param options 面板状态
*/
const toggleAll = (options: CollapseToggleAllOptions = {}) => {
if (props.accordion) {
return
}
if (isBoolean(options)) {
options = { expanded: options }
}
const { expanded, skipDisabled } = options
const names: string[] = []
children.forEach((item, index) => {
if (item.disabled && skipDisabled) {
if (item.$.exposed!.getExpanded()) {
names.push(item.name || index)
}
} else if (isDef(expanded) ? expanded : !item.$.exposed!.getExpanded()) {
names.push(item.name || index)
}
})
updateChange(names)
}
/**
* 查看更多点击
*/
function handleMore() {
emit('update:modelValue', !props.modelValue)
emit('change', {
value: !props.modelValue
})
}
defineExpose<CollapseExpose>({
toggleAll
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
<!--
* @Author: weisheng
* @Date: 2023-06-13 11:34:35
* @LastEditTime: 2025-04-28 22:26:25
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-config-provider/wd-config-provider.vue
* 记得注释
-->
<template>
<view :class="themeClass" :style="themeStyle">
<slot />
</view>
</template>
<script lang="ts">
export default {
name: 'wd-config-provider',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed } from 'vue'
import { configProviderProps } from './types'
import { objToStyle } from '../common/util'
const props = defineProps(configProviderProps)
const themeClass = computed(() => {
return `wot-theme-${props.theme} ${props.customClass}`
})
const themeStyle = computed(() => {
const styleObj = mapThemeVarsToCSSVars(props.themeVars)
return styleObj ? `${objToStyle(styleObj)}${props.customStyle}` : props.customStyle
})
const kebabCase = (str: string): string => {
str = str.replace(str.charAt(0), str.charAt(0).toLocaleLowerCase())
return str.replace(/([a-z])([A-Z])/g, (_, p1, p2) => p1 + '-' + p2.toLowerCase())
}
const colorRgb = (str: string) => {
if (!str) return
var sColor = str.toLowerCase()
//
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
// 16
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
var sColorNew = '#'
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
}
sColor = sColorNew
}
//
var sColorChange: number[] = []
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
}
return sColorChange.join(',')
}
return null
}
const mapThemeVarsToCSSVars = (themeVars: Record<string, string>) => {
if (!themeVars) return
const cssVars: Record<string, string> = {}
Object.keys(themeVars).forEach((key) => {
cssVars[`--wot-${kebabCase(key)}`] = themeVars[key]
})
return cssVars
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,15 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(count-down) {
color: $-dark-color;
}
}
@include b(count-down) {
color: $-count-down-text-color;
font-size: $-count-down-font-size;
line-height: $-count-down-line-height;
}

View File

@ -0,0 +1,41 @@
import type { ComponentPublicInstance, ExtractPropTypes } from 'vue'
import { baseProps, makeBooleanProp, makeRequiredProp, makeStringProp } from '../common/props'
export const countDownProps = {
...baseProps,
/**
* 倒计时时长单位毫秒
*/
time: makeRequiredProp(Number),
/**
* 是否开启毫秒
*/
millisecond: makeBooleanProp(false),
/**
* 格式化时间
*/
format: makeStringProp('HH:mm:ss'),
/**
* 是否自动开始
*/
autoStart: makeBooleanProp(true)
}
export type CountDownProps = ExtractPropTypes<typeof countDownProps>
export type CountDownExpose = {
/**
* 开始倒计时
*/
start: () => void
/**
* 暂停倒计时
*/
pause: () => void
/**
* 重设倒计时 auto-start true重设后会自动开始倒计时
*/
reset: () => void
}
export type CountDownInstance = ComponentPublicInstance<CountDownProps, CountDownExpose>

View File

@ -0,0 +1,52 @@
import { padZero } from '../common/util'
export type TimeData = {
days: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
export function parseFormat(format: string, timeData: TimeData): string {
const { days } = timeData
let { hours, minutes, seconds, milliseconds } = timeData
if (format.includes('DD')) {
format = format.replace('DD', padZero(days))
} else {
hours += days * 24
}
if (format.includes('HH')) {
format = format.replace('HH', padZero(hours))
} else {
minutes += hours * 60
}
if (format.includes('mm')) {
format = format.replace('mm', padZero(minutes))
} else {
seconds += minutes * 60
}
if (format.includes('ss')) {
format = format.replace('ss', padZero(seconds))
} else {
milliseconds += seconds * 1000
}
if (format.includes('S')) {
const ms = padZero(milliseconds, 3)
if (format.includes('SSS')) {
format = format.replace('SSS', ms)
} else if (format.includes('SS')) {
format = format.replace('SS', ms.slice(0, 2))
} else {
format = format.replace('S', ms.charAt(0))
}
}
return format
}

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