feat:转账插件

This commit is contained in:
2026-07-23 14:31:06 +08:00
parent 4916546322
commit d5a6a16ea5
560 changed files with 71777 additions and 44216 deletions
@@ -3,27 +3,95 @@
"displayName": "HashMall 前台客服",
"version": "0.1.0",
"description": "内嵌于 HashMall App、H5 和微信小程序的客服聊天插件",
"keywords": ["客服", "聊天", "WebSocket", "uni-app"],
"keywords": [
"客服",
"聊天",
"WebSocket",
"uni-app"
],
"engines": {
"HBuilderX": "^4.0.0"
"HBuilderX": "^4.0.0",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"category": ["前端组件", "通用组件"],
"sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } },
"contact": { "qq": "" },
"declaration": { "ads": "无", "data": "插件使用商城现有用户登录态和客服消息接口", "permissions": "相册、相机(仅在用户主动发送图片时)" }
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件使用商城现有用户登录态和客服消息接口",
"permissions": "相册、相机(仅在用户主动发送图片时)"
},
"type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": { "tcb": "u", "aliyun": "u" },
"cloud": {
"tcb": "-",
"aliyun": "-"
},
"client": {
"App": { "app-vue": "y", "app-nvue": "u" },
"H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" },
"H5-pc": { "Chrome": "y", "IE": "u", "Edge": "y", "Firefox": "y", "Safari": "y" },
"小程序": { "微信": "y", "阿里": "u", "百度": "u", "字节跳动": "u", "QQ": "u", "京东": "u" },
"Vue": { "vue2": "u", "vue3": "y" }
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
+56 -39
View File
@@ -5,16 +5,14 @@
"description": "no-data",
"keywords": [
"no-data"
],
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
"HBuilderX": "^3.1.0",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
@@ -31,48 +29,67 @@
"data": "",
"permissions": ""
},
"npmurl": ""
"npmurl": "",
"type": "component-vue",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u"
"tcb": "-",
"aliyun": "-"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
@@ -1,8 +0,0 @@
## 1.0.3(2024-10-18)
修复4.25版引起的插件回调只触发一次的问题。
## 1.0.2(2024-09-05)
修复uni.chooseImage或者其他部分情况下弹窗不显示的bug。
## 1.0.1(2024-05-30)
修复云打包可能报错的bug
## 1.0.0(2024-03-09)
支持全局监听权限申请。当申请权限时,会在页面顶部显示申请权限的目的。
@@ -1,115 +0,0 @@
{
"id": "uni-registerRequestPermissionTips",
"displayName": "uni-registerRequestPermissionTips",
"version": "1.0.3",
"description": "支持android平台全局监听权限的申请。当申请权限时,会在页面顶部显示申请权限的目的。主要解决上架华为应用市场审核要求:APP在调用终端权限时,应同步告知用户申请该权限的目的。",
"keywords": [
"权限",
"权限申请",
"上架",
"华为"
],
"repository": "",
"engines": {
"HBuilderX": "^4.0"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
],
"uni-ext-api": {
"uni": {
"registerRequestPermissionTipsListener": {
"name": "registerRequestPermissionTipsListener",
"app": {
"js": false,
"kotlin": true,
"swift": false
}
},
"unregisterRequestPermissionTipsListener": {
"name": "unregisterRequestPermissionTipsListener",
"app": {
"js": false,
"kotlin": true,
"swift": false
}
},
"setRequestPermissionTips": {
"name": "setRequestPermissionTips",
"app": {
"js": false,
"kotlin": true,
"swift": false
}
}
}
},
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-android": "y",
"app-ios": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "n",
"Android Browser": "n",
"微信浏览器(Android)": "n",
"QQ浏览器(Android)": "n"
},
"H5-pc": {
"Chrome": "n",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}
@@ -1,95 +0,0 @@
## registerRequestPermissionTipsListener(listener?)
注册权限监听事件
## unregisterRequestPermissionTipsListener(listener?)
取消注册权限监听事件
## RequestPermissionTipsListener的属性值
|名称 |类型 |描述 |必填 |
|:-- |:-- |:-- |:-- |
|onRequest |(permissions:Array<string>)=>void |申请系统权限回调,permissions为触发权限申请的所有权限 |否 |
|onConfirm |(permissions:Array<string>)=>void |弹出系统权限授权框回调,permissions为触发弹出权限授权框的所有权限 |否 |
|onComplete |(permissions:UTSJSONObject)=>void |权限申请完成回调,permissions包括权限及权限的状态。`grant`为权限已获取,`denied`为权限已拒绝 |否 |
## setRequestPermissionTips(UTSJSONObject)
设置权限监听的说明。支持针对权限设置具体的说明。
参考:`{"android.permission.CAMERA":"<p>相机权限申请说明</p>"}`
安卓权限列表可参考[谷歌官方文档](https://developer.android.com/reference/android/Manifest.permission)。
权限申请说明基于原生TextView实现,可以实现加载html内容,支持的标签及属性可参考:
```
<b> 或 <strong>:加粗文本。
<i> 或 <em>:斜体文本。
<u>:下划线文本。
<sup>:上标文本。
<sub>:下标文本。
<tt>:等宽字体文本。
<big>:放大字体。
<small>:缩小字体。
<strike> 或 <s> 或 <del>:带有删除线的文本。
<p>:段落。
<div>:块级容器。
<h1>,<h2>,<h3>,<h4>,<h5>,<h6>:区域标题元素。
<ul>, <ol>, <li>:无序列表和有序列表。
<br>:换行。
<font color="..."> 和 <font size="...">:设置文本颜色和大小。
```
## 示例
```
<script>
import {
registerRequestPermissionTipsListener,
unregisterRequestPermissionTipsListener,
setRequestPermissionTips
} from "@/uni_modules/uni-registerRequestPermissionTips"
var PermissionTips = {
"android.permission.CAMERA": "<h4 style=\"font-size:40px;\">正在读取通讯录权限</h4><font color=#cccccc>通讯录权限不会获取任何信息,请注意</font>",
"android.permission.READ_PHONE_STATE": "<h4 style=\"font-size:40px;\">正在读取网络状态权限</h4><font color=#cccccc>通讯录权限不会获取任何信息,请注意通讯录权限不会获取任何信息,请注意通讯录权限不会获取任何信息,请注意</font>"
}
export default {
onLaunch: function() {
var brand = uni.getSystemInfoSync().deviceBrand
setRequestPermissionTips(PermissionTips)
registerRequestPermissionTipsListener({
onRequest: (e) => {
console.log(e)
},
onConfirm: (e) => {
console.log(e)
},
onComplete: (e) => {
// 华为手机在权限禁止之后,再次申请权限不会出现权限申请框。此时应该引导用户去系统设置开启此权限,不应该频繁申请。
if (brand.toLowerCase() == "huawei") {
var tips = {}
var hasDeniedPermission = false
for (var k in PermissionTips) {
if (e[k] != "denied") {
tips[k] = PermissionTips[k]
} else {
hasDeniedPermission = true
}
}
setRequestPermissionTips(tips) // 更新弹框提醒,防止华为手机不出现权限申请框时权限提醒框闪烁的情况
if (hasDeniedPermission)
uni.showModal({
content: "权限已经被拒绝,请前往设置中开启"
})
}
}
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
},
onExit: function() {
unregisterRequestPermissionTipsListener(null)
}
}
</script>
```
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="io.dcloud.uts.permissionrequest">
</manifest>
@@ -1,3 +0,0 @@
{
"minSdkVersion": "21"
}
@@ -1,148 +0,0 @@
import { UnregisterRequestPermissionTipsListener, RegisterRequestPermissionTipsListener, RequestPermissionTipsListener, SetRequestPermissionTips } from "../interface";
import RelativeLayout from 'android.widget.RelativeLayout';
import LinearLayout from 'android.widget.LinearLayout';
import Color from 'android.graphics.Color';
import TextView from 'android.widget.TextView';
import ViewGroup from 'android.view.ViewGroup';
import Activity from 'android.app.Activity';
import HashMap from 'java.util.HashMap';
import AnimationUtils from 'android.view.animation.AnimationUtils';
import R from 'io.dcloud.uts.permissionrequest.R'
import Html from 'android.text.Html';
import View from 'android.view.View';
import Runnable from "java.lang.Runnable"
let PermissionTipsView : View | null = null
let permissionTips : HashMap<String, String> = new HashMap<String, String>()
var permissionListener : RequestPermissionListener | null = null
var listener : RequestPermissionTipsListener | null = null
@UTSJS.keepAlive
export function unregisterRequestPermissionTipsListener(e : RequestPermissionTipsListener | null) {
listener = null;
if (permissionListener != null) {
permissionListener!.stop()
permissionListener = null
}
if (PermissionTipsView != null) {
if (PermissionTipsView!.getParent() != null) {
PermissionTipsView!.setAnimation(null);
((PermissionTipsView!.getParent()) as ViewGroup).removeView(PermissionTipsView)
}
PermissionTipsView = null
}
}
@UTSJS.keepAlive
export function registerRequestPermissionTipsListener(l : RequestPermissionTipsListener | null) {
listener = l
if (permissionListener == null) {
permissionListener = uni.createRequestPermissionListener()
permissionListener!.onRequest((permissions : Array<string>) => {
if (listener != null)
listener!.onRequest?.invoke(permissions)
})
permissionListener!.onConfirm((permissions : Array<string>) => {
UTSAndroid.getUniActivity()!.runOnUiThread(new ConfirmRunnable(permissions))
})
permissionListener!.onComplete((permissions : Array<string>) => {
UTSAndroid.getUniActivity()!.runOnUiThread(new CompleteRunnable(permissions))
})
}
}
class ConfirmRunnable implements Runnable {
permissions : Array<string>
constructor(permissions : Array<string>) {
this.permissions = permissions
}
override run() {
let activity = UTSAndroid.getUniActivity()!
if (PermissionTipsView != null && PermissionTipsView!.getParent() != null) {
PermissionTipsView!.setAnimation(null);
((PermissionTipsView!.getParent()) as ViewGroup).removeView(PermissionTipsView)
}
if (this.permissions.length > 0) {
try {
PermissionTipsView = createPermissionWindow(activity, this.permissions);
if (PermissionTipsView != null) {
(activity.findViewById(android.R.id.content) as ViewGroup).addView(PermissionTipsView!)
}
} catch (e) {
console.log(e)
}
}
if (listener != null)
listener!.onConfirm?.invoke(this.permissions)
}
}
class CompleteRunnable implements Runnable {
permissions : Array<string>
constructor(permissions : Array<string>) {
this.permissions = permissions
}
override run() {
let activity = UTSAndroid.getUniActivity()!
if (PermissionTipsView != null) {
PermissionTipsView!.setAnimation(AnimationUtils.loadAnimation(activity, R.anim.popupwindow_exit));
((PermissionTipsView!.getParent()) as ViewGroup).removeView(PermissionTipsView!)
PermissionTipsView = null
}
if (listener != null) {
var permissionStatus = {}
for (var p in this.permissions) {
permissionStatus[p] = UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, [p]) ? "grant" : "denied"
}
listener!.onComplete?.invoke(permissionStatus)
}
}
}
export const setRequestPermissionTips : SetRequestPermissionTips = (tips : UTSJSONObject) => {
permissionTips.clear()
for (var k in tips) {
permissionTips.put(k, tips[k] != null ? tips[k].toString() : "")
}
}
function createPermissionWindow(activity : Activity, permissions : Array<string>) : ViewGroup | null {
let rootView = new RelativeLayout(activity);
rootView.setBackgroundColor(Color.TRANSPARENT);
let backgroundView = new LinearLayout(activity);
backgroundView.setPadding(30, 0, 30, 30);
backgroundView.setOrientation(1)
backgroundView.setBackgroundResource(R.drawable.dcloud_permission_background);
let permissionTipsList : Array<string> = new Array<string>()
for (var p in permissions) {
if (permissionTips.containsKey(p) && permissionTipsList.indexOf(permissionTips.get(p)) == -1) {
permissionTipsList.push(permissionTips.get(p)!)
}
}
for (var p in permissionTipsList) {
let text = new TextView(activity);
text.setText(Html.fromHtml(p, Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING))
text.setPadding(0, 30, 0, 0)
text.setTextSize((5 * getScale()).toFloat())
text.setTextColor(Color.BLACK)
backgroundView.addView(text)
}
if (backgroundView.getChildCount() == 0) {
return null;
}
let rll = new RelativeLayout.LayoutParams(-1, -2)
rll.topMargin = (UTSAndroid.getStatusBarHeight() * getScale()).toInt();
rll.leftMargin = 30;
rll.rightMargin = 30;
rll.bottomMargin = 30;
rootView.addView(backgroundView, rll)
rootView.setAnimation(AnimationUtils.loadAnimation(activity, R.anim.popupwindow_enter));
return rootView;
}
function getScale() : Float {
if (UTSAndroid.getUniActivity() != null) {
return UTSAndroid.getUniActivity()!.resources.displayMetrics.scaledDensity
}
return (0 as number).toFloat();
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromYDelta="-100%"
android:toYDelta="0" >
</translate>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromYDelta="0"
android:toYDelta="-100%" />
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<corners android:radius="8dp" />
<!-- <stroke android:color="@color/black" android:width="2dp"/>-->
<solid android:color="#ffffff"/>
</shape>
@@ -1,19 +0,0 @@
export type RequestPermissionTipsListener = {
onRequest ?: ((permissions : Array<string>) => void) | null,
onConfirm ?: ((permission : Array<string>) => void) | null,
onComplete ?: ((permissions : UTSJSONObject) => void) | null
}
export type RegisterRequestPermissionTipsListener = (listener : RequestPermissionTipsListener | null) => void
export type UnregisterRequestPermissionTipsListener = (listener : RequestPermissionTipsListener | null) => void
export type SetRequestPermissionTips = (tips : UTSJSONObject) => void
export interface Uni {
registerRequestPermissionTipsListener : RegisterRequestPermissionTipsListener,
unregisterRequestPermissionTipsListener : UnregisterRequestPermissionTipsListener
setRequestPermissionTips : SetRequestPermissionTips
}
@@ -0,0 +1,5 @@
## 1.1.1(2026-07-11)
修复 uni-requestMerchantTransfer 商家转账回调解析细节问题
## 1.1.0(2026-07-10)
新增 harmony 平台支持
@@ -0,0 +1,130 @@
{
"id": "uni-requestMerchantTransfer",
"displayName": "uni-requestMerchantTransfer",
"version": "1.1.1",
"description": "商家转账用户确认模式下,拉起页面请求用户确认收款Transfer",
"keywords": [
"确认收款",
"免确认收款授权"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8",
"uni-app": "^4.61",
"uni-app-x": "^4.61"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"uni-ext-api": {
"uni": {
"requestMerchantTransfer": {
"name": "requestMerchantTransfer",
"app": {
"js": false,
"kotlin": true,
"swift": true,
"arkts": true
}
}
}
},
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "x",
"vue3": "√"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": {
"extVersion": "1.1.0",
"minVersion": "34"
},
"ios": {
"extVersion": "1.1.0",
"minVersion": "12"
},
"harmony": {
"extVersion": "1.1.0",
"minVersion": ""
}
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": {
"extVersion": "1.1.0",
"minVersion": "34"
},
"ios": {
"extVersion": "1.1.0",
"minVersion": "12"
},
"harmony": {
"extVersion": "1.1.0",
"minVersion": ""
}
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
# uni-requestMerchantTransfer
实现打开微信请求用户确认收款功能。
[API规范文档](https://doc.dcloud.net.cn/uni-app-x/api/request-merchant-transfer.html)
## uts 语言介绍
uts,全称 uni type script,是一门跨平台的、高性能的、强类型的现代编程语言。
它可以被编译为不同平台的编程语言,如:
Android平台:编译为Kotlin
iOS平台:编译Swift
鸿蒙OS平台:编译为ArkTS
web平台/小程序:编译为JavaScript
uts 采用了与 ts 基本一致的语法规范,支持绝大部分 ES6 API。
但为了跨端,uts进行了一些约束和特定平台的增补。
过去在js引擎下运行支持的语法,大部分在uts的处理下也可以平滑的在kotlin和swift中使用。但有一些无法抹平,需要使用条件编译。
和uni-app的条件编译类似,uts也支持条件编译。写在条件编译里的,可以调用平台特有的扩展语法。
## uts 插件介绍
UTS 插件是一种特定的 uni_modules 插件,其核心目的是允许 uni-app/uni-app x 开发者使用 UTS 语法来调用扩展 API(封装原生系统的API或三方SDK)。
UTS 插件的实现代码主要位于 utssdk 目录下,并按平台进行分离和组织:
| 目录/文件 | 目标平台 | 实现语言 | 作用描述 |
| -- | -- | -- | -- |
|utssdk/app-android|Android|UTS, Kotlin, Java|存放 UTS 插件在 Android 平台上的具体实现源码|
|utssdk/app-ios|iOS|UTS, Swift|存放 UTS 插件在 iOS 平台上的具体实现源码|
|utssdk/app-harmony|HarmonyOS (鸿蒙)|UTS, ArkTS|存放 UTS 插件在 HarmonyOS 平台上的具体实现源码|
|utssdk/*.uts|多平台共用|UTS|存放使用 UTS 语言编写的、可供 所有平台 共用的实现源码|
## 参考文档
- [uts 语言介绍](https://doc.dcloud.net.cn/uni-app-x/uts/)
- [uts 和 ts 的差异](https://doc.dcloud.net.cn/uni-app-x/uts/uts_diff_ts.html)
- [uts 插件开发文档](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin.html)
- [uts 插件原生语言混编开发文档](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin-hybrid.html)
- [uts 插件 Android 平台开发注意事项](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-for-android.html)
- [uts 插件 iOS 平台开发注意事项](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-for-ios.html)
- [uts 插件 HarmonyOS 平台开发注意事项](https://doc.dcloud.net.cn/uni-app-x/plugin/uts-for-ios.html)
@@ -0,0 +1,9 @@
{
"minSdkVersion": "21",
"dependencies": [
"com.tencent.mm.opensdk:wechat-sdk-android:6.8.0"
],
"thirdSDKs": [
"weixin"
]
}
@@ -0,0 +1,59 @@
import { RequestMerchantTransfer, RequestMerchantTransferOptions, RequestMerchantTransferGeneralCallbackResult } from '../interface.uts';
import WXAPIFactory from 'com.tencent.mm.opensdk.openapi.WXAPIFactory'
import URLEncoder from 'java.net.URLEncoder'
import StandardCharsets from 'java.nio.charset.StandardCharsets'
import WXOpenBusinessView from 'com.tencent.mm.opensdk.modelbiz.WXOpenBusinessView'
import Build from 'com.tencent.mm.opensdk.constants.Build'
export const requestMerchantTransfer : RequestMerchantTransfer = function (options : RequestMerchantTransferOptions) {
var appId = options.appId
var api = WXAPIFactory.createWXAPI(UTSAndroid.getAppContext(), appId, false)
var wxSdkVersion = api.wxAppSupportAPI
if (wxSdkVersion >= Build.OPEN_BUSINESS_VIEW_SDK_INT) {
var req = WXOpenBusinessView.Req()
req.businessType = "requestMerchantTransfer"
// 通过 URL 编码处理参数,确保特殊字符不会影响请求
var query = ""
query += 'mchId=' + encodeParams(options.mchId) + '&'
query += 'package=' + encodeParams(options.package) + '&'
if (appId != null) {
query += 'appId=' + encodeParams(options.appId) + '&'
}
if (options.openId != null) {
query += 'openId=' + encodeParams(options.openId) + '&'
}
if (options.subAppId != null) {
query += 'subAppId=' + encodeParams(options.subAppId) + '&'
}
if (options.subMchId != null) {
query += 'subMchId=' + encodeParams(options.subMchId) + '&'
}
query = query.substring(0, query.length - 1)
req.query = query
// 发送请求并检查返回值
var ret = api.sendReq(req)
if (ret) {
var result : RequestMerchantTransferGeneralCallbackResult = {
errMsg: 'ok'
}
options.success?.(result)
options.complete?.(result)
}else{
var result : RequestMerchantTransferGeneralCallbackResult = {
errMsg: 'fail'
}
options.fail?.(result)
options.complete?.(result)
}
} else {
var result : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '当前微信版本过低,请升级微信以使用该功能'
}
options.fail?.(result)
options.complete?.(result)
}
}
function encodeParams(params : string | null) : string | null {
return URLEncoder.encode(params, StandardCharsets.UTF_8.toString())
}
@@ -0,0 +1,5 @@
{
"dependencies": {
"@tencent/wechat_open_sdk": "1.0.15"
}
}
@@ -0,0 +1,120 @@
import { RequestMerchantTransfer, RequestMerchantTransferGeneralCallbackResult, RequestMerchantTransferOptions } from '../interface.uts'
import {
handleWechatMerchantTransferWant,
requestWechatMerchantTransfer,
WechatMerchantTransferNativeResult
} from './wechatMerchantTransfer.ets'
import {
REQUEST_MERCHANT_TRANSFER_APP_ID_REQUIRED,
REQUEST_MERCHANT_TRANSFER_BUSINESS_FAILED,
REQUEST_MERCHANT_TRANSFER_CANCEL,
REQUEST_MERCHANT_TRANSFER_MCH_ID_REQUIRED,
REQUEST_MERCHANT_TRANSFER_OK,
REQUEST_MERCHANT_TRANSFER_OPEN_SDK_ERROR,
REQUEST_MERCHANT_TRANSFER_PACKAGE_REQUIRED,
REQUEST_MERCHANT_TRANSFER_PARSE_RESPONSE_ERROR,
REQUEST_MERCHANT_TRANSFER_WECHAT_NOT_INSTALLED,
REQUEST_MERCHANT_TRANSFER_WECHAT_VERSION_LOW
} from '../unierror.uts'
let isWechatMerchantTransferHandlerRegistered = false
function ensureWechatMerchantTransferHandlerRegistered() : void {
if (isWechatMerchantTransferHandlerRegistered) {
return
}
isWechatMerchantTransferHandlerRegistered = true
UTSHarmony.onAppAbilityCreate((want, _launchParam) => {
handleWechatMerchantTransferWant(want)
})
UTSHarmony.onAppAbilityNewWant((want, _launchParam) => {
handleWechatMerchantTransferWant(want)
})
}
function invokeCallback(options : RequestMerchantTransferOptions, errMsg : string, isSuccess : boolean) : void {
const result : RequestMerchantTransferGeneralCallbackResult = {
errMsg: errMsg
}
if (isSuccess) {
options.success?.(result)
} else {
options.fail?.(result)
}
options.complete?.(result)
}
function isEmptyString(value : string | null) : boolean {
return value == null || value.length == 0
}
function appendQueryParam(query : string, key : string, value : string) : string {
const param = key + '=' + encodeURIComponent(value)
if (query.length > 0) {
return query + '&' + param
}
return param
}
function getValidateErrorMsg(options : RequestMerchantTransferOptions) : string {
if (isEmptyString(options.mchId)) {
return REQUEST_MERCHANT_TRANSFER_MCH_ID_REQUIRED.errMsg
}
if (isEmptyString(options.package)) {
return REQUEST_MERCHANT_TRANSFER_PACKAGE_REQUIRED.errMsg
}
if (isEmptyString(options.appId)) {
return REQUEST_MERCHANT_TRANSFER_APP_ID_REQUIRED.errMsg
}
return ''
}
function buildRequestMerchantTransferQuery(options : RequestMerchantTransferOptions, appId : string) : string {
let query = ''
// 微信官方文档上只有下方3个参数,没有 openId、subAppId、subMchId
// 相关文档:
// 用户确认收款:https://pay.weixin.qq.com/doc/v3/merchant/4019670774
// 免确认收款授权:https://pay.weixin.qq.com/doc/v3/merchant/4020320137
query = appendQueryParam(query, 'mchId', options.mchId)
query = appendQueryParam(query, 'package', options.package)
query = appendQueryParam(query, 'appId', appId)
return query
}
function handleNativeResult(options : RequestMerchantTransferOptions, nativeResult : WechatMerchantTransferNativeResult) : void {
if (nativeResult.result == 'success') {
invokeCallback(options, REQUEST_MERCHANT_TRANSFER_OK, true)
return
}
let errMsg = REQUEST_MERCHANT_TRANSFER_BUSINESS_FAILED.errMsg
if (nativeResult.result == 'cancel') {
errMsg = REQUEST_MERCHANT_TRANSFER_CANCEL.errMsg
} else if (nativeResult.result == 'wechat_not_installed') {
errMsg = REQUEST_MERCHANT_TRANSFER_WECHAT_NOT_INSTALLED.errMsg
} else if (nativeResult.result == 'wechat_version_low') {
errMsg = REQUEST_MERCHANT_TRANSFER_WECHAT_VERSION_LOW.errMsg
} else if (nativeResult.result == 'parse_response_error') {
errMsg = REQUEST_MERCHANT_TRANSFER_PARSE_RESPONSE_ERROR.errMsg
} else if (nativeResult.result == 'open_sdk_error' || nativeResult.result == 'timeout' || nativeResult.result == 'request_interrupted') {
errMsg = REQUEST_MERCHANT_TRANSFER_OPEN_SDK_ERROR.errMsg
}
invokeCallback(options, errMsg, false)
}
export const requestMerchantTransfer : RequestMerchantTransfer = function (options : RequestMerchantTransferOptions) {
const validateErrorMsg = getValidateErrorMsg(options)
if (validateErrorMsg.length > 0) {
invokeCallback(options, validateErrorMsg, false)
return
}
ensureWechatMerchantTransferHandlerRegistered()
const appId : string = options.appId ?? ''
const query = buildRequestMerchantTransferQuery(options, appId)
requestWechatMerchantTransfer(appId, query).then((nativeResult : WechatMerchantTransferNativeResult) => {
handleNativeResult(options, nativeResult)
}).catch((_error : Error) => {
invokeCallback(options, REQUEST_MERCHANT_TRANSFER_OPEN_SDK_ERROR.errMsg, false)
})
}
@@ -0,0 +1,161 @@
import { common, Want } from '@kit.AbilityKit';
import * as wxopensdk from '@tencent/wechat_open_sdk';
export class WechatMerchantTransferNativeResult {
result: string = '';
errMsg: string = '';
extMsg: string = '';
constructor(result: string, errMsg: string, extMsg: string) {
this.result = result;
this.errMsg = errMsg;
this.extMsg = extMsg;
}
}
const MERCHANT_TRANSFER_BUSINESS_TYPE: string = 'requestMerchantTransfer';
const REQUEST_TIMEOUT_MS: number = 120000;
let currentAppId: string = '';
let currentWxApi: wxopensdk.WXApi | null = null;
let pendingResolve: ((result: WechatMerchantTransferNativeResult) => void) | null = null;
let timeoutId: number = -1;
class MerchantTransferRespPayload {
result: string | null = null;
}
function buildResult(result: string, errMsg: string, extMsg: string): WechatMerchantTransferNativeResult {
return new WechatMerchantTransferNativeResult(result, errMsg, extMsg);
}
function getWxApi(appId: string): wxopensdk.WXApi {
let wxApi = currentWxApi;
if (wxApi == null || currentAppId != appId) {
currentAppId = appId;
wxApi = wxopensdk.WXAPIFactory.createWXAPI(appId);
currentWxApi = wxApi;
}
return wxApi;
}
function clearPendingTimeout(): void {
if (timeoutId != -1) {
clearTimeout(timeoutId);
timeoutId = -1;
}
}
function finishPending(result: WechatMerchantTransferNativeResult): void {
const resolve = pendingResolve;
pendingResolve = null;
clearPendingTimeout();
if (resolve != null) {
resolve(result);
}
}
function parseMerchantTransferResp(extMsg: string): WechatMerchantTransferNativeResult {
if (extMsg.length == 0) {
return buildResult('parse_response_error', 'parse response failed', extMsg);
}
try {
const payload = JSON.parse(extMsg) as MerchantTransferRespPayload;
const result: string = payload.result ?? '';
if (result == 'success') {
return buildResult('success', '', extMsg);
}
if (result == 'fail') {
return buildResult('fail', 'business failed', extMsg);
}
if (result == 'cancel') {
return buildResult('cancel', 'business canceled', extMsg);
}
return buildResult('business_failed', 'unknown business result', extMsg);
} catch (_error) {
return buildResult('parse_response_error', 'parse response failed', extMsg);
}
}
class WechatMerchantTransferEventHandler implements wxopensdk.WXApiEventHandler {
onResp(resp: wxopensdk.BaseResp): void {
if (resp instanceof wxopensdk.OpenBusinessViewResp) {
if ("requestMerchantTransfer" === resp.businessType) {
if (resp.errCode == wxopensdk.ErrCode.ERR_UNSUPPORTED) {
finishPending(buildResult('wechat_version_low', resp.errStr ?? 'wechat version is too low', ''));
return;
}
const businessType: string = resp.businessType ?? '';
if (businessType != MERCHANT_TRANSFER_BUSINESS_TYPE) {
return;
}
const extMsg: string = resp.extMsg ?? '';
if (resp.errCode == wxopensdk.ErrCode.ERR_USER_CANCEL && extMsg.length == 0) {
finishPending(buildResult('cancel', resp.errStr ?? 'business canceled', ''));
return;
}
finishPending(parseMerchantTransferResp(extMsg));
}
}
}
onReq(_req: wxopensdk.BaseReq): void {
}
}
const wxEventHandler = new WechatMerchantTransferEventHandler();
export async function requestWechatMerchantTransfer(appId: string, query: string): Promise<WechatMerchantTransferNativeResult> {
if (appId == '') {
return buildResult('invalid_app_id', 'appId is empty', '');
}
if (query == '') {
return buildResult('invalid_query', 'query is empty', '');
}
const wxApi: wxopensdk.WXApi = getWxApi(appId);
if (!wxApi.isWXAppInstalled()) {
return buildResult('wechat_not_installed', 'wechat is not installed', '');
}
const req = new wxopensdk.OpenBusinessViewReq();
req.businessType = MERCHANT_TRANSFER_BUSINESS_TYPE;
req.query = query;
const context = getContext() as common.UIAbilityContext;
const responsePromise = new Promise<WechatMerchantTransferNativeResult>((resolve) => {
if (pendingResolve != null) {
finishPending(buildResult('request_interrupted', 'another merchant transfer request started', ''));
}
pendingResolve = resolve;
timeoutId = setTimeout(() => {
finishPending(buildResult('timeout', 'wait wechat response timeout', ''));
}, REQUEST_TIMEOUT_MS);
});
try {
const sendResult: boolean = await wxApi.sendReq(context, req);
if (!sendResult) {
finishPending(buildResult('open_sdk_error', 'sendReq returned false', ''));
}
} catch (_error) {
finishPending(buildResult('open_sdk_error', 'sendReq failed', ''));
}
return responsePromise;
}
export function handleWechatMerchantTransferWant(want: Want): void {
const wxApi = currentWxApi;
if (wxApi == null || currentAppId.length == 0) {
return;
}
wxApi.handleWant(want, wxEventHandler);
}
@@ -0,0 +1,167 @@
重要!
SDK2.0.5
1. 支持模块化集成​:XCFramework 头文件引用改为标准化格式 #import <WechatOpenSDK/WXApi.h>,解决路径冲突并支持 Swift/ObjC 混合开发
2. 修复openWXApp偶现失败的问题
SDK2.0.4
1.增加privacy manifest文件
2.修复跳微信时可能卡顿的问题
SDK2.0.2
1. 优化XCFramework打包方式
SDK2.0.1
1. SDK支持XCFramework
SDK2.0.0
1. 分享能力支持内容防篡改校验
SDK1.9.9
1. 授权登录支持关闭自动授权
2. 分享支持添加签名,防止篡改
SDK1.9.7
1. 适配CocoaPods
SDK1.9.6
1. 适配iOS 16,减少读写剪切板
SDK1.9.4
1. 修复授权登录取消/拒绝时state字段没有带回
SDK1.9.3
1. 新增发起二维码支付能力
SDK1.9.2
1. 新增发起企微客服会话能力
SDK1.9.1
1. 音乐视频分享类型增加运营H5字段
SDK1.8.9
1. 增加音乐视频分享类型
SDK1.8.8
1. 增加游戏直播消息类型
SDK1.8.7.1
1. 修复Xcode11以下编译不通过
SDK1.8.7
1. 修复iPadOS,未安装微信的情况下,因为UA问题无法授权登录
2. 修复未安装微信的情况下, 适配了UIScene的App因为UIAlertView Crash
3. 增加Universal Link检测函数
SDK1.8.6.2
1. 修改包含"UIWebView"字符的类名
SDK1.8.6.1
1.短信授权登录使用的UIWebview切换成WKWebview
SDK1.8.6
1. 支持Universal Link拉起微信以及返回App
2. SDK移除MTA库
SDK1.8.5
1. 更换MTA库:取消对剪切板的访问, 防止和其他SDK竞争导致crash
2. NSMutableArray的MTA分类方法改名,减少命名冲突
3. 不含支付功能版本移除非税支付和医保支付接口
4. 分享音乐支持填写歌词和高清封面图
SDK1.8.4
1. 调整分享图片大小限制
2. 新增openBusinessView接口
SDK1.8.3
1. SDK增加调起微信刷卡支付接口
2. SDK增加小程序订阅消息接口
3. 修复小程序订阅消息接口没有resp的问题
SDK1.8.2
1. SDK增加开发票授权 WXInvoiceAuthInsert
2. SDK增加非税接口 WXNontaxPay
3. SDK增加医保接口 WXPayInsurance
4. 更换MTA库
SDK1.8.1
1. SDK打开小程序支持指定版本(体验,开发,正式版)
2. SDK分享小程序支持指定版本(体验,开发,正式版)
3. SDK支持输出log日志
SDK1.8.0
1. SDK支持打开小程序
2. SDK分享小程序支持shareTicket
SDK1.7.9
1. SDK订阅一次性消息
SDK1.7.8
1 SDK分享小程序支持大图
SDK1.7.7
1 增加SDK分享小程序
2 增加选择发票接口
SDK1.7.6
1. 提高稳定性
1 修复mta崩溃
2 新增接口支持开发者关闭mta数据统计上报
SDK1.7.5
1. 提高稳定性
2. 加快registerApp接口启动速度
SDK1.7.4
1. 更新支持iOS启用 ATS(App Transport Security)
2. 需要在工程中链接CFNetwork.framework
3. 在工程配置中的”Other Linker Flags”中加入”-Objc -all_load”
SDK1.7.3
1. 增强稳定性,适配iOS10
2. 修复小于32K的jpg格式缩略图设置失败的问题
SDK1.7.2
1. 修复因CTTeleponyNetworkInfo引起的崩溃问题
SDK1.7.1
1. 支持兼容ipv6(提升稳定性)
2. xCode Version 7.3.1 (7D1014) 编译
SDK1.7
1. 支持兼容ipv6
2. 修复若干问题增强稳定性
SDK1.6.3
1. xCode7.2 构建的sdk包。
2. 请使用xCode7.2进行编译。
3. 需要在Build Phases中Link Security.framework
4. 修复若干小问题。
SDK1.6.2
1、xCode7.1 构建的sdk包
2、请使用xCode7.1进行编译
SDK1.6.1
1、修复armv7s下,bitcode可能编译不过
2、解决warning
SDK1.6
1、iOS 9系统策略更新,限制了http协议的访问,此外应用需要在“Info.plist”中将要使用的URL Schemes列为白名单,才可正常检查其他应用是否安装。
受此影响,当你的应用在iOS 9中需要使用微信SDK的相关能力(分享、收藏、支付、登录等)时,需要在“Info.plist”里增加如下代码:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
2、开发者需要在工程中链接上 CoreTelephony.framework
3、解决bitcode编译不过问题
SDK1.5
1、废弃safeSendReq:接口,使用sendReq:即可。
2、新增+(BOOL) sendAuthReq:(SendAuthReq*) req viewController : (UIViewController*) viewController delegate:(id<WXApiDelegate>) delegate;
支持未安装微信情况下Auth,具体见WXApi.h接口描述
3、微信开放平台新增了微信模块用户统计功能,便于开发者统计微信功能模块的用户使用和活跃情况。开发者需要在工程中链接上:SystemConfiguration.framework,libz.dylib,libsqlite3.0.dylib。
@@ -0,0 +1,231 @@
//
// WXApi.h
// 所有Api接口
//
// Created by Wechat on 12-2-28.
// Copyright (c) 2012年 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "WXApiObject.h"
NS_ASSUME_NONNULL_BEGIN
typedef BOOL(^WXGrantReadPasteBoardPermissionCompletion)(void);
#pragma mark - WXApiDelegate
/*! @brief 接收并处理来自微信终端程序的事件消息
*
* 接收并处理来自微信终端程序的事件消息,期间微信界面会切换到第三方应用程序。
* WXApiDelegate 会在handleOpenURL:delegate:中使用并触发。
*/
@protocol WXApiDelegate <NSObject>
@optional
/*! @brief 收到一个来自微信的请求,第三方应用程序处理完后调用sendResp向微信发送结果
*
* 收到一个来自微信的请求,异步处理完成后必须调用sendResp发送处理结果给微信。
* 可能收到的请求有GetMessageFromWXReq、ShowMessageFromWXReq等。
* @param req 具体请求内容,是自动释放的
*/
- (void)onReq:(BaseReq*)req;
/*! @brief 发送一个sendReq后,收到微信的回应
*
* 收到一个来自微信的处理结果。调用一次sendReq后会收到onResp。
* 可能收到的处理结果有SendMessageToWXResp、SendAuthResp等。
* @param resp具体的回应内容,是自动释放的
*/
- (void)onResp:(BaseResp*)resp;
/* ! @brief 用于在iOS16以及以上系统上,控制OpenSDK是否读取剪切板中微信传递的数据以及读取的时机
* 在iOS16以及以上系统,在SDK需要读取剪切板中微信写入的数据时,会回调该方法。没有实现默认会直接读取微信通过剪切板传递过来的数据
* 注意:
* 1. 只在iOS16以及以上的系统版本上回调;
* 2. 不实现时,OpenSDK会直接调用读取剪切板接口,读取微信传递过来的数据;
* 3. 若实现该方法:开发者需要通过调用completion(), 支持异步,通知SDK允许读取剪切板中微信传递的数据,
* 不调用completion()则代表不授权OpenSDK读取剪切板,会导致收不到onReq:, onResp:回调,无法后续业务流程。请谨慎使用
* 4. 不要长时间持有completion不释放,可能会导致内存泄漏。
*/
- (void)onNeedGrantReadPasteBoardPermissionWithURL:(nonnull NSURL *)openURL completion:(nonnull WXGrantReadPasteBoardPermissionCompletion)completion;
@end
#pragma mark - WXApiLogDelegate
@protocol WXApiLogDelegate <NSObject>
- (void)onLog:(NSString*)log logLevel:(WXLogLevel)level;
@end
#pragma mark - WXApi
/*! @brief 微信Api接口函数类
*
* 该类封装了微信终端SDK的所有接口
*/
@interface WXApi : NSObject
/*! @brief WXApi的成员函数,向微信终端程序注册第三方应用。
*
* 需要在每次启动第三方应用程序时调用。
* @attention 请保证在主线程中调用此函数
* @param appid 微信开发者ID
* @param universalLink 微信开发者Universal Link
* @return 成功返回YES,失败返回NO。
*/
+ (BOOL)registerApp:(NSString *)appid universalLink:(NSString *)universalLink;
/*! @brief 处理旧版微信通过URL启动App时传递的数据
*
* 需要在 application:openURL:sourceApplication:annotation:或者application:handleOpenURL中调用。
* @param url 微信启动第三方应用时传递过来的URL
* @param delegate WXApiDelegate对象,用来接收微信触发的消息。
* @return 成功返回YES,失败返回NO。
*/
+ (BOOL)handleOpenURL:(NSURL *)url delegate:(nullable id<WXApiDelegate>)delegate;
/*! @brief 处理微信通过Universal Link启动App时传递的数据
*
* 需要在 application:continueUserActivity:restorationHandler:中调用。
* @param userActivity 微信启动第三方应用时系统API传递过来的userActivity
* @param delegate WXApiDelegate对象,用来接收微信触发的消息。
* @return 成功返回YES,失败返回NO。
*/
+ (BOOL)handleOpenUniversalLink:(NSUserActivity *)userActivity delegate:(nullable id<WXApiDelegate>)delegate;
/*! @brief 检查微信是否已被用户安装
*
* @return 微信已安装返回YES,未安装返回NO。
*/
+ (BOOL)isWXAppInstalled;
/*! @brief 判断当前微信的版本是否支持OpenApi
*
* @return 支持返回YES,不支持返回NO。
*/
+ (BOOL)isWXAppSupportApi;
/*! @brief 判断当前微信的版本是否支持分享微信状态功能
*
* @attention 需在工程LSApplicationQueriesSchemes配置中添加weixinStateAPI
* @return 支持返回YES,不支持返回NO。
*/
+ (BOOL)isWXAppSupportStateAPI;
#ifndef BUILD_WITHOUT_PAY
/*! @brief 判断当前微信的版本是否支持二维码拉起微信支付
*
* @attention 需在工程LSApplicationQueriesSchemes配置中添加weixinQRCodePayAPI
* @return 支持返回YES,不支持返回NO。
*/
+ (BOOL)isWXAppSupportQRCodePayAPI;
#endif
/*! @brief 获取微信的itunes安装地址
*
* @return 微信的安装地址字符串。
*/
+ (NSString *)getWXAppInstallUrl;
/*! @brief 获取当前微信SDK的版本号
*
* @return 返回当前微信SDK的版本号
*/
+ (NSString *)getApiVersion;
/*! @brief 打开微信
*
* @return 成功返回YES,失败返回NO。
*/
+ (BOOL)openWXApp;
/*! @brief 发送请求到微信,等待微信返回onResp
*
* 函数调用后,会切换到微信的界面。第三方应用程序等待微信返回onResp。微信在异步处理完成后一定会调用onResp。支持以下类型
* SendAuthReq、SendMessageToWXReq、PayReq等。
* @param req 具体的发送请求。
* @param completion 调用结果回调block
*/
+ (void)sendReq:(BaseReq *)req completion:(void (^ __nullable)(BOOL success))completion;
/*! @brief 收到微信onReq的请求,发送对应的应答给微信,并切换到微信界面
*
* 函数调用后,会切换到微信的界面。第三方应用程序收到微信onReq的请求,异步处理该请求,完成后必须调用该函数。可能发送的相应有
* GetMessageFromWXResp、ShowMessageFromWXResp等。
* @param resp 具体的应答内容
* @param completion 调用结果回调block
*/
+ (void)sendResp:(BaseResp*)resp completion:(void (^ __nullable)(BOOL success))completion;
/*! @brief 发送Auth请求到微信,支持用户没安装微信,等待微信返回onResp
*
* 函数调用后,会切换到微信的界面。第三方应用程序等待微信返回onResp。微信在异步处理完成后一定会调用onResp。支持SendAuthReq类型。
* @param req 具体的发送请求。
* @param viewController 当前界面对象。
* @param delegate WXApiDelegate对象,用来接收微信触发的消息。
* @param completion 调用结果回调block
*/
+ (void)sendAuthReq:(SendAuthReq *)req viewController:(UIViewController*)viewController delegate:(nullable id<WXApiDelegate>)delegate completion:(void (^ __nullable)(BOOL success))completion;
/*! @brief 测试函数,用于排查当前App通过Universal Link方式分享到微信的流程
注意1: 调用自检函数之前必须要先调用registerApp:universalLink接口, 并确认调用成功
注意2: 自检过程中会有Log产生,可以先调用startLogByLevel函数,根据Log排查问题
注意3: 会多次回调block
注意4: 仅用于新接入SDK时调试使用,请勿在正式环境的调用
*
* 当completion回调的step为WXULCheckStepFinal时,表示检测通过,Universal Link接入成功
* @param completion 回调Block
*/
+ (void)checkUniversalLinkReady:(nonnull WXCheckULCompletion)completion;
/*! @brief WXApi的成员函数,接受微信的log信息。byBlock
注意1:SDK会强引用这个block,注意不要导致内存泄漏,注意不要导致内存泄漏
注意2:调用过一次startLog by block之后,如果再调用一次任意方式的startLoad,会释放上一次logBlock,不再回调上一个logBlock
*
* @param level 打印log的级别
* @param logBlock 打印log的回调block
*/
+ (void)startLogByLevel:(WXLogLevel)level logBlock:(WXLogBolock)logBlock;
/*! @brief WXApi的成员函数,接受微信的log信息。byDelegate
注意1:sdk会弱引用这个delegate,这里可加任意对象为代理,不需要与WXApiDelegate同一个对象
注意2:调用过一次startLog by delegate之后,再调用一次任意方式的startLoad,不会再回调上一个logDelegate对象
* @param level 打印log的级别
* @param logDelegate 打印log的回调代理,
*/
+ (void)startLogByLevel:(WXLogLevel)level logDelegate:(id<WXApiLogDelegate>)logDelegate;
/*! @brief 停止打印log,会清理block或者delegate为空,释放block
* @param
*/
+ (void)stopLog;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,68 @@
//
// WechatAuthSDK.h
// WechatAuthSDK
//
// Created by 李凯 on 13-11-29.
// Copyright (c) 2013年 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
enum AuthErrCode {
WechatAuth_Err_Ok = 0, //Auth成功
WechatAuth_Err_NormalErr = -1, //普通错误
WechatAuth_Err_NetworkErr = -2, //网络错误
WechatAuth_Err_GetQrcodeFailed = -3, //获取二维码失败
WechatAuth_Err_Cancel = -4, //用户取消授权
WechatAuth_Err_Timeout = -5, //超时
};
@protocol WechatAuthAPIDelegate<NSObject>
@optional
- (void)onAuthGotQrcode:(UIImage *)image; //得到二维码
- (void)onQrcodeScanned; //二维码被扫描
- (void)onAuthFinish:(int)errCode AuthCode:(nullable NSString *)authCode; //成功登录
@end
@interface WechatAuthSDK : NSObject{
NSString *_sdkVersion;
__weak id<WechatAuthAPIDelegate> _delegate;
}
@property(nonatomic, weak, nullable) id<WechatAuthAPIDelegate> delegate;
@property(nonatomic, readonly) NSString *sdkVersion; //authSDK版本号
/*! @brief 发送登录请求,等待WechatAuthAPIDelegate回调
*
* @param appId 微信开发者ID
* @param nonceStr 一个随机的尽量不重复的字符串,用来使得每次的signature不同
* @param timeStamp 时间戳
* @param scope 应用授权作用域,拥有多个作用域用逗号(,)分隔
* @param signature 签名
* @param schemeData 会在扫码后拼在scheme后
* @return 成功返回YES,失败返回NO
注:该实现只保证同时只有一个Auth在运行,Auth未完成或未Stop再次调用Auth接口时会返回NO。
*/
- (BOOL)Auth:(NSString *)appId
nonceStr:(NSString *)nonceStr
timeStamp:(NSString *)timeStamp
scope:(NSString *)scope
signature:(NSString *)signature
schemeData:(nullable NSString *)schemeData;
/*! @brief 暂停登录请求
*
* @return 成功返回YES,失败返回NO。
*/
- (BOOL)StopAuth;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>
@@ -0,0 +1,14 @@
{
"deploymentTarget": "12.0",
"simulatorArchitectures": [
"x86_64"
],
"frameworks": [
"CoreGraphics.framework",
"WebKit.framework",
"Security.framework"
],
"thirdSDKs": [
"weixin"
]
}
@@ -0,0 +1,183 @@
import { UIApplication } from "UIKit";
import { URL, NSUserActivity, Bundle, URLComponents, URLQueryItem, Script } from "Foundation";
import { RequestMerchantTransfer, RequestMerchantTransferOptions, RequestMerchantTransferGeneralCallbackResult } from '../interface.uts';
export const requestMerchantTransfer : RequestMerchantTransfer = function (options : RequestMerchantTransferOptions) {
WxpayManager.requestMerchantTransfer(options)
}
export class WxpayManagerHookProxy implements UTSiOSHookProxy {
// 应用正常启动时 (不包括已在后台转到前台的情况)的回调函数。
applicationDidFinishLaunchingWithOptions(application : UIApplication | null, launchOptions : Map<UIApplication.LaunchOptionsKey, any> | null = null) : boolean {
WxpayManager.registerApp()
return false
}
// 通过 url scheme 方式唤起 app 时的回调函数。
applicationOpenURLOptions(app : UIApplication | null, url : URL, options : Map<UIApplication.OpenURLOptionsKey, any> | null = null) : boolean {
WxpayManager.handleOpen(url)
return true
}
// 当应用程序接收到与用户活动相关的数据时调用此方法,例如,当用户使用 Universal Link 唤起应用时。
applicationContinueUserActivityRestorationHandler(application : UIApplication | null, userActivity : NSUserActivity | null, restorationHandler : ((res : [any] | null) => void) | null = null) : boolean {
WxpayManager.handleOpenUniversalLink(userActivity)
return true
}
}
class WxpayManager implements WXApiDelegate {
private static shared = new WxpayManager()
private static options ?: RequestMerchantTransferOptions
@UTSiOS.keyword("fileprivate")
static registerApp() {
const scheme = this.getApplicationScheme()
const universalLinkTmp = this.getApplicationUniversalLink()
if (scheme != null && universalLinkTmp != null && scheme != "" && universalLinkTmp != "") {
WXApi.registerApp(scheme!, universalLink = universalLinkTmp!)
}
}
@UTSiOS.keyword("fileprivate")
static handleOpen(url : URL) {
WXApi.handleOpen(url, delegate = shared)
}
@UTSiOS.keyword("fileprivate")
static handleOpenUniversalLink(userActivity : NSUserActivity | null) {
if (userActivity != null) {
WXApi.handleOpenUniversalLink(userActivity!, delegate = shared)
}
}
private static getApplicationScheme() : string | null {
let scheme : string | null = null
const infoDictionary = Bundle.main.infoDictionary
if (infoDictionary != null) {
const wechat = infoDictionary!['WeChat'] as Map<string, any> | null
if (wechat != null) {
scheme = wechat!['appid'] as string | null
}
}
return scheme
}
private static getApplicationUniversalLink() : string | null {
let universalLink : string | null = null
const infoDictionary = Bundle.main.infoDictionary
if (infoDictionary != null) {
const wechat = infoDictionary!['WeChat'] as Map<string, any> | null
if (wechat != null) {
universalLink = wechat!['universalLink'] as string | null
}
}
return universalLink
}
//@brief 检查微信是否已被用户安装
@UTSiOS.keyword("fileprivate")
static isWXAppInstalled() : boolean {
const url = URL(string = "weixin://")!
if (UIApplication.shared.canOpenURL(url)) {
return true
}
return false
}
static buildQueryString(parameters : Map<string, string>) : string | null {
let components = URLComponents()
let queryItems : [URLQueryItem] = []
parameters.forEach((value, key) => {
queryItems.append(URLQueryItem(name = key, value = value))
})
components.queryItems = queryItems
return components.percentEncodedQuery
}
static requestMerchantTransfer(options : RequestMerchantTransferOptions) {
WxpayManager.options = options
if (WxpayManager.isWXAppInstalled() == false) {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '微信没有安装'
}
options.fail?.(res)
options.complete?.(res)
}
if (WxpayManager.getApplicationScheme() == null || WxpayManager.getApplicationScheme() == "") {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '没有配置对应的URL Scheme'
}
options.fail?.(res)
options.complete?.(res)
return
}
if (WxpayManager.getApplicationUniversalLink() == null || WxpayManager.getApplicationUniversalLink() == "") {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '没有配置对应的Universal Link'
}
options.fail?.(res)
options.complete?.(res)
return
}
if (WxpayManager.options != null) {
const parameters = new Map<string, string>()
parameters.set('mchId', options.mchId)
parameters.set('package', options.package)
if (options.appId != null) {
parameters.set('appId', options.appId!)
}
if (options.openId != null) {
parameters.set('openId', options.openId!)
}
if (options.subAppId != null) {
parameters.set('subAppId', options.subAppId!)
}
if (options.subMchId != null) {
parameters.set('subMchId', options.subMchId!)
}
let reg = new WXOpenBusinessViewReq();
reg.businessType = "requestMerchantTransfer"
let query = WxpayManager.buildQueryString(parameters) ?? ""
reg.query = query
//函数调用后,会切换到微信的界面。第三方应用程序等待微信返回onResp。微信在异步处理完成后一定会调onResp
WXApi.send(reg)
}
}
//@brief 发送一个sendReq后,收到微信的回应
onResp(resp : BaseResp) {
if (resp instanceof WXOpenBusinessViewResp) {
let resp = resp as WXOpenBusinessViewResp
if (resp.extMsg?.includes("cancel") == true) {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '用户取消'
}
WxpayManager.options?.fail?.(res)
WxpayManager.options?.complete?.(res)
}
if (resp.extMsg?.includes("fail") == true) {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '展示页面失败'
}
WxpayManager.options?.fail?.(res)
WxpayManager.options?.complete?.(res)
}
if (resp.extMsg?.includes("success") == true) {
var res : RequestMerchantTransferGeneralCallbackResult = {
errMsg: '确认收款'
}
WxpayManager.options?.success?.(res)
WxpayManager.options?.complete?.(res)
}
}
}
}
@@ -0,0 +1,713 @@
export interface Uni {
/**
商家转账用户确认模式下,拉起页面请求用户确认收款
@tutorial https://doc.dcloud.net.cn/uni-app-x/api/request-merchant-transfer.html
@tutorial_uni_app_x https://doc.dcloud.net.cn/uni-app-x/api/request-merchant-transfer.html
@tutorial_uni_app https://uniapp.dcloud.net.cn/api/plugins/request-merchant-transfer.html
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.41"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
requestMerchantTransfer(options : RequestMerchantTransferOptions) : void;
}
export type RequestMerchantTransfer = (options : RequestMerchantTransferOptions) => void;
export type RequestMerchantTransferOptions = {
/**
商户号
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
mchId : string
/**
商家转账付款单跳转收款页 pkg 信息,商家转账付款单受理成功时返回给商户
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
package : string
/**
商户 appId(微信平台appid),普通模式下必填,服务商模式下,appId 和 subAppId 二选一填写
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
appId ?: string | null
/**
收款用户 openId, 对应传入的商户 appId 下,某用户的 openId
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "x",
"uniVer": "x",
"unixVer": "x"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
openId ?: string | null
/**
子商户 appId(微信平台子appid),服务商模式下,appId 和 subAppId 二选一填写
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "x",
"uniVer": "x",
"unixVer": "x"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
subAppId ?: string | null
/**
子商户号,服务商模式下必填
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "x",
"uniVer": "x",
"unixVer": "x"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
subMchId ?: string | null
/**
接口调用成功的回调函数
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
success ?: RequestMerchantTransferSuccessCallback | null
/**
接口调用失败的回调函数
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
fail ?: RequestMerchantTransferFailCallback | null
/**
接口调用结束的回调函数(调用成功、失败都会执行)
@uniPlatform {
"app": {
"android": {
"osVer": "5.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"ios": {
"osVer": "12.0",
"uniVer": "4.61",
"unixVer": "4.61"
},
"harmony": {
"osVer": "5.0",
"uniVer": "5.14",
"unixVer": "5.14"
}
},
"web": {
"uniVer": "x",
"unixVer": "x"
},
"mp": {
"weixin": {
"hostVer": "√",
"uniVer": "√",
"unixVer": "4.61"
},
"alipay": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"baidu": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"toutiao": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"lark": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"qq": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"kuaishou": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
},
"jd": {
"hostVer": "x",
"uniVer": "x",
"unixVer": "x"
}
}
}
*/
complete ?: RequestMerchantTransferCompleteCallback | null
}
export type RequestMerchantTransferCompleteCallback = (
res : RequestMerchantTransferGeneralCallbackResult
) => void
/** 接口调用失败的回调函数 */
export type RequestMerchantTransferFailCallback = (
res : RequestMerchantTransferGeneralCallbackResult
) => void
/** 接口调用成功的回调函数 */
export type RequestMerchantTransferSuccessCallback = (
res : RequestMerchantTransferGeneralCallbackResult
) => void
export type RequestMerchantTransferGeneralCallbackResult = {
errMsg : string
}
@@ -0,0 +1,53 @@
export type RequestMerchantTransferErrorCode = 9301102 | 9301103 | 9301104 | 9301105 | 9301106 | 9301107 | 9301108 | 9301109 | 9301110
export type RequestMerchantTransferError = {
errCode : RequestMerchantTransferErrorCode
errMsg : string
}
export const REQUEST_MERCHANT_TRANSFER_MCH_ID_REQUIRED : RequestMerchantTransferError = {
errCode: 9301102,
errMsg: 'requestMerchantTransfer:fail mchId is required'
}
export const REQUEST_MERCHANT_TRANSFER_PACKAGE_REQUIRED : RequestMerchantTransferError = {
errCode: 9301103,
errMsg: 'requestMerchantTransfer:fail package is required'
}
export const REQUEST_MERCHANT_TRANSFER_APP_ID_REQUIRED : RequestMerchantTransferError = {
errCode: 9301104,
errMsg: 'requestMerchantTransfer:fail appId is required'
}
export const REQUEST_MERCHANT_TRANSFER_WECHAT_NOT_INSTALLED : RequestMerchantTransferError = {
errCode: 9301105,
errMsg: 'requestMerchantTransfer:fail wechat is not installed'
}
export const REQUEST_MERCHANT_TRANSFER_WECHAT_VERSION_LOW : RequestMerchantTransferError = {
errCode: 9301106,
errMsg: 'requestMerchantTransfer:fail wechat version is too low'
}
export const REQUEST_MERCHANT_TRANSFER_CANCEL : RequestMerchantTransferError = {
errCode: 9301107,
errMsg: 'requestMerchantTransfer:fail cancel'
}
export const REQUEST_MERCHANT_TRANSFER_PARSE_RESPONSE_ERROR : RequestMerchantTransferError = {
errCode: 9301108,
errMsg: 'requestMerchantTransfer:fail parse response error'
}
export const REQUEST_MERCHANT_TRANSFER_OPEN_SDK_ERROR : RequestMerchantTransferError = {
errCode: 9301109,
errMsg: 'requestMerchantTransfer:fail open sdk error'
}
export const REQUEST_MERCHANT_TRANSFER_BUSINESS_FAILED : RequestMerchantTransferError = {
errCode: 9301110,
errMsg: 'requestMerchantTransfer:fail business failed'
}
export const REQUEST_MERCHANT_TRANSFER_OK = 'requestMerchantTransfer:ok'
+21 -5
View File
@@ -12,7 +12,19 @@
## 说明
uview-plus,是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。
【文档有广告,不喜勿入谢谢】
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架构级全新重构,但仍然保持源码的简易,不封装过于复杂,便于修改。
## 可视化设计
@@ -32,7 +44,13 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
<img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="220" height="220" >
| 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">
## 链接
@@ -41,9 +59,6 @@ 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
@@ -72,3 +87,4 @@ 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
@@ -0,0 +1,109 @@
<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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:44:35
* @FilePath : /u-view2.0/uview-ui/libs/config/props/actionSheet.js
* @LastAuthor : jry
* @lastTime : 2025-08-16 10:52:35
* @FilePath : /uview-plus/libs/config/props/actionSheet.js
*/
export default {
// action-sheet组件
@@ -14,6 +14,8 @@ export default {
title: '',
description: '',
actions: [],
nameKey: 'name',
subnameKey: 'subnameKey',
index: '',
cancelText: '',
closeOnClickAction: true,
@@ -1,6 +1,16 @@
/*
* @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 defProps from '../../libs/config/props.js'
import ActionSheetDefaultProps from './actionSheet'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(ActionSheetDefaultProps)
export const props = defineMixin({
props: {
// 操作菜单是否展示 (默认false)
@@ -23,6 +33,16 @@ 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,4 +1,3 @@
<template>
<u-popup
:show="show"
@@ -8,32 +7,39 @@
: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">{{title}}</text>
<text
class="u-action-sheet__header__title u-line-1"
:style="titleDynamicStyle"
>{{title}}</text>
<view
class="u-action-sheet__header__icon-wrap"
@tap.stop="cancel"
>
<u-icon
<up-icon
name="close"
size="17"
color="#c8c9cc"
:color="closeIconColor"
bold
></u-icon>
></up-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"></u-line>
<!-- 分割线 -->
<u-line v-if="description" :color="dividerColor"></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 -->
@@ -62,17 +68,20 @@
@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.name }}</text>
>{{ item[nameKey] }}</text>
<text
v-if="item.subname"
v-if="item[subnameKey]"
class="u-action-sheet__item-wrap__item__subname"
>{{ item.subname }}</text>
:style="[subnameStyle(index)]"
>{{ item[subnameKey] }}</text>
</template>
<!-- 加载状态图标 -->
<u-loading-icon
v-else
custom-class="van-action-sheet__loading"
@@ -83,21 +92,25 @@
<!-- #ifdef MP -->
</button>
<!-- #endif -->
<u-line v-if="index !== actions.length - 1"></u-line>
<!-- 选项间分割线 -->
<u-line v-if="index !== actions.length - 1" :color="dividerColor"></u-line>
</view>
</scroll-view>
</slot>
<!-- 取消按钮前的分割区域 -->
<u-gap
bgColor="#eaeaec"
:bgColor="cancelGapColor"
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>
@@ -114,7 +127,7 @@
/**
* ActionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
* @tutorial https://ijry.github.io/uview-plus/components/actionSheet.html
* @tutorial https://uview-plus.jiangruyi.com/components/actionSheet.html
*
* @property {Boolean} show 操作菜单是否展示 (默认 false )
* @property {String} title 操作菜单标题
@@ -154,20 +167,54 @@
}
},
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) => {
let style = {};
const style = {
color: this.upThemeVar('--up-main-color', '#303133')
};
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 = '#c0c4cc'
if (this.actions[index].disabled) style.color = this.upThemeVar('--up-light-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) {
@@ -180,6 +227,7 @@
this.$emit('update:show', false)
this.$emit('close')
},
// 选择操作项处理
selectHandler(index) {
const item = this.actions[index]
if (item && !item.disabled && !item.loading) {
@@ -190,30 +238,39 @@
}
}
},
// 动态处理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: $u-main-color !default;
$u-action-sheet-title-color: var(--up-main-color, #303133) !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:14px !default;
$u-action-sheet-description-color: var(--up-tips-color, #909193) !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: #c0c4cc !default;
$u-action-sheet-item-wrap-subname-color: var(--up-tips-color, #909193) !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:$u-content-color !default;
$u-action-sheet-cancel-text-color: var(--up-main-color, #303133) !default;
$u-action-sheet-cancel-text-font-size:15px !default;
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
$u-action-sheet-cancel-text-hover-background-color: var(--up-hover-bg-color, rgb(242, 243, 245)) !default;
.u-reset-button {
width: $u-action-sheet-reset-button-width;
@@ -239,8 +296,9 @@
}
&__description {
display: block;
font-size: $u-action-sheet-description-font-size;
color: $u-tips-color;
color: $u-action-sheet-description-color;
margin: $u-action-sheet-description-margin;
text-align: center;
}
@@ -256,7 +314,7 @@
&__name {
font-size: $u-action-sheet-item-wrap-name-font-size;
color: $u-main-color;
color: var(--up-main-color, #303133);
text-align: center;
}
@@ -0,0 +1,77 @@
<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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:47:24
* @FilePath : /u-view2.0/uview-ui/libs/config/props/album.js
* @LastAuthor : jry
* @lastTime : 2025-08-16 16:32:24
* @FilePath : /uview-plus/libs/config/props/album.js
*/
export default {
// album 组件
@@ -1,5 +1,16 @@
/*
* @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 defProps from '../../libs/config/props.js'
import AlbumDefaultProps from './album'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AlbumDefaultProps)
export const props = defineMixin({
props: {
// 图片地址,Array<String>|Array<Object>形式
@@ -1,5 +1,6 @@
<template>
<view class="u-album">
<!-- 相册行容器,每行显示 rowCount 个图片 -->
<view
class="u-album__row"
ref="u-album__row"
@@ -8,13 +9,15 @@
: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="previewFullImage ? onPreviewTap($event, getSrc(item)) : ''"
@tap="onPreviewTap($event, getSrc(item))"
>
<!-- 图片显示 -->
<image
:src="getSrc(item)"
:mode="
@@ -32,6 +35,7 @@
}
]"
></image>
<!-- 超出最大显示数量时的更多提示 -->
<view
v-if="
showMore &&
@@ -64,14 +68,14 @@ import { mixin } from '../../libs/mixin/mixin';
import { addUnit, sleep } from '../../libs/function/index';
import test from '../../libs/function/test';
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
// 不支持百分比单位,这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* Album 相册
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
* @tutorial https://ijry.github.io/uview-plus/components/album.html
* @tutorial https://uview-plus.jiangruyi.com/components/album.html
*
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
@@ -108,14 +112,20 @@ 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,
@@ -139,11 +149,16 @@ 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) => {
// 限制最大展示数量
@@ -160,18 +175,29 @@ 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长度等变化时,重新计算图片的宽度
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
/**
* 计算相册总宽度,用于外部组件对齐
* 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
* @returns {Number} 相册宽度
*/
albumWidth() {
let width = 0
if (this.urls.length === 1) {
@@ -185,59 +211,100 @@ 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)
})
uni.previewImage({
current: url,
urls
})
// 是否阻止事件传播
this.stop && this.preventEvent(e)
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
? this.singleSize
: (res.width / res.height) * this.singleSize
? singleSize
: (res.width / res.height) * singleSize
this.singleHeight = !isHorizotal
? this.singleSize
? 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 &&
@@ -251,8 +318,6 @@ 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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:48:53
* @FilePath : /u-view2.0/uview-ui/libs/config/props/alert.js
* @LastAuthor : jry
* @lastTime : 2025-08-17 17:23:53
* @FilePath : /uview-plus/libs/config/props/alert.js
*/
export default {
// alert警告组件
@@ -17,6 +17,10 @@ export default {
showIcon: false,
effect: 'light',
center: false,
fontSize: 14
fontSize: 14,
transitionMode: 'fade',
duration: 0,
icon: '',
value: true
}
}
@@ -1,5 +1,16 @@
/*
* @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 defProps from '../../libs/config/props.js'
import AlertDefaultProps from './alert'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AlertDefaultProps)
export const props = defineMixin({
props: {
// 显示文字
@@ -41,6 +52,26 @@ 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>
<u-transition
mode="fade"
<up-transition
:mode="transitionMode"
:show="show"
>
<view
@@ -9,22 +9,25 @@
@tap.stop="clickHandler"
:style="[addStyle(customStyle)]"
>
<!-- 左侧图标 -->
<view
class="u-alert__icon"
v-if="showIcon"
>
<u-icon
<up-icon
:name="iconName"
size="18"
:color="iconColor"
></u-icon>
></up-icon>
</view>
<!-- 内容区域 -->
<view
class="u-alert__content"
:style="[{
paddingRight: closable ? '20px' : 0
}]"
>
<!-- 标题 -->
<text
class="u-alert__content__title"
v-if="title"
@@ -34,6 +37,7 @@
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ title }}</text>
<!-- 描述信息 -->
<text
class="u-alert__content__desc"
v-if="description"
@@ -44,19 +48,22 @@
: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"
>
<u-icon
name="close"
:color="iconColor"
size="15"
></u-icon>
<slot name="close">
<up-icon
name="close"
:color="iconColor"
size="15"
></up-icon>
</slot>
</view>
</view>
</u-transition>
</up-transition>
</template>
<script>
@@ -67,10 +74,10 @@
/**
* Alert 警告提示
* @description 警告提示,展现需要关注的信息。
* @tutorial https://ijry.github.io/uview-plus/components/alertTips.html
* @tutorial https://uview-plus.jiangruyi.com/components/alertTips.html
*
* @property {String} title 显示的文字
* @property {String} type 使用预设的颜色 (默认 'warning' )
* @property {String} type 使用预设的颜色 (默认 'warning' )
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
* @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false )
* @property {Boolean} showIcon 是否显示左边的辅助图标 ( 默认 false )
@@ -78,24 +85,34 @@
* @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 点击关闭按钮时触发
* @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
* @event {Function} closed 关闭动画结束时触发
* @example <up-alert :title="title" type = "warning" :closable="closable" :description = "description"></up-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';
@@ -117,25 +134,50 @@
}
}
},
emits: ["click","close"],
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);
}
},
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;
@@ -153,7 +195,7 @@
}
&--primary--light {
background-color: #ecf5ff;
background-color: var(--up-primary-light, #ecf5ff);
}
&--error--dark {
@@ -161,7 +203,7 @@
}
&--error--light {
background-color: #FEF0F0;
background-color: var(--up-error-light, #FEF0F0);
}
&--success--dark {
@@ -169,7 +211,7 @@
}
&--success--light {
background-color: #f5fff0;
background-color: var(--up-success-light, #f5fff0);
}
&--warning--dark {
@@ -177,7 +219,7 @@
}
&--warning--light {
background-color: #FDF6EC;
background-color: var(--up-warning-light, #FDF6EC);
}
&--info--dark {
@@ -185,7 +227,7 @@
}
&--info--light {
background-color: #f4f4f5;
background-color: var(--up-info-light, #f4f4f5);
}
&__icon {
@@ -197,24 +239,22 @@
flex: 1;
&__title {
color: $u-main-color;
color: var(--up-main-color, $u-main-color);
font-size: 14px;
font-weight: bold;
color: #fff;
margin-bottom: 2px;
}
&__desc {
color: $u-main-color;
color: var(--up-main-color, $u-main-color);
font-size: 14px;
flex-wrap: wrap;
color: #fff;
}
}
&__title--dark,
&__desc--dark {
color: #FFFFFF;
color: var(--up-white, #FFFFFF);
}
&__text--primary--light,
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:55
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatarGroup.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/avatarGroup.js
*/
export default {
// avatarGroup 组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import AvatarGroupDefaultProps from './avatarGroup'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AvatarGroupDefaultProps)
export const props = defineMixin({
props: {
// 头像图片组
@@ -40,7 +40,7 @@
/**
* AvatarGroup 头像组
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
* @tutorial https://uview-plus.jiangruyi.com/components/avatar.html
*
* @property {Array} urls 头像图片组 (默认 [] )
* @property {String | Number} maxCount 最多展示的头像数量 ( 默认 5 )
@@ -79,7 +79,6 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar-group {
@include flex;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:22
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatar.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/avatar.js
*/
export default {
// avatar 组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import AvatarDefaultProps from './avatar'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(AvatarDefaultProps)
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 -->
<u-icon
<up-icon
v-else-if="icon"
:name="icon"
:size="fontSize"
:color="color"
></u-icon>
></up-icon>
<up-text
v-else-if="text"
:text="text"
@@ -63,7 +63,7 @@
/**
* Avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
* @tutorial https://uview-plus.jiangruyi.com/components/avatar.html
*
* @property {String} src 头像路径,如加载失败,将会显示默认头像(不能为相对路径)
* @property {String} shape 头像形状 ( circle (默认) | square)
@@ -151,7 +151,6 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar {
@include flex;
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:50:18
* @FilePath : /u-view2.0/uview-ui/libs/config/props/backtop.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/backtop.js
*/
export default {
// backtop组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import BackTopDefaultProps from './backtop'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BackTopDefaultProps)
export const props = defineMixin({
props: {
// 返回顶部的形状,circle-圆形,square-方形
@@ -10,10 +10,10 @@
v-if="!$slots.default && !$slots.$default"
@click="backToTop"
>
<u-icon
<up-icon
:name="icon"
:custom-style="iconStyle"
></u-icon>
></up-icon>
<text
v-if="text"
class="u-back-top__text"
@@ -111,7 +111,6 @@
</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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-23 19:51:50
* @FilePath : /u-view2.0/uview-ui/libs/config/props/badge.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/badge.js
*/
export default {
// 徽标数组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import BadgeDefaultProps from './badge'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BadgeDefaultProps)
export const props = defineMixin({
props: {
// 是否显示圆点
@@ -89,7 +89,6 @@
</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
@@ -0,0 +1,24 @@
/*
* @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,27 +1,60 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import { defineMixin } from '../../libs/vue.js'
import BoxDefaultProps from './box.js'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(BoxDefaultProps)
export const propsBox = defineMixin({
props: {
// 背景色
bgColors: {
type: [Array],
default: ['#EEFCFF', '#FCF8FF', '#FDF8F2']
default: () => defProps.box.bgColors
},
// 高度
height: {
type: [String],
default: "160px"
default: () => defProps.box.height
},
// 圆角
borderRadius: {
type: [String],
default: "6px"
default: () => defProps.box.borderRadius
},
// 间隔
gap: {
type: [String],
default: "15px"
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
},
}
})
@@ -1,18 +1,33 @@
<template>
<view class="u-box" :style="[{height: height}, addStyle(customStyle)]">
<view class="u-box__left" :style="{borderRadius: borderRadius, backgroundColor: bgColors[0]}">
<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">右上</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">右下</slot>
</view>
</view>
<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>
</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>
</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>
</view>
</view>
</view>
</template>
@@ -51,7 +66,6 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-box {
/* #ifndef APP-NVUE */
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:51:27
* @FilePath : /u-view2.0/uview-ui/libs/config/props/button.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/button.js
*/
export default {
// button组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import ButtonDefaultProps from './button'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(ButtonDefaultProps)
export const props = defineMixin({
props: {
// 是否细边框
@@ -39,13 +39,13 @@
>
</template>
<template v-else>
<u-icon
<up-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
:customStyle="{ marginRight: '2px' }"
></u-icon>
></up-icon>
<slot>
<text
class="u-button__text"
@@ -87,12 +87,12 @@
>
</template>
<template v-else>
<u-icon
<up-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
></u-icon>
></up-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 color from '../../libs/config/color';
import { getThemeVar } from '../../libs/theme/runtime.js';
/**
* button 按钮
* @description Button 按钮
* @tutorial https://ijry.github.io/uview-plus/components/button.html
* @tutorial https://uview-plus.jiangruyi.com/components/button.html
*
* @property {Boolean} hairline 是否显示按钮的细边框 (默认 true )
* @property {String} type 按钮的预置样式,info,primary,error,warning,success (默认 'info' )
@@ -192,26 +192,67 @@ 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
: color[`u-${this.type}`];
: this.themeTypeColor;
}
if (this.type === "info") {
return "#c9c9c9";
return this.isDarkTheme ? "#9ca3af" : "#c9c9c9";
}
return "rgb(200, 200, 200)";
},
iconColorCom() {
// 如果是镂空状态,设置了color就用color值,否则使用主题颜色,
// u-icon的color能接受一个主题颜色的值
if (this.iconColor) return this.iconColor;
// up-icon的color能接受一个主题颜色的值
if (this.iconColor) return this.iconColor;
if (this.plain) {
return this.color ? this.color : this.type;
} else {
return this.type === "info" ? "#000000" : "#ffffff";
return this.type === "info" ? this.nvueMainColor : "#ffffff";
}
},
baseColor() {
@@ -240,7 +281,30 @@ 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组件进行单独的设置
@@ -248,10 +312,12 @@ export default {
let style = {};
// 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
if (this.type === "info") {
style.color = "#323233";
style.color = this.nvueMainColor;
}
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;
@@ -271,6 +337,14 @@ 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) {
@@ -306,8 +380,6 @@ export default {
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
/* #ifndef APP-NVUE */
@import "./vue.scss";
/* #endif */
@@ -334,9 +406,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: #323233 !default;
$u-button-info-background-color: #fff !default;
$u-button-info-border-color: #ebedf0 !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-border-width: 1px !default;
$u-button-info-border-style: solid !default;
$u-button-success-color: #fff !default;
@@ -369,7 +441,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: #fff !default;
$u-button-plain-background-color: var(--up-button-plain-background-color, var(--up-card-bg-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: 100%;
width: auto;
white-space: nowrap;
&__text {
@@ -0,0 +1,36 @@
/*
* @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: '上拉收起月历'
}
}
@@ -0,0 +1,70 @@
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
}
}
})
@@ -0,0 +1,507 @@
<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,21 +1,22 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:52:43
* @FilePath : /u-view2.0/uview-ui/libs/config/props/calendar.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/calendar.js
*/
import { t } from '../../libs/i18n'
export default {
// calendar 组件
calendar: {
title: '日期选择',
title: t("up.calendar.chooseDates"),
showTitle: true,
showSubtitle: true,
mode: 'single',
startText: '开始',
endText: '结束',
startText: t("up.common.start"),
endText: t("up.common.end"),
customList: [],
color: '#3c9cff',
minDate: 0,
@@ -26,8 +27,8 @@ export default {
formatter: null,
showLunar: false,
showMark: true,
confirmText: '确定',
confirmDisabledText: '确定',
confirmText: t("up.common.confirm"),
confirmDisabledText: t("up.common.confirm"),
show: false,
closeOnClickOverlay: false,
readonly: false,
@@ -36,10 +37,27 @@ 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,
weekText: ['一', '二', '三', '四', '五', '六', '日'],
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")],
forbidDays: [],
forbidDaysToast: '该日期已禁用',
forbidDaysToast: t("up.calendar.disabled"),
monthFormat: '',
pageInline: false
}
}
@@ -4,10 +4,42 @@
class="u-calendar-header__title"
v-if="showTitle"
>{{ title }}</text>
<text
class="u-calendar-header__subtitle"
<view
class="u-calendar-header__subtitle-wrap"
v-if="showSubtitle"
>{{ subtitle }}</text>
>
<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>
<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>
@@ -51,9 +83,49 @@
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 {
@@ -61,15 +133,36 @@
}
},
methods: {
name() {
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')
}
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-calendar-header {
display: flex;
@@ -78,20 +171,41 @@
&__title {
font-size: 16px;
color: $u-main-color;
color: var(--up-main-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: $u-main-color;
height: 40px;
color: var(--up-main-color, $u-main-color);
text-align: center;
line-height: 40px;
font-weight: bold;
min-width: 120px;
}
&__switch {
width: 44px;
height: 40px;
line-height: 40px;
text-align: center;
color: var(--up-main-color, $u-main-color);
font-size: 22px;
font-weight: bold;
flex-shrink: 0;
&--disabled {
opacity: 0.35;
}
}
&__weekdays {
@@ -100,11 +214,27 @@
&__weekday {
font-size: 13px;
color: $u-main-color;
color: var(--up-main-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">{{ item.year }}年{{ item.month }}月</text>
<text v-if="index !== 0" class="u-calendar-month__title">{{ monthTitle(item) }}</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,11 +33,12 @@
// #endif
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit, deepClone, toast, sleep } from '../../libs/function/index';
import { addUnit, deepClone, toast, sleep, getWindowInfo } 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 'dayjs/esm/index'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import { t } from '../../libs/i18n'
export default {
name: 'u-calendar-month',
mixins: [mpMixin, mixin],
@@ -134,6 +135,16 @@
forbidDaysToast: {
type: String,
default: ''
},
// 今天日期,用于独立高亮
todayDate: {
type: String,
default: ''
},
// 今天日期的独立高亮颜色
todayColor: {
type: String,
default: ''
}
},
data() {
@@ -162,17 +173,24 @@
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(this.width / 7).toFixed(3).slice(0, -1))
const dayWidth = Number(parseFloat(wrapperWidth / 7).toFixed(3).slice(0, -1))
// 得出每个日期的宽度
// #ifdef APP-NVUE
style.width = addUnit(dayWidth, 'px')
// #endif
style.height = addUnit(this.rowHeight)
style.height = addUnit(this.rowHeight, 'px')
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
@@ -192,6 +210,10 @@
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的兼容,只能这么写,无法缩写,也无法通过类名控制等等
@@ -216,12 +238,13 @@
// 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
.selected[len]))) {
style.backgroundColor = colorGradient(this.color, '#ffffff', 100)[90]
const rangeEndColor = this.upThemeVar('--up-card-bg-color', this.upThemeIsDark ? '#1c1c1e' : '#ffffff')
style.backgroundColor = colorGradient(this.color, rangeEndColor, 100)[90]
// 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
style.opacity = 0.7
style.opacity = this.upThemeIsDark ? 0.85 : 0.7
}
} else if (this.selected.length === 1) {
// 之所以需要这么写,是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
// 之所以需要这么写,是因为uni-app的iOS客户端的bug
// 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
@@ -237,6 +260,9 @@
return style
}
},
resolvedTodayColor() {
return this.todayColor || this.color
},
// 某个日期是否被选中
textStyle() {
return (item) => {
@@ -254,6 +280,9 @@
style.color = this.color
}
}
if (this.todayDate && this.dateSame(date, this.todayDate) && !this.isSelectedDate(date)) {
style.color = this.resolvedTodayColor
}
return style
}
},
@@ -292,6 +321,7 @@
mounted() {
this.init()
},
emits: ['monthSelected', 'updateMonthTop'],
methods: {
init() {
// 初始化默认选中
@@ -305,6 +335,13 @@
})
})
},
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)) {
@@ -316,16 +353,21 @@
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 => {
this.width = res.size.width
const width = res && res.size ? Number(res.size.width) : 0
this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0)
})
// #endif
// #ifndef APP-NVUE
this.$uGetRect('.u-calendar-month-wrapper').then(size => {
this.width = size.width
const width = size ? Number(size.width) : 0
this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0)
})
// #endif
},
@@ -350,7 +392,7 @@
// 获取每个月份区域的尺寸
getMonthRectByPromise(el) {
// #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://uview-plus.jiangruyi.com/js/getRect.html
// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
return new Promise(resolve => {
this.$uGetRect(`.${el}`).then(size => {
@@ -414,7 +456,7 @@
if(this.rangePrompt) {
toast(this.rangePrompt)
} else {
toast(`选择天数不能超过 ${this.maxRange} 天`)
toast(t("up.calendar.daysExceed", { days: this.maxRange }))
}
return
}
@@ -474,13 +516,24 @@
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;
@@ -494,7 +547,7 @@
font-size: 14px;
line-height: 42px;
height: 42px;
color: $u-main-color;
color: var(--up-main-color, $u-main-color);
text-align: center;
font-weight: bold;
}
@@ -516,7 +569,7 @@
&__text {
font-size: 155px;
color: rgba(231, 232, 234, 0.83);
color: var(--up-calendar-month-mark-color, rgba(231, 232, 234, 0.83));
}
}
@@ -547,7 +600,7 @@
}
&__buttom-info {
color: $u-content-color;
color: var(--up-content-color, $u-content-color);
text-align: center;
position: absolute;
bottom: 5px;
@@ -561,20 +614,21 @@
}
&--disabled {
color: #cacbcd;
color: var(--up-disabled-color, #cacbcd);
}
}
&__info {
text-align: center;
font-size: 16px;
color: var(--up-main-color, $u-main-color);
&--selected {
color: #ffffff;
}
&--disabled {
color: #cacbcd;
color: var(--up-disabled-color, #cacbcd);
}
}
@@ -605,4 +659,5 @@
}
}
}
</style>
@@ -1,6 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CalendarDefaultProps from './calendar'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CalendarDefaultProps)
export const props = defineMixin({
props: {
// 日历顶部标题
@@ -98,6 +100,46 @@ 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,
@@ -132,6 +174,26 @@ 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: {
@@ -143,6 +205,21 @@ 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,
@@ -156,5 +233,14 @@ 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,9 +2,18 @@
<u-popup
:show="show"
mode="bottom"
closeable
:overlay="overlay"
:duration="duration"
:overlayStyle="overlayStyle"
:overlayOpacity="overlayOpacity"
:zIndex="zIndex"
:safeAreaInsetBottom="safeAreaInsetBottom"
:safeAreaInsetTop="safeAreaInsetTop"
:bgColor="bgColor"
:closeable="!pageInline"
@close="close"
:round="round"
:pageInline="pageInline"
:closeOnClickOverlay="closeOnClickOverlay"
>
<view class="u-calendar">
@@ -14,10 +23,46 @@
: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)
height: addUnit(listHeight, 'px')
}"
scroll-y
@scroll="onScroll"
@@ -44,11 +89,48 @@
:allowSameDay="allowSameDay"
:forbidDays="forbidDays"
:forbidDaysToast="forbidDaysToast"
:monthFormat="monthFormat"
:todayDate="todayDate"
:todayColor="todayColor"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="updateMonthTop"
@updateMonthTop="onUpdateMonthTop"
></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
@@ -64,6 +146,32 @@
</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>
@@ -71,16 +179,17 @@ import uHeader from './header.vue'
import uMonth from './month.vue'
import { props } from './props.js'
import util from './util.js'
import dayjs from 'dayjs/esm/index'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import Calendar from '../../libs/util/calendar.js'
import { mpMixin } from '../../libs/mixin/mpMixin.js'
import { mixin } from '../../libs/mixin/mixin.js'
import { addUnit, range, error, padZero } from '../../libs/function/index';
import { addUnit, getPx, range, error, padZero } from '../../libs/function/index';
import test from '../../libs/function/test';
import { t } from '../../libs/i18n'
/**
* Calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
* @tutorial https://ijry.github.io/uview-plus/components/calendar.html
* @tutorial https://uview-plus.jiangruyi.com/components/calendar.html
*
* @property {String} title 标题内容 (默认 日期选择 )
* @property {Boolean} showTitle 是否显示标题 (默认 true )
@@ -101,14 +210,28 @@ import test from '../../libs/function/test';
* @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 点击确定按钮时触发 选择日期相关的返回参数
@@ -136,6 +259,15 @@ 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
}
@@ -153,6 +285,13 @@ export default {
this.setMonth()
}
},
timePrecision() {
this.initTimeOptions()
this.initTimeValues()
},
defaultTime() {
this.initTimeValues()
},
// 打开弹窗时,设置月份数据
show: {
immediate: true,
@@ -179,6 +318,28 @@ 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]
@@ -186,13 +347,53 @@ export default {
subtitle() {
// 初始化时,this.months为空数组,所以需要特别判断处理
if (this.months.length) {
return `${this.months[this.monthIndex].year}年${
this.months[this.monthIndex].month
}月`
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
}
} 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') {
@@ -213,6 +414,130 @@ 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
@@ -231,7 +556,8 @@ export default {
return
}
if( scene === 'tap') {
this.$emit('confirm', this.selected)
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
}
}
}
@@ -246,7 +572,15 @@ export default {
return error('maxDate不能小于minDate时间')
}
// 滚动区域的高度
this.listHeight = this.rowHeight * 5 + 30
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.setMonth()
},
close() {
@@ -255,7 +589,8 @@ export default {
// 点击确定按钮
confirm() {
if (!this.buttonDisabled) {
this.$emit('confirm', this.selected)
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
}
},
// 获得两个日期之间的月份数
@@ -340,6 +675,67 @@ 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) {
@@ -354,6 +750,7 @@ export default {
if (_index !== -1) {
// #ifndef MP-WEIXIN
this.$nextTick(() => {
this.scrollIntoView = ''
this.scrollIntoView = `month-${_index}`
this.scrollIntoViewScroll = this.scrollIntoView
})
@@ -376,6 +773,12 @@ export default {
}
},
// 更新月份的top值
onUpdateMonthTop(topArr = []) {
if (this.monthSwitch) {
return
}
this.updateMonthTop(topArr)
},
updateMonthTop(topArr = []) {
// 设置对应月份的top值,用于onScroll方法更新月份
topArr.map((item, index) => {
@@ -403,9 +806,88 @@ 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 'dayjs/esm/index'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
export default {
methods: {
// 设置月份数据
@@ -0,0 +1,734 @@
<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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:53:20
* @FilePath : /u-view2.0/uview-ui/libs/config/props/carKeyboard.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/carKeyboard.js
*/
export default {
// 车牌号键盘
@@ -1,6 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CarKeyboardDefaultProps from './carKeyboard'
import { registerComponentProps } 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"
>
<u-icon
<up-icon
size="28"
name="backspace"
color="#303133"
></u-icon>
:color="upThemeVar('--up-main-color', '#303133')"
></up-icon>
</view>
</view>
</view>
@@ -222,26 +222,25 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-car-keyboard-background-color: rgb(224, 228, 230) !default;
$u-car-keyboard-background-color: var(--up-bg-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:#FFFFFF !default;
$u-car-keyboard-button-inner-background-color:var(--up-card-bg-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:$u-main-color !default;
$u-car-keyboard-button-text-color:var(--up-main-color, #303133) !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:$u-main-color !default;
$u-car-keyboard-lang-color:var(--up-main-color, #303133) !default;
$u-car-keyboard-active-color:$u-primary !default;
$u-car-keyboard-line-font-size:15px !default;
$u-car-keyboard-line-color:$u-main-color !default;
$u-car-keyboard-line-color:var(--up-main-color, #303133) !default;
$u-car-keyboard-line-margin:0 1px !default;
$u-car-keyboard-u-hover-class-background-color:#BBBCC6 !default;
$u-car-keyboard-u-hover-class-background-color:var(--up-border-color, #BBBCC6) !default;
.u-keyboard {
@include flex(column);
@@ -0,0 +1,40 @@
/*
* @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,140 +1,136 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CardDefaultProps from './card'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CardDefaultProps)
export const propsCard = defineMixin({
props: {
// 与屏幕两侧是否留空隙
full: {
type: Boolean,
default: false
default: () => defProps.card.full
},
// 标题
title: {
type: String,
default: ''
default: () => defProps.card.title
},
// 标题颜色
titleColor: {
type: String,
default: '#303133'
default: () => defProps.card.titleColor
},
// 标题字体大小
titleSize: {
type: [Number, String],
default: '15px'
default: () => defProps.card.titleSize
},
// 副标题
subTitle: {
type: String,
default: ''
default: () => defProps.card.subTitle
},
// 副标题颜色
subTitleColor: {
type: String,
default: '#909399'
default: () => defProps.card.subTitleColor
},
// 副标题字体大小
subTitleSize: {
type: [Number, String],
default: '13'
default: () => defProps.card.subTitleSize
},
// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
border: {
type: Boolean,
default: true
default: () => defProps.card.border
},
// 用于标识点击了第几个
index: {
type: [Number, String, Object],
default: ''
default: () => defProps.card.index
},
// 用于隔开上下左右的边距,带单位的写法,如:"30px 30px","20px 20px 30px 30px"
margin: {
type: String,
default: '15px'
default: () => defProps.card.margin
},
// card卡片的圆角
borderRadius: {
type: [Number, String],
default: '8px'
default: () => defProps.card.borderRadius
},
// 头部自定义样式,对象形式
headStyle: {
type: Object,
default() {
return {};
}
default: () => defProps.card.headStyle
},
// 主体自定义样式,对象形式
bodyStyle: {
type: Object,
default() {
return {};
}
default: () => defProps.card.bodyStyle
},
// 底部自定义样式,对象形式
footStyle: {
type: Object,
default() {
return {};
}
default: () => defProps.card.footStyle
},
// 头部是否下边框
headBorderBottom: {
type: Boolean,
default: true
default: () => defProps.card.headBorderBottom
},
// 底部是否有上边框
footBorderTop: {
type: Boolean,
default: true
default: () => defProps.card.footBorderTop
},
// 标题左边的缩略图
thumb: {
type: String,
default: ''
default: () => defProps.card.thumb
},
// 缩略图宽高
thumbWidth: {
type: [String, Number],
default: '30px'
default: () => defProps.card.thumbWidth
},
// 缩略图是否为圆形
thumbCircle: {
type: Boolean,
default: false
default: () => defProps.card.thumbCircle
},
// 给head,body,foot的内边距
padding: {
type: [String, Number],
default: '15px'
default: () => defProps.card.padding
},
paddingHead: {
type: [String, Number],
default: ''
default: () => defProps.card.paddingHead
},
paddingBody: {
type: [String, Number],
default: ''
default: () => defProps.card.paddingBody
},
paddingFoot: {
type: [String, Number],
default: ''
default: () => defProps.card.paddingFoot
},
// 是否显示头部
showHead: {
type: Boolean,
default: true
default: () => defProps.card.showHead
},
// 是否显示尾部
showFoot: {
type: Boolean,
default: true
default: () => defProps.card.showFoot
},
// 卡片外围阴影,字符串形式
boxShadow: {
type: String,
default: 'none'
default: () => defProps.card.boxShadow
}
}
})
@@ -136,13 +136,11 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-card {
position: relative;
overflow: hidden;
font-size: 28rpx;
background-color: #ffffff;
background-color: var(--up-card-bg-color, #ffffff);
box-sizing: border-box;
&-full {
@@ -0,0 +1,371 @@
<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,49 +1,64 @@
<template>
<view class="u-cate-tab">
<view class="u-cate-tab" :style="{ height: addUnit(height) }">
<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="[current == index ? 'u-cate-tab__item-active' : '']"
:class="[innerCurrent == 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-view :scroll-top="scrollRightTop" scroll-with-animation :scroll-into-view="scrollIntoView"
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">
<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']">
<view class="item-title">
<text>{{item[tabKeyName]}}</text>
</view>
<view class="item-container">
<template v-for="(item1, index1) in item.children" :key="index1">
<slot name="pageItem" :pageItem="item1">
<view class="thumb-box" >
<image class="item-menu-image" :src="item1.icon" mode=""></image>
<view class="item-menu-name">{{item1[itemKeyName]}}</view>
</view>
</slot>
</template>
</view>
</template>
</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">
<slot name="itemList" :item="item">
</slot>
<template v-if="!$slots['itemList']">
<view class="item-title">
<text>{{item[tabKeyName]}}</text>
</view>
<view class="item-container">
<template v-for="(item1, index1) in item.children" :key="index1">
<slot name="pageItem" :pageItem="item1">
<view class="thumb-box" >
<image class="item-menu-image" :src="item1.icon" mode=""></image>
<view class="item-menu-name">{{item1[itemKeyName]}}</view>
</view>
</slot>
</template>
</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: () => {
@@ -57,18 +72,43 @@
itemKeyName: {
type: String,
default: 'name'
},
current: {
type: Number,
default: 0
}
},
watch: {
tabList() {
this.getMenuItemTop()
}
tabList: {
deep: true,
handler(newVal, oldVal) {
// this.observer();
sleep(30).then(() => {
this.getMenuItemTop();
this.leftMenuStatus(this.innerCurrent);
})
}
},
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,
current: 0, // 预设当前项的值
innerCurrent: 0, // 预设当前项的值
menuHeight: 0, // 左边菜单的高度
menuItemHeight: 0, // 左边菜单item的高度
itemId: '', // 栏目右边scroll-view用于滚动的id
@@ -79,26 +119,38 @@
timer: null, // 定时器
}
},
onMounted() {
mounted() {
// this.observer();
this.innerCurrent = this.current;
this.leftMenuStatus(this.innerCurrent);
this.getMenuItemTop()
// 设置默认index
sleep(50).then(() => {
this.swichMenu(this.innerCurrent)
})
},
methods: {
addUnit,
// 点击左边的栏目切换
async swichMenu(index) {
if(this.arr.length == 0) {
await this.getMenuItemTop();
if (this.mode == 'follow') {
if(this.arr.length == 0) {
await this.getMenuItemTop();
}
if (this.scrollIntoView != 'item' + index) {
this.scrollIntoView = 'item' + index;
}
}
if (index == this.current) return;
this.scrollRightTop = this.oldScrollTop;
if (index == this.innerCurrent) return;
this.$nextTick(function(){
this.scrollRightTop = this.arr[index];
this.current = index;
this.leftMenuStatus(index);
this.innerCurrent = index;
this.$emit('update:current', index);
})
},
// 获取一个目标元素的高度
getElRect(elClass, dataVal) {
new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(this);
query.select('.' + elClass).fields({
size: true
@@ -117,54 +169,73 @@
},
// 观测元素相交状态
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);
// 检测右边scroll-view的id为itemxx的元素与u-cate-tab__right-box的相交状态
// 如果跟.u-cate-tab__right-box底部相交,就动态设置左边栏目的活动状态
this._observerList.push(observer);
// 检测相交状态
observer.relativeTo('.u-cate-tab__right-box', {
top: 0
}).observe('#item' + index, res => {
top: 10
}).observe('#item' + index, (res) => {
if (res.intersectionRatio > 0) {
let id = res.id.substring(4);
this.leftMenuStatus(id);
console.log('res', res);
// 修复:确保正确获取索引
let id = res.id ? res.id.substring(4) : index;
this.leftMenuStatus(parseInt(id));
}
})
})
},
// 设置左边菜单的滚动状态
async leftMenuStatus(index) {
this.current = index;
this.innerCurrent = index;
this.$emit('update: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到顶部的距离
getMenuItemTop() {
new Promise(resolve => {
async getMenuItemTop() {
// await this.$nextTick();
// console.log('getMenuItemTop')
return 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();
}, 10);
}, 100);
return ;
}
// console.log(rects)
this.rects = rects;
this.arr = [];
rects.forEach((rect) => {
// 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
this.arr.push(rect.top - rects[0].top);
resolve();
})
// 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))
@@ -195,7 +266,7 @@
return ;
}
}
}, 10)
}, 100)
}
}
}
@@ -210,11 +281,12 @@
.u-cate-tab__wrap {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
}
.u-search-inner {
background-color: rgb(234, 234, 234);
background-color: var(--up-bg-color, rgb(234, 234, 234));
border-radius: 100rpx;
display: flex;
align-items: center;
@@ -234,23 +306,23 @@
.u-cate-tab__item {
height: 110rpx;
background: #f6f6f6;
background: var(--up-bg-color, #f6f6f6);
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #444;
color: var(--up-content-color, #444);
font-weight: 400;
line-height: 1;
}
.u-cate-tab__item-active {
position: relative;
color: #000;
color: var(--up-main-color, #000);
font-size: 30rpx;
font-weight: 600;
background: #fff;
background: var(--up-card-bg-color, #fff);
}
.u-cate-tab__item-active::before {
@@ -268,7 +340,7 @@
.u-cate-tab__right-box {
flex: 1;
background-color: rgb(250, 250, 250);
background-color: var(--up-page-bg-color, var(--up-bg-color, rgb(250, 250, 250)));
}
.u-cate-tab__page-view {
@@ -277,7 +349,8 @@
.u-cate-tab__page-item {
margin-bottom: 30rpx;
background-color: #fff;
background-color: var(--up-card-bg-color, #fff);
border: 1px solid var(--up-border-color, #e5e7eb);
padding: 16rpx;
border-radius: 8rpx;
}
@@ -288,14 +361,14 @@
.item-title {
font-size: 26rpx;
color: $u-main-color;
color: var(--up-main-color, $u-main-color);
font-weight: bold;
}
.item-menu-name {
font-weight: normal;
font-size: 24rpx;
color: $u-main-color;
color: var(--up-main-color, $u-main-color);
}
.item-container {
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:54:16
* @FilePath : /u-view2.0/uview-ui/libs/config/props/cellGroup.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/cellGroup.js
*/
export default {
// cell-group组件的props
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CellGroupDefaultProps from './cellGroup'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CellGroupDefaultProps)
export const props = defineMixin({
props: {
// 分组标题
@@ -1,8 +1,8 @@
<template>
<view :style="[addStyle(customStyle)]" :class="[customClass]" class="u-cell-group">
<view :style="[groupStyle, 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">{{ title }}</text>
<text class="u-cell-group__title__text" :style="{ color: upThemeVar('--up-main-color', '#303133') }">{{ title }}</text>
</slot>
</view>
<view class="u-cell-group__wrapper">
@@ -32,6 +32,14 @@
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
}
@@ -39,7 +47,6 @@
</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;
@@ -64,4 +71,3 @@
}
}
</style>
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-23 20:53:09
* @FilePath : /u-view2.0/uview-ui/libs/config/props/cell.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/cell.js
*/
export default {
// cell组件的props
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CellDefaultProps from './cell'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CellDefaultProps)
export const props = defineMixin({
props: {
// 标题
@@ -7,32 +7,34 @@
<view class="u-cell__left-icon-wrap" v-if="$slots.icon || icon">
<slot name="icon" v-if="$slots.icon">
</slot>
<u-icon v-else :name="icon"
<up-icon v-else :name="icon"
:custom-style="iconStyle"
:size="size === 'large' ? 22 : 18"></u-icon>
:size="size === 'large' ? 22 : 18"></up-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="[titleTextStyle]"
<text v-else class="u-cell__title-text" :style="[cellTitleDynamicStyle, 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}`]">
<u-icon v-if="rightIcon && !$slots['right-icon']" :name="rightIcon"
:custom-style="rightIconStyle" :color="disabled ? '#c8c9cc' : 'info'"
:size="size === 'large' ? 18 : 16"></u-icon>
<up-icon v-if="rightIcon && !$slots['right-icon']" :name="rightIcon"
:custom-style="rightIconStyle" :color="disabled ? cellDisabledColor : 'info'"
:size="size === 'large' ? 18 : 16"></up-icon>
<slot v-else name="right-icon">
</slot>
</view>
@@ -91,6 +93,24 @@
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'],
@@ -113,7 +133,6 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-cell-padding: 13px 15px !default;
$u-cell-font-size: 15px !default;
@@ -128,8 +147,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: $u-bg-color !default;
$u-cell-disabled-color: #c8c9cc !default;
$u-cell-clickable-color: var(--up-hover-bg-color, #f1f1f1) !default;
$u-cell-disabled-color: $u-disabled-color !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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:54:47
* @FilePath : /u-view2.0/uview-ui/libs/config/props/checkboxGroup.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/checkboxGroup.js
*/
export default {
// checkbox-group组件
@@ -1,6 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CheckboxGroupDefaultProps from './checkboxGroup'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CheckboxGroupDefaultProps)
export const props = defineMixin({
props: {
// 标识符
@@ -14,7 +14,7 @@
/**
* checkboxGroup 复选框组
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://ijry.github.io/uview-plus/components/checkbox.html
* @tutorial https://uview-plus.jiangruyi.com/components/checkbox.html
* @property {String} name 标识符
* @property {Array} value 绑定的值
* @property {String} shape 形状,circle-圆形,square-方形 (默认 'square' )
@@ -108,15 +108,17 @@
// #ifdef VUE2
this.$emit("input", values);
// #endif
// 放在最后更新,否则change事件传出去的values不会更新
this.$emit('change', values)
// 放在最后更新,否则change事件传出去的values不会更新
this.$emit('change', values, {
isChecked: childInstance.$data['isChecked'],
name: childInstance.$props['name'] // 当前变动的checkbox的name值
})
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-checkbox-group {
@@ -1,11 +1,11 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-23 21:06:59
* @FilePath : /u-view2.0/uview-ui/libs/config/props/checkbox.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/checkbox.js
*/
export default {
// checkbox组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CheckboxDefaultProps from './checkbox'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CheckboxDefaultProps)
export const props = defineMixin({
props: {
// checkbox的名称
@@ -11,8 +11,8 @@
:class="iconClasses"
:style="[iconWrapStyle]"
>
<slot name="icon" :elIconSize="elIconSize" :elIconColor="elIconColor">
<u-icon
<slot name="icon" :elIconSize="elIconSize" :elIconColor="elIconColor" :checked="isChecked" :elDisabled="elDisabled">
<up-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 : '#2979ff');
return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : this.upThemeVar('--up-primary', '#2979ff'));
},
// 组件选未中激活时的颜色
elInactiveColor() {
return this.inactiveColor ? this.inactiveColor : (this.parentData.inactiveColor ? this.parentData.inactiveColor :
'#c8c9cc');
this.upThemeVar('--up-border-color', '#c8c9cc'));
},
// label的颜色
elLabelColor() {
return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : this.upThemeVar('--up-content-color', '#606266'))
},
// 组件的形状
elShape() {
@@ -133,7 +133,7 @@
},
elIconColor() {
const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
'#ffffff');
this.upThemeVar('--up-card-bg-color', '#ffffff'));
// 图标的颜色
if (this.elDisabled) {
// disabled状态下,已勾选的checkbox图标改为elInactiveColor
@@ -149,8 +149,12 @@
if (this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled')
}
if (this.isChecked && this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled--checked')
if (this.isChecked) {
if (this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled--checked')
} else {
classes.push('u-checkbox__icon-wrap--checked')
}
}
// 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
// #ifdef MP-ALIPAY || MP-TOUTIAO
@@ -161,7 +165,7 @@
iconWrapStyle() {
// checkbox的整体样式
const style = {}
style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : '#ffffff'
style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : this.upThemeVar('--up-card-bg-color', '#ffffff')
style.borderColor = this.isChecked && !this.elDisabled ? this.elActiveColor : this.elInactiveColor
style.width = addUnit(this.elSize)
style.height = addUnit(this.elSize)
@@ -177,7 +181,7 @@
const style = {}
if (!this.usedAlone) {
if (this.parentData.borderBottom && this.parentData.placement === 'row') {
error('检测到您将borderBottom设置为true,需要同时将u-checkbox-group的placement设置为column才有效')
error('检测到您将borderBottom设置为true,需要同时将up-checkbox-group的placement设置为column才有效')
}
// 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
if (this.parentData.borderBottom && this.parentData.placement === 'column') {
@@ -197,13 +201,14 @@
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
this.updateParentData()
if (!this.parent) {
error('u-checkbox必须搭配u-checkbox-group组件使用')
error('up-checkbox必须搭配up-checkbox-group组件使用')
}
let value = '';
// #ifdef VUE2
const value = this.parentData.value
value = this.parentData.value
// #endif
// #ifdef VUE3
const value = this.parentData.modelValue
value = this.parentData.modelValue
// #endif
// 设置初始化时,是否默认选中的状态,父组件u-checkbox-group的value可能是array,所以额外判断
if (this.checked) {
@@ -248,7 +253,9 @@
}
},
emitEvent() {
this.$emit('change', this.isChecked)
this.$emit('change', this.isChecked, {
name: this.name
})
// 双向绑定
if (this.usedAlone) {
this.$emit('update:checked', this.isChecked)
@@ -281,7 +288,6 @@
</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;
@@ -292,7 +298,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:#ebedf0 !default;
$u-checkbox-icon-wrap-disabled-background-color:var(--up-bg-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;
@@ -0,0 +1,114 @@
<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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:55:02
* @FilePath : /u-view2.0/uview-ui/libs/config/props/circleProgress.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/circleProgress.js
*/
export default {
// circleProgress 组件
@@ -1,5 +1,8 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import CircleProgressDefaultProps from './circleProgress'
import { registerComponentProps } from '../../libs/config/props.js'
const defProps = registerComponentProps(CircleProgressDefaultProps)
export const props = defineMixin({
props: {
percentage: {
@@ -37,7 +37,7 @@
/**
* CircleProgress 圆形进度条 TODO: 待完善
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度环。
* @tutorial https://ijry.github.io/uview-plus/components/circleProgress.html
* @tutorial https://uview-plus.jiangruyi.com/components/circleProgress.html
* @property {String | Number} percentage 圆环进度百分比值,为数值类型,0-100 (默认 30 )
* @example
*/
@@ -112,7 +112,6 @@
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-circle-progress {
@include flex(row);
@@ -0,0 +1,169 @@
<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 : 1.0
* @version : 3.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:55:58
* @FilePath : /u-view2.0/uview-ui/libs/config/props/codeInput.js
* @LastAuthor : jry
* @lastTime : 2025-12-19 08:55:21
* @FilePath : /uview-plus/libs/config/props/codeInput.js
*/
export default {
// codeInput 组件

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