@@ -15,6 +15,7 @@ namespace Learun.Application.WebApi | |||
: base() | |||
{ | |||
Get["/"] = MainIndex; | |||
Get["/index.html"] = MainIndex | |||
Get["/bgimg"] = BgImg; | |||
} | |||
/// <summary> | |||
@@ -0,0 +1,75 @@ | |||
// 本文件是 ESLint 的配置文件 | |||
// ESLint 用于在您二次开发时,每次保存文件后,对代码进行校验,并对可能出错的地方给予提示 | |||
// 一般来说不需要修改此文件,除非您有额外的需求 | |||
//更详细的配置文档请参考:https://github.com/vuejs/eslint-plugin-vue#gear-configs | |||
module.exports = { | |||
"extends": "plugin:vue/base", | |||
parserOptions: { | |||
ecmaVersion: 2017, | |||
sourceType: 'module' | |||
}, | |||
"rules": { | |||
//在computed properties中禁用异步actions | |||
'vue/no-async-in-computed-properties': 'error', | |||
//不允许重复的keys | |||
'vue/no-dupe-keys': 'error', | |||
//不允许重复的attributes | |||
'vue/no-duplicate-attributes': 'warn', | |||
//在 <template> 标签下不允许解析错误 | |||
'vue/no-parsing-error': ['error', { | |||
'x-invalid-end-tag': false, | |||
}], | |||
//不允许覆盖保留关键字 | |||
'vue/no-reserved-keys': 'error', | |||
//强制data必须是一个带返回值的函数 | |||
// 'vue/no-shared-component-data': 'error', | |||
//不允许在computed properties中出现副作用。 | |||
'vue/no-side-effects-in-computed-properties': 'error', | |||
//<template>不允许key属性 | |||
'vue/no-template-key': 'warn', | |||
//在 <textarea> 中不允许mustaches | |||
'vue/no-textarea-mustache': 'error', | |||
//不允许在v-for或者范围内的属性出现未使用的变量定义 | |||
'vue/no-unused-vars': 'warn', | |||
//<component>标签需要v-bind:is属性 | |||
'vue/require-component-is': 'error', | |||
// render 函数必须有一个返回值 | |||
'vue/require-render-return': 'error', | |||
//保证 v-bind:key 和 v-for 指令成对出现 | |||
'vue/require-v-for-key': 'error', | |||
// 检查默认的prop值是否有效 | |||
'vue/require-valid-default-prop': 'error', | |||
// 保证computed属性中有return语句 | |||
'vue/return-in-computed-property': 'error', | |||
// 强制校验 template 根节点 | |||
'vue/valid-template-root': 'error', | |||
// 强制校验 v-bind 指令 | |||
'vue/valid-v-bind': 'error', | |||
// 强制校验 v-cloak 指令 | |||
'vue/valid-v-cloak': 'error', | |||
// 强制校验 v-else-if 指令 | |||
'vue/valid-v-else-if': 'error', | |||
// 强制校验 v-else 指令 | |||
'vue/valid-v-else': 'error', | |||
// 强制校验 v-for 指令 | |||
'vue/valid-v-for': 'error', | |||
// 强制校验 v-html 指令 | |||
'vue/valid-v-html': 'error', | |||
// 强制校验 v-if 指令 | |||
'vue/valid-v-if': 'error', | |||
// 强制校验 v-model 指令 | |||
'vue/valid-v-model': 'error', | |||
// 强制校验 v-on 指令 | |||
'vue/valid-v-on': 'error', | |||
// 强制校验 v-once 指令 | |||
'vue/valid-v-once': 'error', | |||
// 强制校验 v-pre 指令 | |||
'vue/valid-v-pre': 'error', | |||
// 强制校验 v-show 指令 | |||
'vue/valid-v-show': 'error', | |||
// 强制校验 v-text 指令 | |||
'vue/valid-v-text': 'error', | |||
'vue/comment-directive': 0 | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
# Created by https://www.gitignore.io/api/node | |||
# Edit at https://www.gitignore.io/?templates=node | |||
### Node ### | |||
# Logs | |||
logs | |||
*.log | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
lerna-debug.log* | |||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||
# Runtime data | |||
pids | |||
*.pid | |||
*.seed | |||
*.pid.lock | |||
# Directory for instrumented libs generated by jscoverage/JSCover | |||
lib-cov | |||
# Coverage directory used by tools like istanbul | |||
coverage | |||
*.lcov | |||
# nyc test coverage | |||
.nyc_output | |||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||
.grunt | |||
# Bower dependency directory (https://bower.io/) | |||
bower_components | |||
# node-waf configuration | |||
.lock-wscript | |||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||
build/Release | |||
# Dependency directories | |||
# node_modules/ | |||
# jspm_packages/ | |||
# TypeScript v1 declaration files | |||
typings/ | |||
# TypeScript cache | |||
*.tsbuildinfo | |||
# Optional npm cache directory | |||
.npm | |||
# Optional eslint cache | |||
.eslintcache | |||
# Optional REPL history | |||
.node_repl_history | |||
# Output of 'npm pack' | |||
*.tgz | |||
# Yarn Integrity file | |||
.yarn-integrity | |||
# dotenv environment variables file | |||
.env | |||
.env.test | |||
# parcel-bundler cache (https://parceljs.org/) | |||
.cache | |||
# next.js build output | |||
.next | |||
# nuxt.js build output | |||
.nuxt | |||
# vuepress build output | |||
.vuepress/dist | |||
# Serverless directories | |||
.serverless/ | |||
# FuseBox cache | |||
.fusebox/ | |||
# DynamoDB Local files | |||
.dynamodb/ | |||
# End of https://www.gitignore.io/api/node | |||
unpackage |
@@ -0,0 +1,49 @@ | |||
<script> | |||
export default { | |||
// 小程序:onLaunch 仅启动时调用一次 | |||
// H5/App:onLaunch 打开网页/用户点刷新/代码热更新时均会调用; | |||
// 考虑到用户刷新网页时会丢失全局数据、页面栈、页面数据等,因此直接跳回首页即可 | |||
async onLaunch(param) { | |||
// #ifdef H5 || APP-VUE | |||
// H5 刷新时获取当前页面路径 | |||
const pagePath = '/' + param.path | |||
// 如果 H5 刷新后访问的不是首页/登录页/注册页,直接跳转回首页 | |||
if (!['/pages/login', '/pages/home', '/pages/signup'].includes(pagePath)) { | |||
this.$nextTick(() => { | |||
this.TAB_TO('/pages/home') | |||
return | |||
}) | |||
} | |||
// #endif | |||
// #ifdef MP-WEIXIN | |||
// 小程序端,处理更新 (支付宝/钉钉暂不支持) | |||
const updateManager = uni.getUpdateManager() | |||
updateManager.onUpdateReady(() => { | |||
this.HIDE_LOADING() | |||
uni.showModal({ | |||
title: '更新提示', | |||
content: '小程序新版本已准备好,是否更新应用?', | |||
success: ({ confirm }) => { | |||
if (confirm) { | |||
updateManager.applyUpdate() | |||
} | |||
} | |||
}) | |||
}) | |||
// #endif | |||
} | |||
} | |||
</script> | |||
<style lang="less"> | |||
// 现在 CSS 必须在 <style> 中引入,参考:https://ask.dcloud.net.cn/question/86907 | |||
// 后续 uni-app 升级后可能会支持直接 import 样式,到时候可以省略下行代码 | |||
@import '~@/components/learun-mpui/styles/index.css'; | |||
page { | |||
background-color: #f3f3f3; | |||
} | |||
</style> |
@@ -0,0 +1,170 @@ | |||
# 力软框架移动端 · 二次开发手册 | |||
修订日期:2020年 7月 20日 | |||
App 版本号:2.2.0 | |||
HBuilderX 版本不低于:2.7.9 | |||
(本文档只是对初次使用框架做简单的介绍,对于框架 API、组件、平台差异等,您可查看 **/docs** 目录下的详细说明文档) | |||
(本文档格式为 markdown,您可以使用浏览器等工具来打开,获取更佳的阅读体验) | |||
# 文档目录 | |||
- **/README.md** : 二次开发手册(本文档); | |||
- **/docs/api.md** : 框架 API 文档; | |||
- **/docs/platform.md**: 各个平台差异介绍; | |||
- **/docs/component.md**: 移动端组件文档。 | |||
# 开发前的准备 | |||
力软框架移动端使用 uni-app 技术来开发。 | |||
使用 uni-app,您只需编写一套 Vue 代码,就能编译输出成手机 App、各小程序平台、H5 网页端等多个平台的成品。 | |||
力软框架移动端支持 H5、安卓 App/iOS App、微信/支付宝/钉钉小程序。 | |||
需要下载安装以下工具: | |||
- 【必装工具】:[HBuilderX](https://www.dcloud.io/hbuilderx.html) ,uni-app 的官方开发工具,**注意请下载App开发版** | |||
- 开发微信小程序:[微信小程序开发工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) | |||
- 开发支付宝/钉钉小程序:[支付宝小程序开发工具](https://opendocs.alipay.com/mini/ide/download) | |||
- 开发 iOS /安卓 App / H5:HBuilderX 内置网页版调试工具,也可以连接手机真机调试,无需安装其他软件。 | |||
**注意:**请使用最新版 HBuilderX。如果您已安装,可以点击菜单栏的「帮助」>「检查更新」来升级到最新版。请确保您的 HBuilderX 版本不低于本文档开头所标注的HBuilderX 开发工具版本。 | |||
HBuilderX 下载后是个压缩包,将它解压到硬盘上,运行 HBuilderX.exe 即可启动; | |||
小程序开发工具则按照指引,依次点击下一步即可完成安装。 | |||
**本文档只负责提示您开发过程中易错点,以及对力软框架提供的 API、工具类等给出说明,不是使用 uni-app 开发移动应用的教程。** | |||
学习 uni-app 移动端开发,或是在以上工具的安装、使用过程中遇到问题时,可以参考以下文档: | |||
- [uni-app 介绍与文档](https://uniapp.dcloud.io/) | |||
- [微信小程序官方开发指南](https://developers.weixin.qq.com/miniprogram/dev/framework/) | |||
- [支付宝小程序官方开发指南](https://opendocs.alipay.com/mini/developer) | |||
- [钉钉小程序官方开发指南](https://ding-doc.dingtalk.com/doc#/dev/) | |||
- [Vue.js 官网文档](https://cn.vuejs.org/index.html) | |||
# 起步 | |||
启动 HBuilderX,点击左上角菜单:「文件」>「打开目录」,然后选择本项目的文件夹,即可打开本项目。 | |||
安装后**首次**运行 HBuilderX 时,需要的操作: | |||
- 【必做】 点击左上角菜单>「工具」>「插件安装」,在打开的插件列表中安装「less 编译」; | |||
- 【必做】 找到 /config.js 文件,修改公司名称、后台接口地址等相关配置; | |||
- 【微信小程序开发】 点击左上角菜单>「工具」>「设置」>点击「运行配置」页,找到「微信开发者工具路径」,在这里请正确设置微信开发者工具的安装路径; | |||
- 【支付宝/钉钉小程序开发】 点击左上角菜单>「工具」>「设置」>点击「运行配置」页,找到「支付宝开发者工具路径」,在这里请正确设置支付宝开发者工具的安装路径,注意支付宝小程序的路径要设置到 .exe 文件,不能直接选择目录,支付宝和钉钉共用同一个工具路径。 | |||
## 小程序主体注册 | |||
如果是开发小程序,您需要在对应的平台来申请注册自己的小程序主体账号,然后提交小程序名称、图标、类目,并申请小程序 AppID; | |||
后续提交审核、发布小程序,均需要在小程序公众平台上进行操作。 | |||
请先申请小程序 AppID: | |||
[微信小程序后台](https://mp.weixin.qq.com/) | |||
[钉钉小程序后台](https://open-dev.dingtalk.com/) | |||
[支付宝小程序后台](https://openhome.alipay.com/) | |||
申请了小程序 AppID,打开项目根目录下的 manifest.json,请在其中「微信/支付宝小程序配置」页面填入 AppID。 | |||
## 项目目录介绍 | |||
打开本项目后,可以在左侧的浏览项目目录和文件,点击文件即可进行编辑。 | |||
本项目的目录文件介绍: | |||
(目录): | |||
- /docs: 【重要】 详细文档 | |||
- /pages: 【重要】 所有页面均放在此目录下 | |||
- /components: 使用的组件(力软自研组件、其他第三方组件等都在此) | |||
- /static: 图片、音视频等静态资源目录 | |||
- /common: 多个页面通用的 CSS、JS 代码 | |||
- /res: 编译安卓/iOS 手机 App 用到的图标和启动封面图 | |||
- /node_modules: 使用到的 npm 库 | |||
- /unpackage (从未运行过的项目没有此文件夹): 运行项目时会自动把编译生成的代码放在此目录下 | |||
(文件): | |||
- App.vue: 【重要】 运行时的全局 Vue 对象,可以用来设置应用生命周期钩子、导入全局样式等 | |||
- config.js: 【重要】 多个页面的配置文件,可以控制页面显示的内容和方式 | |||
- main.js: 【重要】 项目启动时的入口 JS 文件 | |||
- pages.json: 【重要】 所有页面的路径、样式的配置、引入的组件和目录等配置 | |||
- manifest.json:【重要】 uni-app 相关的设置,在这里设置应用名称、微信 AppID 等 | |||
- package.json: 用到的 npm 库的名称和版本、uni-app 编译到钉钉小程序的编译配置等 | |||
- uni.scss: uni-app 内置的样式 | |||
- index.html: 编译到 H5 端时的页面模板 | |||
## uni-app 页面开发 | |||
uni-app 中,每一个页面都是一个 .vue 文件,它包含了模板、JS、CSS 代码; | |||
文件中可以引入其他 JS、CSS,引入安装好的 npm 库,也可以引入图片视频音乐等媒体。 | |||
**需要注意的是,所有页面必须放置于 /pages 目录中, 且每个页面都必须在 /pages.json 中添加一条对应的记录,否则页面无法导航。** | |||
**当您向力软框架中添加新的页面时,请及时修改 /pages.json 文件,确保所有页面都登记在该文件中。** | |||
例如: | |||
某个小程序页面,它的目录为: | |||
`/pages/mypage/index.vue` | |||
那么在 /pages.json 里,需要在 pages 列表中添加一条记录: | |||
`{ "path": "pages/mypage/index", "style": { "navigationBarTitleText": "我是页面的标题" } }` | |||
# 使用力软代码生成器 | |||
请先确保代码生成器的版本和 App 版本相同,不同版本可能会引发错误。 | |||
以下是使用力软代码生成器创建页面的流程: | |||
- 【自动】 使用力软代码生成器发布功能时,会在 /pages 目录下生成相应的页面文件; | |||
- 【手动】 您需要在 /pages.json 页面配置文件中,手动把这些页面添加到 pages 字段中。 | |||
例如: | |||
假设您使用代码生成器时,设置的输出目录名为 LR_Codedemo,功能类名为 wxtest,那么代码生成器会自动生成两个文件: | |||
`/pages/LR_Codedemo/wxtest/list.vue` (移动页面主页面); | |||
`/pages/LR_Codedemo/wxtest/single.vue` (移动表单页)。 | |||
然后,需要配置 /pages.json 文件: | |||
在 pages 数组里添加以下两条(navigationStyle 是页面的标题,可以自行更改): | |||
``` | |||
{ | |||
"pages": [ | |||
{ "path": "pages/LR_Codedemo/wxtest/list", "style": { "navigationStyle": "移动页面主页面标题" } }, | |||
{ "path": "pages/LR_Codedemo/wxtest/single", "style": { "navigationStyle": "移动表单页标题" } }, | |||
// ...其他页面 | |||
] | |||
} | |||
``` | |||
这样就完成了新自定义页面的生成。 | |||
按照小程序和 uni-app 的规范, 每个页面均需要单独建立一个文件夹;但是这样将使得工程目录过于复杂,因此力软框架中,每个页面使用单个 .vue 文件,和传统 Web 开发相同,请知悉。 | |||
也因为以上原因,新建页面时,请不要勾选「创建同名目录」这一项。 | |||
----- | |||
# 二次开发指南 | |||
**注意:** 小程序端不同于常规 Web 端,它没有浏览器上的 DOM 、BOM,也就是说没有 document、window 对象,因此也无法使用 jQuery 等工具;但是有同类的工具和 API 可以替代。 | |||
**注意:** 本框架中的每个页面为单个 .vue 文件,无需按照小程序或 uni-app 的要求,为每个页面单独建一个文件夹,因为这样会导致目录结构过于复杂。 | |||
每个 .vue 文件的格式如下: | |||
``` html | |||
<template> | |||
<!-- 页面模板内容 --> | |||
</template> | |||
<script> | |||
// 页面 JS 代码 ... | |||
export default { | |||
// ... | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
/* 页面样式 */ | |||
</style> | |||
``` | |||
## 页面模板相关注意事项: | |||
- 模板遵循 Vue 模板写法,[uni-app 页面模板文档](https://uniapp.dcloud.io/component/README); | |||
- 网页中的 `<div>`、`<span>`、`<img>` 等标签,在 uni-app 中需要分别写成 `<view>`、`<text>`、`<image>` 等; | |||
- 监听事件时,`@click` 需要改为 `@tap` 等对应的移动端事件 ; | |||
- (推荐)您也可以维持 HTML 中的写法不做修改,依然写成 `<div>` 或是 `@click`,uni-app 框架会在编译时自动处理,包括 CSS 选择器名也会自动处理; | |||
- 图片、视频、音频等媒体资源必须放置于 /static 目录下,引入示例:`src="/static/.../a.png"`; | |||
- 为了跨端兼容性,请避免使用 .sync 绑定属性。 | |||
## 页面 JS 代码注意事项: | |||
- JS 代码遵循 Vue 开发写法,[uni-app 使用 Vue.js 时的注意事项](https://uniapp.dcloud.io/use); | |||
- 生命周期和常规 Vue 网页不同,uni-app 中存在「页面生命周期」、「应用生命周期」两个概念,请阅读文档以了解; | |||
- 注意对空数据的处理,空数据在 uni-app 的不同平台中可能显示为 null 字符串或 undefined 字符串,而不是为空,这一点不同于网页版 Vue 开发,请注意处理; | |||
- 在使用 `import` 引入其他 JS 文件时,路径中可用 `@` 来代表项目根目录,例如 `import xxx from '@/xxx/xxx.js'`,但是引入资源文件(图片、样式表)时,需要使用 `~@` 来代表根目录。 | |||
## 页面样式注意事项: | |||
- uni-app 中存在一种特殊的 CSS 单位 rpx,也叫 upx(upx 已经废弃,不要使用);具体来讲,750rpx 即等于设备屏幕宽度,例如 iPhone 6 手机屏幕宽度为 375px,因此在 iPhone 6 上的 750rpx = 375px,比例为 2:1;设备屏幕越大则 rpx 对 px 的比例越接近 1:1; | |||
- 不同平台上 rem、em 尺寸可能表现不同(尤其是 rem),因此请慎用 rem、em 单位; | |||
- pages.json 中无法设置页面背景色,需要使用 `page { }` 样式来设置(等同于 H5 中的 `body { }`);pages.json 中的 `backgroundColor` 相关的指的是 iOS 系统上屏幕上下滚动露出的回弹区域背景色,并不是直接设置页面内容的背景色; | |||
- 使用 LESS 样式时候写成 `<style lang="less" scoped>`,注意多了 `lang="less"` 的属性;样式标签的 `scoped` 属性表示这些样式仅对当前页面,绝不会影响到其他页面;使用 LESS 开发时,您需要在 HBuilder X 中安装对应的插件;(也可以使用 SASS 等,方法同) | |||
- 复用的样式代码可以放置在 /common/css 中,引入时的路径: `~@/common/css/.../a.css` 需要写成这种形式; | |||
- 在 /App.vue 中的样式,适用于项目全局; | |||
- 部分页面样式(例如导航栏样式、标题样式等不属于页面内容的样式),需要在 /pages.json 中配置,参考[uni-app pages.json 配置文档](https://uniapp.dcloud.io/collocation/pages),uni 也可能提供了 API 用来动态改变; | |||
- 对于屏幕类型特殊的手机,如 iPhone X,由于底部两边圆角过大且底部有虚拟 Home 键,页面可能会被虚拟 Home 键挡住,使用 CSS 变量 `env(safe-area-inset-bottom)` 可以获取屏幕显示安全区距离屏幕底部的距离。 |
@@ -0,0 +1,65 @@ | |||
// 本页是框架的默认配置页 | |||
// 请不要修改本页的任何内容 | |||
// 如需定制相关配置项,请修改项目根目录下的 config.js 文件内的相应条目 | |||
export default { | |||
// 登录页显示的公司名称 | |||
"company": "(未设置公司名)", | |||
// App 版本号 | |||
"appVersion": "0.0.1", | |||
// 是否允许用户注册 | |||
"enableSignUp": true, | |||
// 请求数据的接口地址;可以配置多个,开发环境下登录页会出现选择菜单供您选择 | |||
"apiHost": [ | |||
"https://www.learun.cn/admsapi/learun/adms", | |||
"http://localhost:31173/learun/adms" | |||
], | |||
// 开发环境下自动填充登录账号密码,与接口地址一一对应,只在开发环境下显示 | |||
"devAccount": [ | |||
{ username: "18702193424", password: "123456" }, | |||
{ username: "System", password: "0000" }, | |||
], | |||
// 开发环境使用的接口地址(数组索引) | |||
"devApiHostIndex": 0, | |||
// 生产环境使用的接口地址(数组索引) | |||
"prodApiHostIndex": 0, | |||
// 额外添加的全局变量,必须在此处注册才能使用 | |||
"globalVariables": [], | |||
// 小程序绑定登录等配置(login=登录,bind=绑定,unbind=解绑) | |||
"miniProgramAccount": { | |||
// 微信小程序 | |||
"weixin": ["login", "bind", "unbind"], | |||
// 支付宝小程序 | |||
"alipay": ["login", "bind", "unbind"], | |||
// 钉钉小程序 | |||
"dingtalk": ["login", "bind", "unbind"] | |||
}, | |||
// 页面相关配置 | |||
"pageConfig": { | |||
// 全局设置是否使用圆形头像 | |||
"roundAvatar": false, | |||
// 「消息」页 | |||
"msg": { | |||
// 周期轮询消息的时间间隔,单位是毫秒 | |||
"fetchMsg": 3000 | |||
}, | |||
// 「通讯录」页 | |||
"contact": { | |||
// 是否显示(分)公司、部门、职员标签 | |||
"tag": true, | |||
// 是否在职员这一级也显示标签 | |||
"staffTag": false | |||
}, | |||
// 「聊天消息」页 | |||
"chat": { | |||
// 周期轮询消息的时间间隔,单位是毫秒 | |||
"fetchMsg": 1500 | |||
} | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
.customlist-item { | |||
padding: 20rpx 20rpx; | |||
border-bottom: 1rpx solid #ddd; | |||
background: #ffffff; | |||
position: relative; | |||
color: #8f8f94; | |||
&:first-child { | |||
border-top: 1rpx solid #ddd; | |||
} | |||
.customlist-item-field { | |||
padding-top: 4px; | |||
&:first-child { | |||
padding-top: 0; | |||
} | |||
.customlist-item-field-title { | |||
white-space: nowrap; | |||
color: #333333; | |||
} | |||
} | |||
} |
@@ -0,0 +1,75 @@ | |||
page { | |||
width: 100vw; | |||
overflow: hidden; | |||
} | |||
.mainpage { | |||
position: fixed; | |||
width: 100vw; | |||
height: calc(100% - var(--window-top)); | |||
left: 0; | |||
background-color: #f1f1f1; | |||
transition: all 0.4s; | |||
&.show { | |||
left: 85vw; | |||
box-shadow: 0 0 60upx rgba(0, 0, 0, 0.2); | |||
transform: scale(0.9, 0.9); | |||
transform-origin: 0; | |||
} | |||
} | |||
.sidepage { | |||
position: absolute; | |||
width: 85vw; | |||
height: calc(100% - var(--window-top)); | |||
left: 0; | |||
top: 0; | |||
transform: scale(0.9, 0.9) translateX(-100%); | |||
opacity: 0; | |||
pointer-events: none; | |||
transition: all 0.4s; | |||
padding: 50upx 0; | |||
.side-title { | |||
font-size: 1.2em; | |||
margin-bottom: 20rpx; | |||
margin-top: 40rpx; | |||
&:first-of-type { | |||
margin-top: 0; | |||
} | |||
} | |||
&.show { | |||
transform: scale(1, 1) translateX(0%); | |||
opacity: 1; | |||
pointer-events: all; | |||
} | |||
} | |||
.sideclose { | |||
position: absolute; | |||
width: 40vw; | |||
height: calc(100% - var(--window-top)); | |||
right: 0; | |||
top: 0; | |||
color: transparent; | |||
padding-bottom: 30upx; | |||
display: flex; | |||
align-items: flex-end; | |||
justify-content: center; | |||
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.6)); | |||
letter-spacing: 5px; | |||
font-size: 50upx; | |||
opacity: 0; | |||
pointer-events: none; | |||
transition: all 0.4s; | |||
&.show { | |||
opacity: 1; | |||
pointer-events: all; | |||
width: 15vw; | |||
color: #fff; | |||
} | |||
} |
@@ -0,0 +1,296 @@ | |||
import get from 'lodash/get' | |||
import moment from 'moment' | |||
/** | |||
* 表单数据处理相关方法 | |||
* (配合 <l-customform> 使用,注意本工具类不包含拉取表单数据的代码) | |||
* | |||
* 提供以下工具方法: | |||
* | |||
* 【新建表单时使用】: | |||
* async getDefaultData(schemeItem, { processId }) | |||
* 获取单个表单项的默认数据 | |||
* | |||
* 【打开一个表单时使用】: | |||
* async getSourceData(schemeItem) | |||
* 获取表单中的选单数据 / 获取单条表单项的选单数据 | |||
* | |||
* async convertToFormValue(schemeItem, val) | |||
* 将从 API 拉取的表单数据格式化规范化 | |||
* | |||
* 【提交表单时使用】: | |||
* async convertToPostData(schemeItem, val, formValue, scheme) | |||
* 将本地表单数据格式化为提交时的格式 | |||
*/ | |||
export default { | |||
methods: { | |||
/** | |||
* 获取一个 scheme 表单项的源数据 (加载表单时使用) | |||
* 参数: 单个 schemeItem | |||
* | |||
* radio、select、checkbox、layer 这四种表单项,需要加载额外的选单数据 | |||
* 选单数据有两种获取方式: | |||
* 1、来自数据字典: | |||
* 数据字典在 this.GET_GLOBAL('dataDictionary') | |||
* 表单使用的字段在 schemeItem.itemCode | |||
* 选单数据中的 text 字段作为显示, value 字段作为值 | |||
* | |||
* 2、来自数据源: | |||
* 将 schemeItem.dataSourceId 按符号「,」逗号分割为数组,分割为: [code, displayField, valueField] | |||
* 数据源需要请求 API 来获取,请求需要带上数据源的编号 code | |||
* displayField、valueField 分别为展示字段和值绑定字段 | |||
* | |||
* 选单数据有两种格式: | |||
* 1、对于 radio、select、checkbox 来说: | |||
* 只需要一个数组,数组形如: [{ text: '选项文字', value: '选项值' }, ...] | |||
* 将获取的数据绑定到组件的 range 属性上即可 | |||
* 全局数据中默认是对象形式,使用 Object.values() 转化即可 | |||
* | |||
* 2、对于 layer 来说: | |||
* 返回一个对象,形如 { source, layerData, selfField } | |||
* source: 为弹层中列出的数据,是一个数组 | |||
* layerData: 需要在弹层窗口中展示的字段及标题文字,形如: [{ name:'要展示的字段名', label:'标题文字' }] | |||
* selfField: 该表单值绑定哪个字段,默认为绑定到自身的字段 | |||
*/ | |||
async getSourceData(schemeItem) { | |||
if (['radio', 'select', 'checkbox'].includes(schemeItem.type)) { | |||
// radio select checkbox 三种情况 | |||
if (!schemeItem.dataSource || Number(schemeItem.dataSource) === 0) { | |||
// dataSource 为 0,使用 clientData | |||
return Object | |||
.values(this.GET_GLOBAL('dataDictionary')[schemeItem.itemCode]) | |||
.map(t => ({ value: t.value, text: t.text })) | |||
} else { | |||
// dataSource 不为 0,使用数据源,需要请求接口,并且提取出显示字段和实际字段 | |||
const [code, displayField = schemeItem.showField, valueField = schemeItem.saveField] = schemeItem.dataSourceId | |||
.split(',') | |||
const sourceData = await this.FETCH_DATASOURCE(code) | |||
if (!sourceData) { return [] } | |||
return sourceData.data.map(t => ({ text: t[displayField], value: t[valueField] })) | |||
} | |||
} else if (['layer'].includes(schemeItem.type)) { | |||
// layer 需要更多属性 | |||
if (!schemeItem.dataSource || Number(schemeItem.dataSource) === 0) { | |||
// dataSource 为 0,使用 clientData | |||
// clientData 对象转数组后,隐含 key:item.text 和 value:item.value 的关系 | |||
const [keyItem, valueItem] = schemeItem.layerData | |||
const source = Object | |||
.values(this.GET_GLOBAL('dataDictionary')[schemeItem.itemCode]) | |||
.map(t => ({ value: t.value, text: t.text })) | |||
return { | |||
source, | |||
layerData: [ | |||
{ name: 'text', label: keyItem.label || '', value: keyItem.value || '' }, | |||
{ name: 'value', label: valueItem.label || '', value: valueItem.value || '' } | |||
] | |||
} | |||
} else { | |||
// dataSource 不为 0,使用数据源,需要请求接口,并且提取出显示字段和实际字段 | |||
const [code] = schemeItem.dataSourceId.split(',') | |||
const sourceData = await this.FETCH_DATASOURCE(code) | |||
if (!sourceData) { return [] } | |||
const source = sourceData.data | |||
return { source, layerData: schemeItem.layerData.filter(t => (!t.hide) && (t.value || t.label)) } | |||
} | |||
} | |||
return [] | |||
}, | |||
/** | |||
* 获取一个 scheme 表单项的默认值 (用户新建表单时使用,或是编辑草稿) | |||
* 参数: 单个 schemeItem , { processId } | |||
* | |||
* 每种类别的表单项分别获取的默认值: | |||
* | |||
* currentInfo: 根据类别取当前用户/部门/公司/时间日期 | |||
* datetime: 根据 dfValue 字段表示昨天/今天/明天,格式化为字符串 | |||
* radio、select: 有 dfValue 则使用,否则取第一条 | |||
* checkbox: 有 dfValue 则使用,否则为空数组 | |||
* encode: 根据 rulecode 请求表单编码 | |||
* upload: 空数组 | |||
* guid: 赋值第二个参数中的 processId,但是如果在子表格中,赋空字符串 | |||
* girdtable: 递归所有表格项 scheme 依次为它们生成默认值 | |||
* datetimerange: 字符串 0 | |||
*/ | |||
async getDefaultData(item, prop) { | |||
const { processId } = prop | |||
switch (item.type) { | |||
case 'currentInfo': | |||
switch (item.dataType) { | |||
case 'user': | |||
return this.GET_GLOBAL('loginUser').userId | |||
case 'department': | |||
return this.GET_GLOBAL('loginUser').departmentId | |||
case 'company': | |||
return this.GET_GLOBAL('loginUser').companyId | |||
case 'time': | |||
return moment().format('YYYY-MM-DD HH:mm:ss') | |||
default: | |||
return '' | |||
} | |||
case 'datetime': | |||
const datetimeFormat = item.table ? | |||
(Number(item.dateformat) === 0 ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss') : | |||
(item.datetime === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD') | |||
const today = moment() | |||
const dfDatetime = [ | |||
today.subtract(1, 'day'), | |||
today, | |||
today.add(1, 'day') | |||
][Number(item.dfvalue)] || today | |||
return dfDatetime.format(datetimeFormat) || '' | |||
case 'radio': | |||
case 'select': | |||
const radioItem = item.__sourceData__.find(t => t.value === item.dfvalue) || item.__sourceData__[0] | |||
return item.type === 'radio' ? radioItem.value : '' | |||
case 'checkbox': | |||
if (!item.dfvalue) { | |||
return [] | |||
} | |||
return item.dfvalue.split(',').filter(t => item.__sourceData__.find(s => s.value === t)) | |||
case 'encode': | |||
const result = await this.FETCH_ENCODE(item.rulecode) | |||
return result | |||
case 'upload': | |||
return [] | |||
case 'guid': | |||
return item.table ? processId : '' | |||
case 'girdtable': | |||
const tableItemObj = {} | |||
for (const fieldItem of item.fieldsData) { | |||
tableItemObj[fieldItem.field] = await this.getDefaultData(fieldItem, prop) | |||
} | |||
return this.COPY(tableItemObj) | |||
case 'datetimerange': | |||
return '0' | |||
default: | |||
return item.dfvalue || '' | |||
} | |||
}, | |||
/** | |||
* 将单条 formData 值转化为 formValue 值 (拉取表单数据时使用) | |||
* 参数: 单个 schemeItem , 数据值 | |||
* | |||
* 具体执行逻辑: | |||
* radio、select: 剔除无效值 | |||
* checkbox: 分割成数组并剔除无效值 | |||
* upload: 分割成数组,拉取其中所有文件的信息 | |||
* datetime: 按照时间日期格式进行格式化字符串 | |||
* 其他类型: 保留原值 | |||
*/ | |||
async convertToFormValue(item, val) { | |||
switch (item.type) { | |||
case 'upload': | |||
if (!val) { return [] } | |||
const uidList = val.split(',') | |||
const fileList = [] | |||
for (const uid of uidList || []) { | |||
const fileInfo = await this.FETCH_FILEINFO(uid) | |||
if (!fileInfo) { continue } | |||
const fileType = fileInfo.F_FileType | |||
const fileSize = fileInfo.F_FileSize | |||
const path = this.API + '/annexes/wxdown?' + this.URL_QUERY(uid, true) | |||
fileList.push({ path, type: fileType, uid, size: fileSize }) | |||
} | |||
return fileList | |||
case 'select': | |||
case 'radio': | |||
if (!val || !item.__sourceData__.map(t => t.value).includes(val)) { | |||
return '' | |||
} | |||
return val | |||
case 'checkbox': | |||
const validValue = item.__sourceData__.map(t => t.value) | |||
const checkboxVal = val.split(',') || [] | |||
return checkboxVal.filter(t => validValue.includes(t)) | |||
case 'datetime': | |||
if (!val) { | |||
return '' | |||
} | |||
return moment(val).format( | |||
Number(item.dateformat) === 0 || item.datetime === 'date' ? | |||
'YYYY-MM-DD' : | |||
'YYYY-MM-DD HH:mm:ss' | |||
) | |||
default: | |||
return val || '' | |||
} | |||
}, | |||
/** | |||
* 将一个 formValue 值转化为 post 提交值(提交表单数据时使用) | |||
* 参数: 单个 schemeItem , 表单项值 , 所有 formValue , scheme | |||
* | |||
* 具体执行逻辑: | |||
* checkbox: 将数组使用符号「,」逗号拼接成字符串 | |||
* datetimerange: 获取开始日期、结束日期,计算差值天数并保留整数 | |||
* datetime: 格式化为完整时间日期字符串 | |||
* upload: 依次上传文件,将返回的文件 ID 使用符号「,」逗号拼接成字符串 | |||
* 其他类型: 保留原值 | |||
*/ | |||
async convertToPostData(item, val, formValue, scheme) { | |||
switch (item.type) { | |||
case 'checkbox': | |||
return val ? val.join(',') : '' | |||
case 'datetimerange': | |||
const startTime = get(formValue, scheme.find(t => t.id === item.startTime).__valuePath__, null) | |||
const endTime = get(formValue, scheme.find(t => t.id === item.endTime).__valuePath__, null) | |||
if (!startTime || !endTime || moment(endTime).isBefore(startTime)) { | |||
return '' | |||
} else { | |||
return moment.duration(moment(endTime).diff(moment(startTime))).asDays().toFixed(0) | |||
} | |||
case 'datetime': | |||
return val ? moment(val).format('YYYY-MM-DD HH:mm:ss') : '' | |||
case 'upload': | |||
const uploadUid = [] | |||
for (const { path, uid } of val) { | |||
if (uid) { | |||
uploadUid.push(uid) | |||
continue | |||
} | |||
const fileId = await this.HTTP_UPLOAD(path) | |||
if (fileId) { | |||
uploadUid.push(fileId) | |||
} | |||
} | |||
return uploadUid.join(',') | |||
default: | |||
return val || '' | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,313 @@ | |||
import get from 'lodash/get' | |||
import omit from 'lodash/omit' | |||
import moment from 'moment' | |||
/** | |||
* 用于代码生成器页面,加载表单数据使用 | |||
* | |||
* 提供以下工具方法: | |||
* | |||
* 【新建表单时使用】: | |||
* async getDefaultForm() | |||
* async getDefaultValue(path, scheme) | |||
* 根据表单 scheme 获取初始化的空表单数据 / 获取单条表单项的初始数据 | |||
* | |||
* 【提交表单时使用】: | |||
* async getPostData() | |||
* async convertToPostData() | |||
* 生成 POST 提交用的表单数据 / 获取单条表单项的提交数据 | |||
* | |||
* verifyForm() | |||
* 验证表单数据,返回一个输入错误信息的数组 | |||
* | |||
* 【打开一个表单时使用】: | |||
* async formatFormData(formData) | |||
* async convertToFormValue(scheme, val, dataSource) | |||
* 将拉取的表单值转化为表单数据 / 将单条表单项的表单值转化为表单数据 | |||
* | |||
*/ | |||
export default { | |||
methods: { | |||
// 获取表单默认值 | |||
async getDefaultForm() { | |||
const result = {} | |||
for (const [tableName, tableItem] of Object.entries(this.scheme)) { | |||
const itemData = {} | |||
for (const [fieldName, scheme] of Object.entries(tableItem)) { | |||
if (fieldName !== '__GIRDTABLE__') { | |||
itemData[fieldName] = await this.getDefaultValue(`${tableName}.${fieldName}`, scheme) | |||
} | |||
} | |||
result[tableName] = '__GIRDTABLE__' in tableItem ? [itemData] : itemData | |||
} | |||
return result | |||
}, | |||
// 获取单条表单项的默认值 | |||
async getDefaultValue(path, schemeItem) { | |||
switch (schemeItem.type) { | |||
case 'keyValue': | |||
return this.processId | |||
case 'currentInfo': | |||
switch (schemeItem.dataType) { | |||
case 'user': | |||
return this.GET_GLOBAL('loginUser').userId | |||
case 'department': | |||
return this.GET_GLOBAL('loginUser').departmentId | |||
case 'company': | |||
return this.GET_GLOBAL('loginUser').companyId | |||
case 'time': | |||
return moment().format('YYYY-MM-DD HH:mm:ss') | |||
default: | |||
return '' | |||
} | |||
case 'datetime': | |||
const datetimeFormat = (Number(schemeItem.dateformat) === 0 ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss') | |||
const today = moment() | |||
const dfDatetime = [ | |||
today.subtract(1, 'day'), | |||
today, | |||
today.add(1, 'day') | |||
][Number(schemeItem.dfvalue)] || today | |||
return dfDatetime.format(datetimeFormat) || '' | |||
case 'radio': | |||
case 'select': | |||
const radioItem = get(this.dataSource, path).find(t => t.value === schemeItem.dfvalue) || | |||
get(this.dataSource, path)[0] | |||
return schemeItem.type === 'radio' ? radioItem.value : '' | |||
case 'checkbox': | |||
if (!schemeItem.dfvalue) { return [] } | |||
return schemeItem.dfvalue.split(',').filter(t => get(this.dataSource, path, []).find(s => s.value === t)) | |||
case 'encode': | |||
if (!schemeItem.rulecode) { return '' } | |||
const result = await this.FETCH_ENCODE(schemeItem.rulecode) | |||
return result || '' | |||
case 'upload': | |||
return [] | |||
case 'guid': | |||
return this.GUID('-') | |||
default: | |||
return schemeItem.dfvalue || '' | |||
} | |||
}, | |||
// 验证表单项输入是否正确,返回一个包含所有错误信息的数组 | |||
verifyForm() { | |||
const result = [] | |||
Object.entries(this.scheme).forEach(([tableName, tableItem]) => { | |||
if ('__GIRDTABLE__' in tableItem) { | |||
this.getValue(tableName).forEach((tableValue, index) => { | |||
Object.entries(tableItem).forEach(([fieldName, scheme]) => { | |||
if (fieldName === '__GIRDTABLE__' || !scheme.verify) { return } | |||
const val = tableValue[fieldName] | |||
const verifyResult = this.verify[scheme.verify](val) | |||
if (verifyResult !== true) { | |||
result.push(`[表格${tableItem.__GIRDTABLE__}第${index}行${scheme.title}列]: ${verifyResult}`) | |||
} | |||
}) | |||
}) | |||
} else { | |||
Object.entries(tableItem).forEach(([fieldName, scheme]) => { | |||
if (!scheme.verify) { return } | |||
const val = this.getValue(`${tableName}.${fieldName}`) | |||
const verifyResult = this.verify[scheme.verify](val) | |||
if (verifyResult !== true) { | |||
result.push(`[${scheme.title}]: ${verifyResult}`) | |||
} | |||
}) | |||
} | |||
}) | |||
return result | |||
}, | |||
// 获取要提交的表单数据(提交时使用) | |||
async getPostData(keyValue) { | |||
const result = {} | |||
for (const [tableName, tableItem] of Object.entries(this.scheme)) { | |||
if ('__GIRDTABLE__' in tableItem) { | |||
// 从表 | |||
const tableArray = [] | |||
const tableData = this.current[tableName] | |||
for (let index = 0; index < tableData.length; ++index) { | |||
const tableValue = tableData[index] | |||
const tableObj = {} | |||
for (const [fieldName, scheme] of Object.entries(tableItem)) { | |||
if (fieldName === '__GIRDTABLE__') { continue } | |||
tableObj[fieldName] = await this.convertToPostData(scheme, tableValue[fieldName]) | |||
} | |||
tableArray.push(tableObj) | |||
} | |||
result[`str${tableName}Entity`] = JSON.stringify(tableArray) | |||
} else { | |||
// 主表 | |||
const strEntity = {} | |||
for (const [fieldName, scheme] of Object.entries(tableItem)) { | |||
strEntity[fieldName] = await this.convertToPostData(scheme, this.current[tableName][fieldName]) | |||
} | |||
result['strEntity'] = JSON.stringify(strEntity) | |||
} | |||
} | |||
if (keyValue) { | |||
result.keyValue = keyValue | |||
} | |||
return result | |||
}, | |||
// 将单项表单数据转为 post 数据(提交时使用) | |||
async convertToPostData(scheme, val) { | |||
switch (scheme.type) { | |||
case 'checkbox': | |||
return val ? val.join(',') : '' | |||
case 'datetimerange': | |||
const startTime = this.getValue(scheme.startTime) | |||
const endTime = this.getValue(scheme.endTime) | |||
if (!startTime || !endTime || moment(endTime).isBefore(startTime)) { | |||
return '' | |||
} else { | |||
return moment.duration(moment(endTime).diff(moment(startTime))).asDays().toFixed(0) | |||
} | |||
case 'datetime': | |||
return val ? moment(val).format('YYYY-MM-DD HH:mm') : '' | |||
case 'upload': | |||
const uploadUid = [] | |||
for (const { path, uid } of val) { | |||
if (uid) { | |||
uploadUid.push(uid) | |||
continue | |||
} | |||
const fileId = await this.HTTP_UPLOAD(path) | |||
if (fileId) { | |||
uploadUid.push(fileId) | |||
} | |||
} | |||
return uploadUid.join(',') | |||
default: | |||
return val || '' | |||
} | |||
}, | |||
// 格式化处理表单数据(拉取时使用) | |||
async formatFormData(formData) { | |||
const data = omit(formData, 'keyValue') | |||
for (const [tableName, schemeItem] of Object.entries(this.scheme)) { | |||
if ('__GIRDTABLE__' in schemeItem) { | |||
if (!data[tableName] || data[tableName].length <= 0) { data[tableName] = [{}] } | |||
const tableData = data[tableName] | |||
for (let index = 0; index < tableData.length; ++index) { | |||
const tableValue = tableData[index] | |||
for (const [fieldName, scheme] of Object.entries(schemeItem)) { | |||
if (fieldName === '__GIRDTABLE__') { continue } | |||
const dataSource = get(this.dataSource, `${tableName}.${fieldName}`) | |||
tableValue[fieldName] = await this.convertToFormValue(scheme, tableValue[fieldName], dataSource) | |||
} | |||
} | |||
} else { | |||
for (const [fieldName, scheme] of Object.entries(schemeItem)) { | |||
const dataSource = get(this.dataSource, `${tableName}.${fieldName}`) | |||
data[tableName][fieldName] = await this.convertToFormValue(scheme, data[tableName][fieldName], dataSource) | |||
} | |||
} | |||
} | |||
return data | |||
}, | |||
// 将单项表单数据格式化(拉取时使用) | |||
async convertToFormValue(scheme, val, dataSource) { | |||
switch (scheme.type) { | |||
case 'upload': | |||
if (!val) { return [] } | |||
const uidList = val.split(',') | |||
const fileList = [] | |||
for (const uid of uidList || []) { | |||
const fileInfo = await this.FETCH_FILEINFO(uid) | |||
if (!fileInfo) { continue } | |||
const fileType = fileInfo.F_FileType | |||
const fileSize = fileInfo.F_FileSize | |||
const path = this.API + '/annexes/wxdown?' + this.URL_QUERY(uid, true) | |||
fileList.push({ path, type: fileType, uid, size: fileSize }) | |||
} | |||
return fileList | |||
case 'radio': | |||
case 'select': | |||
if (!val || !dataSource.map(t => t.value).includes(String(val))) { return '' } | |||
return String(val) | |||
case 'checkbox': | |||
if (!val) { return [] } | |||
const validValue = dataSource.map(t => t.value) | |||
const checkboxVal = val.split(',') || [] | |||
return checkboxVal.filter(t => validValue.includes(t)) | |||
case 'datetime': | |||
if (!val) { return '' } | |||
return moment(val).format( | |||
Number(scheme.dateformat) === 0 || scheme.datetime === 'date' ? | |||
'YYYY-MM-DD' : | |||
'YYYY-MM-DD HH:mm:ss' | |||
) | |||
default: | |||
return val === null || val === undefined ? '' : val | |||
} | |||
} | |||
}, | |||
computed: { | |||
// 验证函数 | |||
verify() { | |||
return { | |||
NotNull: t => t.length > 0 || '不能为空', | |||
Num: t => !isNaN(t) || '须输入数值', | |||
NumOrNull: t => t.length <= 0 || !isNaN(t) || '须留空或输入数值', | |||
Email: t => /^[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_]+.[a-zA-Z0-9]+$/.test(t) || '须符合Email格式', | |||
EmailOrNull: t => t.length <= 0 || /^[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_]+.[a-zA-Z0-9]+$/.test(t) || | |||
'须留空或符合Email格式', | |||
EnglishStr: t => /^[a-zA-Z]*$/.test(t) || '须由英文字母组成', | |||
EnglishStrOrNull: t => t.length <= 0 || /^[a-zA-Z]*$/.test(t) || '须留空或由英文字母组成', | |||
Phone: t => /^[+0-9- ]*$/.test(t) || '须符合电话号码格式', | |||
PhoneOrNull: t => t.length <= 0 || /^[+0-9- ]*$/.test(t) || '须留空或符合电话号码格式', | |||
Fax: t => /^[+0-9- ]*$/.test(t) || '须符合传真号码格式', | |||
Mobile: t => /^1[0-9]{10}$/.test(t) || '须符合手机号码格式', | |||
MobileOrPhone: t => /^[+0-9- ]*$/.test(t) || /^1[0-9]{10}$/.test(t) || '须符合电话或手机号码格式', | |||
MobileOrNull: t => t.length <= 0 || /^1[0-9]{10}$/.test(t) || '须留空或符合手机号码格式', | |||
MobileOrPhoneOrNull: t => t.length <= 0 || /^1[0-9]{10}$/.test(t) || /^[+0-9- ]*$/.test(t) || | |||
'须留空或符合手机/电话号码格式', | |||
Uri: t => /^[a-zA-z]+:\/\/[^\s]*$/.test(t) || '须符合网址Url格式', | |||
UriOrNull: t => t.length <= 0 || /^[a-zA-z]+:\/\/[^\s]*$/.test(t) || '须留空或符合网址Url格式' | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,683 @@ | |||
/** | |||
* [js-md5]{@link https://github.com/emn178/js-md5} | |||
* | |||
* @namespace md5 | |||
* @version 0.7.3 | |||
* @author Chen, Yi-Cyuan [emn178@gmail.com] | |||
* @copyright Chen, Yi-Cyuan 2014-2017 | |||
* @license MIT | |||
*/ | |||
(function () { | |||
'use strict'; | |||
var ERROR = 'input is invalid type'; | |||
var WINDOW = typeof window === 'object'; | |||
var root = WINDOW ? window : {}; | |||
if (root.JS_MD5_NO_WINDOW) { | |||
WINDOW = false; | |||
} | |||
var WEB_WORKER = !WINDOW && typeof self === 'object'; | |||
var NODE_JS = !root.JS_MD5_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; | |||
if (NODE_JS) { | |||
root = global; | |||
} else if (WEB_WORKER) { | |||
root = self; | |||
} | |||
var COMMON_JS = !root.JS_MD5_NO_COMMON_JS && typeof module === 'object' && module.exports; | |||
var AMD = typeof define === 'function' && define.amd; | |||
var ARRAY_BUFFER = !root.JS_MD5_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; | |||
var HEX_CHARS = '0123456789abcdef'.split(''); | |||
var EXTRA = [128, 32768, 8388608, -2147483648]; | |||
var SHIFT = [0, 8, 16, 24]; | |||
var OUTPUT_TYPES = ['hex', 'array', 'digest', 'buffer', 'arrayBuffer', 'base64']; | |||
var BASE64_ENCODE_CHAR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); | |||
var blocks = [], buffer8; | |||
if (ARRAY_BUFFER) { | |||
var buffer = new ArrayBuffer(68); | |||
buffer8 = new Uint8Array(buffer); | |||
blocks = new Uint32Array(buffer); | |||
} | |||
if (root.JS_MD5_NO_NODE_JS || !Array.isArray) { | |||
Array.isArray = function (obj) { | |||
return Object.prototype.toString.call(obj) === '[object Array]'; | |||
}; | |||
} | |||
if (ARRAY_BUFFER && (root.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { | |||
ArrayBuffer.isView = function (obj) { | |||
return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; | |||
}; | |||
} | |||
/** | |||
* @method hex | |||
* @memberof md5 | |||
* @description Output hash as hex string | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {String} Hex string | |||
* @example | |||
* md5.hex('The quick brown fox jumps over the lazy dog'); | |||
* // equal to | |||
* md5('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
/** | |||
* @method digest | |||
* @memberof md5 | |||
* @description Output hash as bytes array | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {Array} Bytes array | |||
* @example | |||
* md5.digest('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
/** | |||
* @method array | |||
* @memberof md5 | |||
* @description Output hash as bytes array | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {Array} Bytes array | |||
* @example | |||
* md5.array('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
/** | |||
* @method arrayBuffer | |||
* @memberof md5 | |||
* @description Output hash as ArrayBuffer | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {ArrayBuffer} ArrayBuffer | |||
* @example | |||
* md5.arrayBuffer('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
/** | |||
* @method buffer | |||
* @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead. | |||
* @memberof md5 | |||
* @description Output hash as ArrayBuffer | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {ArrayBuffer} ArrayBuffer | |||
* @example | |||
* md5.buffer('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
/** | |||
* @method base64 | |||
* @memberof md5 | |||
* @description Output hash as base64 string | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {String} base64 string | |||
* @example | |||
* md5.base64('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
var createOutputMethod = function (outputType) { | |||
return function (message) { | |||
return new Md5(true).update(message)[outputType](); | |||
}; | |||
}; | |||
/** | |||
* @method create | |||
* @memberof md5 | |||
* @description Create Md5 object | |||
* @returns {Md5} Md5 object. | |||
* @example | |||
* var hash = md5.create(); | |||
*/ | |||
/** | |||
* @method update | |||
* @memberof md5 | |||
* @description Create and update Md5 object | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {Md5} Md5 object. | |||
* @example | |||
* var hash = md5.update('The quick brown fox jumps over the lazy dog'); | |||
* // equal to | |||
* var hash = md5.create(); | |||
* hash.update('The quick brown fox jumps over the lazy dog'); | |||
*/ | |||
var createMethod = function () { | |||
var method = createOutputMethod('hex'); | |||
if (NODE_JS) { | |||
method = nodeWrap(method); | |||
} | |||
method.create = function () { | |||
return new Md5(); | |||
}; | |||
method.update = function (message) { | |||
return method.create().update(message); | |||
}; | |||
for (var i = 0; i < OUTPUT_TYPES.length; ++i) { | |||
var type = OUTPUT_TYPES[i]; | |||
method[type] = createOutputMethod(type); | |||
} | |||
return method; | |||
}; | |||
var nodeWrap = function (method) { | |||
var crypto = eval("require('crypto')"); | |||
var Buffer = eval("require('buffer').Buffer"); | |||
var nodeMethod = function (message) { | |||
if (typeof message === 'string') { | |||
return crypto.createHash('md5').update(message, 'utf8').digest('hex'); | |||
} else { | |||
if (message === null || message === undefined) { | |||
throw ERROR; | |||
} else if (message.constructor === ArrayBuffer) { | |||
message = new Uint8Array(message); | |||
} | |||
} | |||
if (Array.isArray(message) || ArrayBuffer.isView(message) || | |||
message.constructor === Buffer) { | |||
return crypto.createHash('md5').update(new Buffer(message)).digest('hex'); | |||
} else { | |||
return method(message); | |||
} | |||
}; | |||
return nodeMethod; | |||
}; | |||
/** | |||
* Md5 class | |||
* @class Md5 | |||
* @description This is internal class. | |||
* @see {@link md5.create} | |||
*/ | |||
function Md5(sharedMemory) { | |||
if (sharedMemory) { | |||
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = | |||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = | |||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = | |||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; | |||
this.blocks = blocks; | |||
this.buffer8 = buffer8; | |||
} else { | |||
if (ARRAY_BUFFER) { | |||
var buffer = new ArrayBuffer(68); | |||
this.buffer8 = new Uint8Array(buffer); | |||
this.blocks = new Uint32Array(buffer); | |||
} else { | |||
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | |||
} | |||
} | |||
this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0; | |||
this.finalized = this.hashed = false; | |||
this.first = true; | |||
} | |||
/** | |||
* @method update | |||
* @memberof Md5 | |||
* @instance | |||
* @description Update hash | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {Md5} Md5 object. | |||
* @see {@link md5.update} | |||
*/ | |||
Md5.prototype.update = function (message) { | |||
if (this.finalized) { | |||
return; | |||
} | |||
var notString, type = typeof message; | |||
if (type !== 'string') { | |||
if (type === 'object') { | |||
if (message === null) { | |||
throw ERROR; | |||
} else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { | |||
message = new Uint8Array(message); | |||
} else if (!Array.isArray(message)) { | |||
if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { | |||
throw ERROR; | |||
} | |||
} | |||
} else { | |||
throw ERROR; | |||
} | |||
notString = true; | |||
} | |||
var code, index = 0, i, length = message.length, blocks = this.blocks; | |||
var buffer8 = this.buffer8; | |||
while (index < length) { | |||
if (this.hashed) { | |||
this.hashed = false; | |||
blocks[0] = blocks[16]; | |||
blocks[16] = blocks[1] = blocks[2] = blocks[3] = | |||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = | |||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = | |||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; | |||
} | |||
if (notString) { | |||
if (ARRAY_BUFFER) { | |||
for (i = this.start; index < length && i < 64; ++index) { | |||
buffer8[i++] = message[index]; | |||
} | |||
} else { | |||
for (i = this.start; index < length && i < 64; ++index) { | |||
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; | |||
} | |||
} | |||
} else { | |||
if (ARRAY_BUFFER) { | |||
for (i = this.start; index < length && i < 64; ++index) { | |||
code = message.charCodeAt(index); | |||
if (code < 0x80) { | |||
buffer8[i++] = code; | |||
} else if (code < 0x800) { | |||
buffer8[i++] = 0xc0 | (code >> 6); | |||
buffer8[i++] = 0x80 | (code & 0x3f); | |||
} else if (code < 0xd800 || code >= 0xe000) { | |||
buffer8[i++] = 0xe0 | (code >> 12); | |||
buffer8[i++] = 0x80 | ((code >> 6) & 0x3f); | |||
buffer8[i++] = 0x80 | (code & 0x3f); | |||
} else { | |||
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); | |||
buffer8[i++] = 0xf0 | (code >> 18); | |||
buffer8[i++] = 0x80 | ((code >> 12) & 0x3f); | |||
buffer8[i++] = 0x80 | ((code >> 6) & 0x3f); | |||
buffer8[i++] = 0x80 | (code & 0x3f); | |||
} | |||
} | |||
} else { | |||
for (i = this.start; index < length && i < 64; ++index) { | |||
code = message.charCodeAt(index); | |||
if (code < 0x80) { | |||
blocks[i >> 2] |= code << SHIFT[i++ & 3]; | |||
} else if (code < 0x800) { | |||
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; | |||
} else if (code < 0xd800 || code >= 0xe000) { | |||
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; | |||
} else { | |||
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); | |||
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; | |||
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; | |||
} | |||
} | |||
} | |||
} | |||
this.lastByteIndex = i; | |||
this.bytes += i - this.start; | |||
if (i >= 64) { | |||
this.start = i - 64; | |||
this.hash(); | |||
this.hashed = true; | |||
} else { | |||
this.start = i; | |||
} | |||
} | |||
if (this.bytes > 4294967295) { | |||
this.hBytes += this.bytes / 4294967296 << 0; | |||
this.bytes = this.bytes % 4294967296; | |||
} | |||
return this; | |||
}; | |||
Md5.prototype.finalize = function () { | |||
if (this.finalized) { | |||
return; | |||
} | |||
this.finalized = true; | |||
var blocks = this.blocks, i = this.lastByteIndex; | |||
blocks[i >> 2] |= EXTRA[i & 3]; | |||
if (i >= 56) { | |||
if (!this.hashed) { | |||
this.hash(); | |||
} | |||
blocks[0] = blocks[16]; | |||
blocks[16] = blocks[1] = blocks[2] = blocks[3] = | |||
blocks[4] = blocks[5] = blocks[6] = blocks[7] = | |||
blocks[8] = blocks[9] = blocks[10] = blocks[11] = | |||
blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; | |||
} | |||
blocks[14] = this.bytes << 3; | |||
blocks[15] = this.hBytes << 3 | this.bytes >>> 29; | |||
this.hash(); | |||
}; | |||
Md5.prototype.hash = function () { | |||
var a, b, c, d, bc, da, blocks = this.blocks; | |||
if (this.first) { | |||
a = blocks[0] - 680876937; | |||
a = (a << 7 | a >>> 25) - 271733879 << 0; | |||
d = (-1732584194 ^ a & 2004318071) + blocks[1] - 117830708; | |||
d = (d << 12 | d >>> 20) + a << 0; | |||
c = (-271733879 ^ (d & (a ^ -271733879))) + blocks[2] - 1126478375; | |||
c = (c << 17 | c >>> 15) + d << 0; | |||
b = (a ^ (c & (d ^ a))) + blocks[3] - 1316259209; | |||
b = (b << 22 | b >>> 10) + c << 0; | |||
} else { | |||
a = this.h0; | |||
b = this.h1; | |||
c = this.h2; | |||
d = this.h3; | |||
a += (d ^ (b & (c ^ d))) + blocks[0] - 680876936; | |||
a = (a << 7 | a >>> 25) + b << 0; | |||
d += (c ^ (a & (b ^ c))) + blocks[1] - 389564586; | |||
d = (d << 12 | d >>> 20) + a << 0; | |||
c += (b ^ (d & (a ^ b))) + blocks[2] + 606105819; | |||
c = (c << 17 | c >>> 15) + d << 0; | |||
b += (a ^ (c & (d ^ a))) + blocks[3] - 1044525330; | |||
b = (b << 22 | b >>> 10) + c << 0; | |||
} | |||
a += (d ^ (b & (c ^ d))) + blocks[4] - 176418897; | |||
a = (a << 7 | a >>> 25) + b << 0; | |||
d += (c ^ (a & (b ^ c))) + blocks[5] + 1200080426; | |||
d = (d << 12 | d >>> 20) + a << 0; | |||
c += (b ^ (d & (a ^ b))) + blocks[6] - 1473231341; | |||
c = (c << 17 | c >>> 15) + d << 0; | |||
b += (a ^ (c & (d ^ a))) + blocks[7] - 45705983; | |||
b = (b << 22 | b >>> 10) + c << 0; | |||
a += (d ^ (b & (c ^ d))) + blocks[8] + 1770035416; | |||
a = (a << 7 | a >>> 25) + b << 0; | |||
d += (c ^ (a & (b ^ c))) + blocks[9] - 1958414417; | |||
d = (d << 12 | d >>> 20) + a << 0; | |||
c += (b ^ (d & (a ^ b))) + blocks[10] - 42063; | |||
c = (c << 17 | c >>> 15) + d << 0; | |||
b += (a ^ (c & (d ^ a))) + blocks[11] - 1990404162; | |||
b = (b << 22 | b >>> 10) + c << 0; | |||
a += (d ^ (b & (c ^ d))) + blocks[12] + 1804603682; | |||
a = (a << 7 | a >>> 25) + b << 0; | |||
d += (c ^ (a & (b ^ c))) + blocks[13] - 40341101; | |||
d = (d << 12 | d >>> 20) + a << 0; | |||
c += (b ^ (d & (a ^ b))) + blocks[14] - 1502002290; | |||
c = (c << 17 | c >>> 15) + d << 0; | |||
b += (a ^ (c & (d ^ a))) + blocks[15] + 1236535329; | |||
b = (b << 22 | b >>> 10) + c << 0; | |||
a += (c ^ (d & (b ^ c))) + blocks[1] - 165796510; | |||
a = (a << 5 | a >>> 27) + b << 0; | |||
d += (b ^ (c & (a ^ b))) + blocks[6] - 1069501632; | |||
d = (d << 9 | d >>> 23) + a << 0; | |||
c += (a ^ (b & (d ^ a))) + blocks[11] + 643717713; | |||
c = (c << 14 | c >>> 18) + d << 0; | |||
b += (d ^ (a & (c ^ d))) + blocks[0] - 373897302; | |||
b = (b << 20 | b >>> 12) + c << 0; | |||
a += (c ^ (d & (b ^ c))) + blocks[5] - 701558691; | |||
a = (a << 5 | a >>> 27) + b << 0; | |||
d += (b ^ (c & (a ^ b))) + blocks[10] + 38016083; | |||
d = (d << 9 | d >>> 23) + a << 0; | |||
c += (a ^ (b & (d ^ a))) + blocks[15] - 660478335; | |||
c = (c << 14 | c >>> 18) + d << 0; | |||
b += (d ^ (a & (c ^ d))) + blocks[4] - 405537848; | |||
b = (b << 20 | b >>> 12) + c << 0; | |||
a += (c ^ (d & (b ^ c))) + blocks[9] + 568446438; | |||
a = (a << 5 | a >>> 27) + b << 0; | |||
d += (b ^ (c & (a ^ b))) + blocks[14] - 1019803690; | |||
d = (d << 9 | d >>> 23) + a << 0; | |||
c += (a ^ (b & (d ^ a))) + blocks[3] - 187363961; | |||
c = (c << 14 | c >>> 18) + d << 0; | |||
b += (d ^ (a & (c ^ d))) + blocks[8] + 1163531501; | |||
b = (b << 20 | b >>> 12) + c << 0; | |||
a += (c ^ (d & (b ^ c))) + blocks[13] - 1444681467; | |||
a = (a << 5 | a >>> 27) + b << 0; | |||
d += (b ^ (c & (a ^ b))) + blocks[2] - 51403784; | |||
d = (d << 9 | d >>> 23) + a << 0; | |||
c += (a ^ (b & (d ^ a))) + blocks[7] + 1735328473; | |||
c = (c << 14 | c >>> 18) + d << 0; | |||
b += (d ^ (a & (c ^ d))) + blocks[12] - 1926607734; | |||
b = (b << 20 | b >>> 12) + c << 0; | |||
bc = b ^ c; | |||
a += (bc ^ d) + blocks[5] - 378558; | |||
a = (a << 4 | a >>> 28) + b << 0; | |||
d += (bc ^ a) + blocks[8] - 2022574463; | |||
d = (d << 11 | d >>> 21) + a << 0; | |||
da = d ^ a; | |||
c += (da ^ b) + blocks[11] + 1839030562; | |||
c = (c << 16 | c >>> 16) + d << 0; | |||
b += (da ^ c) + blocks[14] - 35309556; | |||
b = (b << 23 | b >>> 9) + c << 0; | |||
bc = b ^ c; | |||
a += (bc ^ d) + blocks[1] - 1530992060; | |||
a = (a << 4 | a >>> 28) + b << 0; | |||
d += (bc ^ a) + blocks[4] + 1272893353; | |||
d = (d << 11 | d >>> 21) + a << 0; | |||
da = d ^ a; | |||
c += (da ^ b) + blocks[7] - 155497632; | |||
c = (c << 16 | c >>> 16) + d << 0; | |||
b += (da ^ c) + blocks[10] - 1094730640; | |||
b = (b << 23 | b >>> 9) + c << 0; | |||
bc = b ^ c; | |||
a += (bc ^ d) + blocks[13] + 681279174; | |||
a = (a << 4 | a >>> 28) + b << 0; | |||
d += (bc ^ a) + blocks[0] - 358537222; | |||
d = (d << 11 | d >>> 21) + a << 0; | |||
da = d ^ a; | |||
c += (da ^ b) + blocks[3] - 722521979; | |||
c = (c << 16 | c >>> 16) + d << 0; | |||
b += (da ^ c) + blocks[6] + 76029189; | |||
b = (b << 23 | b >>> 9) + c << 0; | |||
bc = b ^ c; | |||
a += (bc ^ d) + blocks[9] - 640364487; | |||
a = (a << 4 | a >>> 28) + b << 0; | |||
d += (bc ^ a) + blocks[12] - 421815835; | |||
d = (d << 11 | d >>> 21) + a << 0; | |||
da = d ^ a; | |||
c += (da ^ b) + blocks[15] + 530742520; | |||
c = (c << 16 | c >>> 16) + d << 0; | |||
b += (da ^ c) + blocks[2] - 995338651; | |||
b = (b << 23 | b >>> 9) + c << 0; | |||
a += (c ^ (b | ~d)) + blocks[0] - 198630844; | |||
a = (a << 6 | a >>> 26) + b << 0; | |||
d += (b ^ (a | ~c)) + blocks[7] + 1126891415; | |||
d = (d << 10 | d >>> 22) + a << 0; | |||
c += (a ^ (d | ~b)) + blocks[14] - 1416354905; | |||
c = (c << 15 | c >>> 17) + d << 0; | |||
b += (d ^ (c | ~a)) + blocks[5] - 57434055; | |||
b = (b << 21 | b >>> 11) + c << 0; | |||
a += (c ^ (b | ~d)) + blocks[12] + 1700485571; | |||
a = (a << 6 | a >>> 26) + b << 0; | |||
d += (b ^ (a | ~c)) + blocks[3] - 1894986606; | |||
d = (d << 10 | d >>> 22) + a << 0; | |||
c += (a ^ (d | ~b)) + blocks[10] - 1051523; | |||
c = (c << 15 | c >>> 17) + d << 0; | |||
b += (d ^ (c | ~a)) + blocks[1] - 2054922799; | |||
b = (b << 21 | b >>> 11) + c << 0; | |||
a += (c ^ (b | ~d)) + blocks[8] + 1873313359; | |||
a = (a << 6 | a >>> 26) + b << 0; | |||
d += (b ^ (a | ~c)) + blocks[15] - 30611744; | |||
d = (d << 10 | d >>> 22) + a << 0; | |||
c += (a ^ (d | ~b)) + blocks[6] - 1560198380; | |||
c = (c << 15 | c >>> 17) + d << 0; | |||
b += (d ^ (c | ~a)) + blocks[13] + 1309151649; | |||
b = (b << 21 | b >>> 11) + c << 0; | |||
a += (c ^ (b | ~d)) + blocks[4] - 145523070; | |||
a = (a << 6 | a >>> 26) + b << 0; | |||
d += (b ^ (a | ~c)) + blocks[11] - 1120210379; | |||
d = (d << 10 | d >>> 22) + a << 0; | |||
c += (a ^ (d | ~b)) + blocks[2] + 718787259; | |||
c = (c << 15 | c >>> 17) + d << 0; | |||
b += (d ^ (c | ~a)) + blocks[9] - 343485551; | |||
b = (b << 21 | b >>> 11) + c << 0; | |||
if (this.first) { | |||
this.h0 = a + 1732584193 << 0; | |||
this.h1 = b - 271733879 << 0; | |||
this.h2 = c - 1732584194 << 0; | |||
this.h3 = d + 271733878 << 0; | |||
this.first = false; | |||
} else { | |||
this.h0 = this.h0 + a << 0; | |||
this.h1 = this.h1 + b << 0; | |||
this.h2 = this.h2 + c << 0; | |||
this.h3 = this.h3 + d << 0; | |||
} | |||
}; | |||
/** | |||
* @method hex | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as hex string | |||
* @returns {String} Hex string | |||
* @see {@link md5.hex} | |||
* @example | |||
* hash.hex(); | |||
*/ | |||
Md5.prototype.hex = function () { | |||
this.finalize(); | |||
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3; | |||
return HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + | |||
HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + | |||
HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + | |||
HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + | |||
HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + | |||
HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + | |||
HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + | |||
HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + | |||
HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + | |||
HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + | |||
HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + | |||
HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + | |||
HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + | |||
HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + | |||
HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + | |||
HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F]; | |||
}; | |||
/** | |||
* @method toString | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as hex string | |||
* @returns {String} Hex string | |||
* @see {@link md5.hex} | |||
* @example | |||
* hash.toString(); | |||
*/ | |||
Md5.prototype.toString = Md5.prototype.hex; | |||
/** | |||
* @method digest | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as bytes array | |||
* @returns {Array} Bytes array | |||
* @see {@link md5.digest} | |||
* @example | |||
* hash.digest(); | |||
*/ | |||
Md5.prototype.digest = function () { | |||
this.finalize(); | |||
var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3; | |||
return [ | |||
h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 24) & 0xFF, | |||
h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 24) & 0xFF, | |||
h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 24) & 0xFF, | |||
h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 24) & 0xFF | |||
]; | |||
}; | |||
/** | |||
* @method array | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as bytes array | |||
* @returns {Array} Bytes array | |||
* @see {@link md5.array} | |||
* @example | |||
* hash.array(); | |||
*/ | |||
Md5.prototype.array = Md5.prototype.digest; | |||
/** | |||
* @method arrayBuffer | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as ArrayBuffer | |||
* @returns {ArrayBuffer} ArrayBuffer | |||
* @see {@link md5.arrayBuffer} | |||
* @example | |||
* hash.arrayBuffer(); | |||
*/ | |||
Md5.prototype.arrayBuffer = function () { | |||
this.finalize(); | |||
var buffer = new ArrayBuffer(16); | |||
var blocks = new Uint32Array(buffer); | |||
blocks[0] = this.h0; | |||
blocks[1] = this.h1; | |||
blocks[2] = this.h2; | |||
blocks[3] = this.h3; | |||
return buffer; | |||
}; | |||
/** | |||
* @method buffer | |||
* @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead. | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as ArrayBuffer | |||
* @returns {ArrayBuffer} ArrayBuffer | |||
* @see {@link md5.buffer} | |||
* @example | |||
* hash.buffer(); | |||
*/ | |||
Md5.prototype.buffer = Md5.prototype.arrayBuffer; | |||
/** | |||
* @method base64 | |||
* @memberof Md5 | |||
* @instance | |||
* @description Output hash as base64 string | |||
* @returns {String} base64 string | |||
* @see {@link md5.base64} | |||
* @example | |||
* hash.base64(); | |||
*/ | |||
Md5.prototype.base64 = function () { | |||
var v1, v2, v3, base64Str = '', bytes = this.array(); | |||
for (var i = 0; i < 15;) { | |||
v1 = bytes[i++]; | |||
v2 = bytes[i++]; | |||
v3 = bytes[i++]; | |||
base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + | |||
BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + | |||
BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + | |||
BASE64_ENCODE_CHAR[v3 & 63]; | |||
} | |||
v1 = bytes[i]; | |||
base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + | |||
BASE64_ENCODE_CHAR[(v1 << 4) & 63] + | |||
'=='; | |||
return base64Str; | |||
}; | |||
var exports = createMethod(); | |||
if (COMMON_JS) { | |||
module.exports = exports; | |||
} else { | |||
/** | |||
* @method md5 | |||
* @description Md5 hash function, export to global in browsers. | |||
* @param {String|Array|Uint8Array|ArrayBuffer} message message to hash | |||
* @returns {String} md5 hashes | |||
* @example | |||
* md5(''); // d41d8cd98f00b204e9800998ecf8427e | |||
* md5('The quick brown fox jumps over the lazy dog'); // 9e107d9d372bb6826bd81d3542a419d6 | |||
* md5('The quick brown fox jumps over the lazy dog.'); // e4d909c290d0fb1ca068ffaddf22cbd0 | |||
* | |||
* // It also supports UTF-8 encoding | |||
* md5('中文'); // a7bac2239fcdcb3a067903d8077c4a07 | |||
* | |||
* // It also supports byte `Array`, `Uint8Array`, `ArrayBuffer` | |||
* md5([]); // d41d8cd98f00b204e9800998ecf8427e | |||
* md5(new Uint8Array([])); // d41d8cd98f00b204e9800998ecf8427e | |||
*/ | |||
root.md5 = exports; | |||
if (AMD) { | |||
define(function () { | |||
return exports; | |||
}); | |||
} | |||
} | |||
})(); |
@@ -0,0 +1,804 @@ | |||
import moment from 'moment' | |||
import get from 'lodash/get' | |||
import mapValues from 'lodash/mapValues' | |||
import defaultConfig from './config.default.js' | |||
import globalConfig from '@/config.js' | |||
import md5 from '@/common/md5.js' | |||
/** | |||
* 通用工具方法、全局数据 | |||
* 以下定义的所有方法、计算属性,均会被挂载到所有 .vue 页面的 this 上 | |||
* | |||
* 注意:小程序端,计算属性会以数据对象的形式挂载到所有页面的 Page 对象中 | |||
* 因此,数据量较大的情况下,为优化性能请使用调用方法动态获取的形式,避免占用太多内存 | |||
* (例如,GLOBAL 全局对象现在已经移除,改为使用 GET_GLOBAL(key) 来获取全局对象) | |||
*/ | |||
export default { | |||
// 【App 平台】点击返回键后移除 loading,防止某个页面出错按返回键后还有 loading | |||
// #ifdef APP-VUE | |||
onBackPress() { | |||
uni.hideLoading() | |||
}, | |||
// #endif | |||
methods: { | |||
// 暂存一个跨页面变量 (与 this.GET_PARAM 成对使用) | |||
SET_PARAM(val) { | |||
this.SET_GLOBAL('pageParam', val) | |||
}, | |||
// 获取之前暂存的跨页面变量 (与 this.SET_PARAM 成对使用) | |||
GET_PARAM() { | |||
return this.GET_GLOBAL('pageParam') | |||
}, | |||
// 进入某个页面 | |||
NAV_TO(url, param, usePageParam) { | |||
uni.navigateTo({ | |||
url: this.handleNav(url, param, usePageParam) | |||
}) | |||
}, | |||
// 返回上个页面 | |||
// delta 参数为返回的次数 | |||
NAV_BACK(delta = 1) { | |||
uni.navigateBack({ | |||
delta | |||
}) | |||
}, | |||
// 跳转到某个页面,跳转后无法返回 | |||
JUMP_TO(url, param, usePageParam) { | |||
uni.redirectTo({ | |||
url: this.handleNav(url, param, usePageParam) | |||
}) | |||
}, | |||
// 转到某个 Tab 页 | |||
TAB_TO(url, param) { | |||
uni.switchTab({ | |||
url: this.handleNav(url, param, true) | |||
}) | |||
}, | |||
// 重启应用,跳转到指定页面 | |||
RELAUNCH_TO(url) { | |||
uni.reLaunch({ | |||
url | |||
}) | |||
}, | |||
// 获取一个全局变量 | |||
// key 为键名 | |||
GET_GLOBAL(key) { | |||
return this.$store.state[key] | |||
}, | |||
// 设置一个全局变量 | |||
// key 为键名 | |||
// val 为值 | |||
SET_GLOBAL(key, val) { | |||
this.$store.commit(key, val) | |||
}, | |||
// 清空全局变量 | |||
CLEAR_GLOBAL() { | |||
this.$store.commit('clear') | |||
uni.removeStorageSync('token') | |||
}, | |||
// 将数据写入本地缓存 | |||
SET_STORAGE(key, data) { | |||
if (data === null) { | |||
uni.removeStorageSync(key) | |||
} else { | |||
uni.setStorageSync(key, data) | |||
} | |||
}, | |||
// 获取之前写入本地缓存的数据 | |||
GET_STORAGE(key) { | |||
return uni.getStorageSync(key) | |||
}, | |||
// 清空本地缓存 | |||
CLEAR_STORAGE() { | |||
uni.clearStorageSync() | |||
}, | |||
// 展示提示框 | |||
// title 为提示文字 | |||
// mask 为是否禁止点击 | |||
// icon 为显示图标,可以改为 'success' | |||
TOAST(title, icon = 'none', mask = false) { | |||
uni.showToast({ | |||
title, | |||
icon, | |||
mask | |||
}) | |||
}, | |||
// 停止展示 toast 提示框 | |||
HIDE_TOAST() { | |||
uni.hideToast() | |||
}, | |||
// 显示 loading 提示框 | |||
// title 为提示文字 | |||
// mask 为是否禁止点击 | |||
LOADING(title, mask = true) { | |||
uni.showLoading({ | |||
title, | |||
mask | |||
}) | |||
}, | |||
// 停止展示 loading 提示框 | |||
HIDE_LOADING() { | |||
uni.hideLoading() | |||
}, | |||
// 展示确认提示框 | |||
// title 为标题 | |||
// content 为提示框文字内容 | |||
// showCancel 为是否显示取消按钮 | |||
// 返回一个结果,true 或 false,表示用户是否确认 | |||
async CONFIRM(title, content, showCancel = false) { | |||
return new Promise(res => { | |||
uni.showModal({ | |||
title, | |||
content, | |||
showCancel, | |||
success: ({ | |||
confirm | |||
}) => { | |||
res(confirm) | |||
}, | |||
fail: () => { | |||
res(false) | |||
} | |||
}) | |||
}) | |||
}, | |||
// 设置页面标题 | |||
SET_TITLE(title) { | |||
uni.setNavigationBarTitle({ | |||
title | |||
}) | |||
}, | |||
// 从 /config.js 中获取某条配置项 | |||
// 如果在 /config.js 中获取不到,则前往 /common/config.default.js 中获取默认值 | |||
// path 为配置项的访问路径 | |||
// 举例: | |||
// 以下语句获取是否全局开启圆形头像 | |||
// this.CONFIG('pageConfig.roundAvatar') | |||
CONFIG(path) { | |||
return get(globalConfig, path, get(defaultConfig, path)) | |||
}, | |||
// 监听一个全局事件 | |||
ON(name, func) { | |||
uni.$on(name, func) | |||
}, | |||
// 仅单次监听一个全局事件 | |||
ONCE(name, func) { | |||
uni.$once(name, func) | |||
}, | |||
// 触发一个全局事件 | |||
EMIT(name, data) { | |||
uni.$emit(name, data) | |||
}, | |||
// 移除全局事件监听器 | |||
OFF(name, func) { | |||
uni.$off(name, func) | |||
}, | |||
// 拉取指定 code 值的数据源数据 | |||
async FETCH_DATASOURCE(code) { | |||
if (!code) { | |||
return [] | |||
} | |||
return await this.HTTP_GET('/datasource/map', { | |||
code, | |||
ver: '' | |||
}) | |||
}, | |||
// 拉取指定规则编号的表单编码数据 | |||
async FETCH_ENCODE(rulecode) { | |||
if (!rulecode) { | |||
return '' | |||
} | |||
return await this.HTTP_GET('/coderule/code', rulecode) | |||
}, | |||
// 拉取指定 id 的文件信息 | |||
async FETCH_FILEINFO(fileId) { | |||
if (!fileId) { | |||
return null | |||
} | |||
return await this.HTTP_GET('/annexes/wxfileinfo', fileId) | |||
}, | |||
// 封装的 GET 请求,集成了验证信息 | |||
// 返回请求结果或 null | |||
// 对网络错误、返回错误码、登录状态失效等情况做了相应处理 | |||
// url 为请求地址 | |||
// data 为请求附带的提交数据 | |||
async HTTP_GET(url, data, showTips) { | |||
const [err, res] = await this.requestBase(url, data, null, 'GET') | |||
return this.handleResult(err, res, showTips) | |||
}, | |||
// 封装的 POST 请求,集成了验证信息 | |||
// 返回请求结果或 null | |||
// 对网络错误、返回错误码、登录状态失效等情况做了相应处理 | |||
// url 为请求地址 | |||
// data 为请求附带的提交数据 | |||
async HTTP_POST(url, data, showTips) { | |||
const [err, res] = await this.requestBase(url, data, null, 'POST') | |||
return this.handleResult(err, res, showTips) | |||
}, | |||
// 封装的文件上传,集成了验证信息 | |||
// 返回接口返回值或 null | |||
// 对网络错误、返回错误码、登录状态失效等情况做了相应处理 | |||
// url 为请求地址 | |||
// filePath 为临时文件的路径 | |||
// formData 为请求附带的提交数据 | |||
async HTTP_UPLOAD(filePath, formData) { | |||
const [err, res] = await this.UPLOAD('/annexes/wxupload', filePath, formData) | |||
return this.handleResult(err, res) | |||
}, | |||
// 封装的文件下载,集成了验证信息 | |||
// 返回临时文件路径或 null | |||
// 对网络错误、返回错误码、登录状态失效等情况做了相应处理 | |||
// url 为请求地址 | |||
// formData 为请求附带的提交数据 | |||
async HTTP_DOWNLOAD(formData) { | |||
const [err, res] = await this.DOWNLOAD('/annexes/wxdown', formData) | |||
return this.handleResult(err, res) | |||
}, | |||
// 发起一个 GET 请求,封装了身份验证 | |||
// url 为请求地址 | |||
// data 为请求附带的提交数据 | |||
// 返回结果是一个数组: [error, result] | |||
// error 表示错误,一般是网络错误,请求很可能根本没有发出 | |||
// result 包含 { statusCode, headers, data } 分别表示状态码、响应头、数据 | |||
async GET(url, data, header) { | |||
return await this.requestBase(url, data, header, 'GET') | |||
}, | |||
// 发起一个 POST 请求,封装了身份验证 | |||
// url 为请求地址 | |||
// data 为请求附带的提交数据 | |||
// 返回结果是一个数组: [error, result] | |||
// error 表示错误,一般是网络错误,请求很可能根本没有发出 | |||
// result 包含 { statusCode, headers, data } 分别表示状态码、响应头、数据 | |||
async POST(url, data, header) { | |||
return await this.requestBase(url, data, header, 'POST') | |||
}, | |||
// 上传一个文件 (本地临时文件),封装了身份验证 | |||
// url 为提交地址 | |||
// filePath 为本地临时文件路径 | |||
// formData 为上传时附带的参数 | |||
// 返回结果是一个数组: [error, result] | |||
// error 表示错误,一般是网络错误,请求很可能根本没有发出 | |||
// result 包含 { statusCode, data } 分别表示状态码、接口返回的数据 | |||
async UPLOAD(url, filePath, formData) { | |||
const uploadUrl = this.handleUrl(url) | |||
const query = { | |||
loginMark: this.getLoginMark(), | |||
token: this.GET_GLOBAL('token') | |||
} | |||
if (formData && typeof formData === 'object') { | |||
Object.assign(query, formData) | |||
} else if (typeof formData === 'string') { | |||
Object.assign(query, { | |||
data: formData | |||
}) | |||
} | |||
// #ifdef MP-DINGTALK | |||
return new Promise((res, rej) => { | |||
dd.uploadFile({ | |||
url: uploadUrl, | |||
filePath, | |||
fileName: 'file', | |||
fileType: 'image', | |||
formData: query, | |||
success: dt => { | |||
dt.data = JSON.parse(dt.data) | |||
res([null, dt]) | |||
}, | |||
fail: rs => { | |||
rej([rs, null]) | |||
} | |||
}) | |||
}) | |||
// #endif | |||
// #ifndef MP-DINGTALK | |||
return uni.uploadFile({ | |||
url: uploadUrl, | |||
filePath, | |||
name: 'file', | |||
fileType: 'image', | |||
formData: query | |||
}).then(([err, result]) => { | |||
if (!err) { | |||
result.data = JSON.parse(result.data) | |||
return [null, result] | |||
} else { | |||
return [err, null] | |||
} | |||
}) | |||
// #endif | |||
}, | |||
// 下载一个文件(下载后为临时文件),封装了身份验证 | |||
// url 为请求的地址 | |||
// formData 为请求时附带的参数 | |||
// 返回结果是一个数组: [error, result] | |||
// error 表示错误,一般是网络错误,请求很可能根本没有发出 | |||
// result 包含 { statusCode, tempFilePath } 分别表示状态码、下载后的临时文件路径 | |||
async DOWNLOAD(url, formData) { | |||
let downloadUrl = this.handleUrl(url) | |||
const query = {} | |||
if (formData && typeof formData === 'object') { | |||
Object.assign(query, formData) | |||
} else if (typeof formData === 'string') { | |||
Object.assign(query, { | |||
data: formData | |||
}) | |||
} | |||
downloadUrl = downloadUrl + '?' + this.URL_QUERY(query, true) | |||
return uni.downloadFile({ | |||
url: downloadUrl | |||
}).then(([err, result]) => { | |||
if (!err) { | |||
result.data = { | |||
data: result.tempFilePath | |||
} | |||
result.statusCode = 200 | |||
} | |||
return [err, result] | |||
}) | |||
}, | |||
// 拉取客户端全局数据,直接写入全局变量 | |||
// 目前包括了:公司、部门、人员、数据字典 | |||
async FETCH_CLIENTDATA() { | |||
await Promise.all([ | |||
this.HTTP_GET('/company/map').then(res => this.SET_GLOBAL('company', res.data || {})), | |||
this.HTTP_GET('/department/map').then(res => this.SET_GLOBAL('department', res.data || {})), | |||
this.HTTP_GET('/user/map').then(res => this.SET_GLOBAL('user', res.data || {})), | |||
this.HTTP_GET('/dataitem/map').then(res => this.SET_GLOBAL('dataDictionary', res.data || {})) | |||
]) | |||
}, | |||
// 使用 JSON 序列化的方式克隆一个对象或数组 | |||
COPY(val) { | |||
return JSON.parse(JSON.stringify(val)) | |||
}, | |||
// 生成一个32位 GUID 随机字符串 | |||
// joinChar 为分割符,默认为下划线 | |||
GUID(joinChar = '_') { | |||
return `xxxxxxxx${joinChar}xxxx${joinChar}4xxx${joinChar}yxxx${joinChar}xxxxxxxxxxxx`.replace(/[xy]/g, c => { | |||
const r = Math.random() * 16 | 0; | |||
const v = c === 'x' ? r : (r & 0x3 | 0x8); | |||
return v.toString(16); | |||
}) | |||
}, | |||
// 获取指定字符串的 MD5 码 | |||
MD5(val = '') { | |||
return md5(val) | |||
}, | |||
// 将时间日期转化为特定格式 (一般用于 <l-list-item>) | |||
// datetimeString 为要格式化的日期时间字符串 | |||
// 返回值是一个数组,它也可以当做字符串直接使用 | |||
// 举例: | |||
// 如果日期是当天,则返回 ['今天 17:32'] 或者 '今天 17:32' | |||
// 如果日期是今年,则返回 ['6月8日', '17:32'] 或者 '6月8日 17:32' | |||
// 如果日期不是今年,返回 ['2018-06-08'] 或者 '2018-06-08' | |||
TABLEITEM_DATEFORMAT(datetimeString) { | |||
const dt = moment(datetimeString) | |||
let result = [] | |||
if (!dt.isValid()) { | |||
result.toString = () => '' | |||
return result | |||
} | |||
const now = moment() | |||
if (dt.isSame(now, 'day')) { | |||
result = [`今天 ${dt.format('HH:mm')}`] | |||
result.toString = () => `今天 ${dt.format('HH:mm')}` | |||
} else if (dt.isSame(now, 'year')) { | |||
result = [dt.format('M月D日'), dt.format('HH:mm')] | |||
result.toString = () => dt.format('M月D日') + ' ' + dt.format('HH:mm') | |||
} else { | |||
result = [dt.format('YYYY-MM-DD')] | |||
result.toString = () => dt.format('YYYY-MM-DD') | |||
} | |||
return result | |||
}, | |||
// 将一个对象编码并转化为 url 查询字符串 | |||
// obj 为要转换的对象,值为空则会被忽略,值为对象会被转为 JSON 字符串 | |||
// auth 为是否编入身份验证信息 | |||
URL_QUERY(obj, auth = false) { | |||
let queryObject = obj || {} | |||
if (typeof obj === 'string') { | |||
queryObject = { | |||
data: obj | |||
} | |||
} | |||
if (auth) { | |||
Object.assign(queryObject, { | |||
loginMark: this.getLoginMark(), | |||
token: this.GET_GLOBAL('token') | |||
}) | |||
} | |||
return Object.entries(queryObject) | |||
.filter(([k, v]) => k && v) | |||
.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(typeof v === 'object' ? JSON.stringify(v) : v)) | |||
.join('&') | |||
}, | |||
// 将字符串转化为 HTML 格式 (处理表单页面的 HTML) | |||
CONVERT_HTML(str) { | |||
if (!str) { | |||
return '' | |||
} | |||
return str | |||
.replace(/{@zuojian@}|{@youjian@}|{@and@}/g, tag => ({ | |||
'{@zuojian@}': '<', | |||
'{@youjian@}': '>', | |||
'{@and@}': '&' | |||
})[tag] || tag) | |||
.replace(/&|<|>|'|"/g, tag => ({ | |||
'&': '&', | |||
'<': '<', | |||
'>': '>', | |||
''': "'", | |||
'"': '"' | |||
})[tag] || tag) | |||
}, | |||
// 用于编码小程序分享消息的 url 查询字符串 (开发环境下还会打印出来) | |||
// pageParam 为点击分享消息跳转时携带的 pageParam | |||
// query 为点击分享消息跳转时携带的 query | |||
// pagePath 点击分享消息跳转到小程序的页面 (默认为当前页) | |||
// 返回编码好的查询字符串 | |||
MP_SHARE_ENCODE(pageParam, query, pagePath) { | |||
const shareObj = { | |||
fromUser: this.GET_GLOBAL('loginUser').userId, | |||
fromPlatform: this.PLATFORM, | |||
timestamp: new Date().valueOf(), | |||
pagePath: pagePath || this.pagePath, | |||
query: query, | |||
pageParam, | |||
learun: this.APP_VERSION | |||
} | |||
const result = this.URL_QUERY(shareObj) | |||
if (this.DEV) { | |||
console.log('【您正在分享力软小程序页面】') | |||
console.log('====分享对象:====') | |||
console.log(shareObj) | |||
console.log('====启动路径:====') | |||
console.log('/pages/home') | |||
console.log('====启动参数:====') | |||
console.log(result) | |||
console.log('====(以上消息仅开发模式可见)====') | |||
} | |||
return result | |||
}, | |||
// 解析小程序分享字符串 (会自动适配微信小程序的 url 编码) | |||
MP_SHARE_DECODE(info) { | |||
// 微信小程序中获取的分享信息是被 uri 编码过的,需要解码 | |||
// 支付宝/钉钉小程序不需要解码 | |||
// #ifdef MP-WEIXIN | |||
const shareInfo = mapValues(info, decodeURIComponent) | |||
// #endif | |||
shareInfo.pageParam = shareInfo.pageParam ? JSON.parse(shareInfo.pageParam) : undefined | |||
shareInfo.query = shareInfo.query ? this.URL_QUERY(JSON.parse(shareInfo.query)) : undefined | |||
if (this.DEV) { | |||
console.log('【您通过小程序消息分享启动了力软小程序】') | |||
console.log('====小程序分享对象:====') | |||
console.log(shareInfo) | |||
console.log('====即将转入页面:====') | |||
console.log(shareInfo.pagePath) | |||
console.log('====设置的 url query:====') | |||
console.log(shareInfo.query) | |||
console.log('====设置的 pageParam:====') | |||
console.log(shareInfo.pageParam) | |||
console.log('====(以上消息仅开发模式可见)====') | |||
} | |||
this.SET_GLOBAL('pageParam', shareInfo.pageParam) | |||
uni.navigateTo({ | |||
url: `${shareInfo.pagePath}?${this.URL_QUERY(shareInfo.query)}` | |||
}) | |||
}, | |||
// 【内部方法】处理页面跳转 url 和参数 | |||
handleNav(url, param, usePageParam) { | |||
let query = '' | |||
if (param && usePageParam) { | |||
this.SET_PARAM(param) | |||
} else if (param && !usePageParam) { | |||
query += '?' + Object.entries(param).filter(([k, v]) => k && v).map(([k, v]) => k + '=' + v).join('&') | |||
} | |||
return url + query | |||
}, | |||
// 【内部方法】从全局变量和缓存中获取 loginMark 设备标识,获取不到则会重新生成一个 | |||
getLoginMark() { | |||
if (this.GET_GLOBAL('loginMark')) { | |||
return this.GET_GLOBAL('loginMark') | |||
} | |||
const storageData = uni.getStorageSync('loginMark') | |||
if (storageData && storageData !== 'null' && storageData !== 'undefined') { | |||
this.SET_GLOBAL('loginMark', storageData) | |||
return storageData | |||
} | |||
const newLoginMark = this.GUID() | |||
this.SET_GLOBAL('loginMark', newLoginMark) | |||
uni.setStorageSync('loginMark', newLoginMark) | |||
return newLoginMark | |||
}, | |||
// 【内部方法】处理 url,判断是否需要添加后台地址前缀 | |||
handleUrl(url) { | |||
let result = url | |||
if (result.startsWith('http://') || result.startsWith('https://')) { | |||
return result | |||
} | |||
if (!result.startsWith(this.API)) { | |||
result = this.API + result | |||
} | |||
return result | |||
}, | |||
// 【内部方法】HTTP 请求基础方法 | |||
async requestBase(url, data, header, method = 'GET') { | |||
const requestUrl = this.handleUrl(url) | |||
const requestHeader = header || {} | |||
let requestData = { | |||
loginMark: this.getLoginMark(), | |||
token: this.GET_GLOBAL('token') || '' | |||
} | |||
if (data && typeof data === 'object') { | |||
requestData.data = JSON.stringify(data) | |||
} else if (data) { | |||
Object.assign(requestData, { | |||
data | |||
}) | |||
} | |||
return uni.request({ | |||
url: requestUrl, | |||
method, | |||
header: { | |||
'content-type': 'application/x-www-form-urlencoded', | |||
...requestHeader | |||
}, | |||
data: requestData | |||
}) | |||
}, | |||
// 【内部方法】处理网络请求方法的返回结果 | |||
handleResult(err, result, tips) { | |||
// 出现错误,一般是网络连接错误 | |||
if (err || !result) { | |||
uni.hideLoading() | |||
uni.showToast({ | |||
title: '网络请求失败,请检查您的网络连接', | |||
icon: 'none' | |||
}) | |||
return null | |||
} | |||
// 状态码为 410,登录状态失效 | |||
if (result.statusCode === 410 || (result.data && result.data.code === 410)) { | |||
uni.hideLoading() | |||
uni.showToast({ | |||
title: '登录状态无效,正在跳转到登录页…', | |||
icon: 'none' | |||
}) | |||
this.CLEAR_GLOBAL() | |||
uni.reLaunch({ | |||
url: '/pages/login' | |||
}) | |||
return null | |||
uni.hideLoading() | |||
if (tips) { | |||
const errInfo = (result.data && result.data.info) || '(未知原因)' | |||
const errTips = typeof tips === 'string' ? tips : '请求数据时发生错误' | |||
uni.showToast({ | |||
title: `${errTips}: ${errInfo}`, | |||
icon: 'none' | |||
}) | |||
} | |||
return null | |||
} | |||
return result.data.data | |||
}, | |||
// 【即将废弃】请使用 this.CONFIG() 来替代 | |||
config(path) { | |||
return get(globalConfig, path, get(defaultConfig, path)) | |||
}, | |||
// 【即将废弃】请使用 this.SET_PARAM() 来代替 | |||
setPageParam(val) { | |||
this.$store.commit('pageParam', val) | |||
}, | |||
// 【即将废弃】请使用 this.GET_PARAM() 来代替 | |||
getPageParam() { | |||
return this.$store.state.pageParam | |||
} | |||
}, | |||
computed: { | |||
// 请求后台接口的地址 | |||
API() { | |||
return this.$store.state.apiRoot || | |||
this.CONFIG('apiHost')[this.DEV ? this.CONFIG('devApiHostIndex') : this.CONFIG('prodApiHostIndex')] | |||
}, | |||
// 当前页面的路径 | |||
// 举例:登录页为 '/pages/login' | |||
PATH() { | |||
if (!getCurrentPages) { | |||
return '' | |||
} | |||
const pages = getCurrentPages() | |||
return pages ? '/' + pages.slice(-1)[0].route : '' | |||
}, | |||
// 当前是否为开发环境 | |||
DEV() { | |||
return process.env.NODE_ENV === 'development' | |||
}, | |||
// 【仅开发模式】获取当前全局变量 | |||
// 生产环境、正式发行时无效,因为小程序端全局变量会挂载到每个页面,影响性能 | |||
DEV_ONLY_GLOBAL() { | |||
return process.env.NODE_ENV === 'development' && this.$store.state | |||
}, | |||
// 获取当前移动端版本号 (定义在 config.js) | |||
APP_VERSION() { | |||
return this.CONFIG('appVersion') | |||
}, | |||
// 当前运行平台 | |||
// 取值 'alipay'/'weixin'/'dingtalk'/'h5'/'app'/'unknow' | |||
PLATFORM() { | |||
let result = 'unknow' | |||
// #ifdef MP-ALIPAY | |||
// #ifndef MP-DINGTALK | |||
result = 'alipay' | |||
// #endif | |||
// #ifdef MP-DINGTALK | |||
result = 'dingtalk' | |||
// #endif | |||
// #endif | |||
// #ifdef MP-WEIXIN | |||
result = 'weixin' | |||
// #endif | |||
// #ifdef H5 | |||
result = 'h5' | |||
// #endif | |||
// #ifdef APP-VUE | |||
result = 'app' | |||
// #endif | |||
return result | |||
}, | |||
// 获取当前运行平台的中文全称 | |||
// 取值 '支付宝小程序'/'微信小程序'/'钉钉小程序'/'移动 H5 '/'手机 App '/'(未知)' | |||
PLATFORM_TEXT() { | |||
let result = '(未知)' | |||
// #ifdef MP-ALIPAY | |||
// #ifndef MP-DINGTALK | |||
result = '支付宝小程序' | |||
// #endif | |||
// #ifdef MP-DINGTALK | |||
result = '钉钉小程序' | |||
// #endif | |||
// #endif | |||
// #ifdef MP-WEIXIN | |||
result = '微信小程序' | |||
// #endif | |||
// #ifdef H5 | |||
result = '移动 H5 ' | |||
// #endif | |||
// #ifdef APP-VUE | |||
result = '手机 App ' | |||
// #endif | |||
return result | |||
}, | |||
// 【即将废弃】请使用 this.PATH 来代替 | |||
pagePath() { | |||
if (!getCurrentPages) { | |||
return '' | |||
} | |||
const pages = getCurrentPages() | |||
return pages ? '/' + pages.slice(-1)[0].route : '' | |||
}, | |||
// 【即将废弃】请使用 this.API 来代替 | |||
apiRoot() { | |||
return this.$store.state.apiRoot || | |||
this.CONFIG('apiHost')[this.DEV ? this.CONFIG('devApiHostIndex') : this.CONFIG('prodApiHostIndex')] | |||
}, | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
import Vue from 'vue' | |||
import Vuex, { Store } from 'vuex' | |||
import config from '@/config.js' | |||
import defaultConfig from '@/common/config.default.js' | |||
const state = { | |||
apiRoot: null, | |||
loginUser: null, | |||
loginMark: null, | |||
token: null, | |||
company: null, | |||
department: null, | |||
user: null, | |||
dataDictionary: null, | |||
pageParam: null, | |||
jumpParam: null | |||
} | |||
const mutations = { | |||
apiRoot(state, val) { state.apiRoot = val }, | |||
loginUser(state, val) { state.loginUser = val }, | |||
loginMark(state, val) { state.loginMark = val }, | |||
token(state, val) { state.token = val }, | |||
company(state, val) { state.company = val }, | |||
department(state, val) { state.department = val }, | |||
user(state, val) { state.user = val }, | |||
dataDictionary(state, val) { state.dataDictionary = val }, | |||
pageParam(state, val) { state.pageParam = val }, | |||
jumpParam(state, val) { state.jumpParam = val } | |||
} | |||
// 获取用户定义的全局变量,依次注册它们 | |||
const globalVariables = config['globalVariables'] || defaultConfig['globalVariables'] | |||
globalVariables.forEach(t => state[t] = null) | |||
globalVariables.forEach(t => mutations[t] = (state, val) => { state[t] = val }) | |||
mutations.clear = (state) => { | |||
state.loginUser = null | |||
state.token = null | |||
state.company = null | |||
state.department = null | |||
state.user = null | |||
state.dataDictionary = null | |||
state.pageParam = null | |||
state.jumpParam = null | |||
globalVariables.forEach(t => { | |||
state[t] = null | |||
}) | |||
} | |||
Vue.use(Vuex) | |||
export default new Store({ state, mutations }) |
@@ -0,0 +1,170 @@ | |||
<template> | |||
<view> | |||
<!-- 循环按照 formValue 里的行来渲染表格块 --> | |||
<view v-for="(valueItem, valueIndex) in value" :key="valueIndex"> | |||
<!-- 表格内部的组件 只有几种 (layer 类似 select,label 类似 title) --> | |||
<!-- 表格标题 除了第一个,后面的都有删除按钮 --> | |||
<view class="table-item padding-lr"> | |||
<view class="table-item-title">{{ item.title }} (第{{ valueIndex + 1 }}行)</view> | |||
<view v-if="valueIndex !== 0 && edit" @click="tableDelete(valueIndex)" class="table-item-delete text-blue"> | |||
删除 | |||
</view> | |||
</view> | |||
<!-- 循环按照 fieldsData 里面定义的列来渲染组件 --> | |||
<view v-for="tableItem of item.fieldsData" :key="tableItem.id"> | |||
<!-- 标题文字 label --> | |||
<l-input | |||
v-if="tableItem.type === 'label'" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:title="tableItem.name" | |||
disabled | |||
/> | |||
<!-- 文字输入框 input (text) --> | |||
<l-input | |||
v-else-if="tableItem.type === 'input'" | |||
@input="setTableValue(`${valueIndex}.${tableItem.field}`, $event)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:disabled="!edit" | |||
:required="Boolean(tableItem.verify)" | |||
:title="tableItem.name" | |||
/> | |||
<!-- 单选和选择 radio select --> | |||
<l-select | |||
v-else-if="tableItem.type === 'radio' || tableItem.type === 'select'" | |||
@input="setTableValue(`${valueIndex}.${tableItem.field}`, $event)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:required="Boolean(tableItem.verify)" | |||
:range="tableItem.__sourceData__" | |||
:title="tableItem.name" | |||
:disabled="!edit" | |||
/> | |||
<!-- 弹层选择器 layer --> | |||
<l-layer-picker | |||
v-else-if="tableItem.type === 'layer'" | |||
@input="layerSetTableValue(valueIndex, $event, tableItem.__sourceData__)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:required="Boolean(tableItem.verify)" | |||
:readonly="!edit" | |||
:title="tableItem.name" | |||
:source="tableItem.__sourceData__.source" | |||
:layerData="tableItem.__sourceData__.layerData" | |||
/> | |||
<!-- 时间 datetime / time --> | |||
<l-datetime-picker | |||
v-else-if="tableItem.type === 'datetime' && tableItem.datetime === 'datetime'" | |||
@input="setTableValue(`${valueIndex}.${tableItem.field}`, $event)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:required="Boolean(tableItem.verify)" | |||
:disabled="!edit" | |||
:title="tableItem.name" | |||
/> | |||
<!-- 日期 datetime / date --> | |||
<l-date-picker | |||
v-else-if="tableItem.type === 'datetime' && tableItem.datetime !== 'datetime'" | |||
@input="setTableValue(`${valueIndex}.${tableItem.field}`, $event)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:required="Boolean(tableItem.verify)" | |||
:disabled="!edit" | |||
:title="tableItem.name" | |||
/> | |||
<!-- 多选 checkbox --> | |||
<l-checkbox-picker | |||
v-else-if="tableItem.type === 'checkbox'" | |||
@input="setTableValue(`${valueIndex}.${tableItem.field}`, $event)" | |||
:value="getTableValue(`${valueIndex}.${tableItem.field}`)" | |||
:readonly="!edit" | |||
:range="tableItem.__sourceData__" | |||
:required="Boolean(tableItem.verify)" | |||
:title="tableItem.name" | |||
/> | |||
</view> | |||
</view> | |||
<!-- 添加表格按钮 --> | |||
<view | |||
v-if="edit" | |||
@click="tableAdd()" | |||
class="bg-white flex flex-wrap justify-center align-center solid-bottom margin-bottom" | |||
> | |||
<view class="add-btn text-blue padding">+ 添加一行表格</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import get from 'lodash/get' | |||
import set from 'lodash/set' | |||
export default { | |||
name: 'l-customform-table', | |||
props: { | |||
item: { type: Object, required: true }, | |||
value: { type: Array, required: true }, | |||
edit: { type: Boolean, default: true } | |||
}, | |||
methods: { | |||
// 设置表单数据的方法 | |||
setTableValue(path, value) { | |||
const newVal = this.COPY(this.value) | |||
set(newVal, path, value) | |||
this.$emit('input', newVal) | |||
}, | |||
// 获取表单数据的方法 | |||
getTableValue(path) { | |||
return get(this.value, path) | |||
}, | |||
// 弹层,可能一次性设置多个字段 | |||
layerSetTableValue(tableIndex, layerValue, layerInfo) { | |||
const newVal = this.COPY(this.value) | |||
const layerData = layerInfo.layerData || [] | |||
layerData.forEach(({ name, value }) => { | |||
set(newVal, `${tableIndex}.${value}`, layerValue[name] || '') | |||
}) | |||
this.$emit('input', newVal) | |||
}, | |||
// 删除表格行 | |||
tableDelete(tableIndex) { | |||
const newVal = this.value.filter((t, i) => i !== tableIndex) | |||
this.$emit('input', newVal) | |||
}, | |||
// 添加表格行 | |||
tableAdd() { | |||
const newVal = [...this.value, this.COPY(this.item.__defaultItem__)] | |||
this.$emit('input', newVal) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.table-item { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
font-size: 14px; | |||
height: 30px; | |||
.table-item-action { | |||
cursor: pointer; | |||
} | |||
} | |||
.add-btn { | |||
text-align: center; | |||
line-height: 1em; | |||
} | |||
</style> |
@@ -0,0 +1,312 @@ | |||
<template> | |||
<view> | |||
<view | |||
:key="item.id" | |||
v-for="(item, index) of scheme" | |||
:style="{ marginTop: index !== 0 && item.type === 'label' ? '15px' : '0' }" | |||
> | |||
<!-- 标题文字 label --> | |||
<l-title v-if="item.type === 'label'" border>{{ item.title }}</l-title> | |||
<!-- 文字输入框 text --> | |||
<l-input | |||
v-else-if="item.type === 'text'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:disabled="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
/> | |||
<!-- 单选和选择 radio和select --> | |||
<l-select | |||
v-else-if="item.type === 'radio' || item.type === 'select'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:disabled="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:range="item.__sourceData__" | |||
:title="item.title" | |||
/> | |||
<!-- 多选 checkbox --> | |||
<l-checkbox-picker | |||
v-else-if="item.type === 'checkbox'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:readonly="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:range="item.__sourceData__" | |||
:title="item.title" | |||
/> | |||
<!-- 多行文本 textarea --> | |||
<l-textarea | |||
v-else-if="item.type === 'textarea' || item.type === 'texteditor'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:readonly="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
/> | |||
<!-- 时间日期 datetime --> | |||
<l-datetime-picker | |||
v-else-if="item.type === 'datetime' && Number(item.dateformat) === 1" | |||
@input="setValue(item.__valuePath__, $event)" | |||
@change="relChange(item.id, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:disabled="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
/> | |||
<!-- 日期 date --> | |||
<l-date-picker | |||
v-else-if="item.type === 'datetime' && Number(item.dateformat) !== 1" | |||
@input="setValue(item.__valuePath__, $event)" | |||
@change="relChange(item.id, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:required="Boolean(item.verify)" | |||
:disabled="!isEdit(item)" | |||
:title="item.title" | |||
/> | |||
<!-- 日期区间 datetimerange --> | |||
<l-input | |||
v-else-if="item.type === 'datetimerange'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:disabled="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
placeholder="" | |||
/> | |||
<!-- 当前状态 currentInfo:组织结构 organize --> | |||
<l-organize-picker | |||
v-else-if="item.type === 'currentInfo' && item.dataType !== 'time'" | |||
:value="getValue(item.__valuePath__)" | |||
:type="item.dataType" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
readonly | |||
/> | |||
<!-- 当前状态 currentInfo:时间 time --> | |||
<l-input | |||
v-else-if="item.type === 'currentInfo' && item.dataType === 'time'" | |||
:value="getValue(item.__valuePath__)" | |||
:disabled="!isEdit(item)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
disabled | |||
/> | |||
<!-- 编码 encode --> | |||
<l-input | |||
v-else-if="item.type === 'encode'" | |||
:value="getValue(item.__valuePath__)" | |||
:required="Boolean(item.verify)" | |||
:title="item.title" | |||
disabled | |||
/> | |||
<!-- 公司人员结构选单 organize --> | |||
<l-organize-picker | |||
v-else-if="item.type === 'organize'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
@change="relChange(item.id, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:rootId="getValue(item.__relationPath__)" | |||
:type="item.dataType" | |||
:required="Boolean(item.verify)" | |||
:readonly="item.__relationPath__ ? !isEdit(item) || !getValue(item.__relationPath__) : !isEdit(item)" | |||
:title="item.title" | |||
:placeholder="displayPlaceHolder(item)" | |||
/> | |||
<!-- 文件上传 upload --> | |||
<l-upload-file | |||
v-else-if="item.type === 'upload'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:required="Boolean(item.verify)" | |||
:readonly="!isEdit(item)" | |||
:title="item.title" | |||
:number="9" | |||
/> | |||
<!-- HTML内容 html --> | |||
<view v-else-if="item.type === 'html'" class="cu-form-group"> | |||
<view class="bg-white"> | |||
<u-parse :imageProp="{ domain: apiRoot }" :content="CONVERT_HTML(item.title)"></u-parse> | |||
</view> | |||
</view> | |||
<!-- 表格 girdtable --> | |||
<l-customform-table | |||
v-else-if="item.type === 'girdtable'" | |||
@input="setValue(item.__valuePath__, $event)" | |||
:value="getValue(item.__valuePath__)" | |||
:item="item" | |||
:edit="isEdit(item)" | |||
/> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
import get from 'lodash/get' | |||
import set from 'lodash/set' | |||
import moment from 'moment' | |||
export default { | |||
name: 'l-customform', | |||
props: { | |||
scheme: { default: () => [] }, | |||
editMode: { default: true }, | |||
initFormValue: { default: () => ({}) }, | |||
rel: { default: () => ({}) } | |||
}, | |||
data() { | |||
return { | |||
formValue: this.initFormValue | |||
} | |||
}, | |||
methods: { | |||
// 本方法由调用方使用 $refs 的方式调用 | |||
// 给表单设置值 | |||
setFormValue(formValue) { | |||
this.formValue = formValue | |||
}, | |||
// 本方法由调用方使用 $refs 的方式调用 | |||
// 获取表单值 | |||
getFormValue() { | |||
return this.formValue | |||
}, | |||
// 本方法由调用方使用 $refs 的方式调用 | |||
// 依次验证表单项,返回一个所有错误提示的数组,如果为空数组则表示无错误 | |||
verifyValue() { | |||
const errorList = [] | |||
this.scheme | |||
.filter(t => t.verify) | |||
.forEach(schemeItem => { | |||
if (schemeItem.table && schemeItem.field) { | |||
const verifyFunc = this.verify[schemeItem.verify] | |||
const verifyResult = verifyFunc(this.getValue(schemeItem.__valuePath__)) | |||
if (verifyResult !== true) { | |||
errorList.push(`[${schemeItem.title}]: ${verifyResult}`) | |||
} | |||
} else if (schemeItem.fieldsData) { | |||
this.getValue(schemeItem.__valuePath__).forEach((valueItem, valueIndex) => { | |||
schemeItem.fieldsData.forEach(fieldItem => { | |||
const verifyFunc = this.verify[fieldItem.verify] | |||
const verifyResult = verifyFunc( | |||
this.getValue(`${schemeItem.__valuePath__}.${valueIndex}.${fieldItem.field}`) | |||
) | |||
if (verifyResult !== true) { | |||
errorList.push(`[表格${schemeItem.title}第${tableIndex}行${fieldItem.name}列]: ${verifyResult}`) | |||
} | |||
}) | |||
}) | |||
} | |||
}) | |||
return errorList | |||
}, | |||
// 设置表单数据的方法 | |||
setValue(path, value) { | |||
set(this.formValue, path, value) | |||
}, | |||
// 获取表单数据的方法 | |||
getValue(path) { | |||
return get(this.formValue, path) | |||
}, | |||
// 处理互相关联的表单项(organize 级联,datetimerange 日期区间) | |||
relChange(id, val) { | |||
const relList = this.rel[id] || [] | |||
relList.forEach(relItem => { | |||
if (relItem.type === 'organize') { | |||
// 组织结构,重设级联过来的组件 | |||
this.setValue(relItem.path, '') | |||
// 向下递归 | |||
if (this.rel[relItem.id]) { | |||
this.relChange(relItem.id) | |||
} | |||
} else if (relItem.type === 'datetimerange') { | |||
// 时间日期区间,重新计算 | |||
const startTime = this.getValue(relItem.startPath) | |||
const endTime = this.getValue(relItem.endPath) | |||
if (!startTime || !endTime || moment(endTime).isBefore(startTime)) { | |||
return | |||
} | |||
const newVal = moment | |||
.duration(moment(endTime).diff(moment(startTime))) | |||
.asDays() | |||
.toFixed(0) | |||
this.setValue(relItem.path, newVal) | |||
} | |||
}) | |||
}, | |||
// 获取表单项的 placeholder ,在无法编辑时隐藏 | |||
displayPlaceHolder(item) { | |||
if (!this.editMode || item.type !== 'organize') { | |||
return ' ' | |||
} | |||
if (!item.relation) { | |||
return `请选择${item.title || item.name}` | |||
} | |||
const parentType = { user: '部门', department: '公司' }[item.dataType] | |||
const parentValue = this.getValue(item.__relationPath__) | |||
return parentValue ? `请选择${item.title || item.name}` : `请先选择上级${parentType}` | |||
}, | |||
// 获取表单项可否编辑 | |||
isEdit(item) { | |||
return this.editMode && item.edit !== 0 | |||
} | |||
}, | |||
computed: { | |||
verify() { | |||
return { | |||
NotNull: t => (t !== null && t !== undefined && t !== '' && (!Array.isArray(t) || t.length > 0)) || '不能为空', | |||
Num: t => !isNaN(t) || '须输入数值', | |||
NumOrNull: t => t.length <= 0 || !isNaN(t) || '须留空或输入数值', | |||
Email: t => /^[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_]+.[a-zA-Z0-9]+$/.test(t) || '须符合Email格式', | |||
EmailOrNull: t => | |||
t.length <= 0 || /^[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_]+.[a-zA-Z0-9]+$/.test(t) || '须留空或符合Email格式', | |||
EnglishStr: t => /^[a-zA-Z]*$/.test(t) || '须由英文字母组成', | |||
EnglishStrOrNull: t => t.length <= 0 || /^[a-zA-Z]*$/.test(t) || '须留空或由英文字母组成', | |||
Phone: t => /^[+0-9- ]*$/.test(t) || '须符合电话号码格式', | |||
PhoneOrNull: t => t.length <= 0 || /^[+0-9- ]*$/.test(t) || '须留空或符合电话号码格式', | |||
Fax: t => /^[+0-9- ]*$/.test(t) || '须符合传真号码格式', | |||
Mobile: t => /^1[0-9]{10}$/.test(t) || '须符合手机号码格式', | |||
MobileOrPhone: t => /^[+0-9- ]*$/.test(t) || /^1[0-9]{10}$/.test(t) || '须符合电话或手机号码格式', | |||
MobileOrNull: t => t.length <= 0 || /^1[0-9]{10}$/.test(t) || '须留空或符合手机号码格式', | |||
MobileOrPhoneOrNull: t => | |||
t.length <= 0 || /^1[0-9]{10}$/.test(t) || /^[+0-9- ]*$/.test(t) || '须留空或符合手机/电话号码格式', | |||
Uri: t => /^[a-zA-z]+:\/\/[^\s]*$/.test(t) || '须符合网址Url格式', | |||
UriOrNull: t => t.length <= 0 || /^[a-zA-z]+:\/\/[^\s]*$/.test(t) || '须留空或符合网址Url格式' | |||
} | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,62 @@ | |||
<template> | |||
<view class="customlist-banner-action"> | |||
<view | |||
v-if="showDelete" | |||
@click="$emit('delete')" | |||
class="customlist-banner-action-btn line-red text-sm" | |||
style="border: currentColor 1px solid" | |||
> | |||
<l-icon type="delete" /> | |||
删除 | |||
</view> | |||
<view | |||
v-if="showEdit" | |||
@click="$emit('edit')" | |||
class="customlist-banner-action-btn line-blue text-sm" | |||
style="border: currentColor 1px solid" | |||
> | |||
<l-icon type="edit" /> | |||
编辑 | |||
</view> | |||
<view | |||
@click="$emit('view')" | |||
class="customlist-banner-action-btn line-blue text-sm" | |||
style="border: currentColor 1px solid;min-width: 160rpx;" | |||
> | |||
查看 | |||
<l-icon type="right" /> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-customlist-banner', | |||
props: { | |||
showDelete: {}, | |||
showEdit: {} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.customlist-banner-action { | |||
margin-top: 20rpx; | |||
text-align: right; | |||
.customlist-banner-action-btn { | |||
display: inline-block; | |||
padding: 4px 6px; | |||
margin: 0 3px; | |||
border-radius: 3px; | |||
text-align: center; | |||
} | |||
} | |||
</style> | |||
<style lang="less"> | |||
:host { | |||
display: block; | |||
} | |||
</style> |
@@ -0,0 +1,40 @@ | |||
<template> | |||
<!-- #ifdef MP-ALIPAY --> | |||
<view @click="$emit('click', $event)" :class="className" :style="style" class="custom-add"> | |||
<l-icon :type="icon" /> | |||
</view> | |||
<!-- #endif --> | |||
<!-- #ifndef MP-ALIPAY --> | |||
<view @click="$emit('click', $event)" class="custom-add"><l-icon :type="icon" /></view> | |||
<!-- #endif --> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-custom-add', | |||
props: { | |||
icon: { default: 'add' } | |||
} | |||
} | |||
</script> | |||
<style lang="less"> | |||
.custom-add { | |||
position: fixed; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
font-size: 30px; | |||
bottom: 20px; | |||
bottom: calc(20rpx + constant(safe-area-inset-bottom)); | |||
bottom: calc(20rpx + env(safe-area-inset-bottom)); | |||
right: 20px; | |||
height: 40px; | |||
width: 40px; | |||
border-radius: 50%; | |||
background-color: #488aff; | |||
color: #fff; | |||
} | |||
</style> |
@@ -0,0 +1,62 @@ | |||
<template> | |||
<!-- #ifndef MP-ALIPAY --> | |||
<view class="customlist-banner-header"> | |||
<view class="customlist-banner-content"> | |||
<slot></slot> | |||
<view v-if="buttonShow" class="customlist-banner-action"> | |||
<l-icon @click="$emit('buttonClick', $event)" :type="buttonIcon" color="blue" class="text-xxl" /> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-ALIPAY --> | |||
<view :class="className" :style="style" class="customlist-banner-header"> | |||
<view class="customlist-banner-content"> | |||
<slot></slot> | |||
<view v-if="buttonShow" class="customlist-banner-action"> | |||
<l-icon @click="$emit('buttonClick', $event)" :type="buttonIcon" color="blue" class="text-xxl" /> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- #endif --> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-customlist-banner', | |||
props: { | |||
buttonShow: { default: true }, | |||
buttonIcon: { default: 'searchlist' } | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.customlist-banner-header { | |||
position: fixed; | |||
top: var(--window-top); | |||
z-index: 1024; | |||
border-bottom: 1rpx solid #ddd; | |||
height: 80rpx; | |||
width: 100%; | |||
box-shadow: 0 0.5px 3px rgba(0, 0, 0, 0.1); | |||
background: #f1f1f1; | |||
.customlist-banner-content { | |||
color: #8f8f94; | |||
background: #ffffff; | |||
padding: 20rpx 25rpx; | |||
width: 100%; | |||
vertical-align: middle; | |||
border-bottom: 1rpx solid #ddd; | |||
.customlist-banner-action { | |||
position: absolute; | |||
right: 20rpx; | |||
top: 20rpx; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,194 @@ | |||
<template> | |||
<view class="sidepage-datefilter padding-bottom"> | |||
<view class="title">{{ title }}</view> | |||
<l-button | |||
v-if="isDisplay('all')" | |||
@click="changeDateRange('all')" | |||
:line="dateRange !== 'all' ? 'green' : ''" | |||
:color="dateRange === 'all' ? 'green' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
全部 | |||
</l-button> | |||
<l-button | |||
v-if="isDisplay('today')" | |||
@click="changeDateRange('today')" | |||
:line="dateRange !== 'today' ? 'blue' : ''" | |||
:color="dateRange === 'today' ? 'blue' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
今天 | |||
</l-button> | |||
<l-button | |||
v-if="isDisplay('7d')" | |||
@click="changeDateRange('7d')" | |||
:line="dateRange !== '7d' ? 'blue' : ''" | |||
:color="dateRange === '7d' ? 'blue' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
最近7天内 | |||
</l-button> | |||
<l-button | |||
v-if="isDisplay('1m')" | |||
@click="changeDateRange('1m')" | |||
:line="dateRange !== '1m' ? 'blue' : ''" | |||
:color="dateRange === '1m' ? 'blue' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
最近1个月内 | |||
</l-button> | |||
<l-button | |||
v-if="isDisplay('3m')" | |||
@click="changeDateRange('3m')" | |||
:line="dateRange !== '3m' ? 'blue' : ''" | |||
:color="dateRange === '3m' ? 'blue' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
最近3个月内 | |||
</l-button> | |||
<l-button | |||
v-if="isDisplay('custom')" | |||
@click="changeDateRange('custom')" | |||
:line="dateRange !== 'custom' ? 'cyan' : ''" | |||
:color="dateRange === 'custom' ? 'cyan' : ''" | |||
class="block margin-top-sm" | |||
block | |||
> | |||
自定义 | |||
</l-button> | |||
<view v-if="dateRange === 'custom'" class="title">自定义时间区间:</view> | |||
<l-date-picker | |||
v-if="dateRange === 'custom'" | |||
v-model="startDate" | |||
@change="customChange" | |||
title="起始时间" | |||
placeholder="点击来选取" | |||
/> | |||
<l-date-picker | |||
v-if="dateRange === 'custom'" | |||
v-model="endDate" | |||
@change="customChange" | |||
title="结束时间" | |||
placeholder="点击来选取" | |||
/> | |||
</view> | |||
</template> | |||
<script> | |||
import moment from 'moment' | |||
export default { | |||
name: 'l-customlist-sidepage-datefilter', | |||
props: { | |||
title: { default: '按时间日期筛选:' }, | |||
ranges: { default: () => ['all', 'today', '7d', '1m', '3m', 'custom'] }, | |||
value: {} | |||
}, | |||
data() { | |||
return { | |||
dateRange: 'all', | |||
startDate: null, | |||
endDate: null | |||
} | |||
}, | |||
methods: { | |||
// 点击按钮修改快捷选择区间 | |||
changeDateRange(type) { | |||
this.dateRange = type | |||
const { startDate, endDate } = this | |||
const todayEnd = moment().format('YYYY-MM-DD 23:59:59') | |||
if (type === 'all') { | |||
this.$emit('input', null) | |||
this.$emit('change', null) | |||
} else if (type === 'today') { | |||
const e = { | |||
start: moment() | |||
.subtract(1, 'day') | |||
.format('YYYY-MM-DD 00:00:00'), | |||
end: todayEnd | |||
} | |||
this.$emit('input', e) | |||
this.$emit('change', e) | |||
} else if (type === '7d') { | |||
const e = { | |||
start: moment() | |||
.subtract(7, 'days') | |||
.format('YYYY-MM-DD 00:00:00'), | |||
end: todayEnd | |||
} | |||
this.$emit('input', e) | |||
this.$emit('change', e) | |||
} else if (type === '1m') { | |||
const e = { | |||
start: moment() | |||
.subtract(1, 'month') | |||
.format('YYYY-MM-DD 00:00:00'), | |||
end: todayEnd | |||
} | |||
this.$emit('input', e) | |||
this.$emit('change', e) | |||
} else if (type === '3m') { | |||
const e = { | |||
start: moment() | |||
.subtract(3, 'months') | |||
.format('YYYY-MM-DD 00:00:00'), | |||
end: todayEnd | |||
} | |||
this.$emit('input', e) | |||
this.$emit('change', e) | |||
} else if (type === 'custom' && (startDate || startDate)) { | |||
this.customChange() | |||
} | |||
}, | |||
// 自定义区间修改起止 | |||
customChange() { | |||
if (this.dateRange !== 'custom') { | |||
return | |||
} | |||
const { startDate, endDate } = this | |||
const todayEnd = moment().format('YYYY-MM-DD 23:59:59') | |||
if (!(startDate && startDate && moment(startDate).isAfter(endDate))) { | |||
const e = { | |||
start: startDate ? moment(startDate).format('YYYY-MM-DD 00:00:00') : '1900-01-01 00:00:00', | |||
end: endDate ? moment(endDate).format('YYYY-MM-DD 23:59:59') : todayEnd | |||
} | |||
this.$emit('input', e) | |||
this.$emit('change', e) | |||
} else { | |||
this.$emit('input', null) | |||
this.$emit('change', null) | |||
} | |||
}, | |||
// 是否渲染该区间的快捷选择按钮 | |||
isDisplay(type) { | |||
return this.ranges.includes(type) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.sidepage-datefilter { | |||
display: block; | |||
.title { | |||
font-size: 1.2em; | |||
margin-bottom: 20rpx; | |||
margin-top: 40rpx; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,17 @@ | |||
<template> | |||
<view class="customlist"> | |||
<slot></slot> | |||
<view v-if="showTips" class="padding text-gray text-center">{{ tips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-customlist', | |||
props: { | |||
showTips: {}, | |||
tips: { default: '已加载全部项目' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,46 @@ | |||
<template> | |||
<view :class="displayDesc ? 'demo-description-display' : ''" class="demo-description margin-sm padding-sm radius"> | |||
<view v-if="displayDesc" class="demo-description-text text-center"> | |||
<text class="demo-description-content text-left"><slot></slot></text> | |||
</view> | |||
<view v-else @click="displayDesc = true" class="demo-description-tip text-center">{{ tips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'demo-description', | |||
props: { | |||
expand: { default: false }, | |||
tips: { default: '显示说明' } | |||
}, | |||
data() { | |||
return { | |||
displayDesc: this.expand | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.demo-description-display { | |||
background: #eeeeee; | |||
} | |||
.demo-description-content { | |||
display: inline-block; | |||
font-size: 1em; | |||
} | |||
.demo-description-tip { | |||
font-size: 1em; | |||
} | |||
</style> | |||
<style> | |||
:host { | |||
display: block; | |||
} | |||
</style> |
@@ -0,0 +1,85 @@ | |||
<template> | |||
<!-- #ifndef MP-DINGTALK --> | |||
<l-label @click="click" :arrow="!readonly" :required="required" :title="title"> | |||
{{ value || displayPlaceholder }} | |||
</l-label> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-DINGTALK --> | |||
<l-label :arrow="!readonly" :required="required" :title="title"> | |||
<text @click="click">{{ value || displayPlaceholder }}</text> | |||
</l-label> | |||
<!-- #endif --> | |||
</template> | |||
<script> | |||
/** | |||
* 弹层选择器 | |||
* | |||
* 接受两个参数: | |||
* source: 弹层选单源数据 | |||
* layerData: 弹层绑定定义 | |||
* | |||
* 举例: | |||
* // 数据源,这里以数据字典里的公司类别来举例 | |||
* source: this.GET_GLOBAL('dataDictionary').CompanyType | |||
* | |||
* // 需要展示在弹层页面中的字段 | |||
* layerData: [ | |||
* { | |||
* name: 'text', // 对应数据源中的字段 | |||
* label: '公司类别', // 标题文本 | |||
* value: 'F_CompanyName' // 对应要绑定到的字段 | |||
* }, { | |||
* name: 'value', | |||
* label: '对应值', | |||
* value: 'F_CompanyId' | |||
* } | |||
* ] | |||
*/ | |||
export default { | |||
name: 'l-layer-picker', | |||
props: { | |||
value: { default: null }, | |||
title: {}, | |||
required: {}, | |||
placeholder: {}, | |||
readonly: {}, | |||
source: { default: () => [] }, | |||
layerData: { default: () => [] } | |||
}, | |||
methods: { | |||
click() { | |||
if (this.readonly) { | |||
return | |||
} | |||
const { source, layerData } = this | |||
this.ONCE(`select-layer`, data => { | |||
this.$emit('input', data) | |||
this.$emit('change', data) | |||
}) | |||
this.SET_PARAM({ source, layerData }) | |||
this.NAV_TO('/pages/common/select-layer') | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.readonly) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,75 @@ | |||
<template> | |||
<!-- #ifdef MP-DINGTALK --> | |||
<l-label :arrow="!readonly" :required="required" :title="title"> | |||
<text @click="click">{{ display || displayPlaceholder }}</text> | |||
</l-label> | |||
<!-- #endif --> | |||
<!-- #ifndef MP-DINGTALK --> | |||
<l-label @click="click" :arrow="!readonly" :required="required" :title="title"> | |||
{{ display || displayPlaceholder }} | |||
</l-label> | |||
<!-- #endif --> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-organize-picker', | |||
props: { | |||
value: { default: '' }, | |||
type: {}, | |||
title: {}, | |||
required: {}, | |||
placeholder: {}, | |||
readonly: {}, | |||
rootId: {}, | |||
rootType: {} | |||
}, | |||
methods: { | |||
click() { | |||
if (this.readonly) { | |||
return | |||
} | |||
this.ONCE('select-organize', data => { | |||
this.$emit('input', data.id) | |||
this.$emit('change', data.id) | |||
}) | |||
const rootType = this.rootType || { user: 'department', department: 'company', company: 'company' }[this.type] | |||
const rootId = this.rootId | |||
const url = rootId | |||
? `/pages/common/select-organize?type=${this.type}&rootId=${rootId}&rootType=${rootType}` | |||
: `/pages/common/select-organize?type=${this.type}` | |||
this.NAV_TO(url) | |||
} | |||
}, | |||
computed: { | |||
display() { | |||
if (!this.value) { | |||
return this.placeholder | |||
} | |||
const orgItem = this.GET_GLOBAL(this.type)[this.value] | |||
return orgItem ? orgItem.name : this.placeholder | |||
}, | |||
displayPlaceholder() { | |||
if (this.readonly) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,90 @@ | |||
<template> | |||
<view @click="click" class="tree-item"> | |||
<!-- 用户头像 --> | |||
<image | |||
v-if="item.type === 'user'" | |||
class="tree-item-avatar" | |||
:src="avatar" | |||
:style="{ borderRadius: roundAvatar ? '50%' : '3px' }" | |||
mode="aspectFill" | |||
></image> | |||
<!-- 名称 --> | |||
<text class="tree-item-title">{{ item.name }}</text> | |||
<!-- 用户标签 --> | |||
<l-tag :line="tagColor" size="sm" class="margin-left-sm">{{ tagName }}</l-tag> | |||
<!-- 如果开启选择按钮显示,并且级别符合,则显示按钮 --> | |||
<view v-if="button" class="tree-item-action"> | |||
<l-button @click="$emit('buttonClick', item)" line="green" size="sm">选择</l-button> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-organize-single-item', | |||
props: { | |||
item: {}, | |||
button: {} | |||
}, | |||
methods: { | |||
// 点击事件 | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
}, | |||
computed: { | |||
// 获取用户头像 | |||
avatar() { | |||
if (!Number.isNaN(this.item.img)) { | |||
return Number(this.item.img) === 1 ? '/static/img-avatar/chat-boy.jpg' : '/static/img-avatar/chat-girl.jpg' | |||
} | |||
return this.item.img | |||
}, | |||
// 获取树形列表每一项后面 tag 的显示 | |||
tagName() { | |||
return { user: '职员', department: '部门', company: '公司' }[this.item.type] | |||
}, | |||
// 获取 tag 的颜色 | |||
tagColor(item) { | |||
return { company: 'red', department: 'blue', user: 'green' }[this.item.type] | |||
}, | |||
// 头像圆形/方形显示参数 | |||
roundAvatar() { | |||
return this.CONFIG('pageConfig.roundAvatar') | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.tree-item { | |||
background-color: #fff; | |||
display: flex; | |||
min-height: 65rpx; | |||
align-items: center; | |||
.tree-item-avatar { | |||
width: 60rpx; | |||
height: 60rpx; | |||
margin-top: 8rpx; | |||
margin-bottom: 8rpx; | |||
margin-left: 30rpx; | |||
margin-right: 16rpx; | |||
} | |||
.tree-item-action { | |||
position: absolute; | |||
right: 30rpx; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,231 @@ | |||
<template> | |||
<view class="tree-item"> | |||
<!-- 全局根元素时隐藏 --> | |||
<view v-if="root.id !== '0'" @click="selfClick" class="tree-item-self"> | |||
<!-- 已确认子列表为空则显示横杠图标;加载中则显示 loading 图标;否则显示展开/收起图标 --> | |||
<l-icon v-if="isEmpty" type="move" class="tree-item-icon" /> | |||
<l-icon | |||
v-else-if="root.type !== 'user' && !isLoading" | |||
:type="isOpen ? 'unfold' : 'right'" | |||
class="tree-item-icon" | |||
/> | |||
<view v-else-if="root.type !== 'user' && isLoading" class="loading-fix cu-load loading tree-item-icon"></view> | |||
<!-- 是职员,则显示头像 --> | |||
<image | |||
v-else | |||
class="tree-item-avatar" | |||
:src="avatar" | |||
:style="{ borderRadius: roundAvatar ? '50%' : '3px' }" | |||
mode="aspectFill" | |||
></image> | |||
<!-- 名称 --> | |||
<text class="tree-item-title">{{ name }}</text> | |||
<!-- 非用户,显示后置标题 --> | |||
<l-tag v-if="root.type !== 'user' || staffTag" :line="tagColor" size="sm" class="margin-left-sm"> | |||
{{ tagName }} | |||
</l-tag> | |||
<!-- 开启按钮显示且级别对应,则显示按钮 --> | |||
<view v-if="button && root.type === level" class="tree-item-action"> | |||
<l-button @click="$emit('buttonClick', root)" line="green" size="sm">选择</l-button> | |||
</view> | |||
</view> | |||
<!-- 子节点列表;非用户节点且已加载完毕时显示 --> | |||
<view | |||
v-if="root.type !== 'user' && isLoadOk" | |||
v-show="isOpen" | |||
:style="{ paddingLeft: root.id === '0' ? '0' : '25rpx' }" | |||
class="tree-item-children" | |||
> | |||
<l-organize-tree | |||
v-for="child of children" | |||
@userClick="$emit('userClick', $event)" | |||
@buttonClick="$emit('buttonClick', $event)" | |||
:key="child.id" | |||
:open="root.id === '0'" | |||
:root="child" | |||
:button="button" | |||
:level="level" | |||
/> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-organize-tree', | |||
props: { | |||
root: { default: () => ({ type: 'company', id: '0' }) }, | |||
level: { default: 'user' }, | |||
button: {}, | |||
open: { default: true } | |||
}, | |||
data() { | |||
return { | |||
isOpen: this.open, | |||
isLoading: false, | |||
isLoadOk: false, | |||
isEmpty: false, | |||
children: [] | |||
} | |||
}, | |||
async created() { | |||
await this.init() | |||
}, | |||
methods: { | |||
// 组件被创建 | |||
async init() { | |||
// 如果自己是根节点,则创建时直接加载子节点 | |||
if (this.open) { | |||
await this.loadChildren() | |||
} | |||
}, | |||
// 自身被点击,如果不是用户节点则收起/展开 (如果没有加载子节点则还需加载),否则触发点击用户的事件 | |||
selfClick() { | |||
if (this.isLoading) { | |||
return | |||
} | |||
if (this.root.type === 'user') { | |||
this.$emit('userClick', this.root) | |||
return | |||
} | |||
if (this.isLoadOk) { | |||
this.isOpen = !this.isOpen | |||
return | |||
} | |||
this.loadChildren() | |||
}, | |||
// 加载子节点 | |||
async loadChildren() { | |||
this.isLoading = true | |||
let children = [] | |||
// 只有公司/根节点能加载子公司 | |||
if (this.root.type === 'company') { | |||
children = children.concat( | |||
Object.entries(this.GET_GLOBAL('company')) | |||
.filter(([id, item]) => item.parentId === this.root.id) | |||
.map(([id, item]) => ({ ...item, id, type: 'company' })) | |||
) | |||
} | |||
// 加载子部门 | |||
if (this.level === 'department' || this.level === 'user') { | |||
children = children.concat( | |||
Object.entries(this.GET_GLOBAL('department')) | |||
.filter(([id, item]) => item.companyId === this.root.id && Number(item.parentId) !== -1) | |||
.map(([id, item]) => ({ ...item, id, type: 'department' })) | |||
) | |||
} | |||
// 加载子职员 | |||
if (this.level === 'user') { | |||
children = children.concat( | |||
Object.entries(this.GET_GLOBAL('user')) | |||
.filter( | |||
([id, item]) => | |||
this.root.id === | |||
(item.departmentId && Number(item.departmentId) !== 0 ? item.departmentId : item.companyId) | |||
) | |||
.map(([id, item]) => ({ ...item, id, type: 'user' })) | |||
) | |||
} | |||
this.children = children | |||
this.isLoadOk = true | |||
this.isOpen = true | |||
this.isLoading = false | |||
} | |||
}, | |||
computed: { | |||
// 获取用户头像 | |||
avatar() { | |||
if (!Number.isNaN(this.root.img)) { | |||
return Number(this.root.img) === 1 ? '/static/img-avatar/chat-boy.jpg' : '/static/img-avatar/chat-girl.jpg' | |||
} | |||
return this.root.img | |||
}, | |||
// 是否在职员一级显示标签 | |||
staffTag() { | |||
return this.CONFIG('pageConfig.contact.staffTag') | |||
}, | |||
// 获取树形列表每一项后面 tag 的显示 | |||
tagName() { | |||
return { user: '职员', department: '部门', company: '公司' }[this.root.type] | |||
}, | |||
// 获取 tag 的颜色 | |||
tagColor() { | |||
return { company: 'red', department: 'blue', user: 'green' }[this.root.type] | |||
}, | |||
// 节点名称 | |||
name() { | |||
if (this.root.name) { | |||
return this.root.name | |||
} | |||
const rootItem = this.GET_GLOBAL(this.root.type)[this.root.id] | |||
return rootItem ? rootItem.name : '(根节点)' | |||
}, | |||
// 头像圆形/方形显示参数 | |||
roundAvatar() { | |||
return this.CONFIG('pageConfig.roundAvatar') | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
:host { | |||
display: block; | |||
} | |||
.tree-item { | |||
background-color: #fff; | |||
.tree-item-self { | |||
display: flex; | |||
min-height: 65rpx; | |||
align-items: center; | |||
.tree-item-icon { | |||
margin: 0 30rpx; | |||
line-height: 1em; | |||
} | |||
.tree-item-avatar { | |||
width: 60rpx; | |||
height: 60rpx; | |||
margin-top: 8rpx; | |||
margin-bottom: 8rpx; | |||
margin-left: 30rpx; | |||
margin-right: 16rpx; | |||
} | |||
.tree-item-action { | |||
position: absolute; | |||
right: 30rpx; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,86 @@ | |||
<template> | |||
<scroll-view | |||
@scrolltolower="$emit('toBottom')" | |||
@refresherrefresh="onRefresh" | |||
@refresherrestore="onRefreshRestore" | |||
@scroll="scroll" | |||
:scroll-top="scrollTop" | |||
:refresher-triggered="pullDown" | |||
:refresher-enabled="enablePullDown" | |||
:refresher-background="refresherBackground" | |||
:refresher-threshold="80" | |||
class="lr-scroll-list" | |||
style="height: 100%; display: block;" | |||
scroll-anchoring | |||
scroll-y | |||
> | |||
<slot></slot> | |||
</scroll-view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-scroll-list', | |||
props: { | |||
refresherBackground: { default: '#f3f3f3' } | |||
}, | |||
data() { | |||
return { | |||
scrollTop: 0, | |||
pullDown: true, | |||
enablePullDown: true | |||
} | |||
}, | |||
methods: { | |||
// 【外部调用的方法】,滚动到某个高度 | |||
scrollTo(num) { | |||
this.$nextTick(() => { | |||
this.scrollTop = num | |||
}) | |||
}, | |||
// 【外部调用的方法】,结束下拉刷新 | |||
stopPullDown() { | |||
this.pullDown = false | |||
this.isPullDown = false | |||
}, | |||
// 下拉刷新触发 | |||
onRefresh() { | |||
if (this.isPullDown) { | |||
return | |||
} | |||
this.isPullDown = true | |||
this.pullDown = true | |||
this.$emit('pullDown') | |||
}, | |||
// 下拉恢复可用 | |||
onRefreshRestore() { | |||
this.pullDown = false | |||
this.isPullDown = false | |||
}, | |||
// 列表滚动时 | |||
scroll(e) { | |||
// 修复 App 和 H5 端的滚动过快 | |||
// #ifdef APP-VUE || H5 | |||
this.enablePullDown = e.detail.scrollTop <= 0 | |||
// #endif | |||
this.$emit('scroll', e) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less"> | |||
:host { | |||
display: block; | |||
height: 100%; | |||
} | |||
</style> |
@@ -0,0 +1,155 @@ | |||
<template> | |||
<view> | |||
<view class="cu-form-group" style="border-bottom: none; padding-bottom: 0;"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title || '' }} | |||
</view> | |||
</view> | |||
<view class="cu-form-group" style="border-top: none;"> | |||
<view class="grid col-4 grid-square flex-sub"> | |||
<view v-for="(file, index) in value" :key="index" class="bg-img"> | |||
<image | |||
v-if="isImgFile(file.type)" | |||
@click="fileClick(index)" | |||
:src="file.path" | |||
:webp="file.type === 'webp'" | |||
mode="aspectFill" | |||
></image> | |||
<view v-else-if="isDocFile(file.type)" @click="fileClick(index)" class="file-icon solids"> | |||
<l-icon type="text" /> | |||
</view> | |||
<view v-else class="file-icon solids"><l-icon type="text" /></view> | |||
<view v-if="!readonly" @click.stop="delFile(index)" class="cu-tag bg-red" style="height: 24px; width: 24px;"> | |||
<l-icon type="close" color="white" style="width: 18px; height: 24px; font-size: 24px;" /> | |||
</view> | |||
</view> | |||
<view v-if="!readonly && value.length < Number(number)" @click="chooseFile" class="solids"> | |||
<l-icon type="file" /> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-upload-file', | |||
props: { | |||
number: { default: 1 }, | |||
readonly: {}, | |||
value: { default: () => [] }, | |||
title: {}, | |||
required: {} | |||
}, | |||
methods: { | |||
getFileExt(path) { | |||
return /\.(\w{2,5})$/.exec(path)[1] || null | |||
}, | |||
isImgFile(type) { | |||
const typeString = (type || '').toLowerCase() | |||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'image'].includes(typeString) | |||
}, | |||
isDocFile(type) { | |||
const typeString = (type || '').toLowerCase() | |||
return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'].includes(typeString) | |||
}, | |||
delFile(index) { | |||
const newList = JSON.parse(JSON.stringify(this.value)) | |||
newList.splice(index, 1) | |||
this.$emit('input', newList) | |||
this.$emit('change') | |||
this.$emit('del') | |||
}, | |||
chooseFile() { | |||
// #ifdef MP-DINGTALK | |||
dd.chooseImage({ | |||
count: Number(this.number), | |||
success: ({ filePaths }) => { | |||
if (filePaths) { | |||
const newList = JSON.parse(JSON.stringify(this.value || [])).concat( | |||
filePaths.map(t => ({ path: t, type: this.getFileExt(t) })) | |||
) | |||
this.$emit('input', newList) | |||
this.$emit('change', newList) | |||
this.$emit('add') | |||
} | |||
} | |||
}) | |||
// #endif | |||
// #ifndef MP-DINGTALK | |||
uni.chooseImage({ | |||
count: Number(this.number), | |||
sizeType: ['original', 'compressed'], | |||
sourceType: ['album', 'camera'], | |||
success: ({ tempFilePaths }) => { | |||
const newList = JSON.parse(JSON.stringify(this.value || [])).concat( | |||
tempFilePaths.map(t => ({ path: t, type: this.getFileExt(t) })) | |||
) | |||
this.$emit('input', newList) | |||
this.$emit('change', newList) | |||
this.$emit('add') | |||
} | |||
}) | |||
// #endif | |||
}, | |||
async fileClick(index) { | |||
const { path, type, uid, size = 0 } = this.value[index] | |||
if (this.isImgFile(type)) { | |||
uni.previewImage({ urls: [path], current: path }) | |||
} else if (this.isDocFile(type)) { | |||
// #ifndef H5 || MP-DINGTALK | |||
if (size >= 50 * 1024 * 1024) { | |||
this.TOAST('小程序端无法下载超过50MB的文件,此文件大小为${size}KB,超过限制') | |||
return | |||
} | |||
// #endif | |||
// #ifndef MP-DINGTALK | |||
const tempFilePath = await this.HTTP_DOWNLOAD(uid) | |||
uni.openDocument({ filePath: tempFilePath, fileType: type }) | |||
// #endif | |||
// #ifdef MP-DINGTALK | |||
this.TOAST('钉钉小程序只支持查看图片文件') | |||
// #endif | |||
} else { | |||
// #ifndef MP-DINGTALK | |||
this.TOAST('小程序端只支持打开图片和文档(word、pdf等)文件') | |||
// #endif | |||
// #ifdef MP-DINGTALK | |||
this.TOAST('钉钉小程序只支持查看图片文件') | |||
// #endif | |||
// #ifdef APP-VUE | |||
const tempFilePath = await this.HTTP_DOWNLOAD(uid) | |||
uni.openDocument({ filePath: tempFilePath, fileType: type }) | |||
// #endif | |||
// #ifdef H5 | |||
await this.HTTP_DOWNLOAD(uid) | |||
// #endif | |||
} | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.file-icon { | |||
line-height: 100%; | |||
position: static; | |||
} | |||
</style> |
@@ -0,0 +1,131 @@ | |||
<template> | |||
<view class="wf-action lr-hidden"> | |||
<!-- 催办/撤销,适用于打开我的任务 --> | |||
<view v-if="canUrge || canRevoke" class="form-action padding bg-white margin-top lr-hidden" style="padding-top: 0;"> | |||
<l-button v-if="canUrge" @click="$emit('action', 'urge')" size="lg" color="orange" class="block margin-top" block> | |||
催办审核 | |||
</l-button> | |||
<l-button | |||
v-if="canRevoke" | |||
@click="$emit('action', 'revoke')" | |||
size="lg" | |||
color="red" | |||
class="block margin-top" | |||
block | |||
> | |||
撤销流程 | |||
</l-button> | |||
</view> | |||
<!-- 审批按钮列表,适用于打开待办任务 --> | |||
<view | |||
v-if="buttonList.length > 0" | |||
class="form-action padding bg-white margin-top lr-hidden" | |||
style="padding-top: 0;" | |||
> | |||
<l-button | |||
v-for="button of buttonList" | |||
@click="taskAction(button)" | |||
:key="button.id" | |||
:color="button.color" | |||
class="block margin-top" | |||
size="lg" | |||
block | |||
> | |||
{{ button.name }} | |||
</l-button> | |||
</view> | |||
<!-- 子流程草稿/提交按钮 --> | |||
<view | |||
v-if="['child', 'create', 'again'].includes(type)" | |||
class="form-action padding bg-white margin-top lr-hidden" | |||
style="padding-top: 0;" | |||
> | |||
<l-button @click="$emit('action', 'draft')" size="lg" color="orange" class="block margin-top" block> | |||
保存草稿 | |||
</l-button> | |||
<l-button @click="$emit('action', 'submit')" size="lg" color="green" class="block margin-top" block> | |||
提交流程 | |||
</l-button> | |||
</view> | |||
<!-- 已阅按钮,传阅模式使用 --> | |||
<view v-if="type === 'refer'" class="form-action padding bg-white margin-top lr-hidden" style="padding-top: 0;"> | |||
<l-button @click="$emit('action', 'refer')" color="blue" class="block margin-top" size="lg" block> | |||
标记已阅 | |||
</l-button> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-workflow-action', | |||
props: { | |||
currentNode: { default: () => ({}) }, | |||
currentTask: { default: () => ({}) }, | |||
type: {} | |||
}, | |||
computed: { | |||
// 按钮列表(通过 isSign 判断可否加签;如果可以加签,则额外添加一个加签按钮) | |||
buttonList() { | |||
if (this.openFrom !== 'pre') { | |||
return [] | |||
} | |||
const btnList = (this.currentNode.btnList || []).filter(t => Number(t.isHide) !== 1) | |||
if (this.canMultipleSign) { | |||
btnList.push({ | |||
id: '__sign__', | |||
code: '__sign__', | |||
name: '加签' | |||
}) | |||
} | |||
return btnList.map(t => ({ ...t, color: { agree: 'green', disagree: 'red', end: 'red' }[t.code] || 'blue' })) | |||
}, | |||
// 获取表单页是从任务列表哪一栏打开,my=我的,pre=待办,maked=已办 | |||
openFrom() { | |||
return this.currentTask.mark | |||
}, | |||
// 是否显示催办按钮(仅限我的任务,满足以下条件则可以催办:已开始、未结束、未作废) | |||
canUrge() { | |||
if (this.openFrom !== 'my') { | |||
return false | |||
} | |||
return !this.currentTask.F_IsFinished && this.currentTask.F_EnabledMark !== 3 | |||
}, | |||
// 是否显示撤销按钮(仅限我的任务,满足以下条件则可以撤销:未开始) | |||
canRevoke() { | |||
if (this.openFrom !== 'my') { | |||
return false | |||
} | |||
return !this.currentTask.F_IsStart | |||
}, | |||
// 是否允许加签(仅限待办任务,流程中配置允许加签则) | |||
canMultipleSign() { | |||
if (this.openFrom !== 'pre') { | |||
return false | |||
} | |||
return Number(this.currentNode.isSign) === 1 | |||
} | |||
}, | |||
methods: { | |||
// 点击审批按钮 | |||
taskAction(buttonItem) { | |||
this.$emit('audit', buttonItem) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,44 @@ | |||
<template> | |||
<view class="progress"> | |||
<l-timeline title="当前"> | |||
<l-timeline-item v-if="currentTask && currentTask.F_IsFinished" contentStyle="padding:10px" color="blue"> | |||
<text class="text-blod">结束</text> | |||
</l-timeline-item> | |||
<l-timeline-item | |||
v-for="processItem of processList" | |||
:key="processItem.F_Id" | |||
contentStyle="padding:10px 13px" | |||
color="grey" | |||
> | |||
<view | |||
v-if="processItem.F_NodeName" | |||
style="font-size: 1.2em;padding-bottom:2px;margin-bottom: 6px;border-bottom: solid 1px #fff;" | |||
> | |||
{{ processItem.F_NodeName }} | |||
</view> | |||
<view> | |||
<text class="text-bold">{{ processItem.F_CreateUserName || '「系统」' }}</text> | |||
: {{ processItem.F_OperationName }} | |||
</view> | |||
<view v-if="processItem.F_Des"> | |||
<text class="text-bold">审批意见</text> | |||
: {{ processItem.F_Des }} | |||
</view> | |||
<view style="font-size: 0.8em;margin-top:3px">{{ processItem.F_CreateDate }}</view> | |||
</l-timeline-item> | |||
<l-timeline-item contentStyle="padding:10px" color="green"><text class="text-blod">起步</text></l-timeline-item> | |||
</l-timeline> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-workflow-timeline', | |||
props: { | |||
processList: { default: () => [] } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,20 @@ | |||
const colors = [ | |||
{ title: '嫣红', name: 'red', color: '#e54d42' }, | |||
{ title: '桔橙', name: 'orange', color: '#f37b1d' }, | |||
{ title: '明黄', name: 'yellow', color: '#fbbd08' }, | |||
{ title: '橄榄', name: 'olive', color: '#8dc63f' }, | |||
{ title: '森绿', name: 'green', color: '#39b54a' }, | |||
{ title: '天青', name: 'cyan', color: '#1cbbb4' }, | |||
{ title: '海蓝', name: 'blue', color: '#0081ff' }, | |||
{ title: '姹紫', name: 'purple', color: '#6739b6' }, | |||
{ title: '木槿', name: 'mauve', color: '#9c26b0' }, | |||
{ title: '桃粉', name: 'pink', color: '#e03997' }, | |||
{ title: '棕褐', name: 'brown', color: '#a5673f' }, | |||
{ title: '玄灰', name: 'grey', color: '#8799a3' }, | |||
{ title: '草灰', name: 'gray', color: '#aaaaaa' }, | |||
{ title: '墨黑', name: 'black', color: '#333333' }, | |||
{ title: '雅白', name: 'white', color: '#ffffff' } | |||
] | |||
export default colors | |||
export { colors } |
@@ -0,0 +1,302 @@ | |||
const icons = [ | |||
'favor_light', | |||
'appreciate', | |||
'check', | |||
'close', | |||
'edit', | |||
'emoji', | |||
'favorfill', | |||
'favor', | |||
'loading', | |||
'locationfill', | |||
'location', | |||
'phone', | |||
'roundcheckfill', | |||
'roundcheck', | |||
'roundclosefill', | |||
'roundclose', | |||
'roundrightfill', | |||
'roundright', | |||
'search', | |||
'taxi', | |||
'timefill', | |||
'time', | |||
'unfold', | |||
'warnfill', | |||
'warn', | |||
'camerafill', | |||
'camera', | |||
'commentfill', | |||
'comment', | |||
'likefill', | |||
'like', | |||
'notificationfill', | |||
'notification', | |||
'order', | |||
'samefill', | |||
'same', | |||
'deliver', | |||
'evaluate', | |||
'pay', | |||
'send', | |||
'shop', | |||
'ticket', | |||
'back', | |||
'cascades', | |||
'discover', | |||
'list', | |||
'more', | |||
'scan', | |||
'settings', | |||
'questionfill', | |||
'question', | |||
'shopfill', | |||
'form', | |||
'pic', | |||
'filter', | |||
'footprint', | |||
'top', | |||
'pulldown', | |||
'pullup', | |||
'right', | |||
'refresh', | |||
'moreandroid', | |||
'deletefill', | |||
'refund', | |||
'cart', | |||
'qrcode', | |||
'remind', | |||
'delete', | |||
'profile', | |||
'home', | |||
'cartfill', | |||
'discoverfill', | |||
'homefill', | |||
'message', | |||
'addressbook', | |||
'link', | |||
'lock', | |||
'unlock', | |||
'vip', | |||
'weibo', | |||
'activity', | |||
'friendaddfill', | |||
'friendadd', | |||
'friendfamous', | |||
'friend', | |||
'goods', | |||
'selection', | |||
'explore', | |||
'present', | |||
'squarecheckfill', | |||
'square', | |||
'squarecheck', | |||
'round', | |||
'roundaddfill', | |||
'roundadd', | |||
'add', | |||
'notificationforbidfill', | |||
'explorefill', | |||
'fold', | |||
'game', | |||
'redpacket', | |||
'selectionfill', | |||
'similar', | |||
'appreciatefill', | |||
'infofill', | |||
'info', | |||
'forwardfill', | |||
'forward', | |||
'rechargefill', | |||
'recharge', | |||
'vipcard', | |||
'voice', | |||
'voicefill', | |||
'friendfavor', | |||
'wifi', | |||
'share', | |||
'wefill', | |||
'we', | |||
'lightauto', | |||
'lightforbid', | |||
'lightfill', | |||
'camerarotate', | |||
'light', | |||
'barcode', | |||
'flashlightclose', | |||
'flashlightopen', | |||
'searchlist', | |||
'service', | |||
'sort', | |||
'down', | |||
'mobile', | |||
'mobilefill', | |||
'copy', | |||
'countdownfill', | |||
'countdown', | |||
'noticefill', | |||
'notice', | |||
'upstagefill', | |||
'upstage', | |||
'babyfill', | |||
'baby', | |||
'brandfill', | |||
'brand', | |||
'choicenessfill', | |||
'choiceness', | |||
'clothesfill', | |||
'clothes', | |||
'creativefill', | |||
'creative', | |||
'female', | |||
'keyboard', | |||
'male', | |||
'newfill', | |||
'new', | |||
'pullleft', | |||
'pullright', | |||
'rankfill', | |||
'rank', | |||
'bad', | |||
'cameraadd', | |||
'focus', | |||
'friendfill', | |||
'cameraaddfill', | |||
'apps', | |||
'paintfill', | |||
'paint', | |||
'picfill', | |||
'refresharrow', | |||
'colorlens', | |||
'markfill', | |||
'mark', | |||
'presentfill', | |||
'repeal', | |||
'album', | |||
'peoplefill', | |||
'people', | |||
'servicefill', | |||
'repair', | |||
'file', | |||
'repairfill', | |||
'taoxiaopu', | |||
'weixin', | |||
'attentionfill', | |||
'attention', | |||
'commandfill', | |||
'command', | |||
'communityfill', | |||
'community', | |||
'read', | |||
'calendar', | |||
'cut', | |||
'magic', | |||
'backwardfill', | |||
'playfill', | |||
'stop', | |||
'tagfill', | |||
'tag', | |||
'group', | |||
'all', | |||
'backdelete', | |||
'hotfill', | |||
'hot', | |||
'post', | |||
'radiobox', | |||
'rounddown', | |||
'upload', | |||
'writefill', | |||
'write', | |||
'radioboxfill', | |||
'punch', | |||
'shake', | |||
'move', | |||
'safe', | |||
'activityfill', | |||
'crownfill', | |||
'crown', | |||
'goodsfill', | |||
'messagefill', | |||
'profilefill', | |||
'sound', | |||
'sponsorfill', | |||
'sponsor', | |||
'upblock', | |||
'weblock', | |||
'weunblock', | |||
'my', | |||
'myfill', | |||
'emojifill', | |||
'emojiflashfill', | |||
'flashbuyfill', | |||
'text', | |||
'goodsfavor', | |||
'musicfill', | |||
'musicforbidfill', | |||
'card', | |||
'triangledownfill', | |||
'triangleupfill', | |||
'roundleftfill-copy', | |||
'font', | |||
'title', | |||
'recordfill', | |||
'record', | |||
'cardboardfill', | |||
'cardboard', | |||
'formfill', | |||
'coin', | |||
'cardboardforbid', | |||
'circlefill', | |||
'circle', | |||
'attentionforbid', | |||
'attentionforbidfill', | |||
'attentionfavorfill', | |||
'attentionfavor', | |||
'titles', | |||
'icloading', | |||
'full', | |||
'mail', | |||
'peoplelist', | |||
'goodsnewfill', | |||
'goodsnew', | |||
'medalfill', | |||
'medal', | |||
'newsfill', | |||
'newshotfill', | |||
'newshot', | |||
'news', | |||
'videofill', | |||
'video', | |||
'exit', | |||
'skinfill', | |||
'skin', | |||
'moneybagfill', | |||
'usefullfill', | |||
'usefull', | |||
'moneybag', | |||
'redpacket_fill', | |||
'subscription', | |||
'loading1', | |||
'github', | |||
'global', | |||
'settingsfill', | |||
'back_android', | |||
'expressman', | |||
'evaluate_fill', | |||
'group_fill', | |||
'play_forward_fill', | |||
'deliver_fill', | |||
'notice_forbid_fill', | |||
'fork', | |||
'pick', | |||
'wenzi', | |||
'ellipse', | |||
'qr_code', | |||
'dianhua', | |||
'cuIcon', | |||
'loading2', | |||
'btn' | |||
] | |||
export default icons | |||
export { icons } |
@@ -0,0 +1,77 @@ | |||
/* | |||
Learun-MPUI | |||
力软框架小程序 UI 库 (uni-app 专用) | |||
在项目中使用本 UI 库,需要以下三个步骤: | |||
1、将本项目文件夹放置在 /components/ 内; | |||
2、在 /main.js 中添加以下内容,以导入 mixins 和 CSS: | |||
import '@/components/learun-mpui' | |||
3、在 /pages.json 中添加以下内容,以引入组件: | |||
"easycom": { | |||
"custom": { | |||
// #ifdef MP-ALIPAY | |||
"l-(.+)": "@/components/learun-ui-ali/$1.vue", | |||
// #endif | |||
// #ifndef MP-ALIPAY | |||
"l-(.*)": "@/components/learun-ui-wx/$1.vue", | |||
// #endif | |||
"-": "-" | |||
} | |||
} | |||
*/ | |||
import Vue from 'vue' | |||
import uiIcons from './icons.js' | |||
import uiColors from './colors.js' | |||
// 现在 CSS 必须在 <style> 中引入,参考:https://ask.dcloud.net.cn/question/86907 | |||
// 等待 uni-app 后续升级后可以解除下行代码的注释 | |||
// import './styles/index.css' | |||
const mixins = { | |||
props: { | |||
// #ifdef MP-ALIPAY | |||
style: { default: '' }, | |||
className: { default: '' }, | |||
// #endif | |||
}, | |||
methods: { | |||
getStyle(style) { | |||
const styleHelper = t => { | |||
if (!t) { return '' } | |||
return typeof t === 'object' ? | |||
Object.entries(t).map(([k, v]) => v ? `${k.replace(/([A-Z])/g,"-$1").toLowerCase()}: ${v}; ` : '') : | |||
'' + t | |||
} | |||
return styleHelper(this.style) + styleHelper(style) | |||
}, | |||
getColor(colorStr, defaultValue = null) { | |||
if (colorStr.startsWith('#')) { | |||
return colorStr | |||
} | |||
const result = this.colors.find(t => t.color === colorStr) | |||
return result ? result.name : defaultValue | |||
}, | |||
getUiIcons() { | |||
return uiIcons | |||
}, | |||
getUiColors() { | |||
return uiColors | |||
} | |||
} | |||
} | |||
Vue.mixin(mixins) |
@@ -0,0 +1,33 @@ | |||
<template> | |||
<view | |||
:class="[size, round ? 'round' : '', radius ? 'radius' : '', color ? 'bg-' + color : '', className]" | |||
:style="rootStyle" | |||
class="cu-avatar" | |||
> | |||
<slot></slot> | |||
<slot name="badge"></slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-avatar', | |||
props: { | |||
src: {}, | |||
size: { default: 'df' }, | |||
round: {}, | |||
radius: {}, | |||
color: {} | |||
}, | |||
computed: { | |||
rootStyle() { | |||
return this.getStyle({ | |||
backgroundImage: this.src ? 'url(' + this.src + ')' : null, | |||
backgroundColor: this.color || this.src ? null : '#ccc' | |||
}) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,123 @@ | |||
<template> | |||
<view | |||
v-if="type === 'default'" | |||
:class="[hexColor ? '' : 'bg-' + color, fixed ? 'banner-fixed' : '', className]" | |||
:style="rootStyle" | |||
class="cu-bar cu-banner" | |||
> | |||
<view @click="clickLeft" class="action"><slot name="left"></slot></view> | |||
<view @click="clickCenter" class="content"> | |||
<slot></slot> | |||
{{ title }} | |||
</view> | |||
<view @click="clickRight" class="action"><slot name="right"></slot></view> | |||
</view> | |||
<view | |||
v-else-if="type === 'search'" | |||
:class="[hexColor ? '' : 'bg-' + color, fixed ? 'banner-fixed' : '', className]" | |||
:style="rootStyle" | |||
class="cu-bar search cu-banner" | |||
> | |||
<slot name="left"></slot> | |||
<view class="search-form" :class="[fill ? 'radius' : 'round']" :style="inputStyle"> | |||
<slot name="searchInput"> | |||
<text class="cuIcon-search"></text> | |||
<input | |||
@input="searchTextChange" | |||
@focus="$emit('inputFocus', $event)" | |||
:placeholder-style="placeholderStyle" | |||
:adjust-position="false" | |||
:placeholder="placeholder" | |||
:value="value" | |||
:focus="focus" | |||
confirm-type="search" | |||
type="text" | |||
ref="bannerInput" | |||
/> | |||
</slot> | |||
</view> | |||
<view v-if="!noSearchButton" class="action"> | |||
<slot name="searchButton"> | |||
<template v-if="fill"> | |||
<view @click="searchClick">搜索</view> | |||
</template> | |||
<button v-else @click="searchClick" class="cu-btn bg-green shadow-blur round">搜索</button> | |||
</slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-banner', | |||
props: { | |||
type: { default: 'default' }, | |||
color: { default: 'white' }, | |||
hexColor: {}, | |||
inputStyle: {}, | |||
fill: {}, | |||
title: {}, | |||
value: {}, | |||
placeholder: { default: '搜索图片、文章、视频' }, | |||
noSearchButton: {}, | |||
placeholderStyle: {}, | |||
fixed: {}, | |||
noshadow: {}, | |||
focus: {} | |||
}, | |||
methods: { | |||
searchTextChange(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
}, | |||
clickLeft(e) { | |||
this.$emit('clickLeft', e) | |||
}, | |||
clickRight(e) { | |||
this.$emit('clickRight', e) | |||
}, | |||
clickCenter(e) { | |||
this.$emit('clickCenter', e) | |||
}, | |||
searchClick(F) { | |||
this.$emit('searchClick') | |||
}, | |||
inputFocus() { | |||
this.$refs.bannerInput.focus() | |||
} | |||
}, | |||
computed: { | |||
rootStyle() { | |||
return this.type === 'default' | |||
? this.getStyle({ | |||
backgroundColor: this.hexColor, | |||
boxShadow: this.noshadow ? null : '0 1rpx 6rpx rgba(0, 0, 0, 0.1)' | |||
}) | |||
: this.getStyle({ | |||
backgroundColor: this.hexColor, | |||
boxShadow: this.noshadow ? null : '0 1rpx 6rpx rgba(0, 0, 0, 0.1)' | |||
}) | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.banner-fixed { | |||
position: fixed; | |||
width: 100%; | |||
top: var(--window-top); | |||
z-index: 1024; | |||
} | |||
</style> |
@@ -0,0 +1,14 @@ | |||
<template> | |||
<view :class="['bg-' + color, className]" :style="style" class="submit cu-bar-item-button">{{ title }}</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar-item-button', | |||
props: { | |||
title: {}, | |||
color: { default: 'gray' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,39 @@ | |||
<template> | |||
<view v-if="type === 'default'" @click="click" :class="className" :style="style" class="action cu-bar-item"> | |||
<slot v-if="$slots.$default"></slot> | |||
<template v-else> | |||
<l-icon :type="icon" :color="color" size="lg" class="cuIcon-cu-image" /> | |||
<slot name="badge"></slot> | |||
<view :class="'text-' + color"> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
</template> | |||
</view> | |||
<view v-else-if="type === 'round'" :class="className" :style="style" class="action text-gray add-action cu-bar-item"> | |||
<slot v-if="$slots.$default"></slot> | |||
<template v-else> | |||
<button :class="['bg-' + color, 'cuIcon-' + icon]" class="cu-btn shadow"></button> | |||
<slot name="title">{{ title }}</slot> | |||
</template> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar-item', | |||
props: { | |||
type: { default: 'default' }, | |||
title: {}, | |||
icon: {}, | |||
color: { default: 'gray' } | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,14 @@ | |||
<template> | |||
<view :class="[dark ? 'bg-black' : 'bg-white', className]" :style="style" class="cu-bar tabbar"><slot></slot></view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar', | |||
props: { | |||
msginput: {}, | |||
dark: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,62 @@ | |||
<template> | |||
<button | |||
@click="click" | |||
:class="[ | |||
size, | |||
round ? 'round' : '', | |||
shadow ? 'shadow' : '', | |||
buttonColor, | |||
buttonBorder, | |||
block ? 'block' : '', | |||
disabled ? 'disabled' : '', | |||
className | |||
]" | |||
:disabled="disabled ? 'disabled' : null" | |||
:type="disabled && line ? 'default' : null" | |||
:style="style" | |||
hover-class="button-hover" | |||
class="cu-btn" | |||
> | |||
<slot></slot> | |||
</button> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-button', | |||
props: { | |||
size: { default: 'df' }, | |||
color: {}, | |||
round: {}, | |||
shadow: {}, | |||
line: {}, | |||
blod: {}, | |||
icon: {}, | |||
disabled: {}, | |||
block: {} | |||
}, | |||
computed: { | |||
buttonBorder() { | |||
return this.line ? `line${this.blod ? 's' : ''}-${this.line || 'gray'}` : '' | |||
}, | |||
buttonColor() { | |||
if (this.color) { | |||
return `bg-${this.color}` | |||
} else if (!this.line) { | |||
return `bg-gray` | |||
} | |||
return '' | |||
} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,82 @@ | |||
<template> | |||
<view :class="[outline ? '' : 'no-card', type !== 'base' ? type : '', className]" :style="style" class="cu-card"> | |||
<view class="cu-item shadow"> | |||
<template v-if="type === 'case'"> | |||
<view class="image"> | |||
<slot name="img"><image :src="info.img" mode="widthFix"></image></slot> | |||
<slot name="badge"> | |||
<l-tag :color="info.badgeColor">{{ info.badge }}</l-tag> | |||
</slot> | |||
<view class="cu-bar bg-shadeBottom"> | |||
<slot name="title"> | |||
<text class="text-cut">{{ info.title }}</text> | |||
</slot> | |||
</view> | |||
</view> | |||
<view class="cu-list menu-avatar"> | |||
<view class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar :src="info.avatar" size="lg" style="margin: 0 30rpx; position: absolute; left: 0;" round /> | |||
</slot> | |||
<view class="content flex-sub"> | |||
<slot name="user"> | |||
<view class="text-grey">{{ info.user }}</view> | |||
</slot> | |||
<view class="text-gray text-sm flex justify-between"> | |||
<slot name="tips">{{ info.tips }}</slot> | |||
<slot name="footer"></slot> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<template v-else-if="type === 'dynamic'"> | |||
<view class="cu-list menu-avatar"> | |||
<view class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar :src="info.avatar" size="lg" style="margin: 0 30rpx; position: absolute; left: 0;" round /> | |||
</slot> | |||
<view class="content flex-sub"> | |||
<slot name="user"> | |||
<view>{{ info.user }}</view> | |||
</slot> | |||
<slot name="tips"> | |||
<view class="text-gray text-sm flex justify-between">{{ info.tips }}</view> | |||
</slot> | |||
</view> | |||
</view> | |||
</view> | |||
<slot></slot> | |||
<slot name="footer"></slot> | |||
</template> | |||
<template v-else-if="type === 'article'"> | |||
<view class="title"> | |||
<slot name="title"> | |||
<view class="text-cut">{{ info.title }}</view> | |||
</slot> | |||
</view> | |||
<view class="content"> | |||
<slot name="img"><image :src="info.img" mode="aspectFill"></image></slot> | |||
<view class="desc"> | |||
<view class="text-content"><slot></slot></view> | |||
<slot name="footer"></slot> | |||
</view> | |||
</view> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-card', | |||
props: { | |||
type: { default: 'case' }, | |||
outline: {}, | |||
info: { default: () => ({}) } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,72 @@ | |||
<template> | |||
<view :style="rootStyle" :class="className" class="cu-bar foot input"> | |||
<input | |||
@focus="inputFocus" | |||
@blur="inputBlur" | |||
@input="input" | |||
@confirm="sendClick" | |||
:value="value" | |||
:adjust-position="false" | |||
:enableNative="false" | |||
:focus="false" | |||
:placeholder="placeholder" | |||
:disabled="inputDisabled" | |||
:confirm-hold="confirmHold" | |||
cursor-spacing="10" | |||
class="solid-bottom" | |||
confirm-type="send" | |||
type="text" | |||
/> | |||
<slot name="action"> | |||
<button @click="sendClick" :disabled="buttonDisabled" class="cu-btn bg-green shadow">发送</button> | |||
</slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat-input', | |||
props: { | |||
value: {}, | |||
placeholder: {}, | |||
inputDisabled: {}, | |||
buttonDisabled: {}, | |||
confirmHold: {} | |||
}, | |||
data() { | |||
return { inputBottom: 0 } | |||
}, | |||
methods: { | |||
input(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
}, | |||
inputFocus(e) { | |||
if (e && e.detail && e.detail.height) { | |||
this.inputBottom = e.detail.height | |||
} | |||
this.$emit('focus') | |||
}, | |||
inputBlur(e) { | |||
this.inputBottom = 0 | |||
this.$emit('blur') | |||
}, | |||
sendClick(e) { | |||
this.$emit('sendMsg', this.value) | |||
} | |||
}, | |||
computed: { | |||
rootStyle() { | |||
return this.getStyle({ bottom: this.inputBottom + 'px' }) | |||
// return this.getStyle({ bottom: '0px' }) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,101 @@ | |||
<template> | |||
<view :class="[type === 'left' ? '' : 'self', className]" :style="style" class="cu-item"> | |||
<slot v-if="type === 'left'" name="avatar"> | |||
<view | |||
v-if="type === 'left' && avatar" | |||
:style="{ backgroundImage: `url(${avatar})`, borderRadius: roundAvatar ? '50%' : '3px' }" | |||
class="cu-avatar radius" | |||
></view> | |||
<image | |||
v-else-if="type === 'left' && imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'};` + imgStyle" | |||
mode="aspectFill" | |||
class="chat-msg-imgAvatar cu-avatar radius" | |||
></image> | |||
<text | |||
v-else-if="type === 'left' && iconAvatar" | |||
:class="'cuIcon-' + iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
></text> | |||
<!-- 支付宝小程序,使用 l-icon 报错 --> | |||
<!-- | |||
<l-icon | |||
v-else-if="type === 'left' && iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
/> | |||
--> | |||
</slot> | |||
<view class="main"> | |||
<view | |||
:class="[type === 'left' ? (leftColor ? 'bg-' + leftColor : '') : rightColor ? 'bg-' + rightColor : '']" | |||
class="content shadow" | |||
> | |||
<text style="word-break: break-all;"> | |||
<slot></slot> | |||
{{ content }} | |||
</text> | |||
</view> | |||
</view> | |||
<slot v-if="type !== 'left'" name="avatar"> | |||
<view | |||
v-if="type !== 'left' && avatar" | |||
:style="{ backgroundImage: `url(${avatar})`, borderRadius: roundAvatar ? '50%' : '3px' }" | |||
class="cu-avatar radius" | |||
></view> | |||
<image | |||
v-else-if="type !== 'left' && imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'};` + imgStyle" | |||
mode="aspectFill" | |||
class="chat-msg-imgAvatar cu-avatar radius" | |||
></image> | |||
<text | |||
v-else-if="type !== 'left' && iconAvatar" | |||
:class="'cuIcon-' + iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
></text> | |||
<!-- 支付宝小程序,使用 l-icon 报错 --> | |||
<!-- | |||
<l-icon | |||
v-else-if="type !== 'left' && iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
/> | |||
--> | |||
</slot> | |||
<view class="date"> | |||
<slot name="date">{{ date }}</slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat-msg', | |||
props: { | |||
content: { default: '' }, | |||
type: { default: 'left' }, | |||
roundAvatar: {}, | |||
date: {}, | |||
leftColor: {}, | |||
rightColor: { default: 'green' }, | |||
avatar: {}, | |||
iconAvatar: {}, | |||
iconStyle: { default: '' }, | |||
imgAvatar: {}, | |||
imgStyle: { default: '' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,20 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-chat"> | |||
<slot></slot> | |||
<view v-if="empty" class="padding text-gray text-center">{{ emptyTips }}</view> | |||
<view v-if="!empty && nomore" class="padding text-gray text-center">{{ nomoreTips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat', | |||
props: { | |||
empty: {}, | |||
emptyTips: { default: '暂无消息' }, | |||
nomore: { default: true }, | |||
nomoreTips: { default: '没有更多消息了' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,111 @@ | |||
<template> | |||
<l-label @click="click" :arrow="!readonly" :required="required" :title="title" :class="className" :style="style"> | |||
{{ display || displayPlaceholder }} | |||
<l-modal | |||
v-if="modal" | |||
@ok="confirm" | |||
@cancel="cancel" | |||
@checkboxChange="checkboxChange" | |||
:checkbox="innerChecked" | |||
:value="modalOpen" | |||
:range="range" | |||
:readonly="readonly" | |||
type="checkbox" | |||
/> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-checkbox-picker', | |||
props: { | |||
value: {}, | |||
range: { default: () => [] }, | |||
title: {}, | |||
required: {}, | |||
placeholder: {}, | |||
readonly: {} | |||
}, | |||
data() { | |||
return { | |||
modal: false, | |||
modalOpen: false, | |||
innerChecked: this.value || [] | |||
} | |||
}, | |||
methods: { | |||
click() { | |||
if (this.readonly || this.modal || this.modalOpen) { | |||
return | |||
} | |||
this.innerChecked = this.value | |||
this.modal = true | |||
setTimeout(() => { | |||
this.modalOpen = true | |||
this.$emit('open') | |||
}, 300) | |||
}, | |||
checkboxChange(newVal) { | |||
this.innerChecked = newVal | |||
}, | |||
confirm() { | |||
this.modalOpen = false | |||
setTimeout(() => { | |||
this.modal = false | |||
this.$emit('input', this.innerChecked) | |||
this.$emit('change', this.innerChecked) | |||
this.$emit('close') | |||
}, 300) | |||
}, | |||
cancel() { | |||
this.modalOpen = false | |||
setTimeout(() => { | |||
this.modal = false | |||
this.$emit('close') | |||
}, 300) | |||
} | |||
}, | |||
computed: { | |||
display() { | |||
if (this.value.length <= 0) { | |||
return null | |||
} | |||
if (!this.objMode) { | |||
return this.value.join(',') | |||
} | |||
return this.value | |||
.reduce((a, b) => { | |||
const selected = this.range.find(t => t.value === b) | |||
return selected ? a.concat(selected.text) : a | |||
}, []) | |||
.join(',') | |||
}, | |||
displayPlaceholder() { | |||
if (this.readonly) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
}, | |||
objMode() { | |||
return typeof this.range[0] === 'object' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,50 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title">{{ title }}</view> | |||
<checkbox | |||
@change="change" | |||
:class="[isCheck ? 'checked' : '', color ? color : '', round ? 'round' : '']" | |||
:checked="isCheck" | |||
:value="checkboxValue" | |||
:disabled="disabled" | |||
></checkbox> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-checkbox', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
round: {}, | |||
color: {}, | |||
checkboxValue: {}, | |||
checked: {}, | |||
value: { default: () => [] } | |||
}, | |||
methods: { | |||
change(e) { | |||
let newVal = this.value | |||
const isCurrentCheck = e.detail.value | |||
if (isCurrentCheck) { | |||
newVal = [...this.value, this.checkboxValue] | |||
} else { | |||
newVal = this.value.filter(t => t !== this.checkboxValue) | |||
} | |||
this.$emit('input', newVal) | |||
this.$emit('change', newVal) | |||
} | |||
}, | |||
computed: { | |||
isCheck() { | |||
return this.value.includes(this.checkboxValue) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,91 @@ | |||
<template> | |||
<l-label :class="className" :style="style" :arrow="!disabled" :required="required" :title="title"> | |||
<!-- #ifndef MP-DINGTALK --> | |||
<picker @change="change" :fields="fields" :disabled="disabled" :value="value" :start="start" :end="end" mode="date"> | |||
<view class="picker">{{ value || displayPlaceholder }}</view> | |||
</picker> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-DINGTALK --> | |||
<text @click="click">{{ value || displayPlaceholder }}</text> | |||
<!-- #endif --> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-date-picker', | |||
props: { | |||
title: {}, | |||
start: { default: '1900-01-01' }, | |||
end: { default: '2100-01-01' }, | |||
fields: { default: 'day' }, | |||
disabled: {}, | |||
placeholder: {}, | |||
required: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
if (this.disabled) { | |||
return | |||
} | |||
this.$emit('click') | |||
// #ifdef MP-DINGTALK | |||
const getCurrent = () => { | |||
const now = new Date() | |||
const year = now.getFullYear().toString() | |||
const month = (now.getMonth() + 1).toString().padStart(2, '0') | |||
const date = now | |||
.getDate() | |||
.toString() | |||
.padStart(2, '0') | |||
return `${year}-${month}-${date}` | |||
} | |||
const currentDate = this.value ? this.value : getCurrent() | |||
this.$emit('open') | |||
dd.datePicker({ | |||
currentDate, | |||
format: 'yyyy-MM-dd', | |||
success: ({ date }) => { | |||
if (date) { | |||
this.$emit('input', date) | |||
this.$emit('change', date) | |||
} | |||
}, | |||
complete: () => { | |||
this.$emit('close') | |||
} | |||
}) | |||
// #endif | |||
}, | |||
change(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择日期…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,621 @@ | |||
<template> | |||
<view :class="className" :style="style" class="lr-datetime-picker"> | |||
<view | |||
@tap.stop="maskClick" | |||
@touchmove.stop.prevent | |||
:class="{ show: showPicker }" | |||
class="lr-datetime-mask" | |||
catchtouchmove="true" | |||
></view> | |||
<view :class="{ show: showPicker }" class="lr-datetime-content"> | |||
<view @touchmove.stop.prevent catchtouchmove="true" class="lr-datetime-hd"> | |||
<view @tap.stop="pickerCancel" class="lr-datetime-btn">取消</view> | |||
<view @tap.stop="pickerConfirm" :style="{ color: color }" class="lr-datetime-btn">确定</view> | |||
</view> | |||
<view class="lr-datetime-view"> | |||
<picker-view | |||
v-if="isAll" | |||
@change="bindChange" | |||
:indicator-style="itemHeight" | |||
:value="values" | |||
style="height: 100%;" | |||
> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.years" :key="index" class="lr-datetime-item">{{ item }}年</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.months" :key="index" class="lr-datetime-item">{{ item }}月</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.days" :key="index" class="lr-datetime-item">{{ item }}日</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.hours" :key="index" class="lr-datetime-item">{{ item }}时</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.minutes" :key="index" class="lr-datetime-item">{{ item }}分</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.seconds" :key="index" class="lr-datetime-item">{{ item }}秒</view> | |||
</picker-view-column> | |||
</picker-view> | |||
<picker-view | |||
v-else | |||
@change="bindDateChange" | |||
:indicator-style="itemHeight" | |||
:value="dateValues" | |||
style="height: 100%;" | |||
> | |||
<picker-view-column class="yu-picker-column"> | |||
<view v-for="(item, index) in dateObj.dates" :key="index" class="lr-datetime-item">{{ item }}</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.hours" :key="index" class="lr-datetime-item">{{ item }}时</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.minutes" :key="index" class="lr-datetime-item">{{ item }}分</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.seconds" :key="index" class="lr-datetime-item">{{ item }}秒</view> | |||
</picker-view-column> | |||
</picker-view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-datetime-panel', | |||
props: { | |||
//全部日期有效可选 | |||
isAll: { default: true }, | |||
//颜色 | |||
color: { default: '#1aad19' }, | |||
//开始年份 | |||
startYear: { default: '1900' }, | |||
//结束年份 | |||
endYear: { default: '2100' }, | |||
//设置默认日期时间 | |||
val: { default: '' } | |||
}, | |||
data() { | |||
let date = new Date() | |||
let year = date.getFullYear() | |||
let month = date.getMonth() + 1 | |||
let day = date.getDate() | |||
let hour = date.getHours() | |||
let minute = date.getMinutes() | |||
let second = date.getSeconds() | |||
let dates = [] | |||
let months = [] | |||
let years = [] | |||
let days = [] | |||
let hours = [] | |||
let minutes = [] | |||
let seconds = [] | |||
for (let i = month; i <= month + 2; i++) { | |||
//获取包括当前月份在内的3个月内的日期 | |||
let localMonth = i | |||
let localYear = year | |||
if (i == 13) { | |||
localYear += 1 | |||
} | |||
if (i >= 13) { | |||
localMonth -= 12 | |||
} | |||
let total = new Date(localYear, localMonth, 0).getDate() | |||
if (i == month) { | |||
for (let j = day; j <= total; j++) { | |||
let m = localMonth | |||
let d = j | |||
if (localMonth < 10) { | |||
m = '0' + m | |||
} | |||
if (j < 10) { | |||
d = '0' + d | |||
} | |||
let str = year + '-' + m + '-' + d | |||
dates.push(str) | |||
} | |||
} else { | |||
for (let j = 1; j <= total; j++) { | |||
let m = localMonth | |||
let d = j | |||
if (localMonth < 10) { | |||
m = '0' + m | |||
} | |||
if (j < 10) { | |||
d = '0' + d | |||
} | |||
let str = year + '-' + m + '-' + d | |||
dates.push(str) | |||
} | |||
} | |||
} | |||
for (let i = parseInt(this.startYear); i <= this.endYear; i++) { | |||
years.push(i) | |||
} | |||
for (let i = 1; i <= 12; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
months.push(str) | |||
} | |||
if (this.val) { | |||
let valueArr = this.val.split(' ') | |||
let valueDateArr = valueArr[0].split('-') | |||
let totalCurrent = new Date(valueDateArr[0], valueDateArr[1], 0).getDate() | |||
for (let i = 1; i <= totalCurrent; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
days.push(str) | |||
} | |||
} else { | |||
let totalCurrent = new Date(year, month, 0).getDate() | |||
for (let i = 1; i <= totalCurrent; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
days.push(str) | |||
} | |||
} | |||
for (let i = 0; i < 24; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
hours.push(str) | |||
} | |||
for (let i = 0; i < 60; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
minutes.push(str) | |||
} | |||
for (let i = 0; i < 60; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
seconds.push(str) | |||
} | |||
let dateObj = { | |||
dates, | |||
years, | |||
months, | |||
days, | |||
hours, | |||
minutes, | |||
seconds | |||
} | |||
return { | |||
value: this.val, | |||
year, | |||
month, | |||
day, | |||
hour, | |||
minute, | |||
second, | |||
dateObj, | |||
itemHeight: `height: ${uni.upx2px(88)}px;`, | |||
values: [0, 0, 0, 0, 0, 0], | |||
selectArr: [], | |||
selectRes: '', | |||
showPicker: false, | |||
dateValues: [0, 0, 0, 0] | |||
} | |||
}, | |||
mounted() { | |||
this.setDate() | |||
}, | |||
methods: { | |||
setDate(dt) { | |||
if (dt) { | |||
this.value = dt | |||
} | |||
this.initDate() | |||
if (!this.value) { | |||
this.showCurrent() | |||
} | |||
}, | |||
initDate() { | |||
let _this = this | |||
//解析默认显示的日期时间 | |||
if (_this.value) { | |||
let values = [0, 0, 0, 0, 0, 0] | |||
let dateValues = [0, 0, 0, 0] | |||
let valueStr = _this.value | |||
let valueArr = valueStr.split(' ') | |||
let valueDateArr = valueArr[0].split('-') | |||
let valueTimeArr = valueArr[1].split(':') | |||
if (_this.isAll) { | |||
values[0] = valueDateArr[0] - _this.startYear > 0 ? valueDateArr[0] - _this.startYear : 0 | |||
values[1] = parseInt(valueDateArr[1]) - 1 | |||
values[2] = parseInt(valueDateArr[2]) - 1 | |||
values[3] = parseInt(valueTimeArr[0]) | |||
values[4] = parseInt(valueTimeArr[1]) | |||
values[5] = parseInt(valueTimeArr[2]) | |||
_this.$nextTick(() => { | |||
_this.values = values | |||
}) | |||
if (valueDateArr[0] - _this.startYear >= 0) { | |||
_this.selectArr = [ | |||
valueDateArr[0], | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = _this.value | |||
} else { | |||
_this.selectArr = [ | |||
_this.formatNum(_this.startYear), | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = `${this.formatNum(_this.startYear) + | |||
'-' + | |||
valueDateArr[1] + | |||
'-' + | |||
valueDateArr[2] + | |||
' ' + | |||
valueTimeArr[0] + | |||
':' + | |||
valueTimeArr[1] + | |||
':' + | |||
valueTimeArr[2]}` | |||
} | |||
} else { | |||
let str = valueDateArr.join('') | |||
let localStr = _this.formatNum(_this.year) + _this.formatNum(_this.month) + _this.formatNum(_this.day) + '' | |||
if (str < localStr) { | |||
dateValues[0] = 0 | |||
_this.selectArr = [ | |||
_this.formatNum(_this.year), | |||
_this.formatNum(_this.month), | |||
_this.formatNum(_this.day), | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = `${_this.formatNum(_this.year) + | |||
'-' + | |||
_this.formatNum(_this.month) + | |||
'-' + | |||
_this.formatNum(_this.day) + | |||
' ' + | |||
valueTimeArr[0] + | |||
':' + | |||
valueTimeArr[1] + | |||
':' + | |||
valueTimeArr[2]}` | |||
} else { | |||
let num = 0 //计算默认日期和当前日期相隔天数,计算下标 | |||
let start = | |||
_this.formatNum(_this.year) + '-' + _this.formatNum(_this.month) + '-' + _this.formatNum(_this.day) | |||
let res = _this.getBetweenDateStr(start, valueArr[0]) | |||
dateValues[0] = res.length - 1 | |||
_this.selectArr = [ | |||
valueDateArr[0], | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = _this.value | |||
} | |||
dateValues[1] = parseInt(valueTimeArr[0]) | |||
dateValues[2] = parseInt(valueTimeArr[1]) | |||
dateValues[3] = parseInt(valueTimeArr[2]) | |||
_this.$nextTick(() => { | |||
_this.dateValues = dateValues | |||
}) | |||
} | |||
return | |||
} | |||
if (_this.isAll) { | |||
_this.selectArr = [_this.formatNum(_this.startYear), '01', '01', '00', '00', '00'] | |||
_this.selectRes = `${_this.formatNum(_this.startYear) + '-01-01 00:00:00'}` | |||
} else { | |||
_this.selectArr = [_this.formatNum(_this.year), _this.formatNum(_this.month), '01', '00', '00', '00'] | |||
_this.selectRes = `${_this.formatNum(_this.year) + '-' + _this.formatNum(_this.month) + '-01 00:00:00'}` | |||
} | |||
}, | |||
showCurrent() { | |||
//显示当前的日期时间 | |||
let arr = [0, 0, 0, 0, 0, 0] | |||
let dateArr = [0, 0, 0, 0] | |||
this.selectArr = [ | |||
this.formatNum(this.year), | |||
this.formatNum(this.month), | |||
this.formatNum(this.day), | |||
this.formatNum(this.hour), | |||
this.formatNum(this.minute), | |||
this.formatNum(this.second) | |||
] | |||
this.selectRes = `${this.formatNum(this.year) + | |||
'-' + | |||
this.formatNum(this.month) + | |||
'-' + | |||
this.formatNum(this.day) + | |||
' ' + | |||
this.formatNum(this.hour) + | |||
':' + | |||
this.formatNum(this.minute) + | |||
':' + | |||
this.formatNum(this.second)}` | |||
if (this.isAll) { | |||
arr[0] = this.year - this.startYear | |||
arr[1] = this.month - 1 | |||
arr[2] = this.day - 1 | |||
arr[3] = this.hour | |||
arr[4] = this.minute | |||
arr[5] = this.second | |||
this.$nextTick(() => { | |||
this.values = arr | |||
}) | |||
} else { | |||
dateArr[1] = this.hour | |||
dateArr[2] = this.minute | |||
dateArr[3] = this.second | |||
this.$nextTick(() => { | |||
this.dateValues = dateArr | |||
}) | |||
} | |||
}, | |||
initDayArr: (year, month) => { | |||
//初始化月份天数 | |||
let totalDay = new Date(year, month, 0).getDate() | |||
let dayArr = [] | |||
for (let i = 1; i <= totalDay; i++) { | |||
if (i < 10) { | |||
i = '0' + i | |||
} else { | |||
i = i + '' | |||
} | |||
dayArr.push(i) | |||
} | |||
return dayArr | |||
}, | |||
formatNum(num) { | |||
//日期时间的初始化 | |||
return num < 10 ? '0' + num : num + '' | |||
}, | |||
maskClick() { | |||
//日期时间的遮罩 | |||
this.showPicker = false | |||
this.$emit('cancel') | |||
}, | |||
show() { | |||
//日期时间的显示 | |||
this.showPicker = true | |||
}, | |||
hide() { | |||
//日期时间的隐藏 | |||
this.showPicker = false | |||
this.$emit('cancel') | |||
}, | |||
pickerCancel() { | |||
//日期时间取消 | |||
this.showPicker = false | |||
this.$emit('cancel', { | |||
selectArr: this.selectArr | |||
}) | |||
}, | |||
pickerConfirm(e) { | |||
//日期时间确定 | |||
this.showPicker = false | |||
this.$emit('confirm', { | |||
selectArr: this.selectArr, | |||
selectRes: this.selectRes | |||
}) | |||
}, | |||
bindChange(e) { | |||
//默认滚动日期时间方法 | |||
let valueArr = e.detail.value | |||
let year = '', | |||
month = '', | |||
day = '', | |||
hour = '', | |||
minute = '', | |||
second = '' | |||
let selectArr = this.selectArr | |||
let dayArr = [] | |||
year = this.dateObj.years[valueArr[0]] | |||
month = this.dateObj.months[valueArr[1]] | |||
day = this.dateObj.days[valueArr[2]] | |||
hour = this.dateObj.hours[valueArr[3]] | |||
minute = this.dateObj.minutes[valueArr[4]] | |||
second = this.dateObj.seconds[valueArr[5]] | |||
if (year != selectArr[0]) { | |||
dayArr = this.initDayArr(year, month) | |||
this.dateObj.days = dayArr | |||
} | |||
if (month != selectArr[1]) { | |||
dayArr = this.initDayArr(year, month) | |||
this.dateObj.days = dayArr | |||
} | |||
this.selectArr = [year, month, day, hour, minute, second] | |||
this.selectRes = `${year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second}` | |||
this.$nextTick(() => { | |||
this.values = valueArr | |||
}) | |||
}, | |||
bindDateChange(e) { | |||
//有效日期的滚动日期时间方法 | |||
let valueArr = e.detail.value | |||
let dateStr = '', | |||
dateArr = [], | |||
hour = '', | |||
minute = '', | |||
second = '' | |||
let selectArr = this.selectArr | |||
let dayArr = [] | |||
dateStr = this.dateObj.dates[valueArr[0]] | |||
dateArr = dateStr.split('-') | |||
hour = this.dateObj.hours[valueArr[1]] | |||
minute = this.dateObj.minutes[valueArr[2]] | |||
second = this.dateObj.seconds[valueArr[3]] | |||
this.selectArr = [dateArr[0], dateArr[1], dateArr[2], hour, minute, second] | |||
this.selectRes = `${dateArr[0] + '-' + dateArr[1] + '-' + dateArr[2] + ' ' + hour + ':' + minute + ':' + second}` | |||
this.$nextTick(() => { | |||
this.dateValues = valueArr | |||
}) | |||
}, | |||
//遍历两个日期间的所有日期 | |||
getBetweenDateStr(start, end) { | |||
let result = [] | |||
let beginDay = start.split('-') | |||
let endDay = end.split('-') | |||
let diffDay = new Date() | |||
let dateList = new Array() | |||
let i = 0 | |||
diffDay.setDate(beginDay[2]) | |||
diffDay.setMonth(beginDay[1] - 1) | |||
diffDay.setFullYear(beginDay[0]) | |||
result.push(start) | |||
while (i == 0) { | |||
let countDay = diffDay.getTime() + 24 * 60 * 60 * 1000 | |||
diffDay.setTime(countDay) | |||
dateList[2] = diffDay.getDate() | |||
dateList[1] = diffDay.getMonth() + 1 | |||
dateList[0] = diffDay.getFullYear() | |||
if (String(dateList[1]).length == 1) { | |||
dateList[1] = '0' + dateList[1] | |||
} | |||
if (String(dateList[2]).length == 1) { | |||
dateList[2] = '0' + dateList[2] | |||
} | |||
result.push(dateList[0] + '-' + dateList[1] + '-' + dateList[2]) | |||
if (dateList[0] == endDay[0] && dateList[1] == endDay[1] && dateList[2] == endDay[2]) { | |||
i = 1 | |||
} | |||
} | |||
return result | |||
} | |||
} | |||
} | |||
</script> | |||
<style> | |||
.lr-datetime-picker { | |||
position: relative; | |||
z-index: 888; | |||
} | |||
.lr-datetime-mask { | |||
position: fixed; | |||
z-index: 1000; | |||
top: 0; | |||
right: 0; | |||
left: 0; | |||
bottom: 0; | |||
background: rgba(0, 0, 0, 0.4); | |||
visibility: hidden; | |||
opacity: 0; | |||
transition: all 0.3s ease; | |||
} | |||
.lr-datetime-mask.show { | |||
visibility: visible; | |||
opacity: 1; | |||
} | |||
.lr-datetime-content { | |||
position: fixed; | |||
bottom: 0; | |||
left: 0; | |||
width: 100%; | |||
transition: all 0.3s ease; | |||
transform: translateY(100%); | |||
z-index: 3000; | |||
} | |||
.lr-datetime-content.show { | |||
transform: translateY(0); | |||
} | |||
.lr-datetime-hd { | |||
display: flex; | |||
align-items: center; | |||
padding: 0 30upx; | |||
height: 44px; | |||
background-color: #fff; | |||
position: relative; | |||
text-align: center; | |||
font-size: 15px; | |||
justify-content: space-between; | |||
} | |||
.lr-datetime-btn { | |||
font-size: 14px; | |||
} | |||
.lr-datetime-hd:after { | |||
content: ' '; | |||
position: absolute; | |||
left: 0; | |||
bottom: 0; | |||
right: 0; | |||
height: 1px; | |||
border-bottom: 1px solid #e5e5e5; | |||
color: #e5e5e5; | |||
transform-origin: 0 100%; | |||
transform: scaleY(0.5); | |||
} | |||
.lr-datetime-view { | |||
width: 100%; | |||
height: 200px; | |||
overflow: hidden; | |||
background-color: rgba(255, 255, 255, 1); | |||
z-index: 666; | |||
} | |||
.yu-picker-column { | |||
-webkit-flex: 2; | |||
-webkit-box-flex: 2; | |||
flex: 2; | |||
} | |||
.lr-datetime-item { | |||
text-align: center; | |||
width: 100%; | |||
height: 88upx; | |||
line-height: 88upx; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
font-size: 30upx; | |||
} | |||
</style> |
@@ -0,0 +1,143 @@ | |||
<template> | |||
<!-- #ifndef MP-DINGTALK --> | |||
<l-label @click="click" :arrow="!disabled" :required="required" :title="title" :class="className" :style="style"> | |||
{{ value || displayPlaceholder }} | |||
<l-datetime-panel | |||
v-if="datetimeModal" | |||
@confirm="confirm" | |||
@cancel="cancel" | |||
:val="value" | |||
:startYear="1900" | |||
ref="datetime" | |||
allShow | |||
isAll | |||
/> | |||
</l-label> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-DINGTALK --> | |||
<l-label :arrow="arrow" :required="required" :title="title" :class="className" :style="style"> | |||
<text @click="click">{{ value || displayPlaceholder }}</text> | |||
</l-label> | |||
<!-- #endif --> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-datetime-picker', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
placeholder: {}, | |||
required: {}, | |||
value: {} | |||
}, | |||
// #ifndef MP-DINGTALK | |||
data() { | |||
return { | |||
datetimeModal: false | |||
} | |||
}, | |||
// #endif | |||
methods: { | |||
click(e) { | |||
if (this.disabled) { | |||
return | |||
} | |||
// #ifndef MP-DINGTALK | |||
// 支付宝 | |||
if (this.datetimeModal) { | |||
return | |||
} | |||
const datetimePanel = this.$refs.datetime | |||
this.datetimeModal = true | |||
this.$nextTick(() => { | |||
datetimePanel.setDate(this.value) | |||
datetimePanel.show() | |||
this.$emit('open') | |||
}) | |||
// #endif | |||
// #ifdef MP-DINGTALK | |||
// 钉钉 | |||
const getCurrent = () => { | |||
const now = new Date() | |||
const year = now.getFullYear().toString() | |||
const month = (now.getMonth() + 1).toString().padStart(2, '0') | |||
const date = now | |||
.getDate() | |||
.toString() | |||
.padStart(2, '0') | |||
const hours = now | |||
.getHours() | |||
.toString() | |||
.padStart(2, '0') | |||
const minutes = now | |||
.getMinutes() | |||
.toString() | |||
.padStart(2, '0') | |||
return `${year}-${month}-${date} ${hours}:${minutes}` | |||
} | |||
const currentDate = this.value ? this.value : getCurrent() | |||
this.$emit('open') | |||
dd.datePicker({ | |||
currentDate, | |||
format: 'yyyy-MM-dd HH:mm', | |||
success: ({ date }) => { | |||
if (date) { | |||
this.$emit('input', date + ':00') | |||
this.$emit('change', date + ':00') | |||
} | |||
}, | |||
complete: () => { | |||
this.$emit('close') | |||
} | |||
}) | |||
// #endif | |||
}, | |||
// #ifndef MP-DINGTALK | |||
confirm({ selectRes }) { | |||
setTimeout(() => { | |||
this.datetimeModal = false | |||
this.$emit('input', selectRes) | |||
this.$emit('change', selectRes) | |||
this.$emit('close') | |||
}, 300) | |||
}, | |||
cancel() { | |||
setTimeout(() => { | |||
this.datetimeModal = false | |||
}, 300) | |||
this.$emit('close') | |||
} | |||
// #endif | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,28 @@ | |||
<template> | |||
<text | |||
@click="click" | |||
:class="['cuIcon-' + type, color ? 'text-' + color : '', shadow ? 'text-shadow' : '', className]" | |||
:style="style" | |||
class="cuIcon" | |||
> | |||
<slot></slot> | |||
</text> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-icon', | |||
props: { | |||
color: {}, | |||
type: { require: true }, | |||
shadow: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,59 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
<input | |||
@input="input" | |||
@change="input" | |||
:value="value" | |||
:type="type" | |||
:disabled="disabled" | |||
:password="password" | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceholder" | |||
:style="{ textAlign: left ? 'left' : 'right' }" | |||
/> | |||
<slot name="suffix"></slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-input', | |||
props: { | |||
type: { default: 'text' }, | |||
title: {}, | |||
placeholder: {}, | |||
maxlength: { default: -1 }, | |||
disabled: {}, | |||
password: {}, | |||
left: {}, | |||
required: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
input(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请输入${this.title}` : `请输入…` | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,45 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title"> | |||
<slot name="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</slot> | |||
</view> | |||
<view @click="itemClick" :class="[arrow ? 'form-item-arrow' : 'form-item-noarrow']" class="action form-item-action"> | |||
<view class="picker"><slot></slot></view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-label', | |||
props: { | |||
title: {}, | |||
arrow: {}, | |||
required: {} | |||
}, | |||
methods: { | |||
itemClick(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> | |||
<style> | |||
.form-item-action { | |||
flex: 1; | |||
padding-right: 40rpx; | |||
overflow: hidden; | |||
position: relative; | |||
text-align: right; | |||
} | |||
.form-item-noarrow { | |||
padding-right: 0; | |||
} | |||
</style> |
@@ -0,0 +1,90 @@ | |||
<template> | |||
<view @click="$emit('click', $event)" :class="[arrow ? 'arrow' : '', className]" :style="style" class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar | |||
v-if="avatar" | |||
:src="avatar" | |||
:radius="!roundAvatar" | |||
:round="roundAvatar" | |||
:style="avatarStyle" | |||
size="lg" | |||
/> | |||
<image | |||
v-else-if="imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + imgStyle" | |||
mode="aspectFill" | |||
class="list-item-imgAvatar" | |||
/> | |||
<text | |||
v-else-if="iconAvatar" | |||
:class="'cuIcon-' + iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="list-item-iconAvatar flex justify-center align-center" | |||
></text> | |||
<!-- 支付宝小程序,使用 l-icon 报错 --> | |||
<!-- | |||
<l-icon | |||
v-else-if="iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="flex justify-center align-center" | |||
/> | |||
--> | |||
</slot> | |||
<view class="content"> | |||
<view class="text-grey"> | |||
<slot></slot> | |||
{{ title || '' }} | |||
</view> | |||
<view v-if="extra || $slots.extra" class="text-gray text-sm flex"> | |||
<view class="text-cut text-grey"> | |||
<slot name="extra">{{ extra }}</slot> | |||
</view> | |||
</view> | |||
</view> | |||
<view class="action"> | |||
<view v-if="times" class="text-grey text-sm"> | |||
<view v-if="times" style="display: flex; flex-direction: column; align-items: flex-end;"> | |||
<view class="text-right margin-bottom-xs">{{ times[0] }}</view> | |||
<view v-if="times[1]" class="text-right">{{ times[1] }}</view> | |||
</view> | |||
</view> | |||
<view v-else-if="time || $slots.time || $slots.tips" class="text-grey text-sm"> | |||
<slot name="time">{{ time || '' }}</slot> | |||
<slot name="tips"></slot> | |||
</view> | |||
<template v-else> | |||
<slot name="action">{{ action }}</slot> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-list-item', | |||
props: { | |||
title: {}, | |||
action: {}, | |||
arrow: {}, | |||
extra: {}, | |||
time: { default: '' }, | |||
times: {}, | |||
roundAvatar: {}, | |||
avatar: {}, | |||
avatarStyle: { default: '' }, | |||
iconAvatar: {}, | |||
iconStyle: { default: '' }, | |||
imgAvatar: { default: '' }, | |||
imgStyle: { default: '' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,26 @@ | |||
<template> | |||
<view | |||
class="cu-list" | |||
:class="[card ? 'card-menu margin-tb' : '', message ? 'menu-avatar' : 'menu', className]" | |||
:style="style" | |||
> | |||
<slot></slot> | |||
<view v-if="empty" class="padding text-gray text-center">{{ emptyTips }}</view> | |||
<view v-if="!empty && nomore" class="padding text-gray text-center">{{ nomoreTips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-list', | |||
props: { | |||
card: {}, | |||
message: {}, | |||
empty: {}, | |||
emptyTips: { default: '列表为空' }, | |||
nomore: { default: false }, | |||
nomoreTips: { default: '没有更多了' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,24 @@ | |||
<template> | |||
<view | |||
:class="['bg-' + color, error ? 'erro' : completed ? 'over' : 'loading', className]" | |||
:style="style" | |||
class="cu-load" | |||
> | |||
{{ error ? errorText : completed ? completeText : loadingText }} | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-loading', | |||
props: { | |||
completed: {}, | |||
error: {}, | |||
color: { default: 'gray' }, | |||
loadingText: { default: '加载中…' }, | |||
completeText: { default: '加载完成' }, | |||
errorText: { default: '加载失败' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,221 @@ | |||
<template> | |||
<view | |||
@click="modalClick" | |||
:class="[value ? 'show' : '', type === 'bottom' ? 'bottom-modal' : '', className]" | |||
:style="style" | |||
class="cu-modal" | |||
> | |||
<!-- 图片弹出窗口 --> | |||
<view v-if="type === 'img'" class="cu-dialog"> | |||
<view | |||
:style="{ backgroundImage: 'url(' + img + ')', height: '200px' }" | |||
class="bg-img" | |||
style="position: relative;" | |||
> | |||
<view @tap="close" class="action text-white" style="position: absolute;top: 10px;right: 10px;"> | |||
<l-icon type="close" /> | |||
</view> | |||
</view> | |||
<view class="cu-bar bg-white"><view @tap="close" class="action margin-0 flex-sub solid-left">关闭</view></view> | |||
</view> | |||
<!-- 单选弹出窗口 --> | |||
<view v-else-if="type === 'radio'" @tap.stop class="cu-dialog"> | |||
<radio-group @change="radioChange" class="block"> | |||
<view class="cu-list menu text-left"> | |||
<view v-for="(item, index) of range" :key="index" class="cu-item"> | |||
<label class="flex justify-between align-center flex-sub"> | |||
<view class="flex-sub">{{ objectMode ? item[textField] : item }}</view> | |||
<radio | |||
:checked="radioIndex === index ? true : false" | |||
:value="objectMode ? item[valueField] : item" | |||
:class="radioIndex === index ? 'checked' : ''" | |||
class="round" | |||
></radio> | |||
</label> | |||
</view> | |||
</view> | |||
</radio-group> | |||
</view> | |||
<!-- 多选弹出窗口 --> | |||
<view v-else-if="type === 'checkbox'" @tap.stop class="cu-dialog"> | |||
<view class="cu-bar bg-white"> | |||
<view @tap="close(0)" class="action text-blue">取消</view> | |||
<view v-if="!readonly" @tap="close(1)" class="action text-green">确定</view> | |||
</view> | |||
<view class="grid col-3 padding-sm"> | |||
<view v-for="(item, index) in range" :key="index" class="padding-xs"> | |||
<button | |||
@tap="chooseCheckbox(index)" | |||
:class="checkboxIndex.includes(index) ? 'bg-orange' : 'line-orange'" | |||
class="cu-btn orange lg block" | |||
> | |||
{{ objectMode ? item[textField] : item }} | |||
</button> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- 普通/对话/底部弹出窗口 --> | |||
<view v-else class="cu-dialog"> | |||
<view :class="[type === 'bottom' ? '' : 'justify-end pading-left']" class="cu-bar bg-white"> | |||
<!-- 普通/对话弹出窗口 --> | |||
<template v-if="type === 'default' || type === 'confirm'"> | |||
<view class="content"> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
<view @tap="close" class="action"><l-icon type="close" color="red" /></view> | |||
</template> | |||
<!-- 底部弹出窗口 --> | |||
<template v-if="type === 'bottom'"> | |||
<slot name="action"> | |||
<l-button @click="close" class="action padding-lr" line="red">取消</l-button> | |||
<view class="action padding-lr">{{ title }}</view> | |||
</slot> | |||
</template> | |||
</view> | |||
<view class="padding"> | |||
<slot>{{ content }}</slot> | |||
</view> | |||
<!-- 对话弹出窗口的操作区 --> | |||
<template v-if="type === 'confirm'"> | |||
<view class="cu-bar bg-white justify-end padding-right"> | |||
<view class="action"> | |||
<button @tap="close(0)" class="cu-btn line-green text-green">取消</button> | |||
<button @tap="close(1)" class="cu-btn bg-green margin-left">确定</button> | |||
</view> | |||
</view> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-modal', | |||
props: { | |||
title: {}, | |||
content: {}, | |||
value: {}, | |||
type: { default: 'default' }, | |||
img: {}, | |||
range: { default: () => [] }, | |||
radio: {}, | |||
checkbox: {}, | |||
readonly: {}, | |||
textField: { default: 'text' }, | |||
valueField: { default: 'value' } | |||
}, | |||
data() { | |||
return { | |||
radioIndex: undefined, | |||
checkboxIndex: [] | |||
} | |||
}, | |||
created() { | |||
this.init() | |||
}, | |||
methods: { | |||
init() { | |||
if (this.type === 'radio') { | |||
this.radioIndex = this.objectMode | |||
? this.range.findIndex(t => t[this.valueField] === this.radio) | |||
: this.range.indexOf(this.radio) | |||
} else if (this.type === 'checkbox' && Array.isArray(this.checkbox)) { | |||
this.checkboxIndex = this.checkbox.reduce( | |||
(a, b) => | |||
a.concat(this.objectMode ? this.range.findIndex(t => t[this.valueField] === b) : this.range.indexOf(b)), | |||
[] | |||
) | |||
} | |||
}, | |||
close(arg) { | |||
this.$emit('input', false) | |||
this.$emit('close', false) | |||
if (typeof arg === 'number') { | |||
this.$emit(['cancel', 'ok'][arg]) | |||
} | |||
if (this.type === 'checkbox') { | |||
if (arg === 0) { | |||
this.checkboxIndex = this.checkbox | |||
} else { | |||
const checkboxValue = this.checkboxIndex.reduce( | |||
(a, b) => a.concat(this.objectMode ? this.range[b][this.valueField] : this.range[b]), | |||
[] | |||
) | |||
this.$emit('checkboxChange', checkboxValue) | |||
this.$emit('checkboxIndex', this.checkboxIndex) | |||
} | |||
} | |||
if (typeof arg === 'number') { | |||
this.$emit(['cancel', 'ok'][arg]) | |||
} | |||
}, | |||
modalClick() { | |||
if (this.type === 'radio' || this.type === 'checkbox') { | |||
this.close() | |||
} | |||
}, | |||
radioChange(e) { | |||
if (this.readonly) { | |||
return | |||
} | |||
this.radioIndex = this.objectMode | |||
? this.range.findIndex(t => t[this.valueField] === e.detail.value) | |||
: this.range.indexOf(e.detail.value) | |||
this.$emit( | |||
'radioChange', | |||
this.objectMode ? this.range[this.radioIndex][this.valueField] : this.range[this.radioIndex] | |||
) | |||
this.$emit('radioIndex', this.radioIndex) | |||
}, | |||
chooseCheckbox(index) { | |||
if (this.readonly) { | |||
return | |||
} | |||
if (this.checkboxIndex.includes(index)) { | |||
this.checkboxIndex = this.checkboxIndex.filter(t => t !== index) | |||
} else { | |||
this.checkboxIndex.push(index) | |||
} | |||
const checkboxValue = this.checkboxIndex.reduce( | |||
(a, b) => a.concat(this.objectMode ? this.range[b][this.valueField] : this.range[b]), | |||
[] | |||
) | |||
} | |||
}, | |||
watch: { | |||
value(newVal) { | |||
if (newVal) { | |||
this.init() | |||
} | |||
} | |||
}, | |||
computed: { | |||
objectMode() { | |||
return typeof this.range[0] === 'object' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,70 @@ | |||
<template> | |||
<scroll-view | |||
:scroll-left="scrollLeft" | |||
:class="['bg-' + color, type === 'center' ? 'text-center' : '', shadow ? 'nav-shadow' : '', className]" | |||
:style="style" | |||
class="nav" | |||
style="z-index: 10;" | |||
scroll-with-animation | |||
scroll-x | |||
> | |||
<template v-if="type !== 'flex'"> | |||
<view | |||
v-for="(item, idx) in items" | |||
@tap="tabSelect(idx)" | |||
:class="value === idx ? 'text-green cur' : ''" | |||
:key="idx" | |||
class="cu-item" | |||
> | |||
{{ item }} | |||
</view> | |||
</template> | |||
<view v-else class="flex text-center"> | |||
<view | |||
@tap="tabSelect(idx)" | |||
v-for="(item, idx) in items" | |||
:key="idx" | |||
:class="[value === idx ? (color ? 'bg-white cur' : 'bg-green cur') : '']" | |||
class="cu-item flex-sub" | |||
> | |||
{{ item }} | |||
</view> | |||
</view> | |||
</scroll-view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-nav', | |||
props: { | |||
color: { default: 'white' }, | |||
items: { default: () => [] }, | |||
value: {}, | |||
type: { default: 'default' }, | |||
shadow: {} | |||
}, | |||
data() { | |||
return { | |||
scrollLeft: 0 | |||
} | |||
}, | |||
methods: { | |||
tabSelect(idx) { | |||
this.scrollLeft = (idx - 1) * 60 | |||
this.$emit('input', idx) | |||
this.$emit('change', idx) | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.nav-shodow { | |||
box-shadow: 0px 0px 7px 0px #666; | |||
} | |||
</style> |
@@ -0,0 +1,41 @@ | |||
<template> | |||
<view | |||
:class="[ | |||
size, | |||
radius ? 'radius' : '', | |||
round ? 'round' : '', | |||
striped ? 'striped' : '', | |||
active && striped ? 'active' : '', | |||
className | |||
]" | |||
:style="style" | |||
class="cu-progress" | |||
> | |||
<view :class="[color ? 'bg-' + color : '']" :style="{ width: displayPercent + '%' }"> | |||
{{ noText ? '' : displayPercent + '%' }} | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-progress', | |||
props: { | |||
percent: {}, | |||
noText: {}, | |||
color: {}, | |||
radius: {}, | |||
round: {}, | |||
size: { default: 'df' }, | |||
striped: {}, | |||
active: {} | |||
}, | |||
computed: { | |||
displayPercent() { | |||
return Number(this.percent) || 0 | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,46 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<radio | |||
@change="change" | |||
:class="[isCheck ? 'checked' : '', color ? color : '', point ? 'radio' : '']" | |||
:checked="isCheck" | |||
:value="radioValue" | |||
:disabled="disabled" | |||
></radio> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-radio', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
point: {}, | |||
color: {}, | |||
radioValue: {}, | |||
value: {}, | |||
required: {} | |||
}, | |||
methods: { | |||
change(e) { | |||
if (e.detail.value) { | |||
this.$emit('input', this.radioValue) | |||
this.$emit('change', this.radioValue) | |||
} | |||
} | |||
}, | |||
computed: { | |||
isCheck() { | |||
return this.value && this.value == this.radioValue | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,52 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<picker | |||
@change="change" | |||
:customitem="customitem" | |||
:value="value" | |||
:disabled="disabled" | |||
:class="[arrow ? 'picker-arrow' : '']" | |||
mode="region" | |||
> | |||
<view class="picker">{{ display || placeholder }}</view> | |||
</picker> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-region-picker', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
required: {}, | |||
placeholder: { default: '请选择地区…' }, | |||
multiple: {}, | |||
customitem: {}, | |||
arrow: { default: true }, | |||
value: {} | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
display() { | |||
if (!this.value || !this.value.length) { | |||
return this.placeholder | |||
} | |||
return this.value.join(' ') | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,157 @@ | |||
<template> | |||
<l-label :class="className" :style="style" :arrow="!disabled" :required="required" :title="title"> | |||
<picker | |||
@change="change" | |||
:mode="multiple ? 'multiSelector' : 'selector'" | |||
:range="innerRange" | |||
:value="index" | |||
:disabled="disabled" | |||
> | |||
<view class="picker">{{ display }}</view> | |||
</picker> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-select', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
range: {}, | |||
placeholder: {}, | |||
multiple: {}, | |||
value: {}, | |||
emptyText: { default: '(未选择)' }, | |||
splitText: { default: ' ' }, | |||
required: {}, | |||
textField: { default: 'text' }, | |||
valueField: { default: 'value' } | |||
}, | |||
data() { | |||
return { | |||
index: -1 | |||
} | |||
}, | |||
model: { | |||
prop: 'value', | |||
event: 'input' | |||
}, | |||
mounted() { | |||
this.calcIndex() | |||
}, | |||
methods: { | |||
change(e) { | |||
this.index = e.detail.value | |||
this.$emit('input', this.currentModel) | |||
this.$emit('change', this.currentModel) | |||
}, | |||
calcIndex() { | |||
if (this.multiple) { | |||
if (this.objectMode) { | |||
this.index = this.range.map((group, idx) => | |||
group.findIndex(t => this.arrayCompare(t[this.valueField], this.value[idx])) | |||
) | |||
} else { | |||
this.index = this.range.map((group, idx) => group.findIndex(t => this.arrayCompare(t, this.value[idx]))) | |||
} | |||
} else { | |||
if (this.objectMode) { | |||
this.index = this.range.findIndex(t => t[this.valueField] === this.value) | |||
} else { | |||
this.index = this.range.indexOf(this.value) | |||
} | |||
} | |||
}, | |||
arrayCompare(a, b) { | |||
if (!a || !b || a.length !== b.length) { | |||
return false | |||
} | |||
let isOk = true | |||
a.forEach((t, i) => { | |||
if (b[i] !== t) { | |||
isOk = false | |||
} | |||
}) | |||
return isOk | |||
} | |||
}, | |||
computed: { | |||
objectMode() { | |||
return typeof (this.multiple ? this.range[0][0] : this.range[0]) === 'object' | |||
}, | |||
innerRange() { | |||
if (!this.objectMode) { | |||
return this.range | |||
} | |||
if (this.multiple) { | |||
return this.range.map(item => item.map(t => t[this.textField])) | |||
} | |||
return this.range.map(t => t[this.textField]) | |||
}, | |||
currentModel() { | |||
const { multiple, range, index, objectMode } = this | |||
if (!multiple) { | |||
return index === -1 ? undefined : objectMode ? range[index][this.valueField] : range[index] | |||
} | |||
if (!objectMode) { | |||
return range.map((group, idx) => group[index[idx]]) | |||
} | |||
range.map((group, idx) => (index[idx] === -1 ? undefined : group[index[idx]][this.valueField])) | |||
}, | |||
display() { | |||
const { multiple, range, value, index, objectMode, emptyText, displayPlaceholder, splitText } = this | |||
if (!multiple) { | |||
return index === -1 ? displayPlaceholder : objectMode ? range[index][this.textField] : range[index] | |||
} | |||
if (!Array.isArray(index) || index.every(t => t === -1)) { | |||
return displayPlaceholder | |||
} | |||
if (!objectMode) { | |||
return range.map((group, idx) => group[index[idx]] || emptyText).join(splitText) | |||
} | |||
return range | |||
.map((group, idx) => (index[idx] === -1 ? emptyText : group[index[idx]][this.textField])) | |||
.join(splitText) | |||
}, | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
}, | |||
watch: { | |||
value() { | |||
this.calcIndex() | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,29 @@ | |||
<template> | |||
<view :class="[arrow ? 'steps-arrow' : '', className]" :style="style" class="cu-steps"> | |||
<view | |||
v-for="(item, index) in steps" | |||
:key="index" | |||
:class="index < step ? 'text-' + activeColor : ''" | |||
class="cu-item" | |||
> | |||
<text v-if="number" :data-index="index + 1" class="num"></text> | |||
<text v-else :class="index < step || !point ? 'cuIcon-' + item.icon : 'cuIcon-title'"></text> | |||
{{ item.title }} | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-step', | |||
props: { | |||
step: { default: 0 }, | |||
steps: { default: () => [] }, | |||
activeColor: { default: 'blue' }, | |||
point: {}, | |||
arrow: {}, | |||
number: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,33 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="title" style="flex-shrink: 0;">{{ title }}</view> | |||
<switch | |||
@change="change" | |||
:checked="value" | |||
:disabled="disabled" | |||
:class="[value ? 'checked' : '', icon ? 'switch-' + icon : '', color ? color : '', radius ? 'radius' : '']" | |||
></switch> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-switch', | |||
props: { | |||
title: {}, | |||
color: {}, | |||
icon: {}, | |||
radius: {}, | |||
disabled: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,55 @@ | |||
<template> | |||
<view | |||
@click="click" | |||
:class="[ | |||
round ? 'round' : '', | |||
radius ? 'radius' : '', | |||
size && !capsule ? size : '', | |||
color && !capsule ? 'bg-' + color : '', | |||
line && !capsule ? 'line-' + line : '', | |||
light && !capsule ? 'light' : '', | |||
capsule ? 'cu-capsule' : 'cu-tag', | |||
badge ? 'badge' : '', | |||
className | |||
]" | |||
:style="rootStyle" | |||
> | |||
<template v-if="capsule"> | |||
<view :class="[size, color ? 'bg-' + color : '']" class="cu-tag tag-icon"><slot name="icon"></slot></view> | |||
<view :class="[size, color ? 'line-' + color : '']" class="cu-tag"><slot></slot></view> | |||
</template> | |||
<template v-else> | |||
<slot></slot> | |||
</template> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-tag', | |||
props: { | |||
round: {}, | |||
radius: {}, | |||
size: { default: 'df' }, | |||
color: {}, | |||
line: {}, | |||
light: {}, | |||
capsule: {}, | |||
badge: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
}, | |||
computed: { | |||
rootStyle() { | |||
return this.getStyle({ zIndex: this.badge ? 2 : 1 }) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,80 @@ | |||
<template> | |||
<view v-if="!simpleMode" :class="className" :style="style"> | |||
<view class="cu-form-group" style="border-bottom: none; padding-bottom: 0;"> | |||
<view class="title"> | |||
<text v-if="required" style="color: red; font-size: 1.2em;">*</text> | |||
{{ title || '' }} | |||
</view> | |||
</view> | |||
<view class="cu-form-group" style="position: relative;border-top: none"> | |||
<textarea | |||
@input="textareaInput" | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceHolder" | |||
:value="value" | |||
:auto-height="autoHeight" | |||
:disabled="readonly" | |||
:enableNative="false" | |||
style="margin-top: 0; border-top: none" | |||
controlled | |||
></textarea> | |||
</view> | |||
</view> | |||
<view v-else :class="className" :style="style" class="cu-form-group" style="position: relative;"> | |||
<textarea | |||
@input="textareaInput" | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceHolder" | |||
:fixed="fixed" | |||
:value="value" | |||
:style="textareaStyle" | |||
:auto-height="autoHeight" | |||
:disabled="readonly" | |||
:enableNative="false" | |||
controlled | |||
></textarea> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-textarea', | |||
props: { | |||
maxlength: { default: -1 }, | |||
readonly: {}, | |||
placeholder: {}, | |||
fixed: {}, | |||
value: {}, | |||
textareaStyle: {}, | |||
autoHeight: {}, | |||
simpleMode: {}, | |||
required: {}, | |||
title: {}, | |||
disabled: {} | |||
}, | |||
methods: { | |||
textareaInput(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceHolder() { | |||
if (this.readonly) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return !this.simpleMode && this.title ? `请输入${this.title}` : '请输入…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,99 @@ | |||
<template> | |||
<l-label @click="click" :class="className" :style="style" :arrow="!disabled" :required="required" :title="title"> | |||
<!-- #ifndef MP-DINGTALK --> | |||
<picker | |||
@change="change" | |||
:value="value" | |||
:start="start" | |||
:end="end" | |||
:disabled="disabled" | |||
:class="[disabled ? '' : 'picker-arrow']" | |||
mode="time" | |||
> | |||
<view class="picker">{{ value || displayPlaceholder }}</view> | |||
</picker> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-DINGTALK --> | |||
<text @click="click">{{ value || displayPlaceholder }}</text> | |||
<!-- #endif --> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-time-picker', | |||
props: { | |||
title: {}, | |||
start: { default: '00:00' }, | |||
end: { default: '23:59' }, | |||
disabled: {}, | |||
placeholder: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
if (this.disabled) { | |||
return | |||
} | |||
this.$emit('click', e) | |||
// #ifdef MP-DINGTALK | |||
const getCurrent = () => { | |||
const now = new Date() | |||
const hours = now | |||
.getHours() | |||
.toString() | |||
.padStart(2, '0') | |||
const minutes = now | |||
.getMinutes() | |||
.toString() | |||
.padStart(2, '0') | |||
return hours + ':' + minutes | |||
} | |||
const currentDate = this.value ? this.value : getCurrent() | |||
this.$emit('open') | |||
dd.datePicker({ | |||
currentDate, | |||
format: 'HH:mm', | |||
success: ({ date }) => { | |||
if (date) { | |||
this.$emit('input', date + ':00') | |||
this.$emit('change', date + ':00') | |||
} | |||
}, | |||
complete: () => { | |||
this.$emit('close') | |||
} | |||
}) | |||
// #endif | |||
}, | |||
change(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,23 @@ | |||
<template> | |||
<view :class="[icon ? 'cuIcon-' + icon : '', className]" :style="style" class="cu-item cu-timeline-item"> | |||
<view :class="['bg-' + color]" :style="contentStyle" class="content shadow-blur"> | |||
<slot name="time"> | |||
<text v-if="time" class="margin-right-sm">{{ time }}</text> | |||
</slot> | |||
<slot></slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-timeline-item', | |||
props: { | |||
time: {}, | |||
icon: {}, | |||
color: { default: 'blue' }, | |||
contentStyle: { default: '' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,18 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-timeline"> | |||
<view class="cu-time"> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
<slot></slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-timeline', | |||
props: { | |||
title: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,59 @@ | |||
<template> | |||
<view v-if="type !== 'default'" :class="className" :style="style" class="cu-bar bg-white cu-title"> | |||
<!-- 下划线标题(普通/变色) --> | |||
<view v-if="type === 'underline' || type === 'colorfulUnderline'" class="action border-title"> | |||
<text :class="['text-' + color, blod ? 'text-blod' : '']" class="text-xl"><slot></slot></text> | |||
<text v-if="type === 'underline'" :class="'bg-' + color" style="width:2rem"></text> | |||
<text v-else-if="type === 'colorfulUnderline'" class="bg-gradual-blue" style="width:3rem"></text> | |||
</view> | |||
<!-- 扩散型下划线标题 --> | |||
<view v-else-if="type === 'blurUnderline'" class="action sub-title"> | |||
<text :class="['text-' + color, blod ? 'text-blod' : '']" class="text-xl"><slot></slot></text> | |||
<text :class="'bg-' + color"></text> | |||
</view> | |||
<!-- 扩散型副标题 --> | |||
<view v-else-if="type === 'blurSubtitle'" class="action sub-title"> | |||
<text :class="[' text-' + color, blod ? 'text-blod' : '']" class="text-xl"><slot></slot></text> | |||
<text :class="[' text-' + color]" class="text-ABC"> | |||
<slot name="subtitle">{{ subtitle }}</slot> | |||
</text> | |||
</view> | |||
<!-- 后缀型副标题 --> | |||
<view v-else-if="type === 'nextSubtitle'" class="action title-style-3"> | |||
<text :class="['text-' + color, blod ? 'text-blod' : '']" class="text-xl"><slot></slot></text> | |||
<text :class="'text-' + color" class="text-Abc self-end margin-left-sm light"> | |||
<slot name="subtitle">{{ subtitle }}</slot> | |||
</text> | |||
</view> | |||
</view> | |||
<!-- 普通标题(圆点/长圆点) --> | |||
<view v-else :class="[border ? 'solid-bottom' : '', className]" :style="style" class="cu-bar bg-white cu-title"> | |||
<view class="action" style="vertical-align:middle;"> | |||
<text | |||
:class="[subColor ? 'text-' + subColor : '', long ? 'cuIcon-titles' : 'cuIcon-title']" | |||
class="margin-right-xs" | |||
></text> | |||
<text :class="[blod ? 'text-blod' : '']" class="text-xl"><slot></slot></text> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-title', | |||
props: { | |||
type: { default: 'default' }, | |||
color: { default: 'black' }, | |||
subColor: { default: 'blue' }, | |||
long: {}, | |||
subtitle: {}, | |||
blod: {}, | |||
border: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,59 @@ | |||
<template> | |||
<view :class="className" :style="style" class="cu-form-group"> | |||
<view class="grid col-4 grid-square flex-sub"> | |||
<view v-for="(path, index) in value" @tap="viewImg" :key="index" class="bg-img"> | |||
<image :src="path" mode="aspectFill"></image> | |||
<view v-if="!readonly" @tap.stop="delImg(index)" class="cu-tag bg-red" style="height: 24px; width: 24px;"> | |||
<l-icon type="close" style="width: 18px;height: 24px;font-size: 24px;" /> | |||
</view> | |||
</view> | |||
<view v-if="!readonly && value.length < Number(number)" @tap="chooseImg" class="solids"> | |||
<l-icon type="cameraadd" /> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-upload', | |||
props: { | |||
number: { default: 1 }, | |||
readonly: {}, | |||
value: { default: () => [] } | |||
}, | |||
methods: { | |||
delImg(index) { | |||
const newList = JSON.parse(JSON.stringify(this.value)) | |||
newList.splice(index, 1) | |||
this.$emit('input', newList) | |||
this.$emit('change') | |||
this.$emit('del') | |||
}, | |||
chooseImg() { | |||
uni.chooseImage({ | |||
count: Number(this.number), | |||
sizeType: ['original', 'compressed'], | |||
sourceType: ['album', 'camera'], | |||
success: ({ tempFilePaths }) => { | |||
const newList = JSON.parse(JSON.stringify(this.value || [])).concat(tempFilePaths) | |||
this.$emit('input', newList) | |||
this.$emit('change') | |||
this.$emit('add') | |||
} | |||
}) | |||
}, | |||
viewImg(index) { | |||
uni.previewImage({ | |||
urls: this.value, | |||
current: this.value[index] | |||
}) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,27 @@ | |||
<template> | |||
<view | |||
:class="[size, round ? 'round' : '', radius ? 'radius' : '', color ? 'bg-' + color : '']" | |||
:style="{ | |||
backgroundImage: src ? `url(${src})` : 'none', | |||
backgroundColor: color || src ? 'none' : '#ccc' | |||
}" | |||
class="cu-avatar" | |||
> | |||
<slot></slot> | |||
<slot name="badge"></slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-avatar', | |||
props: { | |||
src: {}, | |||
size: { default: 'df' }, | |||
round: {}, | |||
radius: {}, | |||
color: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,114 @@ | |||
<template> | |||
<view | |||
v-if="type === 'default'" | |||
:class="[hexColor ? '' : 'bg-' + color, fixed ? 'banner-fixed' : '']" | |||
:style="{ | |||
backgroundColor: hexColor, | |||
boxShadow: noshadow ? 'none' : '0 1rpx 6rpx rgba(0, 0, 0, 0.1)' | |||
}" | |||
class="cu-bar cu-banner" | |||
> | |||
<view @click="clickLeft" class="action"><slot name="left"></slot></view> | |||
<view @click="clickCenter" class="content"> | |||
<slot>{{ title }}</slot> | |||
</view> | |||
<view @click="clickRight" class="action"><slot name="right"></slot></view> | |||
</view> | |||
<view | |||
v-else-if="type === 'search'" | |||
:class="[hexColor ? '' : 'bg-' + color, fixed ? 'banner-fixed' : '']" | |||
:style="{ | |||
backgroundColor: hexColor, | |||
boxShadow: noshadow ? 'none' : '0 1rpx 6rpx rgba(0, 0, 0, 0.1)' | |||
}" | |||
class="cu-bar search cu-banner" | |||
> | |||
<slot name="left"></slot> | |||
<view class="search-form" :class="[fill ? 'radius' : 'round']" :style="inputStyle"> | |||
<slot name="searchInput"> | |||
<text class="cuIcon-search"></text> | |||
<input | |||
@input="searchTextChange" | |||
@focus="$emit('inputFocus', $event)" | |||
:placeholder-style="placeholderStyle" | |||
:adjust-position="false" | |||
:placeholder="placeholder" | |||
:value="value" | |||
:focus="focus" | |||
confirm-type="search" | |||
type="text" | |||
ref="bannerInput" | |||
/> | |||
</slot> | |||
</view> | |||
<view v-if="!noSearchButton" class="action"> | |||
<slot name="searchButton"> | |||
<template v-if="fill"> | |||
<view @click="searchClick">搜索</view> | |||
</template> | |||
<button v-else @click="searchClick" class="cu-btn bg-green shadow-blur round">搜索</button> | |||
</slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-banner', | |||
props: { | |||
type: { default: 'default' }, | |||
color: { default: 'white' }, | |||
hexColor: {}, | |||
inputStyle: {}, | |||
fill: {}, | |||
title: {}, | |||
value: {}, | |||
placeholder: { default: '搜索图片、文章、视频' }, | |||
noSearchButton: {}, | |||
placeholderStyle: {}, | |||
fixed: {}, | |||
noshadow: {}, | |||
focus: {} | |||
}, | |||
methods: { | |||
searchTextChange(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
}, | |||
clickLeft(e) { | |||
this.$emit('clickLeft', e) | |||
}, | |||
clickRight(e) { | |||
this.$emit('clickRight', e) | |||
}, | |||
clickCenter(e) { | |||
this.$emit('clickCenter', e) | |||
}, | |||
searchClick(F) { | |||
this.$emit('searchClick') | |||
}, | |||
inputFocus() { | |||
this.$refs.bannerInput.focus() | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.banner-fixed { | |||
position: fixed; | |||
width: 100%; | |||
top: var(--window-top); | |||
z-index: 1024; | |||
} | |||
</style> |
@@ -0,0 +1,14 @@ | |||
<template> | |||
<view :class="['bg-' + color]" class="submit cu-bar-item-button">{{ title }}</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar-item-button', | |||
props: { | |||
title: {}, | |||
color: { default: 'gray' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,37 @@ | |||
<template> | |||
<view v-if="type === 'default'" @click="click" class="action cu-bar-item"> | |||
<slot> | |||
<l-icon :type="icon" :color="color" size="lg" class="cuIcon-cu-image" /> | |||
<slot name="badge"></slot> | |||
<view :class="'text-' + color"> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
</slot> | |||
</view> | |||
<view v-else-if="type === 'round'" class="action text-gray add-action cu-bar-item"> | |||
<slot> | |||
<button :class="['bg-' + color, 'cuIcon-' + icon]" class="cu-btn shadow"></button> | |||
<slot name="title">{{ title }}</slot> | |||
</slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar-item', | |||
props: { | |||
type: { default: 'default' }, | |||
title: {}, | |||
icon: {}, | |||
color: { default: 'gray' } | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,14 @@ | |||
<template> | |||
<view :class="[dark ? 'bg-black' : 'bg-white']" class="cu-bar tabbar"><slot></slot></view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-bar', | |||
props: { | |||
msginput: {}, | |||
dark: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,52 @@ | |||
<template> | |||
<button | |||
@click="click" | |||
:disabled="disabled ? 'disabled' : null" | |||
:type="disabled && line ? 'default' : null" | |||
:class="[size, round ? 'round' : '', shadow ? 'shadow' : '', buttonColor, buttonBorder, block ? 'block' : '']" | |||
hover-class="button-hover" | |||
class="cu-btn" | |||
> | |||
<slot></slot> | |||
</button> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-button', | |||
props: { | |||
size: { default: 'df' }, | |||
color: {}, | |||
round: {}, | |||
shadow: {}, | |||
line: {}, | |||
blod: {}, | |||
icon: {}, | |||
disabled: {}, | |||
block: {} | |||
}, | |||
computed: { | |||
buttonBorder() { | |||
return this.line ? `line${this.blod ? 's' : ''}-${this.line || 'gray'}` : '' | |||
}, | |||
buttonColor() { | |||
if (this.color) { | |||
return `bg-${this.color}` | |||
} else if (!this.line) { | |||
return `bg-gray` | |||
} | |||
return '' | |||
} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,82 @@ | |||
<template> | |||
<view :class="[outline ? '' : 'no-card', type !== 'base' ? type : '']" class="cu-card"> | |||
<view class="cu-item shadow"> | |||
<template v-if="type === 'case'"> | |||
<view class="image"> | |||
<slot name="img"><image :src="info.img" mode="widthFix"></image></slot> | |||
<slot name="badge"> | |||
<l-tag :color="info.badgeColor">{{ info.badge }}</l-tag> | |||
</slot> | |||
<view class="cu-bar bg-shadeBottom"> | |||
<slot name="title"> | |||
<text class="text-cut">{{ info.title }}</text> | |||
</slot> | |||
</view> | |||
</view> | |||
<view class="cu-list menu-avatar"> | |||
<view class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar :src="info.avatar" size="lg" style="margin: 0 30rpx; position: absolute; left: 0;" round /> | |||
</slot> | |||
<view class="content flex-sub"> | |||
<slot name="user"> | |||
<view class="text-grey">{{ info.user }}</view> | |||
</slot> | |||
<view class="text-gray text-sm flex justify-between"> | |||
<slot name="tips">{{ info.tips }}</slot> | |||
<slot name="footer"></slot> | |||
</view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<template v-else-if="type === 'dynamic'"> | |||
<view class="cu-list menu-avatar"> | |||
<view class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar :src="info.avatar" size="lg" style="margin: 0 30rpx; position: absolute; left: 0;" round /> | |||
</slot> | |||
<view class="content flex-sub"> | |||
<slot name="user"> | |||
<view>{{ info.user }}</view> | |||
</slot> | |||
<slot name="tips"> | |||
<view class="text-gray text-sm flex justify-between">{{ info.tips }}</view> | |||
</slot> | |||
</view> | |||
</view> | |||
</view> | |||
<slot></slot> | |||
<slot name="footer"></slot> | |||
</template> | |||
<template v-else-if="type === 'article'"> | |||
<view class="title"> | |||
<slot name="title"> | |||
<view class="text-cut">{{ info.title }}</view> | |||
</slot> | |||
</view> | |||
<view class="content"> | |||
<slot name="img"><image :src="info.img" mode="aspectFill"></image></slot> | |||
<view class="desc"> | |||
<view class="text-content"><slot></slot></view> | |||
<slot name="footer"></slot> | |||
</view> | |||
</view> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-card', | |||
props: { | |||
type: { default: 'case' }, | |||
outline: {}, | |||
info: { default: () => ({}) } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,63 @@ | |||
<template> | |||
<view :style="[{ bottom: inputBottom + 'px' }]" class="cu-bar foot input"> | |||
<input | |||
@focus="inputFocus" | |||
@blur="inputBlur" | |||
@input="input" | |||
@confirm="sendClick" | |||
:value="value" | |||
:adjust-position="false" | |||
:enableNative="false" | |||
:focus="false" | |||
:placeholder="placeholder" | |||
:disabled="inputDisabled" | |||
:confirm-hold="confirmHold" | |||
cursor-spacing="10" | |||
class="solid-bottom" | |||
confirm-type="send" | |||
type="text" | |||
/> | |||
<slot name="action"> | |||
<button @click="sendClick" :disabled="buttonDisabled" class="cu-btn bg-green shadow">发送</button> | |||
</slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat-input', | |||
props: { | |||
value: {}, | |||
placeholder: {}, | |||
inputDisabled: {}, | |||
buttonDisabled: {}, | |||
confirmHold: {} | |||
}, | |||
data() { | |||
return { inputBottom: 0 } | |||
}, | |||
methods: { | |||
input(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
}, | |||
inputFocus(e) { | |||
this.inputBottom = e.detail.height | |||
this.$emit('focus') | |||
}, | |||
inputBlur(e) { | |||
this.inputBottom = 0 | |||
this.$emit('blur') | |||
}, | |||
sendClick(e) { | |||
this.$emit('sendMsg', this.value) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,80 @@ | |||
<template> | |||
<view :class="[type === 'left' ? '' : 'self']" class="cu-item"> | |||
<slot v-if="type === 'left'" name="avatar"> | |||
<view | |||
v-if="type === 'left' && avatar" | |||
:style="{ backgroundImage: `url(${avatar})`, borderRadius: roundAvatar ? '50%' : '3px' }" | |||
class="cu-avatar radius" | |||
></view> | |||
<image | |||
v-else-if="type === 'left' && imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'};` + imgStyle" | |||
mode="aspectFill" | |||
class="chat-msg-imgAvatar cu-avatar radius" | |||
></image> | |||
<l-icon | |||
v-else-if="type === 'left' && iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
/> | |||
</slot> | |||
<view class="main"> | |||
<view | |||
:class="[type === 'left' ? (leftColor ? 'bg-' + leftColor : '') : rightColor ? 'bg-' + rightColor : '']" | |||
class="content shadow" | |||
> | |||
<text style="word-break: break-all;"> | |||
<slot>{{ content }}</slot> | |||
</text> | |||
</view> | |||
</view> | |||
<slot v-if="type !== 'left'" name="avatar"> | |||
<view | |||
v-if="type !== 'left' && avatar" | |||
:style="{ backgroundImage: `url(${avatar})`, borderRadius: roundAvatar ? '50%' : '3px' }" | |||
class="cu-avatar radius" | |||
></view> | |||
<image | |||
v-else-if="type !== 'left' && imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'};` + imgStyle" | |||
mode="aspectFill" | |||
class="chat-msg-imgAvatar cu-avatar radius" | |||
></image> | |||
<l-icon | |||
v-else-if="type !== 'left' && iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="chat-msg-iconAvatar flex justify-center align-center" | |||
/> | |||
</slot> | |||
<view class="date"> | |||
<slot name="date">{{ date }}</slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat-msg', | |||
props: { | |||
content: {}, | |||
type: { default: 'left' }, | |||
roundAvatar: {}, | |||
date: {}, | |||
leftColor: {}, | |||
rightColor: { default: 'green' }, | |||
avatar: {}, | |||
iconAvatar: {}, | |||
iconStyle: { default: '' }, | |||
imgAvatar: {}, | |||
imgStyle: { default: '' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,20 @@ | |||
<template> | |||
<view class="cu-chat"> | |||
<slot></slot> | |||
<view v-if="empty" class="padding text-gray text-center">{{ emptyTips }}</view> | |||
<view v-if="!empty && nomore" class="padding text-gray text-center">{{ nomoreTips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-chat', | |||
props: { | |||
empty: {}, | |||
emptyTips: { default: '暂无消息' }, | |||
nomore: { default: true }, | |||
nomoreTips: { default: '没有更多消息了' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,114 @@ | |||
<template> | |||
<l-label @click="click" :arrow="!readonly" :required="required" :title="title"> | |||
{{ display || displayPlaceholder }} | |||
<l-modal | |||
v-if="modal" | |||
@ok="confirm" | |||
@cancel="cancel" | |||
@checkboxChange="checkboxChange" | |||
:checkbox="innerChecked" | |||
:value="modalOpen" | |||
:range="range" | |||
:readonly="readonly" | |||
type="checkbox" | |||
/> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-checkbox-picker', | |||
props: { | |||
value: {}, | |||
range: { default: () => [] }, | |||
title: {}, | |||
required: {}, | |||
placeholder: {}, | |||
readonly: {} | |||
}, | |||
data() { | |||
return { | |||
modal: false, | |||
modalOpen: false, | |||
innerChecked: this.value || [] | |||
} | |||
}, | |||
methods: { | |||
click() { | |||
if (this.readonly || this.modal || this.modalOpen) { | |||
return | |||
} | |||
this.innerChecked = this.value | |||
this.modal = true | |||
setTimeout(() => { | |||
this.modalOpen = true | |||
uni.$emit('modal', true) | |||
this.$emit('open') | |||
}, 300) | |||
}, | |||
checkboxChange(newVal) { | |||
this.innerChecked = newVal | |||
}, | |||
confirm() { | |||
this.modalOpen = false | |||
setTimeout(() => { | |||
this.modal = false | |||
this.$emit('input', this.innerChecked) | |||
this.$emit('change', this.innerChecked) | |||
uni.$emit('modal', false) | |||
this.$emit('close') | |||
}, 300) | |||
}, | |||
cancel() { | |||
this.modalOpen = false | |||
setTimeout(() => { | |||
this.modal = false | |||
uni.$emit('modal', false) | |||
this.$emit('close') | |||
}, 300) | |||
} | |||
}, | |||
computed: { | |||
display() { | |||
if (this.value.length <= 0) { | |||
return null | |||
} | |||
if (!this.objMode) { | |||
return this.value.join(',') | |||
} | |||
return this.value | |||
.reduce((a, b) => { | |||
const selected = this.range.find(t => t.value === b) | |||
return selected ? a.concat(selected.text) : a | |||
}, []) | |||
.join(',') | |||
}, | |||
displayPlaceholder() { | |||
if (this.readonly) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
}, | |||
objMode() { | |||
return typeof this.range[0] === 'object' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,49 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title">{{ title }}</view> | |||
<checkbox | |||
@click="change" | |||
:checked="isCheck" | |||
:value="checkboxValue" | |||
:disabled="disabled" | |||
:class="[isCheck ? 'checked' : '', color ? color : '', round ? 'round' : '']" | |||
></checkbox> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-checkbox', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
round: {}, | |||
color: {}, | |||
checkboxValue: {}, | |||
value: { default: () => [] } | |||
}, | |||
methods: { | |||
change(e) { | |||
let arr = this.value | |||
const isCurrentCheck = this.value.includes(this.checkboxValue) | |||
if (isCurrentCheck) { | |||
arr = arr.filter(t => t !== this.checkboxValue) | |||
} else { | |||
arr.push(this.checkboxValue) | |||
} | |||
this.$emit('input', arr) | |||
this.$emit('change', arr) | |||
} | |||
}, | |||
computed: { | |||
isCheck() { | |||
return this.value.includes(this.checkboxValue) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,60 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<picker | |||
@change="change" | |||
:fields="fields" | |||
:disabled="disabled" | |||
:value="value" | |||
:start="start" | |||
:end="end" | |||
:class="[disabled ? '' : 'picker-arrow']" | |||
mode="date" | |||
> | |||
<view class="picker"> | |||
<slot>{{ value || displayPlaceholder }}</slot> | |||
</view> | |||
</picker> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-date-picker', | |||
props: { | |||
title: {}, | |||
start: { default: '1900-01-01' }, | |||
end: { default: '2100-01-01' }, | |||
fields: { default: 'day' }, | |||
disabled: {}, | |||
placeholder: {}, | |||
required: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择日期…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,621 @@ | |||
<template> | |||
<view class="lr-datetime-picker"> | |||
<view | |||
@tap.stop="maskClick" | |||
@touchmove.stop.prevent | |||
:class="{ show: showPicker }" | |||
class="lr-datetime-mask" | |||
catchtouchmove="true" | |||
></view> | |||
<view :class="{ show: showPicker }" class="lr-datetime-content"> | |||
<view @touchmove.stop.prevent catchtouchmove="true" class="lr-datetime-hd"> | |||
<view @tap.stop="pickerCancel" class="lr-datetime-btn">取消</view> | |||
<view @tap.stop="pickerConfirm" :style="{ color: color }" class="lr-datetime-btn">确定</view> | |||
</view> | |||
<view class="lr-datetime-view"> | |||
<picker-view | |||
v-if="isAll" | |||
@change="bindChange" | |||
:indicator-style="itemHeight" | |||
:value="values" | |||
style="height: 100%;" | |||
> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.years" :key="index" class="lr-datetime-item">{{ item }}年</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.months" :key="index" class="lr-datetime-item">{{ item }}月</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.days" :key="index" class="lr-datetime-item">{{ item }}日</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.hours" :key="index" class="lr-datetime-item">{{ item }}时</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.minutes" :key="index" class="lr-datetime-item">{{ item }}分</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.seconds" :key="index" class="lr-datetime-item">{{ item }}秒</view> | |||
</picker-view-column> | |||
</picker-view> | |||
<picker-view | |||
v-else | |||
@change="bindDateChange" | |||
:indicator-style="itemHeight" | |||
:value="dateValues" | |||
style="height: 100%;" | |||
> | |||
<picker-view-column class="yu-picker-column"> | |||
<view v-for="(item, index) in dateObj.dates" :key="index" class="lr-datetime-item">{{ item }}</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.hours" :key="index" class="lr-datetime-item">{{ item }}时</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.minutes" :key="index" class="lr-datetime-item">{{ item }}分</view> | |||
</picker-view-column> | |||
<picker-view-column> | |||
<view v-for="(item, index) in dateObj.seconds" :key="index" class="lr-datetime-item">{{ item }}秒</view> | |||
</picker-view-column> | |||
</picker-view> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-datetime-panel', | |||
props: { | |||
//全部日期有效可选 | |||
isAll: { default: true }, | |||
//颜色 | |||
color: { default: '#1aad19' }, | |||
//开始年份 | |||
startYear: { default: '1900' }, | |||
//结束年份 | |||
endYear: { default: '2100' }, | |||
//设置默认日期时间 | |||
val: { default: '' } | |||
}, | |||
data() { | |||
let date = new Date() | |||
let year = date.getFullYear() | |||
let month = date.getMonth() + 1 | |||
let day = date.getDate() | |||
let hour = date.getHours() | |||
let minute = date.getMinutes() | |||
let second = date.getSeconds() | |||
let dates = [] | |||
let months = [] | |||
let years = [] | |||
let days = [] | |||
let hours = [] | |||
let minutes = [] | |||
let seconds = [] | |||
for (let i = month; i <= month + 2; i++) { | |||
//获取包括当前月份在内的3个月内的日期 | |||
let localMonth = i | |||
let localYear = year | |||
if (i == 13) { | |||
localYear += 1 | |||
} | |||
if (i >= 13) { | |||
localMonth -= 12 | |||
} | |||
let total = new Date(localYear, localMonth, 0).getDate() | |||
if (i == month) { | |||
for (let j = day; j <= total; j++) { | |||
let m = localMonth | |||
let d = j | |||
if (localMonth < 10) { | |||
m = '0' + m | |||
} | |||
if (j < 10) { | |||
d = '0' + d | |||
} | |||
let str = year + '-' + m + '-' + d | |||
dates.push(str) | |||
} | |||
} else { | |||
for (let j = 1; j <= total; j++) { | |||
let m = localMonth | |||
let d = j | |||
if (localMonth < 10) { | |||
m = '0' + m | |||
} | |||
if (j < 10) { | |||
d = '0' + d | |||
} | |||
let str = year + '-' + m + '-' + d | |||
dates.push(str) | |||
} | |||
} | |||
} | |||
for (let i = parseInt(this.startYear); i <= this.endYear; i++) { | |||
years.push(i) | |||
} | |||
for (let i = 1; i <= 12; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
months.push(str) | |||
} | |||
if (this.val) { | |||
let valueArr = this.val.split(' ') | |||
let valueDateArr = valueArr[0].split('-') | |||
let totalCurrent = new Date(valueDateArr[0], valueDateArr[1], 0).getDate() | |||
for (let i = 1; i <= totalCurrent; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
days.push(str) | |||
} | |||
} else { | |||
let totalCurrent = new Date(year, month, 0).getDate() | |||
for (let i = 1; i <= totalCurrent; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
days.push(str) | |||
} | |||
} | |||
for (let i = 0; i < 24; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
hours.push(str) | |||
} | |||
for (let i = 0; i < 60; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
minutes.push(str) | |||
} | |||
for (let i = 0; i < 60; i++) { | |||
let str = i | |||
if (i < 10) { | |||
str = '0' + str | |||
} else { | |||
str = '' + str | |||
} | |||
seconds.push(str) | |||
} | |||
let dateObj = { | |||
dates, | |||
years, | |||
months, | |||
days, | |||
hours, | |||
minutes, | |||
seconds | |||
} | |||
return { | |||
value: this.val, | |||
year, | |||
month, | |||
day, | |||
hour, | |||
minute, | |||
second, | |||
dateObj, | |||
itemHeight: `height: ${uni.upx2px(88)}px;`, | |||
values: [0, 0, 0, 0, 0, 0], | |||
selectArr: [], | |||
selectRes: '', | |||
showPicker: false, | |||
dateValues: [0, 0, 0, 0] | |||
} | |||
}, | |||
mounted() { | |||
this.setDate() | |||
}, | |||
methods: { | |||
setDate(dt) { | |||
if (dt) { | |||
this.value = dt | |||
} | |||
this.initDate() | |||
if (!this.value) { | |||
this.showCurrent() | |||
} | |||
}, | |||
initDate() { | |||
let _this = this | |||
//解析默认显示的日期时间 | |||
if (_this.value) { | |||
let values = [0, 0, 0, 0, 0, 0] | |||
let dateValues = [0, 0, 0, 0] | |||
let valueStr = _this.value | |||
let valueArr = valueStr.split(' ') | |||
let valueDateArr = valueArr[0].split('-') | |||
let valueTimeArr = valueArr[1].split(':') | |||
if (_this.isAll) { | |||
values[0] = valueDateArr[0] - _this.startYear > 0 ? valueDateArr[0] - _this.startYear : 0 | |||
values[1] = parseInt(valueDateArr[1]) - 1 | |||
values[2] = parseInt(valueDateArr[2]) - 1 | |||
values[3] = parseInt(valueTimeArr[0]) | |||
values[4] = parseInt(valueTimeArr[1]) | |||
values[5] = parseInt(valueTimeArr[2]) | |||
_this.$nextTick(() => { | |||
_this.values = values | |||
}) | |||
if (valueDateArr[0] - _this.startYear >= 0) { | |||
_this.selectArr = [ | |||
valueDateArr[0], | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = _this.value | |||
} else { | |||
_this.selectArr = [ | |||
_this.formatNum(_this.startYear), | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = `${this.formatNum(_this.startYear) + | |||
'-' + | |||
valueDateArr[1] + | |||
'-' + | |||
valueDateArr[2] + | |||
' ' + | |||
valueTimeArr[0] + | |||
':' + | |||
valueTimeArr[1] + | |||
':' + | |||
valueTimeArr[2]}` | |||
} | |||
} else { | |||
let str = valueDateArr.join('') | |||
let localStr = _this.formatNum(_this.year) + _this.formatNum(_this.month) + _this.formatNum(_this.day) + '' | |||
if (str < localStr) { | |||
dateValues[0] = 0 | |||
_this.selectArr = [ | |||
_this.formatNum(_this.year), | |||
_this.formatNum(_this.month), | |||
_this.formatNum(_this.day), | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = `${_this.formatNum(_this.year) + | |||
'-' + | |||
_this.formatNum(_this.month) + | |||
'-' + | |||
_this.formatNum(_this.day) + | |||
' ' + | |||
valueTimeArr[0] + | |||
':' + | |||
valueTimeArr[1] + | |||
':' + | |||
valueTimeArr[2]}` | |||
} else { | |||
let num = 0 //计算默认日期和当前日期相隔天数,计算下标 | |||
let start = | |||
_this.formatNum(_this.year) + '-' + _this.formatNum(_this.month) + '-' + _this.formatNum(_this.day) | |||
let res = _this.getBetweenDateStr(start, valueArr[0]) | |||
dateValues[0] = res.length - 1 | |||
_this.selectArr = [ | |||
valueDateArr[0], | |||
valueDateArr[1], | |||
valueDateArr[2], | |||
valueTimeArr[0], | |||
valueTimeArr[1], | |||
valueTimeArr[2] | |||
] | |||
_this.selectRes = _this.value | |||
} | |||
dateValues[1] = parseInt(valueTimeArr[0]) | |||
dateValues[2] = parseInt(valueTimeArr[1]) | |||
dateValues[3] = parseInt(valueTimeArr[2]) | |||
_this.$nextTick(() => { | |||
_this.dateValues = dateValues | |||
}) | |||
} | |||
return | |||
} | |||
if (_this.isAll) { | |||
_this.selectArr = [_this.formatNum(_this.startYear), '01', '01', '00', '00', '00'] | |||
_this.selectRes = `${_this.formatNum(_this.startYear) + '-01-01 00:00:00'}` | |||
} else { | |||
_this.selectArr = [_this.formatNum(_this.year), _this.formatNum(_this.month), '01', '00', '00', '00'] | |||
_this.selectRes = `${_this.formatNum(_this.year) + '-' + _this.formatNum(_this.month) + '-01 00:00:00'}` | |||
} | |||
}, | |||
showCurrent() { | |||
//显示当前的日期时间 | |||
let arr = [0, 0, 0, 0, 0, 0] | |||
let dateArr = [0, 0, 0, 0] | |||
this.selectArr = [ | |||
this.formatNum(this.year), | |||
this.formatNum(this.month), | |||
this.formatNum(this.day), | |||
this.formatNum(this.hour), | |||
this.formatNum(this.minute), | |||
this.formatNum(this.second) | |||
] | |||
this.selectRes = `${this.formatNum(this.year) + | |||
'-' + | |||
this.formatNum(this.month) + | |||
'-' + | |||
this.formatNum(this.day) + | |||
' ' + | |||
this.formatNum(this.hour) + | |||
':' + | |||
this.formatNum(this.minute) + | |||
':' + | |||
this.formatNum(this.second)}` | |||
if (this.isAll) { | |||
arr[0] = this.year - this.startYear | |||
arr[1] = this.month - 1 | |||
arr[2] = this.day - 1 | |||
arr[3] = this.hour | |||
arr[4] = this.minute | |||
arr[5] = this.second | |||
this.$nextTick(() => { | |||
this.values = arr | |||
}) | |||
} else { | |||
dateArr[1] = this.hour | |||
dateArr[2] = this.minute | |||
dateArr[3] = this.second | |||
this.$nextTick(() => { | |||
this.dateValues = dateArr | |||
}) | |||
} | |||
}, | |||
initDayArr: (year, month) => { | |||
//初始化月份天数 | |||
let totalDay = new Date(year, month, 0).getDate() | |||
let dayArr = [] | |||
for (let i = 1; i <= totalDay; i++) { | |||
if (i < 10) { | |||
i = '0' + i | |||
} else { | |||
i = i + '' | |||
} | |||
dayArr.push(i) | |||
} | |||
return dayArr | |||
}, | |||
formatNum(num) { | |||
//日期时间的初始化 | |||
return num < 10 ? '0' + num : num + '' | |||
}, | |||
maskClick() { | |||
//日期时间的遮罩 | |||
this.showPicker = false | |||
this.$emit('cancel') | |||
}, | |||
show() { | |||
//日期时间的显示 | |||
this.showPicker = true | |||
}, | |||
hide() { | |||
//日期时间的隐藏 | |||
this.showPicker = false | |||
this.$emit('cancel') | |||
}, | |||
pickerCancel() { | |||
//日期时间取消 | |||
this.showPicker = false | |||
this.$emit('cancel', { | |||
selectArr: this.selectArr | |||
}) | |||
}, | |||
pickerConfirm(e) { | |||
//日期时间确定 | |||
this.showPicker = false | |||
this.$emit('confirm', { | |||
selectArr: this.selectArr, | |||
selectRes: this.selectRes | |||
}) | |||
}, | |||
bindChange(e) { | |||
//默认滚动日期时间方法 | |||
let valueArr = e.detail.value | |||
let year = '', | |||
month = '', | |||
day = '', | |||
hour = '', | |||
minute = '', | |||
second = '' | |||
let selectArr = this.selectArr | |||
let dayArr = [] | |||
year = this.dateObj.years[valueArr[0]] | |||
month = this.dateObj.months[valueArr[1]] | |||
day = this.dateObj.days[valueArr[2]] | |||
hour = this.dateObj.hours[valueArr[3]] | |||
minute = this.dateObj.minutes[valueArr[4]] | |||
second = this.dateObj.seconds[valueArr[5]] | |||
if (year != selectArr[0]) { | |||
dayArr = this.initDayArr(year, month) | |||
this.dateObj.days = dayArr | |||
} | |||
if (month != selectArr[1]) { | |||
dayArr = this.initDayArr(year, month) | |||
this.dateObj.days = dayArr | |||
} | |||
this.selectArr = [year, month, day, hour, minute, second] | |||
this.selectRes = `${year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second}` | |||
this.$nextTick(() => { | |||
this.values = valueArr | |||
}) | |||
}, | |||
bindDateChange(e) { | |||
//有效日期的滚动日期时间方法 | |||
let valueArr = e.detail.value | |||
let dateStr = '', | |||
dateArr = [], | |||
hour = '', | |||
minute = '', | |||
second = '' | |||
let selectArr = this.selectArr | |||
let dayArr = [] | |||
dateStr = this.dateObj.dates[valueArr[0]] | |||
dateArr = dateStr.split('-') | |||
hour = this.dateObj.hours[valueArr[1]] | |||
minute = this.dateObj.minutes[valueArr[2]] | |||
second = this.dateObj.seconds[valueArr[3]] | |||
this.selectArr = [dateArr[0], dateArr[1], dateArr[2], hour, minute, second] | |||
this.selectRes = `${dateArr[0] + '-' + dateArr[1] + '-' + dateArr[2] + ' ' + hour + ':' + minute + ':' + second}` | |||
this.$nextTick(() => { | |||
this.dateValues = valueArr | |||
}) | |||
}, | |||
//遍历两个日期间的所有日期 | |||
getBetweenDateStr(start, end) { | |||
let result = [] | |||
let beginDay = start.split('-') | |||
let endDay = end.split('-') | |||
let diffDay = new Date() | |||
let dateList = new Array() | |||
let i = 0 | |||
diffDay.setDate(beginDay[2]) | |||
diffDay.setMonth(beginDay[1] - 1) | |||
diffDay.setFullYear(beginDay[0]) | |||
result.push(start) | |||
while (i == 0) { | |||
let countDay = diffDay.getTime() + 24 * 60 * 60 * 1000 | |||
diffDay.setTime(countDay) | |||
dateList[2] = diffDay.getDate() | |||
dateList[1] = diffDay.getMonth() + 1 | |||
dateList[0] = diffDay.getFullYear() | |||
if (String(dateList[1]).length == 1) { | |||
dateList[1] = '0' + dateList[1] | |||
} | |||
if (String(dateList[2]).length == 1) { | |||
dateList[2] = '0' + dateList[2] | |||
} | |||
result.push(dateList[0] + '-' + dateList[1] + '-' + dateList[2]) | |||
if (dateList[0] == endDay[0] && dateList[1] == endDay[1] && dateList[2] == endDay[2]) { | |||
i = 1 | |||
} | |||
} | |||
return result | |||
} | |||
} | |||
} | |||
</script> | |||
<style> | |||
.lr-datetime-picker { | |||
position: relative; | |||
z-index: 888; | |||
} | |||
.lr-datetime-mask { | |||
position: fixed; | |||
z-index: 1000; | |||
top: 0; | |||
right: 0; | |||
left: 0; | |||
bottom: 0; | |||
background: rgba(0, 0, 0, 0.4); | |||
visibility: hidden; | |||
opacity: 0; | |||
transition: all 0.3s ease; | |||
} | |||
.lr-datetime-mask.show { | |||
visibility: visible; | |||
opacity: 1; | |||
} | |||
.lr-datetime-content { | |||
position: fixed; | |||
bottom: 0; | |||
left: 0; | |||
width: 100%; | |||
transition: all 0.3s ease; | |||
transform: translateY(100%); | |||
z-index: 3000; | |||
} | |||
.lr-datetime-content.show { | |||
transform: translateY(0); | |||
} | |||
.lr-datetime-hd { | |||
display: flex; | |||
align-items: center; | |||
padding: 0 30upx; | |||
height: 44px; | |||
background-color: #fff; | |||
position: relative; | |||
text-align: center; | |||
font-size: 15px; | |||
justify-content: space-between; | |||
} | |||
.lr-datetime-btn { | |||
font-size: 14px; | |||
} | |||
.lr-datetime-hd:after { | |||
content: ' '; | |||
position: absolute; | |||
left: 0; | |||
bottom: 0; | |||
right: 0; | |||
height: 1px; | |||
border-bottom: 1px solid #e5e5e5; | |||
color: #e5e5e5; | |||
transform-origin: 0 100%; | |||
transform: scaleY(0.5); | |||
} | |||
.lr-datetime-view { | |||
width: 100%; | |||
height: 200px; | |||
overflow: hidden; | |||
background-color: rgba(255, 255, 255, 1); | |||
z-index: 666; | |||
} | |||
.yu-picker-column { | |||
-webkit-flex: 2; | |||
-webkit-box-flex: 2; | |||
flex: 2; | |||
} | |||
.lr-datetime-item { | |||
text-align: center; | |||
width: 100%; | |||
height: 88upx; | |||
line-height: 88upx; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
font-size: 30upx; | |||
} | |||
</style> |
@@ -0,0 +1,82 @@ | |||
<template> | |||
<l-label @click="click" :arrow="!disabled" :required="required" :title="title"> | |||
{{ value || displayPlaceholder }} | |||
<l-datetime-panel | |||
v-if="datetimeModal" | |||
@confirm="confirm" | |||
@cancel="cancel" | |||
:val="value" | |||
:startYear="1900" | |||
ref="datetime" | |||
isAll | |||
/> | |||
</l-label> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-datetime-picker', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
placeholder: {}, | |||
required: {}, | |||
value: {} | |||
}, | |||
data() { | |||
return { | |||
datetimeModal: false | |||
} | |||
}, | |||
methods: { | |||
click(e) { | |||
if (this.disabled || this.datetimeModal) { | |||
return | |||
} | |||
this.datetimeModal = true | |||
this.$nextTick(() => { | |||
this.$refs.datetime.setDate(this.value) | |||
this.$refs.datetime.show() | |||
uni.$emit('modal', true) | |||
this.$emit('open') | |||
}) | |||
}, | |||
confirm({ selectRes }) { | |||
setTimeout(() => { | |||
this.datetimeModal = false | |||
this.$emit('input', selectRes) | |||
this.$emit('change', selectRes) | |||
uni.$emit('modal', false) | |||
this.$emit('close') | |||
}, 300) | |||
}, | |||
cancel() { | |||
setTimeout(() => { | |||
this.datetimeModal = false | |||
}, 300) | |||
uni.$emit('modal', false) | |||
this.$emit('close') | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,23 @@ | |||
<template> | |||
<text @click="click" :class="['cuIcon', 'cuIcon-' + type, color ? 'text-' + color : '', shadow ? 'text-shadow' : '']"> | |||
<slot></slot> | |||
</text> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-icon', | |||
props: { | |||
color: {}, | |||
type: { require: true }, | |||
shadow: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,58 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
<input | |||
@input="input" | |||
:value="value" | |||
:type="type" | |||
:disabled="disabled" | |||
:password="password" | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceholder" | |||
:style="{ textAlign: left ? 'left' : 'right' }" | |||
/> | |||
<slot name="suffix"></slot> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-input', | |||
props: { | |||
type: { default: 'text' }, | |||
title: {}, | |||
placeholder: {}, | |||
maxlength: { default: -1 }, | |||
disabled: {}, | |||
password: {}, | |||
left: {}, | |||
value: {}, | |||
required: {} | |||
}, | |||
methods: { | |||
input(e) { | |||
this.$emit('input', e.detail.value) | |||
this.$emit('change', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请输入${this.title}` : `请输入…` | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,45 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<slot name="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</slot> | |||
</view> | |||
<view @click="itemClick" :class="[arrow ? 'form-item-arrow' : 'form-item-noarrow']" class="action form-item-action"> | |||
<view class="picker"><slot></slot></view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-label', | |||
props: { | |||
title: {}, | |||
arrow: {}, | |||
required: {} | |||
}, | |||
methods: { | |||
itemClick(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> | |||
<style> | |||
.form-item-action { | |||
flex: 1; | |||
padding-right: 40rpx; | |||
overflow: hidden; | |||
position: relative; | |||
text-align: right; | |||
} | |||
.form-item-noarrow { | |||
padding-right: 0; | |||
} | |||
</style> |
@@ -0,0 +1,79 @@ | |||
<template> | |||
<view @click="$emit('click', $event)" :class="arrow ? 'arrow' : ''" class="cu-item"> | |||
<slot name="avatar"> | |||
<l-avatar | |||
v-if="avatar" | |||
:src="avatar" | |||
:radius="!roundAvatar" | |||
:round="roundAvatar" | |||
:style="avatarStyle" | |||
size="lg" | |||
/> | |||
<image | |||
v-else-if="imgAvatar" | |||
:src="imgAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + imgStyle" | |||
mode="aspectFill" | |||
class="list-item-imgAvatar" | |||
/> | |||
<l-icon | |||
v-else-if="iconAvatar" | |||
:type="iconAvatar" | |||
:style="`border-radius: ${roundAvatar ? '50%' : '3px'}; ` + iconStyle" | |||
class="list-item-iconAvatar flex justify-center align-center" | |||
/> | |||
</slot> | |||
<view class="content"> | |||
<view class="text-grey"> | |||
<slot>{{ title }}</slot> | |||
</view> | |||
<view v-if="extra || $slots.extra" class="text-gray text-sm flex"> | |||
<view class="text-cut text-grey"> | |||
<slot name="extra">{{ extra }}</slot> | |||
</view> | |||
</view> | |||
</view> | |||
<view class="action"> | |||
<view v-if="times" class="text-grey text-sm"> | |||
<view v-if="times" style="display: flex; flex-direction: column; align-items: flex-end;"> | |||
<view class="text-right margin-bottom-xs">{{ times[0] }}</view> | |||
<view v-if="times[1]" class="text-right">{{ times[1] }}</view> | |||
</view> | |||
</view> | |||
<view v-else-if="time || $slots.time || $slots.tips" class="text-grey text-sm"> | |||
<slot name="time">{{ time }}</slot> | |||
<slot name="tips"></slot> | |||
</view> | |||
<template v-else> | |||
<slot name="action">{{ action }}</slot> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-list-item', | |||
props: { | |||
title: { default: '' }, | |||
action: { default: '' }, | |||
arrow: {}, | |||
extra: { default: '' }, | |||
time: { default: '' }, | |||
times: {}, | |||
roundAvatar: {}, | |||
avatar: {}, | |||
avatarStyle: { default: '' }, | |||
iconAvatar: {}, | |||
iconStyle: { default: '' }, | |||
imgAvatar: { default: '' }, | |||
imgStyle: { default: '' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,22 @@ | |||
<template> | |||
<view :class="[card ? 'card-menu margin-tb' : '', message ? 'menu-avatar' : 'menu']" class="cu-list"> | |||
<slot></slot> | |||
<view v-if="empty" class="padding text-gray text-center">{{ emptyTips }}</view> | |||
<view v-if="!empty && nomore" class="padding text-gray text-center">{{ nomoreTips }}</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-list', | |||
props: { | |||
card: {}, | |||
message: {}, | |||
empty: {}, | |||
emptyTips: { default: '列表为空' }, | |||
nomore: { default: false }, | |||
nomoreTips: { default: '没有更多了' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,20 @@ | |||
<template> | |||
<view :class="['bg-' + color, error ? 'erro' : completed ? 'over' : 'loading']" class="cu-load"> | |||
{{ error ? errorText : completed ? completeText : loadingText }} | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-loading', | |||
props: { | |||
completed: {}, | |||
error: {}, | |||
color: { default: 'gray' }, | |||
loadingText: { default: '加载中…' }, | |||
completeText: { default: '加载完成' }, | |||
errorText: { default: '加载失败' } | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,216 @@ | |||
<template> | |||
<view @click="modalClick" :class="[value ? 'show' : '', type === 'bottom' ? 'bottom-modal' : '']" class="cu-modal"> | |||
<!-- 图片弹出窗口 --> | |||
<view v-if="type === 'img'" class="cu-dialog"> | |||
<view | |||
:style="{ backgroundImage: 'url(' + img + ')', height: '200px' }" | |||
class="bg-img" | |||
style="position: relative;" | |||
> | |||
<view @tap="close" class="action text-white" style="position: absolute;top: 10px;right: 10px;"> | |||
<l-icon type="close" /> | |||
</view> | |||
</view> | |||
<view class="cu-bar bg-white"><view @tap="close" class="action margin-0 flex-sub solid-left">关闭</view></view> | |||
</view> | |||
<!-- 单选弹出窗口 --> | |||
<view v-else-if="type === 'radio'" @tap.stop class="cu-dialog"> | |||
<radio-group @change="radioChange" class="block"> | |||
<view class="cu-list menu text-left"> | |||
<view v-for="(item, index) of range" :key="index" class="cu-item"> | |||
<label class="flex justify-between align-center flex-sub"> | |||
<view class="flex-sub">{{ objectMode ? item[textField] : item }}</view> | |||
<radio | |||
:checked="radioIndex === index ? true : false" | |||
:value="objectMode ? item[valueField] : item" | |||
:class="radioIndex === index ? 'checked' : ''" | |||
class="round" | |||
></radio> | |||
</label> | |||
</view> | |||
</view> | |||
</radio-group> | |||
</view> | |||
<!-- 多选弹出窗口 --> | |||
<view v-else-if="type === 'checkbox'" @tap.stop class="cu-dialog"> | |||
<view class="cu-bar bg-white"> | |||
<view @tap="close(0)" class="action text-blue">取消</view> | |||
<view v-if="!readonly" @tap="close(1)" class="action text-green">确定</view> | |||
</view> | |||
<view class="grid col-3 padding-sm"> | |||
<view v-for="(item, index) in range" :key="index" class="padding-xs"> | |||
<button | |||
@tap="chooseCheckbox(index)" | |||
:class="checkboxIndex.includes(index) ? 'bg-orange' : 'line-orange'" | |||
class="cu-btn orange lg block" | |||
> | |||
{{ objectMode ? item[textField] : item }} | |||
</button> | |||
</view> | |||
</view> | |||
</view> | |||
<!-- 普通/对话/底部弹出窗口 --> | |||
<view v-else class="cu-dialog"> | |||
<view :class="[type === 'bottom' ? '' : 'justify-end pading-left']" class="cu-bar bg-white"> | |||
<!-- 普通/对话弹出窗口 --> | |||
<template v-if="type === 'default' || type === 'confirm'"> | |||
<view class="content"> | |||
<slot name="title">{{ title }}</slot> | |||
</view> | |||
<view @tap="close" class="action"><l-icon type="close" color="red" /></view> | |||
</template> | |||
<!-- 底部弹出窗口 --> | |||
<template v-if="type === 'bottom'"> | |||
<slot name="action"> | |||
<l-button @click="close" class="action padding-lr" line="red">取消</l-button> | |||
<view class="action padding-lr">{{ title }}</view> | |||
</slot> | |||
</template> | |||
</view> | |||
<view class="padding"> | |||
<slot>{{ content }}</slot> | |||
</view> | |||
<!-- 对话弹出窗口的操作区 --> | |||
<template v-if="type === 'confirm'"> | |||
<view class="cu-bar bg-white justify-end padding-right"> | |||
<view class="action"> | |||
<button @tap="close(0)" class="cu-btn line-green text-green">取消</button> | |||
<button @tap="close(1)" class="cu-btn bg-green margin-left">确定</button> | |||
</view> | |||
</view> | |||
</template> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-modal', | |||
props: { | |||
title: {}, | |||
content: {}, | |||
value: {}, | |||
type: { default: 'default' }, | |||
img: {}, | |||
range: { default: () => [] }, | |||
radio: {}, | |||
checkbox: {}, | |||
readonly: {}, | |||
textField: { default: 'text' }, | |||
valueField: { default: 'value' } | |||
}, | |||
data() { | |||
return { | |||
radioIndex: undefined, | |||
checkboxIndex: [] | |||
} | |||
}, | |||
created() { | |||
this.init() | |||
}, | |||
methods: { | |||
init() { | |||
if (this.type === 'radio') { | |||
this.radioIndex = this.objectMode | |||
? this.range.findIndex(t => t[this.valueField] === this.radio) | |||
: this.range.indexOf(this.radio) | |||
} else if (this.type === 'checkbox' && Array.isArray(this.checkbox)) { | |||
this.checkboxIndex = this.checkbox.reduce( | |||
(a, b) => | |||
a.concat(this.objectMode ? this.range.findIndex(t => t[this.valueField] === b) : this.range.indexOf(b)), | |||
[] | |||
) | |||
} | |||
}, | |||
close(arg) { | |||
this.$emit('input', false) | |||
this.$emit('close', false) | |||
if (typeof arg === 'number') { | |||
this.$emit(['cancel', 'ok'][arg]) | |||
} | |||
if (this.type === 'checkbox') { | |||
if (arg === 0) { | |||
this.checkboxIndex = this.checkbox | |||
} else { | |||
const checkboxValue = this.checkboxIndex.reduce( | |||
(a, b) => a.concat(this.objectMode ? this.range[b][this.valueField] : this.range[b]), | |||
[] | |||
) | |||
this.$emit('checkboxChange', checkboxValue) | |||
this.$emit('checkboxIndex', this.checkboxIndex) | |||
} | |||
} | |||
if (typeof arg === 'number') { | |||
this.$emit(['cancel', 'ok'][arg]) | |||
} | |||
}, | |||
modalClick() { | |||
if (this.type === 'radio' || this.type === 'checkbox') { | |||
this.close() | |||
} | |||
}, | |||
radioChange(e) { | |||
if (this.readonly) { | |||
return | |||
} | |||
this.radioIndex = this.objectMode | |||
? this.range.findIndex(t => t[this.valueField] === e.detail.value) | |||
: this.range.indexOf(e.detail.value) | |||
this.$emit( | |||
'radioChange', | |||
this.objectMode ? this.range[this.radioIndex][this.valueField] : this.range[this.radioIndex] | |||
) | |||
this.$emit('radioIndex', this.radioIndex) | |||
}, | |||
chooseCheckbox(index) { | |||
if (this.readonly) { | |||
return | |||
} | |||
if (this.checkboxIndex.includes(index)) { | |||
this.checkboxIndex = this.checkboxIndex.filter(t => t !== index) | |||
} else { | |||
this.checkboxIndex.push(index) | |||
} | |||
const checkboxValue = this.checkboxIndex.reduce( | |||
(a, b) => a.concat(this.objectMode ? this.range[b][this.valueField] : this.range[b]), | |||
[] | |||
) | |||
} | |||
}, | |||
watch: { | |||
value(newVal) { | |||
if (newVal) { | |||
this.init() | |||
} | |||
} | |||
}, | |||
computed: { | |||
objectMode() { | |||
return typeof this.range[0] === 'object' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,75 @@ | |||
<template> | |||
<scroll-view | |||
:scroll-left="scrollLeft" | |||
:class="['bg-' + color, type === 'center' ? 'text-center' : '', shadow ? 'nav-shadow' : '']" | |||
class="nav" | |||
style="z-index: 10;" | |||
scroll-with-animation | |||
scroll-x | |||
> | |||
<template v-if="type !== 'flex'"> | |||
<view | |||
v-for="(item, idx) in items" | |||
@tap="tabSelect(idx)" | |||
:key="idx" | |||
:class="value === idx ? 'text-green cur' : ''" | |||
class="cu-item" | |||
> | |||
{{ item }} | |||
</view> | |||
</template> | |||
<view v-else class="flex text-center"> | |||
<view | |||
@tap="tabSelect(idx)" | |||
v-for="(item, idx) in items" | |||
:key="idx" | |||
:class="[value === idx ? (color ? 'bg-white cur' : 'bg-green cur') : '']" | |||
class="cu-item flex-sub" | |||
> | |||
{{ item }} | |||
</view> | |||
</view> | |||
</scroll-view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-nav', | |||
props: { | |||
color: { default: 'white' }, | |||
items: { default: () => [] }, | |||
value: {}, | |||
type: { default: 'default' }, | |||
shadow: {} | |||
}, | |||
data() { | |||
return { | |||
scrollLeft: 0 | |||
} | |||
}, | |||
methods: { | |||
tabSelect(idx) { | |||
this.scrollLeft = (idx - 1) * 60 | |||
this.$emit('input', idx) | |||
this.$emit('change', idx) | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.nav-shadow { | |||
box-shadow: 0px 0px 7px 0px #666; | |||
} | |||
</style> | |||
<style> | |||
:host { | |||
z-index: 10; | |||
} | |||
</style> |
@@ -0,0 +1,39 @@ | |||
<template> | |||
<view | |||
:class="[ | |||
size, | |||
radius ? 'radius' : '', | |||
round ? 'round' : '', | |||
striped ? 'striped' : '', | |||
active && striped ? 'active' : '' | |||
]" | |||
class="cu-progress" | |||
> | |||
<view :class="[color ? 'bg-' + color : '']" :style="{ width: displayPercent + '%' }"> | |||
{{ noText ? '' : displayPercent + '%' }} | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-progress', | |||
props: { | |||
percent: {}, | |||
noText: {}, | |||
color: {}, | |||
radius: {}, | |||
round: {}, | |||
size: { default: 'df' }, | |||
striped: {}, | |||
active: {} | |||
}, | |||
computed: { | |||
displayPercent() { | |||
return Number(this.percent) || 0 | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,44 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<radio | |||
@click="change" | |||
:checked="isCheck" | |||
:value="radioValue" | |||
:disabled="disabled" | |||
:class="[isCheck ? 'checked' : '', color ? color : '', point ? 'radio' : '']" | |||
></radio> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-radio', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
point: {}, | |||
color: {}, | |||
radioValue: {}, | |||
value: {}, | |||
required: {} | |||
}, | |||
computed: { | |||
isCheck() { | |||
return this.value && this.value == this.radioValue | |||
} | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('input', this.radioValue) | |||
this.$emit('change', this.radioValue) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,54 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<picker | |||
@change="change" | |||
:customitem="customitem" | |||
:value="value" | |||
:disabled="disabled" | |||
:class="[arrow ? 'picker-arrow' : '']" | |||
mode="region" | |||
> | |||
<view class="picker"> | |||
<slot>{{ display || placeholder }}</slot> | |||
</view> | |||
</picker> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-region-picker', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
required: {}, | |||
placeholder: { default: '请选择地区…' }, | |||
multiple: {}, | |||
customitem: {}, | |||
value: {}, | |||
arrow: { default: true } | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
} | |||
}, | |||
computed: { | |||
display() { | |||
if (!this.value || !this.value.length) { | |||
return this.placeholder || '' | |||
} | |||
return this.value.join(' ') | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,164 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
<picker | |||
@change="change" | |||
:mode="multiple ? 'multiSelector' : 'selector'" | |||
:range="innerRange" | |||
:value="index" | |||
:disabled="disabled" | |||
:class="[disabled ? '' : 'picker-arrow']" | |||
> | |||
<view class="picker"> | |||
<slot>{{ display }}</slot> | |||
</view> | |||
</picker> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-select', | |||
props: { | |||
title: {}, | |||
disabled: {}, | |||
range: {}, | |||
placeholder: {}, | |||
multiple: {}, | |||
value: {}, | |||
emptyText: { default: '(未选择)' }, | |||
splitText: { default: ' ' }, | |||
required: {}, | |||
textField: { default: 'text' }, | |||
valueField: { default: 'value' } | |||
}, | |||
data() { | |||
return { | |||
index: -1 | |||
} | |||
}, | |||
model: { | |||
prop: 'value', | |||
event: 'input' | |||
}, | |||
mounted() { | |||
this.calcIndex() | |||
}, | |||
methods: { | |||
change(e) { | |||
this.index = e.detail.value | |||
this.$emit('input', this.currentModel) | |||
this.$emit('change', this.currentModel) | |||
}, | |||
calcIndex() { | |||
if (this.multiple) { | |||
if (this.objectMode) { | |||
this.index = this.range.map((group, idx) => | |||
group.findIndex(t => this.arrayCompare(t[this.valueField], this.value[idx])) | |||
) | |||
} else { | |||
this.index = this.range.map((group, idx) => group.findIndex(t => this.arrayCompare(t, this.value[idx]))) | |||
} | |||
} else { | |||
if (this.objectMode) { | |||
this.index = this.range.findIndex(t => t[this.valueField] === this.value) | |||
} else { | |||
this.index = this.range.indexOf(this.value) | |||
} | |||
} | |||
}, | |||
arrayCompare(a, b) { | |||
if (!a || !b || a.length !== b.length) { | |||
return false | |||
} | |||
let isOk = true | |||
a.forEach((t, i) => { | |||
if (b[i] !== t) { | |||
isOk = false | |||
} | |||
}) | |||
return isOk | |||
} | |||
}, | |||
computed: { | |||
objectMode() { | |||
return typeof (this.multiple ? this.range[0][0] : this.range[0]) === 'object' | |||
}, | |||
innerRange() { | |||
if (!this.objectMode) { | |||
return this.range | |||
} | |||
if (this.multiple) { | |||
return this.range.map(item => item.map(t => t[this.textField])) | |||
} | |||
return this.range.map(t => t[this.textField]) | |||
}, | |||
currentModel() { | |||
const { multiple, range, index, objectMode } = this | |||
if (!multiple) { | |||
return index === -1 ? undefined : objectMode ? range[index][this.valueField] : range[index] | |||
} | |||
if (!objectMode) { | |||
return range.map((group, idx) => group[index[idx]]) | |||
} | |||
range.map((group, idx) => (index[idx] === -1 ? undefined : group[index[idx]][this.valueField])) | |||
}, | |||
display() { | |||
const { multiple, range, value, index, objectMode, emptyText, displayPlaceholder, splitText } = this | |||
if (!multiple) { | |||
return index === -1 ? displayPlaceholder : objectMode ? range[index][this.textField] : range[index] | |||
} | |||
if (!Array.isArray(index) || index.every(t => t === -1)) { | |||
return displayPlaceholder | |||
} | |||
if (!objectMode) { | |||
return range.map((group, idx) => group[index[idx]] || emptyText).join(splitText) | |||
} | |||
return range | |||
.map((group, idx) => (index[idx] === -1 ? emptyText : group[index[idx]][this.textField])) | |||
.join(splitText) | |||
}, | |||
displayPlaceholder() { | |||
if (this.disabled) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return this.title ? `请选择${this.title}` : '请选择…' | |||
} | |||
}, | |||
watch: { | |||
value() { | |||
this.calcIndex() | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,29 @@ | |||
<template> | |||
<view :class="[arrow ? 'steps-arrow' : '']" class="cu-steps"> | |||
<view | |||
v-for="(item, index) in steps" | |||
:key="index" | |||
:class="index < step ? 'text-' + activeColor : ''" | |||
class="cu-item" | |||
> | |||
<text v-if="number" :data-index="index + 1" class="num"></text> | |||
<text v-else :class="index < step || !point ? 'cuIcon-' + item.icon : 'cuIcon-title'"></text> | |||
{{ item.title }} | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-step', | |||
props: { | |||
step: { default: 0 }, | |||
steps: { default: () => [] }, | |||
activeColor: { default: 'blue' }, | |||
point: {}, | |||
arrow: {}, | |||
number: {} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,33 @@ | |||
<template> | |||
<view class="cu-form-group"> | |||
<view class="title">{{ title }}</view> | |||
<switch | |||
@change="change" | |||
:checked="value" | |||
:disabled="disabled" | |||
:class="[value ? 'checked' : '', icon ? 'switch-' + icon : '', color ? color : '', radius ? 'radius' : '']" | |||
></switch> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-switch', | |||
props: { | |||
title: {}, | |||
color: {}, | |||
icon: {}, | |||
radius: {}, | |||
disabled: {}, | |||
value: {} | |||
}, | |||
methods: { | |||
change(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,48 @@ | |||
<template> | |||
<view | |||
@click="click" | |||
:class="[ | |||
round ? 'round' : '', | |||
radius ? 'radius' : '', | |||
size && !capsule ? size : '', | |||
color && !capsule ? 'bg-' + color : '', | |||
line && !capsule ? 'line-' + line : '', | |||
light && !capsule ? 'light' : '', | |||
capsule ? 'cu-capsule' : 'cu-tag', | |||
badge ? 'badge' : '' | |||
]" | |||
:style="{ zIndex: badge ? 2 : 1 }" | |||
> | |||
<template v-if="capsule"> | |||
<view :class="[size, color ? 'bg-' + color : '']" class="cu-tag tag-icon"><slot name="icon"></slot></view> | |||
<view :class="[size, color ? 'line-' + color : '']" class="cu-tag"><slot></slot></view> | |||
</template> | |||
<template v-else> | |||
<slot></slot> | |||
</template> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-tag', | |||
props: { | |||
round: {}, | |||
radius: {}, | |||
size: { default: 'df' }, | |||
color: {}, | |||
line: {}, | |||
light: {}, | |||
capsule: {}, | |||
badge: {} | |||
}, | |||
methods: { | |||
click(e) { | |||
this.$emit('click', e) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,116 @@ | |||
<template> | |||
<view v-if="!simpleMode"> | |||
<view class="cu-form-group" style="border-bottom: none"> | |||
<view class="title"> | |||
<text v-if="required" class="lr-required">*</text> | |||
{{ title }} | |||
</view> | |||
</view> | |||
<view class="cu-form-group" style="position: relative;border-top: none"> | |||
<textarea | |||
@input="textareaInput" | |||
@focus.stop.prevent | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceHolder" | |||
:fixed="fixed" | |||
:value="display" | |||
:auto-height="autoHeight" | |||
:readonly="readonly" | |||
style="margin-top:0;" | |||
></textarea> | |||
<cover-view v-if="readonly" @click.stop.prevent class="textarea-mask"></cover-view> | |||
</view> | |||
</view> | |||
<view v-else class="cu-form-group" style="position: relative;"> | |||
<textarea | |||
@input="textareaInput" | |||
@focus.stop.prevent | |||
:maxlength="maxlength" | |||
:placeholder="displayPlaceHolder" | |||
:fixed="fixed" | |||
:value="display" | |||
:auto-height="autoHeight" | |||
:readonly="readonly" | |||
:style="textareaStyle" | |||
></textarea> | |||
<cover-view v-if="readonly" @click.stop.prevent class="textarea-mask"></cover-view> | |||
</view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'l-textarea', | |||
props: { | |||
maxlength: { default: -1 }, | |||
readonly: {}, | |||
placeholder: {}, | |||
fixed: {}, | |||
value: {}, | |||
textareaStyle: {}, | |||
autoHeight: {}, | |||
simpleMode: {}, | |||
required: {}, | |||
title: {} | |||
}, | |||
data() { | |||
return { | |||
hide: false | |||
} | |||
}, | |||
// #ifdef MP-WEIXIN | |||
beforeMount() { | |||
uni.$on('modal', this.handelModal) | |||
}, | |||
beforeDestroy() { | |||
uni.$off('modal', this.handelModal) | |||
}, | |||
// #endif | |||
methods: { | |||
textareaInput(e) { | |||
this.$emit('change', e.detail.value) | |||
this.$emit('input', e.detail.value) | |||
}, | |||
// #ifdef MP-WEIXIN | |||
handelModal(modal) { | |||
this.hide = !!modal | |||
} | |||
// #endif | |||
}, | |||
computed: { | |||
display() { | |||
return this.hide ? '' : this.value | |||
}, | |||
displayPlaceHolder() { | |||
if (this.readonly || this.hide) { | |||
return '' | |||
} | |||
if (this.placeholder) { | |||
return this.placeholder | |||
} | |||
return !this.simpleMode && this.title ? `请输入${this.title}` : '请输入…' | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="less" scoped> | |||
.textarea-mask { | |||
z-index: 999; | |||
position: absolute; | |||
bottom: 0; | |||
top: 0; | |||
left: 0; | |||
right: 0; | |||
} | |||
</style> |