feat:还原更新框架

This commit is contained in:
2026-07-23 14:49:48 +08:00
parent d5a6a16ea5
commit 75c7434f79
529 changed files with 43714 additions and 68201 deletions
+5 -21
View File
@@ -12,19 +12,7 @@
## 说明
【文档有广告,不喜勿入谢谢】
uview-plus,是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x(已经发布https://ext.dcloud.net.cn/plugin?name=uview-ultra)的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。
## 版本
### uview-plus v3 (uni-app版本)
1. 保持选项式API,尽可能兼容原有uview2各种API,比如mixins,方便大家升级更容易;
2. 新手友好,保持原汁原味js,二次开发方便,未使用typescript重写,但带有独立的类型申明包;
### uview-ultra v4 (uni-app-x版本)
1. 尽可能保持原有组件的API一致,方便大家升级;
2. 使用uts + 组合式API架构级全新重构,但仍然保持源码的简易,不封装过于复杂,便于修改。
uview-plus,是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。
## 可视化设计
@@ -44,13 +32,7 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
| H5 | 微信小程序 | 安卓 | 鸿蒙Next(已上架) |
| --- | --- | --- | --- |
| <img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="160" height="160" /> | <img src="https://uview-plus.jiangruyi.com/common/weixin_mini_qrcode.jpg" width="160" height="160" /> | <img src="https://uview-plus.jiangruyi.com/common/android_qrcode.png" width="160" height="160" /> | <img src="https://uview-plus.jiangruyi.com/common/hos_qrcode.jpg" width="160" height="160" /> |
<br>
<img src="https://uview-plus.jiangruyi.com/common/hos.png">
<img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="220" height="220" >
## 链接
@@ -59,6 +41,9 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
- [升级指南](https://uview-plus.jiangruyi.com/components/changeGuide.html)
- [关于我们](https://uview-plus.jiangruyi.com/cooperation/about.html)
## 交流反馈
欢迎加入我们的QQ群交流反馈:[点此跳转](https://uview-plus.jiangruyi.com/components/addQQGroup.html)
## 关于PR
@@ -87,4 +72,3 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
## 版权信息
uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。
Root 组件基于上游项目 `uni-ku/root` 二次开发,已按 MIT 要求保留其原始授权文件:`libs/root/LICENSE`。
File diff suppressed because it is too large Load Diff
@@ -1,109 +0,0 @@
<template>
<view class="u-action-sheet-data">
<view class="u-action-sheet-data__trigger">
<slot name="trigger"></slot>
<up-input
v-if="!$slots['trigger']"
:modelValue="current"
disabled
disabledColor="#ffffff"
:placeholder="title"
border="none"
></up-input>
<view @click="show = true"
class="u-action-sheet-data__trigger__cover"></view>
</view>
<up-action-sheet
:show="show"
:actions="options"
:title="title"
safeAreaInsetBottom
:description="description"
@close="show = false"
@select="select"
>
</up-action-sheet>
</view>
</template>
<script>
export default {
props: {
modelValue: {
type: [String, Number],
default: ''
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
options: {
type: Array,
default: () => {
return []
}
},
valueKey: {
type: String,
default: 'value'
},
labelKey: {
type: String,
default: 'name'
}
},
data() {
return {
show: false,
current: '',
}
},
created() {
if (this.modelValue) {
this.options.forEach((ele) => {
if (ele[this.valueKey] == this.modelValue) {
this.current = ele[this.labelKey]
}
})
}
},
emits: ['update:modelValue'],
watch: {
modelValue() {
this.options.forEach((ele) => {
if (ele[this.valueKey] == this.modelValue) {
this.current = ele[this.labelKey]
}
})
}
},
methods: {
hideKeyboard() {
uni.hideKeyboard()
},
select(e) {
this.$emit('update:modelValue', e[this.valueKey])
this.current = e[this.labelKey]
},
}
}
</script>
<style lang="scss" scoped>
.u-action-sheet-data {
&__trigger {
position: relative;
&__cover {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
}
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-08-16 10:52:35
* @FilePath : /uview-plus/libs/config/props/actionSheet.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:44:35
* @FilePath : /u-view2.0/uview-ui/libs/config/props/actionSheet.js
*/
export default {
// action-sheet组件
@@ -14,8 +14,6 @@ export default {
title: '',
description: '',
actions: [],
nameKey: 'name',
subnameKey: 'subnameKey',
index: '',
cancelText: '',
closeOnClickAction: true,
@@ -1,16 +1,6 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @LastAuthor : jry
* @lastTime : 2025-08-16 10:52:35
* @FilePath : /uview-plus/libs/config/props/props.js
*/
import { defineMixin } from '../../libs/vue'
import ActionSheetDefaultProps from './actionSheet'
import { registerComponentProps } from '../../libs/config/props.js'
import defProps from '../../libs/config/props.js'
const defProps = registerComponentProps(ActionSheetDefaultProps)
export const props = defineMixin({
props: {
// 操作菜单是否展示 (默认false)
@@ -33,16 +23,6 @@ export const props = defineMixin({
type: Array,
default: () => defProps.actionSheet.actions
},
// 选项名称key
nameKey: {
type: String,
default: () => defProps.actionSheet.nameKey
},
// 选项子名称key
subnameKey: {
type: String,
default: () => defProps.actionSheet.subnameKey
},
// 取消按钮的文字,不为空时显示按钮
cancelText: {
type: String,
@@ -1,3 +1,4 @@
<template>
<u-popup
:show="show"
@@ -7,39 +8,32 @@
:round="round"
>
<view class="u-action-sheet">
<!-- 顶部标题区域 -->
<view
class="u-action-sheet__header"
v-if="title"
>
<text
class="u-action-sheet__header__title u-line-1"
:style="titleDynamicStyle"
>{{title}}</text>
<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
<view
class="u-action-sheet__header__icon-wrap"
@tap.stop="cancel"
>
<up-icon
<u-icon
name="close"
size="17"
:color="closeIconColor"
color="#c8c9cc"
bold
></up-icon>
></u-icon>
</view>
</view>
<!-- 描述信息 -->
<text
class="u-action-sheet__description"
:style="[{
marginTop: `${title && description ? 0 : '18px'}`
}, descriptionDynamicStyle]"
}]"
v-if="description"
>{{description}}</text>
<slot>
<!-- 分割线 -->
<u-line v-if="description" :color="dividerColor"></u-line>
<!-- 操作项列表 -->
<u-line v-if="description"></u-line>
<scroll-view scroll-y class="u-action-sheet__item-wrap" :style="{maxHeight: wrapMaxHeight}">
<view :key="index" v-for="(item, index) in actions">
<!-- #ifdef MP -->
@@ -68,20 +62,17 @@
@tap.stop="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
:hover-stay-time="150"
:style="getItemHoverStyle(index)"
>
<template v-if="!item.loading">
<text
class="u-action-sheet__item-wrap__item__name"
:style="[itemStyle(index)]"
>{{ item[nameKey] }}</text>
>{{ item.name }}</text>
<text
v-if="item[subnameKey]"
v-if="item.subname"
class="u-action-sheet__item-wrap__item__subname"
:style="[subnameStyle(index)]"
>{{ item[subnameKey] }}</text>
>{{ item.subname }}</text>
</template>
<!-- 加载状态图标 -->
<u-loading-icon
v-else
custom-class="van-action-sheet__loading"
@@ -92,25 +83,21 @@
<!-- #ifdef MP -->
</button>
<!-- #endif -->
<!-- 选项间分割线 -->
<u-line v-if="index !== actions.length - 1" :color="dividerColor"></u-line>
<u-line v-if="index !== actions.length - 1"></u-line>
</view>
</scroll-view>
</slot>
<!-- 取消按钮前的分割区域 -->
<u-gap
:bgColor="cancelGapColor"
bgColor="#eaeaec"
height="6"
v-if="cancelText"
></u-gap>
<!-- 取消按钮 -->
<view class="u-action-sheet__item-wrap__item u-action-sheet__cancel"
hover-class="u-action-sheet--hover" @tap="cancel" v-if="cancelText">
<text
@touchmove.stop.prevent
:hover-stay-time="150"
class="u-action-sheet__cancel-text"
:style="cancelTextDynamicStyle"
>{{cancelText}}</text>
</view>
</view>
@@ -127,7 +114,7 @@
/**
* ActionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
* @tutorial https://uview-plus.jiangruyi.com/components/actionSheet.html
* @tutorial https://ijry.github.io/uview-plus/components/actionSheet.html
*
* @property {Boolean} show 操作菜单是否展示 (默认 false )
* @property {String} title 操作菜单标题
@@ -167,54 +154,20 @@
}
},
computed: {
titleDynamicStyle() {
return {
color: this.upThemeVar('--up-main-color', '#303133')
}
},
descriptionDynamicStyle() {
return {
color: this.upThemeVar('--up-tips-color', '#909193')
}
},
closeIconColor() {
return this.upThemeVar('--up-content-color', '#606266')
},
dividerColor() {
return this.upThemeVar('--up-border-color', this.upThemeIsDark ? '#3a3a3c' : '#dadbde')
},
cancelGapColor() {
return this.upThemeVar('--up-gap-bg-color', this.upThemeIsDark ? '#111111' : '#eaeaec')
},
cancelTextDynamicStyle() {
return {
color: this.upThemeVar('--up-main-color', '#303133')
}
},
// 操作项目的样式
itemStyle() {
return (index) => {
const style = {
color: this.upThemeVar('--up-main-color', '#303133')
};
let style = {};
if (this.actions[index].color) style.color = this.actions[index].color
if (this.actions[index].fontSize) style.fontSize = addUnit(this.actions[index].fontSize)
// 选项被禁用的样式
if (this.actions[index].disabled) style.color = this.upThemeVar('--up-light-color', '#c0c4cc')
if (this.actions[index].disabled) style.color = '#c0c4cc'
return style;
}
},
subnameStyle() {
return (index) => ({
color: this.actions[index].disabled
? this.upThemeVar('--up-light-color', '#c0c4cc')
: this.upThemeVar('--up-tips-color', '#909193')
})
}
},
emits: ["close", "select", "update:show"],
methods: {
// 关闭操作菜单事件处理
closeHandler() {
// 允许点击遮罩关闭时,才发出close事件
if(this.closeOnClickOverlay) {
@@ -227,7 +180,6 @@
this.$emit('update:show', false)
this.$emit('close')
},
// 选择操作项处理
selectHandler(index) {
const item = this.actions[index]
if (item && !item.disabled && !item.loading) {
@@ -238,39 +190,30 @@
}
}
},
// 动态处理Hover时候第一个item的圆角
getItemHoverStyle(index) {
if (index === 0 && this.round && !this.title && !this.description) {
return {
borderTopLeftRadius: `${this.round}px`,
borderTopRightRadius: `${this.round}px`,
}
}
return {}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-action-sheet-reset-button-width:100% !default;
$u-action-sheet-title-font-size: 16px !default;
$u-action-sheet-title-padding: 12px 30px !default;
$u-action-sheet-title-color: var(--up-main-color, #303133) !default;
$u-action-sheet-title-color: $u-main-color !default;
$u-action-sheet-header-icon-wrap-right:15px !default;
$u-action-sheet-header-icon-wrap-top:15px !default;
$u-action-sheet-description-font-size:13px !default;
$u-action-sheet-description-color: var(--up-tips-color, #909193) !default;
$u-action-sheet-description-color:14px !default;
$u-action-sheet-description-margin: 18px 15px !default;
$u-action-sheet-item-wrap-item-padding:17px !default;
$u-action-sheet-item-wrap-name-font-size:16px !default;
$u-action-sheet-item-wrap-subname-font-size:13px !default;
$u-action-sheet-item-wrap-subname-color: var(--up-tips-color, #909193) !default;
$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
$u-action-sheet-item-wrap-subname-margin-top:10px !default;
$u-action-sheet-cancel-text-font-size:16px !default;
$u-action-sheet-cancel-text-color: var(--up-main-color, #303133) !default;
$u-action-sheet-cancel-text-color:$u-content-color !default;
$u-action-sheet-cancel-text-font-size:15px !default;
$u-action-sheet-cancel-text-hover-background-color: var(--up-hover-bg-color, rgb(242, 243, 245)) !default;
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
.u-reset-button {
width: $u-action-sheet-reset-button-width;
@@ -296,9 +239,8 @@
}
&__description {
display: block;
font-size: $u-action-sheet-description-font-size;
color: $u-action-sheet-description-color;
color: $u-tips-color;
margin: $u-action-sheet-description-margin;
text-align: center;
}
@@ -314,7 +256,7 @@
&__name {
font-size: $u-action-sheet-item-wrap-name-font-size;
color: var(--up-main-color, #303133);
color: $u-main-color;
text-align: center;
}
@@ -1,77 +0,0 @@
<style scoped lang="scss">
.agreement-content {
width: 100%;;
display: inline-block;
flex-direction: column;
color: var(--up-main-color, #303133);
.agreement-url {
display: inline-block;
color: var(--up-primary, #2979ff);
// #ifdef H5
cursor: pointer;
// #endif
}
}
</style>
<template>
<view class="up-agreement">
<up-modal v-model:show="show" showCancelButton @confirm="confirm" @cancel="close" confirmText="阅读并同意">
<view class="agreement-content">
<slot>
我们非常重视您的个人信息和隐私保护。为了更好地保障您的个人权益,在您使用我们的产品前,
请务必审慎阅读《<text class="agreement-url" @click="urlClick('urlProtocol')">用户协议</text>》
和《<text class="agreement-url" @click="urlClick('urlPrivacy')">隐私政策</text>》内的所有条款,
尤其是:1.我们对您的个人信息的收集/保存/使用/对外提供/保护等规则条款,以及您的用户权利等条款;2. 约定我们的限制责任、免责
条款;3.其他以颜色或加粗进行标识的重要条款。如您对以上协议有任何疑问,请先不要同意,您点击“同意并继续”的行为即表示您已阅读
完毕并同意以上协议的全部内容。
</slot>
</view>
</up-modal>
</view>
</template>
<script>
export default {
name: 'up-agreement',
props: {
urlProtocol: {
type: String,
default: '/pages/user_agreement/agreement/info?title=用户协议'
},
urlPrivacy: {
type: String,
default: '/pages/user_agreement/agreement/info?title=隐私政策'
},
},
emits: ['confirm'],
data() {
return {
show: false
}
},
methods: {
close() {
// #ifdef H5
window.opener = null;
window.close();
// #endif
// #ifdef APP-PLUS
plus.runtime.quit();
// #endif
},
confirm() {
this.show = false;
this.$emit('confirm', 1);
},
showModal() {
this.show = true;
},
urlClick(type) {
uni.navigateTo({
url: this[type]
});
}
}
}
</script>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-08-16 16:32:24
* @FilePath : /uview-plus/libs/config/props/album.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:47:24
* @FilePath : /u-view2.0/uview-ui/libs/config/props/album.js
*/
export default {
// album 组件
@@ -1,16 +1,5 @@
/*
* @Author : jry
* @Description :
* @version : 3.0
* @LastAuthor : jry
* @lastTime : 2025-08-16 16:35:24
* @FilePath : /uview-plus/components/u-album/props.js
*/
import { defineMixin } from '../../libs/vue'
import AlbumDefaultProps from './album'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AlbumDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 图片地址,Array<String>|Array<Object>形式
@@ -1,6 +1,5 @@
<template>
<view class="u-album">
<!-- 相册行容器,每行显示 rowCount 个图片 -->
<view
class="u-album__row"
ref="u-album__row"
@@ -9,15 +8,13 @@
:key="index"
:style="{flexWrap: autoWrap ? 'wrap' : 'nowrap'}"
>
<!-- 图片包装容器 -->
<view
class="u-album__row__wrapper"
v-for="(item, index1) in arr"
:key="index1"
:style="[imageStyle(index + 1, index1 + 1)]"
@tap="onPreviewTap($event, getSrc(item))"
@tap="previewFullImage ? onPreviewTap($event, getSrc(item)) : ''"
>
<!-- 图片显示 -->
<image
:src="getSrc(item)"
:mode="
@@ -35,7 +32,6 @@
}
]"
></image>
<!-- 超出最大显示数量时的更多提示 -->
<view
v-if="
showMore &&
@@ -68,14 +64,14 @@ import { mixin } from '../../libs/mixin/mixin';
import { addUnit, sleep } from '../../libs/function/index';
import test from '../../libs/function/test';
// #ifdef APP-NVUE
// 不支持百分比单位,这里需要通过dom查询组件的宽度
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* Album 相册
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
* @tutorial https://uview-plus.jiangruyi.com/components/album.html
* @tutorial https://ijry.github.io/uview-plus/components/album.html
*
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
@@ -112,20 +108,14 @@ export default {
urls: {
immediate: true,
handler(newVal) {
// 当只有一张图片时,获取图片尺寸信息
if (newVal.length === 1) {
this.getImageRect()
}
}
}
},
emits: ["albumWidth"],
computed: {
/**
* 计算图片样式
* @param {Number} index1 - 行索引
* @param {Number} index2 - 列索引
* @returns {Object} 图片样式对象
*/
imageStyle() {
return (index1, index2) => {
const { space, rowCount, multipleSize, urls } = this,
@@ -149,16 +139,11 @@ export default {
return style
}
},
/**
* 将图片地址数组划分为二维数组,用于按行显示
* @returns {Array} 二维数组,每个子数组代表一行图片
*/
// 将数组划分为二维数组
showUrls() {
if (this.autoWrap) {
// 自动换行模式下,所有图片放在一行中显示
return [ this.urls.slice(0, this.maxCount) ];
} else {
// 固定行数模式下,按 rowCount 分割图片
const arr = []
this.urls.map((item, index) => {
// 限制最大展示数量
@@ -175,29 +160,18 @@ export default {
return arr
}
},
/**
* 计算图片宽度
* @returns {String} 图片宽度样式值
*/
imageWidth() {
return addUnit(
this.urls.length === 1 ? this.singleWidth : this.multipleSize, this.unit
)
},
/**
* 计算图片高度
* @returns {String} 图片高度样式值
*/
imageHeight() {
return addUnit(
this.urls.length === 1 ? this.singleHeight : this.multipleSize, this.unit
)
},
/**
* 计算相册总宽度,用于外部组件对齐
* 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
* @returns {Number} 相册宽度
*/
// 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
albumWidth() {
let width = 0
if (this.urls.length === 1) {
@@ -211,100 +185,59 @@ export default {
return width
}
},
emits: ['preview', 'albumWidth'],
methods: {
addUnit,
/**
* 点击图片预览
* @param {Event} e - 点击事件对象
* @param {String} url - 当前点击图片的地址
*/
// 预览图片
onPreviewTap(e, url) {
// 获取所有图片地址
const urls = this.urls.map((item) => {
return this.getSrc(item)
})
if (this.previewFullImage) {
// 使用系统默认预览图片功能
uni.previewImage({
current: url,
urls
})
// 是否阻止事件传播
this.stop && this.preventEvent(e)
} else {
// 发送自定义预览事件
this.$emit('preview', {
urls,
currentIndex: urls.indexOf(url)
})
}
},
/**
* 获取图片地址
* @param {String|Object} item - 图片项,可以是字符串或对象
* @returns {String} 图片地址
*/
// 获取图片的路径
getSrc(item) {
return test.object(item)
? (this.keyName && item[this.keyName]) || item.src
: item
},
/**
* 单图时,获取图片的尺寸
* 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
* 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
*/
// 单图时,获取图片的尺寸
// 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
getImageRect() {
const src = this.getSrc(this.urls[0])
uni.getImageInfo({
src,
success: (res) => {
let singleSize = this.singleSize;
// 单位
let unit = '';
if (Number.isNaN(Number(this.singleSize))) {
// 大小中有字符 则记录字符
unit = this.singleSize.replace(/\d+/g, ''); // 单位
singleSize = Number(this.singleSize.replace(/\D+/g, ''), 10); // 具体值
}
// 判断图片横向还是竖向展示方式
const isHorizotal = res.width >= res.height
this.singleWidth = isHorizotal
? singleSize
: (res.width / res.height) * singleSize
? this.singleSize
: (res.width / res.height) * this.singleSize
this.singleHeight = !isHorizotal
? singleSize
? this.singleSize
: (res.height / res.width) * this.singleWidth
// 如果有单位统一设置单位
if(unit != null && unit !== ''){
this.singleWidth = this.singleWidth + unit
this.singleHeight = this.singleHeight + unit
}
},
fail: () => {
// 获取图片信息失败时,通过组件宽度计算
this.getComponentWidth()
}
})
},
/**
* 获取组件的宽度,用于计算单图显示尺寸
*/
// 获取组件的宽度
async getComponentWidth() {
// 延时一定时间,以获取dom尺寸
await sleep(30)
// #ifndef APP-NVUE
// H5、小程序等平台通过 $uGetRect 获取组件宽度
this.$uGetRect('.u-album__row').then((size) => {
this.singleWidth = size.width * this.singlePercent
})
// #endif
// #ifdef APP-NVUE
// NVUE 平台通过 dom 插件获取组件宽度
// 这里ref="u-album__row"所在的标签为通过for循环出来,导致this.$refs['u-album__row']是一个数组
const ref = this.$refs['u-album__row'][0]
ref &&
@@ -318,6 +251,8 @@ export default {
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-album {
@include flex(column);
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-08-17 17:23:53
* @FilePath : /uview-plus/libs/config/props/alert.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:48:53
* @FilePath : /u-view2.0/uview-ui/libs/config/props/alert.js
*/
export default {
// alert警告组件
@@ -17,10 +17,6 @@ export default {
showIcon: false,
effect: 'light',
center: false,
fontSize: 14,
transitionMode: 'fade',
duration: 0,
icon: '',
value: true
fontSize: 14
}
}
@@ -1,16 +1,5 @@
/*
* @Author : jry
* @Description :
* @version : 3.0
* @LastAuthor : jry
* @lastTime : 2025-08-17 17:23:53
* @FilePath : /uview-plus/libs/config/props/props.js
*/
import { defineMixin } from '../../libs/vue'
import AlertDefaultProps from './alert'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AlertDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 显示文字
@@ -52,26 +41,6 @@ export const props = defineMixin({
fontSize: {
type: [String, Number],
default: () => defProps.alert.fontSize
},
// 动画类型
transitionMode: {
type: [String],
default: () => defProps.alert.transitionMode
},
// 自动定时关闭毫秒
duration: {
type: [Number],
default: () => defProps.alert.duration
},
// 自定义图标
icon: {
type: [String],
default: () => defProps.alert.icon
},
// 是否显示
modelValue: {
type: [Boolean],
default: () => defProps.alert.value
}
}
})
@@ -1,6 +1,6 @@
<template>
<up-transition
:mode="transitionMode"
<u-transition
mode="fade"
:show="show"
>
<view
@@ -9,25 +9,22 @@
@tap.stop="clickHandler"
:style="[addStyle(customStyle)]"
>
<!-- 左侧图标 -->
<view
class="u-alert__icon"
v-if="showIcon"
>
<up-icon
<u-icon
:name="iconName"
size="18"
:color="iconColor"
></up-icon>
></u-icon>
</view>
<!-- 内容区域 -->
<view
class="u-alert__content"
:style="[{
paddingRight: closable ? '20px' : 0
}]"
>
<!-- 标题 -->
<text
class="u-alert__content__title"
v-if="title"
@@ -37,7 +34,6 @@
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ title }}</text>
<!-- 描述信息 -->
<text
class="u-alert__content__desc"
v-if="description"
@@ -48,22 +44,19 @@
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ description }}</text>
</view>
<!-- 关闭按钮 -->
<view
class="u-alert__close"
v-if="closable"
@tap.stop="closeHandler"
>
<slot name="close">
<up-icon
<u-icon
name="close"
:color="iconColor"
size="15"
></up-icon>
</slot>
></u-icon>
</view>
</view>
</up-transition>
</u-transition>
</template>
<script>
@@ -74,7 +67,7 @@
/**
* Alert 警告提示
* @description 警告提示,展现需要关注的信息。
* @tutorial https://uview-plus.jiangruyi.com/components/alertTips.html
* @tutorial https://ijry.github.io/uview-plus/components/alertTips.html
*
* @property {String} title 显示的文字
* @property {String} type 使用预设的颜色 (默认 'warning' )
@@ -85,34 +78,24 @@
* @property {Boolean} center 文字是否居中 (默认 false )
* @property {String | Number} fontSize 字体大小 (默认 14 )
* @property {Object} customStyle 定义需要用到的外部样式
* @property {String} transitionMode 过渡动画模式 (默认 'fade' )
* @property {String | Number} duration 自动关闭延时(毫秒),设置为0或负数则不自动关闭 (默认 0 )
* @property {String} icon 自定义图标名称,优先级高于type默认图标
* @property {Boolean} modelValue/v-model 绑定值,控制是否显示 (默认 true )
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @event {Function} closed 关闭动画结束时触发
* @example <up-alert :title="title" type = "warning" :closable="closable" :description = "description"></up-alert>
* @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
*/
export default {
name: 'u-alert',
mixins: [mpMixin, mixin, props],
data() {
return {
// 控制组件显示隐藏
show: true
}
},
computed: {
// 根据不同的主题类型返回对应的图标颜色
iconColor() {
return this.effect === 'light' ? this.type : '#fff'
},
// 不同主题对应不同的图标
iconName() {
// 如果用户自定义了图标,则优先使用自定义图标
if (this.icon) return this.icon;
switch (this.type) {
case 'success':
return 'checkmark-circle-fill';
@@ -134,50 +117,25 @@
}
}
},
emits: ["click","close", "closed", "update:modelValue"],
watch: {
modelValue: {
handler(newVal) {
this.show = newVal;
},
immediate: true
},
show: {
handler(newVal) {
this.$emit('update:modelValue', newVal);
// 如果是从显示到隐藏,且启用了自动关闭功能
if (!newVal && this.duration > 0) {
this.$emit('closed');
}
}
}
},
mounted() {
// 如果设置了自动关闭时间,则在指定时间后自动关闭
if (this.duration > 0) {
setTimeout(() => {
this.closeHandler();
}, this.duration);
}
},
emits: ["click","close"],
methods: {
addUnit,
addStyle,
// 点击内容区域触发click事件
// 点击内容
clickHandler() {
this.$emit('click')
},
// 点击关闭按钮触发close事件并隐藏组件
// 点击关闭按钮
closeHandler() {
this.show = false
this.$emit('close');
this.$emit('close')
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-alert {
position: relative;
@@ -195,7 +153,7 @@
}
&--primary--light {
background-color: var(--up-primary-light, #ecf5ff);
background-color: #ecf5ff;
}
&--error--dark {
@@ -203,7 +161,7 @@
}
&--error--light {
background-color: var(--up-error-light, #FEF0F0);
background-color: #FEF0F0;
}
&--success--dark {
@@ -211,7 +169,7 @@
}
&--success--light {
background-color: var(--up-success-light, #f5fff0);
background-color: #f5fff0;
}
&--warning--dark {
@@ -219,7 +177,7 @@
}
&--warning--light {
background-color: var(--up-warning-light, #FDF6EC);
background-color: #FDF6EC;
}
&--info--dark {
@@ -227,7 +185,7 @@
}
&--info--light {
background-color: var(--up-info-light, #f4f4f5);
background-color: #f4f4f5;
}
&__icon {
@@ -239,22 +197,24 @@
flex: 1;
&__title {
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
font-size: 14px;
font-weight: bold;
color: #fff;
margin-bottom: 2px;
}
&__desc {
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
font-size: 14px;
flex-wrap: wrap;
color: #fff;
}
}
&__title--dark,
&__desc--dark {
color: var(--up-white, #FFFFFF);
color: #FFFFFF;
}
&__text--primary--light,
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/avatarGroup.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:55
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatarGroup.js
*/
export default {
// avatarGroup 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import AvatarGroupDefaultProps from './avatarGroup'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AvatarGroupDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 头像图片组
@@ -40,7 +40,7 @@
/**
* AvatarGroup 头像组
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://uview-plus.jiangruyi.com/components/avatar.html
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
*
* @property {Array} urls 头像图片组 (默认 [] )
* @property {String | Number} maxCount 最多展示的头像数量 ( 默认 5 )
@@ -79,6 +79,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar-group {
@include flex;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/avatar.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:22
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatar.js
*/
export default {
// avatar 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import AvatarDefaultProps from './avatar'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AvatarDefaultProps)
import defProps from '../../libs/config/props.js'
import test from '../../libs/function/test';
export const props = defineMixin({
props: {
@@ -23,12 +23,12 @@
<!-- #ifndef MP-WEIXIN && MP-QQ && MP-BAIDU -->
<template v-if="mpAvatar && allowMp"></template>
<!-- #endif -->
<up-icon
<u-icon
v-else-if="icon"
:name="icon"
:size="fontSize"
:color="color"
></up-icon>
></u-icon>
<up-text
v-else-if="text"
:text="text"
@@ -63,7 +63,7 @@
/**
* Avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://uview-plus.jiangruyi.com/components/avatar.html
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
*
* @property {String} src 头像路径,如加载失败,将会显示默认头像(不能为相对路径)
* @property {String} shape 头像形状 ( circle (默认) | square)
@@ -151,6 +151,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar {
@include flex;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/backtop.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:50:18
* @FilePath : /u-view2.0/uview-ui/libs/config/props/backtop.js
*/
export default {
// backtop组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import BackTopDefaultProps from './backtop'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BackTopDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 返回顶部的形状,circle-圆形,square-方形
@@ -10,10 +10,10 @@
v-if="!$slots.default && !$slots.$default"
@click="backToTop"
>
<up-icon
<u-icon
:name="icon"
:custom-style="iconStyle"
></up-icon>
></u-icon>
<text
v-if="text"
class="u-back-top__text"
@@ -111,6 +111,7 @@
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-back-top-flex:1 !default;
$u-back-top-height:100% !default;
$u-back-top-background-color:#E1E1E1 !default;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/badge.js
* @LastAuthor : LQ
* @lastTime : 2021-08-23 19:51:50
* @FilePath : /u-view2.0/uview-ui/libs/config/props/badge.js
*/
export default {
// 徽标数组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import BadgeDefaultProps from './badge'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BadgeDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 是否显示圆点
@@ -89,6 +89,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-badge-primary: $u-primary !default;
$u-badge-error: $u-error !default;
File diff suppressed because it is too large Load Diff
@@ -1,24 +0,0 @@
/*
* @Author : ly
* @Description :
* @version : 4.0
* @Date : 2026-02-02 23:54:58
* @LastAuthor : ly
* @lastTime : 2026-02-02 23:54:58
* @FilePath : /uview-ultra/components/up-box/box.js
*/
export default {
// box 组件
box: {
bgColors: ['#EEFCFF', '#FCF8FF', '#FDF8F2'],
height: '160px',
borderRadius: '6px',
gap: '15px',
leftIcon: '',
leftTitle: '左',
rightTopIcon: '',
rightTopTitle: '右上',
rightBottomIcon: '',
rightBottomTitle: '右下'
}
}
@@ -1,60 +1,27 @@
import { defineMixin } from '../../libs/vue.js'
import BoxDefaultProps from './box.js'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BoxDefaultProps)
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const propsBox = defineMixin({
props: {
// 背景色
bgColors: {
type: [Array],
default: () => defProps.box.bgColors
default: ['#EEFCFF', '#FCF8FF', '#FDF8F2']
},
// 高度
height: {
type: [String],
default: () => defProps.box.height
default: "160px"
},
// 圆角
borderRadius: {
type: [String],
default: () => defProps.box.borderRadius
default: "6px"
},
// 间隔
gap: {
type: [String],
default: () => defProps.box.gap
},
// 左侧图标
leftIcon: {
type: [String],
default: () => defProps.box.leftIcon
},
// 左侧文案
leftTitle: {
type: [String],
default: () => defProps.box.leftTitle
},
// 右上图标
rightTopIcon: {
type: [String],
default: () => defProps.box.rightTopIcon
},
// 右上文案
rightTopTitle: {
type: [String],
default: () => defProps.box.rightTopTitle
},
// 右下图标
rightBottomIcon: {
type: [String],
default: () => defProps.box.rightBottomIcon
},
// 右下文案
rightBottomTitle: {
type: [String],
default: () => defProps.box.rightBottomTitle
default: "15px"
},
}
})
@@ -1,31 +1,16 @@
<template>
<view class="u-box" :style="[{height: height}, addStyle(customStyle)]">
<view class="u-box__left" :style="{borderRadius: borderRadius, backgroundColor: bgColors[0]}">
<slot name="left">
<view class="flex flex-row items-center justify-center">
<u-icon size="36" :name="leftIcon"></u-icon>
<text class="ml-2 text-16px">{{leftTitle}}</text>
</view>
</slot>
<slot name="left">左</slot>
</view>
<view class="u-box__gap" :style="{width: gap, height: height}"></view>
<view class="u-box__right">
<view class="u-box__right-top" :style="{borderRadius: borderRadius, backgroundColor: bgColors[1]}">
<slot name="rightTop">
<view class="flex flex-row items-center justify-center">
<u-icon size="36" :name="rightTopIcon"></u-icon>
<text class="ml-2 text-15px">{{rightTopTitle}}</text>
</view>
</slot>
<slot name="rightTop">右上</slot>
</view>
<view class="u-box__right-gap" :style="{height: gap}"></view>
<view class="u-box__right-bottom" :style="{borderRadius: borderRadius, backgroundColor: bgColors[2]}">
<slot name="rightBottom">
<view class="flex flex-row items-center justify-center">
<u-icon size="36" :name="rightBottomIcon"></u-icon>
<text class="ml-2 text-15px">{{rightBottomTitle}}</text>
</view>
</slot>
<slot name="rightBottom">右下</slot>
</view>
</view>
</view>
@@ -66,6 +51,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-box {
/* #ifndef APP-NVUE */
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/button.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:51:27
* @FilePath : /u-view2.0/uview-ui/libs/config/props/button.js
*/
export default {
// button组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import ButtonDefaultProps from './button'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(ButtonDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 是否细边框
@@ -39,13 +39,13 @@
>
</template>
<template v-else>
<up-icon
<u-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
:customStyle="{ marginRight: '2px' }"
></up-icon>
></u-icon>
<slot>
<text
class="u-button__text"
@@ -87,12 +87,12 @@
>
</template>
<template v-else>
<up-icon
<u-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
></up-icon>
></u-icon>
<text
class="u-button__text"
:style="[
@@ -117,11 +117,11 @@ import { mixin } from '../../libs/mixin/mixin';
import { props } from "./props";
import { addStyle } from '../../libs/function/index';
import { throttle } from '../../libs/function/throttle';
import { getThemeVar } from '../../libs/theme/runtime.js';
import color from '../../libs/config/color';
/**
* button 按钮
* @description Button 按钮
* @tutorial https://uview-plus.jiangruyi.com/components/button.html
* @tutorial https://ijry.github.io/uview-plus/components/button.html
*
* @property {Boolean} hairline 是否显示按钮的细边框 (默认 true )
* @property {String} type 按钮的预置样式,info,primary,error,warning,success (默认 'info' )
@@ -192,67 +192,26 @@ export default {
);
}
},
isDarkTheme() {
return this.$u.theme && this.$u.theme.mode === 'dark';
},
nvueMainColor() {
return this.resolveNvueColor(this.$u.color.mainColor, this.isDarkTheme ? '#f5f5f5' : '#303133');
},
nvueBorderColor() {
return this.resolveNvueColor(this.$u.color.borderColor, this.isDarkTheme ? '#3a3a3c' : '#dadbde');
},
themeTypeColor() {
const fallbackMap = {
primary: '#3c9cff',
success: '#5ac725',
warning: '#f9ae3d',
error: '#f56c6c',
info: '#909399',
};
const type = fallbackMap[this.type] ? this.type : 'info';
return this.resolveNvueColor(
this.upThemeVar(`--up-${type}`, fallbackMap[type]),
fallbackMap[type]
);
},
nvueInfoBackgroundColor() {
return this.upThemeVar(
'--up-button-info-background-color',
this.upThemeVar(
'--up-card-bg-color',
this.isDarkTheme ? '#1c1c1e' : '#ffffff'
)
);
},
nvuePlainBackgroundColor() {
return this.upThemeVar(
'--up-button-plain-background-color',
this.upThemeVar(
'--up-card-bg-color',
this.isDarkTheme ? '#1c1c1e' : '#ffffff'
)
);
},
loadingColor() {
if (this.plain) {
// 如果有设置color值,则用color值,否则使用type主题颜色
return this.color
? this.color
: this.themeTypeColor;
: color[`u-${this.type}`];
}
if (this.type === "info") {
return this.isDarkTheme ? "#9ca3af" : "#c9c9c9";
return "#c9c9c9";
}
return "rgb(200, 200, 200)";
},
iconColorCom() {
// 如果是镂空状态,设置了color就用color值,否则使用主题颜色,
// up-icon的color能接受一个主题颜色的值
// u-icon的color能接受一个主题颜色的值
if (this.iconColor) return this.iconColor;
if (this.plain) {
return this.color ? this.color : this.type;
} else {
return this.type === "info" ? this.nvueMainColor : "#ffffff";
return this.type === "info" ? "#000000" : "#ffffff";
}
},
baseColor() {
@@ -281,30 +240,7 @@ export default {
style.borderWidth = "1px";
style.borderStyle = "solid";
}
return style;
}
// #ifdef APP-NVUE
const typeColor = this.themeTypeColor;
if (this.plain) {
style.color = this.type === 'info' ? this.nvueMainColor : typeColor;
style.backgroundColor = this.nvuePlainBackgroundColor;
style.borderColor = this.type === 'info' ? this.nvueBorderColor : typeColor;
style.borderWidth = this.hairline ? '0.5px' : '1px';
style.borderStyle = 'solid';
} else {
style.borderWidth = this.hairline ? '0.5px' : '1px';
style.borderStyle = 'solid';
if (this.type === 'info') {
style.color = this.nvueMainColor;
style.backgroundColor = this.nvueInfoBackgroundColor;
style.borderColor = this.nvueBorderColor;
} else {
style.color = '#ffffff';
style.backgroundColor = typeColor;
style.borderColor = typeColor;
}
}
// #endif
return style;
},
// nvue版本按钮的字体不会继承父组件的颜色,需要对每一个text组件进行单独的设置
@@ -312,12 +248,10 @@ export default {
let style = {};
// 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
if (this.type === "info") {
style.color = this.nvueMainColor;
style.color = "#323233";
}
if (this.color) {
style.color = this.plain ? this.color : "white";
} else if (this.plain && this.type !== 'info') {
style.color = this.themeTypeColor;
}
style.fontSize = this.textSize + "px";
return style;
@@ -337,14 +271,6 @@ export default {
'error', 'opensetting', 'launchapp', 'agreeprivacyauthorization'],
methods: {
addStyle,
resolveNvueColor(colorValue, fallbackColor) {
if (typeof colorValue !== 'string') return fallbackColor;
if (colorValue.indexOf('var(') > -1) return fallbackColor;
return colorValue;
},
upThemeVar(varName, fallbackColor) {
return getThemeVar(varName, fallbackColor, this.$u);
},
clickHandler(e: any) {
// 非禁止并且非加载中,才能点击
if (!this.disabled && !this.loading) {
@@ -380,6 +306,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
/* #ifndef APP-NVUE */
@import "./vue.scss";
/* #endif */
@@ -406,9 +334,9 @@ $u-button-mini-height: 22px !default;
$u-button-mini-font-size: 10px !default;
$u-button-mini-min-width: 50px !default;
$u-button-disabled-opacity: 0.5 !default;
$u-button-info-color: $u-main-color !default;
$u-button-info-background-color: var(--up-button-info-background-color, var(--up-card-bg-color, #fff)) !default;
$u-button-info-border-color: $u-border-color !default;
$u-button-info-color: #323233 !default;
$u-button-info-background-color: #fff !default;
$u-button-info-border-color: #ebedf0 !default;
$u-button-info-border-width: 1px !default;
$u-button-info-border-style: solid !default;
$u-button-success-color: #fff !default;
@@ -441,7 +369,7 @@ $u-button-square-border-top-left-radius: 3px !default;
$u-button-square-border-bottom-left-radius: 3px !default;
$u-button-square-border-bottom-right-radius: 3px !default;
$u-button-icon-min-width: 1em !default;
$u-button-plain-background-color: var(--up-button-plain-background-color, var(--up-card-bg-color, #fff)) !default;
$u-button-plain-background-color: #fff !default;
$u-button-hairline-border-width: 0.5px !default;
.u-button {
@@ -15,7 +15,7 @@ $u-button-plain-u-button-error-color:$u-error;
$u-button-plain-u-button-warning-color:$u-warning;
.u-button {
width: auto;
width: 100%;
white-space: nowrap;
&__text {
@@ -1,36 +0,0 @@
/*
* @Author : uview-plus
* @Description : calendarStrip default props
* @version : 3.0
* @Date : 2026-05-29 14:40:00
*/
import { t } from '../../libs/i18n'
export default {
calendarStrip: {
modelValue: null,
minDate: 0,
maxDate: 0,
color: '#3c9cff',
weekText: [
t('up.week.one'),
t('up.week.two'),
t('up.week.three'),
t('up.week.four'),
t('up.week.five'),
t('up.week.six'),
t('up.week.seven')
],
fullCalendar: true,
fullCalendarProps: {},
fullMonthNum: 24,
pullDownThreshold: 40,
collapseAfterSelect: true,
readonly: false,
showToday: true,
monthFormat: '',
expandHint: '下拉展开月历',
collapseHint: '上拉收起月历'
}
}
@@ -1,70 +0,0 @@
import { defineMixin } from '../../libs/vue'
import CalendarStripDefaultProps from './calendarStrip'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CalendarStripDefaultProps)
export const props = defineMixin({
props: {
modelValue: {
type: [String, Number, Date, null],
default: () => defProps.calendarStrip.modelValue
},
minDate: {
type: [String, Number],
default: () => defProps.calendarStrip.minDate
},
maxDate: {
type: [String, Number],
default: () => defProps.calendarStrip.maxDate
},
color: {
type: String,
default: () => defProps.calendarStrip.color
},
weekText: {
type: Array,
default: () => defProps.calendarStrip.weekText
},
fullCalendar: {
type: Boolean,
default: () => defProps.calendarStrip.fullCalendar
},
fullCalendarProps: {
type: Object,
default: () => defProps.calendarStrip.fullCalendarProps
},
fullMonthNum: {
type: [String, Number],
default: () => defProps.calendarStrip.fullMonthNum
},
pullDownThreshold: {
type: [String, Number],
default: () => defProps.calendarStrip.pullDownThreshold
},
collapseAfterSelect: {
type: Boolean,
default: () => defProps.calendarStrip.collapseAfterSelect
},
readonly: {
type: Boolean,
default: () => defProps.calendarStrip.readonly
},
showToday: {
type: Boolean,
default: () => defProps.calendarStrip.showToday
},
monthFormat: {
type: String,
default: () => defProps.calendarStrip.monthFormat
},
expandHint: {
type: String,
default: () => defProps.calendarStrip.expandHint
},
collapseHint: {
type: String,
default: () => defProps.calendarStrip.collapseHint
}
}
})
@@ -1,507 +0,0 @@
<template>
<view class="u-calendar-strip">
<view v-if="!fullCalendar || !innerShowFull">
<view class="u-calendar-strip__header u-border-bottom">
<text
class="u-calendar-strip__header__switch"
:class="{ 'u-calendar-strip__header__switch--disabled': switchPrevDisabled }"
@tap="prevMonth"
>‹</text>
<text class="u-calendar-strip__header__title">{{ monthLabel }}</text>
<text
class="u-calendar-strip__header__switch"
:class="{ 'u-calendar-strip__header__switch--disabled': switchNextDisabled }"
@tap="nextMonth"
>›</text>
<text
v-if="fullCalendar"
class="u-calendar-strip__header__toggle"
@click="toggleFull('button')"
>▾</text>
</view>
<view
class="u-calendar-strip__scroll-wrap"
@touchstart="onTouchStart"
@touchend="onTouchEnd"
>
<scroll-view
class="u-calendar-strip__scroll"
scroll-x
enable-flex
:show-scrollbar="false"
:scroll-into-view="scrollIntoView"
>
<view class="u-calendar-strip__scroll__inner">
<view
v-for="(item, index) in monthDays"
:key="index"
class="u-calendar-strip__day"
:id="getDateId(item.date)"
:class="[
item.disabled && 'u-calendar-strip__day--disabled',
item.selected && 'u-calendar-strip__day--selected',
item.today && showToday && !item.selected && 'u-calendar-strip__day--today',
]"
:style="[dayStyle(item)]"
@tap="onDayTap(item)"
>
<text class="u-calendar-strip__day__date">{{ item.day }}</text>
<text class="u-calendar-strip__day__week">{{ getWeekLabel(item.week) }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<view
v-if="fullCalendar && innerShowFull"
class="u-calendar-strip__panel-wrap"
@touchstart="onTouchStart"
@touchend="onTouchEnd"
>
<view class="u-calendar-strip__panel">
<u-calendar
:show="true"
:pageInline="true"
:showTitle="false"
:showConfirm="false"
:closeOnClickOverlay="false"
:monthSwitch="true"
mode="single"
:defaultDate="innerSelectedDate"
:minDate="panelMinDate"
:maxDate="panelMaxDate"
:monthNum="panelMonthNum"
:readonly="readonly"
:showToday="showToday"
:color="color"
v-bind="fullCalendarProps"
@confirm="onPanelConfirm"
></u-calendar>
</view>
<view class="u-calendar-strip__hint u-calendar-strip__hint--panel">
<text class="u-calendar-strip__hint__text" @click="toggleFull('hint')">{{ collapseHint }}</text>
</view>
</view>
</view>
</template>
<script>
import { props } from './props'
import { mpMixin } from '../../libs/mixin/mpMixin'
import { mixin } from '../../libs/mixin/mixin'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js'
import test from '../../libs/function/test'
/**
* CalendarStrip 单行日历
* @description 单行横向日期日历,支持切月、下拉展开完整月历
*/
export default {
name: 'u-calendar-strip',
mixins: [mpMixin, mixin, props],
data() {
return {
innerSelectedDate: '',
currentMonth: '',
scrollIntoView: '',
innerShowFull: false,
touchStartX: 0,
touchStartY: 0
}
},
computed: {
innerMaxDate() {
return test.number(this.maxDate) ? Number(this.maxDate) : this.maxDate
},
innerMinDate() {
return test.number(this.minDate) ? Number(this.minDate) : this.minDate
},
rangeChange() {
return [this.innerMinDate, this.innerMaxDate]
},
hasMaxDate() {
return !!this.innerMaxDate && dayjs(this.innerMaxDate).isValid()
},
hasMinDate() {
return !!this.innerMinDate && dayjs(this.innerMinDate).isValid()
},
minDateDay() {
if (!this.hasMinDate) return ''
return dayjs(this.innerMinDate).format('YYYY-MM-DD')
},
maxDateDay() {
if (!this.hasMaxDate) return ''
return dayjs(this.innerMaxDate).format('YYYY-MM-DD')
},
todayDate() {
return dayjs().format('YYYY-MM-DD')
},
monthLabel() {
if (!this.currentMonth) return ''
const date = dayjs(`${this.currentMonth}-01`)
if (this.monthFormat) return date.format(this.monthFormat)
if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
return date.format('YYYY年MM月')
}
return date.format('MM/YYYY')
},
monthDays() {
if (!this.currentMonth) return []
const start = dayjs(`${this.currentMonth}-01`)
const days = start.daysInMonth()
return new Array(days).fill(0).map((item, index) => {
const date = start.date(index + 1).format('YYYY-MM-DD')
return {
day: index + 1,
date,
week: start.date(index + 1).day(),
disabled: this.isDateDisabled(date),
selected: this.dateSame(date, this.innerSelectedDate),
today: this.dateSame(date, this.todayDate)
}
})
},
switchPrevDisabled() {
if (!this.hasMinDate || !this.currentMonth) return false
const current = dayjs(`${this.currentMonth}-01`)
const minMonth = dayjs(this.minDateDay).startOf('month')
return current.isSame(minMonth, 'month') || current.isBefore(minMonth, 'month')
},
switchNextDisabled() {
if (!this.hasMaxDate || !this.currentMonth) return false
const current = dayjs(`${this.currentMonth}-01`)
const maxMonth = dayjs(this.maxDateDay).startOf('month')
return current.isSame(maxMonth, 'month') || current.isAfter(maxMonth, 'month')
},
panelMinDate() {
if (this.hasMinDate) return this.minDateDay
const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
const anchor = this.currentMonth || dayjs().format('YYYY-MM')
return dayjs(`${anchor}-01`).subtract(monthNum - 1, 'month').startOf('month').format('YYYY-MM-DD')
},
panelMaxDate() {
if (this.hasMaxDate) return this.maxDateDay
const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
const anchor = this.currentMonth || dayjs().format('YYYY-MM')
return dayjs(`${anchor}-01`).add(monthNum - 1, 'month').endOf('month').format('YYYY-MM-DD')
},
panelMonthNum() {
return this.getMonths(this.panelMinDate, this.panelMaxDate)
},
pullHintText() {
return this.innerShowFull ? this.collapseHint : this.expandHint
}
},
watch: {
modelValue: {
immediate: true,
handler(n) {
this.syncByValue(n, false, 'sync')
}
},
rangeChange() {
this.syncByValue(this.innerSelectedDate || this.modelValue, true, 'range')
}
},
emits: ['change', 'confirm', 'monthChange', 'toggleFull', 'update:modelValue'],
methods: {
dateSame(date1, date2) {
if (!date1 || !date2) return false
return dayjs(date1).isSame(dayjs(date2), 'day')
},
normalizeDate(value) {
if (!value) return ''
const parsed = dayjs(value)
if (!parsed.isValid()) return ''
return parsed.format('YYYY-MM-DD')
},
clampDate(date) {
let next = this.normalizeDate(date)
if (!next) return ''
if (this.hasMinDate && dayjs(next).isBefore(dayjs(this.minDateDay), 'day')) {
next = this.minDateDay
}
if (this.hasMaxDate && dayjs(next).isAfter(dayjs(this.maxDateDay), 'day')) {
next = this.maxDateDay
}
return next
},
isDateDisabled(date) {
if (!date) return true
if (this.hasMinDate && dayjs(date).isBefore(dayjs(this.minDateDay), 'day')) {
return true
}
if (this.hasMaxDate && dayjs(date).isAfter(dayjs(this.maxDateDay), 'day')) {
return true
}
return false
},
getDateId(date) {
return `u-calendar-strip-day-${dayjs(date).format('YYYYMMDD')}`
},
getWeekLabel(week) {
const index = week === 0 ? 6 : week - 1
return this.weekText[index] || ''
},
dayStyle(item) {
const style = {}
if (item.selected) {
style.backgroundColor = this.color
}
if (!item.selected && item.today && this.showToday) {
style.borderColor = this.color
}
return style
},
scrollToDate(date) {
if (!date) {
this.scrollIntoView = ''
return
}
const target = this.getDateId(date)
this.scrollIntoView = ''
this.$nextTick(() => {
this.scrollIntoView = target
})
},
syncByValue(value, emit = false, scene = 'sync') {
let next = this.clampDate(value)
if (!next) {
next = this.clampDate(this.todayDate)
}
if (!next) return
this.setSelectedDate(next, scene, emit)
},
setSelectedDate(date, scene = 'tap', emit = true) {
const next = this.clampDate(date)
if (!next || this.isDateDisabled(next)) return
const prevDate = this.innerSelectedDate
const prevMonth = this.currentMonth
this.innerSelectedDate = next
this.currentMonth = dayjs(next).format('YYYY-MM')
this.scrollToDate(next)
if (!emit) return
const payload = {
date: next,
month: this.currentMonth,
scene
}
if (!this.dateSame(prevDate, next)) {
this.$emit('update:modelValue', next)
}
this.$emit('change', payload)
this.$emit('confirm', payload)
if (prevMonth !== this.currentMonth) {
this.$emit('monthChange', {
month: this.currentMonth,
scene
})
}
},
getMonths(minDate, maxDate) {
const minYear = dayjs(minDate).year()
const minMonth = dayjs(minDate).month() + 1
const maxYear = dayjs(maxDate).year()
const maxMonth = dayjs(maxDate).month() + 1
return Math.max(1, (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1)
},
findFirstEnabledDate(month) {
const start = dayjs(`${month}-01`)
const days = start.daysInMonth()
for (let i = 1; i <= days; i++) {
const date = start.date(i).format('YYYY-MM-DD')
if (!this.isDateDisabled(date)) {
return date
}
}
return ''
},
switchMonth(step = 0) {
if (step < 0 && this.switchPrevDisabled) return
if (step > 0 && this.switchNextDisabled) return
const baseMonth = this.currentMonth || dayjs(this.todayDate).format('YYYY-MM')
const target = dayjs(`${baseMonth}-01`).add(step, 'month')
const targetMonth = target.format('YYYY-MM')
const selectedDay = dayjs(this.innerSelectedDate || this.todayDate).date()
const dayInTargetMonth = Math.min(selectedDay, target.daysInMonth())
let next = target.date(dayInTargetMonth).format('YYYY-MM-DD')
next = this.clampDate(next)
if (!next || !dayjs(next).isSame(target, 'month') || this.isDateDisabled(next)) {
next = this.findFirstEnabledDate(targetMonth)
}
if (!next) return
this.setSelectedDate(next, 'switch', true)
},
prevMonth() {
this.switchMonth(-1)
},
nextMonth() {
this.switchMonth(1)
},
onDayTap(item) {
if (this.readonly || item.disabled) return
this.setSelectedDate(item.date, 'tap', true)
},
setFullVisible(show, source = 'button') {
if (!this.fullCalendar) return
if (this.innerShowFull === show) return
this.innerShowFull = show
this.$emit('toggleFull', {
show,
source
})
},
toggleFull(source = 'button') {
this.setFullVisible(!this.innerShowFull, source)
},
onPanelConfirm(e) {
if (!Array.isArray(e) || !e.length) return
this.setSelectedDate(e[0], 'full', true)
if (this.collapseAfterSelect) {
this.setFullVisible(false, 'auto')
}
},
onTouchStart(event) {
if (!this.fullCalendar) return
const point = event?.changedTouches?.[0] || event?.touches?.[0]
if (!point) return
this.touchStartX = point.clientX
this.touchStartY = point.clientY
},
onTouchEnd(event) {
if (!this.fullCalendar) return
const point = event?.changedTouches?.[0] || event?.touches?.[0]
if (!point) return
const deltaX = point.clientX - this.touchStartX
const deltaY = point.clientY - this.touchStartY
const threshold = Number(this.pullDownThreshold) || 40
if (Math.abs(deltaY) < threshold || Math.abs(deltaY) <= Math.abs(deltaX)) {
return
}
if (deltaY > 0 && !this.innerShowFull) {
this.setFullVisible(true, 'pull-down')
}
if (deltaY < 0 && this.innerShowFull) {
this.setFullVisible(false, 'pull-up')
}
}
}
}
</script>
<style lang="scss" scoped>
.u-calendar-strip {
background-color: var(--up-card-bg-color, #ffffff);
border-radius: 10px;
margin: 0 8px;
&__header {
@include flex;
align-items: center;
padding: 0 4px;
height: 40px;
&__switch {
width: 40px;
text-align: center;
font-size: 20px;
line-height: 40px;
color: var(--up-main-color, $u-main-color);
&--disabled {
opacity: 0.35;
}
}
&__title {
flex: 1;
text-align: center;
font-size: 14px;
font-weight: bold;
color: var(--up-main-color, $u-main-color);
}
&__toggle {
width: 40px;
text-align: center;
font-size: 18px;
line-height: 40px;
color: var(--up-content-color, $u-content-color);
}
}
&__scroll-wrap {
padding: 10px 0 6px;
}
&__scroll {
&__inner {
@include flex;
padding: 0 8px;
}
}
&__day {
width: 54px;
height: 62px;
margin-right: 8px;
border-radius: 8px;
border: 1px solid transparent;
@include flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
&__date {
font-size: 18px;
line-height: 22px;
color: var(--up-main-color, $u-main-color);
}
&__week {
margin-top: 4px;
font-size: 12px;
line-height: 16px;
color: var(--up-content-color, $u-content-color);
}
&--selected {
.u-calendar-strip__day__date,
.u-calendar-strip__day__week {
color: #ffffff;
}
}
&--today {
border-style: solid;
}
&--disabled {
opacity: 0.4;
}
}
&__hint {
padding: 4px 0 10px;
text-align: center;
&__text {
font-size: 12px;
color: var(--up-tips-color, $u-tips-color);
}
}
&__panel {
padding: 0 8px 8px;
}
&__panel-wrap {
padding-top: 4px;
}
&__hint--panel {
padding: 0 0 8px;
}
}
</style>
@@ -1,22 +1,21 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/calendar.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:52:43
* @FilePath : /u-view2.0/uview-ui/libs/config/props/calendar.js
*/
import { t } from '../../libs/i18n'
export default {
// calendar 组件
calendar: {
title: t("up.calendar.chooseDates"),
title: '日期选择',
showTitle: true,
showSubtitle: true,
mode: 'single',
startText: t("up.common.start"),
endText: t("up.common.end"),
startText: '开始',
endText: '结束',
customList: [],
color: '#3c9cff',
minDate: 0,
@@ -27,8 +26,8 @@ export default {
formatter: null,
showLunar: false,
showMark: true,
confirmText: t("up.common.confirm"),
confirmDisabledText: t("up.common.confirm"),
confirmText: '确定',
confirmDisabledText: '确定',
show: false,
closeOnClickOverlay: false,
readonly: false,
@@ -37,27 +36,10 @@ export default {
rangePrompt: '',
showRangePrompt: true,
allowSameDay: false,
rangeResultMode: 'all',
enableTime: false,
timePrecision: 'minute',
defaultTime: '',
round: 0,
overlay: true,
duration: 300,
overlayStyle: {},
overlayOpacity: 0.5,
zIndex: 10075,
safeAreaInsetBottom: true,
safeAreaInsetTop: false,
bgColor: '',
monthNum: 3,
monthSwitch: false,
showToday: true,
todayColor: '',
weekText: [t("up.week.one"), t("up.week.two"), t("up.week.three"), t("up.week.four"), t("up.week.five"), t("up.week.six"), t("up.week.seven")],
weekText: ['一', '二', '三', '四', '五', '六', '日'],
forbidDays: [],
forbidDaysToast: t("up.calendar.disabled"),
monthFormat: '',
pageInline: false
forbidDaysToast: '该日期已禁用',
}
}
@@ -4,42 +4,10 @@
class="u-calendar-header__title"
v-if="showTitle"
>{{ title }}</text>
<view
class="u-calendar-header__subtitle-wrap"
<text
class="u-calendar-header__subtitle"
v-if="showSubtitle"
>
<text
v-if="showSwitch"
class="u-calendar-header__switch"
:class="{ 'u-calendar-header__switch--disabled': prevYearDisabled }"
@click="prevYear"
>«</text>
<text
v-if="showSwitch"
class="u-calendar-header__switch"
:class="{ 'u-calendar-header__switch--disabled': prevDisabled }"
@click="prev"
>‹</text>
<text class="u-calendar-header__subtitle">{{ subtitle }}</text>
<text
v-if="showSwitch"
class="u-calendar-header__switch"
:class="{ 'u-calendar-header__switch--disabled': nextDisabled }"
@click="next"
>›</text>
<text
v-if="showSwitch"
class="u-calendar-header__switch"
:class="{ 'u-calendar-header__switch--disabled': nextYearDisabled }"
@click="nextYear"
>»</text>
<text
v-if="showToday"
class="u-calendar-header__today"
:class="{ 'u-calendar-header__today--disabled': todayDisabled }"
@click="today"
>{{ todayText }}</text>
</view>
>{{ subtitle }}</text>
<view class="u-calendar-header__weekdays">
<text class="u-calendar-header__weekdays__weekday">{{ weekText[0] }}</text>
<text class="u-calendar-header__weekdays__weekday">{{ weekText[1] }}</text>
@@ -83,49 +51,9 @@
weekText: {
type: Array,
default: () => {
return []
return ['一', '二', '三', '四', '五', '六', '日']
}
},
// 是否显示月份切换按钮
showSwitch: {
type: Boolean,
default: false
},
// 上个月按钮是否禁用
prevDisabled: {
type: Boolean,
default: false
},
// 下个月按钮是否禁用
nextDisabled: {
type: Boolean,
default: false
},
// 上一年按钮是否禁用
prevYearDisabled: {
type: Boolean,
default: false
},
// 下一年按钮是否禁用
nextYearDisabled: {
type: Boolean,
default: false
},
// 是否显示今天按钮
showToday: {
type: Boolean,
default: true
},
// 今天按钮文案
todayText: {
type: String,
default: ''
},
// 今天按钮是否禁用
todayDisabled: {
type: Boolean,
default: false
}
},
data() {
return {
@@ -133,36 +61,15 @@
}
},
methods: {
prev() {
if (!this.prevDisabled) {
this.$emit('prev')
}
},
next() {
if (!this.nextDisabled) {
this.$emit('next')
}
},
prevYear() {
if (!this.prevYearDisabled) {
this.$emit('prevYear')
}
},
nextYear() {
if (!this.nextYearDisabled) {
this.$emit('nextYear')
}
},
today() {
if (!this.todayDisabled) {
this.$emit('today')
}
name() {
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-calendar-header {
display: flex;
@@ -171,41 +78,20 @@
&__title {
font-size: 16px;
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
text-align: center;
height: 42px;
line-height: 42px;
font-weight: bold;
}
&__subtitle-wrap {
@include flex;
align-items: center;
justify-content: center;
height: 40px;
}
&__subtitle {
font-size: 14px;
color: var(--up-main-color, $u-main-color);
text-align: center;
font-weight: bold;
min-width: 120px;
}
&__switch {
width: 44px;
color: $u-main-color;
height: 40px;
line-height: 40px;
text-align: center;
color: var(--up-main-color, $u-main-color);
font-size: 22px;
line-height: 40px;
font-weight: bold;
flex-shrink: 0;
&--disabled {
opacity: 0.35;
}
}
&__weekdays {
@@ -214,27 +100,11 @@
&__weekday {
font-size: 13px;
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
line-height: 30px;
flex: 1;
text-align: center;
}
}
&__today {
margin-left: 4px;
height: 28px;
line-height: 28px;
padding: 0 10px;
border-radius: 14px;
font-size: 13px;
color: var(--up-primary, $u-primary);
border: 1px solid var(--up-primary, $u-primary);
flex-shrink: 0;
&--disabled {
opacity: 0.35;
}
}
}
</style>
@@ -2,7 +2,7 @@
<view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper">
<view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]"
:ref="`u-calendar-month-${index}`" :id="`month-${index}`">
<text v-if="index !== 0" class="u-calendar-month__title">{{ monthTitle(item) }}</text>
<text v-if="index !== 0" class="u-calendar-month__title">{{ item.year }}年{{ item.month }}月</text>
<view class="u-calendar-month__days">
<view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper">
<text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text>
@@ -33,12 +33,11 @@
// #endif
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit, deepClone, toast, sleep, getWindowInfo } from '../../libs/function/index';
import { addUnit, deepClone, toast, sleep } from '../../libs/function/index';
import { colorGradient } from '../../libs/function/colorGradient';
import test from '../../libs/function/test';
import defProps from '../../libs/config/props';
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import { t } from '../../libs/i18n'
import dayjs from 'dayjs/esm/index'
export default {
name: 'u-calendar-month',
mixins: [mpMixin, mixin],
@@ -135,16 +134,6 @@
forbidDaysToast: {
type: String,
default: ''
},
// 今天日期,用于独立高亮
todayDate: {
type: String,
default: ''
},
// 今天日期的独立高亮颜色
todayColor: {
type: String,
default: ''
}
},
data() {
@@ -173,24 +162,17 @@
return (index1, index2, item) => {
const style = {}
let week = item.week
// 隐藏挂载场景下节点宽度可能拿到0,兜底使用窗口宽度避免首日偏移归零
const wrapperWidth = this.width > 0 ? this.width : (getWindowInfo().windowWidth || 0)
// 不进行四舍五入的形式保留2位小数
const dayWidth = Number(parseFloat(wrapperWidth / 7).toFixed(3).slice(0, -1))
const dayWidth = Number(parseFloat(this.width / 7).toFixed(3).slice(0, -1))
// 得出每个日期的宽度
// #ifdef APP-NVUE
style.width = addUnit(dayWidth, 'px')
// #endif
style.height = addUnit(this.rowHeight, 'px')
style.height = addUnit(this.rowHeight)
if (index2 === 0) {
// 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数
week = (week === 0 ? 7 : week) - 1
// #ifdef APP-NVUE
style.marginLeft = addUnit(week * dayWidth, 'px')
// #endif
// #ifndef APP-NVUE
style.marginLeft = `${(week / 7) * 100}%`
// #endif
}
if (this.mode === 'range') {
// 之所以需要这么写,是因为DCloud公司的iOS客户端导致的bug
@@ -210,10 +192,6 @@
if (this.selected.some(item => this.dateSame(item, date))) {
style.backgroundColor = this.color
}
if (this.todayDate && this.dateSame(date, this.todayDate)) {
style.border = `1px solid ${this.resolvedTodayColor}`
style.boxSizing = 'border-box'
}
if (this.mode === 'single') {
if (date === this.selected[0]) {
// 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
@@ -238,13 +216,12 @@
// 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
.selected[len]))) {
const rangeEndColor = this.upThemeVar('--up-card-bg-color', this.upThemeIsDark ? '#1c1c1e' : '#ffffff')
style.backgroundColor = colorGradient(this.color, rangeEndColor, 100)[90]
style.backgroundColor = colorGradient(this.color, '#ffffff', 100)[90]
// 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
style.opacity = this.upThemeIsDark ? 0.85 : 0.7
style.opacity = 0.7
}
} else if (this.selected.length === 1) {
// 之所以需要这么写,是因为uni-app的iOS客户端的bug
// 之所以需要这么写,是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
// 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
@@ -260,9 +237,6 @@
return style
}
},
resolvedTodayColor() {
return this.todayColor || this.color
},
// 某个日期是否被选中
textStyle() {
return (item) => {
@@ -280,9 +254,6 @@
style.color = this.color
}
}
if (this.todayDate && this.dateSame(date, this.todayDate) && !this.isSelectedDate(date)) {
style.color = this.resolvedTodayColor
}
return style
}
},
@@ -321,7 +292,6 @@
mounted() {
this.init()
},
emits: ['monthSelected', 'updateMonthTop'],
methods: {
init() {
// 初始化默认选中
@@ -335,13 +305,6 @@
})
})
},
monthTitle(item) {
if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
return item.year + '年' + (item.month < 10 ? '0' + item.month : item.month) + '月'
} else {
return (item.month < 10 ? '0' + item.month : item.month) + '/' + item.year
}
},
isForbid(item) {
let date = dayjs(item.date).format("YYYY-MM-DD")
if (this.mode !== 'range' && this.forbidDays.includes(date)) {
@@ -353,21 +316,16 @@
dateSame(date1, date2) {
return dayjs(date1).isSame(dayjs(date2))
},
isSelectedDate(date) {
return this.selected.some(item => this.dateSame(item, date))
},
// 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
getWrapperWidth() {
// #ifdef APP-NVUE
dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => {
const width = res && res.size ? Number(res.size.width) : 0
this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0)
this.width = res.size.width
})
// #endif
// #ifndef APP-NVUE
this.$uGetRect('.u-calendar-month-wrapper').then(size => {
const width = size ? Number(size.width) : 0
this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0)
this.width = size.width
})
// #endif
},
@@ -392,7 +350,7 @@
// 获取每个月份区域的尺寸
getMonthRectByPromise(el) {
// #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://uview-plus.jiangruyi.com/js/getRect.html
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html
// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
return new Promise(resolve => {
this.$uGetRect(`.${el}`).then(size => {
@@ -456,7 +414,7 @@
if(this.rangePrompt) {
toast(this.rangePrompt)
} else {
toast(t("up.calendar.daysExceed", { days: this.maxRange }))
toast(`选择天数不能超过 ${this.maxRange} 天`)
}
return
}
@@ -516,24 +474,13 @@
setSelected(selected, event = true) {
this.selected = selected
event && this.$emit('monthSelected', this.selected,'tap')
},
selectDate(date) {
const targetDate = dayjs(date).format("YYYY-MM-DD")
for (let monthIndex = 0; monthIndex < this.months.length; monthIndex++) {
const dayIndex = this.months[monthIndex].date.findIndex(item => {
return this.dateSame(item.date, targetDate)
})
if (dayIndex !== -1) {
this.clickHandler(monthIndex, dayIndex, this.months[monthIndex].date[dayIndex])
return
}
}
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-calendar-month-wrapper {
margin-top: 4px;
@@ -547,7 +494,7 @@
font-size: 14px;
line-height: 42px;
height: 42px;
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
text-align: center;
font-weight: bold;
}
@@ -569,7 +516,7 @@
&__text {
font-size: 155px;
color: var(--up-calendar-month-mark-color, rgba(231, 232, 234, 0.83));
color: rgba(231, 232, 234, 0.83);
}
}
@@ -600,7 +547,7 @@
}
&__buttom-info {
color: var(--up-content-color, $u-content-color);
color: $u-content-color;
text-align: center;
position: absolute;
bottom: 5px;
@@ -614,21 +561,20 @@
}
&--disabled {
color: var(--up-disabled-color, #cacbcd);
color: #cacbcd;
}
}
&__info {
text-align: center;
font-size: 16px;
color: var(--up-main-color, $u-main-color);
&--selected {
color: #ffffff;
}
&--disabled {
color: var(--up-disabled-color, #cacbcd);
color: #cacbcd;
}
}
@@ -659,5 +605,4 @@
}
}
}
</style>
@@ -1,8 +1,6 @@
import { defineMixin } from '../../libs/vue'
import CalendarDefaultProps from './calendar'
import { registerComponentProps } from '../../libs/config/props.js'
import defProps from '../../libs/config/props.js'
const defProps = registerComponentProps(CalendarDefaultProps)
export const props = defineMixin({
props: {
// 日历顶部标题
@@ -100,46 +98,6 @@ export const props = defineMixin({
type: Boolean,
default: () => defProps.calendar.show
},
// 是否显示遮罩
overlay: {
type: Boolean,
default: () => defProps.calendar.overlay
},
// 动画时长,单位ms
duration: {
type: [String, Number],
default: () => defProps.calendar.duration
},
// 自定义遮罩的样式
overlayStyle: {
type: [Object, String],
default: () => defProps.calendar.overlayStyle
},
// 遮罩的透明度,0-1之间
overlayOpacity: {
type: [Number, String],
default: () => defProps.calendar.overlayOpacity
},
// 层级
zIndex: {
type: [String, Number],
default: () => defProps.calendar.zIndex
},
// 是否为iPhoneX留出底部安全距离
safeAreaInsetBottom: {
type: Boolean,
default: () => defProps.calendar.safeAreaInsetBottom
},
// 是否留出顶部安全距离(状态栏高度)
safeAreaInsetTop: {
type: Boolean,
default: () => defProps.calendar.safeAreaInsetTop
},
// 弹窗背景色,设置为transparent可去除白色背景
bgColor: {
type: String,
default: () => defProps.calendar.bgColor
},
// 是否允许点击遮罩关闭日历
closeOnClickOverlay: {
type: Boolean,
@@ -174,26 +132,6 @@ export const props = defineMixin({
allowSameDay: {
type: Boolean,
default: () => defProps.calendar.allowSameDay
},
// 区间模式下确认返回值格式,all-返回区间内所有日期,boundary-仅返回起止日期
rangeResultMode: {
type: String,
default: () => defProps.calendar.rangeResultMode
},
// 是否开启时分秒选择
enableTime: {
type: Boolean,
default: () => defProps.calendar.enableTime
},
// 时间精度:hour-仅时,minute-时分,second-时分秒
timePrecision: {
type: String,
default: () => defProps.calendar.timePrecision
},
// 默认时间,格式支持 HH / HH:mm / HH:mm:ss
defaultTime: {
type: String,
default: () => defProps.calendar.defaultTime
},
// 圆角值
round: {
@@ -205,21 +143,6 @@ export const props = defineMixin({
type: [Number, String],
default: 3
},
// 是否启用非滚动的单月切换模式
monthSwitch: {
type: Boolean,
default: () => defProps.calendar.monthSwitch
},
// 是否显示今天按钮
showToday: {
type: Boolean,
default: () => defProps.calendar.showToday
},
// 今天日期的独立高亮颜色,默认跟随主题色
todayColor: {
type: String,
default: () => defProps.calendar.todayColor
},
// 星期文案
weekText: {
type: Array,
@@ -233,14 +156,5 @@ export const props = defineMixin({
type: String,
default: defProps.calendar.forbidDaysToast
},
monthFormat:{
type: String,
default: defProps.calendar.monthFormat
},
// 是否页面内展示
pageInline:{
type: Boolean,
default: defProps.calendar.pageInline
}
}
})
@@ -2,18 +2,9 @@
<u-popup
:show="show"
mode="bottom"
:overlay="overlay"
:duration="duration"
:overlayStyle="overlayStyle"
:overlayOpacity="overlayOpacity"
:zIndex="zIndex"
:safeAreaInsetBottom="safeAreaInsetBottom"
:safeAreaInsetTop="safeAreaInsetTop"
:bgColor="bgColor"
:closeable="!pageInline"
closeable
@close="close"
:round="round"
:pageInline="pageInline"
:closeOnClickOverlay="closeOnClickOverlay"
>
<view class="u-calendar">
@@ -23,46 +14,10 @@
:showSubtitle="showSubtitle"
:showTitle="showTitle"
:weekText="weekText"
:showSwitch="monthSwitch"
:showToday="showToday"
:todayText="todayText"
:todayDisabled="todayDisabled"
:prevDisabled="switchPrevDisabled"
:nextDisabled="switchNextDisabled"
:prevYearDisabled="switchPrevYearDisabled"
:nextYearDisabled="switchNextYearDisabled"
@prev="prevMonth"
@next="nextMonth"
@prevYear="prevYear"
@nextYear="nextYear"
@today="jumpToToday"
></uHeader>
<view v-if="showTimePanel" class="u-calendar__time-panel">
<view v-if="mode === 'single'" class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ singleDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('single')">
<text class="u-calendar__time-text">{{ singleTime }}</text>
</view>
</view>
<view v-else-if="mode === 'range' && rangeResultMode === 'boundary'">
<view class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ rangeStartDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('start')">
<text class="u-calendar__time-text">{{ rangeStartTime }}</text>
</view>
</view>
<view class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ rangeEndDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('end')">
<text class="u-calendar__time-text">{{ rangeEndTime }}</text>
</view>
</view>
</view>
</view>
<scroll-view
v-if="!monthSwitch"
:style="{
height: addUnit(listHeight, 'px')
height: addUnit(listHeight)
}"
scroll-y
@scroll="onScroll"
@@ -89,48 +44,11 @@
:allowSameDay="allowSameDay"
:forbidDays="forbidDays"
:forbidDaysToast="forbidDaysToast"
:monthFormat="monthFormat"
:todayDate="todayDate"
:todayColor="todayColor"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="onUpdateMonthTop"
@updateMonthTop="updateMonthTop"
></uMonth>
</scroll-view>
<view
v-else
:style="{
height: addUnit(listHeight, 'px')
}"
>
<uMonth
:color="color"
:rowHeight="rowHeight"
:showMark="showMark"
:months="currentMonths"
:mode="mode"
:maxCount="maxCount"
:startText="startText"
:endText="endText"
:defaultDate="defaultDate"
:minDate="innerMinDate"
:maxDate="innerMaxDate"
:maxMonth="monthNum"
:readonly="readonly"
:maxRange="maxRange"
:rangePrompt="rangePrompt"
:showRangePrompt="showRangePrompt"
:allowSameDay="allowSameDay"
:forbidDays="forbidDays"
:forbidDaysToast="forbidDaysToast"
:monthFormat="monthFormat"
:todayDate="todayDate"
:todayColor="todayColor"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="onUpdateMonthTop"
></uMonth>
</view>
<slot name="footer" v-if="showConfirm">
<view class="u-calendar__confirm">
<u-button
@@ -146,32 +64,6 @@
</slot>
</view>
</u-popup>
<u-popup
:show="timePickerShow"
mode="center"
:round="8"
:closeOnClickOverlay="true"
@close="closeTimePicker"
>
<view class="u-calendar__time-picker">
<view class="u-calendar__time-picker__header">
<text class="u-calendar__time-picker__cancel" @click="closeTimePicker">取消</text>
<text class="u-calendar__time-picker__title">选择时间</text>
<text class="u-calendar__time-picker__confirm" @click="confirmTimePicker">确定</text>
</view>
<picker-view class="u-calendar__time-picker__body" :value="timePickerValue" @change="onTimePickerChange">
<picker-view-column>
<view v-for="(item, index) in hourOptions" :key="`h-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
<picker-view-column v-if="timePrecision !== 'hour'">
<view v-for="(item, index) in minuteOptions" :key="`m-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
<picker-view-column v-if="timePrecision === 'second'">
<view v-for="(item, index) in secondOptions" :key="`s-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
</template>
<script>
@@ -179,17 +71,16 @@ import uHeader from './header.vue'
import uMonth from './month.vue'
import { props } from './props.js'
import util from './util.js'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import dayjs from 'dayjs/esm/index'
import Calendar from '../../libs/util/calendar.js'
import { mpMixin } from '../../libs/mixin/mpMixin.js'
import { mixin } from '../../libs/mixin/mixin.js'
import { addUnit, getPx, range, error, padZero } from '../../libs/function/index';
import { addUnit, range, error, padZero } from '../../libs/function/index';
import test from '../../libs/function/test';
import { t } from '../../libs/i18n'
/**
* Calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
* @tutorial https://uview-plus.jiangruyi.com/components/calendar.html
* @tutorial https://ijry.github.io/uview-plus/components/calendar.html
*
* @property {String} title 标题内容 (默认 日期选择 )
* @property {Boolean} showTitle 是否显示标题 (默认 true )
@@ -210,28 +101,14 @@ import { t } from '../../libs/i18n'
* @property {String} confirmText 确定按钮的文字 (默认 '确定' )
* @property {String} confirmDisabledText 确认按钮处于禁用状态时的文字 (默认 '确定' )
* @property {Boolean} show 是否显示日历弹窗 (默认 false )
* @property {Boolean} overlay 是否显示遮罩 (默认 true )
* @property {String | Number} duration 动画时长,单位ms (默认 300 )
* @property {Object | String} overlayStyle 自定义遮罩的样式
* @property {Number | String} overlayOpacity 遮罩透明度,0-1之间 (默认 0.5 )
* @property {String | Number} zIndex 弹出层的z-index值 (默认 10075 )
* @property {Boolean} safeAreaInsetBottom 是否留出底部安全距离 (默认 true )
* @property {Boolean} safeAreaInsetTop 是否留出顶部安全距离(状态栏高度) (默认 false )
* @property {String} bgColor 弹窗背景色,设置为transparent可去除白色背景
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false )
* @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false )
* @property {String | Number} maxRange 日期区间最多可选天数,默认无限制,mode = range时有效
* @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案,mode = range时有效
* @property {Boolean} showRangePrompt 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true )
* @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false )
* @property {String} rangeResultMode 区间模式下确认返回值格式,all-返回区间内所有日期,boundary-仅返回起止日期 (默认 'all' )
* @property {Boolean} enableTime 是否开启时分秒选择 (默认 false )
* @property {String} timePrecision 时间精度,hour-仅时,minute-时分,second-时分秒 (默认 'minute' )
* @property {String} defaultTime 默认时间,支持HH/HH:mm/HH:mm:ss,不传则按00:00补齐
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
* @property {Number|String} monthNum 最多展示的月份数量 (默认 3 )
* @property {Boolean} monthSwitch 是否启用非滚动的单月切换模式 (默认 false )
* @property {String} todayColor 今天日期的独立高亮颜色,默认跟随主题色
* @property {Array} weekText 星期文案 (默认 ['一', '二', '三', '四', '五', '六', '日'] )
*
* @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数
@@ -259,15 +136,6 @@ export default {
scrollIntoView: '',
scrollIntoViewScroll: '',
scrollTop:0,
timePickerShow: false,
timePickerTarget: 'single',
timePickerValue: [0, 0, 0],
hourOptions: [],
minuteOptions: [],
secondOptions: [],
singleTime: '00:00',
rangeStartTime: '00:00',
rangeEndTime: '00:00',
// 过滤处理方法
innerFormatter: (value) => value
}
@@ -285,13 +153,6 @@ export default {
this.setMonth()
}
},
timePrecision() {
this.initTimeOptions()
this.initTimeValues()
},
defaultTime() {
this.initTimeValues()
},
// 打开弹窗时,设置月份数据
show: {
immediate: true,
@@ -318,28 +179,6 @@ export default {
? Number(this.minDate)
: this.minDate
},
todayDate() {
return dayjs().format('YYYY-MM-DD')
},
todayText() {
return t('up.calendar.today')
},
todayDisabled() {
const today = dayjs(this.todayDate)
const minDate = this.innerMinDate
? dayjs(this.innerMinDate)
: null
const maxDate = this.innerMaxDate
? dayjs(this.innerMaxDate)
: null
if (minDate && today.isBefore(minDate, 'day')) {
return true
}
if (maxDate && today.isAfter(maxDate, 'day')) {
return true
}
return false
},
// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
selectedChange() {
return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
@@ -347,53 +186,13 @@ export default {
subtitle() {
// 初始化时,this.months为空数组,所以需要特别判断处理
if (this.months.length) {
if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
return this.months[this.monthIndex].year + '年' + (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '月'
} else {
return (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '/' + this.months[this.monthIndex].year
}
return `${this.months[this.monthIndex].year}年${
this.months[this.monthIndex].month
}月`
} else {
return ''
}
},
currentMonths() {
if (this.monthSwitch && this.months.length) {
return [this.months[this.monthIndex]]
}
return this.months
},
switchPrevDisabled() {
return this.monthIndex <= 0
},
switchNextDisabled() {
return this.monthIndex >= this.months.length - 1
},
switchPrevYearDisabled() {
return this.monthIndex - 12 < 0
},
switchNextYearDisabled() {
return this.monthIndex + 12 > this.months.length - 1
},
showTimePanel() {
if (!this.enableTime) return false
if (this.mode === 'single') return true
if (this.mode === 'range' && this.rangeResultMode === 'boundary') {
return true
}
return false
},
singleDateLabel() {
return this.selected[0] || '--'
},
rangeStartDateLabel() {
return this.selected[0] || '--'
},
rangeEndDateLabel() {
if (this.selected.length >= 2) {
return this.selected[this.selected.length - 1]
}
return '--'
},
buttonDisabled() {
// 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态
if (this.mode === 'range') {
@@ -414,130 +213,6 @@ export default {
emits: ["confirm", "close"],
methods: {
addUnit,
padTime(num) {
return String(num).padStart(2, '0')
},
initTimeOptions() {
this.hourOptions = Array.from({ length: 24 }, (_, i) => this.padTime(i))
this.minuteOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i))
this.secondOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i))
},
parseTimeValue(value = '') {
const raw = String(value || '').trim()
const parts = raw.length ? raw.split(':') : []
const getNumber = (index, max) => {
const current = Number(parts[index] || 0)
if (Number.isNaN(current)) return 0
return Math.max(0, Math.min(max, current))
}
const hour = getNumber(0, 23)
const minute = getNumber(1, 59)
const second = getNumber(2, 59)
return [hour, minute, second]
},
getDefaultTimeValue() {
const [hour, minute, second] = this.parseTimeValue(this.defaultTime)
if (this.timePrecision === 'hour') {
return this.padTime(hour)
}
if (this.timePrecision === 'second') {
return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}`
}
return `${this.padTime(hour)}:${this.padTime(minute)}`
},
initTimeValues() {
const value = this.getDefaultTimeValue()
this.singleTime = value
this.rangeStartTime = value
this.rangeEndTime = value
},
timeToPickerValue(value = '') {
const [hour, minute, second] = this.parseTimeValue(value)
return [hour, minute, second]
},
pickerValueToTime(value = []) {
const hour = Number(value[0] || 0)
const minute = Number(value[1] || 0)
const second = Number(value[2] || 0)
if (this.timePrecision === 'hour') {
return this.padTime(hour)
}
if (this.timePrecision === 'second') {
return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}`
}
return `${this.padTime(hour)}:${this.padTime(minute)}`
},
openTimePicker(target) {
this.timePickerTarget = target
let currentValue = this.singleTime
if (target === 'start') currentValue = this.rangeStartTime
if (target === 'end') currentValue = this.rangeEndTime
this.timePickerValue = this.timeToPickerValue(currentValue)
this.timePickerShow = true
},
onTimePickerChange(e) {
this.timePickerValue = e.detail.value
},
closeTimePicker() {
this.timePickerShow = false
},
confirmTimePicker() {
const value = this.pickerValueToTime(this.timePickerValue)
if (this.timePickerTarget === 'single') this.singleTime = value
if (this.timePickerTarget === 'start') this.rangeStartTime = value
if (this.timePickerTarget === 'end') this.rangeEndTime = value
this.timePickerShow = false
},
timeToSecond(timeText = '') {
const [hour, minute, second] = this.parseTimeValue(timeText)
return hour * 3600 + minute * 60 + second
},
validateSameDayRangeTime() {
if (!this.enableTime || this.mode !== 'range' || this.rangeResultMode !== 'boundary') {
return true
}
if (this.selected.length < 2) return true
const startDate = this.selected[0]
const endDate = this.selected[this.selected.length - 1]
if (startDate !== endDate) return true
const startSeconds = this.timeToSecond(this.rangeStartTime)
const endSeconds = this.timeToSecond(this.rangeEndTime)
if (endSeconds < startSeconds) {
uni.showToast({
title: '结束时间不能早于开始时间',
icon: 'none'
})
return false
}
return true
},
appendTime(dateText, timeText) {
return `${dateText} ${timeText}`
},
getConfirmValue(selected = this.selected) {
let result = selected
if (
this.mode === 'range' &&
this.rangeResultMode === 'boundary' &&
selected.length >= 2
) {
const len = selected.length - 1
result = [selected[0], selected[len]]
}
if (!this.showTimePanel || !this.enableTime) {
return result
}
if (this.mode === 'single' && result.length >= 1) {
return [this.appendTime(result[0], this.singleTime)]
}
if (this.mode === 'range' && this.rangeResultMode === 'boundary' && result.length >= 2) {
return [
this.appendTime(result[0], this.rangeStartTime),
this.appendTime(result[1], this.rangeEndTime)
]
}
return result
},
// 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
setFormatter(e) {
this.innerFormatter = e
@@ -556,8 +231,7 @@ export default {
return
}
if( scene === 'tap') {
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
this.$emit('confirm', this.selected)
}
}
}
@@ -572,15 +246,7 @@ export default {
return error('maxDate不能小于minDate时间')
}
// 滚动区域的高度
let bottomPadding = 0;
if (this.pageInline) {
bottomPadding = 0
} else {
bottomPadding = 30
}
this.listHeight = this.rowHeight * (this.monthSwitch ? 6 : 5) + bottomPadding
this.initTimeOptions()
this.initTimeValues()
this.listHeight = this.rowHeight * 5 + 30
this.setMonth()
},
close() {
@@ -589,8 +255,7 @@ export default {
// 点击确定按钮
confirm() {
if (!this.buttonDisabled) {
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
this.$emit('confirm', this.selected)
}
},
// 获得两个日期之间的月份数
@@ -675,67 +340,6 @@ export default {
year: dayjs(minDate).add(i, 'month').year()
})
}
if (this.monthSwitch) {
this.monthIndex = this.getDefaultMonthIndex()
}
},
getDefaultMonthIndex() {
let selected = dayjs().format('YYYY-MM')
if (this.defaultDate) {
if (!test.array(this.defaultDate)) {
selected = dayjs(this.defaultDate).format('YYYY-MM')
} else if (this.defaultDate.length) {
selected = dayjs(this.defaultDate[0]).format('YYYY-MM')
}
}
const index = this.months.findIndex(({ year, month }) => {
return `${year}-${padZero(month)}` === selected
})
return index === -1 ? 0 : index
},
prevMonth() {
if (!this.switchPrevDisabled) {
this.monthIndex -= 1
}
},
nextMonth() {
if (!this.switchNextDisabled) {
this.monthIndex += 1
}
},
prevYear() {
if (!this.switchPrevYearDisabled) {
this.monthIndex -= 12
}
},
nextYear() {
if (!this.switchNextYearDisabled) {
this.monthIndex += 12
}
},
jumpToToday() {
if (this.todayDisabled) {
return
}
const targetMonth = dayjs(this.todayDate).format('YYYY-MM')
const selectToday = () => {
if (this.mode === 'range') {
return
}
this.$refs.month && this.$refs.month.selectDate(this.todayDate)
}
if (this.monthSwitch) {
const todayMonthIndex = this.months.findIndex(({ year, month }) => {
return `${year}-${padZero(month)}` === targetMonth
})
if (todayMonthIndex !== -1) {
this.monthIndex = todayMonthIndex
this.$nextTick(selectToday)
}
return
}
this.scrollIntoDefaultMonth(targetMonth)
this.$nextTick(selectToday)
},
// 滚动到默认设置的月份
scrollIntoDefaultMonth(selected) {
@@ -750,7 +354,6 @@ export default {
if (_index !== -1) {
// #ifndef MP-WEIXIN
this.$nextTick(() => {
this.scrollIntoView = ''
this.scrollIntoView = `month-${_index}`
this.scrollIntoViewScroll = this.scrollIntoView
})
@@ -773,12 +376,6 @@ export default {
}
},
// 更新月份的top值
onUpdateMonthTop(topArr = []) {
if (this.monthSwitch) {
return
}
this.updateMonthTop(topArr)
},
updateMonthTop(topArr = []) {
// 设置对应月份的top值,用于onScroll方法更新月份
topArr.map((item, index) => {
@@ -806,88 +403,9 @@ export default {
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-calendar {
color: var(--up-main-color, $u-main-color);
background-color: var(--up-card-bg-color, #ffffff);
&__time-panel {
padding: 8px 16px 0;
}
&__time-row {
height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
&__time-date {
font-size: 13px;
color: #303133;
}
&__time-trigger {
min-width: 92px;
height: 30px;
padding: 0 10px;
border-radius: 15px;
border: 1px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
&__time-text {
font-size: 14px;
color: #303133;
}
&__time-picker {
width: 320px;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
}
&__time-picker__header {
height: 44px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
&__time-picker__cancel {
font-size: 14px;
color: #909399;
}
&__time-picker__title {
font-size: 15px;
color: #303133;
font-weight: 600;
}
&__time-picker__confirm {
font-size: 14px;
color: #3c9cff;
}
&__time-picker__body {
width: 320px;
height: 200px;
}
&__time-picker__item {
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
color: #303133;
}
&__confirm {
padding: 7px 18px;
}
@@ -1,4 +1,4 @@
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import dayjs from 'dayjs/esm/index'
export default {
methods: {
// 设置月份数据
@@ -1,734 +0,0 @@
<template>
<view
class="u-canvas"
:id="rootId"
:style="{
width: useRootHeightAndWidth ? '100%' : actualWidth + unit,
height: useRootHeightAndWidth ? '100%' : actualHeight + unit
}"
>
<!-- #ifdef MP || H5 -->
<canvas
class="u-canvas__canvas"
:id="canvasId"
:canvas-id="canvasId"
type="2d"
:disable-scroll="disableScroll"
:style="{ width: actualWidth + unit, height: actualHeight + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
/>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<canvas
class="u-canvas__canvas"
:id="canvasId"
:canvas-id="canvasId"
:disable-scroll="disableScroll"
:style="{ width: actualWidth + unit, height: actualHeight + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
/>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<gcanvas
class="u-canvas__canvas"
ref="gcanvas"
:style="{ width: actualWidth + unit, height: actualHeight + unit }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
/>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef APP-NVUE
import {
enable,
WeexBridge,
Image as GImage
} from '../../libs/util/gcanvas/index.js';
// #endif
export default {
name: 'u-canvas',
emits: ['ready', 'touchstart', 'touchmove', 'touchend'],
props: {
canvasId: {
type: String,
default: () => `u-canvas${Math.floor(Math.random() * 1000000)}`
},
width: {
type: [String, Number],
default: 300
},
height: {
type: [String, Number],
default: 300
},
unit: {
type: String,
default: 'px'
},
useRootHeightAndWidth: {
type: Boolean,
default: false
},
bgColor: {
type: String,
default: '#ffffff'
},
disableScroll: {
type: Boolean,
default: false
}
},
data() {
return {
rootId: `u-canvas-root-${Math.floor(Math.random() * 1000000)}`,
ctx: null,
widthLocal: this.parseSize(this.width),
heightLocal: this.parseSize(this.height),
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'normal',
dpr: 1
};
},
computed: {
actualWidth() {
return this.useRootHeightAndWidth ? this.widthLocal : this.parseSize(this.width);
},
actualHeight() {
return this.useRootHeightAndWidth ? this.heightLocal : this.parseSize(this.height);
}
},
watch: {
width() {
this.refresh();
},
height() {
this.refresh();
},
bgColor() {
this.clearCanvas();
}
},
created() {
this._canvasNode = null;
this._selectorResult = null;
this._canvasElement = null;
this._imageCache = Object.create(null);
this._isNvue = false;
},
mounted() {
this.$nextTick(() => {
this.initCanvas();
});
},
methods: {
parseSize(value) {
if (typeof value === 'number') {
return value;
}
if (typeof value !== 'string') {
return 0;
}
if (value.endsWith('rpx') || value.endsWith('upx')) {
return uni.upx2px(parseFloat(value));
}
if (value.endsWith('px')) {
return parseFloat(value) || 0;
}
return parseFloat(value) || 0;
},
onTouchStart(event) {
this.$emit('touchstart', event);
},
onTouchMove(event) {
this.$emit('touchmove', event);
},
onTouchEnd(event) {
this.$emit('touchend', event);
},
async getCanvasNode(id = this.canvasId, isCanvas = true) {
return new Promise((resolve) => {
try {
// #ifdef APP-NVUE
setTimeout(() => {
const gcanvas = this.$refs.gcanvas;
if (!gcanvas) {
resolve(false);
return;
}
this._isNvue = true;
resolve(enable(gcanvas, { bridge: WeexBridge }));
}, 100);
// #endif
// #ifndef APP-NVUE
uni.createSelectorQuery()
.in(this)
.select(`#${id}`)
.fields(
{
node: isCanvas,
size: true
},
(res) => {
resolve(res || false);
}
)
.exec();
// #endif
} catch (error) {
console.error('获取画布节点失败:', error);
resolve(false);
}
});
},
getCanvasElement() {
return this._canvasElement || null;
},
getRawContext() {
return this.ctx;
},
getCanvasContext() {
// #ifdef APP-PLUS
return uni.createCanvasContext(this.canvasId, this);
// #endif
// #ifdef APP-NVUE
return this._canvasElement && typeof this._canvasElement.getContext === 'function'
? this._canvasElement.getContext('2d')
: null;
// #endif
// #ifdef MP || H5
return this._canvasElement && typeof this._canvasElement.getContext === 'function'
? this._canvasElement.getContext('2d')
: null;
// #endif
},
async setNewSize() {
const rootNode = await this.getCanvasNode(this.rootId, false);
if (!rootNode) {
return;
}
if (rootNode.width) {
this.widthLocal = rootNode.width;
}
if (rootNode.height) {
this.heightLocal = rootNode.height;
}
},
async initCanvas(force = false) {
try {
if (this.useRootHeightAndWidth) {
await this.setNewSize();
}
if (this.ctx && !force) {
this.$emit('ready', {
width: this.actualWidth,
height: this.actualHeight
});
return true;
}
this._canvasNode = await this.getCanvasNode(this.canvasId);
if (!this._canvasNode) {
return false;
}
this._selectorResult = this._canvasNode;
this._canvasElement = this._canvasNode.node || this._canvasNode;
this.dpr = uni.getSystemInfoSync().pixelRatio || 1;
// #ifdef MP || H5
if (this._canvasElement) {
this._canvasElement.width = Math.ceil(this.actualWidth * this.dpr);
this._canvasElement.height = Math.ceil(this.actualHeight * this.dpr);
}
// #endif
this.ctx = this.getCanvasContext();
if (!this.ctx) {
return false;
}
// #ifdef MP || H5
if (typeof this.ctx.setTransform === 'function') {
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
} else if (typeof this.ctx.scale === 'function') {
this.ctx.scale(this.dpr, this.dpr);
}
// #endif
this.applyFont();
this.clearCanvas();
this.$emit('ready', {
width: this.actualWidth,
height: this.actualHeight
});
return true;
} catch (error) {
console.error('初始化Canvas失败:', error);
return false;
}
},
refresh() {
return this.initCanvas(true);
},
getWidth() {
return this.actualWidth;
},
getHeight() {
return this.actualHeight;
},
clearCanvas() {
if (!this.ctx) return;
this.clearRect(0, 0, this.actualWidth, this.actualHeight);
if (this.bgColor && this.bgColor !== 'transparent') {
this.beginPath();
this.rect(0, 0, this.actualWidth, this.actualHeight);
this.setFillStyle(this.bgColor);
this.fill();
}
this.draw();
},
callContext(method, ...args) {
if (this.ctx && typeof this.ctx[method] === 'function') {
return this.ctx[method](...args);
}
return undefined;
},
rect(x, y, width, height) {
return this.callContext('rect', x, y, width, height);
},
clearRect(x, y, width, height) {
return this.callContext('clearRect', x, y, width, height);
},
fillRect(x, y, width, height) {
return this.callContext('fillRect', x, y, width, height);
},
strokeRect(x, y, width, height) {
return this.callContext('strokeRect', x, y, width, height);
},
fill() {
return this.callContext('fill');
},
stroke() {
return this.callContext('stroke');
},
beginPath() {
return this.callContext('beginPath');
},
closePath() {
return this.callContext('closePath');
},
moveTo(x, y) {
return this.callContext('moveTo', x, y);
},
lineTo(x, y) {
return this.callContext('lineTo', x, y);
},
arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
return this.callContext('arc', x, y, radius, startAngle, endAngle, anticlockwise);
},
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
return this.callContext('bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y);
},
quadraticCurveTo(cpx, cpy, x, y) {
return this.callContext('quadraticCurveTo', cpx, cpy, x, y);
},
clip() {
return this.callContext('clip');
},
save() {
return this.callContext('save');
},
restore() {
return this.callContext('restore');
},
translate(x, y) {
return this.callContext('translate', x, y);
},
rotate(angle) {
return this.callContext('rotate', angle);
},
scale(x, y) {
return this.callContext('scale', x, y);
},
setFillStyle(color) {
if (!this.ctx) return;
if (typeof this.ctx.setFillStyle === 'function') {
this.ctx.setFillStyle(color);
} else {
this.ctx.fillStyle = color;
}
},
setStrokeStyle(color) {
if (!this.ctx) return;
if (typeof this.ctx.setStrokeStyle === 'function') {
this.ctx.setStrokeStyle(color);
} else {
this.ctx.strokeStyle = color;
}
},
setLineWidth(width) {
if (!this.ctx) return;
if (typeof this.ctx.setLineWidth === 'function') {
this.ctx.setLineWidth(width);
} else {
this.ctx.lineWidth = width;
}
},
setLineCap(lineCap = 'round') {
if (!this.ctx) return;
if (typeof this.ctx.setLineCap === 'function') {
this.ctx.setLineCap(lineCap);
} else {
this.ctx.lineCap = lineCap;
}
},
setLineJoin(lineJoin = 'round') {
if (!this.ctx) return;
if (typeof this.ctx.setLineJoin === 'function') {
this.ctx.setLineJoin(lineJoin);
} else {
this.ctx.lineJoin = lineJoin;
}
},
setTextAlign(align = 'left') {
if (!this.ctx) return;
if (typeof this.ctx.setTextAlign === 'function') {
this.ctx.setTextAlign(align);
} else {
this.ctx.textAlign = align;
}
},
setTextBaseline(baseline = 'alphabetic') {
if (!this.ctx) return;
if (typeof this.ctx.setTextBaseline === 'function') {
this.ctx.setTextBaseline(baseline);
} else {
this.ctx.textBaseline = baseline;
}
},
setFontSize(fontSize) {
this.fontSize = Number(fontSize) || this.fontSize;
if (this.ctx && typeof this.ctx.setFontSize === 'function') {
this.ctx.setFontSize(this.fontSize);
}
this.applyFont();
},
setFont(font) {
if (!this.ctx) return;
if ('font' in this.ctx) {
this.ctx.font = font;
return;
}
const matched = String(font).match(/(\d+(?:\.\d+)?)px/);
if (matched) {
this.fontSize = Number(matched[1]);
}
this.applyFont();
},
setGlobalAlpha(alpha) {
if (!this.ctx) return;
if (typeof this.ctx.setGlobalAlpha === 'function') {
this.ctx.setGlobalAlpha(alpha);
} else {
this.ctx.globalAlpha = alpha;
}
},
setShadow(offsetX = 0, offsetY = 0, blur = 0, color = 'rgba(0,0,0,0)') {
if (!this.ctx) return;
if (typeof this.ctx.setShadow === 'function') {
this.ctx.setShadow(offsetX, offsetY, blur, color);
return;
}
this.ctx.shadowOffsetX = offsetX;
this.ctx.shadowOffsetY = offsetY;
this.ctx.shadowBlur = blur;
this.ctx.shadowColor = color;
},
setLineStyle(lineColor, lineWidth) {
this.setLineCap('round');
this.setLineJoin('round');
this.setStrokeStyle(lineColor);
this.setLineWidth(lineWidth);
},
applyFont() {
if (!this.ctx) return;
const font = `${this.fontWeight === 'normal' ? '' : `${this.fontWeight} `}${this.fontSize}px ${this.fontFamily}`.trim();
if ('font' in this.ctx) {
this.ctx.font = font;
} else if (typeof this.ctx.setFont === 'function') {
this.ctx.setFont(font);
} else if (typeof this.ctx.setFontSize === 'function') {
this.ctx.setFontSize(this.fontSize);
}
},
fillText(text, x, y) {
return this.callContext('fillText', String(text), x, y);
},
measureText(text) {
if (this.ctx && typeof this.ctx.measureText === 'function') {
return this.ctx.measureText(String(text));
}
return {
width: String(text).length * this.fontSize * 0.6
};
},
createLinearGradient(x0, y0, x1, y1) {
if (this.ctx && typeof this.ctx.createLinearGradient === 'function') {
return this.ctx.createLinearGradient(x0, y0, x1, y1);
}
return null;
},
createRadialGradient(x0, y0, r0, x1, y1, r1) {
if (this.ctx && typeof this.ctx.createRadialGradient === 'function') {
return this.ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
}
return null;
},
loadImage(src) {
if (this._imageCache[src]) {
return Promise.resolve(this._imageCache[src]);
}
return new Promise((resolve, reject) => {
let image = null;
// #ifdef APP-NVUE
image = new GImage();
// #endif
// #ifdef MP
const canvas = this.getCanvasElement();
if (canvas && typeof canvas.createImage === 'function') {
image = canvas.createImage();
}
// #endif
// #ifdef H5
image = new Image();
image.crossOrigin = 'anonymous';
// #endif
if (!image) {
resolve(src);
return;
}
image.onload = () => {
this._imageCache[src] = image;
resolve(image);
};
image.onerror = reject;
image.src = src;
});
},
async drawImage(source, ...args) {
if (!this.ctx || typeof this.ctx.drawImage !== 'function') {
return false;
}
if (typeof source !== 'string' || (typeof this.ctx.setFillStyle === 'function' && !this._isNvue)) {
this.ctx.drawImage(source, ...args);
return true;
}
const image = await this.loadImage(source);
this.ctx.drawImage(image, ...args);
return true;
},
draw(isLastDraw = false, callback) {
if (this.ctx && typeof this.ctx.draw === 'function') {
return this.ctx.draw(isLastDraw, callback);
}
if (typeof callback === 'function') {
setTimeout(callback, 0);
}
return undefined;
},
toTempFilePath(options = {}) {
return new Promise((resolve, reject) => {
const width = options.width || this.actualWidth;
const height = options.height || this.actualHeight;
const request = {
x: options.x || 0,
y: options.y || 0,
width,
height,
destWidth: options.destWidth || width,
destHeight: options.destHeight || height,
fileType: options.fileType || 'png',
quality: options.quality === undefined ? 1 : options.quality
};
const success = (res) => {
if (typeof options.success === 'function') options.success(res);
resolve(res);
};
const fail = (err) => {
if (typeof options.fail === 'function') options.fail(err);
reject(err);
};
const complete = (res) => {
if (typeof options.complete === 'function') options.complete(res);
};
// #ifdef H5
const canvas = this.getCanvasElement() || (this.ctx && this.ctx.canvas);
if (canvas && typeof canvas.toDataURL === 'function') {
try {
let exportCanvas = canvas;
if (
request.x !== 0 ||
request.y !== 0 ||
request.width !== this.actualWidth ||
request.height !== this.actualHeight ||
request.destWidth !== request.width ||
request.destHeight !== request.height
) {
exportCanvas = document.createElement('canvas');
exportCanvas.width = request.destWidth;
exportCanvas.height = request.destHeight;
const exportCtx = exportCanvas.getContext('2d');
exportCtx.drawImage(
canvas,
request.x * this.dpr,
request.y * this.dpr,
request.width * this.dpr,
request.height * this.dpr,
0,
0,
request.destWidth,
request.destHeight
);
}
const mime = request.fileType === 'jpg' || request.fileType === 'jpeg'
? 'image/jpeg'
: 'image/png';
const res = {
tempFilePath: exportCanvas.toDataURL(mime, request.quality)
};
success(res);
complete(res);
return;
} catch (error) {
fail(error);
complete(error);
return;
}
}
// #endif
// #ifdef APP-NVUE
if (this.ctx && typeof this.ctx.toTempFilePath === 'function') {
this.ctx.toTempFilePath(
request.x,
request.y,
request.width,
request.height,
request.destWidth,
request.destHeight,
request.fileType,
request.quality,
(res) => {
success(res);
complete(res);
}
);
return;
}
// #endif
const canvasNode = this.getCanvasElement();
const uniOptions = {
...request,
canvasId: this.canvasId,
success,
fail,
complete
};
if (canvasNode) {
uniOptions.canvas = canvasNode;
}
uni.canvasToTempFilePath(uniOptions, this);
});
},
async exportImage(fileType = 'png', quality = 1) {
const res = await this.toTempFilePath({
fileType,
quality,
width: this.actualWidth,
height: this.actualHeight,
destWidth: this.actualWidth,
destHeight: this.actualHeight
});
return res.tempFilePath || res.apFilePath;
},
getImageData(options = {}) {
return new Promise((resolve, reject) => {
const request = {
canvasId: this.canvasId,
x: options.x || 0,
y: options.y || 0,
width: options.width || this.actualWidth,
height: options.height || this.actualHeight,
success: (res) => {
if (typeof options.success === 'function') options.success(res);
resolve(res);
},
fail: (err) => {
if (typeof options.fail === 'function') options.fail(err);
reject(err);
},
complete: options.complete
};
uni.canvasGetImageData(request, this);
});
},
putImageData(options = {}) {
return new Promise((resolve, reject) => {
const request = {
canvasId: this.canvasId,
x: options.x || 0,
y: options.y || 0,
width: options.width || this.actualWidth,
height: options.height || this.actualHeight,
data: options.data,
success: (res) => {
if (typeof options.success === 'function') options.success(res);
resolve(res);
},
fail: (err) => {
if (typeof options.fail === 'function') options.fail(err);
reject(err);
},
complete: options.complete
};
uni.canvasPutImageData(request, this);
});
}
}
};
</script>
<style lang="scss" scoped>
.u-canvas {
position: relative;
overflow: hidden;
}
.u-canvas__canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/carKeyboard.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:53:20
* @FilePath : /u-view2.0/uview-ui/libs/config/props/carKeyboard.js
*/
export default {
// 车牌号键盘
@@ -1,8 +1,6 @@
import { defineMixin } from '../../libs/vue'
import CarKeyboardDefaultProps from './carKeyboard'
import { registerComponentProps } from '../../libs/config/props.js'
import defProps from '../../libs/config/props.js'
const defProps = registerComponentProps(CarKeyboardDefaultProps)
export const props = defineMixin({
props: {
// 是否打乱键盘按键的顺序
@@ -56,11 +56,11 @@
hover-class="u-hover-class"
:hover-stay-time="200"
>
<up-icon
<u-icon
size="28"
name="backspace"
:color="upThemeVar('--up-main-color', '#303133')"
></up-icon>
color="#303133"
></u-icon>
</view>
</view>
</view>
@@ -222,25 +222,26 @@
</script>
<style lang="scss" scoped>
$u-car-keyboard-background-color: var(--up-bg-color, rgb(224, 228, 230)) !default;
@import "../../libs/css/components.scss";
$u-car-keyboard-background-color: rgb(224, 228, 230) !default;
$u-car-keyboard-padding:6px 0 6px !default;
$u-car-keyboard-button-inner-width:64rpx !default;
$u-car-keyboard-button-inner-background-color:var(--up-card-bg-color, #ffffff) !default;
$u-car-keyboard-button-inner-background-color:#FFFFFF !default;
$u-car-keyboard-button-height:80rpx !default;
$u-car-keyboard-button-inner-box-shadow:0 1px 0px #999992 !default;
$u-car-keyboard-button-border-radius:4px !default;
$u-car-keyboard-button-inner-margin:8rpx 5rpx !default;
$u-car-keyboard-button-text-font-size:16px !default;
$u-car-keyboard-button-text-color:var(--up-main-color, #303133) !default;
$u-car-keyboard-button-text-color:$u-main-color !default;
$u-car-keyboard-center-inner-margin: 0 4rpx !default;
$u-car-keyboard-special-button-width:134rpx !default;
$u-car-keyboard-lang-font-size:16px !default;
$u-car-keyboard-lang-color:var(--up-main-color, #303133) !default;
$u-car-keyboard-lang-color:$u-main-color !default;
$u-car-keyboard-active-color:$u-primary !default;
$u-car-keyboard-line-font-size:15px !default;
$u-car-keyboard-line-color:var(--up-main-color, #303133) !default;
$u-car-keyboard-line-color:$u-main-color !default;
$u-car-keyboard-line-margin:0 1px !default;
$u-car-keyboard-u-hover-class-background-color:var(--up-border-color, #BBBCC6) !default;
$u-car-keyboard-u-hover-class-background-color:#BBBCC6 !default;
.u-keyboard {
@include flex(column);
@@ -1,40 +0,0 @@
/*
* @Author : jry
* @Description :
* @version : 3.0
* @Date : 2025-04-26 16:37:21
* @LastAuthor : jry
* @lastTime : 2025-04-26 16:37:21
* @FilePath : /uview-plus/libs/config/props/card.js
*/
export default {
// card组件的props
card: {
full: false,
title: '',
titleColor: '#303133',
titleSize: '15px',
subTitle: '',
subTitleColor: '#909399',
subTitleSize: '13px',
border: true,
index: '',
margin: '15px',
borderRadius: '8px',
headStyle: {},
bodyStyle: {},
footStyle: {},
headBorderBottom: true,
footBorderTop: true,
thumb: '',
thumbWidth: '30px',
thumbCircle: false,
padding: '15px',
paddingHead: '',
paddingBody: '',
paddingFoot: '',
showHead: true,
showFoot: true,
boxShadow: 'none'
}
}
@@ -1,136 +1,140 @@
import { defineMixin } from '../../libs/vue'
import CardDefaultProps from './card'
import { registerComponentProps } from '../../libs/config/props.js'
import defProps from '../../libs/config/props.js'
const defProps = registerComponentProps(CardDefaultProps)
export const propsCard = defineMixin({
props: {
// 与屏幕两侧是否留空隙
full: {
type: Boolean,
default: () => defProps.card.full
default: false
},
// 标题
title: {
type: String,
default: () => defProps.card.title
default: ''
},
// 标题颜色
titleColor: {
type: String,
default: () => defProps.card.titleColor
default: '#303133'
},
// 标题字体大小
titleSize: {
type: [Number, String],
default: () => defProps.card.titleSize
default: '15px'
},
// 副标题
subTitle: {
type: String,
default: () => defProps.card.subTitle
default: ''
},
// 副标题颜色
subTitleColor: {
type: String,
default: () => defProps.card.subTitleColor
default: '#909399'
},
// 副标题字体大小
subTitleSize: {
type: [Number, String],
default: () => defProps.card.subTitleSize
default: '13'
},
// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
border: {
type: Boolean,
default: () => defProps.card.border
default: true
},
// 用于标识点击了第几个
index: {
type: [Number, String, Object],
default: () => defProps.card.index
default: ''
},
// 用于隔开上下左右的边距,带单位的写法,如:"30px 30px","20px 20px 30px 30px"
margin: {
type: String,
default: () => defProps.card.margin
default: '15px'
},
// card卡片的圆角
borderRadius: {
type: [Number, String],
default: () => defProps.card.borderRadius
default: '8px'
},
// 头部自定义样式,对象形式
headStyle: {
type: Object,
default: () => defProps.card.headStyle
default() {
return {};
}
},
// 主体自定义样式,对象形式
bodyStyle: {
type: Object,
default: () => defProps.card.bodyStyle
default() {
return {};
}
},
// 底部自定义样式,对象形式
footStyle: {
type: Object,
default: () => defProps.card.footStyle
default() {
return {};
}
},
// 头部是否下边框
headBorderBottom: {
type: Boolean,
default: () => defProps.card.headBorderBottom
default: true
},
// 底部是否有上边框
footBorderTop: {
type: Boolean,
default: () => defProps.card.footBorderTop
default: true
},
// 标题左边的缩略图
thumb: {
type: String,
default: () => defProps.card.thumb
default: ''
},
// 缩略图宽高
thumbWidth: {
type: [String, Number],
default: () => defProps.card.thumbWidth
default: '30px'
},
// 缩略图是否为圆形
thumbCircle: {
type: Boolean,
default: () => defProps.card.thumbCircle
default: false
},
// 给head,body,foot的内边距
padding: {
type: [String, Number],
default: () => defProps.card.padding
default: '15px'
},
paddingHead: {
type: [String, Number],
default: () => defProps.card.paddingHead
default: ''
},
paddingBody: {
type: [String, Number],
default: () => defProps.card.paddingBody
default: ''
},
paddingFoot: {
type: [String, Number],
default: () => defProps.card.paddingFoot
default: ''
},
// 是否显示头部
showHead: {
type: Boolean,
default: () => defProps.card.showHead
default: true
},
// 是否显示尾部
showFoot: {
type: Boolean,
default: () => defProps.card.showFoot
default: true
},
// 卡片外围阴影,字符串形式
boxShadow: {
type: String,
default: () => defProps.card.boxShadow
default: 'none'
}
}
})
@@ -136,11 +136,13 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-card {
position: relative;
overflow: hidden;
font-size: 28rpx;
background-color: var(--up-card-bg-color, #ffffff);
background-color: #ffffff;
box-sizing: border-box;
&-full {
@@ -1,371 +0,0 @@
<template>
<up-popup :show="popupShow" mode="bottom" :popup="false"
:mask="true" :closeable="closeable" :safe-area-inset-bottom="true"
:close-icon-color="closeIconColor" :z-index="uZIndex"
:maskCloseAble="maskCloseAble" @close="close">
<view class="up-p-t-30 up-p-l-20 up-m-b-10" v-if="headerDirection ==='column'">
<up-steps v-if="popupShow" dot direction="column" v-model:current="tabsIndex">
<view v-for="(item, index) in genTabsList" @click="toFatherIndex(index)">
<up-steps-item
:title="item.name"/>
</view>
</up-steps>
</view>
<view class="up-p-t-20 up-m-b-10" v-else>
<up-tabs v-if="popupShow" :list="genTabsList"
:scrollable="true" v-model:current="tabsIndex" @change="tabsChange" ref="tabs"></up-tabs>
</view>
<view class="area-box">
<view class="u-flex" :class="{ 'change':isChange }"
:style="{transform: optionsCols == 2 && isChange ? 'translateX(-33.3333333%)' : ''}">
<template v-for="(levelData, levelIndex) in levelList" :key="levelIndex">
<view v-if="optionsCols == 2 || levelIndex == tabsIndex" class="area-item"
:style="{ width: optionsCols == 2 ? '33.33333%' : '750rpx'}">
<view class="u-padding-10" :style="[levelPaneStyle, { height: '100%' }]">
<scroll-view :scroll-y="true" style="height: 100%">
<up-cell-group v-if="levelIndex === 0 || selectedValueIndexs[levelIndex - 1] !== undefined">
<up-cell v-for="(item,index) in levelData"
:title="item[labelKey]" :arrow="false"
:index="index" :key="index"
@click="levelChange(levelIndex, index)">
<template v-slot:right-icon>
<up-icon v-if="selectedValueIndexs[levelIndex] === index"
size="17" name="checkbox-mark"></up-icon>
</template>
</up-cell>
</up-cell-group>
</scroll-view>
</view>
</view>
</template>
</view>
</view>
<!-- 添加按钮区域 -->
<view class="u-cascader-action up-flex up-flex-between">
<view class="u-padding-20 up-flex-fill">
<up-button @click="handleCancel" type="default">{{ t("up.common.cancel") }}</up-button>
</view>
<view class="u-padding-20 up-flex-fill">
<up-button @click="handleConfirm" type="primary">{{ t("up.common.confirm") }}</up-button>
</view>
</view>
</up-popup>
</template>
<script>
/**
* u-cascader 通用无限级联选择器
* @property {String Number} z-index 弹出时的z-index值(默认1075)
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
* @property {Array} data 级联数据
* @property {Array} default-value 默认选中的值
* @property {String} valueKey 指定选项的值为选项对象中的哪个属性值
* @property {String} labelKey 指定选项标签为选项对象中的哪个属性值
* @property {String} childrenKey 指定选项的子选项为选项对象中的哪个属性值
* @property {Boolean} autoClose 是否在选择最后一级时自动关闭并触发confirm(默认false)
* @property {Boolean} closeable 是否显示关闭图标(默认true)
*/
import { t } from '../../libs/i18n'
export default {
name: 'up-cascader',
props: {
// 通过双向绑定控制组件的弹出与收起
show: {
type: Boolean,
default: false
},
// 级联数据
data: {
type: Array,
default() {
return [];
}
},
// 默认选中的值
modelValue: {
type: Array,
default() {
return [];
}
},
// 指定选项的值为选项对象中的哪个属性值
valueKey: {
type: String,
default: 'value'
},
// 指定选项标签为选项对象中的哪个属性值
labelKey: {
type: String,
default: 'label'
},
// 指定选项的子选项为选项对象中的哪个属性值
childrenKey: {
type: String,
default: 'children'
},
// 是否允许通过点击遮罩关闭Picker
maskCloseAble: {
type: Boolean,
default: true
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 是否在选择最后一级时自动关闭并触发confirm
autoClose: {
type: Boolean,
default: false
},
// 选中项目的展示方向direction垂直方向适合文字长度过长
headerDirection: {
type: String,
default: 'row'
},
// 选项区域列数,支持1列和2列,默认为2列
optionsCols: {
type: [Number],
default: 2
},
// 是否显示关闭图标
closeable: {
type: Boolean,
default: true
}
},
data() {
return {
// 存储每一级的数据
levelList: [],
// 存储每一级选中的索引
selectedValueIndexs: [],
tabsIndex: 0,
popupShow: false,
// 新增confirmValues用于存储确认的值
confirmValues: []
}
},
watch: {
data: {
handler() {
this.initLevelList();
this.setDefaultValue();
},
immediate: true
},
show() {
this.popupShow = this.show;
},
modelValue: {
handler() {
// 初始化选中值
this.setDefaultValue();
},
immediate: true
}
},
computed: {
isChange() {
return this.tabsIndex > 1;
},
genTabsList() {
let tabsList = [{
name: "请选择"
}];
// 根据选中的值动态生成tabs
for (let i = 0; i < this.selectedValueIndexs.length; i++) {
if (this.selectedValueIndexs[i] !== undefined && this.levelList[i]) {
const selectedItem = this.levelList[i][this.selectedValueIndexs[i]];
if (selectedItem) {
tabsList[i] = {
name: selectedItem[this.labelKey]
};
// 如果还有下一级,则添加"请选择"
if (i === this.selectedValueIndexs.length - 1 &&
selectedItem[this.childrenKey] &&
selectedItem[this.childrenKey].length > 0) {
tabsList.push({
name: "请选择"
});
}
}
}
}
return tabsList;
},
uZIndex() {
// 如果用户有传递z-index值,优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
},
closeIconColor() {
return 'var(--up-main-color, #303133)';
},
levelPaneStyle() {
return {
backgroundColor: 'var(--up-bg-color, #f7f7f7)'
};
}
},
// 新增confirm事件
emits: ['update:modelValue', 'update:show', 'change', 'confirm', 'cancel'],
methods: {
t,
getSelectedValues() {
const result = [];
for (let i = 0; i < this.selectedValueIndexs.length; i++) {
const selectedIndex = this.selectedValueIndexs[i];
if (selectedIndex === undefined) continue;
if (!this.levelList[i] || !this.levelList[i][selectedIndex]) continue;
result.push(this.levelList[i][selectedIndex][this.valueKey]);
}
return result;
},
initLevelList() {
// 初始化第一级数据
if (this.data && this.data.length > 0) {
this.levelList = [this.data];
this.selectedValueIndexs = [];
}
},
setDefaultValue() {
// 检查data是否为空
if (!this.data || this.data.length == 0) return;
// 检查modelValue是否为空
if (!this.modelValue || this.modelValue.length == 0) {
this.confirmValues = [];
return;
}
// 根据默认值设置选中项
// 根据modelValue获取indexs给selectedValueIndexs
this.selectedValueIndexs = [];
this.levelList = []; // 设置层级数据为空
let currentLevelData = this.data;
for (let i = 0; i < this.modelValue.length; i++) {
const value = this.modelValue[i];
const index = currentLevelData.findIndex(item => item[this.valueKey] === value);
this.levelList[i] = currentLevelData; // 设置每一层级的数据
if (index !== -1) {
this.selectedValueIndexs.push(index);
// 更新下一级的数据
if (currentLevelData[index][this.childrenKey]) {
currentLevelData = currentLevelData[index][this.childrenKey];
} else {
// 如果没有子级数据,则停止处理
break;
}
} else {
// 如果找不到匹配项,则停止处理
break;
}
}
// 同步确认值,避免“仅回显未改动时确认返回空数组”
this.confirmValues = this.getSelectedValues();
},
close() {
this.$emit('cancel');
this.$emit('update:show', false);
},
tabsChange(item) {
},
levelChange(levelIndex, index) {
// 设置当前级的选中值
this.$set(this.selectedValueIndexs, levelIndex, index);
// 清除后续级别的选中值
this.selectedValueIndexs.splice(levelIndex + 1);
this.tabsIndex = Math.min(this.tabsIndex, levelIndex);
// 清除后续级别的列表
this.levelList.splice(levelIndex + 1);
// 获取当前选中项
const currentItem = this.levelList[levelIndex][index];
// 如果有子级数据,则初始化下一级
if (currentItem && currentItem[this.childrenKey] && currentItem[this.childrenKey].length > 0) {
// 确保levelList数组足够长
if (this.levelList.length <= levelIndex + 1) {
this.levelList.push(currentItem[this.childrenKey]);
} else {
this.$set(this.levelList, levelIndex + 1, currentItem[this.childrenKey]);
}
// 切换到下一级tab
this.tabsIndex = levelIndex + 1;
} else {
// 没有子级数据,说明是最后一级
if (this.autoClose) {
// 如果启用自动关闭,则触发change事件并关闭
this.emitChange();
this.handleConfirm();
} else {
// 否则只触发change事件,不关闭
this.emitChange(false);
}
}
},
// 修改emitChange方法,增加closePopup参数
emitChange(closePopup = true) {
// 构造选中结果
const result = this.getSelectedValues();
// 更新confirmValues
this.confirmValues = [...result];
// 触发change事件,返回value数组
this.$emit('change', this.confirmValues);
// 根据参数决定是否关闭弹窗
if (closePopup) {
this.close();
}
},
handleCancel() {
this.close();
},
handleConfirm() {
const values = this.confirmValues.length ? this.confirmValues : this.getSelectedValues();
this.confirmValues = [...values];
// 确认时触发confirm事件
this.$emit('update:modelValue', values);
this.$emit('confirm', values);
this.close();
},
// 跳转父节点
toFatherIndex(index){
this.tabsIndex = index;
},
}
}
</script>
<style lang="scss">
.area-box {
width: 100%;
overflow: hidden;
height: 800rpx;
>view {
width: 150%;
transition: transform 0.3s ease-in-out 0s;
transform: translateX(0);
&.change {
// transform: translateX(-33.3333333%);
}
}
.area-item {
// width: 750rpx;
height: 800rpx;
}
}
// 添加按钮区域样式
.u-cascader-action {
border-top: 1px solid var(--up-border-color, #eee);
}
</style>
@@ -1,27 +1,22 @@
<template>
<view class="u-cate-tab" :style="{ height: addUnit(height) }">
<view class="u-cate-tab">
<view class="u-cate-tab__wrap">
<scroll-view class="u-cate-tab__view u-cate-tab__menu-scroll-view"
scroll-y scroll-with-animation :scroll-top="scrollTop"
:scroll-into-view="itemId">
<view v-for="(item, index) in tabList" :key="index" class="u-cate-tab__item"
:class="[innerCurrent == index ? 'u-cate-tab__item-active' : '']"
:class="[current == index ? 'u-cate-tab__item-active' : '']"
@tap.stop="swichMenu(index)">
<slot name="tabItem" :item="item">
</slot>
<text v-if="!$slots['tabItem']" class="u-line-1">{{item[tabKeyName]}}</text>
</view>
</scroll-view>
<scroll-view :scroll-top="scrollRightTop" scroll-with-animation :scroll-into-view="scrollIntoView"
<scroll-view :scroll-top="scrollRightTop" scroll-with-animation
scroll-y class="u-cate-tab__right-box" @scroll="rightScroll">
<view class="u-cate-tab__right-top">
<slot name="rightTop" :tabList="tabList">
</slot>
</view>
<view class="u-cate-tab__page-view">
<template :key="index" v-for="(item , index) in tabList">
<view v-if="mode == 'follow' || ( mode == 'tab' && index == innerCurrent)"
class="u-cate-tab__page-item" :id="'item' + index">
<view class="u-cate-tab__page-item" :id="'item' + index"
v-for="(item , index) in tabList" :key="index">
<slot name="itemList" :item="item">
</slot>
<template v-if="!$slots['itemList']">
@@ -40,25 +35,15 @@
</view>
</template>
</view>
</template>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { addUnit, sleep } from '../../libs/function/index';
export default {
name: 'up-cate-tab',
props: {
mode: {
type: String,
default: 'follow' // follo跟随联动, tab单一显示。
},
height: {
type: String,
default: '100%'
},
tabList: {
type: Array,
default: () => {
@@ -72,43 +57,18 @@
itemKeyName: {
type: String,
default: 'name'
},
current: {
type: Number,
default: 0
}
},
watch: {
tabList: {
deep: true,
handler(newVal, oldVal) {
// this.observer();
sleep(30).then(() => {
this.getMenuItemTop();
this.leftMenuStatus(this.innerCurrent);
})
tabList() {
this.getMenuItemTop()
}
},
current(nval) {
this.innerCurrent = nval;
this.leftMenuStatus(this.innerCurrent);
sleep(30).then(() => {
this.swichMenu(this.innerCurrent)
})
},
height() {
// console.log('height change');
this.getMenuItemTop();
this.leftMenuStatus(this.innerCurrent);
}
},
emits: ['update:current'],
data() {
return {
scrollTop: 0, //tab标题的滚动条位置
scrollIntoView: '', // 滚动至哪个元素
oldScrollTop: 0,
innerCurrent: 0, // 预设当前项的值
current: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
itemId: '', // 栏目右边scroll-view用于滚动的id
@@ -119,38 +79,26 @@
timer: null, // 定时器
}
},
mounted() {
// this.observer();
this.innerCurrent = this.current;
this.leftMenuStatus(this.innerCurrent);
onMounted() {
this.getMenuItemTop()
// 设置默认index
sleep(50).then(() => {
this.swichMenu(this.innerCurrent)
})
},
methods: {
addUnit,
// 点击左边的栏目切换
async swichMenu(index) {
if (this.mode == 'follow') {
if(this.arr.length == 0) {
await this.getMenuItemTop();
}
if (this.scrollIntoView != 'item' + index) {
this.scrollIntoView = 'item' + index;
}
}
if (index == this.innerCurrent) return;
if (index == this.current) return;
this.scrollRightTop = this.oldScrollTop;
this.$nextTick(function(){
this.innerCurrent = index;
this.$emit('update:current', index);
this.scrollRightTop = this.arr[index];
this.current = index;
this.leftMenuStatus(index);
})
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
return new Promise((resolve, reject) => {
new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this);
query.select('.' + elClass).fields({
size: true
@@ -169,73 +117,54 @@
},
// 观测元素相交状态
async observer() {
await this.$nextTick();
// 清除之前的观察器
if (this._observerList) {
this._observerList.forEach(observer => {
observer.disconnect();
});
}
this._observerList = [];
this.tabList.map((val, index) => {
let observer = uni.createIntersectionObserver(this);
this._observerList.push(observer);
// 检测相交状态
// 检测右边scroll-view的id为itemxx的元素与u-cate-tab__right-box的相交状态
// 如果跟.u-cate-tab__right-box底部相交,就动态设置左边栏目的活动状态
observer.relativeTo('.u-cate-tab__right-box', {
top: 10
}).observe('#item' + index, (res) => {
top: 0
}).observe('#item' + index, res => {
if (res.intersectionRatio > 0) {
console.log('res', res);
// 修复:确保正确获取索引
let id = res.id ? res.id.substring(4) : index;
this.leftMenuStatus(parseInt(id));
let id = res.id.substring(4);
this.leftMenuStatus(id);
}
})
})
},
// 设置左边菜单的滚动状态
async leftMenuStatus(index) {
this.innerCurrent = index;
this.$emit('update:current', index);
this.current = index;
// 如果为0,意味着尚未初始化
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
await this.getElRect('u-cate-tab__menu-scroll-view', 'menuHeight');
await this.getElRect('u-cate-tab__item', 'menuItemHeight');
}
// console.log(this.menuHeight, this.menuItemHeight)
// 将菜单活动item垂直居中
this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2;
},
// 获取右边菜单每个item到顶部的距离
async getMenuItemTop() {
// await this.$nextTick();
// console.log('getMenuItemTop')
return new Promise(resolve => {
getMenuItemTop() {
new Promise(resolve => {
let selectorQuery = uni.createSelectorQuery().in(this);
selectorQuery.selectAll('.u-cate-tab__page-item').boundingClientRect((rects) => {
// 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
if(!rects.length) {
setTimeout(() => {
this.getMenuItemTop();
}, 100);
}, 10);
return ;
}
// console.log(rects)
this.rects = rects;
this.arr = [];
rects.forEach((rect) => {
// 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
this.arr.push(rect.top - rects[0].top);
})
// console.log(this.arr)
resolve();
})
}).exec()
})
},
// 右边菜单滚动
async rightScroll(e) {
if (this.mode !== 'follow') return;
this.oldScrollTop = e.detail.scrollTop;
// console.log(e.detail.scrollTop)
// console.log(JSON.stringify(this.arr))
@@ -266,7 +195,7 @@
return ;
}
}
}, 100)
}, 10)
}
}
}
@@ -281,12 +210,11 @@
.u-cate-tab__wrap {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
}
.u-search-inner {
background-color: var(--up-bg-color, rgb(234, 234, 234));
background-color: rgb(234, 234, 234);
border-radius: 100rpx;
display: flex;
align-items: center;
@@ -306,23 +234,23 @@
.u-cate-tab__item {
height: 110rpx;
background: var(--up-bg-color, #f6f6f6);
background: #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: var(--up-content-color, #444);
color: #444;
font-weight: 400;
line-height: 1;
}
.u-cate-tab__item-active {
position: relative;
color: var(--up-main-color, #000);
color: #000;
font-size: 30rpx;
font-weight: 600;
background: var(--up-card-bg-color, #fff);
background: #fff;
}
.u-cate-tab__item-active::before {
@@ -340,7 +268,7 @@
.u-cate-tab__right-box {
flex: 1;
background-color: var(--up-page-bg-color, var(--up-bg-color, rgb(250, 250, 250)));
background-color: rgb(250, 250, 250);
}
.u-cate-tab__page-view {
@@ -349,8 +277,7 @@
.u-cate-tab__page-item {
margin-bottom: 30rpx;
background-color: var(--up-card-bg-color, #fff);
border: 1px solid var(--up-border-color, #e5e7eb);
background-color: #fff;
padding: 16rpx;
border-radius: 8rpx;
}
@@ -361,14 +288,14 @@
.item-title {
font-size: 26rpx;
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
font-weight: bold;
}
.item-menu-name {
font-weight: normal;
font-size: 24rpx;
color: var(--up-main-color, $u-main-color);
color: $u-main-color;
}
.item-container {
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/cellGroup.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:54:16
* @FilePath : /u-view2.0/uview-ui/libs/config/props/cellGroup.js
*/
export default {
// cell-group组件的props
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CellGroupDefaultProps from './cellGroup'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CellGroupDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 分组标题
@@ -1,8 +1,8 @@
<template>
<view :style="[groupStyle, addStyle(customStyle)]" :class="[customClass]" class="u-cell-group">
<view :style="[addStyle(customStyle)]" :class="[customClass]" class="u-cell-group">
<view v-if="title" class="u-cell-group__title">
<slot name="title">
<text class="u-cell-group__title__text" :style="{ color: upThemeVar('--up-main-color', '#303133') }">{{ title }}</text>
<text class="u-cell-group__title__text">{{ title }}</text>
</slot>
</view>
<view class="u-cell-group__wrapper">
@@ -32,14 +32,6 @@
export default {
name: 'u-cell-group',
mixins: [mpMixin, mixin, props],
computed: {
groupStyle() {
const fallbackBg = this.upThemeIsDark ? '#1c1c1e' : '#ffffff'
return {
backgroundColor: this.upThemeVar('--up-card-bg-color', fallbackBg)
}
}
},
methods: {
addStyle
}
@@ -47,6 +39,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-cell-group-title-padding: 16px 16px 8px !default;
$u-cell-group-title-font-size: 15px !default;
@@ -71,3 +64,4 @@
}
}
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/cell.js
* @LastAuthor : LQ
* @lastTime : 2021-08-23 20:53:09
* @FilePath : /u-view2.0/uview-ui/libs/config/props/cell.js
*/
export default {
// cell组件的props
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CellDefaultProps from './cell'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CellDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 标题
@@ -7,34 +7,32 @@
<view class="u-cell__left-icon-wrap" v-if="$slots.icon || icon">
<slot name="icon" v-if="$slots.icon">
</slot>
<up-icon v-else :name="icon"
<u-icon v-else :name="icon"
:custom-style="iconStyle"
:size="size === 'large' ? 22 : 18"></up-icon>
:size="size === 'large' ? 22 : 18"></u-icon>
</view>
<view class="u-cell__title">
<!-- 将slot与默认内容用if/else分开主要是因为微信小程序不支持slot嵌套传递,这样才能解决collapse组件的slot不失效问题,label暂时未用到。 -->
<slot name="title" v-if="$slots.title || !title">
</slot>
<text v-else class="u-cell__title-text" :style="[cellTitleDynamicStyle, titleTextStyle]"
<text v-else class="u-cell__title-text" :style="[titleTextStyle]"
:class="[required && 'u-cell--required', disabled && 'u-cell--disabled', size === 'large' && 'u-cell__title-text--large']">{{ title }}</text>
<slot name="label">
<text class="u-cell__label" v-if="label"
:style="[cellLabelDynamicStyle]"
:class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__label--large']">{{ label }}</text>
</slot>
</view>
</view>
<slot name="value">
<text class="u-cell__value"
:style="[cellValueDynamicStyle]"
:class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__value--large']"
v-if="!testEmpty(value)">{{ value }}</text>
</slot>
<view class="u-cell__right-icon-wrap" v-if="$slots['right-icon'] || isLink"
:class="[`u-cell__right-icon-wrap--${arrowDirection}`]">
<up-icon v-if="rightIcon && !$slots['right-icon']" :name="rightIcon"
:custom-style="rightIconStyle" :color="disabled ? cellDisabledColor : 'info'"
:size="size === 'large' ? 18 : 16"></up-icon>
<u-icon v-if="rightIcon && !$slots['right-icon']" :name="rightIcon"
:custom-style="rightIconStyle" :color="disabled ? '#c8c9cc' : 'info'"
:size="size === 'large' ? 18 : 16"></u-icon>
<slot v-else name="right-icon">
</slot>
</view>
@@ -93,24 +91,6 @@
computed: {
titleTextStyle() {
return addStyle(this.titleStyle)
},
cellDisabledColor() {
return this.upThemeVar('--up-disabled-color', '#c8c9cc')
},
cellTitleDynamicStyle() {
return {
color: this.upThemeVar('--up-main-color', '#303133')
}
},
cellLabelDynamicStyle() {
return {
color: this.upThemeVar('--up-tips-color', '#909399')
}
},
cellValueDynamicStyle() {
return {
color: this.upThemeVar('--up-content-color', '#606266')
}
}
},
emits: ['click'],
@@ -133,6 +113,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-cell-padding: 13px 15px !default;
$u-cell-font-size: 15px !default;
@@ -147,8 +128,8 @@
$u-cell-label-line-height: 18px !default;
$u-cell-value-font-size: 14px !default;
$u-cell-value-color: $u-content-color !default;
$u-cell-clickable-color: var(--up-hover-bg-color, #f1f1f1) !default;
$u-cell-disabled-color: $u-disabled-color !default;
$u-cell-clickable-color: $u-bg-color !default;
$u-cell-disabled-color: #c8c9cc !default;
$u-cell-padding-top-large: 13px !default;
$u-cell-padding-bottom-large: 13px !default;
$u-cell-value-font-size-large: 15px !default;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/checkboxGroup.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:54:47
* @FilePath : /u-view2.0/uview-ui/libs/config/props/checkboxGroup.js
*/
export default {
// checkbox-group组件
@@ -1,8 +1,6 @@
import { defineMixin } from '../../libs/vue'
import CheckboxGroupDefaultProps from './checkboxGroup'
import { registerComponentProps } from '../../libs/config/props.js'
import defProps from '../../libs/config/props.js'
const defProps = registerComponentProps(CheckboxGroupDefaultProps)
export const props = defineMixin({
props: {
// 标识符
@@ -14,7 +14,7 @@
/**
* checkboxGroup 复选框组
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://uview-plus.jiangruyi.com/components/checkbox.html
* @tutorial https://ijry.github.io/uview-plus/components/checkbox.html
* @property {String} name 标识符
* @property {Array} value 绑定的值
* @property {String} shape 形状,circle-圆形,square-方形 (默认 'square' )
@@ -109,16 +109,14 @@
this.$emit("input", values);
// #endif
// 放在最后更新,否则change事件传出去的values不会更新
this.$emit('change', values, {
isChecked: childInstance.$data['isChecked'],
name: childInstance.$props['name'] // 当前变动的checkbox的name值
})
this.$emit('change', values)
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-checkbox-group {
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/checkbox.js
* @LastAuthor : LQ
* @lastTime : 2021-08-23 21:06:59
* @FilePath : /u-view2.0/uview-ui/libs/config/props/checkbox.js
*/
export default {
// checkbox组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CheckboxDefaultProps from './checkbox'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CheckboxDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// checkbox的名称
@@ -11,8 +11,8 @@
:class="iconClasses"
:style="[iconWrapStyle]"
>
<slot name="icon" :elIconSize="elIconSize" :elIconColor="elIconColor" :checked="isChecked" :elDisabled="elDisabled">
<up-icon
<slot name="icon" :elIconSize="elIconSize" :elIconColor="elIconColor">
<u-icon
class="u-checkbox__icon-wrap__icon"
name="checkbox-mark"
:size="elIconSize"
@@ -111,16 +111,16 @@
},
// 组件选中激活时的颜色
elActiveColor() {
return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : this.upThemeVar('--up-primary', '#2979ff'));
return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#2979ff');
},
// 组件选未中激活时的颜色
elInactiveColor() {
return this.inactiveColor ? this.inactiveColor : (this.parentData.inactiveColor ? this.parentData.inactiveColor :
this.upThemeVar('--up-border-color', '#c8c9cc'));
'#c8c9cc');
},
// label的颜色
elLabelColor() {
return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : this.upThemeVar('--up-content-color', '#606266'))
return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
},
// 组件的形状
elShape() {
@@ -133,7 +133,7 @@
},
elIconColor() {
const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
this.upThemeVar('--up-card-bg-color', '#ffffff'));
'#ffffff');
// 图标的颜色
if (this.elDisabled) {
// disabled状态下,已勾选的checkbox图标改为elInactiveColor
@@ -149,12 +149,8 @@
if (this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled')
}
if (this.isChecked) {
if (this.elDisabled) {
if (this.isChecked && this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled--checked')
} else {
classes.push('u-checkbox__icon-wrap--checked')
}
}
// 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
// #ifdef MP-ALIPAY || MP-TOUTIAO
@@ -165,7 +161,7 @@
iconWrapStyle() {
// checkbox的整体样式
const style = {}
style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : this.upThemeVar('--up-card-bg-color', '#ffffff')
style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : '#ffffff'
style.borderColor = this.isChecked && !this.elDisabled ? this.elActiveColor : this.elInactiveColor
style.width = addUnit(this.elSize)
style.height = addUnit(this.elSize)
@@ -181,7 +177,7 @@
const style = {}
if (!this.usedAlone) {
if (this.parentData.borderBottom && this.parentData.placement === 'row') {
error('检测到您将borderBottom设置为true,需要同时将up-checkbox-group的placement设置为column才有效')
error('检测到您将borderBottom设置为true,需要同时将u-checkbox-group的placement设置为column才有效')
}
// 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
if (this.parentData.borderBottom && this.parentData.placement === 'column') {
@@ -201,14 +197,13 @@
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
this.updateParentData()
if (!this.parent) {
error('up-checkbox必须搭配up-checkbox-group组件使用')
error('u-checkbox必须搭配u-checkbox-group组件使用')
}
let value = '';
// #ifdef VUE2
value = this.parentData.value
const value = this.parentData.value
// #endif
// #ifdef VUE3
value = this.parentData.modelValue
const value = this.parentData.modelValue
// #endif
// 设置初始化时,是否默认选中的状态,父组件u-checkbox-group的value可能是array,所以额外判断
if (this.checked) {
@@ -253,9 +248,7 @@
}
},
emitEvent() {
this.$emit('change', this.isChecked, {
name: this.name
})
this.$emit('change', this.isChecked)
// 双向绑定
if (this.usedAlone) {
this.$emit('update:checked', this.isChecked)
@@ -288,6 +281,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-checkbox-icon-wrap-margin-right:6px !default;
$u-checkbox-icon-wrap-font-size:6px !default;
$u-checkbox-icon-wrap-border-width:1px !default;
@@ -298,7 +292,7 @@
$u-checkbox-icon-wrap-checked-color:#fff !default;
$u-checkbox-icon-wrap-checked-background-color:red !default;
$u-checkbox-icon-wrap-checked-border-color:#2979ff !default;
$u-checkbox-icon-wrap-disabled-background-color:var(--up-bg-color, #ebedf0) !default;
$u-checkbox-icon-wrap-disabled-background-color:#ebedf0 !default;
$u-checkbox-icon-wrap-disabled-checked-color:#c8c9cc !default;
$u-checkbox-label-margin-left:5px !default;
$u-checkbox-label-margin-right:12px !default;
@@ -1,114 +0,0 @@
<style scoped lang="scss">
.up-choose {
::v-deep .up-tag {
font-weight: 600;
}
&:last-child {
margin-right: 0;
}
}
.up-choose-wrap {
flex-wrap: wrap;
}
.up-choose-nowrap {
flex-wrap: nowrap;
white-space: nowrap;
}
</style>
<template>
<scroll-view
:scroll-x="wrap === false"
:class="['up-choose', wrap ? 'up-choose-wrap' : 'up-choose-nowrap']">
<template :key="item.id" v-for="(item,index) in options">
<view :style="{width: itemWidth, display: 'inline-block'}">
<slot :item="item" :index="index">
<up-tag :type="index == currentIndex ? 'primary' : 'info'"
size="large" :plain="index == currentIndex ? false : true"
:class="currentIndex === index ? 'active': ''" :height="itemHeight"
:style="{width: itemWidth, padding: itemPadding}"
@click="change(index)">
{{item[labelName]}}
</up-tag>
</slot>
</view>
</template>
</scroll-view>
</template>
<script>
export default {
name: 'up-choose',
props: {
options:{
type: Array,
default: ()=>{
return [];
}
},
modelValue: {
type: [Number,String,Array],
default: false
},
type: {
type: [String],
default: 'radio'
},
itemWidth: {
type: [String],
default: 'auto'
},
itemHeight: {
type: [String],
default: '50px'
},
itemPadding: {
type: [String],
default: '8px'
},
labelName: {
type: String,
default: 'title'
},
valueName: {
type: String,
default: 'value'
},
customClick: {
type: Boolean,
default: false
},
// 是否换行
wrap: {
type: Boolean,
default: true
}
},
data() {
return {
currentIndex: ''
}
},
watch: {
modelValue: {
immediate: true,
handler(newVal) {
this.currentIndex = newVal
}
}
},
emits: ['update:modelValue', 'custom-click'],
methods: {
change(index){
if (this.customClick) {
this.$emit('custom-click', index);
} else {
this.currentIndex = index;
this.$emit('update:modelValue', index);
}
}
}
}
</script>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/circleProgress.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:55:02
* @FilePath : /u-view2.0/uview-ui/libs/config/props/circleProgress.js
*/
export default {
// circleProgress 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CircleProgressDefaultProps from './circleProgress'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CircleProgressDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
percentage: {
@@ -37,7 +37,7 @@
/**
* CircleProgress 圆形进度条 TODO: 待完善
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度环。
* @tutorial https://uview-plus.jiangruyi.com/components/circleProgress.html
* @tutorial https://ijry.github.io/uview-plus/components/circleProgress.html
* @property {String | Number} percentage 圆环进度百分比值,为数值类型,0-100 (默认 30 )
* @example
*/
@@ -112,6 +112,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-circle-progress {
@include flex(row);
@@ -1,169 +0,0 @@
<template>
<view class="u-city-locate">
<up-index-list :indexList="indexList">
<template #header>
<view class="u-current-city-wrap">
<view class="u-current-city-title">{{ t("up.cityLocate.locateCity") }}</view>
<view class="u-current-city-item" @tap="location">
<view class="u-location-city">{{locationCity}}</view>
</view>
</view>
</template>
<template :key="index" v-for="(item, index) in cityList">
<!-- #ifdef APP-NVUE -->
<up-index-anchor :text="indexList[index]"></up-index-anchor>
<!-- #endif -->
<up-index-item>
<!-- #ifndef APP-NVUE -->
<up-index-anchor :text="indexList[index]"></up-index-anchor>
<!-- #endif -->
<view class="hot-city-list" v-if="index == 0">
<view class="" v-for="(item1, index1) in item" @tap="selectedCity(item1)">
<view class="hot-city-item">{{ item1[nameKey] }}</view>
</view>
</view>
<view v-else class="item-list" v-for="(item1, index1) in item" :key="index1">
<view class="list__item" @tap="selectedCity(item1)">
<text class="list__item__city-name">{{item1[nameKey]}}</text>
</view>
<up-line></up-line>
</view>
</up-index-item>
</template>
<template #footer>
<view class="u-safe-area-inset--bottom">
<text class="list__footer"></text>
</view>
</template>
</up-index-list>
</view>
</template>
<script>
import { t } from '../../libs/i18n'
export default{
name: 'u-city-locate',
props:{
indexList: {
type: Array,
default: ['🔥']
},
cityList:{
type: Array,
default: () => {
return [
[{
name: '北京',
value: 'beijing'
},
{
name: '上海',
value: 'shanghai'
},
{
name: '广州',
value: 'guangzhou'
},
{
name: '深圳',
value: 'shenzhen'
},
{
name: '杭州',
value: 'hangzhou'
}]
]
}
},
locationType: {
type: String,
default: 'wgs84'
},
currentCity: {
type: String,
default: ''
},
nameKey: {
type: String,
default: 'name'
}
},
computed:{
},
watch:{
currentCity(val) {
this.locationCity = val;
}
},
data(){
return{
locationCity: t("up.cityLocate.locating") + '....'
}
},
emits: ['location-success', 'select-city'],
methods:{
t,
// 获取城市
selectedCity(city){
this.locationCity = city[this.nameKey];
this.$emit('select-city', {
locationCity: this.locationCity
});
},
// 定位操作
location(){
let That = this;
uni.getLocation({
type: this.locationType,
geocode:true,
success(res){
console.log(res);
That.locationCity = res.address && res.address.city;
That.$emit('location-success', {
...res,
locationCity: That.locationCity
});
},
fail(){
That.locationCity = t("up.cityLocate.fail");
}
});
},
},
// 页面挂载后进行异步操作
created(){
},
mounted(){
this.location();
}
}
</script>
<style lang="scss">
.list__item {
padding: 8px 1px;
}
.u-current-city-title {
color: var(--up-tips-color, #909399);
margin-bottom: 5px;
}
.u-location-city,
.list__item__city-name {
color: var(--up-main-color, #303133);
}
.u-current-city-item {
height: 30px;
}
.hot-city-list {
display: flex !important;
flex-direction: row !important;
padding: 12px 0;
.hot-city-item {
padding: 6px 12px;
margin: 5px;
color: var(--up-main-color, #303133);
border: 1px solid var(--up-border-color, #ededed);
background-color: var(--up-card-bg-color, #ffffff);
}
}
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/codeInput.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:55:58
* @FilePath : /u-view2.0/uview-ui/libs/config/props/codeInput.js
*/
export default {
// codeInput 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CodeInputDefaultProps from './codeInput'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CodeInputDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 键盘弹起时,是否自动上推页面
@@ -58,7 +58,7 @@
/**
* CodeInput 验证码输入
* @description 该组件一般用于验证用户短信验证码的场景,也可以结合uview-plus的键盘组件使用
* @tutorial https://uview-plus.jiangruyi.com/components/codeInput.html
* @tutorial https://ijry.github.io/uview-plus/components/codeInput.html
* @property {String | Number} maxlength 最大输入长度 (默认 6 )
* @property {Boolean} dot 是否用圆点填充 (默认 false )
* @property {String} mode 显示模式,box-盒子模式,line-底部横线模式 (默认 'box' )
@@ -220,6 +220,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-code-input-cursor-width: 1px;
$u-code-input-cursor-height: 20px;
$u-code-input-cursor-animation-duration: 1s;
@@ -1,20 +1,20 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/code.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:55:27
* @FilePath : /u-view2.0/uview-ui/libs/config/props/code.js
*/
import { t } from '../../libs/i18n'
export default {
// code 组件
code: {
seconds: 60,
startText: t("up.code.send"),
changeText: t("up.code.resendAfter"),
endText: t("up.code.resend"),
startText: '获取验证码',
changeText: 'X秒重新获取',
endText: '重新获取',
keepRunning: false,
uniqueKey: ''
}
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CodeDefaultProps from './code'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CodeDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 倒计时总秒数
@@ -11,7 +11,7 @@
/**
* Code 验证码输入框
* @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件 不提供界面显示,只提供提示语,由用户将提示语嵌入到具体的场景
* @tutorial https://uview-plus.jiangruyi.com/components/code.html
* @tutorial https://ijry.github.io/uview-plus/components/code.html
* @property {String | Number} seconds 倒计时所需的秒数(默认 60 )
* @property {String} startText 开始前的提示语,见官网说明(默认 '获取验证码' )
* @property {String} changeText 倒计时期间的提示语,必须带有字母"x",见官网说明(默认 'X秒重新获取' )
@@ -128,4 +128,5 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/col.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:56:12
* @FilePath : /u-view2.0/uview-ui/libs/config/props/col.js
*/
export default {
// col 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import ColDefaultProps from './col'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(ColDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 占父容器宽度的多少等分,总分为12份
@@ -20,7 +20,7 @@
/**
* CodeInput 栅格系统的列
* @description 该组件一般用于Layout 布局 通过基础的 12 分栏,迅速简便地创建布局
* @tutorial https://uview-plus.jiangruyi.com/components/Layout.html
* @tutorial https://ijry.github.io/uview-plus/components/Layout.html
* @property {String | Number} span 栅格占据的列数,总12等份 (默认 12 )
* @property {String | Number} offset 分栏左边偏移,计算方式与span相同 (默认 0 )
* @property {String} justify 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) (默认 'start' )
@@ -100,6 +100,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-col {
padding: 0;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/collapseItem.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:56:42
* @FilePath : /u-view2.0/uview-ui/libs/config/props/collapseItem.js
*/
export default {
// collapseItem 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CollapseItemDefaultProps from './collapseItem'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CollapseItemDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 标题
@@ -24,7 +24,7 @@
</template>
<template #icon>
<slot name="icon">
<up-icon v-if="!$slots.icon && icon" :size="22" :name="icon"></up-icon>
<u-icon v-if="!$slots.icon && icon" :size="22" :name="icon"></u-icon>
</slot>
</template>
<template #value>
@@ -36,7 +36,7 @@
</template>
<template #right-icon>
<template v-if="showRight">
<up-icon v-if="!$slots['right-icon']" :size="16" name="arrow-right"></up-icon>
<u-icon v-if="!$slots['right-icon']" :size="16" name="arrow-right"></u-icon>
<slot name="right-icon">
</slot>
</template>
@@ -71,7 +71,7 @@
/**
* collapseItem 折叠面板Item
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
* @tutorial https://uview-plus.jiangruyi.com/components/collapse.html
* @tutorial https://ijry.github.io/uview-plus/components/collapse.html
* @property {String} title 标题
* @property {String} value 标题右侧内容
* @property {String} label 标题下方的描述信息
@@ -201,7 +201,7 @@
// 查询内容高度
queryRect() {
// #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://uview-plus.jiangruyi.com/js/getRect.html
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html
// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
return new Promise(resolve => {
this.$uGetRect(`#${this.elId}`).then(size => {
@@ -225,6 +225,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-collapse-item {
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/collapse.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:56:30
* @FilePath : /u-view2.0/uview-ui/libs/config/props/collapse.js
*/
export default {
// collapse 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CollapseDefaultProps from './collapse'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CollapseDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 当前展开面板的name,非手风琴模式:[<string | number>],手风琴模式:string | number
@@ -12,7 +12,7 @@
/**
* collapse 折叠面板
* @description 通过折叠面板收纳内容区域
* @tutorial https://uview-plus.jiangruyi.com/components/collapse.html
* @tutorial https://ijry.github.io/uview-plus/components/collapse.html
* @property {String | Number | Array} value 当前展开面板的name,非手风琴模式:[<string | number>],手风琴模式:string | number
* @property {Boolean} accordion 是否手风琴模式( 默认 false )
* @property {Boolean} border 是否显示外边框 ( 默认 true )
@@ -87,4 +87,5 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/columnNotice.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:57:16
* @FilePath : /u-view2.0/uview-ui/libs/config/props/columnNotice.js
*/
export default {
// columnNotice 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import ColumnNoticeDefaultProps from './columnNotice'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(ColumnNoticeDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 显示的内容,字符串
@@ -8,11 +8,11 @@
class="u-notice__left-icon"
v-if="icon"
>
<up-icon
<u-icon
:name="icon"
:color="color"
size="19"
></up-icon>
></u-icon>
</view>
</slot>
<swiper
@@ -40,19 +40,19 @@
class="u-notice__right-icon"
v-if="['link', 'closable'].includes(mode)"
>
<up-icon
<u-icon
v-if="mode === 'link'"
name="arrow-right"
:size="17"
:color="color"
></up-icon>
<up-icon
></u-icon>
<u-icon
v-if="mode === 'closable'"
name="close"
:size="16"
:color="color"
@click="close"
></up-icon>
></u-icon>
</view>
</view>
</template>
@@ -66,7 +66,7 @@
/**
* ColumnNotice 滚动通知中的垂直滚动 内部组件
* @description 该组件用于滚动通告场景,是其中的垂直滚动方式
* @tutorial https://uview-plus.jiangruyi.com/components/noticeBar.html
* @tutorial https://ijry.github.io/uview-plus/components/noticeBar.html
* @property {Array} text 显示的内容,字符串
* @property {String} icon 是否显示左侧的音量图标 ( 默认 'volume' )
* @property {String} mode 通告模式,link-显示右箭头,closable-显示右侧关闭图标
@@ -128,6 +128,7 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-notice {
@include flex;
@@ -1,10 +1,9 @@
<template>
<view @click="handleClick">
<slot>{{ t("up.common.copy") }}</slot>
<slot>复制</slot>
</view>
</template>
<script>
import { t } from '../../libs/i18n'
export default {
name: "up-copy",
props: {
@@ -18,17 +17,16 @@ export default {
},
notice: {
type: String,
default: t("up.common.copy") + t("up.common.success")
default: '复制成功'
}
},
emits: ['success'],
methods: {
t,
handleClick() {
let content = this.content;
if (!content) {
uni.showToast({
title: t("up.common.none"),
title: '暂无',
icon: 'none',
duration: 2000,
});
@@ -36,7 +34,7 @@ export default {
}
content = typeof content === 'string' ? content : content.toString() // 复制内容,必须字符串,数字需要转换为字符串
/**
* 复制逻辑
* 小程序端 和 app端的复制逻辑
*/
let that = this;
uni.setClipboardData({
@@ -44,7 +42,7 @@ export default {
success: function() {
if (that.alertStyle == 'modal') {
uni.showModal({
title: "up.common.tip",
title: '提示',
content: that.notice
});
} else {
@@ -57,7 +55,7 @@ export default {
},
fail:function(){
uni.showToast({
title: t("up.common.copy") + t("up.common.fail"),
title: '复制失败',
icon: 'none',
duration:3000,
});
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/countDown.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 17:11:29
* @FilePath : /u-view2.0/uview-ui/libs/config/props/countDown.js
*/
export default {
// u-count-down 计时器组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CountDownDefaultProps from './countDown'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CountDownDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 倒计时时长,单位ms
@@ -1,7 +1,6 @@
<template>
<view class="u-count-down">
<slot :days="timeData.days" :hours="timeData.hours"
:minutes="timeData.minutes" :seconds="timeData.seconds">
<slot>
<text class="u-count-down__text">{{ formattedTime }}</text>
</slot>
</view>
@@ -113,7 +112,6 @@
this.remainTime = remain
// 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
const timeData = parseTimeData(remain)
this.timeData = timeData;
this.$emit('change', timeData)
// 得出格式化后的时间
this.formattedTime = parseFormat(this.format, timeData)
@@ -153,6 +151,7 @@
lang="scss"
scoped
>
@import "../../libs/css/components.scss";
$u-count-down-text-color:$u-content-color !default;
$u-count-down-text-font-size:15px !default;
$u-count-down-text-line-height:22px !default;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/countTo.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:57:32
* @FilePath : /u-view2.0/uview-ui/libs/config/props/countTo.js
*/
export default {
// countTo 组件
@@ -1,8 +1,5 @@
import { defineMixin } from '../../libs/vue'
import CountToDefaultProps from './countTo'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CountToDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 开始的数值,默认从0增长到某一个数
@@ -17,7 +17,7 @@ import { addUnit } from '../../libs/function/index';
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
* @tutorial https://uview-plus.jiangruyi.com/components/countTo.html
* @tutorial https://ijry.github.io/uview-plus/components/countTo.html
* @property {String | Number} startVal 开始的数值,默认从0增长到某一个数(默认 0 )
* @property {String | Number} endVal 要滚动的目标数值,必须 (默认 0 )
* @property {String | Number} duration 滚动到目标数值的动画持续时间,单位为毫秒(ms) (默认 2000 )
@@ -178,6 +178,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
@@ -1,405 +0,0 @@
<template>
<view class="up-coupon" :class="[`up-coupon--${shape}`, `up-coupon--${type}`, `up-coupon--${size}`, {'up-coupon--disabled': disabled}]"
:style="[couponStyle]" @click="handleClick">
<view class="up-coupon__content">
<!-- 左侧金额区域 -->
<view class="up-coupon__amount">
<slot name="unit" :unit="unit" :unitPosition="unitPosition" v-if="unitPosition === 'left'">
<text class="up-coupon__amount-unit" v-if="unitPosition === 'left'">{{ unit }}</text>
</slot>
<slot name="amount" :amount="amount">
<text class="up-coupon__amount-value">{{ amount }}</text>
</slot>
<slot name="unit" :unit="unit" :unitPosition="unitPosition" v-if="unitPosition === 'right'">
<text class="up-coupon__amount-unit" v-if="unitPosition === 'right'">{{ unit }}</text>
</slot>
<slot name="limit" :limit="limit">
<text class="up-coupon__amount-limit" v-if="limit">{{ limit }}</text>
</slot>
</view>
<!-- 中间描述区域 -->
<view class="up-coupon__info">
<slot name="title" :title="title">
<text class="up-coupon__info-title">{{ title }}</text>
</slot>
<slot name="desc" :desc="desc">
<text class="up-coupon__info-desc" v-if="desc">{{ desc }}</text>
</slot>
<slot name="time" :time="time">
<text class="up-coupon__info-time" v-if="time">{{ time }}</text>
</slot>
</view>
<!-- 右侧操作区域 -->
<view class="up-coupon__action u-padding-right-20">
<slot name="action" :actionText="actionText" :circle="circle">
<up-tag type="error" :bgColor="type ? 'transparent' : '#eb433d'"
:borderColor="type ? '#eee' : '#eb433d'" borderRadius="6px"
size="medium" class="up-coupon__action-text"
:shape="circle ? 'circle': 'circle'">{{ actionText }}</up-tag>
</slot>
</view>
</view>
<!-- 红包绳子效果 -->
<view v-if="shape === 'envelope'" class="up-coupon__rope"></view>
<!-- 默认插槽,可用于添加额外内容 -->
<slot></slot>
</view>
</template>
<script>
export default {
name: 'up-coupon',
props: {
// 金额
amount: {
type: [String, Number],
default: ''
},
// 金额单位
unit: {
type: String,
default: '¥'
},
// 单位位置
unitPosition: {
type: String,
default: 'left'
},
// 使用限制
limit: {
type: String,
default: ''
},
// 标题
title: {
type: String,
default: '优惠券'
},
// 描述
desc: {
type: String,
default: ''
},
// 有效期
time: {
type: String,
default: ''
},
// 操作按钮文字
actionText: {
type: String,
default: '使用'
},
// 形状:coupon-优惠券, envelope-红包, card-卡片
shape: {
type: String,
default: 'coupon'
},
// 尺寸:small, medium, large
size: {
type: String,
default: 'medium'
},
// 是否圆形按钮
circle: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 文字颜色
color: {
type: String,
default: ''
},
// 内置背景类型
type: {
type: String,
default: ''
},
},
computed: {
couponStyle() {
const style = {};
if (this.bgColor) style.background = this.bgColor;
if (this.color) style.color = this.color;
return style;
},
dotCount() {
// 根据尺寸计算锯齿数量
const map = {
small: 8,
medium: 10,
large: 12
};
return map[this.size] || 10;
}
},
methods: {
handleClick() {
if (this.disabled) return;
this.$emit('click');
}
}
}
</script>
<style lang="scss" scoped>
.up-coupon {
position: relative;
overflow: hidden;
border-radius: 8rpx;
background: #ffebf0;
color: $u-main-color;
width: 100%;
&--coupon {
border-radius: 16rpx;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: -24rpx;
top: 50%;
transform: translateY(-50%);
width: 48rpx;
height: 48rpx;
background-color: #fff;
border-radius: 50%;
}
&::after {
content: '';
position: absolute;
right: -24rpx;
top: 50%;
transform: translateY(-50%);
width: 48rpx;
height: 48rpx;
background-color: #fff;
border-radius: 50%;
}
}
&--envelope {
border-radius: 16rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 20rpx;
background: repeating-linear-gradient(-45deg, #ffd000, #ffd000 10rpx, #ffa000 10rpx, #ffa000 20rpx);
}
}
&--card {
border-radius: 16rpx;
}
&--small {
// width: 520rpx;
height: 160rpx;
}
&--medium {
// width: 600rpx;
height: 180rpx;
}
&--large {
// width: 700rpx;
height: 220rpx;
}
&--disabled {
opacity: 0.5;
}
&__content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 30rpx;
position: relative;
z-index: 2;
}
&__amount {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-left: 10rpx;
padding-right: 30rpx;
border-right: 1px dashed #ccc;
&-unit {
font-size: 24rpx;
font-weight: normal;
}
&-value {
font-size: 56rpx;
font-weight: bold;
color: red;
line-height: 1;
margin: 10rpx 0;
}
&-limit {
font-size: 24rpx;
opacity: 0.9;
}
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
padding-left: 30rpx;
&-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
&-desc {
font-size: 24rpx;
opacity: 0.9;
margin-bottom: 10rpx;
}
&-time {
font-size: 20rpx;
opacity: 0.8;
}
}
&__action {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
&__dots {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 30rpx 0;
z-index: 1;
}
&__dot {
width: 32rpx;
height: 32rpx;
background-color: #fff;
border-radius: 50%;
margin: 0 -16rpx;
z-index: 3;
}
&__rope {
position: absolute;
top: -40rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 80rpx;
background: linear-gradient(to right, #ffd000, #ffa000);
border-radius: 40rpx 40rpx 0 0;
z-index: 1;
&::before {
content: '';
position: absolute;
top: 0;
left: -20rpx;
width: 20rpx;
height: 40rpx;
background: linear-gradient(to bottom, #ffd000, #ffa000);
border-radius: 10rpx 0 0 10rpx;
}
&::after {
content: '';
position: absolute;
top: 0;
right: -20rpx;
width: 20rpx;
height: 40rpx;
background: linear-gradient(to bottom, #ffd000, #ffa000);
border-radius: 0 10rpx 10rpx 0;
}
}
// 不同主题样式
&--primary {
background: linear-gradient(90deg, #43afff, #3b8cff);
color: #fff;
.up-coupon__amount {
border-right: 1px dashed #eee;
}
.up-coupon__amount-value {
color: #fff;
}
}
&--success {
background: linear-gradient(90deg, #67dda9, #19be6b);
color: #fff !important;
.up-coupon__amount {
border-right: 1px dashed #eee;
}
.up-coupon__amount-value {
color: #fff;
}
}
&--warning {
background: linear-gradient(90deg, #ff9739, #ff6a39);
color: #fff;
.up-coupon__amount {
border-right: 1px dashed #eee;
}
.up-coupon__amount-value {
color: #fff;
}
}
&--error {
background: linear-gradient(90deg, #ff7070, #ff4747);
color: #fff;
.up-coupon__amount {
border-right: 1px dashed #eee;
}
.up-coupon__amount-value {
color: #fff;
}
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,12 @@
/*
* @Author : LQ
* @Description :
* @version : 3.0
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/datetimePicker.js
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:57:48
* @FilePath : /u-view2.0/uview-ui/libs/config/props/datetimePicker.js
*/
import { t } from '../../libs/i18n'
export default {
// datetimePicker 组件
datetimePicker: {
@@ -23,24 +22,16 @@ export default {
maxHour: 23,
minMinute: 0,
maxMinute: 59,
minSecond: 0,
maxSecond: 59,
filter: null,
formatter: null,
loading: false,
itemHeight: 44,
cancelText: t("up.common.cancel"),
confirmText: t("up.common.confirm"),
cancelText: '取消',
confirmText: '确认',
cancelColor: '#909193',
confirmColor: '#3c9cff',
visibleItemCount: 5,
closeOnClickOverlay: false,
defaultIndex: [],
inputBorder: 'surround',
disabled: false,
disabledColor: '',
placeholder: t("up.common.pleaseChoose"),
inputProps: {},
pageInline: false
defaultIndex: []
}
}
File diff suppressed because one or more lines are too long
@@ -1,28 +1,15 @@
import { defineMixin } from '../../libs/vue'
import DatetimePickerDefaultProps from './datetimePicker'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(DatetimePickerDefaultProps)
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 是否显示input
hasInput: {
type: Boolean,
default: false
},
inputProps: {
type: Object,
default: () => {
return {}
}
},
inputBorder: {
type: String,
default: () => defProps.input.inputBorder
default: () => false
},
disabled: {
type: Boolean,
default: () => defProps.input.disabled
default: () => false
},
disabledColor:{
type: String,
@@ -30,7 +17,7 @@ export const props = defineMixin({
},
placeholder: {
type: String,
default: () => defProps.input.placeholder
default: () => '请选择'
},
format: {
type: String,
@@ -75,7 +62,7 @@ export const props = defineMixin({
type: String,
default: () => defProps.datetimePicker.title
},
// 展示格式,支持date、time、year-month、datetime、datehour、timesecond、datetimesecond
// 展示格式,mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择
mode: {
type: String,
default: () => defProps.datetimePicker.mode
@@ -92,36 +79,26 @@ export const props = defineMixin({
// 最小默认值为前10年
default: () => defProps.datetimePicker.minDate
},
// 可选的最小小时,仅mode=time/timesecond有效
// 可选的最小小时,仅mode=time有效
minHour: {
type: Number,
default: () => defProps.datetimePicker.minHour
},
// 可选的最大小时,仅mode=time/timesecond有效
// 可选的最大小时,仅mode=time有效
maxHour: {
type: Number,
default: () => defProps.datetimePicker.maxHour
},
// 可选的最小分钟,仅mode=time/timesecond有效
// 可选的最小分钟,仅mode=time有效
minMinute: {
type: Number,
default: () => defProps.datetimePicker.minMinute
},
// 可选的最大分钟,仅mode=time/timesecond有效
// 可选的最大分钟,仅mode=time有效
maxMinute: {
type: Number,
default: () => defProps.datetimePicker.maxMinute
},
// 可选的最小秒,仅mode=timesecond有效
minSecond: {
type: Number,
default: () => defProps.datetimePicker.minSecond
},
// 可选的最大秒,仅mode=timesecond有效
maxSecond: {
type: Number,
default: () => defProps.datetimePicker.maxSecond
},
// 选项过滤函数
filter: {
type: [Function, null],
@@ -176,21 +153,6 @@ export const props = defineMixin({
defaultIndex: {
type: Array,
default: () => defProps.datetimePicker.defaultIndex
},
// 是否页面内展示
pageInline:{
type: Boolean,
default: () => defProps.datetimePicker.pageInline
},
// 蒙层样式样式
maskClass: {
type: String,
defualt: ''
},
// 蒙层样式样式
maskStyle: {
type: String,
defualt: ''
}
}
})
@@ -5,17 +5,20 @@
>
<slot name="trigger" :value="inputValue">
<up-input
:placeholder="placeholder"
:readonly="!!showByClickInput"
border="surround"
v-model="inputValue"
v-bind="inputPropsInner"
:disabled="disabled"
:disabledColor="disabledColor"
></up-input>
<cover-view class="input-cover">
</cover-view>
<div class="input-cover">
</div>
</slot>
</view>
<u-picker
ref="picker"
:show="pageInline || show || (hasInput && showByClickInput)"
:show="show || (hasInput && showByClickInput)"
:popupMode="popupMode"
:closeOnClickOverlay="closeOnClickOverlay"
:columns="columns"
@@ -29,9 +32,6 @@
:cancelColor="cancelColor"
:confirmColor="confirmColor"
:toolbarRightSlot="toolbarRightSlot"
:pageInline="pageInline"
:maskClass="maskClass"
:maskStyle="resolvedMaskStyle"
@close="close"
@cancel="cancel"
@confirm="confirm"
@@ -61,26 +61,24 @@
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import dayjs from './dayjs.esm.min.js';
import dayjs from 'dayjs/esm/index';
import { range, error, padZero } from '../../libs/function/index';
import test from '../../libs/function/test';
/**
* DatetimePicker 时间日期选择器
* @description 此选择器用于时间日期
* @tutorial https://uview-plus.jiangruyi.com/components/datetimePicker.html
* @tutorial https://ijry.github.io/uview-plus/components/datetimePicker.html
* @property {Boolean} show 用于控制选择器的弹出与收起 ( 默认 false )
* @property {Boolean} showToolbar 是否显示顶部的操作栏 ( 默认 true )
* @property {String | Number} modelValue 绑定值
* @property {String} title 顶部标题
* @property {String} mode 展示格式 mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择,mode=datehour为日期小时选择,mode=timesecond为时分秒选择,mode=datetimesecond为日期时分秒选择 ( 默认 ‘datetime )
* @property {String} mode 展示格式 mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择 ( 默认 ‘datetime )
* @property {Number} maxDate 可选的最大时间 默认值为后10年
* @property {Number} minDate 可选的最小时间 默认值为前10年
* @property {Number} minHour 可选的最小小时,仅mode=time/timesecond有效 ( 默认 0 )
* @property {Number} maxHour 可选的最大小时,仅mode=time/timesecond有效 ( 默认 23 )
* @property {Number} minMinute 可选的最小分钟,仅mode=time/timesecond有效 ( 默认 0 )
* @property {Number} maxMinute 可选的最大分钟,仅mode=time/timesecond有效 ( 默认 59 )
* @property {Number} minSecond 可选的最小秒,仅mode=timesecond有效 ( 默认 0 )
* @property {Number} maxSecond 可选的最大秒,仅mode=timesecond有效 ( 默认 59 )
* @property {Number} minHour 可选的最小小时,仅mode=time有效 ( 默认 0 )
* @property {Number} maxHour 可选的最大小时,仅mode=time有效 ( 默认 23 )
* @property {Number} minMinute 可选的最小分钟,仅mode=time有效 ( 默认 0 )
* @property {Number} maxMinute 可选的最大分钟,仅mode=time有效 ( 默认 59 )
* @property {Function} filter 选项过滤函数
* @property {Function} formatter 选项格式化函数
* @property {Boolean} loading 是否显示加载中状态 ( 默认 false )
@@ -113,16 +111,7 @@
},
watch: {
show(newValue, oldValue) {
if (!newValue && this.hasInput) {
this.showByClickInput = false
}
if (newValue) {
// #ifdef VUE3
this.innerValue = this.correctValue(this.modelValue)
// #endif
// #ifdef VUE2
this.innerValue = this.correctValue(this.value)
// #endif
this.updateColumnValue(this.innerValue)
}
},
@@ -145,64 +134,22 @@
computed: {
// 如果以下这些变量发生了变化,意味着需要重新初始化各列的值
propsChange() {
return [this.mode, this.maxDate, this.minDate, this.minHour, this.maxHour, this.minMinute, this.maxMinute, this.minSecond, this.maxSecond, this.filter, this.modelValue]
},
resolvedMaskStyle() {
if (this.upHasProp('maskStyle')) {
return this.maskStyle || ''
}
if (this.upThemeIsDark) {
const topBottomStrong = 'rgba(28, 28, 30, 0.95)'
const topBottomWeak = 'rgba(28, 28, 30, 0.6)'
return `background-image: linear-gradient(180deg, ${topBottomStrong}, ${topBottomWeak}), linear-gradient(0deg, ${topBottomStrong}, ${topBottomWeak});`
}
return ''
},
// input的props
inputPropsInner() {
return {
border: this.inputBorder,
placeholder: this.placeholder,
disabled: this.disabled,
disabledColor: this.disabledColor,
...this.inputProps
}
return [this.mode, this.maxDate, this.minDate, this.minHour, this.maxHour, this.minMinute, this.maxMinute, this.filter, ]
}
},
mounted() {
this.init()
// pageInline模式下picker-view常驻显示,在原生端(Android/HarmonyOS)需要
// 等待原生picker-view组件完成初始化后再重新设置选中值,否则滚动位置可能不生效
if (this.pageInline) {
setTimeout(() => {
this.updateIndexs(this.innerValue)
}, 200)
}
},
// #ifdef VUE3
emits: ['close', 'cancel', 'confirm', 'change', 'update:modelValue'],
// #endif
methods: {
toInt(value, fallback = 0) {
const num = parseInt(value, 10)
return Number.isFinite(num) ? num : fallback
},
// 按列安全读取 picker 值,避免快速滚动时出现越界/空值
safeColumnValue(values, columnIndex, optionIndex, fallback = '') {
const column = Array.isArray(values[columnIndex]) ? values[columnIndex] : []
if (!column.length) return fallback
let index = Number(optionIndex)
if (!Number.isFinite(index)) index = 0
index = range(0, column.length - 1, index)
const value = column[index]
return value == null ? fallback : value
},
getInputValue(newValue) {
if (newValue == '' || !newValue || newValue == undefined) {
this.inputValue = ''
return
}
if (this.mode === 'time' || this.mode === 'timesecond') {
if (this.mode == 'time') {
this.inputValue = newValue
} else {
if (this.format) {
@@ -216,21 +163,12 @@
case 'year-month':
format = 'YYYY-MM'
break;
case 'datehour':
format = 'YYYY-MM-DD HH'
break;
case 'datetime':
format = 'YYYY-MM-DD HH:mm'
break;
case 'datetimesecond':
format = 'YYYY-MM-DD HH:mm:ss'
break;
case 'time':
format = 'HH:mm'
break;
case 'timesecond':
format = 'HH:mm:ss'
break;
default:
break;
}
@@ -256,13 +194,7 @@
},
// 关闭选择器
close() {
if (this.hasInput) {
this.showByClickInput = false
}
if (this.closeOnClickOverlay) {
if (this.hasInput) {
this.showByClickInput = false
}
this.$emit('close')
}
},
@@ -292,13 +224,7 @@
},
//用正则截取输出值,当出现多组数字时,抛出错误
intercept(e,type){
if (e === undefined || e === null) {
return type ? '0000' : '00'
}
let judge = String(e).match(/\d+/g)
if (!judge || judge.length === 0) {
return type ? '0000' : '00'
}
let judge = e.match(/\d+/g)
//判断是否掺杂数字
if(judge.length>1){
error("请勿在过滤或格式化函数时添加数字")
@@ -315,66 +241,30 @@
// 列发生变化时触发
change(e) {
const { indexs, values } = e
const safeValues = Array.isArray(values) ? values : []
let selectValue = ''
if (this.mode === 'time' || this.mode === 'timesecond') {
if(this.mode === 'time') {
// 根据value各列索引,从各列数组中,取出当前时间的选中值
const hourText = this.safeColumnValue(safeValues, 0, indexs[0], padZero(this.minHour))
const minuteText = this.safeColumnValue(safeValues, 1, indexs[1], padZero(this.minMinute))
let hour = this.toInt(this.intercept(hourText), this.minHour)
let minute = this.toInt(this.intercept(minuteText), this.minMinute)
let second = this.minSecond
hour = range(this.minHour, this.maxHour, hour)
minute = range(this.minMinute, this.maxMinute, minute)
selectValue = `${padZero(hour)}:${padZero(minute)}`
if (this.mode === 'timesecond') {
const secondText = this.safeColumnValue(safeValues, 2, indexs[2], padZero(this.minSecond))
second = range(this.minSecond, this.maxSecond, this.toInt(this.intercept(secondText), this.minSecond))
selectValue = `${selectValue}:${padZero(second)}`
}
selectValue = `${this.intercept(values[0][indexs[0]])}:${this.intercept(values[1][indexs[1]])}`
} else {
const validCurrent = dayjs(this.innerValue).isValid() ? dayjs(this.innerValue) : dayjs(this.minDate)
const currentYear = validCurrent.year()
const currentMonth = validCurrent.month() + 1
const currentDate = validCurrent.date()
const currentHour = validCurrent.hour()
const currentMinute = validCurrent.minute()
const currentSecond = validCurrent.second()
const yearText = this.safeColumnValue(safeValues, 0, indexs[0], `${currentYear}`)
const monthText = this.safeColumnValue(safeValues, 1, indexs[1], padZero(currentMonth))
// 将选择的值转为数值,比如'03'转为数值的3,'2019'转为数值的2019
let year = this.toInt(this.intercept(yearText, 'year'), currentYear)
let month = this.toInt(this.intercept(monthText), currentMonth)
let hour = 0, minute = 0, second = 0
month = range(1, 12, month)
const year = parseInt(this.intercept(values[0][indexs[0]],'year'))
const month = parseInt(this.intercept(values[1][indexs[1]]))
let date = parseInt(values[2] ? this.intercept(values[2][indexs[2]]) : 1)
let hour = 0, minute = 0
// 此月份的最大天数
const maxDate = dayjs(`${year}-${month}`).daysInMonth()
const dayText = this.safeColumnValue(safeValues, 2, indexs[2], padZero(Math.min(currentDate, maxDate)))
let date = this.toInt(this.intercept(dayText), Math.min(currentDate, maxDate))
// year-month模式下,date不会出现在列中,设置为1,为了符合后边需要减1的需求
if (this.mode === 'year-month') {
date = 1
}
// 不允许超过maxDate值
date = range(1, maxDate, date)
if (this.mode === 'datehour' || this.mode === 'datetime' || this.mode === 'datetimesecond') {
const hourText = this.safeColumnValue(safeValues, 3, indexs[3], padZero(currentHour))
hour = range(0, 23, this.toInt(this.intercept(hourText), currentHour))
}
if (this.mode === 'datetime' || this.mode === 'datetimesecond') {
const minuteText = this.safeColumnValue(safeValues, 4, indexs[4], padZero(currentMinute))
minute = range(0, 59, this.toInt(this.intercept(minuteText), currentMinute))
}
if (this.mode === 'datetimesecond') {
const secondText = this.safeColumnValue(safeValues, 5, indexs[5], padZero(currentSecond))
second = range(0, 59, this.toInt(this.intercept(secondText), currentSecond))
date = Math.min(maxDate, date)
if (this.mode === 'datetime') {
hour = parseInt(this.intercept(values[3][indexs[3]]))
minute = parseInt(this.intercept(values[4][indexs[4]]))
}
// 转为时间模式
selectValue = Number(new Date(year, month - 1, date, hour, minute, second))
if (!Number.isFinite(selectValue)) {
selectValue = this.correctValue(this.innerValue)
}
selectValue = Number(new Date(year, month - 1, date, hour, minute))
}
// 取出准确的合法值,防止超越边界的情况
selectValue = this.correctValue(selectValue)
@@ -395,44 +285,33 @@
this.innerValue = value
this.updateColumns()
// 延迟执行,等待u-picker组件列数据更新完后再设置选中值索引
// pageInline模式下picker-view常驻显示,需额外等待原生组件滚动到位
this.$nextTick(() => {
setTimeout(() => {
this.updateIndexs(value)
}, this.pageInline ? 100 : 0)
})
}, 0);
},
// 更新索引
updateIndexs(value) {
let values = []
const formatter = this.formatter || this.innerFormatter
if (this.mode === 'time' || this.mode === 'timesecond') {
if (this.mode === 'time') {
// 将time模式的时间用:分隔成数组
const timeArr = value.split(':')
// 使用formatter格式化方法进行管道处理
values = [formatter('hour', timeArr[0]), formatter('minute', timeArr[1])]
if (this.mode === 'timesecond') {
values.push(formatter('second', timeArr[2]))
}
} else {
const date = new Date(value)
values = [
formatter('year', `${dayjs(value).year()}`),
// 月份补0
formatter('month', padZero(dayjs(value).month() + 1))
]
if (this.mode === 'date' || this.mode === 'datehour' || this.mode === 'datetime' || this.mode === 'datetimesecond') {
if (this.mode === 'date') {
// date模式,需要添加天列
values.push(formatter('day', padZero(dayjs(value).date())))
}
if (this.mode === 'datehour' || this.mode === 'datetime' || this.mode === 'datetimesecond') {
values.push(formatter('hour', padZero(dayjs(value).hour())))
}
if (this.mode === 'datetime' || this.mode === 'datetimesecond') {
if (this.mode === 'datetime') {
// 数组的push方法,可以写入多个参数
values.push(formatter('minute', padZero(dayjs(value).minute())))
}
if (this.mode === 'datetimesecond') {
values.push(formatter('second', padZero(dayjs(value).second())))
values.push(formatter('day', padZero(dayjs(value).date())), formatter('hour', padZero(dayjs(value).hour())), formatter('minute', padZero(dayjs(value).minute())))
}
}
@@ -480,34 +359,22 @@
},
// 得出合法的时间
correctValue(value) {
const isDateMode = !['time', 'timesecond'].includes(this.mode)
const isDateMode = this.mode !== 'time'
// if (isDateMode && !test.date(value)) {
if (isDateMode && !dayjs.unix(value).isValid()) {
// 如果是日期类型,但是又没有设置合法的当前时间的话,使用最小时间为当前时间
value = this.minDate
} else if (!isDateMode && !value) {
// 如果是时间类型,而又没有默认值的话,就用最小时间
value = this.mode === 'timesecond'
? `${padZero(this.minHour)}:${padZero(this.minMinute)}:${padZero(this.minSecond)}`
: `${padZero(this.minHour)}:${padZero(this.minMinute)}`
value = `${padZero(this.minHour)}:${padZero(this.minMinute)}`
}
// 时间类型
if (!isDateMode) {
if (String(value).indexOf(':') === -1) return error('时间错误,请传递如12:24的格式')
const timeArr = String(value).split(':')
let hour = timeArr[0]
let minute = timeArr[1]
let second = timeArr[2]
let [hour, minute] = value.split(':')
// 对时间补零,同时控制在最小值和最大值之间
const hourNum = Number(hour)
const minuteNum = Number(minute)
hour = padZero(range(this.minHour, this.maxHour, Number.isNaN(hourNum) ? this.minHour : hourNum))
minute = padZero(range(this.minMinute, this.maxMinute, Number.isNaN(minuteNum) ? this.minMinute : minuteNum))
if (this.mode === 'timesecond') {
const secondNum = Number(second)
second = padZero(range(this.minSecond, this.maxSecond, Number.isNaN(secondNum) ? this.minSecond : secondNum))
return `${hour}:${minute}:${second}`
}
hour = padZero(range(this.minHour, this.maxHour, Number(hour)))
minute = padZero(range(this.minMinute, this.maxMinute, Number(minute)))
return `${ hour }:${ minute }`
} else {
// 如果是日期格式,控制在最小日期和最大日期之间
@@ -518,8 +385,8 @@
},
// 获取每列的最大和最小值
getRanges() {
if (this.mode === 'time' || this.mode === 'timesecond') {
const timeColumns = [
if (this.mode === 'time') {
return [
{
type: 'hour',
range: [this.minHour, this.maxHour],
@@ -527,18 +394,11 @@
{
type: 'minute',
range: [this.minMinute, this.maxMinute],
},
];
}
]
if (this.mode === 'timesecond') {
timeColumns.push({
type: 'second',
range: [this.minSecond, this.maxSecond],
})
}
return timeColumns
}
const { maxYear, maxDate, maxMonth, maxHour, maxMinute, maxSecond } = this.getBoundary('max', this.innerValue);
const { minYear, minDate, minMonth, minHour, minMinute, minSecond } = this.getBoundary('min', this.innerValue);
const { maxYear, maxDate, maxMonth, maxHour, maxMinute, } = this.getBoundary('max', this.innerValue);
const { minYear, minDate, minMonth, minHour, minMinute, } = this.getBoundary('min', this.innerValue);
const result = [
{
type: 'year',
@@ -560,50 +420,28 @@
type: 'minute',
range: [minMinute, maxMinute],
},
{
type: 'second',
range: [minSecond, maxSecond],
},
];
// 兜底:防止边界计算异常导致日列出现空范围(快速滚动场景)
if (result[2] && result[2].type === 'day') {
const start = Number(result[2].range[0])
const end = Number(result[2].range[1])
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) {
const fallbackDays = dayjs(this.innerValue).isValid() ? dayjs(this.innerValue).daysInMonth() : 31
result[2].range = [1, fallbackDays]
}
}
if (this.mode === 'date')
result.splice(3, 3);
if (this.mode === 'datehour')
result.splice(4, 2);
if (this.mode === 'datetime')
result.splice(5, 1);
result.splice(3, 2);
if (this.mode === 'year-month')
result.splice(2, 4);
result.splice(2, 3);
return result;
},
// 根据minDate、maxDate、minHour、maxHour等边界值,判断各列的开始和结束边界值
getBoundary(type, innerValue) {
let value = new Date(innerValue)
if(isNaN(value.getTime())){
value = new Date()
}
const value = new Date(innerValue)
const boundary = new Date(this[`${type}Date`])
const year = dayjs(boundary).year()
let month = 1
let date = 1
let hour = 0
let minute = 0
let second = 0
if (type === 'max') {
month = 12
// 月份的天数
date = dayjs(value).daysInMonth()
hour = 23
minute = 59
second = 59
}
// 获取边界值,逻辑是:当年达到了边界值(最大或最小年),就检查月允许的最大和最小值,以此类推
if (dayjs(value).year() === year) {
@@ -614,9 +452,6 @@
hour = dayjs(boundary).hour()
if (dayjs(value).hour() === hour) {
minute = dayjs(boundary).minute()
if (dayjs(value).minute() === minute) {
second = dayjs(boundary).second()
}
}
}
}
@@ -626,8 +461,7 @@
[`${type}Month`]: month,
[`${type}Date`]: date,
[`${type}Hour`]: hour,
[`${type}Minute`]: minute,
[`${type}Second`]: second
[`${type}Minute`]: minute
}
},
onShowByClickInput(){
@@ -641,6 +475,7 @@
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-datetime-picker {
flex: 1;
&__has-input {
@@ -662,7 +497,7 @@
flex-direction: column;
justify-content: center;
border-radius: 4px;
border: 1px solid var(--up-border-color, #eee);
border: 1px solid #eee;
padding: 0 10px;
}
}

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