宴席2.0
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
sjeam 2025-08-12 09:11:58 +08:00
parent 89f64b5418
commit cf8b8842a5
384 changed files with 50594 additions and 30 deletions

View File

@ -325,3 +325,22 @@ be imported.
<p align='center'>
<img alt="微信收款码" src="./src/static/images/pay.png" height="330" style="display:inline-block; height:330px;">
</p> -->
引入了wot ui 组件
### 引入
https://wot-design-uni.cn/guide/join-group.html
安装 area-data 组件
# 通过 pnpm
pnpm add @vant/area-data
使用
在 Vant 的 Area 组件中使用时,直接引用 areaList 对象即可:
import { areaList } from '@vant/area-data';
在 Vant 的 Cascader 组件中使用时,请使用 useCascaderAreaData 方法:
import { useCascaderAreaData } from '@vant/area-data';
const cascaderAreaData = useCascaderAreaData();

8
env/.env vendored
View File

@ -5,8 +5,8 @@ VITE_APP_TITLE=uniapp-vue3模板项目
VITE_APP_ENV=development
# 接口地址
# VITE_API_BASE_URL=http://www.caipu.com:8545
VITE_API_BASE_URL=https://ceshi-recipeapi.lihaink.cn/
VITE_API_BASE_URL=http://www.caipu.com:8545
# VITE_API_BASE_URL=https://ceshi-recipeapi.lihaink.cn/
# 端口号
VITE_APP_PORT=9527
@ -15,8 +15,8 @@ VITE_APP_PORT=9527
VITE_APP_PROXY=true
# API代理前缀
# VITE_API_PREFIX= http://www.caipu.com:8545
VITE_API_PREFIX= https://ceshi-recipeapi.lihaink.cn/
VITE_API_PREFIX= http://www.caipu.com:8545
# VITE_API_PREFIX= https://ceshi-recipeapi.lihaink.cn/
# 删除console
VITE_DROP_CONSOLE=false

View File

