@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+*.lock
+package-lock.json
+
+node_modules
+.DS_Store
+dist
+*.local
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
@@ -0,0 +1,9 @@
+{
+ "printWidth": 1000,
+ "tabWidth": 2,
+ "useTabs": true,
+ "singleQuote": false,
+ "semi": true,
+ "trailingComma": "none",
+ "bracketSpacing": true
+}
@@ -0,0 +1,25 @@
+# 宇宸uniapp示例项目
+## 项目特点
+- 使用vite构建,使用vue3+ts+vite+uniapp 构建
+- [vk-uvviewui](https://vkuviewdoc.fsq.pub/) 组件
+## 运行要求
+- 请使用node 18以上版本运行
+- 使用vue3+ts+vite+uniapp
+- 可以使用npm、yarn、pnpm 三种包管理工具,推荐使用pnpm
+## 运行代码
+1. 安装依赖
+```
+pnpm i
+2. 运行
+# H5
+npm run dev:h5
+# 微信小程序
+npm run dev:mp-weixin
+## 注意事项
+- 凡是编译到微信小程序端,为避免主包过大,建议除tabbar的页面外,其它页面都放到分包中,具体可参考[分包加载配置](https://uniapp.dcloud.net.cn/collocation/pages.html#subpackages)、[关于分包优化的说明](https://uniapp.dcloud.net.cn/collocation/manifest.html#%E5%85%B3%E4%BA%8E%E5%88%86%E5%8C%85%E4%BC%98%E5%8C%96%E7%9A%84%E8%AF%B4%E6%98%8E)
@@ -0,0 +1,6 @@
+@echo off
+Del /S /Q "..\tp3\Public\testh5\"
+echo "delete testh5"
+xcopy ".\dist\build\h5" "..\tp3\Public\testh5\" /s /e
+echo "finished!"
+pause
+Del /S /Q "..\tp3\Public\xxh5\"
+echo "delete xxh5"
+xcopy ".\dist\build\h5" "..\tp3\Public\xxh5\" /s /e
+Del /S /Q "..\tp3\Public\zxh5\"
+echo "delete zxh5"
+xcopy ".\dist\build\h5" "..\tp3\Public\zxh5\" /s /e
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <script>
+ var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+ CSS.supports('top: constant(a)'))
+ document.write(
+ '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+ (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+ </script>
+ <title></title>
+ <!--preload-links-->
+ <!--app-context-->
+ </head>
+ <body>
+ <div id="app"><!--app-html--></div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
@@ -0,0 +1,83 @@
+ "name": "ycxxkj_vue3_ts",
+ "version": "0.0.2",
+ "scripts": {
+ "dev:app": "uni -p app",
+ "dev:app-android": "uni -p app-android",
+ "dev:app-ios": "uni -p app-ios",
+ "dev:custom": "uni -p",
+ "dev:h5": "uni",
+ "dev:h5:ssr": "uni --ssr",
+ "dev:mp-alipay": "uni -p mp-alipay",
+ "dev:mp-baidu": "uni -p mp-baidu",
+ "dev:mp-jd": "uni -p mp-jd",
+ "dev:mp-kuaishou": "uni -p mp-kuaishou",
+ "dev:mp-lark": "uni -p mp-lark",
+ "dev:mp-qq": "uni -p mp-qq",
+ "dev:mp-toutiao": "uni -p mp-toutiao",
+ "dev:mp-weixin": "uni -p mp-weixin",
+ "dev:mp-weixin-mini": "uni -p mp-weixin --minify",
+ "dev:quickapp-webview": "uni -p quickapp-webview",
+ "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
+ "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
+ "build:app": "uni build -p app",
+ "build:app-android": "uni build -p app-android",
+ "build:app-ios": "uni build -p app-ios",
+ "build:custom": "uni build -p",
+ "build:h5": "uni build",
+ "build:h5:ssr": "uni build --ssr",
+ "build:mp-alipay": "uni build -p mp-alipay",
+ "build:mp-baidu": "uni build -p mp-baidu",
+ "build:mp-jd": "uni build -p mp-jd",
+ "build:mp-kuaishou": "uni build -p mp-kuaishou",
+ "build:mp-lark": "uni build -p mp-lark",
+ "build:mp-qq": "uni build -p mp-qq",
+ "build:mp-toutiao": "uni build -p mp-toutiao",
+ "build:mp-weixin": "uni build -p mp-weixin",
+ "build:quickapp-webview": "uni build -p quickapp-webview",
+ "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
+ "build:quickapp-webview-union": "uni build -p quickapp-webview-union",
+ "type-check": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "@dcloudio/uni-app": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-app-harmony": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-app-plus": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-components": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-h5": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-alipay": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-baidu": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-jd": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-lark": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-qq": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-toutiao": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-weixin": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-mp-xhs": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-quickapp-webview": "3.0.0-alpha-4010520240507001",
+ "@ycxxkj/uniapp": "^1.0.7",
+ "await-to-js": "^3.0.0",
+ "dayjs": "^1.11.9",
+ "js-md5": "^0.8.3",
+ "pinia": "2.0.33",
+ "vue": "^3.4.29",
+ "vue-i18n": "^9.13.1"
+ "devDependencies": {
+ "@dcloudio/types": "^3.4.8",
+ "@dcloudio/uni-automator": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-cli-shared": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/uni-stacktracey": "3.0.0-alpha-4010520240507001",
+ "@dcloudio/vite-plugin-uni": "3.0.0-alpha-4010520240507001",
+ "@types/node": "^20.4.1",
+ "@types/uni-app": "^1.4.4",
+ "@uni-helper/uni-app-types": "*",
+ "@vue/runtime-core": "^3.4.21",
+ "@vue/tsconfig": "^0.1.3",
+ "sass": "^1.63.6",
+ "sass-loader": "^13.3.2",
+ "typescript": "^4.9.4",
+ "vite": "5.2.8",
+ "vue-tsc": "^1.0.24"
+ }
@@ -0,0 +1,69 @@
+ "description": "项目配置文件",
+ "packOptions": {
+ "ignore": []
+ "setting": {
+ "bundle": false,
+ "userConfirmedBundleSwitch": false,
+ "urlCheck": true,
+ "scopeDataCheck": false,
+ "coverView": true,
+ "es6": true,
+ "postcss": true,
+ "compileHotReLoad": false,
+ "lazyloadPlaceholderEnable": false,
+ "preloadBackgroundData": false,
+ "minified": true,
+ "autoAudits": false,
+ "newFeature": false,
+ "uglifyFileName": false,
+ "uploadWithSourceMap": true,
+ "useIsolateContext": false,
+ "nodeModules": false,
+ "enhance": true,
+ "useMultiFrameRuntime": true,
+ "useApiHook": true,
+ "useApiHostProcess": true,
+ "showShadowRootInWxmlPanel": true,
+ "packNpmManually": false,
+ "enableEngineNative": false,
+ "packNpmRelationList": [],
+ "minifyWXSS": true,
+ "showES6CompileOption": false,
+ "minifyWXML": true
+ "compileType": "miniprogram",
+ "libVersion": "2.33.0",
+ "appid": "wx57671aa7b8c91b0a",
+ "projectname": "miniprogram-1",
+ "debugOptions": {
+ "hidedInDevtools": []
+ "scripts": {},
+ "staticServerOptions": {
+ "baseURL": "",
+ "servePath": ""
+ "isGameTourist": false,
+ "condition": {
+ "search": {
+ "list": []
+ "conversation": {
+ "game": {
+ "plugin": {
+ "gamePlugin": {
+ "miniprogram": {
+<script setup lang="ts">
+import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
+onLaunch(() => {
+ console.log("App Launch");
+});
+onShow(() => {
+ console.log("App Show");
+onHide(() => {
+ console.log("App Hide");
+</script>
+<style lang="scss">
+@import "./uni_modules/vk-uview-ui/index.scss";
+@import "./static/style/iconfont.css";
+@import "./static/style/common.scss";
+@import "./static/style/main.scss";
+//#ifdef H5
+uni-page-head {
+ display: none;
+//#endif
+</style>
@@ -0,0 +1,137 @@
+## 插件简绍
+### 实现原理
+> 二维码识别功能使用的是jsQR这个库,调用摄像头使用的 navigator.mediaDevices.getUserMedia 这个H5的api。通过canvas画布把摄像头获取到的数据展现到页面上,同时循环监听jsQR解析。
+### 使用环境
+经测试发现大部分浏览器都可以正常使用(微信,QQ,谷歌,火狐,safari),少数的安卓自带浏览器无法使用(浏览器内核版本过低)。需要https环境才能使用,本地测试可以在 manifest.json 中点击源码展示,找到h5 ,添加:"devServer" : { "https" : true}
+需要https环境才能使用!!!
+**小知识点:苹果设备上不论什么浏览器都是safari套壳的,不论是谷歌还是火狐都是safari套壳。这也就是代表在苹果上无需担心无法使用此插件。(造成这样的现象是因为苹果有一则协议,浏览器只能使用safari内核)**
+在安卓系统上可以打开闪光灯
+#### 通过脚手架创建的 uni 项目,需要看这里
+如果你是通过脚手架创建的 uni 的项目,需要自行安装 `jsQR` 依赖,并且修改组件中源码中的引入。通过 HBuilder 创建的项目不需要此操作。
+```bash
+# 安装 jsQR
+npm install jsqr --save
+# 修改组件源码对 jsQR 依赖
+import jsQR from "jsqr"
+### 插件使用
+**插件已支持 uni_modules 支持组件easycom,以下代码演示的是普通使用**
+``` html
+<!-- HTML -->
+ <mumu-get-qrcode @success='qrcodeSucess' @error="qrcodeError" ></mumu-get-qrcode>
+``` javascript
+// js
+import mumuGetQrcode from '@/uni_modules/mumu-getQrcode/components/mumu-getQrcode/mumu-getQrcode.vue'
+// 嫌路径长的话可以单独复制出来
+export default {
+ components: {
+ mumuGetQrcode
+ methods: {
+ qrcodeSucess(data) {
+ uni.showModal({
+ title: '成功',
+ content: data,
+ success: () => {
+ uni.navigateBack({})
+ })
+ qrcodeError(err) {
+ console.log(err)
+ title: '摄像头授权失败',
+ content: '摄像头授权失败,请检测当前浏览器是否有摄像头权限。',
+### 相关API
+##### 可传属性(Props)
+| 参数 | 说明 | 类型 | 默认值 |
+| ---------- | ------------------------------------------------- | ------- | ----------- |
+| continue | 是否连续获取。false 监听一次 true 持续监听 | Boolean | false |
+| exact | 选调用摄像头。environment 后摄像头 user 前摄像头 | String | environment |
+| size | 扫码屏幕大小。whole 全屏 balf 半屏 | String | whole |
+| definition | 调用摄像头清晰度。fasle 正常 true 高清 | Boolean | false |
+##### 事件(Events)
+| 事件名 | 说明 | 回调参数 |
+| ------- | ------------------------------------------ | ------------------ |
+| success | 检测到图中有二维码并读取到数据是回调 | 二维码数据 |
+| error | 组件内部发送错误,通常是摄像头没有调用成功 | 错误信息,详情见下 |
+**常见的错误信息:**
+- `AbortError`[中止错误]
+ 尽管用户和操作系统都授予了访问设备硬件的权利,而且未出现可能抛出`NotReadableError`异常的硬件问题,但仍然有一些问题的出现导致了设备无法被使用。
+- `NotAllowedError`[拒绝错误]
+ 用户拒绝了当前的浏览器实例的访问请求;或者用户拒绝了当前会话的访问;或者用户在全局范围内拒绝了所有媒体访问请求。
+- `NotFoundError`[找不到错误]
+ 找不到满足请求参数的媒体类型。
+- `NotReadableError`[无法读取错误]
+ 尽管用户已经授权使用相应的设备,操作系统上某个硬件、浏览器或者网页层面发生的错误导致设备无法被访问。
+- `OverconstrainedError`[无法满足要求错误]
+ 指定的要求无法被设备满足,此异常是一个类型为`OverconstrainedError`的对象,拥有一个`constraint`属性,这个属性包含了当前无法被满足的`constraint`对象,还拥有一个`message`属性,包含了阅读友好的字符串用来说明情况。
+- `SecurityError`[安全错误]
+ 在`getUserMedia()` 被调用的 [`Document`](https://developer.mozilla.org/zh-CN/docs/Web/API/Document) 上面,使用设备媒体被禁止。这个机制是否开启或者关闭取决于单个用户的偏好设置。
+- `TypeError`[类型错误]
+ constraints 对象未设置[空],或者都被设置为`false`。
+##### 插槽 (slot)
+| 插槽名称 | 说明 | 默认值 |
+| -------- | -------------------------------- | ------------------ |
+| error | 当发送错误时,在页面上显示的内容 | 相机权限被拒绝提示 |
+### 案例演示
+
+## 支持作者
+
@@ -0,0 +1,164 @@
+<template>
+ <view class="nav_bar pos_r">
+ <u-navbar :title="props.title" :background="{ background: 'rgba(' + transformationRGB(props.background) + ',' + (store.top - 2) / 100 + ')' }" :border-bottom="false" :title-color="props.color" :back-icon-color="props.color" :is-back="state.pageCount > 1 ? true : false"> </u-navbar>
+ <!-- <view @click="test1">aaaaaa</view> -->
+ <!-- <view class="status_bar" :style="{ height: state.statusBarHeight + 'px' }"></view> -->
+ <!-- <view class="nav_body" :style="{ height: state.navBarHeight + 'px' }">
+ <view class="icon">
+ <u-icon name="arrow-left" size="32" v-if="state.pageCount > 1" :color="props.color" @click="goBack('normal')"></u-icon>
+ <template v-else>
+ <u-icon name="home" size="32" :color="props.color" @click="goHome" v-if="state.showHome"></u-icon>
+ </template>
+ </view>
+ <view class="title" :style="{ color: props.color }">{{ props.title }} </view>
+ </view> -->
+</template>
+import { onPageScroll, onShow } from "@dcloudio/uni-app";
+import { ref, onMounted, onUnmounted, reactive, watch, computed, onActivated } from "vue";
+import { useScrollStore } from "@/stores/scroll";
+let props = defineProps({
+ home: {
+ type: String,
+ default: "/pages/index/index"
+ /**
+ * 标题
+ */
+ title: {
+ default: ""
+ * 颜色
+ color: {
+ default: "#000000"
+ * 背景色
+ background: {
+ default: "#65b1fb"
+let store = useScrollStore();
+let state = reactive({
+ navBarHeight: 0 as any,
+ statusBarHeight: 0 as any,
+ windowWidth: 0,
+ pageCount: 1,
+ showHome: true
+let pages: any = getCurrentPages();
+let curPage = pages[pages.length - 1];
+curPage.$vm.scrollTop = 0;
+let test1 = () => {
+ console.log("curPage.$vm.scrollTop test1", curPage.$vm.scrollTop);
+};
+ console.log("curPage.$vm.scrollTop onShow===============", curPage.$vm.scrollTop);
+ store.setTop(curPage.$vm.scrollTop);
+onMounted(() => {
+ let navHeight = computedNavHeight();
+ state.navBarHeight = navHeight.navBarHeight;
+ state.statusBarHeight = navHeight.statusBarHeight;
+ state.windowWidth = navHeight.windowWidth;
+ let path = curPage.$page.fullPath;
+ state.pageCount = pages.length;
+ if (path == props.home) {
+ state.showHome = false;
+ curPage.$vm.scrollTop = 0;
+let computedNavHeight = () => {
+ let sysInfo = uni.getSystemInfoSync();
+ console.log("系统信息:sysInfo:", sysInfo);
+ //屏幕宽度
+ let windowWidth = sysInfo.windowWidth;
+ // 胶囊位置信息
+ let rect: any = { bottom: 56, height: 32, left: 281, right: 368, top: 24, width: 87 };
+ // #ifdef MP
+ rect = uni.getMenuButtonBoundingClientRect();
+ console.log("胶囊信息:", rect);
+ // #endif
+ //状态栏的高度
+ let statusBarHeight = rect.top + 4;
+ let menuButtonRect = rect; // JSON.parse(JSON.stringify(rect));
+ // 导航栏高度
+ let navBarHeight = rect.height + 2;
+ return {
+ windowWidth,
+ statusBarHeight,
+ menuButtonRect,
+ navBarHeight
+ };
+let goBack = (type = "normal", delta = 1) => {
+ let pages = getCurrentPages();
+ if (pages.length <= 1) {
+ uni.reLaunch({ url: "/pages/index/index" });
+ return;
+ if (type != "normal") {
+ let url: string = pages[0].route ? pages[0].route : "/";
+ uni.reLaunch({ url: url });
+ // uni.reLaunch({ url: pages[0].route });
+ uni.navigateBack({
+ delta: delta
+ });
+let goHome = () => {
+ uni.reLaunch({ url: props.home });
+let transformationRGB = (hex = "#65b1fb") => {
+ hex = hex.replace("#", "");
+ let regSplit = /([0-9a-fA-F]{2})/;
+ let rgb = hex.split(regSplit) as any[];
+ rgb = rgb.filter((item) => item != "");
+ rgb = rgb.map((item) => parseInt("0x" + item));
+ // console.log("transformationRGB:", rgb);
+ return rgb;
+<style lang="scss" scoped>
+.nav_bar {
+ position: sticky;
+ top: 0;
+ z-index: 99;
+ .bg {
+ position: absolute;
+ left: 0;
+ background-color: #000;
+ width: 100%;
+ height: 100%;
+ .status_bar {
+ height: 40rpx;
+ .nav_body {
+ // background-color: #000;
+ display: flex;
+ align-items: center;
+ .icon {
+ padding: 0 24rpx;
+ .title {
+ font-size: 32rpx;
+ line-height: 32rpx;
@@ -0,0 +1,12 @@
+ <view class="p20 fs24 tc">copyright©2023 {{ state.version }}</view>
+import { reactive, ref } from "vue";
+// import Config from "../../config";
+import Config from "../../config";
+ version: Config.version
@@ -0,0 +1,39 @@
+ <image :src="state.imgsrc" :mode="props.mode" :class="props.class" />
+import { onMounted, reactive, ref } from "vue";
+import Config from "@/config";
+console.log("#debug#🚀 ~ Config:", Config);
+//编写props
+ class: {
+ src: {
+ mode: {
+ default: "aspectFill"
+ domain: Config.domain,
+ imgsrc: ""
+ //如果props.src包含https://或http://,则不需要拼接
+ if (props.src.indexOf("http://") == -1 && props.src.indexOf("https://") == -1) {
+ state.imgsrc = Config.domain + props.src;
+ } else {
+ state.imgsrc = props.src;
+ console.log("#debug#🚀 ~ onMounted ~ props.src:", props.src);
+ console.log("#debug#🚀 ~ onMounted ~ state.imgsrc:", state.imgsrc);
@@ -0,0 +1,31 @@
+/**
+ * 系统基础参数配置
+const Config = {
+ domain: "https://zx.shaoshi.ycxxkj.com", //域名
+ floder: "/", //虚拟目录
+ apiHost: "index.php", //api地址
+ apiSignKey: "123456", //签名密钥
+ version: "v1.0.6" //版本号,2024年2月21日15:47:16
+let envType = 1; //1生产环境
+// envType = 2; //公司测试,
+// envType = 3; //个人测试
+envType = 4; //小学
+if (envType == 2) {
+ Config.domain = "https://php73.ycxxkj.com";
+ Config.floder = "/shaoshi_school_test/tp3/";
+ Config.version += "-test";
+if (envType == 3) {
+ Config.domain = "http://local.lzj";
+ Config.floder = "/shaoshi_school/tp3/";
+ Config.version += "-master";
+if (envType == 4) {
+ Config.domain = "https://xx.shaoshi.ycxxkj.com";
+ Config.version += "-xx";
+Config.apiHost = Config.domain + Config.floder + Config.apiHost;
+export default Config;
@@ -0,0 +1,19 @@
+/// <reference types="vite/client" />
+declare module "*.vue" {
+ import { DefineComponent } from "vue";
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+// 环境变量 TypeScript的智能提示
+interface ImportMetaEnv {
+ VITE_APP_TITLE: string;
+ VITE_APP_PORT: string;
+ VITE_APP_BASE_API: string;
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
@@ -0,0 +1,139 @@
+export const provinceList = [
+ { label: "北京", value: "北京", selected: 0 },
+ { label: "天津", value: "天津", selected: 0 },
+ { label: "河北", value: "河北", selected: 0 },
+ { label: "山西", value: "山西", selected: 0 },
+ { label: "内蒙古", value: "内蒙古", selected: 0 },
+ { label: "辽宁", value: "辽宁", selected: 0 },
+ { label: "吉林", value: "吉林", selected: 0 },
+ { label: "黑龙江", value: "黑龙江", selected: 0 },
+ { label: "上海", value: "上海", selected: 0 },
+ { label: "江苏", value: "江苏", selected: 0 },
+ { label: "浙江", value: "浙江", selected: 0 },
+ { label: "安徽", value: "安徽", selected: 0 },
+ { label: "福建", value: "福建", selected: 0 },
+ { label: "江西", value: "江西", selected: 0 },
+ { label: "山东", value: "山东", selected: 0 },
+ { label: "河南", value: "河南", selected: 0 },
+ { label: "湖北", value: "湖北", selected: 0 },
+ { label: "湖南", value: "湖南", selected: 0 },
+ { label: "广东", value: "广东", selected: 0 },
+ { label: "广西", value: "广西", selected: 0 },
+ { label: "海南", value: "海南", selected: 0 },
+ { label: "重庆", value: "重庆", selected: 0 },
+ { label: "四川", value: "四川", selected: 0 },
+ { label: "贵州", value: "贵州", selected: 0 },
+ { label: "云南", value: "云南", selected: 0 },
+ { label: "西藏", value: "西藏", selected: 0 },
+ { label: "陕西", value: "陕西", selected: 0 },
+ { label: "甘肃", value: "甘肃", selected: 0 },
+ { label: "青海", value: "青海", selected: 0 },
+ { label: "宁夏", value: "宁夏", selected: 0 },
+ { label: "新疆", value: "新疆", selected: 0 },
+ { label: "台湾", value: "台湾", selected: 0 },
+ { label: "香港", value: "香港", selected: 0 },
+ { label: "澳门", value: "澳门", selected: 0 }
+];
+export const ispList = [
+ { label: "电信", value: "1", selected: 0 },
+ { label: "移动", value: "2", selected: 0 },
+ { label: "联通", value: "3", selected: 0 }
+export const natTypeList = [
+ { label: "公网", value: "public" },
+ { label: "内网", value: "inner" }
+export const dialTypeList = [
+ { label: "固定公网单IP", value: "staticNetSingle", type: "1" },
+ { label: "固定公网多IP", value: "staticNetCouple", type: "1" },
+ { label: "服务器拨号", value: "serverDial", type: "1" },
+ { label: "软路由", value: "virtualRoute", type: "1" },
+ { label: "静态单IP", value: "1", type: "2" }
+export const dialTypeSingleList = [{ label: "静态单IP", value: "1" }];
+export const ipTypeList = [
+ { label: "多IP", value: "1" },
+ { label: "单IP", value: "2" }
+export const ipProtocolList = [
+ { label: "双线", value: "1" },
+ { label: "IPV4", value: "2" },
+ { label: "IPV6", value: "3" }
+export const netCardList = [
+ { netDevName: "eth0", ip: "192.168.1.12", speed: "1000", selected: false },
+ { netDevName: "eth1", ip: "192.168.1.13", speed: "1000", selected: true },
+ { netDevName: "eth2", ip: "192.168.1.15", speed: "1000", selected: false },
+ { netDevName: "eth3", ip: "", speed: "1000", selected: false },
+ { netDevName: "eth4", ip: "", speed: "", selected: false }
+export const businessTypeList = [
+ { label: "PX", value: "1" },
+ { label: "QN", value: "2" }
+export const stateList = [
+ { label: "待审核", value: "bound", color: "#F33535", selected: 0, css: "dot_error" },
+ { label: "交付中", value: "waitAudit", color: "#F33535", selected: 0, css: "dot_error" },
+ { label: "验收未通过", value: "auditFailed", color: "#F33535", selected: 0, css: "dot_error" },
+ { label: "服务中", value: "inService", color: "#00af51", selected: 0, css: "dot_success" },
+ { label: "已下线", value: "offline", color: "#F33535", selected: 0, css: "dot_warning" },
+ { label: "已清退", value: "repelled", color: "#F33535", selected: 0, css: "dot_warning" }
+export const resourceTypeList = [
+ { label: "汇聚资源", value: "aggregation" },
+ { label: "专线资源", value: "dedicated" }
+export const onlineStatusList = [
+ { label: "离线", value: "outline", selected: 0, color: "#F33535", css: "dot_error" },
+ { label: "在线", value: "online", selected: 0, color: "#00af51", css: "dot_success" },
+ { label: "网络异常", value: "error", selected: 0, color: "#f0ad4e", css: "dot_warning" }
+export const dialStatusList = [
+ { label: "拨号失败", value: "failed" },
+ { label: "已拨通", value: "succeed" }
+export const connectStatusList = [
+ { label: "联网失败", value: "failed" },
+ { label: "已联网", value: "succeed" }
+export const cardTypeList = [
+ { label: "银行卡", value: 1, icon: "jc-icon-yinxingqia002", slected: 0, color: "#107c84" },
+ { label: "支付宝", value: 2, icon: "jc-icon-zhifubaozhifu", slected: 0, color: "#009fe8" },
+ { label: "微信", value: 3, icon: "jc-icon-weixinzhifu", slected: 0, color: "#6bcc03" }
+export const deviceHelper = {
+ getConnectStatusText(value: string) {
+ return this.getText(connectStatusList, value);
+ getDialStatusText(value: string) {
+ return this.getText(dialStatusList, value);
+ getOnlineStatusText(value: string) {
+ return this.getText(onlineStatusList, value);
+ getStateText(value: string) {
+ return this.getText(stateList, value);
+ getResourceTypeText(value: string) {
+ return this.getText(resourceTypeList, value);
+ getDialTypeText(value: string) {
+ return this.getText(dialTypeList, value);
+ getNatTypeText(value: string) {
+ return this.getText(natTypeList, value);
+ getText(list: any[], value: string, key: string = "label") {
+ let text = "";
+ list.forEach((item) => {
+ if (item.value === value) {
+ text = item[key];
+ return text.trim();
@@ -0,0 +1,65 @@
+ { name: "北京", value: "北京", selected: 0 },
+ { name: "天津", value: "天津", selected: 0 },
+ { name: "河北", value: "河北", selected: 0 },
+ { name: "山西", value: "山西", selected: 0 },
+ { name: "内蒙古", value: "内蒙古", selected: 0 },
+ { name: "辽宁", value: "辽宁", selected: 0 },
+ { name: "吉林", value: "吉林", selected: 0 },
+ { name: "黑龙江", value: "黑龙江", selected: 0 },
+ { name: "上海", value: "上海", selected: 0 },
+ { name: "江苏", value: "江苏", selected: 0 },
+ { name: "浙江", value: "浙江", selected: 0 },
+ { name: "安徽", value: "安徽", selected: 0 },
+ { name: "福建", value: "福建", selected: 0 },
+ { name: "江西", value: "江西", selected: 0 },
+ { name: "山东", value: "山东", selected: 0 },
+ { name: "河南", value: "河南", selected: 0 },
+ { name: "湖北", value: "湖北", selected: 0 },
+ { name: "湖南", value: "湖南", selected: 0 },
+ { name: "广东", value: "广东", selected: 0 },
+ { name: "广西", value: "广西", selected: 0 },
+ { name: "海南", value: "海南", selected: 0 },
+ { name: "重庆", value: "重庆", selected: 0 },
+ { name: "四川", value: "四川", selected: 0 },
+ { name: "贵州", value: "贵州", selected: 0 },
+ { name: "云南", value: "云南", selected: 0 },
+ { name: "西藏", value: "西藏", selected: 0 },
+ { name: "陕西", value: "陕西", selected: 0 },
+ { name: "甘肃", value: "甘肃", selected: 0 },
+ { name: "青海", value: "青海", selected: 0 },
+ { name: "宁夏", value: "宁夏", selected: 0 },
+ { name: "新疆", value: "新疆", selected: 0 },
+ { name: "台湾", value: "台湾", selected: 0 },
+ { name: "香港", value: "香港", selected: 0 },
+ { name: "澳门", value: "澳门", selected: 0 }
+export const periodList = [
+ { name: "上午", value: 1 },
+ { name: "下午", value: 2 }
+export const passStatusList = [
+ { name: "待审核", value: 0 },
+ { name: "审核通过", value: 1 },
+ { name: "审核不通过", value: -1 }
+export const EnumHelper = {
+ getPeriodText(value: string | number) {
+ value = parseInt(value.toString());
+ return this.getText(periodList, value);
+ getPassStatusText(value: string | number) {
+ return this.getText(passStatusList, value);
+ getText(list: any[], value: string | number, key: string = "name") {
+ if (item.value.toString() === value.toString()) {
+const tagInfo = "[/libs/model/Home.ts]:";
+import Http from "../net/Http";
+ * YzhApi
+const Home = {};
+export default Home;
@@ -0,0 +1,15 @@
+const tagInfo = "[/libs/model/Index.ts]:";
+ * 首页
+const Index = {
+ async login(username: string, password: string) {
+ let url = "/verify_mobile/index/login";
+ let res = await Http.post(url, { username, password });
+ return res;
+export default Index;
+const tagInfo = "[/libs/model/Coupon.ts]:";
+const LeaveApply = {
+ async getParentApply() {
+ let url = "/LeaveApply/getParentApply";
+ let res = await Http.get(url);
+ console.log("#debug#🚀 ~ getParentApply ~ res:", res);
+ async doParentApply(param: object) {
+ let url = "/LeaveApply/doParentApply?key=1";
+ let res = await Http.post(url, param);
+ async getParentApplyList(param: object) {
+ let url = "/LeaveApply/getParentApplyList";
+ let res = await Http.get(url, param, {}, false);
+ async staticDay(day: string) {
+ let url = "/Vacation/staticDay?day=" + day;
+export default LeaveApply;
@@ -0,0 +1,30 @@
+const tagInfo = "[/libs/model/Order.ts]:";
+const Order = {
+ async list(params: any) {
+ let url = "/verify_mobile/order/list";
+ let res = await Http.post(url, params, {}, false);
+ async detail(id: number) {
+ let url = "/verify_mobile/order/detail";
+ let res = await Http.post(url, { id });
+ async check(verify_code: string) {
+ let url = "/verify_mobile/order/check";
+ let res = await Http.post(url, { verify_code });
+ async verify(verify_code: string, verify_count: number) {
+ let url = "/verify_mobile/order/verify";
+ let res = await Http.post(url, { verify_code, verify_count });
+export default Order;
+ *
+const TeacherBind = {
+ async addBind(param: object) {
+ let url = "/TeacherBind/addBind";
+ async list(param: object) {
+ let url = "/TeacherBind/list";
+ let res = await Http.get(url, param);
+ async delete(id: number | string) {
+ let url = "/TeacherBind/delete";
+ let res = await Http.get(url, { id });
+ async changeLogin(id: number | string) {
+ let url = "/TeacherBind/changeLogin";
+export default TeacherBind;
@@ -0,0 +1,35 @@
+const TeacherMoral = {
+ async init() {
+ let url = "/TeacherMoral/init";
+ async searchTeacher(keyword: string) {
+ let url = "/TeacherMoral/searchTeacher";
+ let res = await Http.get(url, { keyword });
+ async add(param: object) {
+ let url = "/TeacherMoral/add";
+ let url = "/TeacherMoral/list";
+ async detail(id: number | string) {
+ let url = "/TeacherMoral/detail";
+export default TeacherMoral;
+const tagInfo = "[/libs/model/Upload.ts]:";
+ * 上传模块
+const Upload = {
+ * 上传头像
+ * @param filePath
+ * @returns
+ async uploadMemberIcon(filePath: string, member_id: string | number, name: string = "icon") {
+ let url = "url";
+ let res = Http.upload(url, filePath, name);
+export default Upload;
@@ -0,0 +1,22 @@
+const errorMsg: any = {
+ 612: "612",
+ 614: "手机号已被注册",
+ 613: "密码不正确",
+ 621: "手机号不正确",
+const ErrorCode = {
+ getMsg(code: string | number, message: string = '') {
+ let msg: string = "";
+ if (errorMsg[code]) {
+ msg = errorMsg[code];
+ return msg;
+ msg = " 接口错误,错误码:" + code;
+ if (message) {
+ msg += "," + message
+export default ErrorCode;
@@ -0,0 +1,172 @@
+import Sign from "./Sign";
+import to from "await-to-js";
+import { YcStorage } from "@ycxxkj/uniapp";
+ * 网络请求帮助类
+const http = {
+ SUCCESS_CODE: 1000,
+ async get(url: string, param: any = {}, header: object = {}, isShowLoading: boolean = true): Promise<any> {
+ let [err, res] = await to(http.request(url, param, header, isShowLoading, "GET"));
+ if (err) {
+ return err;
+ async post(url: string, param: any = {}, header: object = {}, isShowLoading: boolean = true) {
+ let [err, res] = await to(http.request(url, param, header, isShowLoading, "POST"));
+ async patch(url: string, param: any = {}, header: object = {}, isShowLoading: boolean = true) {
+ let [err, res] = await to(http.request(url, param, header, isShowLoading, "PATCH"));
+ async delete(url: string, param: any = {}, header: object = {}, isShowLoading: boolean = true) {
+ let [err, res] = await to(http.request(url, param, header, isShowLoading, "DELETE"));
+ async upload(url: string, filePath: string, name: string = "file", param: any = {}, isShowLoading: boolean = true) {
+ let [err, res] = await to(http.uploadFile(url, filePath, name, param, isShowLoading));
+ uploadFile(url: string, filePath: string, name: string = "file", param: any = {}, isShowLoading: boolean = true): Promise<any> {
+ if (isShowLoading) {
+ uni.showLoading({
+ title: "正在上传..."
+ let combineObj = http.combimeData(param);
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: Config.apiHost + url,
+ filePath: filePath,
+ name: name,
+ formData: combineObj.param,
+ header: combineObj.header,
+ success: (res: any) => {
+ let result = res.data;
+ if (typeof res.data == "string") {
+ result = JSON.parse(res.data);
+ resolve(result);
+ fail: (res: any) => {
+ reject(result);
+ complete() {
+ uni.hideLoading();
+ request(url: string, param: object, header: object = {}, isShowLoading: boolean = true, method: any = "POST"): Promise<any> {
+ title: "正在加载..."
+ let combineObj = http.combimeData(param, header);
+ console.log("#debug#🚀 ~ request ~ combineObj:", combineObj);
+ uni.request({
+ data: combineObj.param,
+ method: method,
+ success(response: any) {
+ // if (response.statusCode == 600) {
+ // //处理登录流程
+ // toLogin();
+ // return;
+ // }
+ if (response.statusCode != 200) {
+ reject({ code: response.statusCode, desc: response.data.error, data: response.data });
+ console.log("request success", response);
+ let res = response.data;
+ if (res.code == 600) {
+ //登录
+ //这里写未登录的逻辑
+ toLogin();
+ reject(res);
+ if (res.code == 602) {
+ resolve(res);
+ fail(err) {
+ let error = {
+ code: 9999,
+ msg: "网络请求错误",
+ data: err
+ reject(error);
+ * 组合header和bodydata参数
+ * @param {*} param
+ combimeData(param: any = {}, header: any = {}) {
+ let token = YcStorage.get("token"); // uni.getStorageSync("token"); //从缓存取token
+ if (!header["Content-Type"]) {
+ header["Content-Type"] = "application/json";
+ if (token) {
+ header["Authorization"] = token;
+ let openid = YcStorage.get("openid");
+ if (openid) {
+ param.openid = openid;
+ param._sourse = "uniapp";
+ // param = sign(param);//签名
+ return { param, header };
+const toLogin = () => {
+ YcStorage.clear();
+ console.log("lzj500🚀 ~ file: Http.ts:153 ~ toLogin ~ pages:", pages);
+ let callback = "/pages/index/index";
+ if (pages.length > 0) {
+ let curPage: any = pages[pages.length - 1];
+ console.log("lzj500🚀 ~ file: Http.ts:156 ~ toLogin ~ curPage:", curPage);
+ callback = curPage.$page.fullPath;
+ YcStorage.set("callback", callback);
+ uni.reLaunch({
+ url: "/pages/index/index"
+ //let curPage = pages[pages.length - 1];
+export default http;
@@ -0,0 +1,21 @@
+import md5 from "js-md5";
+import { YcStringUtil } from "@ycxxkj/uniapp";
+const api_sign_secret = Config.apiSignKey; //签名密钥
+const tagInfo = "/libs/net/sign.js";
+export default function (data: any) {
+ if (!data) {
+ data = {}; //如果没有传data就实例化一个空对象
+ const timestamp: number = new Date().getTime() / 1000; //获取当前时间戳(秒)
+ data._timestamp = timestamp;
+ const sortData: any = {};
+ Object.keys(data)
+ .sort()
+ .forEach((key) => {
+ sortData[key] = YcStringUtil.parseString(data[key]); //对data所有参数按键值进行排序
+ //console.log(tagInfo, JSON.stringify(sortData) + api_sign_secret);
+ sortData._sign = md5(JSON.stringify(sortData) + api_sign_secret); //签名=md5(参数的json字符串+签名密钥)
+ return sortData;
@@ -0,0 +1,113 @@
+// import UniHelper from "../utils/UniHelper";
+import { YcBase64, YcReg, YcStorage, YcStringUtil, YcUniapp, Yc } from "@ycxxkj/uniapp";
+import { deviceHelper } from "../data/device";
+let ycxxkj = {
+ YcBase64,
+ YcReg,
+ YcStorage,
+ YcStringUtil,
+ YcUniapp,
+ Yc,
+ useScrollStore
+const Page = {
+ SUCCESS_CODE: 0,
+ config: Config,
+ setTitle: (title: string = "核销端") => {
+ uni.setNavigationBarTitle({ title });
+ checkAuth(option: any) {
+ console.log("#debug#🚀 ~ checkAuth ~ option:", option);
+ let signStr = option.id + "_" + option.time;
+ let sign = md5(signStr);
+ console.log("#debug#🚀 ~ checkAuth ~ signStr:", signStr);
+ console.log("#debug#🚀 ~ checkAuth ~ sign:", sign);
+ if (sign != option.key) {
+ YcUniapp.alert("路径错误");
+ this.historyBack();
+ YcStorage.set("openid", option.openid);
+ historyBack() {
+ window.history.back();
+ window.location.reload();
+ getVersion: () => {
+ // @ts-ignore
+ // const version = __VERSION__ as string;
+ let version = "copyright © 1.0.3";
+ return version;
+ switch(url: string) {
+ uni.switchTab({
+ url
+ goBack(delta: number = 1) {
+ delta
+ goto: (url: string) => {
+ console.log("#debug#🚀 ~ file: Page.ts:30 ~ goto url:", url);
+ uni.navigateTo({
+ relauch(url: string) {
+ redirect(url: string) {
+ uni.redirectTo({ url });
+ isLogin() {
+ let token = ycxxkj.YcStorage.get("token");
+ return true;
+ return false;
+ onPageScroll(e: any) {
+ const store = useScrollStore();
+ // console.log("onPageScroll:e", e, store);
+ store.setTop(e.scrollTop);
+ let curPage = pages[pages.length - 1];
+ curPage.$vm.scrollTop = e.scrollTop;
+ // console.log("curPage.$vm.scrollTop", curPage.$vm, curPage.$vm.scrollTop);
+ test() {
+ console.log("onLoad yc is", ycxxkj);
+ showVal(val: any, def: string | number = "-", suffix: string = ""): string | number | any {
+ if (!val && val !== 0 && val !== "0") {
+ return def;
+ return val + suffix;
+ * 格式化金额 ,由分转为元,并保留2位小数
+ * @param val
+ priceFormat(val: string | number) {
+ let price = Number(val);
+ if (isNaN(price)) {
+ price = 0;
+ return (price / 100).toFixed(2);
+ deviceHelper,
+ ...ycxxkj
+ // ...UniHelper
+export default Page;
@@ -0,0 +1,237 @@
+// import MapHelper from "./map_helper";
+const UniHelper = {
+ async login() {
+ uni.login({
+ provider: "weixin",
+ success: (res) => {
+ console.log(res.code);
+ fail: (res) => {
+ //扫描
+ takePhoto() {
+ uni.scanCode({
+ scanType: ["qrcode"],
+ resolve(res.result);
+ copyText(data: string) {
+ uni.setClipboardData({
+ data: data,
+ success: function () {
+ console.log("success");
+ resolve(true);
+ previewImage(images: Array<string>, current = 0) {
+ console.log("previewImage", images);
+ uni.previewImage({
+ urls: images,
+ current: current,
+ longPressActions: {
+ itemList: ["发送给朋友", "保存图片", "收藏"],
+ success: function (data) {
+ console.log("选中了第" + (data.tapIndex + 1) + "个按钮,第" + (data.index + 1) + "张图片");
+ fail: function (err) {
+ console.log(err.errMsg);
+ ///========消息=============
+ toast(title: string, duration = 2000, icon: any = "none") {
+ uni.showToast({
+ title: title,
+ duration: duration,
+ icon: icon,
+ //隐藏键盘
+ hideKeyboard(timeout: number = 200) {
+ return new Promise(() => {
+ setTimeout(() => {
+ uni.hideKeyboard();
+ }, timeout);
+ * 加载中
+ * @param {*} title
+ loading(title: string = "加载中...") {
+ mask: true
+ * 隐藏 loading 提示框
+ hideLoading() {
+ * 消息提示,有确认按钮
+ * @param {*} msg
+ alert(msg: string) {
+ content: msg,
+ showCancel: false,
+ * 消息二次确认
+ * @param {*} content
+ async confirm(title: string, content: string) {
+ content: content,
+ success: function (res) {
+ if (res.confirm) {
+ console.log("用户点击确定");
+ } else if (res.cancel) {
+ console.log("用户点击取消");
+ resolve(false);
+ * 带输入的确认框
+ * @param title
+ * @param content
+ * @param placeholderText
+ * @param confirmText
+ async prompt(title: string, content: string = "", placeholderText: string = "", confirmText: string = "确定"): Promise<{ confirm: boolean; content: string | undefined }> {
+ console.log("prompt", {
+ content,
+ confirmText,
+ editable: true,
+ placeholderText
+ placeholderText,
+ resolve({ confirm: true, content: res.content });
+ resolve({ confirm: false, content: res.content });
+ ////////==========上传============//////////////////////////
+ * 选1张图片
+ async simpleChooseImage() {
+ let [err, res]: [any, any] = await to(this.chooseImage(1));
+ console.log("simpleChooseImage", err, res);
+ return res.tempFilePaths[0];
+ * 选图片,改promise
+ * @param {*} count
+ * @param {*} sizeType
+ * @param {*} sourceType
+ chooseImage(count: number = 9, sizeType = ["compressed"], sourceType = ["album", "camera "]) {
+ uni.chooseImage({
+ count, //默认9
+ sizeType, //: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
+ sourceType: ["album"], //从相册选择
+ console.log(JSON.stringify(res.tempFilePaths));
+ async scanCode(onlyFromCamera: boolean = false, scanType: string[] = ["qrCode", "barCode"]) {
+ onlyFromCamera,
+ scanType,
+ resolve({ code: 0, ...res });
+ fail: function (res) {
+ reject({ code: 999, msg: res.errMsg, ...res });
+ /* getLocation(type = "gcj02") {
+ uni.getLocation({
+ geocode: true,
+ success: async function (res) {
+ console.log("当前位置: res", res);
+ console.log("当前位置的经度:" + res.longitude);
+ console.log("当前位置的纬度:" + res.latitude);
+ let res1 = MapHelper.wgs84togcj02(res.latitude, res.longitude);
+ res.latitude = res1.lat;
+ res.longitude = res1.lon;
+ res.lat = res1.lat;
+ res.lng = res1.lon;
+ // uni.showModal({
+ // content: "当前位置的经度:" + res.longitude + "当前位置的纬度:" + res.latitude + "," + JSON.stringify(res1)
+ // })
+ } */
+export default UniHelper;
@@ -0,0 +1,16 @@
+import uView from "./uni_modules/vk-uview-ui";
+import { createSSRApp } from "vue";
+import App from "./App.vue";
+import { createPinia } from "pinia";
+export function createApp() {
+ const app = createSSRApp(App);
+ // 使用 uView UI
+ const pinia = createPinia();
+ app.use(uView);
+ app.use(pinia);
+ app,
+ pinia // 此处必须将 Pinia 返回
@@ -0,0 +1,77 @@
+ "name": "",
+ "appid": "",
+ "description": "",
+ "versionName": "1.0.0",
+ "versionCode": "100",
+ "transformPx": false,
+ /* 5+App特有相关 */
+ "app-plus": {
+ "usingComponents": true,
+ "nvueStyleCompiler": "uni-app",
+ "compilerVersion": 3,
+ "splashscreen": {
+ "alwaysShowBeforeRender": true,
+ "waiting": true,
+ "autoclose": true,
+ "delay": 0
+ /* 模块配置 */
+ "modules": {},
+ /* 应用发布信息 */
+ "distribute": {
+ /* android打包配置 */
+ "android": {
+ "permissions": [
+ "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+ "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+ "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+ "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+ "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+ "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+ "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+ "<uses-feature android:name=\"android.hardware.camera\"/>",
+ "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+ ]
+ /* ios打包配置 */
+ "ios": {},
+ /* SDK配置 */
+ "sdkConfigs": {}
+ "h5": {
+ "router": {
+ "base": "./"
+ /* 快应用特有相关 */
+ "quickapp": {},
+ /* 小程序特有相关 */
+ "mp-weixin": {
+ "appid": "wxa1a102b370aae6f5",
+ "urlCheck": false
+ "usingComponents": true
+ "mp-alipay": {
+ "mp-baidu": {
+ "mp-toutiao": {
+ "uniStatistics": {
+ "enable": false
+ "vueVersion": "3"
+ "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+ {
+ "path": "pages/index/index",
+ "style": {}
+ ],
+ "subPackages": [
+ "root": "user",
+ "pages": [
+ "path": "index/index"
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "安顺达燃气",
+ "navigationStyle": "default",
+ "navigationBarBackgroundColor": "#F8F8F8",
+ "backgroundColor": "#F8F8F8"
+ <!-- bg_main -->
+ <view class="pos_r page_box bg_main"> 看到这就是成功了 </view>
+import Page from "@/libs/pages/Page";
+import { onPageScroll, onLoad } from "@dcloudio/uni-app";
+onPageScroll(Page.onPageScroll);
+ finished: true,
+ loadmore: "loadmore" // loading / nomore /loadmore
+onLoad((options: any) => {
+ Page.setTitle("首页");
+<style lang="scss" scoped></style>
+export {}
+declare module "vue" {
+ type Hooks = App.AppInstance & Page.PageInstance;
+ interface ComponentCustomOptions extends Hooks {}
@@ -0,0 +1,552 @@
+//================common begin=================
+//颜色
+$u-color-border: #eee;
+$u-color-theme: #999;
+$u-color-warn: #f44;
+$u-color-bg: #eff4ff; //全站背景色
+$u-color-theme-light: #ccc;
+$u-color-theme-red: #f44;
+$u-type-primary: #007aff;
+$u-type-warning: #f0ad4e;
+$u-type-success: #00af51;
+$u-type-info: #5bc0de;
+$u-type-error: #dd524d;
+$u-main-color: #333;
+$u-color-content: #666;
+$u-tips-color: #999;
+$u-light-color: #ccc;
+$u-placeholder-color: #808080;
+$u-icon-color: #999;
+$u-selectd-color: #52AAFF;
+$u-color-link: #72A4E5;
+a:hover {
+ text-decoration: none;
+//边框
+.bds {
+ border: solid 1rpx $u-color-border !important;
+.bdd {
+ border: dashed 1rpx $u-color-border !important;
+.bdd_t {
+ border-top: dashed 1rpx $u-color-border !important;
+.bdd_l {
+ border-left: dashed 1rpx $u-color-border !important;
+.bdd_r {
+ border-right: dashed 1rpx $u-color-border !important;
+.bds_b {
+ border-bottom: solid 1rpx $u-color-border !important;
+.bds_t {
+ border-top: solid 1rpx $u-color-border !important;
+.bds_l {
+ border-left: solid 1rpx $u-color-border !important;
+.bds_r {
+ border-right: solid 1rpx $u-color-border !important;
+.bdd_b {
+ border-bottom: dashed 1rpx $u-color-border !important;
+.bdn {
+ border: none !important;
+.bdn_b {
+ border-bottom: none !important;
+.bdn_t {
+ border-top: none !important;
+.bdn_l {
+ border-left: none !important;
+.main_bg {
+ background-color: #F7F7F7;
+.cursor {
+ cursor: pointer;
+.cursor_none {
+ cursor: default !important;
+//浮动
+.fl {
+ float: left !important;
+.fr {
+ float: right !important;
+.clear_both {
+ clear: both;
+.clearfix:after {
+ content: "\20";
+ display: block;
+ height: 0;
+.ma0 {
+ margin: 0 auto !important;
+//文字
+.tl {
+ text-align: left !important;
+.tr {
+ text-align: right !important;
+.tc {
+ text-align: center !important;
+.fwb {
+ font-weight: bold;
+//循环
+@for $i from 1 through 9 {
+ .fwb#{$i*100} {
+ font-weight: #{$i*100};
+@for $i from 0 through 10 {
+ .opacity#{$i} {
+ opacity: calc($i / 10);
+@for $i from 12 through 100 {
+ .fs#{$i} {
+ font-size: $i*1rpx !important;
+ .lh#{$i} {
+ line-height: $i*1rpx !important;
+@for $i from 1 through 150 {
+ .h#{$i*10} {
+ height: #{$i*10}rpx !important;
+@for $i from 0 through 40 {
+ .m#{$i*5} {
+ margin: #{$i*5}rpx !important;
+ .mb#{$i*5} {
+ margin-bottom: #{$i*5}rpx !important;
+@for $i from 0 through 80 {
+ .mt#{$i*5} {
+ margin-top: #{$i*5}rpx !important
+ .ml#{$i*5} {
+ margin-left: #{$i*5}rpx !important;
+ .mr#{$i*5} {
+ margin-right: #{$i*5}rpx !important;
+@for $i from 0 through 24 {
+ .p#{$i*5} {
+ padding: #{$i*5}rpx !important;
+ .pl#{$i*5} {
+ padding-left: #{$i*5}rpx !important;
+ .pr#{$i*5} {
+ padding-right: #{$i*5}rpx !important;
+ .pb#{$i*5} {
+ padding-bottom: #{$i*5}rpx !important;
+ .pt#{$i*5} {
+ padding-top: #{$i*5}rpx !important;
+@for $i from 1 through 100 {
+ .pct#{$i} {
+ width: $i*1% !important
+ .w#{$i*20} {
+ width: #{$i*20}rpx !important;
+ .border_radius_#{$i} {
+ border-radius: #{$i}rpx;
+@for $i from 1 through 10 {
+ .nowrap#{$i} {
+ /* 超出部分隐藏 */
+ overflow: hidden;
+ /* 末尾加省略号 */
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ word-break: break-all;
+ -webkit-box-orient: vertical;
+ /* 行数,也可以设为1,用来做单行 */
+ -webkit-line-clamp:#{$i}; //在第几行加省略号
+@for $i from 0 through 100 {
+ .zindex#{$i*1} {
+ z-index: #{$i*1} !important;
+//字体省略nowrap
+.nowrap {
+ -o-text-overflow: ellipsis;
+ /*兼容opera*/
+ /*这就是省略号喽*/
+ /*设置超过的隐藏*/
+ white-space: nowrap;
+ /*设置不折行*/
+//定位
+.pos_r {
+ position: relative;
+.pos_a {
+.pos_f {
+ position: fixed;
+.pos_s {
+ top: 0
+//圆角
+.border_radius_s {
+ border-radius: 10rpx;
+.border_radius_m {
+ border-radius: 20rpx;
+.border_radius_l {
+ border-radius: 40rpx;
+//空白
+.blank_xxs {
+ height: 12rpx;
+.blank_xs {
+ height: 24rpx;
+.blank_s {
+ height: 32rpx;
+.blank_m {
+ height: 48rpx;
+.blank_l {
+ height: 60rpx;
+.blank_xl {
+ height: 72rpx;
+.blank_xxl {
+ height: 84rpx;
+.black_index {
+ height: 140rpx;
+.blank_tabbar {
+ height: 220rpx;
+//字体颜色
+.text_color_theme {
+ color: $u-color-theme;
+.text_color_theme_warn {
+ color: $u-color-warn;
+.text_color_theme_light {
+ color: $u-color-theme-light;
+.text_color_theme_red {
+ color: $u-color-theme-red;
+.text_color_primary {
+ color: $u-type-primary;
+.text_color_warning {
+ color: $u-type-warning;
+.text_color_success {
+ color: $u-type-success;
+.text_color_info {
+ color: $u-type-info;
+.text_color_error {
+ color: $u-type-error;
+.text_color_link {
+ color: $u-color-link;
+.text_color_main {
+ color: $u-main-color;
+.text_color_content {
+ color: $u-color-content;
+.text_color_tips {
+ color: $u-tips-color;
+.text_color_light {
+ color: $u-light-color;
+.text_color_placeholder {
+ color: $u-placeholder-color;
+.text_color_white {
+ color: #fff;
+.text_color_icon {
+ color: $u-icon-color;
+.text_color_selected {
+ color: $u-selectd-color;
+//盒子
+.dis_blk {
+ display: block !important;
+.dis_none {
+ display: none !important;
+.dis_inline {
+ display: inline !important;
+.dis_inb {
+ display: inline-block !important;
+.dis_flex {
+ display: flex !important;
+.dis_flex_align_center {
+.dis_flex_align_start {
+ align-items: flex-start;
+.dis_flex_align_right_center {
+ justify-content: flex-end;
+.dis_flex_align_between_center {
+ justify-content: space-between;
+.dis_flex_align_around_center {
+ justify-content: space-around;
+.dis_flex_align_content_center {
+ justify-content: center;
+.dis_flex_align_content_evenly {
+ justify-content: space-evenly;
+.dis_flex_align_center_flex_w {
+ flex-wrap: wrap;
+.bg_main {
+ background-color: $u-color-bg;
+.bg_primary {
+ background-color: $u-type-primary;
+.bg_warning {
+ background-color: $u-type-warning;
+.bg_error {
+ background-color: $u-type-error;
+.bg_success {
+ background-color: $u-type-success;
+.bg_f3 {
+ background-color: #f3f3f3;
+//背景色
+.bg_f5 {
+ background: #F5F5F5;
+.bg_f7 {
+ background: #f7f7f7;
+.bg_ff {
+ background: #fff;
+.bg_none {
+ background: none;
+.bg_light {
+ background-color: #f7fafe;
@@ -0,0 +1,43 @@
+@font-face {
+ font-family: "jc-iconfont"; /* Project id 4207988 */
+ src: url('//at.alicdn.com/t/c/font_4207988_5uc8r9kquhs.woff2?t=1692120508306') format('woff2'),
+ url('//at.alicdn.com/t/c/font_4207988_5uc8r9kquhs.woff?t=1692120508306') format('woff'),
+ url('//at.alicdn.com/t/c/font_4207988_5uc8r9kquhs.ttf?t=1692120508306') format('truetype');
+.jc-iconfont {
+ font-family: "jc-iconfont" !important;
+ font-size: 16px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+.jc-icon-zhifubaozhifu:before {
+ content: "\e634";
+.jc-icon-zhifupingtai-yinlian:before {
+ content: "\e614";
+.jc-icon-weixinzhifu:before {
+ content: "\e607";
+.jc-icon-yinxingqia002:before {
+ content: "\e62e";
+.jc-icon-xiangqing1:before {
+ content: "\ea07";
+.jc-icon-yinhangqia:before {
+ content: "\e680";
+.jc-icon-24gf-fileText:before {
+ content: "\eac4";
@@ -0,0 +1,176 @@
+view,
+text {
+ word-wrap: break-word;
+page {
+ background-color: #F3F3F3;
+.login_bg {
+ z-index: 0;
+ .bgimg {
+ width: 750rpx;
+ height: 100vh;
+.pageimg {
+ z-index: -2;
+ opacity: 0.8;
+$pageWidth: 690rpx;
+// $pageWidth: 600rpx;
+//标准宽度
+.page_width {
+ width: $pageWidth;
+//顶部图片
+.header_bg {
+//卡片
+.card_box {
+ background: #FFFFFF;
+ box-shadow: 0px 3rpx 18rpx 0px rgba(0, 71, 147, 0.09);
+ border-radius: 12rpx;
+ margin: 0 auto;
+ padding: 24rpx 36rpx;
+ .left {
+ .title_icon {
+ width: 9rpx;
+ height: 26rpx;
+ background: #f98f30;
+ border-radius: 5rpx;
+ color: #f98f30;
+.dot {
+ width: 18rpx;
+ height: 18rpx;
+ display: inline-block;
+ border-radius: 50%;
+ margin-right: 10rpx;
+.dot_primary {
+ background: #4D98F4;
+ border: 4rpx solid rgba(77, 152, 244, 0.15);
+ box-shadow:
+ 0 0 0 1px #a0cfff,
+ 0 0 0 3px #ecf5ff,
+ 0 0 2px 3px rgba(77, 152, 244, 0.15);
+.dot_error {
+ background: #F33535;
+ border: 4rpx solid rgba(243, 53, 53, 0.15);
+ 0 0 0 1px #fab6b6,
+ 0 0 0 3px #fef0f0,
+ 0 0 2px 3px rgba(#fa3534, 0.15);
+.dot_warning {
+ background: #F5A629;
+ border: 4rpx solid rgba(240, 155, 45, 0.15);
+ 0 0 0 1px #fcbd71,
+ 0 0 0 3px #fdf6ec,
+ 0 0 2px 3px rgba(240, 155, 45, 0.15);
+.dot_success {
+ background: #0CB986;
+ border: 4px solid rgba(12, 185, 134, 0.15);
+ 0 0 0 1px #71d5a1,
+ 0 0 0 3px #dbf1e1,
+ 0 0 2px 3px rgba(12, 185, 134, 0.15);
+.out_box {
+ //外围盒子
+ background: #f1f7ff;
+ // background: #000;
+ border-radius: 20rpx 20rpx 0rpx 0rpx;
+ padding: 20rpx;
+ min-height: calc(100vh - 400rpx);
+.bg_theme {
+ background: $uni-color-primary;
+.bg_main2 {
+.fixed_bottom {
+ bottom: 0;
+ z-index: 2;
+//表格
+.table_box {
+ .u-tr {
+ &:nth-child(2n) {
+ background: #f6fafe;
+ ::v-deep .u-th {
+ background: #edf4fe;
+ color: #4d98f4;
+ line-height: 40rpx;
+ font-size: 24rpx;
+ :v-deep .u-td {
+ font-size: 20rpx;
+ padding: 20rpx 0;
@@ -0,0 +1,13 @@
+import { defineStore } from "pinia";
+export const counterStore = defineStore("counter", {
+ state: () => ({
+ count: 0
+ }),
+ getters: {},
+ actions: {
+ increment() {
+ this.count++;
@@ -0,0 +1,14 @@
+export const useScrollStore = defineStore("scroll", {
+ state: () => {
+ return { top: 0 };
+ // 也可以这样定义
+ // state: () => ({ count: 0 })
+ setTop(top: number) {
+ this.top = top;
@@ -0,0 +1,5 @@
+ import { Component } from "vue";
+ const component: Component;
@@ -0,0 +1,78 @@
+ * 这里是uni-app内置的常用样式变量
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+/* 颜色变量 */
+@import "@/uni_modules/vk-uview-ui/theme.scss";
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+/* 文字基本颜色 */
+$uni-text-color: #333; // 基本色
+$uni-text-color-inverse: #fff; // 反色
+$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable: #c0c0c0;
+/* 背景颜色 */
+$uni-bg-color: #fff;
+$uni-bg-color-grey: #f8f8f8;
+$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
+$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
+/* 边框颜色 */
+$uni-border-color: #c8c7cc;
+/* 尺寸变量 */
+/* 文字尺寸 */
+$uni-font-size-sm: 12px;
+$uni-font-size-base: 14px;
+$uni-font-size-lg: 16;
+/* 图片尺寸 */
+$uni-img-size-sm: 20px;
+$uni-img-size-base: 26px;
+$uni-img-size-lg: 40px;
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+/* 文章场景相关 */
+$uni-color-title: #2c405a; // 文章标题颜色
+$uni-font-size-title: 20px;
+$uni-color-subtitle: #555; // 二级标题颜色
+$uni-font-size-subtitle: 18px;
+$uni-color-paragraph: #3f536e; // 文章段落颜色
+$uni-font-size-paragraph: 15px;
@@ -0,0 +1,147 @@
+## 1.5.0(2023-06-01)
+* 【修复】`u-parse` 在vue3真机白屏问题和h5样式污染问题
+* 【修复】`u-select` 默认值的问题
+* 【修复】`u-parse` 在app和微信小程序中 `preview` 属性不生效的问题
+## 1.4.8(2023-05-17)
+* 【优化】部分组件属性的可选值的代码提示
+* 【优化】`u-circle-progres` 组件 在进度条到100%的时候发出一个 `finish` 事件
+* 【修复】`u-picker` 的时间选择器在ios下默认值无效的bug
+* 【修复】`u-card` 圆角问题
+* 【修复】`u-button` 支持 `info` 类型
+## 1.4.7(2023-04-24)
+* 【修复】`u-picker` 在vue3的App环境下默认值无效的bug
+## 1.4.5(2022-12-05)
+* 【修复】`u-row`、`u-col` 使用 `@click.stop` 会报错的问题,同时建议改用 `@click.native.stop` 来代替 `@click.stop`
+## 1.4.4(2022-11-12)
+* 【修复】`u-calendar` `u-modal` `u-picker` `u-popup` 组件在页面进入后马上需要弹窗时,无法正常弹窗的问题。
+## 1.4.3(2022-10-22)
+* 【优化】部分组件的细节
+## 1.4.2(2022-10-15)
+* 【修复】`1.4.1` 引出的 `u-subsection` 的部分问题
+## 1.4.1(2022-10-14)
+* 【修复】`u-count-to` 若设置了千分位符合,会错误显示-的问题
+* 【修复】`u-subsection` 部分细节问题
+## 1.4.0(2022-10-07)
+* 【修复】`u-section` 点击更多,会触发两次事件的问题
+* 【修复】`loadMore` 加载更多,icon-type为circle不会转动的问题
+* 【修复】`u-subsection` current 有转入值时,变更值,样式不更新(需用 `v-model="current"` 代替 `:current="current"`)
+## 1.3.13(2022-09-28)
+* 【修复】`u-avatar-cropper` 组件在vue3中会报错的问题。
+## 1.3.12(2022-08-30)
+* 【优化】`u-keyboard` 组件内部细节。
+## 1.3.11(2022-08-30)
+* 【修复】`u-subsection` 组件的 `list` 属性不支持动态修改的问题。
+## 1.3.10(2022-07-30)
+* 【优化】上传组件部分细节
+## 1.3.9(2022-07-07)
+* 【更新】省市区数据源
+* 【优化】`u-subsection` 组件支持在右上角显示数字角标
+```html
+ <u-subsection :list="list"></u-subsection>
+```js
+ data() {
+ list: [
+ name: '待发货',
+ num: 10
+ name: '待付款',
+ num: 5
+ name: '待评价',
+ num: 15
+## 1.3.8(2022-06-13)
+* 【优化】组件 `u-icon`,使之更方便的兼容第三方icon(满足规则自动计算customPrefix)
+**规则如下:**
+* 当 `name` 中包含 `-icon-` 字符串时
+* 如 `vk-icon-goods`,则组件的 `customPrefix` 属性自动识别为 `vk-icon` ,`name`属性 自动识别为 `goods`
+* 如 `vk-2-icon-goods-list`,则组件的 `customPrefix` 属性自动识别为 `vk-2-icon` ,`name`属性 自动识别为 `goods-list`
+## 1.3.7(2022-06-10)
+* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall` 在 `vue3` 模式下的细节问题。
+## 1.3.6(2022-06-10)
+## 1.3.5(2022-05-28)
+* 【优化】组件 `u-mask` `u-popup` `u-select` `u-modal` `u-keyboard` `u-calendar` `u-action-sheet` `u-picker` 均新增 `blur` 属性,可用于设置弹出遮罩的模糊度,默认为0(不模糊)
+* 
+## 1.3.4(2022-05-03)
+* 【修复】`u-tabs` 组件细节问题。
+## 1.1.4(2022-03-22)
+* 【修复】`u-field` 组件 `arrowDirection` 属性无效的问题。
+## 1.1.3(2022-03-21)
+* 【优化】部分细节。
+## 1.1.2(2022-03-21)
+## 1.1.1(2022-03-17)
+## 1.1.0(2022-03-12)
+* 【重要】`u-picker` 组件新增 `regionDiscern` 方法 智能识别省市区街道地址
+如将字符串 `浙江省杭州市西湖区希望路1333弄是啊我庭12号楼1203` 中识别为
+```json
+ "province": {
+ "code": "330000",
+ "name": "浙江省"
+ "city": {
+ "code": "330100",
+ "name": "杭州市"
+ "area": {
+ "code": "330106",
+ "name": "西湖区"
+ "address": "龙井路1号",
+ "formatted_address": "浙江省杭州市西湖区龙井路1号"
+而组件的 `addressDiscern` 方法还可以识别收货信息,如 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
+## 1.0.13(2022-03-12)
+## 1.0.12(2022-03-09)
+* 【修复】`u-radio-group` 在 vue3 模式下,设置默认值可能会无效的问题。
+## 1.0.11(2022-03-07)
+## 1.0.10(2022-03-05)
+* 【修复】`u-radio` 中的值相等的判断 == 改为 ===
+* 【优化】部分注释的错别字。
+## 1.0.9(2022-03-03)
+* 【修复】`u-parse` 在 vue3模式下编译到app无法正常显示的问题。
+## 1.0.8(2022-02-26)
+* 【优化】`u-form` 组件新增2个属性 `inputAlign` 和 `clearable` 用于统一设置表单内所有 `u-input` 组件的对应属性默认值
+* 【优化】更新城市数据源信息
+## 1.0.7(2022-02-25)
+* 【重要】`u-picker` 组件新增 `addressDiscern` 方法 智能识别收货信息
+如在 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
+即使这样的字符串也能识别 `!!!!~~~$张三~~~上海市嘉定区希望路1333弄是啊我庭12号楼1203【【【【13888888888】`
+## 1.0.6(2022-02-24)
+* 【优化】`u-form-item` 组件的 `prop` 属性支持 a.b 形式
+## 1.0.5(2022-01-11)
+* 【修复】`u-sticky` 组件 在微信小程序中无法正常吸顶的问题
+## 1.0.4(2021-12-31)
+* 【优化】`u-dropdown-item` 组件 0和"" 无法区分的问题。
+* 【修复】`u-modal` 在Vue3版本中使用了mask-close-able属性无效的问题
+## 1.0.3(2021-12-20)
+【优化】u-icon在微信小程序下可能会显示null字符串的问题
+## 1.0.2(2021-12-09)
+* 1、【优化】`u-button` 组件新增 `timerId` 属性
+* 之前的效果是:所有按钮一定时间内只能点击1次(`共用计算时间`)导致点击按钮A后无法马上点击按钮B
+* 优化的效果是:每个按钮一定时间内只能点击1次(`分开计算时间`)且支持设置相同的 timerId 来达到指定按钮 `共用计算时间`
+## 1.0.1(2021-11-22)
+* 修复 u-parse 组件在微信小程序上的显示问题。
+## 1.0.0(2021-11-18)
+uView Vue3.0 横空出世,继承uView1.0意志,再战江湖,风云再起!by vk 2021-11-18
@@ -0,0 +1,246 @@
+ <u-popup
+ :blur="blur"
+ mode="bottom"
+ :border-radius="borderRadius"
+ :popup="false"
+ v-model="popupValue"
+ :maskCloseAble="maskCloseAble"
+ length="auto"
+ :safeAreaInsetBottom="safeAreaInsetBottom"
+ @close="popupClose"
+ :z-index="uZIndex"
+ >
+ <view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
+ <text>{{ tips.text }}</text>
+ <block v-for="(item, index) in list" :key="index">
+ <view
+ @touchmove.stop.prevent
+ @tap="itemClick(index)"
+ :style="[itemStyle(index)]"
+ class="u-action-sheet-item u-line-1"
+ :class="[index < list.length - 1 ? 'u-border-bottom' : '']"
+ :hover-stay-time="150"
+ <text>{{ item[labelName] }}</text>
+ <text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">
+ {{ item.subText }}
+ </text>
+ </block>
+ <view class="u-gab" v-if="cancelBtn"></view>
+ class="u-actionsheet-cancel u-action-sheet-item"
+ hover-class="u-hover-class"
+ v-if="cancelBtn"
+ @tap="close"
+ {{ cancelText }}
+ </u-popup>
+<script>
+ * actionSheet 操作菜单
+ * @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
+ * @tutorial https://www.uviewui.com/components/actionSheet.html
+ * @property {Array<Object>} list 按钮的文字数组,见官方文档示例
+ * @property {Object} tips 顶部的提示文字,见官方文档示例
+ * @property {String} cancel-text 取消按钮的提示文字
+ * @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
+ * @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
+ * @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Number String} z-index z-index值(默认1075)
+ * @event {Function} click 点击ActionSheet列表项时触发
+ * @event {Function} close 点击取消按钮时触发
+ * @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
+ name: "u-action-sheet",
+ emits: ["update:modelValue", "input", "click", "close"],
+ props: {
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ type: Boolean,
+ default: false
+ modelValue: {
+ // 点击遮罩是否可以关闭actionsheet
+ maskCloseAble: {
+ default: true
+ // 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
+ list: {
+ type: Array,
+ default() {
+ // 如下
+ // return [{
+ // text: '确定',
+ // color: '',
+ // fontSize: ''
+ // }]
+ return [];
+ // 顶部的提示文字
+ tips: {
+ type: Object,
+ text: "",
+ color: "",
+ fontSize: "26"
+ // 底部的取消按钮
+ cancelBtn: {
+ // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+ safeAreaInsetBottom: {
+ // 弹出的顶部圆角值
+ borderRadius: {
+ type: [String, Number],
+ default: 0
+ // 弹出的z-index值
+ zIndex: {
+ // 取消按钮的文字提示
+ cancelText: {
+ default: "取消"
+ // 自定义label属性名
+ labelName: {
+ default: "text"
+ // 遮罩的模糊度
+ blur: {
+ type: [Number, String],
+ computed: {
+ valueCom() {
+ // #ifndef VUE3
+ return this.value;
+ // #ifdef VUE3
+ return this.modelValue;
+ // 顶部提示的样式
+ tipsStyle() {
+ let style = {};
+ if (this.tips.color) style.color = this.tips.color;
+ if (this.tips.fontSize) style.fontSize = this.tips.fontSize + "rpx";
+ return style;
+ // 操作项目的样式
+ itemStyle() {
+ return index => {
+ if (this.list[index].color) style.color = this.list[index].color;
+ if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + "rpx";
+ // 选项被禁用的样式
+ if (this.list[index].disabled) style.color = "#c0c4cc";
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+ popupValue: false
+ watch: {
+ valueCom(v1, v2) {
+ this.popupValue = v1;
+ // 点击取消按钮
+ close() {
+ // 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
+ // 这是一个vue发送事件的特殊用法
+ this.popupClose();
+ this.$emit("close");
+ // 弹窗关闭
+ popupClose() {
+ this.$emit("input", false);
+ this.$emit("update:modelValue", false);
+ // 点击某一个item
+ itemClick(index) {
+ // disabled的项禁止点击
+ if (this.list[index].disabled) return;
+ this.$emit("click", index);
+@import "../../libs/css/style.components.scss";
+.u-tips {
+ font-size: 26rpx;
+ text-align: center;
+ padding: 34rpx 0;
+ line-height: 1.5;
+.u-action-sheet-item {
+ @include vue-flex;
+ line-height: 1;
+ flex-direction: column;
+.u-action-sheet-item__subtext {
+ margin-top: 20rpx;
+.u-gab {
+ background-color: rgb(234, 234, 236);
+.u-actionsheet-cancel {
@@ -0,0 +1,257 @@
+ <view class="u-alert-tips" v-if="show" :class="[
+ !show ? 'u-close-alert-tips': '',
+ type ? 'u-alert-tips--bg--' + type + '-light' : '',
+ type ? 'u-alert-tips--border--' + type + '-disabled' : '',
+ ]" :style="{
+ backgroundColor: bgColor,
+ borderColor: borderColor
+ }">
+ <view class="u-icon-wrap">
+ <u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
+ <view class="u-alert-content" @tap.stop="click">
+ <view class="u-alert-title" :style="[uTitleStyle]">
+ {{title}}
+ <view v-if="description" class="u-alert-desc" :style="[descStyle]">
+ {{description}}
+ <u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
+ :size="22" class="u-close-icon" :style="{
+ top: description ? '18rpx' : '24rpx'
+ }"></u-icon>
+ <text v-if="closeAble && closeText" class="u-close-text" :style="{
+ }">{{closeText}}</text>
+ * alertTips 警告提示
+ * @description 警告提示,展现需要关注的信息
+ * @tutorial https://uviewui.com/components/alertTips.html
+ * @property {String} title 显示的标题文字
+ * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
+ * @property {String} type 关闭按钮(默认为叉号icon图标)
+ * @property {String} icon 图标名称
+ * @property {Object} icon-style 图标的样式,对象形式
+ * @property {Object} title-style 标题的样式,对象形式
+ * @property {Object} desc-style 描述的样式,对象形式
+ * @property {String} close-able 用文字替代关闭图标,close-able为true时有效
+ * @property {Boolean} show-icon 是否显示左边的辅助图标
+ * @property {Boolean} show 显示或隐藏组件
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 点击关闭按钮时触发
+ export default {
+ name: 'u-alert-tips',
+ emits: ["click", "close"],
+ // 显示文字
+ default: ''
+ // 主题,success/warning/info/error
+ type: {
+ default: 'warning'
+ // 辅助性文字
+ description: {
+ // 是否可关闭
+ closeAble: {
+ // 关闭按钮自定义文本
+ closeText: {
+ // 是否显示图标
+ showIcon: {
+ // 文字颜色,如果定义了color值,icon会失效
+ // 背景颜色
+ bgColor: {
+ // 边框颜色
+ borderColor: {
+ // 是否显示
+ show: {
+ // 左边显示的icon
+ icon: {
+ // icon的样式
+ iconStyle: {
+ return {}
+ // 标题的样式
+ titleStyle: {
+ // 描述文字的样式
+ descStyle: {
+ uTitleStyle() {
+ // 如果有描述文字的话,标题进行加粗
+ style.fontWeight = this.description ? 500 : 'normal';
+ // 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
+ return this.$u.deepMerge(style, this.titleStyle);
+ uIcon() {
+ // 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
+ return this.icon ? this.icon : this.$u.type2icon(this.type);
+ uIconType() {
+ // 如果有设置图标的样式,优先使用,没有的话,则用type的样式
+ return Object.keys(this.iconStyle).length ? '' : this.type;
+ // 点击内容
+ click() {
+ this.$emit('click');
+ // 点击关闭按钮
+ this.$emit('close');
+ @import "../../libs/css/style.components.scss";
+ .u-alert-tips {
+ padding: 16rpx 30rpx;
+ border-radius: 8rpx;
+ transition: all 0.3s linear;
+ border: 1px solid #fff;
+ &--bg--primary-light {
+ background-color: $u-type-primary-light;
+ &--bg--info-light {
+ background-color: $u-type-info-light;
+ &--bg--success-light {
+ background-color: $u-type-success-light;
+ &--bg--warning-light {
+ background-color: $u-type-warning-light;
+ &--bg--error-light {
+ background-color: $u-type-error-light;
+ &--border--primary-disabled {
+ border-color: $u-type-primary-disabled;
+ &--border--success-disabled {
+ border-color: $u-type-success-disabled;
+ &--border--error-disabled {
+ border-color: $u-type-error-disabled;
+ &--border--warning-disabled {
+ border-color: $u-type-warning-disabled;
+ &--border--info-disabled {
+ border-color: $u-type-info-disabled;
+ .u-close-alert-tips {
+ opacity: 0;
+ visibility: hidden;
+ .u-icon {
+ margin-right: 16rpx;
+ .u-alert-title {
+ font-size: 28rpx;
+ .u-alert-desc {
+ text-align: left;
+ color: $u-content-color;
+ .u-close-icon {
+ top: 20rpx;
+ right: 20rpx;
+ .u-close-hover {
+ color: red;
+ .u-close-text {
@@ -0,0 +1,290 @@
+ <view class="content">
+ <view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
+ <canvas
+ class="cropper"
+ :disable-scroll="true"
+ @touchstart="touchStart"
+ @touchmove="touchMove"
+ @touchend="touchEnd"
+ :style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
+ canvas-id="cropper"
+ id="cropper"
+ ></canvas>
+ :style="{
+ position: 'fixed',
+ top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
+ width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ height: `${cropperOpt.height * cropperOpt.pixelRatio}`
+ }"
+ canvas-id="targetId"
+ id="targetId"
+ <view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
+ <!-- #ifdef H5 -->
+ <view class="upload" @tap="uploadTap">选择图片</view>
+ <!-- #endif -->
+ <!-- #ifndef H5 -->
+ <view class="upload" @tap="uploadTap">重新选择</view>
+ <view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
+import WeCropper from './weCropper.js';
+ // 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
+ // mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
+ boundStyle: {
+ lineWidth: 4,
+ borderColor: 'rgb(245, 245, 245)',
+ mask: 'rgba(0, 0, 0, 0.35)'
+ // // 裁剪框宽度,单位rpx
+ // rectWidth: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 裁剪框高度,单位rpx
+ // rectHeight: {
+ // // 输出图片宽度,单位rpx
+ // destWidth: {
+ // // 输出图片高度,单位rpx
+ // destHeight: {
+ // // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
+ // fileType: {
+ // type: String,
+ // default: 'jpg',
+ // // 生成的图片质量
+ // // H5上无效,目前不考虑使用此参数
+ // quality: {
+ // type: [Number, String],
+ // default: 1
+ // 底部导航的高度
+ bottomNavHeight: 50,
+ originWidth: 200,
+ width: 0,
+ height: 0,
+ cropperOpt: {
+ id: 'cropper',
+ targetId: 'targetCropper',
+ pixelRatio: 1,
+ scale: 2.5,
+ zoom: 8,
+ cut: {
+ x: (this.width - this.originWidth) / 2,
+ y: (this.height - this.originWidth) / 2,
+ width: this.originWidth,
+ height: this.originWidth
+ lineWidth: uni.upx2px(this.boundStyle.lineWidth),
+ mask: this.boundStyle.mask,
+ color: this.boundStyle.borderColor
+ // 裁剪框和输出图片的尺寸,高度默认等于宽度
+ // 输出图片宽度,单位px
+ destWidth: 200,
+ // 裁剪框宽度,单位px
+ rectWidth: 200,
+ // 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
+ fileType: 'jpg',
+ src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
+ onLoad(option) {
+ let rectInfo = uni.getSystemInfoSync();
+ this.width = rectInfo.windowWidth;
+ this.height = rectInfo.windowHeight - this.bottomNavHeight;
+ this.cropperOpt.width = this.width;
+ this.cropperOpt.height = this.height;
+ this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
+ if (option.destWidth) this.destWidth = option.destWidth;
+ if (option.rectWidth) {
+ let rectWidth = Number(option.rectWidth);
+ this.cropperOpt.cut = {
+ x: (this.width - rectWidth) / 2,
+ y: (this.height - rectWidth) / 2,
+ width: rectWidth,
+ height: rectWidth
+ this.rectWidth = option.rectWidth;
+ if (option.fileType) this.fileType = option.fileType;
+ // 初始化
+ this.cropper = new WeCropper(this.cropperOpt)
+ .on('ready', ctx => {
+ // wecropper is ready for work!
+ .on('beforeImageLoad', ctx => {
+ // before picture loaded, i can do something
+ .on('imageLoad', ctx => {
+ // picture loaded
+ .on('beforeDraw', (ctx, instance) => {
+ // before canvas draw,i can do something
+ // 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
+ uni.setNavigationBarColor({
+ frontColor: '#ffffff',
+ backgroundColor: '#000000'
+ count: 1, // 默认9
+ sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+ success: res => {
+ this.src = res.tempFilePaths[0];
+ // 获取裁剪图片资源后,给data添加src属性及其值
+ this.cropper.pushOrign(this.src);
+ touchStart(e) {
+ this.cropper.touchStart(e);
+ touchMove(e) {
+ this.cropper.touchMove(e);
+ touchEnd(e) {
+ this.cropper.touchEnd(e);
+ getCropperImage(isPre = false) {
+ if(!this.src) return this.$u.toast('请先选择图片再裁剪');
+ let cropper_opt = {
+ destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
+ destWidth: Number(this.destWidth),
+ fileType: this.fileType
+ this.cropper.getCropperImage(cropper_opt, (path, err) => {
+ title: '温馨提示',
+ content: err.message
+ if (isPre) {
+ current: '', // 当前显示图片的 http 链接
+ urls: [path] // 需要预览的图片 http 链接列表
+ uni.$emit('uAvatarCropper', path);
+ this.$u.route({
+ type: 'back'
+ uploadTap() {
+ const self = this;
+ sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ self.src = res.tempFilePaths[0];
+ self.cropper.pushOrign(this.src);
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+.content {
+ background: rgba(255, 255, 255, 1);
+.cropper {
+ z-index: 11;
+.cropper-buttons {
+ background-color: #000000;
+ color: #eee;
+.cropper-wrapper {
+ flex-direction: row;
+ width: 100vw;
+.cropper-buttons .upload,
+.cropper-buttons .getCropperImage {
+ width: 50%;
+.cropper-buttons .upload {
+ padding-left: 50rpx;
+ text-align: right;
+ padding-right: 50rpx;
@@ -0,0 +1,153 @@
+ <view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
+ bottom: bottom + 'rpx',
+ right: right + 'rpx',
+ borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
+ zIndex: uZIndex,
+ opacity: opacity
+ }, customStyle]">
+ <view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
+ <u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
+ <view class="u-back-top__content__tips">
+ {{tips}}
+ <slot v-else />
+ name: 'u-back-top',
+ // 返回顶部的形状,circle-圆形,square-方形
+ default: 'circle'
+ // 自定义图标
+ default: 'arrow-upward'
+ // 提示文字
+ // 返回顶部滚动时间
+ duration: {
+ default: 100
+ // 滚动距离
+ scrollTop: {
+ // 距离顶部多少距离显示,单位rpx
+ top: {
+ default: 400
+ // 返回顶部按钮到底部的距离,单位rpx
+ bottom: {
+ default: 200
+ // 返回顶部按钮到右边的距离,单位rpx
+ right: {
+ default: 40
+ // 层级
+ default: '9'
+ // 图标的样式,对象形式
+ color: '#909399',
+ fontSize: '38rpx'
+ // 整个组件的样式
+ customStyle: {
+ showBackTop(nVal, oVal) {
+ // 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
+ // 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
+ if(nVal) {
+ this.uZIndex = this.zIndex;
+ this.opacity = 1;
+ this.uZIndex = -1;
+ this.opacity = 0;
+ showBackTop() {
+ // 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
+ // 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
+ return this.scrollTop > uni.upx2px(this.top);
+ // 不透明度,为了让组件有一个显示和隐藏的过渡动画
+ opacity: 0,
+ // 组件的z-index值,隐藏时设置为-1,就会看不到
+ uZIndex: -1
+ backToTop() {
+ uni.pageScrollTo({
+ scrollTop: 0,
+ duration: this.duration
+ .u-back-top {
+ width: 80rpx;
+ height: 80rpx;
+ z-index: 9;
+ background-color: #E1E1E1;
+ transition: opacity 0.4s;
+ &__content {
+ &__tips {
+ transform: scale(0.8);
@@ -0,0 +1,216 @@
+ <view v-if="show" class="u-badge" :class="[
+ isDot ? 'u-badge-dot' : '',
+ size == 'mini' ? 'u-badge-mini' : '',
+ type ? 'u-badge--bg--' + type : ''
+ ]" :style="[{
+ top: offset[0] + 'rpx',
+ right: offset[1] + 'rpx',
+ fontSize: fontSize + 'rpx',
+ position: absolute ? 'absolute' : 'static',
+ color: color,
+ backgroundColor: bgColor
+ }, boxStyle]"
+ {{showText}}
+ * badge 角标
+ * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+ * @tutorial https://www.uviewui.com/components/badge.html
+ * @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
+ * @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
+ * @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
+ * @property {String Number} overflow-count 展示封顶的数字值(默认99)
+ * @property {String} type 使用预设的背景颜色(默认error)
+ * @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
+ * @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
+ * @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
+ * @property {String} color 字体颜色(默认#ffffff)
+ * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
+ * @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
+ * @example <u-badge type="error" count="7"></u-badge>
+ name: 'u-badge',
+ // primary,warning,success,error,info
+ default: 'error'
+ // default, mini
+ size: {
+ default: 'default'
+ //是否是圆点
+ isDot: {
+ // 显示的数值内容
+ count: {
+ // 展示封顶的数字值
+ overflowCount: {
+ type: Number,
+ default: 99
+ // 当数值为 0 时,是否展示 Badge
+ showZero: {
+ // 位置偏移
+ offset: {
+ default: () => {
+ return [20, 20]
+ // 是否开启绝对定位,开启了offset才会起作用
+ absolute: {
+ // 字体大小
+ fontSize: {
+ default: '24'
+ // 字体演示
+ default: '#ffffff'
+ // badge的背景颜色
+ // 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
+ isCenter: {
+ // 是否将badge中心与父组件右上角重合
+ boxStyle() {
+ if(this.isCenter) {
+ style.top = 0;
+ style.right = 0;
+ // Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
+ style.transform = "translateY(-50%) translateX(50%)";
+ style.top = this.offset[0] + 'rpx';
+ style.right = this.offset[1] + 'rpx';
+ style.transform = "translateY(0) translateX(0)";
+ // 如果尺寸为mini,后接上scale()
+ if(this.size == 'mini') {
+ style.transform = style.transform + " scale(0.8)";
+ // isDot类型时,不显示文字
+ showText() {
+ if(this.isDot) return '';
+ else {
+ if(this.count > this.overflowCount) return `${this.overflowCount}+`;
+ else return this.count;
+ // 是否显示组件
+ show() {
+ // 如果count的值为0,并且showZero设置为false,不显示组件
+ if(this.count == 0 && this.showZero == false) return false;
+ else return true;
+ .u-badge {
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ line-height: 24rpx;
+ padding: 4rpx 8rpx;
+ border-radius: 100rpx;
+ &--bg--primary {
+ &--bg--error {
+ &--bg--success {
+ &--bg--info {
+ background-color: $u-type-info;
+ &--bg--warning {
+ .u-badge-dot {
+ height: 16rpx;
+ width: 16rpx;
+ .u-badge-mini {
+ transform-origin: center center;
+ // .u-primary {
+ // background: $u-type-primary;
+ // color: #fff;
+ // .u-error {
+ // background: $u-type-error;
+ // .u-warning {
+ // background: $u-type-warning;
+ // .u-success {
+ // background: $u-type-success;
+ // .u-black {
+ // background: #585858;
+ .u-info {
@@ -0,0 +1,661 @@
+ <button
+ id="u-wave-btn"
+ class="u-btn u-line-1 u-fix-ios-appearance"
+ :class="['u-size-' + size, plain ? 'u-btn--' + type + '--plain' : '', loading ? 'u-loading' : '', shape == 'circle' ? 'u-round-circle' : '', hairLine ? showHairLineBorder : 'u-btn--bold-border', 'u-btn--' + type, disabled ? `u-btn--${type}--disabled` : '']"
+ :hover-start-time="Number(hoverStartTime)"
+ :hover-stay-time="Number(hoverStayTime)"
+ :disabled="disabled"
+ :form-type="formType"
+ :open-type="openType"
+ :app-parameter="appParameter"
+ :hover-stop-propagation="hoverStopPropagation"
+ :send-message-title="sendMessageTitle"
+ send-message-path="sendMessagePath"
+ :lang="lang"
+ :data-name="dataName"
+ :session-from="sessionFrom"
+ :send-message-img="sendMessageImg"
+ :show-message-card="showMessageCard"
+ @getphonenumber="getphonenumber"
+ @getuserinfo="getuserinfo"
+ @error="error"
+ @opensetting="opensetting"
+ @launchapp="launchapp"
+ @chooseavatar="chooseavatar"
+ :style="[
+ customStyle,
+ overflow: ripple ? 'hidden' : 'visible'
+ ]"
+ @tap.stop="click($event)"
+ :hover-class="getHoverClass"
+ :loading="loading"
+ <slot></slot>
+ v-if="ripple"
+ class="u-wave-ripple"
+ :class="[waveActive ? 'u-wave-active' : '']"
+ top: rippleTop + 'px',
+ left: rippleLeft + 'px',
+ width: fields.targetWidth + 'px',
+ height: fields.targetWidth + 'px',
+ 'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
+ ></view>
+ </button>
+ * button 按钮
+ * @description Button 按钮
+ * @tutorial https://www.uviewui.com/components/button.html
+ * @property {String} size 按钮的大小
+ * @value large 大
+ * @value normal 常规
+ * @value mini 小
+ * @property {Boolean} ripple 是否开启点击水波纹效果
+ * @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
+ * @property {String} type 按钮的样式类型
+ * @value info 默认按钮
+ * @value primary 主要按钮
+ * @value error 危险按钮
+ * @value warning 警告按钮
+ * @value success 成功按钮
+ * @property {Boolean} plain 按钮是否镂空,背景色透明
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
+ * @property {Boolean} shape 按钮外观形状,见文档说明
+ * @value square 矩形
+ * @value circle 圆角
+ * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
+ * @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ * @property {String} openType 开放能力
+ * @value feedback 通用 - 打开“意见反馈”页面,用户可提交反馈内容并上传日志(App、微信小程序、QQ小程序)
+ * @value share 通用 - 触发用户转发(微信小程序、百度小程序、支付宝小程序、字节跳动小程序、飞书小程序、QQ小程序、快手小程序、京东小程序、360小程序)
+ * @value getUserInfo 通用 - 获取用户信息(微信小程序、百度小程序、QQ小程序、快手小程序、京东小程序、360小程序)
+ * @value contact 通用 - 打开客服会话,如果用户在会话中点击消息卡片后返回应用,可以从 回调中获得具体信息(微信小程序、百度小程序、快手小程序、字节小程序)
+ * @value getPhoneNumber 通用 - 获取用户手机号(微信小程序、百度小程序、字节跳动小程序、支付宝小程序、快手小程序、京东小程序。App平台另见一键登陆)
+ * @value launchApp 通用 - 小程序中打开APP,可以通过app-parameter属性设定向APP传的参数 (微信小程序、QQ小程序、快手小程序、京东小程序)
+ * @value openSetting 通用 - 打开授权设置页(微信小程序、QQ小程序、百度小程序、快手小程序、京东小程序、360小程序)
+ * @value chooseAvatar 微信小程序 - 获取用户头像
+ * @value uploadDouyinVideo 抖音小程序 - 发布抖音视频
+ * @value im 抖音小程序 - 跳转到抖音IM客服
+ * @value getAuthorize 支付宝小程序 - 授权
+ * @value lifestyle 支付宝小程序 - 关注生活号
+ * @value contactShare 支付宝小程序 - 分享到通讯录好友
+ * @value openGroupProfile QQ小程序 - 呼起QQ群资料卡页面,可以通过group-id属性设定需要打开的群资料卡的群号,同时manifest.json中必须配置groupIdList
+ * @value openGuildProfile QQ小程序 - 呼起频道页面,可以通过guild-id属性设定需要打开的频道ID
+ * @value openPublicProfile QQ小程序 - 打开公众号资料卡,可以通过public-id属性设定需要打开的公众号资料卡的号码,同时manifest.json中必须配置publicIdList
+ * @value shareMessageToFriend QQ小程序 - 在自定义开放数据域组件中,向指定好友发起分享据
+ * @value addFriend QQ小程序 - 添加好友, 对方需要通过该小程序进行授权,允许被加好友后才能调用成功用户授权
+ * @value addColorSign QQ小程序 - 添加彩签,点击后添加状态有用户提示,无回调
+ * @value addGroupApp QQ小程序 - 添加群应用(只有管理员或群主有权操作),添加后给button绑定@addgroupapp事件接收回调数据
+ * @value addToFavorites QQ小程序 - 收藏当前页面,点击按钮后会触发Page.onAddToFavorites方法
+ * @value chooseAddress 百度小程序 - 选择用户收货地址
+ * @value chooseInvoiceTitle 百度小程序 - 选择用户发票抬头
+ * @value login 百度小程序 - 登录,可以从@login回调中确认是否登录成功
+ * @value subscribe 百度小程序 - 订阅类模板消息,需要用户授权才可发送
+ * @value favorite 快手小程序 - 触发用户收藏
+ * @value watchLater 快手小程序 - 触发用户稍后再看
+ * @value openProfile 快手小程序 - 触发打开用户主页
+ * @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ * @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
+ * @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
+ * @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
+ * @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
+ * @event {Function} click 按钮点击
+ * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
+ * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
+ * @event {Function} error 当使用开放能力时,发生错误的回调
+ * @event {Function} opensetting 在打开授权设置页并关闭后回调
+ * @event {Function} launchapp 打开 APP 成功的回调
+ * @event {Function} chooseavatar 获取用户头像,可以从@chooseavatar回调中获取到头像信息,open-type="chooseAvatar"时有效
+ * @example <u-button>月落</u-button>
+ name: "u-button",
+ emits: ["click", "getphonenumber", "getuserinfo", "error", "opensetting", "launchapp", "chooseavatar"],
+ // 是否细边框
+ hairLine: {
+ // 按钮的预置样式,default,primary,error,warning,success
+ default: "default"
+ // 按钮尺寸,default,medium,mini
+ // 按钮形状,circle(两边为半圆),square(带圆角)
+ shape: {
+ default: "square"
+ // 按钮是否镂空
+ plain: {
+ // 是否禁止状态
+ disabled: {
+ // 是否加载中
+ loading: {
+ // 开放能力,具体请看uniapp稳定关于button组件部分说明
+ // https://uniapp.dcloud.io/component/button
+ openType: {
+ // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ // 取值为submit(提交表单),reset(重置表单)
+ formType: {
+ // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
+ // 只微信小程序、QQ小程序有效
+ appParameter: {
+ // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
+ hoverStopPropagation: {
+ // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
+ lang: {
+ default: "en"
+ // 会话来源,open-type="contact"时有效。只微信小程序有效
+ sessionFrom: {
+ // 会话内消息卡片标题,open-type="contact"时有效
+ // 默认当前标题,只微信小程序有效
+ sendMessageTitle: {
+ // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
+ // 默认当前分享路径,只微信小程序有效
+ sendMessagePath: {
+ // 会话内消息卡片图片,open-type="contact"时有效
+ // 默认当前页面截图,只微信小程序有效
+ sendMessageImg: {
+ // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
+ // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
+ showMessageCard: {
+ // 手指按(触摸)按钮时按钮时的背景颜色
+ hoverBgColor: {
+ // 水波纹的背景颜色
+ rippleBgColor: {
+ // 是否开启水波纹效果
+ ripple: {
+ // 按下的类名
+ hoverClass: {
+ // 自定义样式,对象形式
+ return {};
+ // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ dataName: {
+ // 节流,一定时间内只能触发一次
+ throttleTime: {
+ default: 500
+ // 按住后多久出现点击态,单位毫秒
+ hoverStartTime: {
+ default: 20
+ // 手指松开后点击态保留时间,单位毫秒
+ hoverStayTime: {
+ default: 150
+ timerId: {
+ type: [String, Number]
+ // 当没有传bgColor变量时,按钮按下去的颜色类名
+ getHoverClass() {
+ // 如果开启水波纹效果,则不启用hover-class效果
+ if (this.loading || this.disabled || this.ripple || this.hoverClass) return "";
+ let hoverClass = "";
+ hoverClass = this.plain ? "u-" + this.type + "-plain-hover" : "u-" + this.type + "-hover";
+ return hoverClass;
+ // 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
+ showHairLineBorder() {
+ if (["primary", "success", "error", "warning"].indexOf(this.type) >= 0 && !this.plain) {
+ return "";
+ return "u-hairline-border";
+ let btnTimerId = this.timerId || "button_" + Math.floor(Math.random() * 100000000 + 0);
+ btnTimerId,
+ rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
+ rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
+ fields: {}, // 波纹按钮节点信息
+ waveActive: false // 激活水波纹
+ // 按钮点击
+ click(e) {
+ // 进行节流控制,每this.throttle毫秒内,只在开始处执行
+ this.$u.throttle(
+ () => {
+ // 如果按钮时disabled和loading状态,不触发水波纹效果
+ if (this.loading === true || this.disabled === true) return;
+ if (this.ripple) {
+ // 每次点击时,移除上一次的类,再次添加,才能触发动画效果
+ this.waveActive = false;
+ this.$nextTick(function () {
+ this.getWaveQuery(e);
+ this.$emit("click", e);
+ this.throttleTime,
+ true,
+ this.btnTimerId
+ );
+ // 查询按钮的节点信息
+ getWaveQuery(e) {
+ this.getElQuery().then((res) => {
+ // 查询返回的是一个数组节点
+ let data = res[0];
+ // 查询不到节点信息,不操作
+ if (!data.width || !data.width) return;
+ // 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
+ // 最终的方形(变换后的圆形)才能覆盖整个按钮
+ data.targetWidth = data.height > data.width ? data.height : data.width;
+ if (!data.targetWidth) return;
+ this.fields = data;
+ let touchesX = "",
+ touchesY = "";
+ // #ifdef MP-BAIDU
+ touchesX = e.changedTouches[0].clientX;
+ touchesY = e.changedTouches[0].clientY;
+ // #ifdef MP-ALIPAY
+ touchesX = e.detail.clientX;
+ touchesY = e.detail.clientY;
+ // #ifndef MP-BAIDU || MP-ALIPAY
+ touchesX = e.touches[0].clientX;
+ touchesY = e.touches[0].clientY;
+ // 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
+ // 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
+ // 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
+ this.rippleTop = touchesY - data.top - data.targetWidth / 2;
+ this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
+ this.$nextTick(() => {
+ this.waveActive = true;
+ // 获取节点信息
+ getElQuery() {
+ return new Promise((resolve) => {
+ let queryInfo = "";
+ // 获取元素节点信息,请查看uniapp相关文档
+ // https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
+ queryInfo = uni.createSelectorQuery().in(this);
+ //#ifdef MP-ALIPAY
+ queryInfo = uni.createSelectorQuery();
+ //#endif
+ queryInfo.select(".u-btn").boundingClientRect();
+ queryInfo.exec((data) => {
+ resolve(data);
+ // 下面为对接uniapp官方按钮开放能力事件回调的对接
+ getphonenumber(res) {
+ this.$emit("getphonenumber", res);
+ getuserinfo(res) {
+ this.$emit("getuserinfo", res);
+ error(res) {
+ this.$emit("error", res);
+ opensetting(res) {
+ this.$emit("opensetting", res);
+ launchapp(res) {
+ this.$emit("launchapp", res);
+ chooseavatar(res) {
+ this.$emit("chooseavatar", res);
+.u-btn::after {
+ border: none;
+.u-btn {
+ border: 0;
+ //border-radius: 10rpx;
+ // 避免边框某些场景可能被“裁剪”,不能设置为hidden
+ overflow: visible;
+ padding: 0 40rpx;
+ z-index: 1;
+ box-sizing: border-box;
+ transition: all 0.15s;
+ &--bold-border {
+ border: 1px solid #ffffff;
+ &--default {
+ border-color: #c0c4cc;
+ background-color: #ffffff;
+ &--primary {
+ color: #ffffff;
+ border-color: $u-type-primary;
+ &--success {
+ border-color: $u-type-success;
+ &--error {
+ border-color: $u-type-error;
+ &--warning {
+ border-color: $u-type-warning;
+ &--default--disabled {
+ border-color: #e4e7ed;
+ &--primary--disabled {
+ color: #ffffff !important;
+ border-color: $u-type-primary-disabled !important;
+ background-color: $u-type-primary-disabled !important;
+ &--success--disabled {
+ border-color: $u-type-success-disabled !important;
+ background-color: $u-type-success-disabled !important;
+ &--error--disabled {
+ border-color: $u-type-error-disabled !important;
+ background-color: $u-type-error-disabled !important;
+ &--warning--disabled {
+ border-color: $u-type-warning-disabled !important;
+ background-color: $u-type-warning-disabled !important;
+ &--primary--plain {
+ color: $u-type-primary !important;
+ background-color: $u-type-primary-light !important;
+ &--success--plain {
+ color: $u-type-success !important;
+ background-color: $u-type-success-light !important;
+ &--error--plain {
+ color: $u-type-error !important;
+ background-color: $u-type-error-light !important;
+ &--warning--plain {
+ color: $u-type-warning !important;
+ background-color: $u-type-warning-light !important;
+ &--info {
+ border-color: $u-type-info;
+ &--info--disabled {
+ border-color: $u-type-info-disabled !important;
+ background-color: $u-type-info-disabled !important;
+ &--info--plain {
+ color: $u-type-info !important;
+ background-color: $u-type-info-light !important;
+.u-hairline-border:after {
+ content: " ";
+ pointer-events: none;
+ // 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
+ // 中心点作为变形(scale())的原点
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
+ width: 199.8%;
+ height: 199.7%;
+ -webkit-transform: scale(0.5, 0.5);
+ transform: scale(0.5, 0.5);
+ border: 1px solid currentColor;
+.u-wave-ripple {
+ border-radius: 100%;
+ background-clip: padding-box;
+ user-select: none;
+ transform: scale(0);
+ opacity: 1;
+ transform-origin: center;
+.u-wave-ripple.u-wave-active {
+ transform: scale(2);
+ transition: opacity 1s linear, transform 0.4s linear;
+.u-round-circle {
+.u-round-circle::after {
+.u-loading::after {
+ background-color: hsla(0, 0%, 100%, 0.35);
+.u-size-default {
+ font-size: 30rpx;
+ line-height: 80rpx;
+.u-size-medium {
+ width: auto;
+ height: 70rpx;
+ line-height: 70rpx;
+ padding: 0 30rpx;
+.u-size-mini {
+ font-size: 22rpx;
+ padding-top: 1px;
+ height: 50rpx;
+ line-height: 50rpx;
+ padding: 0 20rpx;
+.u-primary-plain-hover {
+ background: $u-type-primary-dark !important;
+.u-default-plain-hover {
+ color: $u-type-primary-dark !important;
+ background: $u-type-primary-light !important;
+.u-success-plain-hover {
+ background: $u-type-success-dark !important;
+.u-warning-plain-hover {
+ background: $u-type-warning-dark !important;
+.u-error-plain-hover {
+ background: $u-type-error-dark !important;
+.u-info-plain-hover {
+ background: $u-type-info-dark !important;
+.u-default-hover {
+ border-color: $u-type-primary-dark !important;
+.u-primary-hover {
+.u-success-hover {
+.u-info-hover {
+.u-warning-hover {
+.u-error-hover {
@@ -0,0 +1,673 @@
+ <u-popup :blur="blur" closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="popupValue"
+ length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex"
+ :border-radius="borderRadius" :closeable="closeable">
+ <view class="u-calendar">
+ <view class="u-calendar__header">
+ <view class="u-calendar__header__text" v-if="!$slots['tooltip']">
+ {{ toolTip }}
+ <slot v-else name="tooltip" />
+ <view class="u-calendar__action u-flex u-row-center">
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor"
+ @click="changeYearHandler(0)"></u-icon>
+ <u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor"
+ @click="changeMonthHandler(0)"></u-icon>
+ <view class="u-calendar__action__text">{{ showTitle }}</view>
+ <u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor"
+ @click="changeMonthHandler(1)"></u-icon>
+ <u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor"
+ @click="changeYearHandler(1)"></u-icon>
+ <view class="u-calendar__week-day">
+ <view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{ item }}</view>
+ <view class="u-calendar__content">
+ <!-- 前置空白部分 -->
+ <block v-for="(item, index) in weekdayArr" :key="index">
+ <view class="u-calendar__content__item"></view>
+ <view class="u-calendar__content__item" :class="{
+ 'u-hover-class': openDisAbled(year, month, index + 1),
+ 'u-calendar__content--start-date': (mode == 'range' && startDate == `${year}-${month}-${index + 1}`) || mode == 'date',
+ 'u-calendar__content--end-date': (mode == 'range' && endDate == `${year}-${month}-${index + 1}`) || mode == 'date'
+ }" :style="{ backgroundColor: getColor(index, 1) }" v-for="(item, index) in daysArr" :key="index"
+ @tap="dateClick(index)">
+ <view class="u-calendar__content__item__inner" :style="{ color: getColor(index, 2) }">
+ <view>{{ index + 1 }}</view>
+ <view class="u-calendar__content__item__tips" :style="{ color: activeColor }"
+ v-if="mode == 'range' && startDate == `${year}-${month}-${index + 1}` && startDate != endDate">
+ {{ startText }}</view>
+ v-if="mode == 'range' && endDate == `${year}-${month}-${index + 1}`">{{ endText }}</view>
+ <view class="u-calendar__content__bg-month">{{ month }}</view>
+ <view class="u-calendar__bottom">
+ <view class="u-calendar__bottom__choose">
+ <text>{{ mode == 'date' ? activeDate : startDate }}</text>
+ <text v-if="endDate">至{{ endDate }}</text>
+ <view class="u-calendar__bottom__btn">
+ <u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
+ * calendar 日历
+ * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
+ * @tutorial http://uviewui.com/components/calendar.html
+ * @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
+ * @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
+ * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
+ * @property {String Number} max-year 可切换的最大年份(默认2050)
+ * @property {String Number} min-year 可切换的最小年份(默认1950)
+ * @property {String Number} min-date 最小可选日期(默认1950-01-01)
+ * @property {String Number} max-date 最大可选日期(默认当前日期)
+ * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
+ * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
+ * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
+ * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
+ * @property {String} color 日期字体的默认颜色(默认#303133)
+ * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
+ * @property {String Number} z-index 弹出时的z-index值(默认10075)
+ * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
+ * @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
+ * @property {String} range-color 选择范围内字体颜色(默认#2979ff)
+ * @property {String} start-text 起始日期底部的提示文字(默认 '开始')
+ * @property {String} end-text 结束日期底部的提示文字(默认 '结束')
+ * @property {String} btn-type 底部确定按钮的主题(默认 'primary')
+ * @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
+ * @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
+ * @example <u-calendar v-model="show" :mode="mode"></u-calendar>
+ name: 'u-calendar',
+ emits: ["update:modelValue", "input", "change"],
+ // 是否允许通过点击遮罩关闭Picker
+ // 是否允许切换年份
+ changeYear: {
+ // 是否允许切换月份
+ changeMonth: {
+ // date-单个日期选择,range-开始日期+结束日期选择
+ default: 'date'
+ // 可切换的最大年份
+ maxYear: {
+ default: 2050
+ // 可切换的最小年份
+ minYear: {
+ default: 1950
+ // 最小可选日期(不在范围内日期禁用不可选)
+ minDate: {
+ default: '1950-01-01'
+ * 最大可选日期
+ * 默认最大值为今天,之后的日期不可选
+ * 2030-12-31
+ * */
+ maxDate: {
+ // 弹窗顶部左右两边的圆角值
+ // 月份切换按钮箭头颜色
+ monthArrowColor: {
+ default: '#606266'
+ // 年份切换按钮箭头颜色
+ yearArrowColor: {
+ default: '#909399'
+ // 默认日期字体颜色
+ default: '#303133'
+ // 选中|起始结束日期背景色
+ activeBgColor: {
+ default: '#19be6b'
+ // 选中|起始结束日期字体颜色
+ activeColor: {
+ // 范围内日期背景色
+ rangeBgColor: {
+ default: 'rgba(41,121,255,0.13)'
+ // 范围内日期字体颜色
+ rangeColor: {
+ // mode=range时生效,起始日期自定义文案
+ startText: {
+ default: '开始'
+ // mode=range时生效,结束日期自定义文案
+ endText: {
+ default: '结束'
+ //按钮样式类型
+ btnType: {
+ default: 'primary'
+ // 当前选中日期带选中效果
+ isActiveCurrent: {
+ // 切换年月是否触发事件 mode=date时生效
+ isChange: {
+ // 是否显示右上角的关闭图标
+ closeable: {
+ toolTip: {
+ default: '选择日期'
+ popupValue: false,
+ // 星期几,值为1-7
+ weekday: 1,
+ weekdayArr: [],
+ // 当前月有多少天
+ days: 0,
+ daysArr: [],
+ showTitle: '',
+ year: 2020,
+ month: 0,
+ day: 0,
+ startYear: 0,
+ startMonth: 0,
+ startDay: 0,
+ endYear: 0,
+ endMonth: 0,
+ endDay: 0,
+ today: '',
+ activeDate: '',
+ startDate: '',
+ endDate: '',
+ isStart: true,
+ min: null,
+ max: null,
+ weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
+ dataChange() {
+ return `${this.mode}-${this.minDate}-${this.maxDate}`;
+ dataChange(val) {
+ this.init()
+ valueCom: {
+ immediate: true,
+ handler(val) {
+ this.popupValue = val;
+ created() {
+ getColor(index, type) {
+ let color = type == 1 ? '' : this.color;
+ let day = index + 1
+ let date = `${this.year}-${this.month}-${day}`
+ let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+ let start = this.startDate.replace(/\-/g, '/')
+ let end = this.endDate.replace(/\-/g, '/')
+ if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+ color = type == 1 ? this.activeBgColor : this.activeColor;
+ } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+ color = type == 1 ? this.rangeBgColor : this.rangeColor;
+ return color;
+ init() {
+ let now = new Date();
+ this.year = now.getFullYear();
+ this.month = now.getMonth() + 1;
+ this.day = now.getDate();
+ this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+ this.activeDate = this.today;
+ this.min = this.initDate(this.minDate);
+ this.max = this.initDate(this.maxDate || this.today);
+ this.startDate = "";
+ this.startYear = 0;
+ this.startMonth = 0;
+ this.startDay = 0;
+ this.endYear = 0;
+ this.endMonth = 0;
+ this.endDay = 0;
+ this.endDate = "";
+ this.isStart = true;
+ this.changeData();
+ //日期处理
+ initDate(date) {
+ let fdate = date.split('-');
+ year: Number(fdate[0] || 1920),
+ month: Number(fdate[1] || 1),
+ day: Number(fdate[2] || 1)
+ openDisAbled: function (year, month, day) {
+ let bool = true;
+ let date = `${year}/${month}/${day}`;
+ // let today = this.today.replace(/\-/g, '/');
+ let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+ let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+ let timestamp = new Date(date).getTime();
+ if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+ bool = false;
+ return bool;
+ generateArray: function (start, end) {
+ return Array.from(new Array(end + 1).keys()).slice(start);
+ formatNum: function (num) {
+ return num < 10 ? '0' + num : num + '';
+ //一个月有多少天
+ getMonthDay(year, month) {
+ let days = new Date(year, month, 0).getDate();
+ return days;
+ getWeekday(year, month) {
+ let date = new Date(`${year}/${month}/01 00:00:00`);
+ return date.getDay();
+ checkRange(year) {
+ let overstep = false;
+ if (year < this.minYear || year > this.maxYear) {
+ title: "日期超出范围啦~",
+ icon: 'none'
+ overstep = true;
+ return overstep;
+ changeMonthHandler(isAdd) {
+ if (isAdd) {
+ let month = this.month + 1;
+ let year = month > 12 ? this.year + 1 : this.year;
+ if (!this.checkRange(year)) {
+ this.month = month > 12 ? 1 : month;
+ this.year = year;
+ let month = this.month - 1;
+ let year = month < 1 ? this.year - 1 : this.year;
+ this.month = month < 1 ? 12 : month;
+ changeYearHandler(isAdd) {
+ let year = isAdd ? this.year + 1 : this.year - 1;
+ changeData() {
+ this.days = this.getMonthDay(this.year, this.month);
+ this.daysArr = this.generateArray(1, this.days)
+ this.weekday = this.getWeekday(this.year, this.month);
+ this.weekdayArr = this.generateArray(1, this.weekday)
+ this.showTitle = `${this.year}年${this.month}月`;
+ if (this.isChange && this.mode == 'date') {
+ this.btnFix(true);
+ dateClick: function (day) {
+ day += 1;
+ if (!this.openDisAbled(this.year, this.month, day)) {
+ this.day = day;
+ let date = `${this.year}-${this.month}-${day}`;
+ if (this.mode == 'date') {
+ this.activeDate = date;
+ let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
+ if (this.isStart || compare) {
+ this.startDate = date;
+ this.startYear = this.year;
+ this.startMonth = this.month;
+ this.startDay = this.day;
+ this.activeDate = "";
+ this.isStart = false;
+ this.endDate = date;
+ this.endYear = this.year;
+ this.endMonth = this.month;
+ this.endDay = this.day;
+ // 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
+ this.$emit('input', false);
+ getWeekText(date) {
+ date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+ let week = date.getDay();
+ return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+ btnFix(show) {
+ if (!show) {
+ this.close();
+ let arr = this.activeDate.split('-')
+ let year = this.isChange ? this.year : Number(arr[0]);
+ let month = this.isChange ? this.month : Number(arr[1]);
+ let day = this.isChange ? this.day : Number(arr[2]);
+ //当前月有多少天
+ let days = this.getMonthDay(year, month);
+ let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+ let weekText = this.getWeekText(result);
+ let isToday = false;
+ if (`${year}-${month}-${day}` == this.today) {
+ //今天
+ isToday = true;
+ this.$emit('change', {
+ year: year,
+ month: month,
+ day: day,
+ days: days,
+ result: result,
+ week: weekText,
+ isToday: isToday,
+ // switch: show //是否是切换年月操作
+ if (!this.startDate || !this.endDate) return;
+ let startMonth = this.formatNum(this.startMonth);
+ let startDay = this.formatNum(this.startDay);
+ let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+ let startWeek = this.getWeekText(startDate)
+ let endMonth = this.formatNum(this.endMonth);
+ let endDay = this.formatNum(this.endDay);
+ let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+ let endWeek = this.getWeekText(endDate);
+ startYear: this.startYear,
+ startMonth: this.startMonth,
+ startDay: this.startDay,
+ startDate: startDate,
+ startWeek: startWeek,
+ endYear: this.endYear,
+ endMonth: this.endMonth,
+ endDay: this.endDay,
+ endDate: endDate,
+ endWeek: endWeek
+.u-calendar {
+ &__header {
+ background-color: #fff;
+ &__text {
+ margin-top: 30rpx;
+ padding: 0 60rpx;
+ &__action {
+ padding: 40rpx 0 40rpx 0;
+ &__icon {
+ margin: 0 16rpx;
+ padding: 0 16rpx;
+ &__week-day {
+ padding: 6px 0;
+ flex: 1;
+ &--end-date {
+ border-top-right-radius: 8rpx;
+ border-bottom-right-radius: 8rpx;
+ &--start-date {
+ border-top-left-radius: 8rpx;
+ border-bottom-left-radius: 8rpx;
+ &__item {
+ width: 14.2857%;
+ &__inner {
+ &__desc {
+ transform: scale(0.75);
+ bottom: 2rpx;
+ bottom: 8rpx;
+ &__bg-month {
+ font-size: 130px;
+ line-height: 130px;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ color: #e4e7ed;
+ &__bottom {
+ padding: 0 40rpx 30rpx;
+ &__choose {
+ &__btn {
+}</style>
@@ -0,0 +1,267 @@
+ <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
+ <view class="u-keyboard-grids">
+ <view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
+ <view :hover-stay-time="100" @touchstart="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
+ v-for="(item, j) in group" :key="j">
+ {{ item }}
+ <view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
+ hover-class="u-hover-class">
+ <u-icon :size="38" name="backspace" :bold="true"></u-icon>
+ <view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @click="changeCarInputMode">
+ <text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
+ /
+ <text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
+ name: "u-keyboard",
+ emits: ["change", "backspace"],
+ // 是否打乱键盘按键的顺序
+ random: {
+ // 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
+ abc: false
+ areaList() {
+ let data = [
+ '京',
+ '沪',
+ '粤',
+ '津',
+ '冀',
+ '豫',
+ '云',
+ '辽',
+ '黑',
+ '湘',
+ '皖',
+ '鲁',
+ '苏',
+ '浙',
+ '赣',
+ '鄂',
+ '桂',
+ '甘',
+ '晋',
+ '陕',
+ '蒙',
+ '吉',
+ '闽',
+ '贵',
+ '渝',
+ '川',
+ '青',
+ '琼',
+ '宁',
+ '挂',
+ '藏',
+ '港',
+ '澳',
+ '新',
+ '使',
+ '学'
+ ];
+ let tmp = [];
+ // 打乱顺序
+ if (this.random) data = this.$u.randomArray(data);
+ // 切割成二维数组
+ tmp[0] = data.slice(0, 10);
+ tmp[1] = data.slice(10, 20);
+ tmp[2] = data.slice(20, 30);
+ tmp[3] = data.slice(30, 36);
+ return tmp;
+ EngKeyBoardList() {
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 0,
+ 'Q',
+ 'W',
+ 'E',
+ 'R',
+ 'T',
+ 'Y',
+ 'U',
+ 'I',
+ 'O',
+ 'P',
+ 'A',
+ 'S',
+ 'D',
+ 'F',
+ 'G',
+ 'H',
+ 'J',
+ 'K',
+ 'L',
+ 'Z',
+ 'X',
+ 'C',
+ 'V',
+ 'B',
+ 'N',
+ 'M'
+ // 点击键盘按钮
+ carInputClick(i, j) {
+ let value = '';
+ // 不同模式,获取不同数组的值
+ if (this.abc) value = this.EngKeyBoardList[i][j];
+ else value = this.areaList[i][j];
+ if(!this.abc) this.abc = true;
+ this.$emit('change', value);
+ if(this.vibrate) uni.vibrateShort();
+ // 修改汽车牌键盘的输入模式,中文|英文
+ changeCarInputMode() {
+ this.abc = !this.abc;
+ updateCarInputMode(abc) {
+ this.abc = abc;
+ // 点击退格键
+ backspaceClick() {
+ let count = 1;
+ this.backspaceFn(count);
+ clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+ this.timer = null;
+ this.timer = setInterval(() => {
+ count++;
+ }, 250);
+ backspaceFn(count){
+ this.$emit('backspace',count);
+ clearTimer() {
+ clearInterval(this.timer);
+ .u-keyboard-grids {
+ background: rgb(215, 215, 217);
+ padding: 24rpx 0;
+ .u-keyboard-grids-item {
+ .u-keyboard-grids-btn {
+ width: 62rpx;
+ flex: 0 0 64rpx;
+ font-size: 36rpx;
+ margin: 8rpx 5rpx;
+ box-shadow: 0 2rpx 0rpx #888992;
+ font-weight: 500;
+ .u-carinput-hover {
+ background-color: rgb(185, 188, 195) !important;
+ .u-keyboard-back {
+ width: 96rpx;
+ right: 22rpx;
+ bottom: 32rpx;
+ background-color: rgb(185, 188, 195);
+ .u-keyboard-change {
+ left: 22rpx;
+ .u-keyboard-change .inactive.zh {
+ transform: scale(0.85) translateY(-10rpx);
+ .u-keyboard-change .inactive.en {
+ transform: scale(0.85) translateY(10rpx);
+ .u-keyboard-change .active {
+ color: rgb(237, 112, 64);
+ .u-keyboard-change .zh {
+ transform: translateY(-10rpx);
+ .u-keyboard-change .en {
+ transform: translateY(10rpx);
@@ -0,0 +1,300 @@
+ class="u-card"
+ @tap.stop="click"
+ :class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
+ '--radius': borderRadius + 'rpx',
+ margin: margin,
+ boxShadow: boxShadow
+ v-if="showHead"
+ class="u-card__head"
+ :style="[{padding: padding + 'rpx'}, headStyle]"
+ :class="{
+ 'u-border-bottom': headBorderBottom
+ @tap="headClick"
+ <view v-if="!$slots.head" class="u-flex u-row-between">
+ <view class="u-card__head--left u-flex u-line-1" v-if="title">
+ <image
+ :src="thumb"
+ class="u-card__head--left__thumb"
+ mode="aspectFill"
+ v-if="thumb"
+ height: thumbWidth + 'rpx',
+ width: thumbWidth + 'rpx',
+ borderRadius: thumbCircle ? '100rpx' : '6rpx'
+ ></image>
+ <text
+ class="u-card__head--left__title u-line-1"
+ fontSize: titleSize + 'rpx',
+ color: titleColor
+ {{ title }}
+ <view class="u-card__head--right u-line-1" v-if="subTitle">
+ class="u-card__head__title__text"
+ fontSize: subTitleSize + 'rpx',
+ color: subTitleColor
+ {{ subTitle }}
+ <slot name="head" v-else />
+ <view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
+ v-if="showFoot"
+ class="u-card__foot"
+ @tap="footClick"
+ :style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
+ 'u-border-top': footBorderTop
+ <slot name="foot" />
+ * card 卡片
+ * @description 卡片组件一般用于多个列表条目,且风格统一的场景
+ * @tutorial https://www.uviewui.com/components/card.html
+ * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
+ * @property {String} title 头部左边的标题
+ * @property {String} title-color 标题颜色(默认#303133)
+ * @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
+ * @property {String} sub-title 头部右边的副标题
+ * @property {String} sub-title-color 副标题颜色(默认#909399)
+ * @property {String | Number} sub-title-size 副标题字体大小(默认26)
+ * @property {Boolean} border 是否显示边框(默认true)
+ * @property {String | Number} index 用于标识点击了第几个卡片
+ * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
+ * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
+ * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
+ * @property {Object} head-style 头部自定义样式,对象形式
+ * @property {Object} body-style 中部自定义样式,对象形式
+ * @property {Object} foot-style 底部自定义样式,对象形式
+ * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
+ * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
+ * @property {Boolean} show-head 是否显示头部(默认true)
+ * @property {Boolean} show-head 是否显示尾部(默认true)
+ * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
+ * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
+ * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
+ * @event {Function} click 整个卡片任意位置被点击时触发
+ * @event {Function} head-click 卡片头部被点击时触发
+ * @event {Function} body-click 卡片主体部分被点击时触发
+ * @event {Function} foot-click 卡片底部部分被点击时触发
+ * @example <u-card padding="30" title="card"></u-card>
+ name: 'u-card',
+ emits: ["click", "head-click", "body-click", "foot-click"],
+ // 与屏幕两侧是否留空隙
+ full: {
+ // 标题
+ // 标题颜色
+ titleColor: {
+ // 标题字体大小,单位rpx
+ titleSize: {
+ default: '30'
+ // 副标题
+ subTitle: {
+ // 副标题颜色
+ subTitleColor: {
+ // 副标题字体大小,单位rpx
+ subTitleSize: {
+ default: '26'
+ // 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
+ border: {
+ // 用于标识点击了第几个
+ index: {
+ type: [Number, String, Object],
+ // 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
+ margin: {
+ default: '30rpx'
+ // card卡片的圆角
+ default: '16'
+ // 头部自定义样式,对象形式
+ headStyle: {
+ // 主体自定义样式,对象形式
+ bodyStyle: {
+ // 底部自定义样式,对象形式
+ footStyle: {
+ // 头部是否下边框
+ headBorderBottom: {
+ // 底部是否有上边框
+ footBorderTop: {
+ // 标题左边的缩略图
+ thumb: {
+ // 缩略图宽高,单位rpx
+ thumbWidth: {
+ default: '60'
+ // 缩略图是否为圆形
+ thumbCircle: {
+ // 给head,body,foot的内边距
+ padding: {
+ // 是否显示头部
+ showHead: {
+ // 是否显示尾部
+ showFoot: {
+ // 卡片外围阴影,字符串形式
+ boxShadow: {
+ default: 'none'
+ this.$emit('click', this.index);
+ headClick() {
+ this.$emit('head-click', this.index);
+ bodyClick() {
+ this.$emit('body-click', this.index);
+ footClick() {
+ this.$emit('foot-click', this.index);
+.u-card {
+ &-full {
+ // 如果是与屏幕之间不留空隙,应该设置左右边距为0
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ &--border:after {
+ border-radius: var(--radius,16rpx);
+ &__head {
+ &--left {
+ &__thumb {
+ &__title {
+ max-width: 400rpx;
+ &--right {
+ margin-left: 6rpx;
+ &__body {
+ &__foot {
@@ -0,0 +1,70 @@
+ <view class="u-cell-box">
+ <view class="u-cell-title" v-if="title" :style="[titleStyle]">
+ <view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
+ <slot />
+ * cellGroup 单元格父组件Group
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
+ * @tutorial https://www.uviewui.com/components/cell.html
+ * @property {String} title 分组标题
+ * @property {Boolean} border 是否显示外边框(默认true)
+ * @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
+ * @example <u-cell-group title="设置喜好">
+ name: "u-cell-group",
+ // 分组标题
+ // 是否显示分组list上下边框
+ // 分组标题的样式,对象形式,注意驼峰属性写法
+ // 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
+ default () {
+ index: 0,
+ .u-cell-box {
+ .u-cell-title {
+ padding: 30rpx 32rpx 10rpx 32rpx;
+ .u-cell-item-box {
+ background-color: #FFFFFF;
@@ -0,0 +1,317 @@
+ @tap="click"
+ class="u-cell"
+ :class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
+ hover-stay-time="150"
+ :hover-class="hoverClass"
+ <u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
+ <view class="u-flex" v-else>
+ <slot name="icon"></slot>
+ class="u-cell_title"
+ width: titleWidth ? titleWidth + 'rpx' : 'auto'
+ titleStyle
+ <block v-if="title !== ''">{{ title }}</block>
+ <slot name="title" v-else></slot>
+ <view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
+ <block v-if="label !== ''">{{ label }}</block>
+ <slot name="label" v-else></slot>
+ <view class="u-cell__value" :style="[valueStyle]">
+ <block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
+ <slot v-else></slot>
+ <view class="u-flex u-cell_right" v-if="$slots['right-icon']">
+ <slot name="right-icon"></slot>
+ <u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
+ * cellItem 单元格Item
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
+ * @property {String} title 左侧标题
+ * @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {String} value 右侧内容
+ * @property {String} label 标题下方的描述信息
+ * @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
+ * @property {Boolean} border-top 是否显示cell的上边框(默认false)
+ * @property {Boolean} center 是否使内容垂直居中(默认false)
+ * @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
+ * // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
+ * @property {Boolean} arrow 是否显示右侧箭头(默认true)
+ * @property {Boolean} required 箭头方向,可选值(默认right)
+ * @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
+ * @property {Object} title-style 标题样式,对象形式
+ * @property {Object} value-style 右侧内容样式,对象形式
+ * @property {Object} label-style 标题下方描述信息的样式,对象形式
+ * @property {String} bg-color 背景颜色(默认transparent)
+ * @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
+ * @property {String Number} title-width 标题的宽度,单位rpx
+ * @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
+ name: 'u-cell-item',
+ emits: ["click"],
+ // 左侧图标名称(只能uView内置图标),或者图标src
+ // 左侧标题
+ // 右侧内容
+ // 标题下方的描述信息
+ label: {
+ // 是否显示下边框
+ borderBottom: {
+ // 是否显示上边框
+ borderTop: {
+ // 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
+ // 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
+ // borderGap: {
+ // type: Boolean,
+ // default: true
+ // 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
+ default: 'u-cell-hover'
+ // 是否显示右侧箭头
+ arrow: {
+ // 内容是否垂直居中
+ center: {
+ // 是否显示左边表示必填的星号
+ required: {
+ // 标题的宽度,单位rpx
+ titleWidth: {
+ // 右侧箭头方向,可选值:right|up|down,默认为right
+ arrowDirection: {
+ default: 'right'
+ // 控制标题的样式
+ // 右侧显示内容的样式
+ valueStyle: {
+ // 描述信息的样式
+ labelStyle: {
+ default: 'transparent'
+ // 用于识别被点击的是第几个cell
+ // 是否使用lable插槽
+ useLabelSlot: {
+ // 左边图标的大小,单位rpx,只对传入icon字段时有效
+ iconSize: {
+ default: 34
+ // 左边图标的样式,对象形式
+ arrowStyle() {
+ if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
+ else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
+ else style.transform = 'rotate(0deg)';
+.u-cell {
+ padding: 26rpx 32rpx;
+ line-height: 54rpx;
+.u-cell_title {
+.u-cell__left-icon-wrap {
+.u-cell__right-icon-wrap {
+ margin-left: 10rpx;
+ color: #969799;
+.u-cell__left-icon-wrap,
+.u-cell-border:after {
+ content: ' ';
+ border-bottom: 1px solid $u-border-color;
+ right: 0;
+ transform: scaleY(0.5);
+.u-cell-border {
+.u-cell__label {
+ margin-top: 6rpx;
+ line-height: 36rpx;
+.u-cell__value {
+ vertical-align: middle;
+.u-cell__title,
+.u-cell--required {
+.u-cell--required:before {
+ content: '*';
+ left: 8px;
+ margin-top: 4rpx;
+ font-size: 14px;
+.u-cell_right {
@@ -0,0 +1,178 @@
+ <view class="u-checkbox-group u-clearfix" :class="uFromData.inputAlign == 'right' ? 'flex-end' : ''"><slot></slot></view>
+import Emitter from "../../libs/util/emitter.js";
+ * checkboxGroup 开关选择器父组件Group
+ * @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
+ * @tutorial https://www.uviewui.com/components/checkbox.html
+ * @property {String Number} max 最多能选中多少个checkbox(默认999)
+ * @property {String Number} size 组件整体的大小,单位rpx(默认40)
+ * @property {Boolean} disabled 是否禁用所有checkbox(默认false)
+ * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
+ * @property {String} width 宽度,需带单位
+ * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
+ * @property {Boolean} wrap 是否每个checkbox都换行(默认false)
+ * @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
+ * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
+ * @example <u-checkbox-group></u-checkbox-group>
+ name: "u-checkbox-group",
+ mixins: [Emitter],
+ // 匹配某一个radio组件,如果某个radio的name值等于此值,那么这个radio就被会选中
+ type: [String, Number, Array, Boolean],
+ // 最多能选中多少个checkbox
+ max: {
+ default: 999
+ // 所有选中项的 name
+ // value: {
+ // default: Array,
+ // default() {
+ // return []
+ // 是否禁用所有复选框
+ // 在表单内提交时的标识符
+ name: {
+ type: [Boolean, String],
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ // 形状,square为方形,circle为圆型
+ // 选中状态下的颜色
+ default: "#2979ff"
+ // 组件的整体大小
+ // 每个checkbox占u-checkbox-group的宽度
+ width: {
+ default: "auto"
+ // 是否每个checkbox都换行
+ wrap: {
+ // 图标的大小,单位rpx
+ values: [],
+ uFromData: {
+ inputAlign: "left"
+ // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ this.children = [];
+ mounted() {
+ // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ let parent = this.$u.$parent.call(this, "u-form");
+ if (parent) {
+ Object.keys(this.uFromData).map(key => {
+ this.uFromData[key] = parent[key];
+ emitEvent(obj) {
+ let values = this.values || [];
+ if (obj.value) {
+ let index = values.indexOf(obj.name);
+ if (index === -1) {
+ values.push(obj.name);
+ if (index > -1) {
+ values.splice(index, 1);
+ this.$emit("change", values);
+ // 通过emit事件,设置父组件通过v-model双向绑定的值
+ this.$emit("input", values);
+ this.$emit("update:modelValue", values);
+ // 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
+ // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch("u-form-item", "onFieldChange", values);
+ }, 60);
+ _emitEvent(obj) {
+ //this.$emit("change", values);
+.u-checkbox-group {
+ /* #ifndef MP || APP-NVUE */
+.u-checkbox-group.flex-end {
@@ -0,0 +1,322 @@
+ <view class="u-checkbox" :style="[checkboxStyle]">
+ <view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
+ <u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor" />
+ class="u-checkbox__label"
+ @tap="onClickLabel"
+ fontSize: $u.addUnit(labelSize)
+ * checkbox 复选框
+ * @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
+ * @property {String Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String Number} name checkbox组件的标示符
+ * @property {String} shape 形状,外观形状,shape-方形,circle-圆形(默认circle)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
+ * @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
+ * @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
+ * @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
+ name: "u-checkbox",
+ // 是否为选中状态
+ // checkbox的名称
+ // 是否禁用
+ type: [String, Boolean],
+ // 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
+ // label的字体大小,rpx单位
+ labelSize: {
+ parentDisabled: false,
+ newParams: {}
+ // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ this.parent = this.$u.$parent.call(this, "u-checkbox-group");
+ // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
+ this.parent && this.parent.children.push(this);
+ // 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
+ isDisabled() {
+ return this.disabled !== "" ? this.disabled : this.parent ? this.parent.disabled : false;
+ // 是否禁用label点击
+ isLabelDisabled() {
+ return this.labelDisabled !== "" ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
+ // 组件尺寸,对应size的值,默认值为34rpx
+ checkboxSize() {
+ return this.size ? this.size : this.parent ? this.parent.size : 34;
+ // 组件的勾选图标的尺寸,默认20
+ checkboxIconSize() {
+ return this.iconSize ? this.iconSize : this.parent ? this.parent.iconSize : 20;
+ // 组件选中激活时的颜色
+ elActiveColor() {
+ return this.activeColor ? this.activeColor : this.parent ? this.parent.activeColor : "primary";
+ // 组件的形状
+ elShape() {
+ return this.shape ? this.shape : this.parent ? this.parent.shape : "square";
+ iconStyle() {
+ // 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
+ if (this.elActiveColor && this.valueCom && !this.isDisabled) {
+ style.borderColor = this.elActiveColor;
+ style.backgroundColor = this.elActiveColor;
+ style.width = this.$u.addUnit(this.checkboxSize);
+ style.height = this.$u.addUnit(this.checkboxSize);
+ // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
+ iconColor() {
+ return this.valueCom ? "#ffffff" : "transparent";
+ iconClass() {
+ let classes = [];
+ classes.push("u-checkbox__icon-wrap--" + this.elShape);
+ if (this.valueCom == true) classes.push("u-checkbox__icon-wrap--checked");
+ if (this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled");
+ if (this.valueCom && this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled--checked");
+ // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+ return classes.join(" ");
+ checkboxStyle() {
+ if (this.parent && this.parent.width) {
+ style.width = this.parent.width;
+ // 各家小程序因为它们特殊的编译结构,使用float布局
+ style.float = "left";
+ // #ifndef MP
+ // H5和APP使用flex布局
+ style.flex = `0 0 ${this.parent.width}`;
+ if (this.parent && this.parent.wrap) {
+ style.width = "100%";
+ // H5和APP使用flex布局,将宽度设置100%,即可自动换行
+ style.flex = "0 0 100%";
+ this._emitEvent();
+ handler: function(newVal, oldVal) {
+ _emitEvent() {
+ let value = this.valueCom;
+ let obj = {
+ value,
+ name: this.name
+ // 执行父组件u-checkbox-group的事件方法
+ if (this.parent && this.parent.emitEvent) this.parent.emitEvent(obj);
+ onClickLabel() {
+ if (!this.isLabelDisabled && !this.isDisabled) {
+ this.setValue();
+ toggle() {
+ if (!this.isDisabled) {
+ emitEvent() {
+ value: !this.valueCom,
+ this.$emit("change", obj);
+ // 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
+ setValue() {
+ // 判断是否超过了可选的最大数量
+ let checkedNum = 0;
+ if (this.parent && this.parent.children) {
+ // 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
+ this.parent.children.map(val => {
+ if (val.value) checkedNum++;
+ // 如果原来为选中状态,那么可以取消
+ if (value == true) {
+ this.emitEvent();
+ this.$emit("input", !value);
+ this.$emit("update:modelValue", !value);
+ // 如果超出最多可选项,提示
+ if (this.parent && checkedNum >= this.parent.max) {
+ return this.$u.toast(`最多可选${this.parent.max}项`);
+ // 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
+.u-checkbox {
+ line-height: 1.8;
+ &__icon-wrap {
+ flex: none;
+ display: -webkit-flex;
+ width: 42rpx;
+ height: 42rpx;
+ color: transparent;
+ transition-property: color, border-color, background-color;
+ font-size: 20px;
+ border: 1px solid #c8c9cc;
+ transition-duration: 0.2s;
+ /* #ifdef MP-TOUTIAO */
+ // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+ line-height: 0;
+ &--circle {
+ &--square {
+ border-radius: 6rpx;
+ &--checked {
+ &--disabled {
+ background-color: #ebedf0;
+ border-color: #c8c9cc;
+ &--disabled--checked {
+ color: #c8c9cc !important;
+ &__label {
+ margin-right: 24rpx;
+ color: #c8c9cc;
@@ -0,0 +1,227 @@
+ class="u-circle-progress"
+ width: widthPx + 'px',
+ height: widthPx + 'px',
+ <!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
+ class="u-canvas-bg"
+ :canvas-id="elBgId"
+ :id="elBgId"
+ height: widthPx + 'px'
+ class="u-canvas"
+ :canvas-id="elId"
+ :id="elId"
+ * circleProgress 环形进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
+ * @tutorial https://www.uviewui.com/components/circleProgress.html
+ * @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
+ * @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
+ * @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
+ * @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
+ * @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
+ * @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
+ * @property {String} type 如设置,active-color值将会失效
+ * @property {String} bg-color 整个组件背景颜色,默认为白色
+ * @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
+ name: 'u-circle-progress',
+ emits: ["end","finish"],
+ // 圆环进度百分比值
+ percent: {
+ default: 0,
+ // 限制值在0到100之间
+ validator: val => {
+ return val >= 0 && val <= 100;
+ // 底部圆环的颜色(灰色的圆环)
+ inactiveColor: {
+ default: '#ececec'
+ // 圆环激活部分的颜色
+ // 圆环线条的宽度,单位rpx
+ borderWidth: {
+ default: 14
+ // 整个圆形的宽度,单位rpx
+ // 整个圆环执行一圈的时间,单位ms
+ default: 1500
+ // 主题类型
+ // 整个圆环进度区域的背景色
+ // #ifdef MP-WEIXIN
+ elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
+ elId: 'uCircleProgressElId',
+ // #ifndef MP-WEIXIN
+ elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
+ elId: this.$u.guid(),
+ widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
+ borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
+ startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
+ progressContext: null, // 活动圆的canvas上下文
+ newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ percent(nVal, oVal = 0) {
+ if (nVal > 100) nVal = 100;
+ if (nVal < 0) oVal = 0;
+ // 此值其实等于this.percent,命名一个新
+ this.newPercent = nVal;
+ this.oldPercent = oVal;
+ // 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
+ // 将此值减少或者新增到新的百分比值
+ this.drawCircleByProgress(oVal);
+ }, 50);
+ // 赋值,用于加载后第一个画圆使用
+ this.newPercent = this.percent;
+ this.oldPercent = 0;
+ // 有type主题时,优先起作用
+ circleColor() {
+ if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
+ else return this.activeColor;
+ // 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
+ this.drawProgressBg();
+ this.drawCircleByProgress(this.oldPercent);
+ drawProgressBg() {
+ let ctx = uni.createCanvasContext(this.elBgId, this);
+ ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
+ ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
+ ctx.beginPath(); // 开始描绘路径
+ // 设置一个原点(110,110),半径为100的圆的路径到当前路径
+ let radius = this.widthPx / 2;
+ ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
+ ctx.stroke(); // 对路径进行描绘
+ ctx.draw();
+ drawCircleByProgress(progress) {
+ // 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
+ let ctx = this.progressContext;
+ if (!ctx) {
+ ctx = uni.createCanvasContext(this.elId, this);
+ this.progressContext = ctx;
+ // 表示进度的两端为圆形
+ ctx.setLineCap('round');
+ // 设置线条的宽度和颜色
+ ctx.setLineWidth(this.borderWidthPx);
+ ctx.setStrokeStyle(this.circleColor);
+ // 将总过渡时间除以100,得出每修改百分之一进度所需的时间
+ let time = Math.floor(this.duration / 100);
+ // 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
+ // 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
+ let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
+ ctx.beginPath();
+ // 半径为整个canvas宽度的一半
+ ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
+ ctx.stroke();
+ // 如果变更后新值大于旧值,意味着增大了百分比
+ if (this.newPercent > this.oldPercent) {
+ // 每次递增百分之一
+ progress++;
+ // 如果新增后的值,大于需要设置的值百分比值,停止继续增加
+ if (progress > this.newPercent) return;
+ // 同理于上面
+ progress--;
+ if (progress < this.newPercent) return;
+ if (progress >= this.percent) {
+ this.$emit("end", progress);
+ if (progress >= 100) {
+ this.$emit("finish", progress);
+ // 定时器,每次操作间隔为time值,为了让进度条有动画效果
+ this.drawCircleByProgress(progress);
+ }, time);
+.u-circle-progress {
+.u-canvas-bg {
+.u-canvas {
@@ -0,0 +1,157 @@
+ <view class="u-col" :class="[
+ 'u-col-' + span
+ padding: `0 ${Number(gutter)/2 + 'rpx'}`,
+ marginLeft: 100 / 12 * offset + '%',
+ flex: `0 0 ${100 / 12 * span}%`,
+ alignItems: uAlignItem,
+ justifyContent: uJustify,
+ textAlign: textAlign
+ @tap="click">
+ * col 布局单元格
+ * @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
+ * @tutorial https://www.uviewui.com/components/layout.html
+ * @property {String Number} span 栅格占据的列数,总12等分(默认0)
+ * @property {String} text-align 文字水平对齐方式(默认left)
+ * @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
+ * @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
+ name: "u-col",
+ // 占父容器宽度的多少等分,总分为12份
+ span: {
+ default: 12
+ // 指定栅格左侧的间隔数(总12栏)
+ // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+ justify: {
+ default: 'start'
+ // 垂直对齐方式,可选值为top、center、bottom
+ align: {
+ default: 'center'
+ // 文字对齐方式
+ textAlign: {
+ default: 'left'
+ // 是否阻止事件传播
+ stop: {
+ gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
+ this.parent = false;
+ // 获取父组件实例,并赋值给对应的参数
+ this.parent = this.$u.$parent.call(this, 'u-row');
+ if (this.parent) {
+ this.gutter = this.parent.gutter;
+ uJustify() {
+ if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
+ else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
+ else return this.justify;
+ uAlignItem() {
+ if (this.align == 'top') return 'flex-start';
+ if (this.align == 'bottom') return 'flex-end';
+ else return this.align;
+ this.$emit('click',e);
+ .u-col {
+ /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+ float: left;
+ .u-col-0 {
+ width: 0;
+ .u-col-1 {
+ width: calc(100%/12);
+ .u-col-2 {
+ width: calc(100%/12 * 2);
+ .u-col-3 {
+ width: calc(100%/12 * 3);
+ .u-col-4 {
+ width: calc(100%/12 * 4);
+ .u-col-5 {
+ width: calc(100%/12 * 5);
+ .u-col-6 {
+ width: calc(100%/12 * 6);
+ .u-col-7 {
+ width: calc(100%/12 * 7);
+ .u-col-8 {
+ width: calc(100%/12 * 8);
+ .u-col-9 {
+ width: calc(100%/12 * 9);
+ .u-col-10 {
+ width: calc(100%/12 * 10);
+ .u-col-11 {
+ width: calc(100%/12 * 11);
+ .u-col-12 {
+ width: calc(100%/12 * 12);
@@ -0,0 +1,206 @@
+ <view class="u-collapse-item" :style="[itemStyle]">
+ <view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
+ <block v-if="!$slots['title-all']">
+ <view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
+ isShow && activeStyle && !arrow ? activeStyle : '']">
+ <slot v-else name="title" />
+ <u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
+ class="u-arrow-down-icon" name="arrow-down"></u-icon>
+ <slot v-else name="title-all" />
+ <view class="u-collapse-body" :style="[{
+ height: isShow ? height + 'px' : '0'
+ }]">
+ <view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
+ * collapseItem 手风琴Item
+ * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
+ * @tutorial https://www.uviewui.com/components/collapse.html
+ * @property {String} title 面板标题
+ * @property {String Number} index 主要用于事件的回调,标识那个Item被点击
+ * @property {Boolean} disabled 面板是否可以打开或收起(默认false)
+ * @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
+ * @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
+ * @property {String} align 标题的对齐方式(默认left)
+ * @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
+ * @event {Function} change 某个item被打开或者收起时触发
+ * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
+ name: "u-collapse-item",
+ emits: ["change"],
+ // 标题的对齐方式
+ // 是否可以点击收起
+ // collapse显示与否
+ open: {
+ // 唯一标识符
+ //活动样式
+ activeStyle: {
+ // 标识当前为第几个
+ isShow: false,
+ height: 0, // body内容的高度
+ headStyle: {}, // 头部样式,对象形式
+ bodyStyle: {}, // 主体部分样式
+ itemStyle: {}, // 每个item的整体样式
+ arrowColor: '', // 箭头的颜色
+ hoverClass: '', // 头部按下时的效果样式类
+ arrow: true, // 是否显示右侧箭头
+ open(val) {
+ this.isShow = val;
+ // 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
+ this.isShow = this.open;
+ // 异步获取内容,或者动态修改了内容时,需要重新初始化
+ this.parent = this.$u.$parent.call(this, 'u-collapse');
+ if(this.parent) {
+ this.nameSync = this.name ? this.name : this.parent.childrens.length;
+ // 不存在时才添加本实例
+ !this.parent.childrens.includes(this) && this.parent.childrens.push(this);
+ this.headStyle = this.parent.headStyle;
+ this.bodyStyle = this.parent.bodyStyle;
+ this.arrowColor = this.parent.arrowColor;
+ this.hoverClass = this.parent.hoverClass;
+ this.arrow = this.parent.arrow;
+ this.itemStyle = this.parent.itemStyle;
+ this.queryRect();
+ // 点击collapsehead头部
+ if (this.disabled) return;
+ if (this.parent && this.parent.accordion == true) {
+ this.parent.childrens.map(val => {
+ // 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
+ if (this != val) {
+ val.isShow = false;
+ this.isShow = !this.isShow;
+ // 触发本组件的事件
+ index: this.index,
+ show: this.isShow
+ // 只有在打开时才发出事件
+ if (this.isShow) this.parent && this.parent.onChange();
+ this.$forceUpdate();
+ // 查询内容高度
+ queryRect() {
+ // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+ // 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
+ this.$uGetRect('#' + this.elId).then(res => {
+ this.height = res.height;
+ this.init();
+ .u-collapse-head {
+ .u-collapse-title {
+ .u-arrow-down-icon {
+ transition: all 0.3s;
+ margin-right: 20rpx;
+ margin-left: 14rpx;
+ .u-arrow-down-icon-active {
+ transform: rotate(180deg);
+ .u-collapse-body {
+ .u-collapse-content {
@@ -0,0 +1,100 @@
+ <view class="u-collapse">
+ * collapse 手风琴
+ * @description 通过折叠面板收纳内容区域
+ * @property {Boolean} accordion 是否手风琴模式(默认true)
+ * @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
+ * @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
+ * @property {Object} head-style 标题自定义样式,对象形式
+ * @property {Object} body-style 主体自定义样式,对象形式
+ * @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
+ * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
+ * @example <u-collapse></u-collapse>
+ name:"u-collapse",
+ // 是否手风琴模式
+ accordion: {
+ // 头部的样式
+ // 主体的样式
+ // 每一个item的样式
+ itemStyle: {
+ // 是否显示右侧的箭头
+ // 箭头的颜色
+ arrowColor: {
+ // 标题部分按压时的样式类,"none"为无效果
+ default: 'u-hover-class'
+ this.childrens = []
+ // 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
+ this.childrens.forEach((vm, index) => {
+ vm.init();
+ // collapse item被点击,由collapse item调用父组件方法
+ onChange() {
+ let activeItem = [];
+ if (vm.isShow) {
+ activeItem.push(vm.nameSync);
+ // 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
+ if (this.accordion) activeItem = activeItem.join('');
+ this.$emit('change', activeItem);
@@ -0,0 +1,238 @@
+ class="u-notice-bar"
+ background: computeBgColor,
+ padding: padding
+ :class="[
+ type ? `u-type-${type}-light-bg` : ''
+ <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
+ <swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
+ <swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
+ class="u-news-item u-line-1"
+ :style="[textStyle]"
+ @tap="click(index)"
+ :class="['u-type-' + type]"
+ </swiper-item>
+ </swiper>
+ <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
+ <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
+ emits: ["close", "getMore", "end"],
+ // 显示的内容,数组
+ // 显示的主题,success|error|primary|info|warning
+ // 是否显示左侧的音量图标
+ volumeIcon: {
+ // 是否显示右侧的右箭头图标
+ moreIcon: {
+ // 是否显示右侧的关闭图标
+ closeIcon: {
+ // 是否自动播放
+ autoplay: {
+ // 文字颜色,各图标也会使用文字颜色
+ // 滚动方向,row-水平滚动,column-垂直滚动
+ direction: {
+ default: 'row'
+ // 字体大小,单位rpx
+ default: 26
+ // 滚动一个周期的时间长,单位ms
+ default: 2000
+ // 音量喇叭的大小
+ volumeSize: {
+ // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+ speed: {
+ default: 160
+ // 水平滚动时,是否采用衔接形式滚动
+ isCircular: {
+ // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
+ default: 'horizontal'
+ // 播放状态,play-播放,paused-暂停
+ playState: {
+ default: 'play'
+ // 是否禁止用手滑动切换
+ // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
+ disableTouch: {
+ // 通知的边距
+ default: '18rpx 24rpx'
+ // 计算字体颜色,如果没有自定义的,就用uview主题颜色
+ computeColor() {
+ if (this.color) return this.color;
+ // 如果是无主题,就默认使用content-color
+ else if(this.type == 'none') return '#606266';
+ else return this.type;
+ // 文字内容的样式
+ textStyle() {
+ if (this.color) style.color = this.color;
+ else if(this.type == 'none') style.color = '#606266';
+ style.fontSize = this.fontSize + 'rpx';
+ // 垂直或者水平滚动
+ vertical() {
+ if(this.mode == 'horizontal') return false;
+ // 计算背景颜色
+ computeBgColor() {
+ if (this.bgColor) return this.bgColor;
+ else if(this.type == 'none') return 'transparent';
+ // animation: false
+ // 点击通告栏
+ click(index) {
+ this.$emit('click', index);
+ // 点击更多箭头按钮
+ getMore() {
+ this.$emit('getMore');
+ change(e) {
+ let index = e.detail.current;
+ if(index == this.list.length - 1) {
+ this.$emit('end');
+.u-notice-bar {
+ flex-wrap: nowrap;
+ padding: 18rpx 24rpx;
+.u-swiper {
+ margin-left: 12rpx;
+.u-swiper-item {
+.u-news-item {
+.u-right-icon {
+.u-left-icon {
@@ -0,0 +1,175 @@
+ <view class="u-count-down">
+ <slot>
+ <text class="u-count-down__text" :style="customStyle">{{ formattedTime }}</text>
+ </slot>
+import { isSameSecond, parseFormat, parseTimeData } from "./utils";
+ * u-count-down 倒计时
+ * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
+ * @tutorial https://uviewui.com/components/countDown.html
+ * @property {String | Number} timestamp 倒计时时长,单位ms (默认 0 )
+ * @property {String} format 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 (默认 'HH:mm:ss' )
+ * @property {Boolean} autoStart 是否自动开始倒计时 (默认 true )
+ * @event {Function} end 倒计时结束时触发
+ * @event {Function} change 倒计时变化时触发
+ * @event {Function} start 开始倒计时
+ * @event {Function} pause 暂停倒计时
+ * @event {Function} reset 重设倒计时,若 auto-start 为 true,重设后会自动开始倒计时
+ * @example <u-count-down :timestamp="timestamp"></u-count-down>
+ name: "u-count-down",
+ emits: ["change", "end", "finish"],
+ // 倒计时时长,单位ms
+ timestamp: {
+ // 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒
+ format: {
+ default: "DD:HH:mm:ss"
+ // 是否自动开始倒计时
+ autoStart: {
+ type: [String, Object],
+ timer: null,
+ // 各单位(天,时,分等)剩余时间
+ timeData: parseTimeData(0),
+ // 格式化后的时间,如"03:23:21"
+ formattedTime: "0",
+ // 倒计时是否正在进行中
+ runing: false,
+ endTime: 0, // 结束的毫秒时间戳
+ remainTime: 0 // 剩余的毫秒时间
+ timestamp(n) {
+ this.reset();
+ format(newVal, oldVal) {
+ this.pause();
+ this.start();
+ // 开始倒计时
+ start() {
+ if (this.runing) return;
+ // 标识为进行中
+ this.runing = true;
+ // 结束时间戳 = 此刻时间戳 + 剩余的时间
+ this.endTime = Date.now() + this.remainTime;
+ this.toTick();
+ // 根据是否展示毫秒,执行不同操作函数
+ toTick() {
+ if (this.format.indexOf("SSS") > -1) {
+ this.microTick();
+ this.macroTick();
+ macroTick() {
+ this.clearTimeout();
+ // 每隔一定时间,更新一遍定时器的值
+ // 同时此定时器的作用也能带来毫秒级的更新
+ this.timer = setTimeout(() => {
+ // 获取剩余时间
+ const remain = this.getRemainTime();
+ // 重设剩余时间
+ if (!isSameSecond(remain, this.remainTime) || remain === 0) {
+ this.setRemainTime(remain);
+ // 如果剩余时间不为0,则继续检查更新倒计时
+ if (this.remainTime !== 0) {
+ }, 30);
+ microTick() {
+ this.setRemainTime(this.getRemainTime());
+ // 获取剩余的时间
+ getRemainTime() {
+ // 取最大值,防止出现小于0的剩余时间值
+ return Math.max(this.endTime - Date.now(), 0);
+ // 设置剩余的时间
+ setRemainTime(remain) {
+ this.remainTime = remain;
+ // 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
+ const timeData = parseTimeData(remain);
+ this.$emit("change", timeData);
+ // 得出格式化后的时间
+ this.formattedTime = parseFormat(this.format, timeData);
+ // 如果时间已到,停止倒计时
+ if (remain <= 0) {
+ this.$emit("end");
+ this.$emit("finish");
+ // 重置倒计时
+ reset() {
+ this.remainTime = this.timestamp;
+ this.setRemainTime(this.remainTime);
+ if (this.autoStart) {
+ // 暂停倒计时
+ pause() {
+ this.runing = false;
+ // 清空定时器
+ clearTimeout() {
+ clearTimeout(this.timer);
+ beforeDestroy() {
+ beforeUnmount() {
+<style lang="scss"></style>
@@ -0,0 +1,62 @@
+// 补0,如1 -> 01
+function padZero(num, targetLength = 2) {
+ let str = `${num}`
+ while (str.length < targetLength) {
+ str = `0${str}`
+ return str
+const SECOND = 1000
+const MINUTE = 60 * SECOND
+const HOUR = 60 * MINUTE
+const DAY = 24 * HOUR
+export function parseTimeData(time) {
+ const days = Math.floor(time / DAY)
+ const hours = Math.floor((time % DAY) / HOUR)
+ const minutes = Math.floor((time % HOUR) / MINUTE)
+ const seconds = Math.floor((time % MINUTE) / SECOND)
+ const milliseconds = Math.floor(time % SECOND)
+ days,
+ hours,
+ minutes,
+ seconds,
+ milliseconds
+export function parseFormat(format, timeData) {
+ let {
+ } = timeData
+ // 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
+ if (format.indexOf('DD') === -1) {
+ hours += days * 24
+ // 对天补0
+ format = format.replace('DD', padZero(days))
+ // 其他同理于DD的格式化处理方式
+ if (format.indexOf('HH') === -1) {
+ minutes += hours * 60
+ format = format.replace('HH', padZero(hours))
+ if (format.indexOf('mm') === -1) {
+ seconds += minutes * 60
+ format = format.replace('mm', padZero(minutes))
+ if (format.indexOf('ss') === -1) {
+ milliseconds += seconds * 1000
+ format = format.replace('ss', padZero(seconds))
+ return format.replace('SSS', padZero(milliseconds, 3))
+export function isSameSecond(time1, time2) {
+ return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
@@ -0,0 +1,266 @@
+ class="u-count-num"
+ fontWeight: bold ? 'bold' : 'normal',
+ color: color
+ {{ displayValueCom }}
+ * countTo 数字滚动
+ * @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
+ * @tutorial https://www.uviewui.com/components/countTo.html
+ * @property {String Number} nullVal 空值或NaN时显示的值,默认 -
+ * @property {String Number} start-val 开始值
+ * @property {String Number} end-val 结束值
+ * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
+ * @property {Boolean} autoplay 是否自动开始滚动(默认true)
+ * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
+ * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
+ * @property {String} separator 千位分隔符,见官网说明
+ * @property {String} color 字体颜色(默认#303133)
+ * @property {String Number} font-size 字体大小,单位rpx(默认50)
+ * @property {Boolean} bold 字体是否加粗(默认false)
+ * @event {Function} end 数值滚动到目标值时触发
+ * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
+ name: "u-count-to",
+ emits: ["end"],
+ // 没有值时显示
+ nullVal: {
+ default: "-"
+ // 开始的数值,默认从0增长到某一个数
+ startVal: {
+ // 要滚动的目标数值,必须
+ endVal: {
+ required: true
+ // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
+ // 设置数值后是否自动开始滚动
+ // 要显示的小数位数
+ decimals: {
+ // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
+ useEasing: {
+ // 十进制分割
+ decimal: {
+ default: "."
+ // 字体颜色
+ default: "#303133"
+ default: 50
+ // 是否加粗字体
+ bold: {
+ // 千位分隔符,类似金额的分割(¥23,321.05中的",")
+ separator: {
+ localStartVal: this.startVal,
+ displayValue: this.formatNumber(this.startVal),
+ printVal: null,
+ paused: false, // 是否暂停
+ localDuration: Number(this.duration),
+ startTime: null, // 开始的时间
+ timestamp: null, // 时间戳
+ remaining: null, // 停留的时间
+ rAF: null,
+ lastTime: 0 // 上一次的时间
+ countDown() {
+ return this.startVal > this.endVal;
+ displayValueCom() {
+ let str;
+ let { displayValue, nullVal, endVal } = this;
+ if (isNaN(endVal)) {
+ str = nullVal;
+ str = displayValue;
+ return str;
+ startVal() {
+ this.autoplay && this.start();
+ endVal() {
+ easingFn(t, b, c, d) {
+ return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
+ requestAnimationFrame(callback) {
+ const currTime = new Date().getTime();
+ // 为了使setTimteout的尽可能的接近每秒60帧的效果
+ const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
+ const id = setTimeout(() => {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ this.lastTime = currTime + timeToCall;
+ return id;
+ cancelAnimationFrame(id) {
+ clearTimeout(id);
+ // 开始滚动数字
+ this.localStartVal = this.startVal;
+ this.startTime = null;
+ this.localDuration = this.duration;
+ this.paused = false;
+ this.rAF = this.requestAnimationFrame(this.count);
+ // 暂定状态,重新再开始滚动;或者滚动状态下,暂停
+ reStart() {
+ if (this.paused) {
+ this.resume();
+ this.stop();
+ this.paused = true;
+ // 暂停
+ stop() {
+ this.cancelAnimationFrame(this.rAF);
+ // 重新开始(暂停的情况下)
+ resume() {
+ this.localDuration = this.remaining;
+ this.localStartVal = this.printVal;
+ this.requestAnimationFrame(this.count);
+ // 重置
+ this.displayValue = this.formatNumber(this.startVal);
+ count(timestamp) {
+ if (!this.startTime) this.startTime = timestamp;
+ this.timestamp = timestamp;
+ const progress = timestamp - this.startTime;
+ this.remaining = this.localDuration - progress;
+ if (this.useEasing) {
+ if (this.countDown) {
+ this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
+ this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
+ this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
+ this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
+ this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
+ this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
+ this.displayValue = this.formatNumber(this.printVal);
+ if (progress < this.localDuration) {
+ // 判断是否数字
+ isNumber(val) {
+ return !isNaN(parseFloat(val));
+ formatNumber(num) {
+ // 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
+ num = Number(num);
+ num = num.toFixed(Number(this.decimals));
+ num += "";
+ const x = num.split(".");
+ let x1 = x[0];
+ const x2 = x.length > 1 ? this.decimal + x[1] : "";
+ const rgx = /(\d+)(\d{3})/;
+ if (this.separator && !this.isNumber(this.separator)) {
+ while (rgx.test(x1)) {
+ x1 = x1.replace(rgx, "$1" + this.separator + "$2");
+ return x1 + x2;
+ destroyed() {
+ unmounted() {
+.u-count-num {
+ <view class="u-divider" :style="{
+ height: height == 'auto' ? 'auto' : height + 'rpx',
+ marginBottom: marginBottom + 'rpx',
+ marginTop: marginTop + 'rpx'
+ }" @tap="click">
+ <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+ <view v-if="useSlot" class="u-divider-text" :style="{
+ fontSize: fontSize + 'rpx'
+ }"><slot /></view>
+ * divider 分割线
+ * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
+ * @tutorial https://www.uviewui.com/components/divider.html
+ * @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
+ * @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
+ * @property {String} color 文字颜色(默认#909399)
+ * @property {String Number} fontSize 字体大小,单位rpx(默认26)
+ * @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
+ * @property {String Number} height 整个divider的高度,单位rpx(默认40)
+ * @property {String} type 将线条设置主题色(默认primary)
+ * @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @event {Function} click divider组件被点击时触发
+ * @example <u-divider color="#fa3534">长河落日圆</u-divider>
+ name: 'u-divider',
+ // 单一边divider横线的宽度(数值),单位rpx。或者百分比
+ halfWidth: {
+ // divider横线的颜色,如设置,
+ default: '#dcdfe6'
+ // 主题色,可以是primary|info|success|warning|error之一值
+ // 文字颜色
+ // 文字大小,单位rpx
+ // 整个divider的背景颜色
+ // 整个divider的高度单位rpx
+ height: {
+ default: 'auto'
+ // 上边距
+ marginTop: {
+ // 下边距
+ marginBottom: {
+ // 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
+ useSlot: {
+ lineStyle() {
+ if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
+ else style.width = this.halfWidth + 'rpx';
+ // borderColor优先级高于type值
+ if(this.borderColor) style.borderColor = this.borderColor;
+.u-divider {
+.u-divider-line {
+ transform: scale(1, 0.5);
+ &--bordercolor--primary {
+ &--bordercolor--success {
+ &--bordercolor--error {
+ &--bordercolor--info {
+ &--bordercolor--warning {
+.u-divider-text {
@@ -0,0 +1,166 @@
+ class="u-dropdown-item"
+ v-if="active"
+ @touchmove.stop.prevent="() => {}"
+ @tap.stop.prevent="() => {}"
+ <block v-if="!$slots.default && !$slots.$default">
+ <scroll-view
+ scroll-y="true"
+ height: $u.addUnit(height)
+ <view class="u-dropdown-item__options">
+ <u-cell-group>
+ <u-cell-item
+ @click="cellClick(item.value)"
+ :arrow="false"
+ :title="item.label"
+ v-for="(item, index) in options"
+ :key="index"
+ :title-style="{
+ color: value === item.value ? activeColor : inactiveColor
+ <u-icon
+ v-if="valueCom === item.value"
+ name="checkbox-mark"
+ :color="activeColor"
+ size="32"
+ ></u-icon>
+ </u-cell-item>
+ </u-cell-group>
+ </scroll-view>
+ * dropdown-item 下拉菜单
+ * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+ * @tutorial http://uviewui.com/components/dropdown.html
+ * @property {String | Number} v-model 双向绑定选项卡选择值
+ * @property {String} title 菜单项标题
+ * @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
+ * @property {Boolean} disabled 是否禁用此选项卡(默认false)
+ * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+ * @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
+ * @example <u-dropdown-item title="标题"></u-dropdown-item>
+ name: "u-dropdown-item",
+ // 当前选中项的value值
+ type: [Number, String, Array],
+ // 菜单项标题
+ // 选项数据,如果传入了默认slot,此参数无效
+ options: {
+ // 是否禁用此菜单项
+ // 下拉弹窗的高度
+ active: false, // 当前项是否处于展开状态
+ activeColor: "#2979ff", // 激活时左边文字和右边对勾图标的颜色
+ inactiveColor: "#606266" // 未激活时左边文字和右边对勾图标的颜色
+ // 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
+ propsChange() {
+ return `${this.title}-${this.disabled}`;
+ propsChange(n) {
+ // 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
+ // 将所有子组件数据重新整理一遍
+ if (this.parent) this.parent.init();
+ // 父组件的实例
+ // 获取父组件u-dropdown
+ let parent = this.$u.$parent.call(this, "u-dropdown");
+ this.parent = parent;
+ // 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
+ this.activeColor = parent.activeColor;
+ this.inactiveColor = parent.inactiveColor;
+ // 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
+ // push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
+ let exist = parent.children.find(val => {
+ return this === val;
+ if (!exist) parent.children.push(this);
+ if (parent.children.length == 1) this.active = true;
+ // 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
+ parent.menuList.push({
+ title: this.title,
+ disabled: this.disabled
+ // cell被点击
+ cellClick(value) {
+ // 修改通过v-model绑定的值
+ this.$emit("input", value);
+ this.$emit("update:modelValue", value);
+ // 通知父组件(u-dropdown)收起菜单
+ this.parent.close();
+ // 发出事件,抛出当前勾选项的value
+ this.$emit("change", value);
@@ -0,0 +1,299 @@
+ <view class="u-dropdown">
+ <view class="u-dropdown__menu" :style="{
+ }" :class="{
+ 'u-border-bottom': borderBottom
+ <view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
+ <view class="u-flex">
+ <text class="u-dropdown__menu__item__text" :style="{
+ color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
+ fontSize: $u.addUnit(titleSize)
+ }">{{item.title}}</text>
+ <view class="u-dropdown__menu__item__arrow" :class="{
+ 'u-dropdown__menu__item__arrow--rotate': index === current
+ <u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
+ <view class="u-dropdown__content" :style="[contentStyle, {
+ transition: `opacity ${duration / 1000}s linear`,
+ top: $u.addUnit(height),
+ height: contentHeight + 'px'
+ }]"
+ @tap="maskClick" @touchmove.stop.prevent>
+ <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
+ <view class="u-dropdown__content__mask"></view>
+ * dropdown 下拉菜单
+ * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
+ * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
+ * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
+ * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
+ * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
+ * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
+ * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
+ * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
+ * @event {Function} open 下拉菜单被打开时触发
+ * @event {Function} close 下拉菜单被关闭时触发
+ * @example <u-dropdown></u-dropdown>
+ name: 'u-dropdown',
+ emits: ["open", "close"],
+ // 菜单标题和选项的激活态颜色
+ default: '#2979ff'
+ // 菜单标题和选项的未激活态颜色
+ // 点击遮罩是否关闭菜单
+ closeOnClickMask: {
+ // 点击当前激活项标题是否关闭菜单
+ closeOnClickSelf: {
+ // 过渡时间
+ default: 300
+ // 标题菜单的高度,单位任意,数值默认为rpx单位
+ default: 80
+ // 标题的字体大小
+ default: 28
+ // 下拉出来的内容部分的圆角值
+ // 菜单右侧的icon图标
+ menuIcon: {
+ default: 'arrow-down'
+ // 菜单右侧图标的大小
+ menuIconSize: {
+ showDropdown: true, // 是否打开下来菜单,
+ menuList: [], // 显示的菜单
+ active: false, // 下拉菜单的状态
+ // 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
+ // 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
+ current: 99999,
+ // 外层内容的样式,初始时处于底层,且透明
+ contentStyle: {
+ zIndex: -1,
+ opacity: 0
+ // 让某个菜单保持高亮的状态
+ highlightIndex: 99999,
+ contentHeight: 0
+ // 下拉出来部分的样式
+ popupStyle() {
+ // 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
+ style.transform = `translateY(${this.active ? 0 : '-100%'})`
+ style['transition-duration'] = this.duration / 1000 + 's';
+ style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
+ // 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
+ this.getContentHeight();
+ // 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
+ // 以保证数据的正确性
+ this.menuList = [];
+ this.children.map(child => {
+ child.init();
+ // 点击菜单
+ menuClick(index) {
+ // 判断是否被禁用
+ if (this.menuList[index].disabled) return;
+ // 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
+ if (index === this.current && this.closeOnClickSelf) {
+ // 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
+ this.children[index].active = false;
+ }, this.duration)
+ this.open(index);
+ // 打开下拉菜单
+ open(index) {
+ // 重置高亮索引,否则会造成多个菜单同时高亮
+ // this.highlightIndex = 9999;
+ // 展开时,设置下拉内容的样式
+ this.contentStyle = {
+ zIndex: 11,
+ // 标记展开状态以及当前展开项的索引
+ this.active = true;
+ this.current = index;
+ // 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
+ // 之所以不是因display: none,是因为nvue没有display这个属性
+ this.children.map((val, idx) => {
+ val.active = index == idx ? true : false;
+ this.$emit('open', this.current);
+ // 设置下拉菜单处于收起状态
+ this.$emit('close', this.current);
+ // 设置为收起状态,同时current归位,设置为空字符串
+ this.active = false;
+ this.current = 99999;
+ // 下拉内容的样式进行调整,不透明度设置为0
+ // 点击遮罩
+ maskClick() {
+ // 如果不允许点击遮罩,直接返回
+ if (!this.closeOnClickMask) return;
+ // 外部手动设置某个菜单高亮
+ highlight(index = undefined) {
+ this.highlightIndex = index !== undefined ? index : 99999;
+ // 获取下拉菜单内容的高度
+ getContentHeight() {
+ // 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
+ // 才能让遮罩占满菜单一下,直到屏幕底部的高度
+ // this.$u.sys()为uView封装的获取设备信息的方法
+ let windowHeight = this.$u.sys().windowHeight;
+ this.$uGetRect('.u-dropdown__menu').then(res => {
+ // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
+ // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
+ // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
+ // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
+ this.contentHeight = windowHeight - res.bottom;
+ .u-dropdown {
+ &__menu {
+ &__arrow {
+ transition: transform .3s;
+ &--rotate {
+ z-index: 8;
+ left: 0px;
+ &__mask {
+ background: rgba(0, 0, 0, .3);
+ &__popup {
+ z-index: 10;
+ transform: translate3D(0, -100%, 0);
@@ -0,0 +1,193 @@
+ <view class="u-empty" v-if="show" :style="{
+ :name="src ? src : 'empty-' + mode"
+ :custom-style="iconStyle"
+ :label="text ? text : icons[mode]"
+ label-pos="bottom"
+ :label-color="color"
+ :label-size="fontSize"
+ :size="iconSize"
+ :color="iconColor"
+ margin-top="14"
+ <view class="u-slot-wrap">
+ <slot name="bottom"></slot>
+ * empty 内容为空
+ * @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
+ * @tutorial https://www.uviewui.com/components/empty.html
+ * @property {String} color 文字颜色(默认#c0c4cc)
+ * @property {String} text 文字提示(默认“无内容”)
+ * @property {String} src 自定义图标路径,如定义,mode参数会失效
+ * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
+ * @property {String} mode 内置的图标,见官网说明(默认data)
+ * @property {String Number} img-width 图标的宽度,单位rpx(默认240)
+ * @property {String} img-height 图标的高度,单位rpx(默认auto)
+ * @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
+ * @property {Boolean} show 是否显示组件(默认true)
+ * @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
+ name: "u-empty",
+ // 图标路径
+ text: {
+ default: '#c0c4cc'
+ // 图标的颜色
+ iconColor: {
+ // 图标的大小
+ default: 120
+ // 选择预置的图标类型
+ default: 'data'
+ // 图标宽度,单位rpx
+ imgWidth: {
+ // 图标高度,单位rpx
+ imgHeight: {
+ // 组件距离上一个元素之间的距离
+ icons: {
+ car: '购物车为空',
+ page: '页面不存在',
+ search: '没有搜索结果',
+ address: '没有收货地址',
+ wifi: '没有WiFi',
+ order: '订单为空',
+ coupon: '没有优惠券',
+ favor: '暂无收藏',
+ permission: '无权限',
+ history: '无历史记录',
+ news: '无新闻列表',
+ message: '消息列表为空',
+ list: '列表为空',
+ data: '数据为空'
+ // icons: [{
+ // icon: 'car',
+ // text: '购物车为空'
+ // },{
+ // icon: 'page',
+ // text: '页面不存在'
+ // icon: 'search',
+ // text: '没有搜索结果'
+ // icon: 'address',
+ // text: '没有收货地址'
+ // icon: 'wifi',
+ // text: '没有WiFi'
+ // icon: 'order',
+ // text: '订单为空'
+ // icon: 'coupon',
+ // text: '没有优惠券'
+ // icon: 'favor',
+ // text: '暂无收藏'
+ // icon: 'permission',
+ // text: '无权限'
+ // icon: 'history',
+ // text: '无历史记录'
+ // icon: 'news',
+ // text: '无新闻列表'
+ // icon: 'message',
+ // text: '消息列表为空'
+ // icon: 'list',
+ // text: '列表为空'
+ // icon: 'data',
+ // text: '数据为空'
+ // }],
+ .u-empty {
+ .u-image {
+ margin-bottom: 20rpx;
+ .u-slot-wrap {
@@ -0,0 +1,402 @@
+ <view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
+ <view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
+ <view class="u-label" :class="[required ? 'u-required' : '']" :style="{
+ justifyContent: justifyContent,
+ flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
+ <view class="u-icon-wrap" v-if="icon">
+ <u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
+ <text class="u-label-text" :class="[$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
+ <view class="fild-body">
+ <view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
+ <textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="valueCom"
+ :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
+ :focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
+ @tap="fieldClick" />
+ <input
+ v-else
+ :style="[fieldStyle]"
+ :type="type"
+ class="u-flex-1 u-field__input-wrap"
+ :value="valueCom"
+ :password="password || type === 'password'"
+ :placeholder="placeholder"
+ :placeholderStyle="placeholderStyle"
+ :maxlength="inputMaxlength"
+ :focus="focus"
+ :confirmType="confirmType"
+ @focus="onFocus"
+ @blur="onBlur"
+ @input="onInput"
+ @confirm="onConfirm"
+ @tap="fieldClick"
+ />
+ <u-icon :size="clearSize" v-if="clearable && valueCom != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
+ <view class="u-button-wrap"><slot name="right" /></view>
+ <u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
+ <view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
+ paddingLeft: labelWidth + 'rpx'
+ }">{{ errorMessage }}</view>
+ * field 输入框
+ * @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
+ * @tutorial https://www.uviewui.com/components/field.html
+ * @property {String} type 输入框的类型(默认text)
+ * @property {String} icon label左边的图标,限uView的图标名称
+ * @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
+ * @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
+ * @property {String} label 输入框左边的文字提示
+ * @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
+ * @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
+ * @property {Number String} label-width label的宽度,单位rpx(默认130)
+ * @property {String} label-align label的文字对齐方式(默认left)
+ * @property {Object} field-style 自定义输入框的样式,对象形式
+ * @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
+ * @property {String} input-align 输入框内容对齐方式(默认left)
+ * @property {Boolean} border-bottom 是否显示field的下边框(默认true)
+ * @property {Boolean} border-top 是否显示field的上边框(默认false)
+ * @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
+ * @property {String} placeholder 输入框的提示文字
+ * @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} disabled 是否不可输入(默认false)
+ * @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+ * @event {Function} input 输入框内容发生变化时触发
+ * @event {Function} focus 输入框获得焦点时触发
+ * @event {Function} blur 输入框失去焦点时触发
+ * @event {Function} confirm 点击完成按钮时触发
+ * @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
+ * @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
+ * @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
+ name:"u-field",
+ emits: ["update:modelValue", "input", "focus", "blur", "confirm", "right-icon-click", "click"],
+ value: [Number, String],
+ modelValue: [Number, String],
+ icon: String,
+ rightIcon: String,
+ required: Boolean,
+ label: String,
+ password: Boolean,
+ clearable: {
+ // 左边标题的宽度单位rpx
+ labelWidth: {
+ default: 130
+ // 对齐方式,left|center|right
+ labelAlign: {
+ inputAlign: {
+ autoHeight: {
+ errorMessage: {
+ placeholder: String,
+ placeholderStyle: String,
+ focus: Boolean,
+ fixed: Boolean,
+ default: 'text'
+ maxlength: {
+ default: 140
+ confirmType: {
+ default: 'done'
+ // lable的位置,可选为 left-左边,top-上边
+ labelPosition: {
+ // 输入框的自定义样式
+ fieldStyle: {
+ // 清除按钮的大小
+ clearSize: {
+ default: 30
+ // lable左边的图标样式,对象形式
+ // 是否自动去除两端的空格
+ trim: {
+ focused: false,
+ itemIndex: 0,
+ inputWrapStyle() {
+ style.textAlign = this.inputAlign;
+ // 判断lable的位置,如果是left的话,让input左边两边有间隙
+ if(this.labelPosition == 'left') {
+ style.margin = `0 8rpx`;
+ // 如果lable是top的,input的左边就没必要有间隙了
+ style.marginRight = `8rpx`;
+ rightIconStyle() {
+ let arrowDirectionObj = {
+ "top": "-90deg",
+ "bottom": "90deg",
+ "left": "180deg",
+ "right": "0deg",
+ let deg = arrowDirectionObj[this.arrowDirection] || "0deg";
+ style.transform = 'rotate('+deg+')';
+ labelStyle() {
+ if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
+ if(this.labelAlign == 'center') style.justifyContent = 'center';
+ if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
+ // uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
+ justifyContent() {
+ if(this.labelAlign == 'left') return 'flex-start';
+ if(this.labelAlign == 'center') return 'center';
+ if(this.labelAlign == 'right') return 'flex-end';
+ // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+ inputMaxlength() {
+ return Number(this.maxlength)
+ // label的位置
+ fieldInnerStyle() {
+ style.flexDirection = 'row';
+ style.flexDirection = 'column';
+ onInput(event) {
+ let value = event.detail.value;
+ // 判断是否去除空格
+ if(this.trim) value = this.$u.trim(value);
+ this.$emit('input', value);
+ onFocus(event) {
+ this.focused = true;
+ this.$emit('focus', event);
+ onBlur(event) {
+ // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+ // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+ this.focused = false;
+ }, 100)
+ this.$emit('blur', event);
+ onConfirm(e) {
+ this.$emit('confirm', e.detail.value);
+ onClear(event) {
+ this.$emit('input', '');
+ this.$emit("update:modelValue", '');
+ rightIconClick() {
+ this.$emit('right-icon-click');
+ fieldClick() {
+.u-field {
+ padding: 20rpx 28rpx;
+.u-field-inner {
+.u-textarea-inner {
+.u-textarea-class {
+ min-height: 96rpx;
+.fild-body {
+.u-arror-right {
+ margin-left: 8rpx;
+.u-label-text {
+.u-label-left-gap {
+.u-label-postion-top {
+.u-label {
+ width: 130rpx;
+ flex: 1 1 130rpx;
+.u-required::before {
+ left: -16rpx;
+ height: 9px;
+.u-field__input-wrap {
+.u-clear-icon {
+.u-error-message {
+.placeholder-style {
+ color: rgb(150, 151, 153);
+.u-input-class {
+.u-button-wrap {
@@ -0,0 +1,509 @@
+ <view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
+ <view class="u-form-item__body" :style="{
+ flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
+ <!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
+ <view class="u-form-item--left" :style="{
+ width: uLabelWidth,
+ flex: `0 0 ${uLabelWidth}`,
+ marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
+ <!-- 为了块对齐 -->
+ <view class="u-form-item--left__content" v-if="required || leftIcon || label">
+ <!-- nvue不支持伪元素before -->
+ <text v-if="required" class="u-form-item--left__content--required">*</text>
+ <view class="u-form-item--left__content__icon" v-if="leftIcon">
+ <u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
+ <view class="u-form-item--left__content__label" :style="[elLabelStyle, {
+ 'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
+ {{label}}
+ <view class="u-form-item--right u-flex">
+ <view class="u-form-item--right__content">
+ <view class="u-form-item--right__content__slot" :style="elLabelPosition == 'left' && elInputAlign == 'right' ? 'text-align:right;display: inline-block;line-height:initial;' : ''">
+ <view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
+ <u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
+ <slot name="right" />
+ <view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
+ paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
+ textAlign: elInputAlign == 'right' ? 'right' : 'left'
+ }">{{validateMessage}}</view>
+ import Emitter from '../../libs/util/emitter.js';
+ import schema from '../../libs/util/async-validator';
+ // 去除警告信息
+ schema.warning = function() {};
+ * form-item 表单item
+ * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+ * @tutorial http://uviewui.com/components/form.html
+ * @property {String} label 左侧提示文字
+ * @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
+ * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+ * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+ * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+ * @property {Object} label-style lable的样式,对象形式
+ * @property {String} label-align lable的对齐方式
+ * @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {Object} left-icon-style 左侧图标的样式,对象形式
+ * @property {Object} right-icon-style 右侧图标的样式,对象形式
+ * @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
+ * @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
+ name: 'u-form-item',
+ inject: {
+ uForm: {
+ return null
+ // input的label提示语
+ // 绑定的值
+ prop: {
+ // 是否显示表单域的下划线边框
+ // label的位置,left-左边,top-上边
+ // label的宽度,单位rpx
+ // lable的样式,对象形式
+ // lable字体的对齐方式
+ // 右侧图标
+ rightIcon: {
+ // 左侧图标
+ leftIcon: {
+ // 左侧图标的样式
+ leftIconStyle: {
+ rightIconStyle: {
+ // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
+ inputAlign:{
+ initialValue: '', // 存储的默认值
+ // isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ validateState: '', // 是否校验成功
+ validateMessage: '', // 校验失败的提示语
+ // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+ errorType: ['message'],
+ fieldValue: '', // 获取当前子组件input的输入的值
+ // 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
+ parentData: {
+ borderBottom: true,
+ labelWidth: 90,
+ labelPosition: 'left',
+ labelStyle: {},
+ labelAlign: 'left',
+ inputAlign: 'left',
+ validateState(val) {
+ this.broadcastInputError();
+ // 监听u-form组件的errorType的变化
+ "uForm.errorType"(val) {
+ this.errorType = val;
+ // 计算后的label宽度,由于需要多个判断,故放到computed中
+ uLabelWidth() {
+ // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
+ return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
+ .elLabelWidth)) : '100%';
+ showError() {
+ return type => {
+ // 如果errorType数组中含有none,或者toast提示类型
+ if (this.errorType.indexOf('none') >= 0) return false;
+ else if (this.errorType.indexOf(type) >= 0) return true;
+ else return false;
+ // label的宽度
+ elLabelWidth() {
+ // label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
+ return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
+ .labelWidth :
+ 90);
+ // label的样式
+ elLabelStyle() {
+ return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
+ {});
+ // label的位置,左侧或者上方
+ elLabelPosition() {
+ return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
+ 'left');
+ // label的对齐方式
+ elLabelAlign() {
+ return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
+ // label的下划线
+ elBorderBottom() {
+ // 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
+ return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
+ true;
+ elInputAlign() {
+ return this.inputAlign ? this.inputAlign : (this.parentData.inputAlign ? this.parentData.inputAlign : 'left');
+ broadcastInputError() {
+ // 子组件发出事件,第三个参数为true或者false,true代表有错误
+ this.broadcast('u-input', 'onFormItemError', this.validateState === 'error' && this.showError('border'));
+ // 判断是否需要required校验
+ setRules() {
+ let that = this;
+ // 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ // 从父组件u-form拿到当前u-form-item需要验证 的规则
+ // let rules = this.getRules();
+ // if (rules.length) {
+ // this.isRequired = rules.some(rule => {
+ // // 如果有必填项,就返回,没有的话,就是undefined
+ // return rule.required;
+ // });
+ // blur事件
+ this.$on('onFieldBlur', that.onFieldBlur);
+ // change事件
+ this.$on('onFieldChange', that.onFieldChange);
+ // 从u-form的rules属性中,取出当前u-form-item的校验规则
+ getRules() {
+ // 父组件的所有规则
+ let rules = this.parent.rules;
+ rules = rules ? rules[this.prop] : [];
+ // 保证返回的是一个数组形式
+ return [].concat(rules || []);
+ // blur事件时进行表单校验
+ onFieldBlur() {
+ this.validation('blur');
+ // change事件进行表单校验
+ onFieldChange() {
+ this.validation('change');
+ // 过滤出符合要求的rule规则
+ getFilteredRule(triggerType = '') {
+ let rules = this.getRules();
+ // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
+ if (!triggerType) return rules;
+ // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
+ // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
+ // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
+ return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
+ getData(dataObj, name, defaultValue) {
+ let newDataObj;
+ if (dataObj) {
+ newDataObj = JSON.parse(JSON.stringify(dataObj));
+ let k = "",
+ d = ".",
+ l = "[",
+ r = "]";
+ name = name.replace(/\s+/g, k) + d;
+ let tstr = k;
+ for (let i = 0; i < name.length; i++) {
+ let theChar = name.charAt(i);
+ if (theChar != d && theChar != l && theChar != r) {
+ tstr += theChar;
+ } else if (newDataObj) {
+ if (tstr != k) newDataObj = newDataObj[tstr];
+ tstr = k;
+ if (typeof newDataObj === "undefined" && typeof defaultValue !== "undefined") newDataObj = defaultValue;
+ return newDataObj;
+ setData(dataObj, name, value) {
+ // 通过正则表达式 查找路径数据
+ let dataValue;
+ if (typeof value === "object") {
+ dataValue = JSON.parse(JSON.stringify(value));
+ dataValue = value;
+ let regExp = new RegExp("([\\w$]+)|\\[(:\\d)\\]", "g");
+ const patten = name.match(regExp);
+ // 遍历路径 逐级查找 最后一级用于直接赋值
+ for (let i = 0; i < patten.length - 1; i++) {
+ let keyName = patten[i];
+ if (typeof dataObj[keyName] !== "object") dataObj[keyName] = {};
+ dataObj = dataObj[keyName];
+ // 最后一级
+ dataObj[patten[patten.length - 1]] = dataValue;
+ // 校验数据
+ validation(trigger, callback = () => {}) {
+ // 检验之间,先获取需要校验的值
+ //this.fieldValue = this.parent.model[this.prop];
+ // 修改支持a.b
+ this.fieldValue = this.getData(this.parent.model,this.prop);
+ // blur和change是否有当前方式的校验规则
+ let rules = this.getFilteredRule(trigger);
+ // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
+ // 对count变量的统计错误而无法进入上一层的回调
+ if (!rules || rules.length === 0) {
+ return callback('');
+ // 设置当前的装填,标识为校验中
+ this.validateState = 'validating';
+ // 调用async-validator的方法
+ let validator = new schema({
+ [this.prop]: rules
+ validator.validate({
+ [this.prop]: this.fieldValue
+ }, {
+ firstFields: true
+ }, (errors, fields) => {
+ // 记录状态和报错信息
+ this.validateState = !errors ? 'success' : 'error';
+ this.validateMessage = errors ? errors[0].message : '';
+ let field = errors ? errors[0].field : '';
+ // 调用回调方法
+ callback(this.validateMessage, {
+ state: this.validateState,
+ key: field,
+ msg: this.validateMessage
+ // 清空当前的u-form-item
+ resetField() {
+ //this.parent.model[this.prop] = this.initialValue;
+ this.setData(this.parent.model,this.prop,this.initialValue);
+ // 设置为`success`状态,只是为了清空错误标记
+ this.validateState = 'success';
+ // 组件创建完成时,将当前实例保存到u-form中
+ this.parent = this.$u.$parent.call(this, 'u-form');
+ // 历遍parentData中的属性,将parent中的同名属性赋值给parentData
+ Object.keys(this.parentData).map(key => {
+ this.parentData[key] = this.parent[key];
+ // 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
+ if (this.prop) {
+ // 将本实例添加到父组件中
+ this.parent.fields.push(this);
+ this.errorType = this.parent.errorType;
+ // 设置初始值
+ this.initialValue = this.fieldValue;
+ // 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
+ // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
+ this.setRules();
+ // 组件销毁前,将实例从u-form的缓存中移除
+ // 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
+ if (this.parent && this.prop) {
+ this.parent.fields.map((item, index) => {
+ if (item === this) this.parent.fields.splice(index, 1);
+ .u-form-item {
+ // align-items: flex-start;
+ line-height: $u-form-item-height;
+ &__border-bottom--error:after {
+ padding-right: 10rpx;
+ margin-right: 8rpx;
+ &--required {
+ padding-top: 6rpx;
+ &__slot {
+ /* #ifndef MP */
+ &__message {
+ margin-top: 12rpx;
@@ -0,0 +1,148 @@
+ <view class="u-form"><slot /></view>
+ * form 表单
+ * @property {Object} model 表单数据对象
+ * @property {Object} rules 通过ref设置,见官网说明
+ * @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
+ * @example <u-form :model="form" ref="uForm"></u-form>
+ name: 'u-form',
+ // 当前form的需要验证字段的集合
+ model: {
+ // 验证规则
+ // rules: {
+ // type: [Object, Function, Array],
+ // return {};
+ // border-bottom-下边框呈现红色,none-无提示
+ errorType: {
+ return ['message', 'toast']
+ default: 90
+ // 表单内所有input的inputAlign属性的值
+ // 表单内所有input的clearable属性的值
+ clearable:{
+ provide() {
+ uForm: this
+ rules: {}
+ // 存储当前form下的所有u-form-item的实例
+ // 不能定义在data中,否则微信小程序会造成循环引用而报错
+ this.fields = [];
+ setRules(rules) {
+ this.rules = rules;
+ // 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
+ resetFields() {
+ this.fields.map(field => {
+ field.resetField();
+ // 校验全部数据
+ validate(callback) {
+ return new Promise(resolve => {
+ // 对所有的u-form-item进行校验
+ let valid = true; // 默认通过
+ let count = 0; // 用于标记是否检查完毕
+ let errorArr = []; // 存放错误信息
+ let errorObjArr = []; // 存放错误信息对象
+ // 调用每一个u-form-item实例的validation的校验方法
+ field.validation('', (errorMsg, errObj) => {
+ // 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
+ if (errorMsg) {
+ valid = false;
+ errorArr.push(errorMsg);
+ errorObjArr.push(errObj)
+ // 当历遍了所有的u-form-item时,调用promise的then方法
+ if (++count === this.fields.length) {
+ resolve(valid, errorObjArr[0]); // 进入promise的then方法
+ // 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
+ if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
+ this.$u.toast(errorArr[0]);
+ if (typeof callback == 'function') callback(valid, errorObjArr[0]);
@@ -0,0 +1,52 @@
+ <u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
+ <view class="u-update-content">
+ <rich-text :nodes="content"></rich-text>
+ </u-modal>
+ show: false,
+ content: `
+ 1. 修复badge组件的size参数无效问题<br>
+ 2. 新增Modal模态框组件<br>
+ 3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
+ 4. 修复键盘组件在微信小程序上遮罩无效的问题
+ `,
+ onReady() {
+ this.show = true;
+ cancel() {
+ this.closeModal();
+ confirm() {
+ closeModal() {
+ uni.navigateBack();
+ .u-full-content {
+ background-color: #00C777;
+ .u-update-content {
+ line-height: 1.7;
+ padding: 30rpx;
@@ -0,0 +1,54 @@
+ <view class="u-gap" :style="[gapStyle]"></view>
+ * gap 间隔槽
+ * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
+ * @tutorial https://www.uviewui.com/components/gap.html
+ * @property {String} bg-color 背景颜色(默认#f3f4f6)
+ * @property {String Number} height 分割槽高度,单位rpx(默认30)
+ * @example <u-gap height="80" bg-color="#bbb"></u-gap>
+ name: "u-gap",
+ default: 'transparent ' // 背景透明
+ // 高度
+ // 与上一个组件的距离
+ // 与下一个组件的距离
+ gapStyle() {
+ backgroundColor: this.bgColor,
+ height: this.height + 'rpx',
+ marginTop: this.marginTop + 'rpx',
+ marginBottom: this.marginBottom + 'rpx'
@@ -0,0 +1,127 @@
+ <view class="u-grid-item" :hover-class="parentData.hoverClass"
+ :hover-stay-time="200" @tap="click" :style="{
+ background: bgColor,
+ width: width,
+ <view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
+ * gridItem 提示
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
+ * @tutorial https://www.uviewui.com/components/grid.html
+ * @property {String} bg-color 宫格的背景颜色(默认#ffffff)
+ * @property {String Number} index 点击宫格时,返回的值
+ * @property {Object} custom-style 自定义样式,对象形式
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid-item></u-grid-item>
+ name: "u-grid-item",
+ // 点击时返回的index
+ padding: '30rpx 0'
+ hoverClass: '', // 按下去的时候,是否显示背景灰色
+ col: 3, // 父组件划分的宫格数
+ border: true, // 是否显示边框,根据父组件决定
+ this.updateParentData();
+ // this.parent在updateParentData()中定义
+ this.parent.children.push(this);
+ // 每个grid-item的宽度
+ width() {
+ return 100 / Number(this.parentData.col) + '%';
+ // 获取父组件的参数
+ updateParentData() {
+ // 此方法写在mixin中
+ this.getParentData('u-grid');
+ this.parent && this.parent.click(this.index);
+ .u-grid-item {
+ /* #ifdef MP */
+ .u-grid-item-hover {
+ background: #f7f7f7 !important;
+ .u-grid-marker-box {
+ .u-grid-marker-wrap {
+ .u-grid-item-box {
+ padding: 30rpx 0;
@@ -0,0 +1,109 @@
+ <view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
+ * grid 宫格布局
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
+ * @property {String Number} col 宫格的列数(默认3)
+ * @property {Boolean} border 是否显示宫格的边框(默认true)
+ * @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
+ * @example <u-grid :col="3" @click="click"></u-grid>
+ name: 'u-grid',
+ // 分成几列
+ col: {
+ default: 3
+ // 是否显示边框
+ // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
+ // 宫格按压时的样式类,"none"为无效果
+ // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+ parentData() {
+ if(this.children.length) {
+ // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.updateParentData) == 'function' && child.updateParentData();
+ // 计算父组件的值是否发生变化
+ return [this.hoverClass, this.col, this.size, this.border];
+ // 宫格对齐方式
+ gridStyle() {
+ switch(this.align) {
+ case 'left':
+ style.justifyContent = 'flex-start';
+ break;
+ case 'center':
+ style.justifyContent = 'center';
+ case 'right':
+ style.justifyContent = 'flex-end';
+ default: style.justifyContent = 'flex-start';
+.u-grid {
@@ -0,0 +1,369 @@
+ <view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
+ <image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
+ class="u-icon__icon"
+ :class="customClass"
+ :style="[iconStyle]"
+ @touchstart="touchstart"
+ v-if="showDecimalIcon"
+ :style="[decimalIconStyle]"
+ :class="decimalIconClass"
+ class="u-icon__decimal"
+ ></text>
+ <!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示,微信小程序不传值默认为null,故需要增加null的判断 -->
+ v-if="label !== '' && label !== null"
+ class="u-icon__label"
+ color: labelColor,
+ fontSize: $u.addUnit(labelSize),
+ marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
+ marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
+ marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
+ marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0
+ {{ label }}
+ * icon 图标
+ * @description 基于字体的图标集,包含了大多数常见场景的图标。
+ * @tutorial https://www.uviewui.com/components/icon.html
+ * @property {String} name 图标名称,见示例图标集
+ * @property {String} color 图标颜色(默认inherit)
+ * @property {String | Number} size 图标字体大小,单位rpx(默认32)
+ * @property {String | Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String} label 图标右侧的label文字(默认28)
+ * @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} label-color label字体颜色(默认#606266)
+ * @property {Object} custom-style icon的样式,对象形式
+ * @property {String} custom-prefix 自定义字体图标库时,需要写上此值
+ * @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
+ * @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
+ * @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
+ * @property {String} width 显示图片小图标时的宽度
+ * @property {String} height 显示图片小图标时的高度
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {Boolean} show-decimal-icon 是否为DecimalIcon
+ * @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
+ * @property {String | Number} percent 显示的百分比,仅Decimal时有效
+ * @event {Function} click 点击图标时触发
+ * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
+ name: "u-icon",
+ emits: ["click", "touchstart"],
+ // 图标类名
+ // 图标颜色,可接受主题色
+ default: "inherit"
+ // 是否显示粗体
+ // 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
+ // 触摸图标时的类名
+ // 自定义扩展前缀,方便用户扩展自己的图标库
+ customPrefix: {
+ default: "uicon"
+ // 图标右边或者下面的文字
+ // label的位置,只能右边或者下边
+ labelPos: {
+ default: "right"
+ // label的大小
+ default: "28"
+ // label的颜色
+ labelColor: {
+ default: "#606266"
+ // label与图标的距离(横向排列)
+ marginLeft: {
+ default: "6"
+ // label与图标的距离(竖向排列)
+ marginRight: {
+ // 图片的mode
+ imgMode: {
+ default: "widthFix"
+ // 自定义样式
+ // 用于显示图片小图标时,图片的宽度
+ // 用于显示图片小图标时,图片的高度
+ // 用于解决某些情况下,让图标垂直居中的用途
+ // 是否为DecimalIcon
+ showDecimalIcon: {
+ // 背景颜色,可接受主题色,仅Decimal时有效
+ default: "#ececec"
+ // 显示的百分比,仅Decimal时有效
+ default: "50"
+ customClass() {
+ let { customPrefix, name } = this;
+ let index = name.indexOf("-icon-");
+ customPrefix = name.substring(0, index + 5);
+ classes.push(name);
+ classes.push(`${customPrefix}-${name}`);
+ // uView的自定义图标类名为u-iconfont
+ if (customPrefix === "uicon") {
+ classes.push("u-iconfont");
+ classes.push(customPrefix);
+ // 主题色,通过类配置
+ if (
+ this.showDecimalIcon &&
+ this.inactiveColor &&
+ this.$u.config.type.includes(this.inactiveColor)
+ ) {
+ classes.push("u-icon__icon--" + this.inactiveColor);
+ } else if (this.color && this.$u.config.type.includes(this.color))
+ classes.push("u-icon__icon--" + this.color);
+ // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+ // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+ //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+ classes = classes.join(" ");
+ return classes;
+ style = {
+ fontSize: this.size == "inherit" ? "inherit" : this.$u.addUnit(this.size),
+ fontWeight: this.bold ? "bold" : "normal",
+ // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+ top: this.$u.addUnit(this.top)
+ // 非主题色值时,才当作颜色值
+ !this.$u.config.type.includes(this.inactiveColor)
+ style.color = this.inactiveColor;
+ } else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
+ // 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
+ isImg() {
+ return this.name.indexOf("/") !== -1;
+ imgStyle() {
+ // 如果设置width和height属性,则优先使用,否则使用size属性
+ style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size);
+ style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size);
+ decimalIconStyle() {
+ top: this.$u.addUnit(this.top),
+ width: this.percent + "%"
+ if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
+ decimalIconClass() {
+ classes.push(this.customPrefix + "-" + this.name);
+ if (this.customPrefix == "uicon") {
+ classes.push(this.customPrefix);
+ if (this.color && this.$u.config.type.includes(this.color))
+ else classes.push("u-icon__icon--primary");
+ this.$emit("click", this.index);
+ touchstart() {
+ this.$emit("touchstart", this.index);
+@import "../../iconfont.css";
+.u-icon {
+ flex-direction: row-reverse;
+ &--top {
+ flex-direction: column-reverse;
+ &--bottom {
+ &__decimal {
+ &__img {
+ height: auto;
+ will-change: transform;
@@ -0,0 +1,283 @@
+ <view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
+ v-if="!isError"
+ :src="src"
+ :mode="mode"
+ @error="onErrorHandler"
+ @load="onLoadHandler"
+ :lazy-load="lazyLoad"
+ class="u-image__image"
+ :show-menu-by-longpress="showMenuByLongpress"
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
+ v-if="showLoading && loading"
+ class="u-image__loading"
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
+ <slot v-if="$slots.loading" name="loading" />
+ <u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
+ v-if="showError && isError && !loading"
+ class="u-image__error"
+ <slot v-if="$slots.error" name="error" />
+ <u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
+ * Image 图片
+ * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
+ * @tutorial https://uviewui.com/components/image.html
+ * @property {String} src 图片地址
+ * @property {String} mode 裁剪模式,见官网说明
+ * @value scaleToFill 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
+ * @value aspectFit 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
+ * @value aspectFill 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
+ * @value widthFix 宽度不变,高度自动变化,保持原图宽高比不变
+ * @value heightFix 高度不变,宽度自动变化,保持原图宽高比不变 App 和 H5 平台 HBuilderX 2.9.3+ 支持、微信小程序需要基础库 2.10.3
+ * @value top 不缩放图片,只显示图片的顶部区域
+ * @value bottom 不缩放图片,只显示图片的底部区域
+ * @value center 不缩放图片,只显示图片的中间区域
+ * @value left 不缩放图片,只显示图片的左边区域
+ * @value right 不缩放图片,只显示图片的右边区域
+ * @value top left 不缩放图片,只显示图片的左上边区域
+ * @value top right 不缩放图片,只显示图片的右上边区域
+ * @value bottom left 不缩放图片,只显示图片的左下边区域
+ * @value bottom right 不缩放图片,只显示图片的右下边区域
+ * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
+ * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
+ * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
+ * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
+ * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
+ * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
+ * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
+ * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
+ * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
+ * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
+ * @property {Boolean} fade 是否需要淡入效果(默认 true)
+ * @property {String Number} width 传入图片路径时图片的宽度
+ * @property {String Number} height 传入图片路径时图片的高度
+ * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
+ * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
+ * @event {Function} click 点击图片时触发
+ * @event {Function} error 图片加载失败时触发
+ * @event {Function} load 图片加载成功时触发
+ * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
+ name: 'u-image',
+ emits: ["click", "error", "load"],
+ // 图片地址
+ // 裁剪模式
+ default: 'aspectFill'
+ // 宽度,单位任意
+ default: '100%'
+ // 高度,单位任意
+ // 图片形状,circle-圆形,square-方形
+ default: 'square'
+ // 圆角,单位任意
+ // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
+ lazyLoad: {
+ // 开启长按图片显示识别微信小程序码菜单
+ showMenuByLongpress: {
+ // 加载中的图标,或者小图片
+ loadingIcon: {
+ default: 'photo'
+ // 加载失败的图标,或者小图片
+ errorIcon: {
+ default: 'error-circle'
+ // 是否显示加载中的图标或者自定义的slot
+ showLoading: {
+ // 是否显示加载错误的图标或者自定义的slot
+ showError: {
+ // 是否需要淡入效果
+ fade: {
+ // 只支持网络资源,只对微信小程序有效
+ webp: {
+ // 过渡时间,单位ms
+ // 背景颜色,用于深色页面加载图片时,为了和背景色融合
+ default: '#f3f4f6'
+ // 图片是否加载错误,如果是,则显示错误占位图
+ isError: false,
+ // 初始化组件时,默认为加载中状态
+ loading: true,
+ // 不透明度,为了实现淡入淡出的效果
+ opacity: 1,
+ // 过渡时间,因为props的值无法修改,故需要一个中间值
+ durationTime: this.duration,
+ // 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
+ backgroundStyle: {}
+ handler (n) {
+ if(!n) {
+ // 如果传入null或者'',或者false,或者undefined,标记为错误状态
+ this.isError = true;
+ this.loading = false;
+ this.isError = false;
+ this.loading = true;
+ wrapStyle() {
+ // 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
+ style.width = this.$u.addUnit(this.width);
+ style.height = this.$u.addUnit(this.height);
+ // 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
+ style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
+ // 如果设置圆角,必须要有hidden,否则可能圆角无效
+ style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
+ if (this.fade) {
+ style.opacity = this.opacity;
+ style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
+ // 点击图片
+ onClick() {
+ // 图片加载失败
+ onErrorHandler(err) {
+ this.$emit('error', err);
+ // 图片加载完成,标记loading结束
+ onLoadHandler() {
+ this.$emit('load');
+ // 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
+ // 否则无需fade效果时,png图片依然能看到下方的背景色
+ if (!this.fade) return this.removeBgColor();
+ // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
+ // 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
+ // 到图片展示的过程中的淡入效果
+ this.durationTime = 0;
+ // 延时50ms,否则在浏览器H5,过渡效果无效
+ this.durationTime = this.duration;
+ this.removeBgColor();
+ }, this.durationTime);
+ // 移除图片的背景色
+ removeBgColor() {
+ // 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
+ this.backgroundStyle = {
+ backgroundColor: 'transparent'
+.u-image {
+ transition: opacity 0.5s ease-in-out;
+ &__image {
+ &__loading,
+ &__error {
+ background-color: $u-bg-color;
+ font-size: 46rpx;
@@ -0,0 +1,89 @@
+ <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+ <view>
+ <view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
+ <view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
+ <slot v-if="useSlot" />
+ <block v-else>
+ <text>{{ index }}</text>
+ * indexAnchor 索引列表锚点
+ * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+ * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+ * @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
+ * @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
+ * @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
+ * @event {Function} default 锚点位置显示内容,默认为索引字符
+ * @example <u-index-anchor :index="item" />
+ name: "u-index-anchor",
+ active: false,
+ wrapperStyle: {},
+ anchorStyle: {}
+ this.parent = this.$u.$parent.call(this, 'u-index-list');
+ this.parent.updateData();
+ customAnchorStyle() {
+ return Object.assign(this.anchorStyle, this.customStyle);
+ .u-index-anchor {
+ padding: 14rpx 24rpx;
+ color: #606266;
+ line-height: 1.2;
+ background-color: rgb(245, 245, 245);
+ .u-index-anchor--active {
+ color: #2979ff;
@@ -0,0 +1,315 @@
+ <view class="u-index-bar">
+ <view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
+ @touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
+ <view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
+ :data-index="index">
+ <view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
+ zIndex: alertZIndex
+ <text>{{indexList[touchmoveIndex]}}</text>
+ var indexList = function() {
+ var indexList = [];
+ var charCodeOfA = 'A'.charCodeAt(0);
+ for (var i = 0; i < 26; i++) {
+ indexList.push(String.fromCharCode(charCodeOfA + i));
+ return indexList;
+ * indexList 索引列表
+ * @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
+ * @property {Array} index-list 索引字符列表,数组(默认A-Z)
+ * @property {Number String} z-index 锚点吸顶时的层级(默认965)
+ * @property {Boolean} sticky 是否开启锚点自动吸顶(默认true)
+ * @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0)
+ * @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff)
+ * @event {Function} select 选中右边索引字符时触发
+ * @example <u-index-list :scrollTop="scrollTop"></u-index-list>
+ name: "u-index-list",
+ sticky: {
+ offsetTop: {
+ indexList: {
+ return indexList()
+ // #ifdef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
+ // #ifndef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
+ // 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错
+ activeAnchorIndex: 0,
+ showSidebar: true,
+ // children: [],
+ touchmove: false,
+ touchmoveIndex: 0,
+ scrollTop() {
+ this.updateData()
+ // 弹出toast的z-index值
+ alertZIndex() {
+ return this.$u.zIndex.toast;
+ updateData() {
+ this.timer && clearTimeout(this.timer);
+ this.showSidebar = !!this.children.length;
+ this.setRect().then(() => {
+ this.onScroll();
+ }, 0);
+ setRect() {
+ return Promise.all([
+ this.setAnchorsRect(),
+ this.setListRect(),
+ this.setSiderbarRect()
+ ]);
+ setAnchorsRect() {
+ return Promise.all(this.children.map((anchor, index) => anchor
+ .$uGetRect('.u-index-anchor-wrapper')
+ .then((rect) => {
+ Object.assign(anchor, {
+ height: rect.height,
+ top: rect.top
+ })));
+ setListRect() {
+ return this.$uGetRect('.u-index-bar').then((rect) => {
+ Object.assign(this, {
+ top: rect.top + this.scrollTop
+ setSiderbarRect() {
+ return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
+ this.sidebar = {
+ getActiveAnchorIndex() {
+ const {
+ children
+ } = this;
+ sticky
+ for (let i = this.children.length - 1; i >= 0; i--) {
+ const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
+ const reachTop = sticky ? preAnchorHeight : 0;
+ if (reachTop >= children[i].top) {
+ return i;
+ return -1;
+ onScroll() {
+ children = []
+ if (!children.length) {
+ sticky,
+ stickyOffsetTop,
+ zIndex,
+ scrollTop,
+ activeColor
+ const active = this.getActiveAnchorIndex();
+ this.activeAnchorIndex = active;
+ if (sticky) {
+ let isActiveAnchorSticky = false;
+ if (active !== -1) {
+ isActiveAnchorSticky =
+ children[active].top <= 0;
+ children.forEach((item, index) => {
+ if (index === active) {
+ let wrapperStyle = '';
+ let anchorStyle = {
+ color: `${activeColor}`
+ if (isActiveAnchorSticky) {
+ wrapperStyle = {
+ height: `${children[index].height}px`
+ anchorStyle = {
+ top: `${stickyOffsetTop}px`,
+ zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+ item.active = active;
+ item.wrapperStyle = wrapperStyle;
+ item.anchorStyle = anchorStyle;
+ } else if (index === active - 1) {
+ const currentAnchor = children[index];
+ const currentOffsetTop = currentAnchor.top;
+ const targetOffsetTop = index === children.length - 1 ?
+ this.top :
+ children[index + 1].top;
+ const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
+ const translateY = parentOffsetHeight - currentAnchor.height;
+ const anchorStyle = {
+ position: 'relative',
+ transform: `translate3d(0, ${translateY}px, 0)`,
+ item.active = false;
+ item.anchorStyle = '';
+ item.wrapperStyle = '';
+ onTouchMove(event) {
+ this.touchmove = true;
+ const sidebarLength = this.children.length;
+ const touch = event.touches[0];
+ const itemHeight = this.sidebar.height / sidebarLength;
+ let clientY = 0;
+ clientY = touch.clientY;
+ let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
+ if (index < 0) {
+ index = 0;
+ } else if (index > sidebarLength - 1) {
+ index = sidebarLength - 1;
+ this.touchmoveIndex = index;
+ this.scrollToAnchor(index);
+ onTouchStop() {
+ this.touchmove = false;
+ this.scrollToAnchorIndex = null;
+ scrollToAnchor(index) {
+ if (this.scrollToAnchorIndex === index) {
+ this.scrollToAnchorIndex = index;
+ const anchor = this.children.find((item) => item.index === this.indexList[index]);
+ if (anchor) {
+ this.$emit('select', anchor.index);
+ duration: 0,
+ scrollTop: anchor.top + this.scrollTop
+ .u-index-bar {
+ position: relative
+ .u-index-bar__sidebar {
+ transform: translateY(-50%);
+ .u-index-bar__index {
+ padding: 8rpx 18rpx;
+ line-height: 1
+ .u-indexed-list-alert {
+ width: 120rpx;
+ height: 120rpx;
+ right: 90rpx;
+ margin-top: -60rpx;
+ border-radius: 24rpx;
+ font-size: 50rpx;
+ background-color: rgba(0, 0, 0, 0.65);
+ padding: 0;
+ z-index: 9999999;
+ .u-indexed-list-alert text {
@@ -0,0 +1,465 @@
+ class="u-input"
+ 'u-input--border': border,
+ 'u-input--error': validateState
+ padding: padding ? padding : `0 ${border ? 20 : 0}rpx`,
+ borderColor: borderColor,
+ textAlign: inputAlignCom,
+ backgroundColor: backgroundColor,
+ @tap.stop="inputClick"
+ <textarea
+ v-if="type == 'textarea'"
+ class="u-input__input u-input__textarea"
+ :style="[getStyle]"
+ :value="defaultValue"
+ :fixed="fixed"
+ :autoHeight="autoHeight"
+ :selection-end="uSelectionEnd"
+ :selection-start="uSelectionStart"
+ :cursor-spacing="getCursorSpacing"
+ :show-confirm-bar="showConfirmbar"
+ :adjust-position="adjustPosition"
+ @input="handleInput"
+ @blur="handleBlur"
+ class="u-input__input"
+ :type="type == 'password' ? 'text' : type"
+ :password="type == 'password' && !showPassword"
+ :disabled="disabled || type === 'select'"
+ <view class="u-input__right-icon u-flex">
+ class="u-input__right-icon__clear u-input__right-icon__item"
+ @tap="onClear"
+ v-if="clearableCom && valueCom != '' && focused"
+ <u-icon size="32" name="close-circle-fill" color="#c0c4cc" />
+ v-if="passwordIcon && type == 'password'"
+ :name="!showPassword ? 'eye' : 'eye-fill'"
+ color="#c0c4cc"
+ @click="showPassword = !showPassword"
+ class="u-input__right-icon--select u-input__right-icon__item"
+ v-if="type == 'select'"
+ 'u-input__right-icon--select--reverse': selectOpen
+ <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
+ * input 输入框
+ * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
+ * @tutorial http://uviewui.com/components/input.html
+ * @property {String} type 模式选择,见官网说明
+ * @value text 文本输入键盘
+ * @value number 数字输入键盘
+ * @value idcard 身份证输入键盘
+ * @value digit 带小数点的数字键盘
+ * @value password 密码输入键盘
+ * @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
+ * @property {} v-model 用于双向绑定输入框的值
+ * @property {String} input-align 输入框文字的对齐方式(默认left)
+ * @property {String} placeholder placeholder显示值(默认 '请输入内容')
+ * @property {Boolean} disabled 是否禁用输入框(默认false)
+ * @property {String Number} maxlength 输入框的最大可输入长度(默认140)
+ * @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1)
+ * @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1)
+ * @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0)
+ * @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;")
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done)
+ * @property {Object} custom-style 自定义输入框的样式,对象形式
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true)
+ * @property {Boolean} border 是否显示边框(默认false)
+ * @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100)
+ * @example <u-input v-model="value" :type="type" :border="border" />
+ name: "u-input",
+ emits: ["update:modelValue", "input", "change", "blur", "focus", "click", "touchstart"],
+ // 输入框的类型,textarea,text,number
+ placeholder: {
+ default: "请输入内容"
+ placeholderStyle: {
+ default: "color: #c0c4cc;"
+ default: "done"
+ // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
+ fixed: {
+ // 是否自动获得焦点
+ focus: {
+ // 密码类型时,是否显示右侧的密码图标
+ passwordIcon: {
+ // input|textarea是否显示边框
+ // 输入框的边框颜色
+ default: "#dcdfe6"
+ // type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态
+ // open-打开,close-关闭
+ selectOpen: {
+ // 高度,单位rpx
+ // 是否可清空
+ // 指定光标与键盘的距离,单位 px
+ cursorSpacing: {
+ // 光标起始位置,自动聚焦时有效,需与selection-end搭配使用
+ selectionStart: {
+ default: -1
+ // 光标结束位置,自动聚焦时有效,需与selection-start搭配使用
+ selectionEnd: {
+ // 是否显示键盘上方带有”完成“按钮那一栏
+ showConfirmbar: {
+ // 弹出键盘时是否自动调节高度,uni-app默认值是true
+ adjustPosition: {
+ // input的背景色
+ backgroundColor: {
+ // input的padding
+ defaultValue: "",
+ inputHeight: 70, // input的高度
+ textareaHeight: 100, // textarea的高度
+ validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色
+ focused: false, // 当前是否处于获得焦点的状态
+ showPassword: false, // 是否预览密码
+ lastValue: "" ,// 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间
+ uForm:{
+ inputAlign: "",
+ clearable: ""
+ valueCom(nVal, oVal) {
+ this.defaultValue = nVal;
+ // 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
+ if (nVal != oVal && this.type == "select")
+ this.handleInput({
+ detail: {
+ value: nVal
+ inputAlignCom(){
+ return this.inputAlign || this.uForm.inputAlign || "left";
+ clearableCom(){
+ if (typeof this.clearable == "boolean") return this.clearable;
+ if (typeof this.uForm.clearable == "boolean") return this.uForm.clearable;
+ return Number(this.maxlength);
+ getStyle() {
+ // 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度
+ style.minHeight = this.height
+ ? this.height + "rpx"
+ : this.type == "textarea"
+ ? this.textareaHeight + "rpx"
+ : this.inputHeight + "rpx";
+ style = Object.assign(style, this.customStyle);
+ //
+ getCursorSpacing() {
+ return Number(this.cursorSpacing);
+ // 光标起始位置
+ uSelectionStart() {
+ return String(this.selectionStart);
+ // 光标结束位置
+ uSelectionEnd() {
+ return String(this.selectionEnd);
+ // 监听u-form-item发出的错误事件,将输入框边框变红色
+ this.$on("onFormItemError", this.onFormItemError);
+ this.defaultValue = this.valueCom;
+ let parent = this.$u.$parent.call(this, 'u-form');
+ Object.keys(this.uForm).map(key => {
+ this.uForm[key] = parent[key];
+ * change 事件
+ * @param event
+ handleInput(event) {
+ if (this.trim) value = this.$u.trim(value);
+ // vue 原生的方法 return 出去
+ // 当前model 赋值
+ this.defaultValue = value;
+ // 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
+ // 尚未更新到u-form-item,导致获取的值为空,从而校验混论
+ // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+ // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+ // #ifdef MP-TOUTIAO
+ if (this.$u.trim(value) == this.lastValue) return;
+ this.lastValue = value;
+ this.dispatch("u-form-item", "onFieldChange", value);
+ }, 40);
+ * blur 事件
+ handleBlur(event) {
+ }, 100);
+ this.$emit("blur", event.detail.value);
+ this.dispatch("u-form-item", "onFieldBlur", event.detail.value);
+ onFormItemError(status) {
+ this.validateState = status;
+ this.$emit("focus");
+ this.$emit("confirm", e.detail.value);
+ this.$emit("input", "");
+ this.$emit("update:modelValue", "");
+ inputClick() {
+ this.$emit("click");
+.u-input {
+ &__input {
+ //height: $u-form-item-height;
+ &__textarea {
+ padding: 10rpx 0;
+ line-height: normal;
+ &--border {
+ border-radius: 4px;
+ border: 1px solid $u-form-item-border-color;
+ border-color: $u-type-error !important;
+ &__right-icon {
+ &--select {
+ transition: transform 0.4s;
+ &--reverse {
+ transform: rotate(-180deg);
@@ -0,0 +1,285 @@
+ class=""
+ :mask="mask"
+ :zIndex="uZIndex"
+ <view class="u-tooltip" v-if="tooltip">
+ class="u-tooltip-item u-tooltip-cancel"
+ hover-class="u-tooltip-cancel-hover"
+ @tap="onCancel"
+ {{ cancelBtn ? cancelText : "" }}
+ <view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
+ {{
+ tips ? tips : mode == "number" ? "数字键盘" : mode == "card" ? "身份证键盘" : "车牌号键盘"
+ }}
+ v-if="confirmBtn"
+ @tap="onConfirm"
+ class="u-tooltip-item u-tooltips-submit"
+ hover-class="u-tooltips-submit-hover"
+ {{ confirmBtn ? confirmText : "" }}
+ <block v-if="mode == 'number' || mode == 'card'">
+ <u-number-keyboard
+ :random="random"
+ @backspace="backspace"
+ @change="change"
+ :dotEnabled="dotEnabled"
+ ></u-number-keyboard>
+ <u-car-keyboard
+ ref="uCarKeyboard"
+ ></u-car-keyboard>
+ * keyboard 键盘
+ * @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。
+ * @tutorial https://www.uviewui.com/components/keyboard.html
+ * @property {String} mode 键盘类型,见官网基本使用的说明(默认number)
+ * @property {Boolean} dot-enabled 是否显示"."按键,只在mode=number时有效(默认true)
+ * @property {Boolean} tooltip 是否显示键盘顶部工具条(默认true)
+ * @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
+ * @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮(默认true)
+ * @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮(默认true)
+ * @property {Boolean} mask 是否显示遮罩(默认true)
+ * @property {String} confirm-text 确认按钮的文字
+ * @property {String} cancel-text 取消按钮的文字
+ * @property {Number String} z-index 弹出键盘的z-index值(默认1075)
+ * @property {Boolean} random 是否打乱键盘按键的顺序(默认false)
+ * @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘(默认true)
+ * @event {Function} change 按键被点击(不包含退格键被点击)
+ * @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击
+ * @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击
+ * @event {Function} backspace 键盘退格键被点击
+ * @example <u-keyboard mode="number" v-model="show"></u-keyboard>
+ emits: ["update:modelValue", "input", "change", "cancel", "confirm", "backspace"],
+ // 通过双向绑定控制键盘的弹出与收起
+ // 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘
+ default: "number"
+ // 是否显示键盘的"."符号
+ dotEnabled: {
+ // 是否显示顶部工具条
+ tooltip: {
+ // 是否显示工具条中间的提示
+ showTips: {
+ // 工具条中间的提示文字
+ // 是否显示工具条左边的"取消"按钮
+ // 是否显示工具条右边的"完成"按钮
+ confirmBtn: {
+ // 是否允许通过点击遮罩关闭键盘
+ // 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
+ mask: {
+ // z-index值
+ // 取消按钮的文字
+ // 确认按钮的文字
+ confirmText: {
+ default: "确认"
+ valueCom:{
+ handler(v1, v2) {
+ this.$emit("change", e);
+ // 键盘关闭
+ // 通过发送input这个特殊的事件名,可以修改父组件传给props的value的变量,也即双向绑定
+ // 输入完成
+ onConfirm() {
+ this.$emit("confirm");
+ // 取消输入
+ onCancel() {
+ this.$emit("cancel");
+ // 退格键
+ backspace(count) {
+ this.$emit("backspace", count);
+ if (this.$refs.uCarKeyboard) this.$refs.uCarKeyboard.changeCarInputMode();
+ updateCarInputMode(abcKey) {
+ if (this.$refs.uCarKeyboard) this.$refs.uCarKeyboard.updateCarInputMode(abcKey);
+ // 关闭键盘
+ // close() {
+ // this.show = false;
+ // // 打开键盘
+ // open() {
+ // this.show = true;
+.u-keyboard {
+ z-index: 1003;
+.u-tooltip {
+.u-tooltip-item {
+ color: #333333;
+ flex: 0 0 33.333333%;
+ padding: 20rpx 10rpx;
+.u-tooltips-submit {
+ flex-grow: 1;
+ flex-wrap: 0;
+ padding-right: 40rpx;
+.u-tooltip-cancel {
+ padding-left: 40rpx;
+ color: #888888;
+.u-tooltips-submit-hover {
+.u-tooltip-cancel-hover {
+ <view class="u-progress" :style="{
+ borderRadius: round ? '100rpx' : 0,
+ height: height + 'rpx',
+ backgroundColor: inactiveColor
+ <view :class="[
+ type ? `u-type-${type}-bg` : '',
+ striped ? 'u-striped' : '',
+ striped && stripedActive ? 'u-striped-active' : ''
+ ]" class="u-active" :style="[progressStyle]">
+ <slot v-if="$slots.default || $slots.$default" />
+ <block v-else-if="showPercent">
+ {{percent + '%'}}
+ * lineProgress 线型进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
+ * @tutorial https://www.uviewui.com/components/lineProgress.html
+ * @property {String Number} percent 进度条百分比值,为数值类型,0-100
+ * @property {Boolean} round 进度条两端是否为半圆(默认true)
+ * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
+ * @property {String} inactive-color 进度条的底色(默认#ececec)
+ * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
+ * @property {String Number} height 进度条的高度,单位rpx(默认28)
+ * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
+ * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
+ * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
+ name: "u-line-progress",
+ // 两端是否显示半圆形
+ round: {
+ // 主题颜色
+ // 激活部分的颜色
+ // 进度百分比,数值
+ // 是否在进度条内部显示百分比的值
+ showPercent: {
+ // 进度条的高度,单位rpx
+ // 是否显示条纹
+ striped: {
+ // 条纹是否显示活动状态
+ stripedActive: {
+ progressStyle() {
+ style.width = this.percent + '%';
+ if(this.activeColor) style.backgroundColor = this.activeColor;
+ .u-progress {
+ height: 15px;
+ .u-active {
+ justify-items: flex-end;
+ transition: all 0.4s ease;
+ .u-striped {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 39px 39px;
+ .u-striped-active {
+ animation: progress-stripes 2s linear infinite;
+ @keyframes progress-stripes {
+ 0% {
+ background-position: 0 0;
+ 100% {
+ background-position: 39px 0;
@@ -0,0 +1,84 @@
+ <view class="u-line" :style="[lineStyle]">
+ * line 线条
+ * @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
+ * @tutorial https://www.uviewui.com/components/line.html
+ * @property {String} color 线条的颜色(默认#e4e7ed)
+ * @property {String} length 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+ * @property {String} direction 线条的方向,row-横向,col-竖向(默认row)
+ * @property {String} border-style 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线(默认solid)
+ * @property {Boolean} hair-line 是否显示细线条(默认true)
+ * @property {String} margin 线条与上下左右元素的间距,字符串形式,如"30rpx"
+ * @example <u-line color="red"></u-line>
+ name: 'u-line',
+ default: '#e4e7ed'
+ // 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+ length: {
+ // 线条方向,col-竖向,row-横向
+ // 是否显示细边框
+ // 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx"
+ default: '0'
+ // 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线
+ borderStyle: {
+ default: 'solid'
+ style.margin = this.margin;
+ // 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了
+ if(this.direction == 'row') {
+ // 此处采用兼容分开写,兼容nvue的写法
+ style.borderBottomWidth = '1px';
+ style.borderBottomStyle = this.borderStyle;
+ style.width = this.$u.addUnit(this.length);
+ if(this.hairLine) style.transform = 'scaleY(0.5)';
+ // 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了
+ style.borderLeftWidth = '1px';
+ style.borderLeftStyle = this.borderStyle;
+ style.height = this.$u.addUnit(this.length);
+ if(this.hairLine) style.transform = 'scaleX(0.5)';
+ style.borderColor = this.color;
+ .u-line {
+ <text class="u-link" @tap.stop="openLink" :style="{
+ borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none',
+ paddingBottom: underLine ? '0rpx' : '0'
+ * link 超链接
+ * @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。
+ * @tutorial https://www.uviewui.com/components/link.html
+ * @property {String} color 文字颜色(默认#606266)
+ * @property {String Number} font-size 字体大小,单位rpx(默认28)
+ * @property {Boolean} under-line 是否显示下划线(默认false)
+ * @property {String} href 跳转的链接,要带上http(s)
+ * @property {String} line-color 下划线颜色,默认同color参数颜色
+ * @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
+ * @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
+ name: "u-link",
+ // 是否显示下划线
+ underLine: {
+ // 要跳转的链接
+ href: {
+ // 小程序中复制到粘贴板的提示语
+ mpTips: {
+ default: '链接已复制,请在浏览器打开'
+ // 下划线颜色
+ lineColor: {
+ openLink() {
+ // #ifdef APP-PLUS
+ plus.runtime.openURL(this.href)
+ window.open(this.href)
+ data: this.href,
+ uni.hideToast();
+ this.$u.toast(this.mpTips);
+ .u-link {
+ <view class="u-loading-page">