@ -68,12 +68,14 @@
"@dcloudio/uni-components": "3.0.0-4060420250429001",
"@dcloudio/uni-h5": "3.0.0-4060420250429001",
"@dcloudio/uni-mp-weixin": "3.0.0-4060420250429001",
"@vant/area-data": "^2.0.0",
"dayjs": "^1.11.13",
"pinia": "2.2.4",
"pinia-plugin-persistedstate": "4.1.3",
"uview-plus": "^3.4.28",
"vue": "3.4.21",
"vue-i18n": "9.1.9",
"wot-design-uni": "^1.11.1",
"z-paging": "^2.8.4"
},
"devDependencies": {

21
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ importers:
'@dcloudio/uni-mp-weixin':
specifier: 3.0.0-4060420250429001
version: 3.0.0-4060420250429001(@nuxt/kit@3.17.2)(postcss@8.5.3)(vue@3.4.21(typescript@5.8.3))
'@vant/area-data':
specifier: ^2.0.0
version: 2.0.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
@ -41,6 +44,9 @@ importers:
vue-i18n:
specifier: 9.1.9
version: 9.1.9(vue@3.4.21(typescript@5.8.3))
wot-design-uni:
specifier: ^1.11.1
version: 1.11.1(vue@3.4.21(typescript@5.8.3))
z-paging:
specifier: ^2.8.4
version: 2.8.6
@ -2342,6 +2348,9 @@ packages:
cpu: [x64]
os: [win32]
'@vant/area-data@2.0.0':
resolution: {integrity: sha512-zgP4AA8z09S9QTNgVCCHo9cHjcybrv22RJDYPjuCkecn4SB98T5EoPQh2TwqbQXmUhbaOGgiZGy3OUaUxnY7qg==}
'@vitejs/plugin-legacy@5.3.2':
resolution: {integrity: sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -5977,6 +5986,12 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wot-design-uni@1.11.1:
resolution: {integrity: sha512-Pjfnz+4v0XDWvVJ/mkgVUzneOpB/WR0H+iD+Wt0hbELopuZhA1DzFvUsRA90cP/7sPsKiwiwKl3aXecG++4SkQ==}
engines: {HBuilderX: ^3.8.7}
peerDependencies:
vue: '>=3.2.47'
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -8839,6 +8854,8 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.7.2':
optional: true
'@vant/area-data@2.0.0': {}
'@vitejs/plugin-legacy@5.3.2(terser@5.39.0)(vite@5.2.8(@types/node@22.15.17)(sass@1.79.6)(terser@5.39.0))':
dependencies:
'@babel/core': 7.27.1
@ -13183,6 +13200,10 @@ snapshots:
word-wrap@1.2.5: {}
wot-design-uni@1.11.1(vue@3.4.21(typescript@5.8.3)):
dependencies:
vue: 3.4.21(typescript@5.8.3)
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0

View File

@ -0,0 +1,38 @@
// 可以将此代码放置于项目src/hooks/useColPickerData.ts中
import { useCascaderAreaData } from '@vant/area-data'
export type CascaderOption = {
text: string
value: string
children?: CascaderOption[]
}
/**
* 使'@vant/area-data'ColPicker组件的数据
* @returns
*/
export default function useColPickerData() {
// '@vant/area-data' 数据源
const colPickerData: CascaderOption[] = useCascaderAreaData()
// 根据code查找子节点不传code则返回所有节点
function findChildrenByCode(data: CascaderOption[], code?: string): CascaderOption[] | null {
if (!code) {
return data
}
for (const item of data) {
if (item.value === code) {
return item.children || null
}
if (item.children) {
const childrenResult = findChildrenByCode(item.children, code)
if (childrenResult) {
return childrenResult
}
}
}
return null
}
return { colPickerData, findChildrenByCode }
}

View File

@ -5,4 +5,7 @@ import useModal from './use-modal';
import usePermission from './use-permission';
import useShare from './use-share';
export { useClipboard, useLoading, useLocation, useModal, usePermission, useShare };
import useColPickerData from './area-data';
export {useColPickerData, useClipboard, useLoading, useLocation, useModal, usePermission, useShare };

View File

@ -81,7 +81,9 @@
"h5" : {
"router" : {
"mode" : "hash",
"base" : "/caipu_uni/"
// "base" : "/caipu_uni/"
"base" : ""
},
"sdkConfigs" : {
"maps" : {

View File

@ -1,10 +1,12 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue",
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue"
}
},
"pages": [
@ -106,6 +108,89 @@
}
]
},
{
"root": "pages/banquet",
"pages": [
{
"path": "home/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "cook/list",
"style": {
"navigationBarTitleText": "厨师",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "cook/detail",
"style": {
"navigationBarTitleText": "厨师详情",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "hotel/list",
"style": {
"navigationBarTitleText": "酒店",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "hotel/detail",
"style": {
"navigationBarTitleText": "酒店详情",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "combo/index",
"style": {
"navigationBarTitleText": "餐标选择",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "combo/detail",
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "order/detail",
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom"
},
"needLogin": true
},
{
"path": "order/pay",
"style": {
"navigationBarTitleText": "付款详情",
"navigationStyle": "custom"
},
"needLogin": true
}
]
}
],
@ -120,18 +205,38 @@
"selectedColor": "#59CB56",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [{
"list": [
{
"iconPath": "static/images/tabbar/shouye.png",
"selectedIconPath": "static/images/tabbar/shouye_selected.png",
"pagePath": "pages/tab/home/index",
"text": "菜单分类"
"pagePath": "pages/banquet/home/index",
"text": "首页"
},
{
"iconPath": "static/images/tabbar/shouye.png",
"selectedIconPath": "static/images/tabbar/shouye_selected.png",
"pagePath": "pages/banquet/cook/list",
"text": "选厨师"
},
{
"iconPath": "static/images/tabbar/bofang.png",
"selectedIconPath": "static/images/tabbar/bofang_selected.png",
"pagePath": "pages/tab/list/index",
"text": "食谱清单"
"pagePath": "pages/banquet/hotel/list",
"text": "选酒店"
},
// {
// "iconPath": "static/images/tabbar/shouye.png",
// "selectedIconPath": "static/images/tabbar/shouye_selected.png",
// "pagePath": "pages/tab/home/index",
// "text": "菜单分类"
// },
// {
// "iconPath": "static/images/tabbar/bofang.png",
// "selectedIconPath": "static/images/tabbar/bofang_selected.png",
// "pagePath": "pages/tab/list/index",
// "text": "食谱清单"
// },
{
"iconPath": "static/images/tabbar/wode.png",
"selectedIconPath": "static/images/tabbar/wode_selected.png",

View File

@ -0,0 +1,392 @@
<style lang='scss'>
.custom-count-down {
display: inline-block;
width: 22px;
color: #ff3232;
font-size: 12px;
text-align: center;
background-color: #ff404021;
border-radius: 2px;
}
.custom-count-down-colon {
display: inline-block;
margin: 0 4px;
color: #ff4040;
}
.cell-icon {
display: block;
box-sizing: border-box;
width: 16px;
height: 16px;
margin-right: 4px;
background: url('https://img10.360buyimg.com/jmadvertisement/jfs/t1/71075/7/3762/1820/5d1f26d1E0d600b9e/a264c901943080ac.png') no-repeat;
background-size: cover;
}
</style>
<template>
<view>
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`订单确认`" titleColor="#303133"
bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133"
:safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<u-popup :show="propShowOrder" :round="10" mode="bottom" @close="close" customStyle="height:800rpx;">
<wd-form ref="form" :model="orderInfo" errorType="message">
<wd-cell-group border>
<view style="padding: 20rpx;">
<u-cell class="" style="line-height: 60rpx;" :border="true">
<template #title>
<view class="h-50 p-2 align-center font-blod" @click="close">
支付金额 <wd-text size="36rpx" bold text="16354.156" mode="price" type="error" prefix="¥" suffix="元" />
</view>
<view class="h-50 p-2 align-center">
剩余时间
<view style="display: inline-block;">
<wd-count-down :time="(30 * 60 * 60 * 1000)">
<template #default="{ current }">
<span class="custom-count-down">{{ current.hours > 10 ? current.hours : '0' + current.hours }}</span>
<span class="custom-count-down-colon">:</span>
<span class="custom-count-down">{{ current.minutes }}</span>
<span class="custom-count-down-colon">:</span>
<span class="custom-count-down">{{ current.seconds }}</span>
</template>
</wd-count-down>
</view>
</view>
</template>
</u-cell>
<wd-cell-group>
<!-- <wd-cell title="微信支付" value="内容">
<template #icon>
<view class="cell-icon"></view>
12321
</template>
</wd-cell> -->
</wd-cell-group>
<u-alert :show-icon="true" title="微信支付" type="" effect="dark" description=""></u-alert>
<u-radio-group v-model="value">
<view class="h-60 flex">
<up-text size="28rpx" :text="` 680套餐`" :flex1="true" align="left" wordWrap="normal" :show="true"
prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
<view style="display: inline-block; float:right">
<u-radio activeColor="#59CB56" label=""></u-radio>
</view>
</view>
</u-radio-group>
</view>
<view class="prop-flex-bottom">
<up-button class="" style="" text="确认支付" type="primary" color="#59CB56" shape="circle" size="normal"
custom-style="width: calc(100vw - 100rpx);" @click="addOrder">
</up-button>
</view>
</wd-cell-group>
</wd-form>
</u-popup>
<wd-card>
<template #title>
<view class="title font-size-32 font-bold">
<view>第一餐</view>
<view class="title-tip">
<text class="font-size-32 color-red font-bold ">213213.00</text>
</view>
</view>
<view class="title font-size-28 pt-4">
<view>做宴时段2025-11-12</view>
<view class="title-tip font-size-28">
做宴时段早上
</view>
</view>
</template>
<view class="content ">
<up-cell-group :border="false">
<view class="detail-box" v-for="(item, index) in matchedItems_sp" :key="index">
<u-cell :border="false">
<template #icon>
<wd-img :width="60" :height="60" :src="item.image" :enable-preview="true" :radius="8" :round=false
custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="28rpx" :text="`${item.name}680套餐`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon>
</view>
</template>
<template #label>
<view class="h-60 flex color-gray">
<up-text size="26rpx" :text="`${item.intro}高级版-快速吸粉 | 周期一年`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle=""
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<u-icon slot="right" label-color="red" :label="`X30`"></u-icon>
</view>
</template>
</u-cell>
</view>
</up-cell-group>
</view>
<template #footer>
<!-- <view>
<wd-button size="small" style="margin-right: 8px;">评价</wd-button>
<wd-button size="small" plain>立即使用</wd-button>
</view> -->
</template>
</wd-card>
<wd-card>
<template #title>
<view class="title font-size-32">
<view>费用合计</view>
<view class="title-tip">
<text class="font-size-32 color-red ">213213.00</text>
</view>
</view>
</template>
</wd-card>
<wd-card>
<wd-form ref="form" :model="orderInfo" errorType="message">
<wd-cell-group border>
<u-cell class="" style="line-height: 60rpx;" :border="true">
<template #title>
</template>
</u-cell>
<u-alert :show-icon="true" title="温馨提示:请确认酒席需求" type="" effect="dark" description=" "></u-alert>
<view class="pb-4" />
<wd-input label="联系姓名" prop="name" clearable v-model="orderInfo.name" placeholder="请填写联系姓名" :rules="[
{
required: false,
validator: (value: string) => {
return /^[a-zA-Z0-9]+$/.test(value)
},
message: '请输入姓名'
}
]" />
<wd-input label="联系电话" prop="phone" clearable v-model="orderInfo.phone" placeholder="请填写联系电话"
:rules="[{ required: false, pattern: /^1[3-9]\d{9}$/, message: '请输入有效手机号码' }]" />
<wd-col-picker label="做宴地址" v-model="orderInfo.district_name" prop="district_name" :columns="area"
:column-change="columnChange" placeholder="请填写做宴地址" @confirm="handleConfirm" :rules="[
{
required: false,
validator: (value: string) => {
return /^[a-zA-Z0-9]+$/.test(value)
},
message: '请填写做宴地址'
}
]"></wd-col-picker>
<wd-input label="详细地址" prop="street_name" clearable v-model="orderInfo.street_name" placeholder="请填写详细地址"
:rules="[
{
required: false,
validator: (value: string) => {
return /^[a-zA-Z0-9]+$/.test(value)
},
message: '请输入正确的玛卡巴卡'
}
]" />
<wd-textarea label="备注" v-model="orderInfo.total_num" placeholder="请填写备注" />
<wd-button plain hairline custom-style="width: calc(100vw - 100rpx);"
@click="handleSubmit">确定预定做席后再支付</wd-button>
<wd-button hairline custom-style="width: calc(100vw - 100rpx);" @click="handleSubmit">付定金锁定订单</wd-button>
</wd-cell-group>
</wd-form>
<!-- 一般的检举内容由承办的党的委员会或纪律检查委员会将处理意见或复议复查结论同申诉人见面听取其意见复议复查的结论和决定应交给申诉人一份 -->
<template #footer>
<!-- <wd-button size="small" plain>查看详情</wd-button> -->
</template>
</wd-card>
</view>
</template>
<script lang="ts">
import { useColPickerData, useModal } from '@/hooks'
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
// import { setToken, getToken, isLogin } from '@/utils/auth';
const { colPickerData, findChildrenByCode } = useColPickerData()
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
propShowOrder: false,
area: [],
value: [],
orderInfo: {
name: '',
phone: '',
district_name: [],
street_name: '',
remark: '',
},
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
this.changeSP(1)
this.getArea()
},
created () {
},
methods: {
close () {
this.propShowOrder = false;
},
handleSubmit () {
this.propShowOrder = true;
},
//
addOrder () {
this.$refs.form.validate()
.then(({ valid, errors }) => {
if (valid) {
this.menu_list[this.currentTab].is_set = 1;
this.menu_list[this.currentTab].orderInfo = this.orderInfo;
// this.menu_list[this.currentTab].orderInfo.columns = this.columns[this.currentTab].value;
// this.menu_list[this.currentTab].orderInfo.total_price = this.totalPrice;
this.close();
}
})
.catch((error) => {
console.log(error, 'error')
})
},
getArea () {
this.area = [colPickerData.map(item => ({
value: item.value,
label: item.text
}))];
},
handleConfirm (value) {
console.log(value)
console.log(this.value)
},
columnChange ({ selectedItem, resolve, finish }) {
try {
const areaData = findChildrenByCode(colPickerData, selectedItem.value);
if (areaData && areaData.length) {
resolve(areaData.map(item => ({
value: item.value,
label: item.text
})));
} else {
finish();
}
} catch (error) {
console.error('处理列变更时出错:', error);
finish(); //
}
},
handleClickCategory (index: number) {
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
console.log(id);
uni.navigateTo({
url: `/pages/common/goods/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,406 @@
<style lang='scss'>
html,
body {
height: 0px;
}
</style>
<template>
<view>
<u-popup :show="propShowOrder" :round="10" mode="bottom" @close="close" customStyle="height: 1000rpx;">
<wd-form ref="form" :model="orderInfo" errorType="message">
<wd-cell-group border>
<view style="padding: 20rpx;">
<u-cell class="" style="line-height: 60rpx;" :border="true">
<template #title>
<view class="h-50 align-center font-bold" @click="close">
填写{{ menu_name }}信息
</view>
</template>
</u-cell>
<scroll-view style="height: 1000rpx;" scroll-y="true">
<u-alert :show-icon="true" title="温馨提示:确定前请检查日期时间段" type="" effect="dark" description=" "></u-alert>
<view class="pb-4" />
<wd-calendar label="做宴日期" placeholder="请选择日期" prop="time" :formatter="formatter" :min-date="min_data"
v-model="orderInfo.time" :rules="[{ required: false, pattern: /^\d{13}$/, message: '请选择日期' }]"
@confirm="handleTimeConfirm" />
<wd-select-picker label="做宴时段" v-model="orderInfo.columns" :columns="columns"
type="radio"></wd-select-picker>
<wd-input label="做宴桌数" placeholder="请输做宴桌数" prop="total_num" type="number" clearable
v-model="orderInfo.total_num" :rules="[{ required: false, pattern: /^[1-9]\d*$/, message: '请输入有效的桌数' }]"
placeholder-style="color: #999999; font-size: 14px;" />
</scroll-view>
</view>
<view class="prop-flex-bottom">
<up-button class="" style="" text="确认当餐信息" type="primary" color="#59CB56" shape="circle" size="normal"
custom-style="width: calc(100vw - 100rpx);" @click="addOrder">
</up-button>
</view>
</wd-cell-group>
</wd-form>
</u-popup>
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`选择${menu_name}`"
titleColor="#303133" bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx"
leftIconColor="#303133" :safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<up-search v-model="search.keyword" class="flex,flex-row p-1" style="" shape="round" bgColor="#f2f2f2"
placeholder="输入食谱或者套餐" :clearabled="true" :showAction="false" inputAlign="left" borderColor="transparent"
searchIconColor="#909399" color="#606266" placeholderColor="#909399" searchIcon="search" margin="10rpx"
maxlength="-1" height="60rpx" @change="searchSP()">
</up-search>
<view style="width: calc(100vw - 180rpx);display: inline-block;">
<wd-tabs v-model="currentTab" @change="handleChange" auto-line-width color="#59CB56" lineWidth="80rpx">
<block v-for="(item, index) in menu_list" :key="item">
<wd-tab :title="`第${index + 1}餐`" :name="index" setActive="item.name">
</wd-tab>
</block>
</wd-tabs>
</view>
<wd-button plain @click="handleSubmit" type="error"
custom-style="width:160rpx;float:right;margin-left:0rpx;margin:10rpx" size="small">
<wd-icon name="edit" size="24rpx"></wd-icon>设置餐标</wd-button>
<!-- <wd-button type="text" icon="plus" icon-position="right">+加餐</wd-button> -->
<z-paging :fixed="false" height="calc(100vh - 440rpx)" width="200rpx" loading-more-default-text=""
style=" left:0; padding: 0px; position: fixed;" class="fv-page flex-col px-4">
<wd-sidebar v-model="childrenIndex">
<view v-for="(item, index) in tabList" :key="index" @click="handleChildrenClick(index)">
<wd-sidebar-item :value="index" :label="item.name" :badge="item.num" customStyle="font-size:24rpx;" />
</view>
</wd-sidebar>
<!-- <u-scroll-list :indicator="false">
<view class="scroll-list" style="flex-direction: column;">
<view class="scroll-list__goods-item-column " v-for="(item, index) in tabList" :key="index"
:class="(index === childrenIndex) ? 'column-active' : 'column-active-false'"
@click="handleChildrenClick(index)">
<slot name="tabItem">
</slot>
<text class="scroll-list__goods-item__text">
<wd-badge :modelValue="item.num ? item.num : 1" top="-8rpx" right="-8rpx" bg-color="#59CB56 ">
{{ item.name }}
</wd-badge>
</text>
</view>
</view>
</u-scroll-list> -->
</z-paging>
<z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 440rpx)"
width="calc(100vw - 200rpx)" style="right: 0; background-color: #FFFFFFFF; position: fixed;"
class="fv-page flex-col">
<view v-for="(item, index) in matchedItems_sp" :key="index">
<up-cell :border='false' @click="goodsDetail(item.id)">
<template #icon>
<up-image :src="item.image" width="140rpx" height="140rpx"></up-image>
</template>
<template v-slot:title>
<view class="h-50 flex">
<up-text size="28rpx" :text="`${item.name}`" :flex1="true" align="left" wordWrap="normal" :show="true"
prefixIcon="" iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex">
<up-text color="#767676" size="24rpx" :text="`配置:${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex ellipsis_text">
<up-text color="#fa4e62" size="28rpx" :text="`¥36.00 元${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<view @click.stop="addCart(item.id)">
<wd-input-number v-model="totalNum" @change="addCart" input-width="60rpx" />
</view>
</view>
</template>
</up-cell>
</view>
</z-paging>
<!-- // -->
<wd-fab :draggable="true" position="right-center" inactiveIcon="edit">
<view style="width: 200rpx;padding: 10x;">
<wd-button plain @click="addGoods" custom-style="width:180rpx;float:right;margin:10rpx;" size="small"> <wd-icon
name="add" size="22rpx"></wd-icon> 添加餐次</wd-button>
<wd-button plain @click="delGoods" custom-style="width:180rpx;float:right;margin:10rpx;" type="error"
size="small"><wd-icon name="delete" size="22rpx"></wd-icon>删除餐次</wd-button>
</view>
</wd-fab>
<view class="flex-sub box-border flex" style="bottom: 10rpx;padding: 10rpx;">
<view style="width:calc(100vw - 180rpx); margin: 0rpx 0rpx 0rpx 20rpx;" width="">
<text>已选{{this.menu_list.length}},合计<text color-red>213213</text></text>
</view>
<up-button class="" style="" text="选好了" type="primary" color="#59CB56" shape="circle" size="small"
custom-style="width:200rpx;float:right;margin-right:0rpx;" @click="handleConfirm" />
</view>
</view>
</template>
<script lang="ts">
// useColPickerData
//
import { useColPickerData,useModal } from '@/hooks'
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { get } from 'http';
// import { setToken, getToken, isLogin } from '@/utils/auth';
// const { colPickerData, findChildrenByCode } = useColPickerData()
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
tpl: '确定前检查日期时间段',
propShowOrder: false,
value: [],
totalNum: 0,
orderInfo: {
time: '',
columns: '早上',
total_num: 1,
total_price: 0,
},
newOrderInfo: {
time: '',
columns: '早上',
total_num: 1,
total_price: 0,
},
totalPrice: 0,
currentTab: 0, // tab
min_data: new Date().getTime(),
isDisabledDate: [1754755200000, 1754755200000],
// time_list: [],
menu_name: '第1餐',
menu_list: [{
id: 0, is_set: 0, name: '第1餐', orderInfo: {
time: '',
columns: '早上',
total_num: 1,
total_price: 0,
},
}],
columns: [{ value: '早上', label: '早上' }, { value: '中午', label: '中午' }, { value: '晚上', label: '晚上' }],
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
},
created () {
},
methods: {
addGoods () {
//
var new_menu_name = '第' + parseInt(this.menu_list.length + 1) + '餐';
this.menu_list.push({ id: 0, is_set: 0, name: new_menu_name , orderInfo: this.newOrderInfo });
},
delGoods () {
if (this.menu_list.length === 1) {
return uni.$u.toast('至少要保留一个餐项');
}
useModal().showModal('success', {
title: '删除提示',
content: '确定删除'+this.menu_name+'吗?',
showCancel: true,
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
//
const index = this.currentTab; // currentTab
if (index !== undefined && index >= 0 && index < this.menu_list.length) {
this.menu_list.splice(index, 1);
}
// CommonApi.commonPost('/api/user/deleteAddress', { id: id }).catch((res) => {
// if (res.code === 1) {
// // this.propShowAddress = false;
// this.getAddressInfoListData();
// } else {
// uni.$u.toast(res.msg);
// }
// });
}
else if (res.cancel) {
// console.log('');
}
},
});
},
//
handleChange () {
this.$nextTick(() => {
this.handleClickCategory(this.currentTab)
this.menu_name = this.menu_list[this.currentTab].name; // menu_name
this.orderInfo = this.menu_list[this.currentTab].orderInfo; // orderInfo
});
},
//
addOrder () {
this.$refs.form.validate()
.then(({ valid, errors }) => {
if (valid) {
this.menu_list[this.currentTab].is_set = 1;
this.menu_list[this.currentTab].orderInfo = this.orderInfo;
// this.menu_list[this.currentTab].orderInfo.columns = this.columns[this.currentTab].value;
// this.menu_list[this.currentTab].orderInfo.total_price = this.totalPrice;
this.close();
}
})
.catch((error) => {
console.log(error, 'error')
})
},
close () {
this.propShowOrder = false;
},
handleSubmit () {
this.propShowOrder = true;
},
//
handleConfirm () {
// console.log(value)
var id = 1;
uni.navigateTo({
url: `/pages/banquet/combo/detail?id=${id}`
});
},
formatter (day) {
const date = new Date(day.date)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth()
const da = date.getDate()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth()
const nowDa = now.getDate()
if (year === nowYear && month === nowMonth && da === nowDa) {
day.topInfo = '今天'
}
// const disabledDates = ['2025-08-05', '2023-08-15'];
// const formattedDate = `${year}-${month}-${da}`;
if (this.isDisabledDate.includes(day.date)) {
day.topInfo = '已预定';
day.disabled = true; //
day.disabled_class = 'disabled-date'; //
}
return day
},
handleTimeConfirm (res) {
console.log(res.value)
this.orderInfo.time = res.value
},
//
handleClickCategory (index: number) {
console.log(index);
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
// console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
// console.log(id);
uni.navigateTo({
url: `/pages/common/goods/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,206 @@
<template>
<view class="fv-page flex-col" style="">
<!-- <up-navbar class="" style="" :autoBack="true" bgColor="#00000000" :fixed="true" titleColor="#595757FF"
leftIconColor="#FFFFFFFF">
</up-navbar> -->
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`厨师详情`" titleColor="#303133"
bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133"
:safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<wd-card type="rectangle">
<up-cell-group :border="false">
<view class="">
<u-cell :border="false">
<template #icon>
<wd-img :width="120" :height="120" :src="matchedItems_sc.image" :enable-preview="false" :radius="8"
:round=false custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="32rpx" :text="`${matchedItems_sc.name}大厨师`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
</view>
</template>
<template #label>
<view class="h-50 flex color-gray">
<wd-tag custom-class="space">家常菜</wd-tag>
<wd-tag custom-class="space" type="primary">川菜</wd-tag>
<wd-tag custom-class="space" type="danger">粤菜</wd-tag>
<wd-tag custom-class="space" type="warning">湘菜</wd-tag>
<wd-tag custom-class="space" type="success">特色菜</wd-tag>
<!-- <u-text size="" :text="`${matchedItems_sc.intro}川菜 家常菜`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</u-text> -->
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${matchedItems_sc.intro}最大承载人数6000人`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${matchedItems_sc.intro}四川省泸州市龙马潭区`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<wd-button size="small" @click="addCart(1)" >下单预定</wd-button>
</template>
</u-cell>
</view>
</up-cell-group>
</wd-card>
<wd-card title="厨师展示视频" type="rectangle">
<up-swiper v-if="imageList" height="320rpx" :list="imageList" keyName="url" :autoplay="false"></up-swiper>
</wd-card>
<wd-card title="档期日历" type="rectangle">
<wd-calendar-view v-model="time_value" :formatter="formatter" :min-date="min_data" :max_data="max_data" @change="handleChange" />
</wd-card>
<view class="flex box-border flex-sub-cart" v-if="is_add">
<view style="margin: 20rpx;">
<!-- <u-text class=""
custom-style="width: calc(100vw - 120rpx);text-align:center;display: flex; justify-content: center; align-items: center;height: 70rpx;border: 2px solid #59cb56;border-radius: 40rpx;background-color: #5acb5617;margin: 0rpx 20rpx!important;"
color="#59CB56" size="26rpx" :text="'加入食谱清单'" :flex1="true" align="center" wordWrap="normal" :show="true"
iconStyle="26rpx" decoration="none">
</u-text> -->
</view>
</view>
</view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { setToken, getToken, isLogin } from '@/utils/auth';
import { image } from "@/uni_modules/uview-plus/libs/function/test";
import { url } from "inspector";
export default defineComponent({
data () {
return {
search: {
id: 0,
},
is_add: 0, //
time_value:[],
min_data: new Date().getTime(),
max_data: this.calculateMaxDate(),
isDisabledDate: [1754755200000, 1754755200000],
matchedItems_sc: [],
matchedItems_sp: [],
imageList: [],
};
},
onLoad (option) {
// console.log(option);
this.search.id = option.id;
this.is_add = option.is_add | 0;
this.getCategoryList()
this.changeSP()
},
created () {
},
methods: {
//
calculateMaxDate() {
const currentDate = new Date();
const futureDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
return futureDate.getTime();
},
formatter (day) {
const date = new Date(day.date)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth()
const da = date.getDate()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth()
const nowDa = now.getDate()
if (year === nowYear && month === nowMonth && da === nowDa) {
day.topInfo = '今天'
}
// const disabledDates = ['2025-08-05', '2023-08-15'];
// const formattedDate = `${year}-${month}-${da}`;
if (this.isDisabledDate.includes(day.date)) {
day.topInfo = '已预定';
day.disabled = true; //
day.disabled_class = 'disabled-date'; //
}
return day
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/detail', this.search).catch((res) => {
if (res.code === 1) {
this.matchedItems_sc = res.data;
//
this.imageList = res.data.image ? [{
url: res.data.image,
title: res.data.name,
poster: res.data.image
}] : [];
} else {
uni.$u.toast(res.msg);
}
});
},
//
changeSP () {
// console.log(pageNo);
// this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
this.matchedItems_sp = res.data;
// this.$refs.pagingRefSP?.complete(res.data);
} else {
uni.$u.toast(res.msg);
}
});
},
//
addCart (id) {
uni.navigateTo({
url: `/pages/banquet/combo/index??id=${id}`
});
// CommonApi.commonPost('/api/cart/add', {
// buy_now: false,
// dishes_id: this.search.id,
// }).catch((res) => {
// if (res.code === 1) {
// uni.$u.toast('');
// uni.navigateTo({
// url: '/pages/cart/index'
// });
// } else {
// uni.$u.toast(res.msg);
// }
// });
}
},
});
</script>

View File

@ -0,0 +1,273 @@
<template>
<view>
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`厨师`" titleColor="#303133"
bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133"
:safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<up-search v-model="search.keyword" class="flex,flex-row p-1" style="" shape="round" bgColor="#f2f2f2"
placeholder="请输入厨师名称或者区域" :clearabled="true" :showAction="false" inputAlign="left" borderColor="transparent"
searchIconColor="#909399" color="#606266" placeholderColor="#909399" searchIcon="search" margin="10rpx"
maxlength="-1" height="60rpx" @change="searchSP()">
</up-search>
<z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 280rpx)"
width="calc(100vw)" style="right: 0; background-color: #FFFFFFFF; position: fixed;" class="fv-page flex-col">
<wd-card type="rectangle">
<up-cell-group :border="false">
<view class="detail-box" v-for="(item, index) in matchedItems_sp" :key="index">
<u-cell :border="false" @click="goodsDetail(item.id)">
<template #icon>
<wd-img :width="100" :height="100" :src="item.image" :enable-preview="false" :radius="8" :round=false
custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="32rpx" :text="`${item.name}大厨师`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
</view>
</template>
<template #label>
<view class="h-50 flex color-gray">
<wd-tag custom-class="space">家常菜</wd-tag>
<wd-tag custom-class="space" type="primary">川菜</wd-tag>
<wd-tag custom-class="space" type="danger">粤菜</wd-tag>
<wd-tag custom-class="space" type="warning">湘菜</wd-tag>
<wd-tag custom-class="space" type="success">特色菜</wd-tag>
<!-- <u-text size="" :text="`${item.intro}川菜 家常菜`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</u-text> -->
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${item.intro}最大承载人数6000人`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${item.intro}四川省泸州市龙马潭区`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
</template>
</u-cell>
</view>
</up-cell-group>
</wd-card>
</z-paging>
</view>
</template>
<script lang="ts">
// useColPickerData
//
import { useColPickerData } from '@/hooks'
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { get } from 'http';
// import { setToken, getToken, isLogin } from '@/utils/auth';
const { colPickerData, findChildrenByCode } = useColPickerData()
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
// area: useColPickerData().colPickerData.map((item) => {
// return {
// value: item.value,
// label: item.text
// }
// }),
area: [],
value: [],
currentTab: 0, // tab
min_data: new Date().getTime(),
isDisabledDate: [1754755200000, 1754755200000],
time_list: [],
menu_list: [{ id: 0, name: '第一餐' }, { id: 1, name: '第二餐' }, { id: 2, name: '第三餐' }],
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
this.getArea()
},
created () {
},
methods: {
handleConfirm ({ value }) {
console.log(value)
},
getArea () {
try {
this.area = [colPickerData.map(item => ({
value: item.value,
label: item.text
}))];
} catch (error) {
console.error('获取区域数据时出错:', error);
uni.$u.toast('获取区域数据失败');
}
},
columnChange ({ selectedItem, resolve, finish }) {
try {
const areaData = findChildrenByCode(colPickerData, selectedItem.value);
if (areaData && areaData.length) {
resolve(areaData.map(item => ({
value: item.value,
label: item.text
})));
} else {
finish();
}
} catch (error) {
console.error('处理列变更时出错:', error);
finish(); //
}
},
formatter (day) {
const date = new Date(day.date)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth()
const da = date.getDate()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth()
const nowDa = now.getDate()
if (year === nowYear && month === nowMonth && da === nowDa) {
day.topInfo = '今天'
}
// const disabledDates = ['2025-08-05', '2023-08-15'];
// const formattedDate = `${year}-${month}-${da}`;
if (this.isDisabledDate.includes(day.date)) {
day.topInfo = '已预定';
day.disabled = true; //
day.disabled_class = 'disabled-date'; //
}
// if (day.type === 'start') {
// day.bottomInfo = ''
// }
// if (day.type === 'end') {
// day.bottomInfo = ''
// }
// if (day.type === 'same') {
// day.bottomInfo = '/'
// }
return day
},
handleTimeConfirm (res) {
console.log(res.value)
this.time_list = res.value
},
handleChange () {
this.$nextTick(() => { this.handleClickCategory(this.currentTab) });
},
handleClickCategory (index: number) {
console.log(index);
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
// console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
// console.log(id);
uni.navigateTo({
url: `/pages/banquet/cook/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,218 @@
<style>
.wd-grid-item-img {
width: 100%;
height: 100px;
border-radius: 20rpx;
}
.wd-grid-item-img-cook {
width: 100%;
height: 180px;
border-radius: 20rpx;
}
</style>
<template>
<view>
<up-navbar class=" " style="font-weight: bold;" leftIcon="" title="首页" titleColor="#303133" bgColor="#FFFFFFFF"
titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133" :safeAreaInsetTop="true"
:placeholder="true" :fixed="true"></up-navbar>
<view style="padding:0px 20rpx"> <u-swiper radius="20rpx" height="360rpx" :list="swiperList" indicator
indicatorMode="dot" circular @click="handleClick"></u-swiper></view>
<wd-grid :gutter="10" :column="2" clickable>
<wd-grid-item use-slot @click="goTabCook">
<image class="wd-grid-item-img" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<!-- <view class="text font-bold" >找厨师</view> -->
</wd-grid-item>
<wd-grid-item use-slot @click="goTabHotel">
<image class="wd-grid-item-img" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<!-- <view class="text font-bold">找酒店</view> -->
</wd-grid-item>
<wd-grid-item use-slot url="/pages/banquet/cook/detail?id=1&is_add=1">
<image class="wd-grid-item-img-cook" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<view class="text font-bold">张德标</view>
</wd-grid-item>
<wd-grid-item use-slot url="/pages/banquet/cook/detail?id=2&is_add=1">
<image class="wd-grid-item-img-cook" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<view class="text font-bold">张德标</view>
</wd-grid-item>
<wd-grid-item use-slot url="/pages/banquet/cook/detail?id=3&is_add=1">
<image class="wd-grid-item-img-cook" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<view class="text font-bold">张德标</view>
</wd-grid-item>
<wd-grid-item use-slot url="/pages/banquet/cook/detail?id=4&is_add=1">
<image class="wd-grid-item-img-cook" src="https://cdn.uviewui.com/uview/goods/1.jpg" />
<view class="text font-bold">张德标</view>
</wd-grid-item>
</wd-grid>
<!-- <z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 440rpx)"
width="calc(100vw - 200rpx)" style="right: 0; background-color: #FFFFFFFF; position: fixed;"
class="fv-page flex-col">
<view v-for="(item, index) in matchedItems_sp" :key="index">
<up-cell :border='false' @click="goodsDetail(item.id)">
<template #icon>
<up-image :src="item.image" width="140rpx" height="140rpx"></up-image>
</template>
<template v-slot:title>
<view class="h-50 flex">
<up-text size="28rpx" :text="`${item.name}`" :flex1="true" align="left" wordWrap="normal" :show="true"
prefixIcon="" iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex">
<up-text color="#767676" size="24rpx" :text="`主料:${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex ellipsis_text">
<up-text color="#767676" size="24rpx" :text="`营养:${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<view @click.stop="addCart(item.id)">
<u-icon slot="right" label="" size="42rpx" name="plus-circle" color="#59CB56"
labelColor="#59CB56"></u-icon>
</view>
</view>
</template>
</up-cell>
</view>
</z-paging> -->
</view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { setToken, getToken, isLogin } from '@/utils/auth';
export default defineComponent({
data () {
return {
swiperList: [
{ url: 'https://cdn.uviewui.com/uview/goods/1.jpg', title: '小熊猫!' },
{ url: 'https://cdn.uviewui.com/uview/album/1.jpg', title: '卡!皮!巴!拉!' },
],
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
},
created () {
},
methods: {
goTabCook () {
uni.switchTab({
url: '/pages/banquet/cook/list'
});
},
goTabHotel () {
uni.switchTab({
url: '/pages/banquet/hotel/list'
});
},
//
handleClick (e) {
console.log(e)
},
// onChange (e) {
// console.log(e)
// },
handleClickCategory (index: number) {
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
console.log(id);
uni.navigateTo({
url: `/pages/common/goods/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,244 @@
<template>
<view class="fv-page flex-col" style="">
<!-- <up-navbar class="" style="" :autoBack="true" bgColor="#00000000" :fixed="true" titleColor="#595757FF"
leftIconColor="#FFFFFFFF">
</up-navbar> -->
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`酒店详情`" titleColor="#303133"
bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133"
:safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<wd-card type="rectangle">
<up-cell-group :border="false">
<view class="">
<u-cell :border="false">
<template #icon>
<wd-img :width="120" :height="120" :src="matchedItems_sc.image" :enable-preview="false" :radius="8"
:round=false custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="32rpx" :text="`${matchedItems_sc.name}大酒店`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
</view>
</template>
<template #label>
<view class="h-50 flex color-gray">
<wd-tag custom-class="space">家常菜</wd-tag>
<wd-tag custom-class="space" type="primary">川菜</wd-tag>
<wd-tag custom-class="space" type="danger">粤菜</wd-tag>
<wd-tag custom-class="space" type="warning">湘菜</wd-tag>
<wd-tag custom-class="space" type="success">特色菜</wd-tag>
<!-- <u-text size="" :text="`${matchedItems_sc.intro}川菜 家常菜`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</u-text> -->
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${matchedItems_sc.intro}最大承载人数6000人`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${matchedItems_sc.intro}四川省泸州市龙马潭区`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<wd-button size="small" @click="addCart(1)" >下单预定</wd-button>
</template>
</u-cell>
</view>
</up-cell-group>
</wd-card>
<wd-card title="酒店展示视频" type="rectangle">
<up-swiper v-if="imageList" height="320rpx" :list="imageList" keyName="url" :autoplay="false"></up-swiper>
</wd-card>
<wd-card title="推荐酒席套餐" type="rectangle">
<up-cell-group :border="false">
<view class="detail-box" v-for="(item, index) in matchedItems_sp" :key="index">
<u-cell :border="false" @click="goodsDetail(item.id)">
<template #icon>
<wd-img :width="100" :height="100" :src="item.image" :enable-preview="false" :radius="8" :round=false
custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="32rpx" :text="`${item.name}大酒店`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
</view>
</template>
<template #label>
<view class="h-50 flex color-gray">
<wd-tag custom-class="space">家常菜</wd-tag>
<wd-tag custom-class="space" type="primary">川菜</wd-tag>
<wd-tag custom-class="space" type="danger">粤菜</wd-tag>
<wd-tag custom-class="space" type="warning">湘菜</wd-tag>
<wd-tag custom-class="space" type="success">特色菜</wd-tag>
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`最大承载人数6000人${item.intro}`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`四川省泸州市龙马潭区${item.intro}`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<wd-text text="16354.156" mode="price" type="error" prefix="¥" />
</view>
<view class="h-20 flex color-gray">
<wd-text size="24rpx" text="16354.156" mode="price" decoration="line-through" prefix="¥" />
<view style="margin-top: -20rpx;">
<wd-button size="small" @click="addCart(1)" >下单预定</wd-button>
</view>
</view>
</template>
</u-cell>
</view>
</up-cell-group>
</wd-card>
<view class="flex box-border flex-sub-cart" v-if="is_add">
<view style="margin: 20rpx;">
<!-- <u-text class=""
custom-style="width: calc(100vw - 120rpx);text-align:center;display: flex; justify-content: center; align-items: center;height: 70rpx;border: 2px solid #59cb56;border-radius: 40rpx;background-color: #5acb5617;margin: 0rpx 20rpx!important;"
color="#59CB56" size="26rpx" :text="'加入食谱清单'" :flex1="true" align="center" wordWrap="normal" :show="true"
iconStyle="26rpx" decoration="none">
</u-text> -->
</view>
</view>
</view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { setToken, getToken, isLogin } from '@/utils/auth';
import { image } from "@/uni_modules/uview-plus/libs/function/test";
import { url } from "inspector";
export default defineComponent({
data () {
return {
search: {
id: 0,
},
is_add: 0, //
matchedItems_sc: [],
matchedItems_sp: [],
imageList: [],
};
},
onLoad (option) {
// console.log(option);
this.search.id = option.id;
this.is_add = option.is_add | 0;
this.getCategoryList()
this.changeSP()
},
created () {
},
methods: {
getCategoryList () {
CommonApi.commonGet('/api/dishes/detail', this.search).catch((res) => {
if (res.code === 1) {
this.matchedItems_sc = res.data;
//
this.imageList = res.data.image ? [{
url: res.data.image,
title: res.data.name,
poster: res.data.image
}] : [];
} else {
uni.$u.toast(res.msg);
}
});
},
//
changeSP () {
// console.log(pageNo);
// this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
this.matchedItems_sp = res.data;
// this.$refs.pagingRefSP?.complete(res.data);
} else {
uni.$u.toast(res.msg);
}
});
},
//
addCart (id) {
uni.navigateTo({
url: `/pages/banquet/combo/index??id=${id}`
});
// CommonApi.commonPost('/api/cart/add', {
// buy_now: false,
// dishes_id: this.search.id,
// }).catch((res) => {
// if (res.code === 1) {
// uni.$u.toast('');
// uni.navigateTo({
// url: '/pages/cart/index'
// });
// } else {
// uni.$u.toast(res.msg);
// }
// });
}
},
});
// const imageList = reactive([
// {
// url: 'https://cdn.uviewui.com/uview/resources/video.mp4',
// title: '西',
// poster: 'https://cdn.uviewui.com/uview/swiper/swiper1.png'
// },
// {
// url: 'https://s3.bmp.ovh/imgs/2024/12/16/35bc6d28ab1c8bc7.png',
// title: '',
// // url URLURL
// // url URL
// },
// {
// url: 'https://cdn.uviewui.com/uview/swiper/swiper3.png',
// title: '西',
// // url URLURL
// //
// },
// ]);
</script>

View File

@ -0,0 +1,273 @@
<template>
<view>
<up-navbar :autoBack="true" style="font-weight: bold;" leftIcon="arrow-left" :title="`酒店`" titleColor="#303133"
bgColor="#FFFFFFFF" titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133"
:safeAreaInsetTop="true" :placeholder="true" :fixed="true">
</up-navbar>
<up-search v-model="search.keyword" class="flex,flex-row p-1" style="" shape="round" bgColor="#f2f2f2"
placeholder="请输入酒店名称或者区域" :clearabled="true" :showAction="false" inputAlign="left" borderColor="transparent"
searchIconColor="#909399" color="#606266" placeholderColor="#909399" searchIcon="search" margin="10rpx"
maxlength="-1" height="60rpx" @change="searchSP()">
</up-search>
<z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 280rpx)"
width="calc(100vw)" style="right: 0; background-color: #FFFFFFFF; position: fixed;" class="fv-page flex-col">
<wd-card type="rectangle">
<up-cell-group :border="false">
<view class="detail-box" v-for="(item, index) in matchedItems_sp" :key="index">
<u-cell :border="false" @click="goodsDetail(item.id)">
<template #icon>
<wd-img :width="100" :height="100" :src="item.image" :enable-preview="false" :radius="8" :round=false
custom-class="margin-right-24" />
</template>
<template #title>
<view class="h-60 flex">
<up-text size="32rpx" :text="`${item.name}大酒店`" :flex1="true" align="left" wordWrap="normal"
:show="true" prefixIcon="" customStyle="font-weight:bold"
iconStyle="font-size:28rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`¥2000.00元`"></u-icon> -->
</view>
</template>
<template #label>
<view class="h-50 flex color-gray">
<wd-tag custom-class="space">家常菜</wd-tag>
<wd-tag custom-class="space" type="primary">川菜</wd-tag>
<wd-tag custom-class="space" type="danger">粤菜</wd-tag>
<wd-tag custom-class="space" type="warning">湘菜</wd-tag>
<wd-tag custom-class="space" type="success">特色菜</wd-tag>
<!-- <u-text size="" :text="`${item.intro}川菜 家常菜`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</u-text> -->
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${item.intro}最大承载人数6000人`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
<view class="h-50 flex color-gray">
<up-text size="" :text="`${item.intro}四川省泸州市龙马潭区`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon="" customStyle="font-size:24rpx"
iconStyle="font-size:24rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<!-- <u-icon slot="right" label-color="red" :label="`X30`"></u-icon> -->
</view>
</template>
</u-cell>
</view>
</up-cell-group>
</wd-card>
</z-paging>
</view>
</template>
<script lang="ts">
// useColPickerData
//
import { useColPickerData } from '@/hooks'
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { get } from 'http';
// import { setToken, getToken, isLogin } from '@/utils/auth';
const { colPickerData, findChildrenByCode } = useColPickerData()
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
// area: useColPickerData().colPickerData.map((item) => {
// return {
// value: item.value,
// label: item.text
// }
// }),
area: [],
value: [],
currentTab: 0, // tab
min_data: new Date().getTime(),
isDisabledDate: [1754755200000, 1754755200000],
time_list: [],
menu_list: [{ id: 0, name: '第一餐' }, { id: 1, name: '第二餐' }, { id: 2, name: '第三餐' }],
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
this.getArea()
},
created () {
},
methods: {
handleConfirm ({ value }) {
console.log(value)
},
getArea () {
try {
this.area = [colPickerData.map(item => ({
value: item.value,
label: item.text
}))];
} catch (error) {
console.error('获取区域数据时出错:', error);
uni.$u.toast('获取区域数据失败');
}
},
columnChange ({ selectedItem, resolve, finish }) {
try {
const areaData = findChildrenByCode(colPickerData, selectedItem.value);
if (areaData && areaData.length) {
resolve(areaData.map(item => ({
value: item.value,
label: item.text
})));
} else {
finish();
}
} catch (error) {
console.error('处理列变更时出错:', error);
finish(); //
}
},
formatter (day) {
const date = new Date(day.date)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth()
const da = date.getDate()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth()
const nowDa = now.getDate()
if (year === nowYear && month === nowMonth && da === nowDa) {
day.topInfo = '今天'
}
// const disabledDates = ['2025-08-05', '2023-08-15'];
// const formattedDate = `${year}-${month}-${da}`;
if (this.isDisabledDate.includes(day.date)) {
day.topInfo = '已预定';
day.disabled = true; //
day.disabled_class = 'disabled-date'; //
}
// if (day.type === 'start') {
// day.bottomInfo = ''
// }
// if (day.type === 'end') {
// day.bottomInfo = ''
// }
// if (day.type === 'same') {
// day.bottomInfo = '/'
// }
return day
},
handleTimeConfirm (res) {
console.log(res.value)
this.time_list = res.value
},
handleChange () {
this.$nextTick(() => { this.handleClickCategory(this.currentTab) });
},
handleClickCategory (index: number) {
console.log(index);
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
// console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
// console.log(id);
uni.navigateTo({
url: `/pages/banquet/hotel/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,207 @@
<style lang='scss'>
.ellipsis {
white-space: nowrap;
/* 防止文字换行 */
overflow: hidden;
/* 隐藏超出部分的文字 */
text-overflow: ellipsis;
/* 在末尾显示省略号 */
// width: 260rpx!important;
width: calc(100vw - 80rpx) !important;
/* 确保元素有宽度 */
}
.ellipsis_text {
// width: 340rpx !important;
// width: calc(100vw - 80rpx)!important;
}
.zp-l-text-rpx {
display: none !important;
}
html,
body {
height: 0px;
}
</style>
<template>
<view>
<up-navbar class=" " style="font-weight: bold;" leftIcon="" title="食谱菜单" titleColor="#303133" bgColor="#FFFFFFFF"
titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133" :safeAreaInsetTop="true"
:placeholder="true" :fixed="true"></up-navbar>
<up-search v-model="search.keyword" class="flex,flex-row p-1" style="" shape="round" bgColor="#f2f2f2"
placeholder="输入食谱" :clearabled="true" :showAction="false" inputAlign="left" borderColor="transparent"
searchIconColor="#909399" color="#606266" placeholderColor="#909399" searchIcon="search" margin="10rpx"
maxlength="-1" height="60rpx" @change="searchSP()">
</up-search>
<u-scroll-list :indicator="false">
<view class="scroll-list" style="flex-direction: row;width: 56px;">
<view class="scroll-list__goods-item " v-for="(item, index) in categoryList" :key="index"
:class="(index === categoryIndex) ? 'row-active' : ''" @click="handleClickCategory(index)"
:style="{ backgroundColor: item.bgColor }">
<image class="scroll-list__goods-item__image"
:src="item.image ? item.image : 'https://s3.bmp.ovh/imgs/2024/12/16/35bc6d28ab1c8bc7.png'"
:class="(index === categoryIndex) ? 'border-row-active' : ''"></image>
<text class="scroll-list__goods-item__text">{{ item.name }}</text>
</view>
</view>
</u-scroll-list>
<z-paging :fixed="false" height="calc(100vh - 440rpx)" width="200rpx" loading-more-default-text=""
style=" left:0; padding: 0px; position: fixed;" class="fv-page flex-col px-4">
<u-scroll-list :indicator="false">
<view class="scroll-list" style="flex-direction: column;">
<view class="scroll-list__goods-item-column " v-for="(item, index) in tabList" :key="index"
:class="(index === childrenIndex) ? 'column-active' : ''" @click="handleChildrenClick(index)"
:style="{ backgroundColor: item.bgColor }">
<!-- <image class="scroll-list__goods-item__image" :src="item.image"></image> -->
<slot name="tabItem">
</slot>
<!-- <view style="padding: 20px 0px;"> </view> -->
<text class="scroll-list__goods-item__text">{{ item.name }}</text>
</view>
</view>
</u-scroll-list>
</z-paging>
<z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 440rpx)"
width="calc(100vw - 200rpx)" style="right: 0; background-color: #FFFFFFFF; position: fixed;"
class="fv-page flex-col">
<view v-for="(item, index) in matchedItems_sp" :key="index">
<up-cell :border='false' @click="goodsDetail(item.id)">
<template #icon>
<up-image :src="item.image" width="140rpx" height="140rpx"></up-image>
</template>
<template v-slot:title>
<view class="h-50 flex">
<up-text size="28rpx" :text="`${item.name}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex">
<up-text color="#767676" size="24rpx" :text="`主料:${item.intro}`" :flex1="true"
align="left" wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1"
decoration="none">
</up-text>
</view>
<view class="h-50 flex ellipsis_text" >
<up-text color="#767676" size="24rpx" :text="`营养:${item.intro}`" :flex1="true"
align="left" wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1"
decoration="none">
</up-text>
<view @click.stop="addCart(item.id)">
<u-icon slot="right" label="" size="42rpx" name="plus-circle" color="#59CB56" labelColor="#59CB56"></u-icon>
</view>
</view>
</template>
</up-cell>
</view>
</z-paging>
</view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { setToken, getToken, isLogin } from '@/utils/auth';
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
},
created () {
},
methods: {
handleClickCategory (index: number) {
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP(){
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
console.log(id);
uni.navigateTo({
url: `/pages/common/goods/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add',{
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -0,0 +1,332 @@
<style lang='scss'>
html,
body {
height: 0px;
}
.disabled-date>.wd-month__day-container>.wd-month__day-text {
color: #ffffff !important;
}
.disabled-date {
border-radius: var(--wot-calendar-active-border, 8px);
background: var(--wot-calendar-active-color, var(--wot-color-theme, #366cff));
// background-color: #78da1c;
/* 禁用日期的背景颜色 */
color: #ffffff;
/* 禁用日期的文字颜色 */
pointer-events: none;
/* 禁用日期的点击事件 */
// opacity: 0.5;
/* 禁用日期的透明度 */
text-decoration: line-through;
/* 禁用日期的文字带有删除线 */
}
</style>
<template>
<view>
<up-navbar class=" " style="font-weight: bold;" leftIcon="" title="食谱菜单" titleColor="#303133" bgColor="#FFFFFFFF"
titleWidth="600rpx" height="80rpx" leftIconSize="40rpx" leftIconColor="#303133" :safeAreaInsetTop="true"
:placeholder="true" :fixed="true"></up-navbar>
<up-search v-model="search.keyword" class="flex,flex-row p-1" style="" shape="round" bgColor="#f2f2f2"
placeholder="输入食谱" :clearabled="true" :showAction="false" inputAlign="left" borderColor="transparent"
searchIconColor="#909399" color="#606266" placeholderColor="#909399" searchIcon="search" margin="10rpx"
maxlength="-1" height="60rpx" @change="searchSP()">
</up-search>
<!-- <up-tabs v-model:current="currentTab" style="font-weight: bold;" :list="menu_list" key-name="name"
line-color="#18C936" line-width="80rpx" line-height="6rpx" line-bg-size="cover" :scrollable="true" /> -->
<!-- <u-scroll-list :indicator="false">
<view class="scroll-list" style="flex-direction: row;width: 56px;">
<view class="scroll-list__goods-item " v-for="(item, index) in categoryList" :key="index"
:class="(index === categoryIndex) ? 'row-active' : ''" @click="handleClickCategory(index)"
:style="{ backgroundColor: item.bgColor }">
<image class="scroll-list__goods-item__image"
:src="item.image ? item.image : 'https://s3.bmp.ovh/imgs/2024/12/16/35bc6d28ab1c8bc7.png'"
:class="(index === categoryIndex) ? 'border-row-active' : ''"></image>
<text class="scroll-list__goods-item__text">{{ item.name }}</text>
</view>
</view>
</u-scroll-list> -->
<wd-col-picker label="选择地址" v-model="value" :columns="area" :column-change="columnChange"
@confirm="handleConfirm"></wd-col-picker>
<wd-calendar :formatter="formatter" type="dates" :min-date="min_data" v-model="time_list"
@confirm="handleTimeConfirm" />
<wd-tabs v-model="currentTab" @change="handleChange" auto-line-width color="#59CB56" lineWidth="40rpx">
<block v-for="item in menu_list" :key="item">
<wd-tab :title="`${item.name}`" :name="item.id" setActive="item.name">
<view class="content">
</view>
</wd-tab>
</block>
</wd-tabs>
<!--
<wd-button>主要按钮</wd-button>
<wd-button type="success">成功按钮</wd-button>
<wd-button type="info">信息按钮</wd-button>
<wd-button type="warning">警告按钮</wd-button>
<wd-button type="error">危险按钮</wd-button> -->
<z-paging :fixed="false" height="calc(100vh - 440rpx)" width="200rpx" loading-more-default-text=""
style=" left:0; padding: 0px; position: fixed;" class="fv-page flex-col px-4">
<u-scroll-list :indicator="false">
<view class="scroll-list" style="flex-direction: column;">
<view class="scroll-list__goods-item-column " v-for="(item, index) in tabList" :key="index"
:class="(index === childrenIndex) ? 'column-active' : ''" @click="handleChildrenClick(index)"
:style="{ backgroundColor: item.bgColor }">
<!-- <image class="scroll-list__goods-item__image" :src="item.image"></image> -->
<slot name="tabItem">
</slot>
<!-- <view style="padding: 20px 0px;"> </view> -->
<text class="scroll-list__goods-item__text">{{ item.name }}</text>
</view>
</view>
</u-scroll-list>
</z-paging>
<z-paging ref="pagingRefSP" v-model="matchedItems_sp" @query="changeSP" :fixed="false" height="calc(100vh - 440rpx)"
width="calc(100vw - 200rpx)" style="right: 0; background-color: #FFFFFFFF; position: fixed;"
class="fv-page flex-col">
<view v-for="(item, index) in matchedItems_sp" :key="index">
<up-cell :border='false' @click="goodsDetail(item.id)">
<template #icon>
<up-image :src="item.image" width="140rpx" height="140rpx"></up-image>
</template>
<template v-slot:title>
<view class="h-50 flex">
<up-text size="28rpx" :text="`${item.name}`" :flex1="true" align="left" wordWrap="normal" :show="true"
prefixIcon="" iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex">
<up-text color="#767676" size="24rpx" :text="`主料:${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
</view>
<view class="h-50 flex ellipsis_text">
<up-text color="#767676" size="24rpx" :text="`营养:${item.intro}`" :flex1="true" align="left"
wordWrap="normal" :show="true" prefixIcon=""
iconStyle="font-size:26rpx;color:#59CB56;margin-right:10rpx;" lines="1" decoration="none">
</up-text>
<view @click.stop="addCart(item.id)">
<u-icon slot="right" label="" size="42rpx" name="plus-circle" color="#59CB56"
labelColor="#59CB56"></u-icon>
</view>
</view>
</template>
</up-cell>
</view>
</z-paging>
</view>
</template>
<script lang="ts">
// useColPickerData
//
import { useColPickerData } from '@/hooks'
import { defineComponent } from "vue";
import { CommonApi, UserApi } from "@/api";
import { get } from 'http';
// import { setToken, getToken, isLogin } from '@/utils/auth';
const { colPickerData, findChildrenByCode } = useColPickerData()
// const value = ref<string[]>([])
// const area = ref<any[]>([
// ])
export default defineComponent({
data () {
return {
search: {
category_id: '',
keyword: '',
page: 1,
limit: 10,
},
// area: useColPickerData().colPickerData.map((item) => {
// return {
// value: item.value,
// label: item.text
// }
// }),
area: [],
value: [],
currentTab: 0, // tab
min_data: new Date().getTime(),
isDisabledDate: [1754755200000, 1754755200000],
time_list: [],
menu_list: [{ id: 0, name: '第一餐' }, { id: 1, name: '第二餐' }, { id: 2, name: '第三餐' }],
// keyword_sp: '',
categoryIndex: 0,
childrenIndex: 0,
categoryList: [],
tabList: [],
good_list: [],
matchedItems_sp: [],
};
},
onLoad () {
// if (isLogin()) {
this.getCategoryList()
// }
this.getArea()
},
created () {
},
methods: {
handleConfirm ({ value }) {
console.log(value)
},
getArea () {
try {
this.area = [colPickerData.map(item => ({
value: item.value,
label: item.text
}))];
} catch (error) {
console.error('获取区域数据时出错:', error);
uni.$u.toast('获取区域数据失败');
}
},
columnChange ({ selectedItem, resolve, finish }) {
try {
const areaData = findChildrenByCode(colPickerData, selectedItem.value);
if (areaData && areaData.length) {
resolve(areaData.map(item => ({
value: item.value,
label: item.text
})));
} else {
finish();
}
} catch (error) {
console.error('处理列变更时出错:', error);
finish(); //
}
},
formatter (day) {
const date = new Date(day.date)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth()
const da = date.getDate()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth()
const nowDa = now.getDate()
if (year === nowYear && month === nowMonth && da === nowDa) {
day.topInfo = '今天'
}
// const disabledDates = ['2025-08-05', '2023-08-15'];
// const formattedDate = `${year}-${month}-${da}`;
if (this.isDisabledDate.includes(day.date)) {
day.topInfo = '已预定';
day.disabled = true; //
day.disabled_class = 'disabled-date'; //
}
// if (day.type === 'start') {
// day.bottomInfo = ''
// }
// if (day.type === 'end') {
// day.bottomInfo = ''
// }
// if (day.type === 'same') {
// day.bottomInfo = '/'
// }
return day
},
handleTimeConfirm (res) {
console.log(res.value)
this.time_list = res.value
},
handleChange () {
this.$nextTick(() => { this.handleClickCategory(this.currentTab) });
},
handleClickCategory (index: number) {
console.log(index);
this.categoryIndex = index;
this.tabList = this.categoryList[index].children;
this.handleChildrenClick(0);
},
handleChildrenClick (index: number) {
this.childrenIndex = index;
this.search.category_id = this.tabList[index].id;
this.$refs.pagingRefSP?.reload();
},
getCategoryList () {
CommonApi.commonGet('/api/dishes/category').catch((res) => {
if (res.code === 1) {
this.categoryList = res.data;
this.handleClickCategory(0)
} else {
uni.$u.toast(res.msg);
}
});
},
//
searchSP () {
this.$refs.pagingRefSP?.reload();
// this.changeSP(1);
},
//
changeSP (pageNo: number) {
// console.log(pageNo);
this.search.page = pageNo;
CommonApi.commonGet('/api/dishes/dishes', this.search).catch((res) => {
if (res.code === 1) {
// this.matchedItems_sp = res.data;
this.$refs.pagingRefSP?.complete(res.data);
// console.log(res);
} else {
uni.$u.toast(res.msg);
}
});
},
//
goodsDetail (id: number) {
// console.log(id);
uni.navigateTo({
url: `/pages/common/goods/detail?id=${id}&is_add=1`
});
},
//
addCart (id) {
CommonApi.commonPost('/api/cart/add', {
buy_now: false,
dishes_id: id,
}).catch((res) => {
if (res.code === 1) {
uni.$u.toast('加入购物车成功');
} else {
uni.$u.toast(res.msg);
}
});
}
},
});
</script>

View File

@ -18,7 +18,6 @@ body {
<template>
<view>
<up-navbar class=" " :autoBack="true" style="font-weight: bold;"
leftIcon="arrow-left" title="订单列表" titleColor="#303133" bgColor="#FFFFFFFF" titleWidth="600rpx"
height="80rpx" leftIconSize="38rpx" leftIconColor="#303133" :safeAreaInsetTop="true" :placeholder="true"

View File

@ -89,7 +89,7 @@
</view>
</view>
<up-button class="" style="" text="立即保存" type="primary" color="#59CB56" shape="circle" size="normal"
custom-style="width: calc(100% - 100rpx);" @click="updateAddress">
custom-style="width: calc(100vw - 100rpx);" @click="updateAddress">
</up-button>
</view>
</u-popup>
@ -149,7 +149,7 @@
<view class="pb-4" />
</scroll-view>
<up-button class="" style="" text="添加地址" type="primary" color="#59CB56" shape="circle" size="normal"
custom-style="width: calc(100% - 100rpx);" @click="addAddress">
custom-style="width: calc(100vw - 100rpx);" @click="addAddress">
</up-button>
</view>
</u-popup>

View File

@ -7,13 +7,13 @@
text-overflow: ellipsis;
/* 在末尾显示省略号 */
// width: 260rpx!important;
width: calc(100% - 80rpx) !important;
width: calc(100vw - 80rpx) !important;
/* 确保元素有宽度 */
}
.ellipsis_text {
// width: 340rpx !important;
// width: calc(100% - 80rpx)!important;
// width: calc(100vw - 80rpx)!important;
}
.zp-l-text-rpx {

View File

@ -200,7 +200,17 @@
</view>
</up-cell-group>
</z-paging>
<u-tabbar
value="1"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
>
<u-tabbar-item text="首页" icon="home"></u-tabbar-item>
<u-tabbar-item text="放映厅" icon="photo"></u-tabbar-item>
<u-tabbar-item text="直播" icon="play-right"></u-tabbar-item>
<u-tabbar-item text="我的" icon="account"></u-tabbar-item>
</u-tabbar>
<view class="flex-sub box-border flex">
<view style="margin: 0rpx 0rpx 0rpx 20rpx;">
<u-text class="" color="#59CB56" size="26rpx" :text="`购物车菜谱${cartNUM}个`" :flex1="true" align="left"

View File

@ -29,7 +29,8 @@
<u-cell icon="" title="微信号" is-link right-icon="lock" :value="userStore.user.code" :border="false" size="large"
custom-style="padding:20rpx;font-size:28rpx" />
<u-cell icon="order" title="宴席" is-link value="" :border="false" size="large"
custom-style="padding:20rpx;font-size:28rpx" @click="banquet" />
<u-cell icon="order" title="订单详情" is-link value="" :border="false" size="large"
custom-style="padding:20rpx;font-size:28rpx" @click="orderList" />
</view>
@ -92,6 +93,14 @@ function orderList () {
url: '/pages/common/goods/order',
});
}
function banquet () {
uni.$u.route({
type: 'navigateTo',
url: '/pages/banquet/combo/index',
});
}
async function loginOuts () {
// 退

View File

@ -4,18 +4,44 @@ page {
background-color: $u-bg-color;
}
html,
body {
height: 0px!important;
}
// 日期组件样式
.disabled-date>.wd-month__day-container>.wd-month__day-text {
color: #ffffff !important;
}
.disabled-date {
border-radius: var(--wot-calendar-active-border, 8px);
background: var(--wot-calendar-active-color, var(--wot-color-theme, #366cff));
// background-color: #78da1c;
/* 禁用日期的背景颜色 */
color: #ffffff;
/* 禁用日期的文字颜色 */
pointer-events: none;
/* 禁用日期的点击事件 */
// opacity: 0.5;
/* 禁用日期的透明度 */
text-decoration: line-through;
/* 禁用日期的文字带有删除线 */
}
html,
body {
height: 100vh;
}
.align-center{
text-align: center;
}
.prop-flex-bottom{
position: fixed; display: flex; left: 0; right: 0; bottom: 20rpx;
}
.detail-box {
margin: 10rpx;
margin: 20rpx 0rpx;
background-color: #fff;
border: 1rpx solid #dfdfdf;
border-radius: 20rpx;
@ -45,7 +71,6 @@ body {
&__goods-item {
padding: 0px 40rpx;
// margin-right: 20px;
&__image {
border: 2rpx solid rgb(177, 177, 177);
@ -66,10 +91,9 @@ body {
&__goods-item-column {
width: 200rpx;
padding: 20rpx 0rpx;
padding: 40rpx 0rpx;
.scroll-list__goods-item__text {
width: 200rpx;
}
}
.border-row-active {
@ -84,14 +108,21 @@ body {
// border-left: 10rpx solid #59cb56;
color: #59cb56;
background-color: #FFFFFF;
.scroll-list__goods-item__text {
width: 190rpx;
border-left: 10rpx solid #59cb56;
border-left: 8rpx solid #59cb56;
}
}
.column-active-false {
width: 200rpx;
// border-left: 10rpx solid #59cb56;
color: #59cb56;
background-color: #FFFFFF;
.scroll-list__goods-item__text {
width: 190rpx;
border-left: 8rpx solid #ffffff;
}
}
&__goods-item:hover {
color: #59cb56;
@ -194,3 +225,51 @@ body {
border-radius: 10rpx;
background-color: #ffffff;
}
//悬浮样式 wot ui
:deep(.custom-button) {
min-width: auto !important;
box-sizing: border-box;
width: 32px !important;
height: 32px !important;
border-radius: 16px !important;
margin: 8rpx;
}
:deep(.custom-radio) {
height: 32px !important;
line-height: 32px !important;
}
//卡片样式 wot ui
.content,
.title {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.content {
justify-content: flex-start;
}
.title {
justify-content: space-between;
}
.title-tip {
color: rgb(27, 27, 27);
font-size: 12px;
}
.margin-right-24{
margin-right: 24rpx;
}
.font-size-24{
font-size: 24rpx;
}
.font-size-28{
font-size: 28rpx;
}
.font-size-32{
font-size: 32rpx;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 weisheng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,145 @@
<p align="center">
<img alt="logo" src="https://wot-design-uni.cn/logo.png" width="200">
</p>
<h1 align="center">Wot UI</h1>
<p align="center">📱 一个基于vue3+Typescript构建参照<a href="https://ftf.jd.com/wot-design/">wot-design</a>打造的uni-app组件库</p>
<p align="center">
<a href="https://github.com/Moonofweisheng/wot-design-uni">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/Moonofweisheng/wot-design-uni?logo=github&color=%234d80f0&link=https%3A%2F%2Fgithub.com%2FMoonofweisheng%2Fwot-design-uni&style=flat-square">
</a>
<a href="https://github.com/Moonofweisheng/wot-design-uni">
<img alt="GitHub" src="https://img.shields.io/codecov/c/github/Moonofweisheng/wot-design-uni?style=flat-square">
</a>
<a href="https://www.npmjs.com/package/wot-design-uni">
<img alt="npm" src="https://img.shields.io/npm/dm/wot-design-uni?logo=npm&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fwot-design-uni&style=flat-square">
</a>
<a href="https://www.npmjs.com/package/wot-design-uni">
<img alt="npm" src="https://img.shields.io/npm/v/wot-design-uni?logo=npm&color=%234d80f0&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fwot-design-uni&style=flat-square">
</a>
<a href="https://github.com/actions-cool/" target="_blank" referrerpolicy="no-referrer">
<img src="https://img.shields.io/badge/using-actions--cool-red?style=flat-square" alt="actions-cool" />
</a>
<a href="https://app.netlify.com/sites/wot-design-uni/deploys" target="_blank" referrerpolicy="no-referrer">
<img src="https://api.netlify.com/api/v1/badges/0991d8a9-0fb0-483b-8961-5bde066bbd50/deploy-status" alt="deploy-status" />
</a>
</p>
<p align="center">
🚀 <a href="https://wot-design-uni.cn">文档网站 (推荐)</a>&nbsp;
✈️ <a href="https://wot-design-uni.pages.dev/">文档网站 (cloudflare)</a>&nbsp;
🔥 <a href="https://wot-design-uni.netlify.app/">文档网站 (Netlify)</a>&nbsp;
🚫 <a href="https://wot-design-uni.gitee.io/">文档网站 (Gitee暂时下线)</a>
</p>
## ✨ 特性
- 🎯 多平台覆盖,支持 微信小程序、支付宝小程序、钉钉小程序、H5、APP 等.
- 🚀 70+ 个高质量组件,覆盖移动端主流场景.
- 💪 使用 Typescript 构建,提供良好的组件类型系统.
- 🌍 支持国际化,内置 15 种语言包.
- 📖 提供丰富的文档和组件示例.
- 🎨 支持修改 CSS 变量实现主题定制.
- 🍭 支持暗黑模式
## 📱 预览
扫描二维码访问演示注意因微信审核机制限制当前的微信小程序示例可能不是最新版本可以clone代码到本地预览。
<p style="display:flex;gap:24px">
<img src="https://wot-design-uni.cn/wx.jpg" width="200" height="200"/>
<img src="https://wot-design-uni.cn/alipay.png" width="200" height="200" />
<img src="https://wot-design-uni.cn/h5.png" width="200" height="200" />
<img src="https://wot-design-uni.cn/android.png" width="200" height="200" />
</p>
## 快速上手
详细说明见 [快速上手](https://wot-design-uni.cn/guide/quick-use.html)。
## 链接
- [常见问题](https://wot-design-uni.cn/guide/common-problems.html)
- [更新日志](https://wot-design-uni.cn/guide/changelog.html)
- [Discussions 讨论区](https://github.com/Moonofweisheng/wot-design-uni/discussions)
- [QQ 群](https://wot-design-uni.cn/guide/join-group.html)
## 优秀案例
[这里](https://wot-design-uni.cn/guide/cases.html)我们收集了一些优秀的案例,欢迎大家体验!
我们也非常欢迎大家一起贡献优秀的 Demo 与案例,欢迎在此 [issue](https://github.com/Moonofweisheng/wot-design-uni/issues/16) 提交案例。
## 周边生态
| 项目 | 描述 |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [awesome-uni-app](https://github.com/uni-helper/awesome-uni-app) | 多端统一开发框架 uni-app 优秀开发资源汇总 |
| [create-uni](https://github.com/uni-helper/create-uni) | 快速创建 uni-app 项目 |
| [wot-demo](https://github.com/Moonofweisheng/wot-demo) | 基于 [vitesse-uni-app](https://github.com/uni-helper/vitesse-uni-app) 的wot-design-uni快速起手demo |
| [wot-starter-retail](https://github.com/Moonofweisheng/wot-starter-retail) | 基于 wot-design-uni 的 uni-app 零售行业模板 |
| [Wot UI Snippets](https://marketplace.visualstudio.com/items?itemName=kiko.wot-design-uni-snippets) | Wot UI 代码块提示 |
| [uni-mini-ci](https://github.com/Moonofweisheng/uni-mini-ci) | 一个 uni-app 小程序端构建后支持 CI持续集成的插件 |
| [uni-mini-router](https://github.com/Moonofweisheng/uni-mini-router) | 一个基于 vue3 和 Typescript 的轻量级 uni-app 路由库 |
| [unibest](https://github.com/unibest-tech/unibest) | 基于 wot-design-uni 的 uni-app 模板 |
| [wot-design-uni AI 助手](https://www.coze.cn/store/bot/7347916532258701363) | 一个能回答你关于 wot-design-uni 组件库问题的智能助手 |
| [uni-ku-root](https://github.com/uni-ku/root) | 一个模拟 App.vue 原有能力的根组件插件 |
## 贡献指南
修改代码请阅读我们的 [贡献指南](https://github.com/Moonofweisheng/wot-design-uni/blob/develop/.github/CONTRIBUTING.md)。
使用过程中发现任何问题都可以提 [Issue](https://github.com/Moonofweisheng/wot-design-uni/issues) 给我们,当然,我们也非常欢迎你给我们发 [PR](https://github.com/Moonofweisheng/wot-design-uni/pulls)。
## 贡献者们
感谢以下所有给 Wot UI 贡献过代码的 [开发者](https://github.com/Moonofweisheng/wot-design-uni/graphs/contributors)。
<a href="https://github.com/Moonofweisheng/wot-design-uni/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Moonofweisheng/wot-design-uni" />
</a>
## 捐赠本项目
开发一个 UI 组件库是一项耗时的工作,尤其是要多端适配。为此 Wot UI 经常肝到深夜 ……
如果您认为 Wot UI 帮助到了您的开发工作,您可以捐赠 Wot UI 的研发工作,捐赠无门槛,哪怕是一杯可乐也好。
捐赠后您的昵称、留言等将会展示在[捐赠榜单](https://wot-design-uni.cn/reward/donor.html)中。
### 爱发电捐赠
<a href="https://afdian.com/a/weisheng233">https://afdian.com/a/weisheng233</a>
### 扫码捐赠
<p>
<img src="https://wot-design-uni.cn/weixinQrcode.jpg" width="200" height="200" style="margin-right:30px"/>
<img src="https://wot-design-uni.cn/alipayQrcode.jpg" width="200" height="200" />
</p>
## 鸣谢
- [wot-design](https://github.com/jd-ftf/wot-design-mini) - 感谢 wot-design 团队多年来的不断维护,让 wot-design-uni 能够站在巨人的肩膀上。
- [uni-helper](https://github.com/uni-helper) - 感谢 uni-helper 团队提供的 uni-app 工具库,让 wot-design-uni 能够更方便地使用。
- [捐赠者](https://wot-design-uni.cn/reward/donor.html) - 感谢所有捐赠者,是你们的捐赠让 wot-design-uni 能够更好地发展。
## 开源协议
本项目基于 [MIT](https://zh.wikipedia.org/wiki/MIT%E8%A8%B1%E5%8F%AF%E8%AD%89) 协议,请自由地享受和参与开源。

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时调用
})
}
// 提供abort方法来中止Promise
abort(error?: any) {
if (this._reject) {
this._reject(error) // 调用reject方法来中止Promise
}
}
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: #59CB56 !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 !=#59CB56 {
@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,973 @@
@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; // 屏幕两边留白
$-size-side-padding-small: var(--wot-size-side-padding-small, 6px) !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-fs-large: var(--wot-cell-value-fs-large, 16px) !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-small) !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-color: var(--wot-toast-color, $-color-white) !default; // 文字颜色
$-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-line-height: var(--wot-toast-line-height, 20px) !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-loading-margin-bottom: var(--wot-toast-loading-margin-bottom, 16px) !default; // loading动画的margin-bottom
$-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,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[] => {
// 将hex转换为rgb
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 truefalse
*/
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类型truefalse
*/
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是视频链接则为truefalse
*/
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是图片格式truefalse
*/
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'
// 小程序端不支持从vue导出的isVNode方法参考uni-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支持全部类型过滤,微信小程序支持all和file时过滤,其余平台不支持)
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,118 @@
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),
/**
* fixed (H5: teleport, APP: renderjs, 小程序: root-portal)
* boolean
* false
*/
rootPortal: makeBooleanProp(false)
}
export type ActionSheetProps = ExtractPropTypes<typeof actionSheetProps>

View File

@ -0,0 +1,155 @@
<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"
:root-portal="rootPortal"
@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,390 @@
<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.disabled_class ? item.disabled_class : ''} ${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,
disabled_class: '',
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,110 @@
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
disabled_class?: string | string[] // 添加 class 字段
}
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 '../../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 '../../../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,158 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(calendar) {
@include e(title) {
color: $-dark-color;
}
:deep(.wd-calendar__arrow),
:deep(.wd-calendar__close),
:deep(.wd-calendar__clear) {
color: $-dark-color;
}
@include e(range-label-item) {
color: $-dark-color;
@include when(placeholder) {
color: $-dark-color-gray;
}
}
@include e(range-sperator) {
color: $-dark-color-gray;
}
:deep(.wd-calendar__cell--placeholder) {
.wd-cell__value {
color: $-dark-color-gray;
}
}
}
}
@include b(calendar) {
@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;
}
@include edeep(cell) {
@include when(disabled) {
.wd-cell__value {
color: $-input-disabled-color;
cursor: not-allowed;
}
}
@include when(error) {
.wd-cell__value {
color: $-input-error-color;
}
:deep(.wd-calendar__arrow) {
color: $-input-error-color;
}
}
@include when(large) {
.wd-calendar__arrow {
font-size: $-cell-icon-size-large;
}
}
@include m(placeholder) {
.wd-cell__value {
color: $-input-placeholder-color;
}
}
}
@include edeep(arrow) {
display: block;
font-size: $-cell-icon-size;
color: $-cell-arrow-color;
line-height: $-cell-line-height;
}
@include edeep(clear) {
display: block;
font-size: $-cell-icon-size;
color: $-cell-clear-color;
line-height: $-cell-line-height;
}
}

View File

@ -0,0 +1,222 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 20:40:34
* @LastEditTime: 2025-07-11 16:00:26
* @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: makeStringProp('33%'),
/**
*
*/
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),
/**
* fixed (H5: teleport, APP: renderjs, 小程序: root-portal)
*/
rootPortal: makeBooleanProp(false),
/**
*
*/
clearable: makeBooleanProp(false)
}
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,450 @@
<template>
<view :class="`wd-calendar ${customClass}`">
<template v-if="withCell">
<wd-cell
v-if="!$slots.default"
:title="label"
:value="showValue || placeholder || translate('placeholder')"
:required="required"
:size="size"
:title-width="labelWidth"
:prop="prop"
:rules="rules"
:clickable="!disabled && !readonly"
:value-align="alignRight ? 'right' : 'left'"
:center="center"
:custom-class="cellClass"
:custom-style="customStyle"
:custom-title-class="customLabelClass"
:custom-value-class="customValueClass"
:ellipsis="ellipsis"
:use-title-slot="!!$slots.label"
@click="open"
>
<template #title v-if="$slots.label">
<slot name="label"></slot>
</template>
<template #right-icon>
<wd-icon v-if="showArrow" custom-class="wd-calendar__arrow" name="arrow-right" />
<view v-else-if="showClear" @click.stop="handleClear">
<wd-icon custom-class="wd-calendar__clear" name="error-fill" />
</view>
</template>
</wd-cell>
<view v-else @click="open">
<slot></slot>
</view>
</template>
<wd-action-sheet
v-model="pickerShow"
:duration="250"
:close-on-click-modal="closeOnClickModal"
:safe-area-inset-bottom="safeAreaInsetBottom"
:z-index="zIndex"
:root-portal="rootPortal"
@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 wdCell from '../wd-cell/wd-cell.vue'
import { ref, computed, watch } from 'vue'
import dayjs from '../../dayjs'
import { deepClone, isArray, isEqual, padZero, pause } from '../common/util'
import { getWeekNumber, isRange } from '../wd-calendar-view/utils'
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', 'clear'])
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 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 ''
}
})
const cellClass = computed(() => {
const classes = ['wd-calendar__cell']
if (props.disabled) classes.push('is-disabled')
if (props.readonly) classes.push('is-readonly')
if (props.error) classes.push('is-error')
if (!showValue.value) classes.push('wd-calendar__cell--placeholder')
return classes.join(' ')
})
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 range = computed(() => {
return (type: CalendarType) => {
return isRange(type)
}
})
//
const showClear = computed(() => {
return props.clearable && !props.disabled && !props.readonly && showValue.value.length > 0
})
//
const showArrow = computed(() => {
return !props.disabled && !props.readonly && !showClear.value
})
function handleClear() {
emit('clear')
emit('update:modelValue', null)
}
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,210 @@
@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-color;
}
@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;
box-sizing: border-box;
width: 100%;
overflow: hidden;
@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;
overflow: hidden;
@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;
min-width: 0;
}
@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;
min-width: 0;
}
@include e(value) {
position: relative;
flex: 1;
font-size: $-cell-value-fs;
color: $-cell-value-color;
vertical-align: middle;
@include m(left) {
text-align: left;
}
@include m(right) {
text-align: right;
}
@include m(ellipsis) {
@include lineEllipsis;
min-width: 0;
}
}
@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;
}
.wd-cell__value {
font-size: $-cell-value-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,103 @@
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(''),
/**
* value leftrightcenter
*/
valueAlign: makeStringProp<'left' | 'right'>('right'),
/**
*
*/
ellipsis: makeBooleanProp(false),
/**
* title插槽v-slot和v-if冲突问题
* https://github.com/dcloudio/uni-app/issues/4847
*/
useTitleSlot: makeBooleanProp(true)
}
export type CellProps = ExtractPropTypes<typeof cellProps>

View File

@ -0,0 +1,141 @@
<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
v-if="showLeft"
:class="['wd-cell__left', isRequired ? 'is-required' : '']"
:style="titleWidth ? 'min-width:' + titleWidth + ';max-width:' + titleWidth + ';' : ''"
>
<!--左侧icon部位-->
<slot name="icon">
<wd-icon v-if="icon" :name="icon" :custom-class="`wd-cell__icon ${customIconClass}`"></wd-icon>
</slot>
<view class="wd-cell__title">
<!--title BEGIN-->
<slot name="title" v-if="useTitleSlot && $slots.title"></slot>
<view v-else-if="title" :class="customTitleClass">{{ title }}</view>
<!--title END-->
<!--label BEGIN-->
<slot name="label">
<view v-if="label" :class="`wd-cell__label ${customLabelClass}`">{{ label }}</view>
</slot>
<!--label END-->
</view>
</view>
<!--right content BEGIN-->
<view class="wd-cell__right">
<view class="wd-cell__body">
<!--文案内容-->
<view :class="`wd-cell__value ${customValueClass} wd-cell__value--${valueAlign} ${ellipsis ? 'wd-cell__value--ellipsis' : ''}`">
<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, useSlots } 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 slots = useSlots()
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
})
//
const showLeft = computed(() => {
// props
// iconicon
const hasIcon = slots.icon || props.icon
// titletitle
const hasTitle = (slots.title && props.useTitleSlot) || props.title
// labellabel
const hasLabel = slots.label || props.label
return hasIcon || hasTitle || hasLabel
})
/**
* @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,168 @@
@import '../common/abstracts/variable';
@import '../common/abstracts/mixin';
.wot-theme-dark {
@include b(col-picker) {
@include e(list-item) {
@include when(disabled) {
color: $-dark-color3;
}
}
@include e(list-item-tip) {
color: $-dark-color-gray;
}
:deep(.wd-col-picker__arrow) {
color: $-dark-color;
}
@include e(list) {
color: $-dark-color;
}
@include e(selected) {
color: $-dark-color;
}
:deep(.wd-col-picker__cell--placeholder) {
.wd-cell__value {
color: $-dark-color-gray;
}
}
}
}
@include b(col-picker) {
@include edeep(cell) {
@include when(disabled) {
.wd-cell__value {
color: $-input-disabled-color;
cursor: not-allowed;
}
}
@include when(error) {
.wd-cell__value {
color: $-input-error-color;
}
.wd-col-picker__arrow {
color: $-input-error-color;
}
}
@include when(large) {
.wd-col-picker__arrow {
font-size: $-cell-icon-size-large;
}
}
@include m(placeholder) {
.wd-cell__value {
color: $-input-placeholder-color;
}
}
}
@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,162 @@
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(''),
/**
* fixed (H5: teleport, APP: renderjs, 小程序: root-portal)
*/
rootPortal: makeBooleanProp(false)
}
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>

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