dufeng 1 year ago
commit
52fb67a0e1
100 changed files with 5219 additions and 0 deletions
  1. 70 0
      App.vue
  2. 41 0
      classes/class.member.js
  3. 127 0
      classes/class.mini.login.js
  4. 132 0
      classes/class.mp.login.js
  5. 35 0
      classes/class.pwd.login.js
  6. 98 0
      components/di-calendar-picker/di-calendar-picker.vue
  7. 169 0
      components/di-card/di-card.vue
  8. 92 0
      components/di-checkbox-list/di-checkbox-list.vue
  9. 93 0
      components/di-date-picker/di-date-picker.vue
  10. 85 0
      components/di-descriptions-item/di-descriptions-item.vue
  11. 88 0
      components/di-descriptions/di-descriptions.vue
  12. 322 0
      components/di-person-picker/di-person-picker.vue
  13. 57 0
      components/di-radio-list/di-radio-list.vue
  14. 96 0
      components/di-region-picker/di-region-picker.vue
  15. 55 0
      components/di-scroll-menu-list/di-scroll-menu-list.vue
  16. 115 0
      components/di-select/di-select.vue
  17. 44 0
      components/di-switch/di-switch.vue
  18. 206 0
      components/di-upload/di-upload.vue
  19. 20 0
      index.html
  20. 44 0
      main.js
  21. 85 0
      manifest.json
  22. 46 0
      mixins/detail.js
  23. 276 0
      mixins/form.js
  24. 219 0
      mixins/list.js
  25. 61 0
      mixins/more.js
  26. 126 0
      pages.json
  27. 38 0
      pages/component-page/crud/detail.vue
  28. 178 0
      pages/component-page/crud/form.vue
  29. 68 0
      pages/component-page/crud/list.vue
  30. 77 0
      pages/component-page/index.vue
  31. 93 0
      pages/component-page/navigation/index.vue
  32. 39 0
      pages/home/banner/index.vue
  33. 568 0
      pages/home/calendar/calendar.vue
  34. 132 0
      pages/home/home.vue
  35. 52 0
      pages/index/index.vue
  36. 122 0
      pages/login/index.vue
  37. 71 0
      pages/login/loginForm.vue
  38. 79 0
      pages/login/registerForm.vue
  39. 89 0
      pages/personal/personal.vue
  40. 32 0
      pages/workflowTask/approveCenter/approveList.js
  41. 43 0
      pages/workflowTask/approveCenter/doneList.vue
  42. 91 0
      pages/workflowTask/approveCenter/index.vue
  43. 46 0
      pages/workflowTask/approveCenter/launchList.vue
  44. 71 0
      pages/workflowTask/approveCenter/todoList.vue
  45. 58 0
      pages/workflowTask/index.vue
  46. 92 0
      pages/workflowTask/startWorkflow/processDefinitionList.vue
  47. 166 0
      pages/workflowTask/startWorkflow/startWorkflow.vue
  48. 30 0
      pages/workflowTask/webviewPages/dealTask.vue
  49. 22 0
      pages/workflowTask/webviewPages/flowDiagram.vue
  50. 64 0
      pages/workflowTask/webviewPages/startFlow.vue
  51. 9 0
      pages/workflowTask/webviewPages/webview.js
  52. BIN
      static/images/component.png
  53. BIN
      static/images/component_selected.png
  54. BIN
      static/images/detail.png
  55. BIN
      static/images/diboot/diboot-cloud.jpeg
  56. BIN
      static/images/diboot/diboot-lowcode.jpeg
  57. BIN
      static/images/diboot/diboot-workflow.png
  58. BIN
      static/images/diboot/diboot-workflow2.png
  59. BIN
      static/images/form.png
  60. BIN
      static/images/home.png
  61. BIN
      static/images/home_selected.png
  62. BIN
      static/images/list.png
  63. BIN
      static/images/menu/buy.png
  64. BIN
      static/images/menu/email.png
  65. BIN
      static/images/menu/expense.png
  66. BIN
      static/images/menu/leave.png
  67. BIN
      static/images/menu/signIn.png
  68. BIN
      static/images/menu/startflow.png
  69. BIN
      static/images/menu/todo.png
  70. BIN
      static/images/menu/train.png
  71. BIN
      static/images/menu/travel.png
  72. BIN
      static/images/personal.png
  73. BIN
      static/images/personal_selected.png
  74. BIN
      static/images/workflow/done.png
  75. BIN
      static/images/workflow/myLanuch.png
  76. BIN
      static/images/workflow/start.png
  77. BIN
      static/images/workflow/todo.png
  78. BIN
      static/logo.png
  79. 8 0
      styles/variables.scss
  80. 77 0
      uni.scss
  81. 0 0
      unpackage/dist/build/.automator/h5/.automator.json
  82. BIN
      unpackage/dist/build/h5.zip
  83. 2 0
      unpackage/dist/build/h5/index.html
  84. BIN
      unpackage/dist/build/h5/static/images/component.png
  85. BIN
      unpackage/dist/build/h5/static/images/component_selected.png
  86. BIN
      unpackage/dist/build/h5/static/images/detail.png
  87. BIN
      unpackage/dist/build/h5/static/images/diboot/diboot-cloud.jpeg
  88. BIN
      unpackage/dist/build/h5/static/images/diboot/diboot-lowcode.jpeg
  89. BIN
      unpackage/dist/build/h5/static/images/diboot/diboot-workflow.png
  90. BIN
      unpackage/dist/build/h5/static/images/diboot/diboot-workflow2.png
  91. BIN
      unpackage/dist/build/h5/static/images/form.png
  92. BIN
      unpackage/dist/build/h5/static/images/home.png
  93. BIN
      unpackage/dist/build/h5/static/images/home_selected.png
  94. BIN
      unpackage/dist/build/h5/static/images/list.png
  95. BIN
      unpackage/dist/build/h5/static/images/menu/buy.png
  96. BIN
      unpackage/dist/build/h5/static/images/menu/email.png
  97. BIN
      unpackage/dist/build/h5/static/images/menu/expense.png
  98. BIN
      unpackage/dist/build/h5/static/images/menu/leave.png
  99. BIN
      unpackage/dist/build/h5/static/images/menu/signIn.png
  100. BIN
      unpackage/dist/build/h5/static/images/menu/startflow.png

+ 70 - 0
App.vue

@@ -0,0 +1,70 @@
+<script>
+	export default {
+		onShow: function() {
+			if(uni.getStorageSync("authtoken")) {
+				let bindWpTag = uni.getStorageSync("bindWpTag")				
+				// 如果是发起绑定
+				if(bindWpTag) {
+					this.$mpLogin
+					.bindWxMp()
+					.then(() => {
+						// 刷新个人页面
+						uni.reLaunch({
+							url: '/pages/personal/personal'
+						})
+					})
+				}
+			} else {
+				let redirect = uni.getStorageSync("redirect")
+				if(redirect) {
+					this.$mpLogin
+					.setTip(this.$refs.uTips)
+					.go()
+					.then(() => {
+						// 跳转到首页
+						uni.switchTab({
+							url: '/pages/home/home'
+						})
+					})
+				} else {
+					// 直接redirectTo/reLaunch会导致小程序点击事件无法使用,需要增加延迟
+					// reLaunch H5中会导致表单校验失效
+					let timer = setTimeout(() => {
+						clearTimeout(timer)
+					      uni.redirectTo({
+					          url: 'pages/login/index'
+					      })
+					  }, 0)
+				}
+
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "uview-ui/index.scss";
+
+	/*每个页面公共css */
+	page, .h100  {
+		height: 100%;
+	}
+	.page-bg-color {
+		background-color: $u-bg-color;
+	}
+	.page-card {
+		border-radius: 20rpx;
+		background-color: #fff;
+		overflow: hidden;
+	}
+	.di-scroll {
+		width: 100%;
+		height: 100%;
+		&-list {
+			box-sizing: border-box;
+			background-color: #fff;
+			overflow: hidden;
+			margin: 20rpx;
+		}
+	}
+</style>

+ 41 - 0
classes/class.member.js

@@ -0,0 +1,41 @@
+import {service as dibootApi} from '@/utils/dibootApi.js'
+export default class Member {
+	constructor(){
+		this.$vue = null
+		this.$tip = null
+	}
+	/**
+	 * 设置tip对象
+	 * @param {Object} $tip
+	 */
+	setTip($tip) {
+		this.$tip = $tip
+		return this
+	}
+	/**
+	 * 设置vue对象
+	 * @param {Object} $vue
+	 */
+	setVue($vue) {
+		this.$vue = $vue
+		return this
+	}
+	/**
+	 * 获取用户信息
+	 */
+	async getMemberInfo() {
+		const res = await dibootApi.get('/h5/userInfo')
+		if (res.code === 0) {
+			uni.setStorageSync("userInfo", JSON.stringify(res.data))
+		} else {
+			console.log('加载用户错误:', res)
+			uni.clearStorageSync()
+			let timer = setTimeout(() => {
+				clearTimeout(timer)
+			      uni.redirectTo({
+			          url: 'pages/login/login'
+			      })
+			  }, 0)
+		}
+	}
+}

+ 127 - 0
classes/class.mini.login.js

@@ -0,0 +1,127 @@
+import {service as dibootApi} from '@/utils/dibootApi.js'
+import Member from './class.member.js'
+
+export default class MiniLogin extends Member {
+	constructor() {
+		super()
+		this.$path = null
+		this.bindWx = false
+	}
+
+	setUrlPath(path){
+		this.$path = path
+		return this
+	}
+
+	setBindWx(bindWx){
+		this.bindWx = bindWx
+		return this
+	}
+	/**
+	 * 微信小程序授权用户信息
+	 */
+	go() {
+		let _this = this
+		// 微信登陆授权
+		wx.getUserProfile({
+		    desc : '用于完善用户资料',
+		    lang : 'zh_CN',
+		    success : function( res ){
+				uni.login({
+				  provider: 'weixin',
+				  success: function (loginRes) {
+					// 微信登陆
+					_this.miniAuthLogin(loginRes.code, res.userInfo)
+				 }
+				});
+		    },
+		    fail : function( res ){
+		        console.log('wx.getUserProfile=>获取用户失败', res);
+		    }
+		})
+	}
+	/**
+	 * 微信小程序登陆
+	 * @param {Object} code 微信code,
+	 * @param {Object} encodePhone 加密的手机号信息
+	 */
+	async miniAuthLogin(code, infoRes) {
+		let msg = this.bindWx ? '绑定中' : '登录中'
+		let msgFail = this.bindWx ? '绑定失败' : '登陆失败'
+		uni.showLoading({
+		    title: msg
+		});
+		try {
+			// 调用登陆接口
+			const res = await dibootApi.get('/wx-ma/auth/getSessionInfo', {params: {code}})
+			if(res.code === 0) {
+				const {sessionKey, openid} = res.data
+				// 存储sessionKey
+				uni.setStorageSync("sessionKey", sessionKey)
+				if(this.bindWx) {
+					this.bindWxMa({sessionKey, openid, ...infoRes})
+				} else {
+					// 存储用户信息
+					this.wxStorageUserInfo({sessionKey, openid, ...infoRes})
+				}
+			} else {
+				this.$tip ? this.$tip.show({ title: msgFail, type: 'error', duration: '3000'}) : uni.showToast({ title: res.msg, icon: 'error'})
+				uni.hideLoading()
+			}
+		} catch(e) {
+			console.log(e)
+			this.$tip ? this.$tip.show({ title: e.errMsg, type: 'error', duration: '3000'})  : uni.showToast({ title: '网络异常', icon: 'error'})
+			uni.hideLoading()
+		}
+
+	}
+	/**
+	 * 存储用户信息
+	 *
+	 * @param {Object} data
+	 * sessionKey, openid, signature, rawData, encryptedData, iv
+	 */
+	async wxStorageUserInfo(data) {
+		const saveRes = await dibootApi.post('/wx-ma/auth/getAndSaveWxMember', data)
+		if(saveRes.code === 0 ) {
+			uni.setStorageSync("member", JSON.stringify(saveRes.data))
+			// 调用iam登陆接口
+			const loginForm =  {authAccount: saveRes.data.openid, authType: 'WX_MP'}
+			const loginRes = await dibootApi.post('/wx-ma/auth/login', loginForm)
+			if(loginRes.code === 0) {
+				uni.setStorageSync("authtoken", loginRes.data)
+				this.$tip ? this.$tip.show({ title: '登录成功', type: 'success' }) : uni.showToast({ title: '登录成功', icon: 'success' })
+				uni.hideLoading()
+				// 跳转到首页
+				uni.switchTab({
+					url: this.$path
+				})
+			} else {
+				this.$tip ? this.$tip.show({ title: '登录失败', type: 'error', duration: '3000'})  : uni.showToast({ title: '登录失败', icon: 'error'})
+				uni.hideLoading()
+			}
+		} else {
+			this.$tip ? this.$tip.show({ title: saveRes.msg, type: 'error', duration: '3000'})  : uni.showToast({ title: saveRes.msg, icon: 'error'})
+			uni.hideLoading()
+		}
+	}
+	/**
+	 * 绑定微信
+	 * @param {Object} data
+	 */
+	async bindWxMa(data) {
+		const bindRes = await dibootApi.post('/wx-ma/bindMa', data)
+		if(bindRes.code === 0 ) {
+			uni.setStorageSync("member", JSON.stringify(bindRes.data))
+			this.$tip ? this.$tip.show({ title: '绑定成功', type: 'success' }) : uni.showToast({ title: '绑定成功', icon: 'success' })
+			uni.hideLoading()
+			// 跳转到首页
+			uni.reLaunch({
+				url: this.$path
+			})
+		} else {
+			this.$tip ? this.$tip.show({ title: bindRes.msg, type: 'error', duration: '3000'})  : uni.showToast({ title: bindRes.msg, icon: 'error'})
+			uni.hideLoading()
+		}
+	}
+}

+ 132 - 0
classes/class.mp.login.js

@@ -0,0 +1,132 @@
+import {service as dibootApi} from '@/utils/dibootApi.js'
+import constant from '@/utils/constant.js'
+import Member from './class.member.js'
+
+/**
+ * 微信公众号登陆
+ */
+export default class MpLogin extends Member {
+	constructor() {
+		super()
+	}
+
+	/**
+	 * 微信公众号登陆
+	 */
+	redirect(bindWp = false) {
+		let authtoken = uni.getStorageSync("authtoken")
+		let redirect = uni.getStorageSync("redirect")
+		if(bindWp) {
+			// 获取是否绑定标记
+			let bindWpTag = uni.getStorageSync("bindWpTag")
+			if (!redirect && !bindWpTag) {
+				this.buildOauth2Url(bindWp)
+			}
+		} else {
+			if (!authtoken && !redirect) {
+				this.buildOauth2Url(bindWp)
+			}
+		}
+	}
+	/**
+	 * 获取授权URL
+	 */
+	async buildOauth2Url(bindWp) {
+		const res = await dibootApi.get(`/wx-mp/auth/buildOAuthUrl?url=${encodeURIComponent(constant.frontIndex())}`)
+		if (res.code === 0) {
+			window.location.href = res.data
+			uni.setStorageSync('redirect', true)
+			if(bindWp) {
+				uni.setStorageSync('bindWpTag', true)
+			}
+		} else {
+			console.log('buildOauth2Url错误:', res)
+		}
+	}
+	/**
+	 * 登陆
+	 */
+	go() {
+		return new Promise(async (reslove, reject) => {
+			try {
+				uni.showLoading({title: '登陆中'})
+				const res = await dibootApi.get('/wx-mp/auth/apply', {
+					params: {
+						code: this.getQueryString4hash('code'),
+						state: this.getQueryString4hash('state')
+					}
+				})
+				if(res.code === 0) {
+					uni.setStorageSync("authtoken", res.data)
+					uni.removeStorageSync("redirect")
+					let tipMsg = { title: '登录成功', type: 'success' }
+					this.$tip ? this.$tip.show(tipMsg) : uni.showToast(tipMsg)
+					reslove({code: true})
+				} else {
+					uni.removeStorageSync("redirect")
+					this.$tip ? this.$tip.show({ title: res.msg, type: 'error', duration: '3000'})  : uni.showToast({ title: res.msg, icon: 'error'})
+				}
+			} catch(e) {
+				console.log(e)
+				uni.removeStorageSync("redirect")
+				this.$tip ? this.$tip.show({ title: e.errMsg, type: 'error', duration: '3000'})  : uni.showToast({ title: '网络异常', icon: 'error'})
+			} finally {
+				uni.hideLoading()
+			}
+		})
+	}
+	/**
+	 * 绑定微信
+	 * @param {Object} data
+	 */
+	async bindWxMp() {
+		return new Promise(async (reslove, reject) => {
+			try {
+				uni.showLoading({title: '绑定中'})
+				const res = await dibootApi.get('/wx-mp/bindMp', {
+					params: {
+						code: this.getQueryString4hash('code'),
+						state: this.getQueryString4hash('state')
+					}
+				})
+				if(res.code === 0) {
+					uni.setStorageSync("member", JSON.stringify(res.data))
+					let tipMsg = { title: '绑定成功', type: 'success' }
+					this.$tip ? this.$tip.show(tipMsg) : uni.showToast(tipMsg)
+					reslove({code: true})
+				} else {
+					this.$tip ? this.$tip.show({ title: res.msg, type: 'error', duration: '3000'})  : uni.showToast({ title: res.msg, icon: 'error'})
+					reslove({code: true})
+				}
+			} catch(e) {
+				console.log(e)
+				this.$tip ? this.$tip.show({ title: e.errMsg, type: 'error', duration: '3000'})  : uni.showToast({ title: '网络异常', icon: 'error'})
+				reslove({code: true})
+			} finally {
+				uni.removeStorageSync("redirect")
+				uni.removeStorageSync("bindWpTag")
+				uni.hideLoading()
+			}
+		})
+	}
+	/**
+	 * 获取code和state值
+	 * @param {Object} name
+	 */
+	getQueryString4hash(name) {
+		const href = window.location.href
+		const arr1 = href.split('?')
+		if (arr1 && arr1.length > 1) {
+			const paramStr = arr1[1]
+			const params = paramStr.split('&')
+			for (let i = 0; i < params.length; i++) {
+				const paramObjs = params[i].split('=')
+				if (paramObjs && paramObjs.length > 1 && paramObjs[0] === name) {
+					return paramObjs[1]
+				}
+			}
+		}
+		return ''
+	}
+
+}

+ 35 - 0
classes/class.pwd.login.js

@@ -0,0 +1,35 @@
+import {service as dibootApi} from '@/utils/dibootApi.js'
+import Member from './class.member.js'
+
+export default class PwdLogin extends Member {
+	constructor() {
+		super()
+	}
+	/**
+	 * 登陆
+	 */
+	go(form) {
+		return new Promise(async (reslove, reject) => {
+			try {
+				uni.showLoading({title: '登陆中'})
+                const res = await dibootApi.post('/system/login', {client:"TQ-HW-PC",...form})
+				if(res.code === 0) {
+                    console.log('登录成功',res)
+					uni.setStorageSync("authtoken", res.token)
+					this.getMemberInfo()
+					let tipMsg = { title: '登录成功', type: 'success' }
+					this.$tip ? this.$tip.show(tipMsg) : uni.showToast(tipMsg)
+					reslove({code: true})
+				} else {
+					this.$tip ? this.$tip.show({ title: res.msg, type: 'error', duration: '3000'}) : uni.showToast({ title: res.msg, icon: 'error'})
+				}
+			} catch(e) {
+				console.log(e)
+				this.$tip ? this.$tip.show({ title: e.errMsg, type: 'error', duration: '3000'})  : uni.showToast({ title: '网络异常', icon: 'error'})
+			} finally {
+				uni.hideLoading()
+			}
+		})
+	}
+
+}

+ 98 - 0
components/di-calendar-picker/di-calendar-picker.vue

@@ -0,0 +1,98 @@
+<template>
+	<view class="di-calendar-picker">
+		<u-input ref='diCalendar' v-model="tempVal" @click="show = true" disabled :select-open="show"
+			type="select" :placeholder="placeholder" />
+		<u-calendar v-model="show" :mode="mode" @change="handleSelect" btn-type="success"
+			:active-bg-color="activeBgcolor" :range-color="rangeColor" :range-bg-color="rangeBgcolor" safe-area-inset-bottom z-index="99999"/>
+	</view>
+</template>
+
+<script>
+	/**
+	 * di-calendar-picker 时间选择器
+	 * @description yyyy-MM-dd格式的选择
+	 * @property {String} value 可以使用v-model双向绑定
+	 * @property {String} placeholder 提示信息
+	 * @property {String} active-bg-color 激活的背景色
+	 * @property {String} range-bg-color 激活范围的背景色
+	 * @property {String} range-color 激活范围的字体颜色
+	 * @property {String} mode = [date|range] 模式选择,"date"-日期模式(默认),"range"-选择日期范围
+	 * @event {Function} confirm 点击确定按钮,传递出所选的完整的时间对象
+	 */
+	export default {
+		data() {
+			return {
+				show: false,
+				tempVal: this.value
+			}
+		},
+		watch: {
+			value(val) {
+				this.tempVal = val
+			}
+		},
+		methods: {
+			/**
+			 * 确认选择
+			 * 
+			 * @param {Object} value
+			 */
+			handleSelect(value) {
+				let result = ''
+				if(this.mode === 'range') {
+					const { startDate, endDate} = value
+					result = [startDate, endDate].join('~')
+				} else {
+					result = value.result
+				}
+				this.tempVal = result
+				this.handleInputEvent(result)
+				this.$emit('confirm', value)
+			},
+			/**
+			 * 发送input消息
+			 * 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值
+			 * 但是微信小程序上 尚未更新到u-form-item,导致获取的值为空
+			 */
+			handleInputEvent(value) {
+				this.$emit('input', value)
+				this.$nextTick(function(){
+					// 将当前的值发送到 u-form-item 进行校验
+					this.$refs.diCalendar.dispatch('u-form-item', 'on-form-change', value);
+				})
+			}
+		},
+		props: {
+			placeholder: {
+				type: String,
+				default: '请选择日期'
+			},
+			value: {
+				type: String,
+				require: true
+			},
+			mode: {
+				type: String,
+				default: 'date'
+			},
+			activeBgcolor: {
+				type: String,
+				default: '#19be6b'
+			},
+			rangeBgcolor: {
+				type: String,
+				default: '#dbf1e1'
+			},
+			rangeColor: {
+				type: String,
+				default: '#18b566'
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.di-calendar-picker {
+	width: 100%;
+}
+</style>

+ 169 - 0
components/di-card/di-card.vue

@@ -0,0 +1,169 @@
+<template>
+	<view class="di-card" :class="{'u-border-bottom' : true}" @click.native="handleClick">
+		<!-- 多图卡片 -->
+		<view v-if="mode === 'multiple'" class="card mode-multiple">
+			<view class="card-content">
+				<view class="card-content__title">
+					<slot name="title">
+						<text>{{title}}</text>
+					</slot>
+				</view>
+				<view class="card-image">
+					<view class="card-image__item" v-for="(item, index) in imageList" :key="index">
+						<image class="el-image" :src="item" mode="aspectFill"></image>
+					</view>
+				</view>
+				<view class="card-content__footer">
+					<slot name="footer"></slot>
+				</view>
+			</view>
+		</view>
+		<!-- 大图模式/左图右文字模式 -->
+		<view v-else class="card" :class="{'mode-picture-card': mode === 'pictureCard'}">
+			<view class="card-image">
+				<image class="el-image" :src="imageList[0]" mode="aspectFill"></image>
+			</view>
+			<view class="card-content">
+				<view class="card-content__title">
+					<slot name="title">
+						<text>{{title}}</text>
+					</slot>
+				</view>
+				<view class="card-content__footer">
+					<slot name="footer"></slot>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 图片卡片组件
+	 * @description 适合文章列表的图片卡片组件
+	 * @property {String Number} index 卡片唯一值
+	 * @property {String} title 卡片标题
+	 * @property {Array} image-list 卡片图片
+	 * @property {String} mode = [default|pictureCard|multiple] 模式选择,"default"- 左图右文字(默认),"pictureCard"-大图模式,"multiple"-多图模式
+	 * @event {Function} click
+	 */
+	export default {
+		methods: {
+			handleClick() {
+				this.$emit('click', this.index)
+			}
+		},
+		props: {
+			index: {
+				type: [String, Number],
+				require: true
+			},
+			// 模式
+			mode: {
+				type: String,
+				default: 'default'
+			},
+			title: {
+				type: String,
+				require: true
+			},
+			imageList: {
+				type: Array,
+				require: true
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.di-card {
+		.card {
+			display: flex;
+			margin: 5px;
+			padding: 10px;
+			box-sizing: border-box;
+			.card-image {
+				width: 64px;
+				height: 64px;
+				border-radius: 5px;
+				overflow: hidden;
+				//避免挤压
+				flex-shrink: 0;
+				image {
+					height: 100%;
+					width: 100%;
+				}
+			}
+			.card-content {
+				display: flex;
+				flex-direction: column;
+				padding-left: 10px;
+				width: 100%;
+				justify-content: space-between;
+				.card-content__title {
+					position: relative;
+					font-size: 28rpx;
+					font-weight: 400;
+					color: #333;
+					line-height: 1.2;
+					//超过两行溢出隐藏
+					text {
+						overflow: hidden;
+						text-overflow: ellipsis;
+						display: -webkit-box;
+						-webkit-line-clamp: 2;
+						-webkit-box-orient: vertical;
+					}
+				}
+				.card-content__footer {
+					display: flex;
+					justify-content: flex-end;
+					align-items: center;
+					font-size: 24rpx;
+				}
+			}
+			&.mode-multiple {
+				.card-content {
+					padding-left: 0;
+					width: 100%;
+				}
+				.card-image {
+					display: flex;
+					width: 100%;
+					height: 70px;
+					margin: 10px 0px;
+					
+					.card-image__item {
+						display: flex;
+						width: 100%;
+						box-sizing: border-box;
+						border-radius: 5px;
+						margin-left: 10px;
+						&:first-child {
+							margin-left: 0;
+						}
+						.el-image {
+							width: 100%;
+							height: 100%;
+						}
+					}
+				}
+			}
+			// 大图模式
+			&.mode-picture-card {
+				flex-direction: column;
+				.card-image {
+					width: 100% ;
+					height: 100px;
+				}
+				.card-content {
+					margin-top: 10px;
+					padding-left: 0;
+					.card-content__footer {
+						margin-top: 10px;
+					}
+				}
+			}
+		}
+	}
+</style>

+ 92 - 0
components/di-checkbox-list/di-checkbox-list.vue

@@ -0,0 +1,92 @@
+<template>
+	<u-checkbox-group ref='diCheckbox' class="di-checkbox-list" @change="handleChange" :active-color="activeColor">
+		<u-checkbox class="di-checkbox-list__item"  v-model="item.checked" v-for="(item, index) in checkboxList" :key="index"
+			:name="item.value">
+			{{ item.label }}
+		</u-checkbox>
+	</u-checkbox-group>
+</template>
+
+<script>
+	/**
+	* di-checkbox-list checkbox列表
+	* @description checkbox列表组件,封装u-checkbox,适配form
+	* @property  {Array String}  value 支持数组和字符串,修改后响应为,所以建议使用字符串
+	* @property  {String}  active-color 激活时候的颜色
+	* @property  {Array}  list 传入labelValue列表
+	*/
+	export default {
+		name: 'di-checkbox-list',
+		data() {
+			return {
+				checkboxList: []
+			}
+		},
+		methods: {
+			/**
+			 * 改变选择
+			 * @param {Object} e
+			 */
+			handleChange() {
+				let values = [];
+				this.checkboxList.map(val => {
+					if(val.checked) values.push(val.value);
+				})
+				let valueStr = values.join(',')
+				this.$emit('input', valueStr)
+				this.$forceUpdate()
+				// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+				setTimeout(() => {
+					this.$refs.diCheckbox.dispatch('u-form-item', 'on-form-change', valueStr);
+				}, 60)
+				
+			},
+			/**
+			 * 渲染成适配uview数据
+			 * @param {Object} list
+			 */
+			__setCheckboxList(list) {
+				let time = setTimeout(() => {
+					clearTimeout(time)
+				this.checkboxList = list.map(item => {
+					item['checked'] = this.value2List.includes(item.value)
+					return item
+				})}, 0)
+			}
+		},
+		computed: {
+			/**
+			 * 将数据转化成list
+			 */
+			value2List() {
+				return this.value && typeof this.value === 'string' && this.value.split(',') || this.value || []
+			}
+		},
+		watch: {
+			list: {
+				immediate: true,
+				handler(value) {
+					this.__setCheckboxList(value)
+				}
+			}
+		},
+		props: {
+			value: {
+				type: [Array, String],
+				require: true
+			},
+			list: {
+				type: Array,
+				require: true
+			},
+			activeColor: {
+				type: String,
+				default: '#19be6b'
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 93 - 0
components/di-date-picker/di-date-picker.vue

@@ -0,0 +1,93 @@
+<template>
+	<view class="di-date-picker">
+		<u-input ref="diDate" v-model="tempVal" @click="show = true" disabled :select-open="show"
+			type="select" :placeholder="placeholder" />
+		<u-picker confirm-color="#18b566" v-model="show" :params="pickerParams" mode="time" @confirm="handlePicker" :default-time="value"></u-picker>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 时间选择器
+	 * @property {String} value 可以使用v-model双向绑定
+	 * @property {String} placeholder 提示信息
+	 * @property {String} mode = [date|datetime] 模式选择,"date"-日期模式(默认),"datetime"-日期时间选择
+	 * @event {Function} confirm 点击确定按钮,传递出所选的完整的时间对象
+	 */
+	export default {
+		data() {
+			return {
+				show: false,
+				tempVal: this.value
+			}
+		},
+		watch: {
+			value(val) {
+				this.tempVal = val
+			}
+		},
+		methods: {
+			/**
+			 * 确认选择
+			 * 
+			 * @param {Object} value
+			 */
+			handlePicker(value) {
+				let date = `${value.year}-${value.month}-${value.day}`
+				if(this.mode === 'datetime') {
+					date += ` ${value.hour}:${value.minute}:${value.second}`
+				}
+				this.tempVal = date
+				this.handleInputEvent(date)
+				this.$emit('confirm', value)
+			},
+			/**
+			 * 发送input消息
+			 * 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值
+			 * 但是微信小程序上 尚未更新到u-form-item,导致获取的值为空
+			 */
+			handleInputEvent(value) {
+				this.$emit('input', value)
+				this.$nextTick(function(){
+					// 将当前的值发送到 u-form-item 进行校验
+					this.$refs.diDate.dispatch('u-form-item', 'on-form-change', value);
+				})
+			}
+		},
+		computed: {
+			pickerParams(){
+				let params = {
+					year: true,
+					month: true,
+					day: true
+				}
+				if(this.mode === 'datetime') {
+					params.hour = true 
+					params.minute = true 
+					params.second = true 
+				}
+				return params
+			}
+		},
+		props: {
+			placeholder: {
+				type: String,
+				default: '请选择'
+			},
+			value: {
+				type: String,
+				require: true
+			},
+			mode: {
+				type: String,
+				default: 'date'
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.di-date-picker {
+	width: 100%;
+}
+</style>

+ 85 - 0
components/di-descriptions-item/di-descriptions-item.vue

@@ -0,0 +1,85 @@
+<template>
+	<u-row class="di-descriptions-item" align="top">
+		<u-col :span="labelCol || parentCol.labelCol || 3" class="di-descriptions-item__label">
+			<slot name="label">
+				{{label ? label + ':' : ''}}
+			</slot>
+		</u-col>
+		<u-col :span="valueCol || parentCol.valueCol || 9" class="di-descriptions-item__value">
+			<slot name="value">
+				<view :class="{'di-descriptions-item__ellipsis': ellipsis}">
+				{{value || ''}}
+				</view>
+			</slot>
+		</u-col>
+	</u-row>
+</template>
+
+<script>
+	/**
+	* di-descriptions-item 描述列表项
+	* @description 描述列表项,用于展示label value的值,比如对象详情页面等。 搭配父组件di-descriptions
+	* @property {String slot} label label
+	* @property {String Number slot} value value
+	* @property {Boolean} ellipsis value过长是否显示省略(默认false)
+	* @property {Number String} label-col label宽度,等分12份,可以继承di-descriptions#label-col,默认值3
+	* @property {Number String} value-col value宽度,等分12份,可以继承di-descriptions#value-col,默认值9
+	*/
+	export default {
+		computed: {
+			parentCol() {
+				if(this.descriptions && this.descriptions.$options && this.descriptions.$options.propsData) {
+					let {labelCol, valueCol} = this.descriptions.$options.propsData
+					return {labelCol, valueCol}
+				}
+				return {}
+			},
+			descriptions() {
+			  let parent = this.$parent;
+			  let parentName = parent.$options._componentTag;
+			  while (parentName !== 'di-descriptions') {
+				if(!(parent = parent.$parent)) {
+					break
+				}
+			    parentName = parent.$options._componentTag;
+			  }
+			  return parent;
+			}
+		},
+		props: {
+			label: {
+				type: String,
+				require: true
+			},
+			value: {
+				type: [String, Number],
+				require: true
+			},
+			ellipsis: {
+				type: Boolean,
+				default: false
+			},
+			labelCol: {
+				type: [String, Number]
+			},
+			valueCol: {
+				type: [String, Number]
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.di-descriptions-item {
+	color: $u-content-color;
+	margin-bottom: 10rpx;
+	&__label {
+		color: #a3a3a3;;
+	}
+	&__ellipsis {
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+	}
+}
+</style>

+ 88 - 0
components/di-descriptions/di-descriptions.vue

@@ -0,0 +1,88 @@
+<template>
+	<u-cell-group class="di-descriptions" :border="border">
+		<u-cell-item :arrow="false" :use-label-slot='true' :border-bottom='borderBottom' :border-top="false">
+			<template #title>
+			<view class="di-descriptions__title" :class="{'u-border-bottom': titleBottom}">
+				<view class="di-descriptions__title-left">
+					<slot name="title">
+						<text v-if="title">{{title}}</text>
+					</slot>
+				</view>
+				<view class="di-descriptions__title-right">
+					<slot name="right"></slot>
+				</view>
+			</view>
+			</template>
+			<template slot="label">
+				<view class="di-descriptions__body">
+					<slot></slot>
+				</view>
+			</template>
+		</u-cell-item>
+	</u-cell-group>
+</template>
+
+<script>
+	/**
+	* di-descriptions 描述列表
+	* @description 描述列表,用于展示label value的值,常见于详情页的信息展示。 搭配 di-descriptions-item
+	* @property {String slot} title 头部信息,也可以使用插槽
+	* @property {String slot} right 插槽
+	* @property {Boolean} border 是否显示边框(默认false)
+	* @property {Boolean} border-bottom 是否显示底部边框,与border分开使用,单位rpx(默认false)
+	* @property {Boolean} title-bottom 是否显示title底部边框,单位rpx(默认false)
+	* @property {Number String} label-col label宽度,等分12份,设置后子元素di-descriptions-item可以继承
+	* @property {Number String} value-col value宽度,等分12份,设置后子元素di-descriptions-item可以继承
+	*/
+	export default {
+		// #ifdef MP-WEIXIN
+		// 解决小程序样式无法使用
+		options: { styleIsolation: 'shared' },
+		// #endif
+		name: 'DiDescriptions',
+		props: {
+			title: {
+				type: String | Number
+			},
+			border: {
+				type: Boolean,
+				default: false
+			},
+			borderBottom: {
+				type: Boolean,
+				default: false
+			},
+			titleBottom: {
+				type: Boolean,
+				default: false
+			},
+			labelCol: {
+				type: String | Number,
+				default: 3
+			},
+			valueCol: {
+				type: String | Number,
+				default: 9
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.di-descriptions {
+	// width: 100%;
+	/deep/.u-cell_title {
+		width: 100% !important;
+	}
+	&__title {
+		font-weight: bold;
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 5px;
+		&-right{
+			min-width: 60px;
+			text-align: right;
+		}
+	}
+}
+</style>

+ 322 - 0
components/di-person-picker/di-person-picker.vue

@@ -0,0 +1,322 @@
+<template>
+	<view>
+		<!-- 结果列表 -->
+		<view>
+			<u-tag v-for="item in checkedOrgs" :key="item.name" :text="item.name" class="tag" />
+			<u-tag v-for="item in checkedUsers" :key="item.realname" :text="item.realname" class="tag" type="success" />
+			<view style="margin-left: 10px; color: #2979FF; display: inline-block;" @click="visible = true">
+				{{ checkedOrgs.length || checkedUsers.length ? '重选' : '添加' }}
+			</view>
+		</view>
+		<!-- 弹出 -->
+		<u-popup v-model="visible" mode="bottom" @open="openPopup">
+			<view id="top" style="padding-top: 5px;">
+				<!-- 操作按钮 -->
+				<view class="division" style="display: flex;padding-bottom: 5px;">
+					<u-button size="mini" plain @click="cancel">取消</u-button>
+					<view style="position: absolute; right: 10px;">
+						<u-button size="mini" plain type="primary" @click="submit">
+							确定({{ checkedOrgs.length + checkedUsers.length }})
+						</u-button>
+					</view>
+				</view>
+				<!-- 用户模糊搜索 -->
+				<view v-if="type !== 'org'" style="padding: 0 10px;">
+					<u-form-item left-icon="search">
+						<u-input v-model="searchValue" clearable placeholder="搜索用户" @input="search" />
+					</u-form-item>
+				</view>
+				<!-- 已选列表 -->
+				<view class="division">
+					<u-tag v-for="(item, index) in checkedOrgs" :key="item.name" :text="item.name" closeable class="tag"
+						@close="remove(checkedOrgs,index)" />
+					<u-tag v-for="(item, index)  in checkedUsers" :key="item.realname" :text="item.realname" closeable
+						class="tag" type="success" @close="remove(checkedUsers,index)" />
+				</view>
+				<!-- 面包屑 -->
+				<view class="division" v-if="!searchValue">
+					<text v-for="(item, index) in breadList" :key="index" @click="goOrg(item)">
+						<text v-if="index > 0">{{' '}} / {{' '}}</text>
+						<text :style="{color: activateBread(index) ? '' : '#3370ff'}">{{ item.name }}</text>
+					</text>
+				</view>
+			</view>
+			<scroll-view :style="{height: `${rollHeight}px`}" scroll-y @scrolltolower="handleOnreachBottom">
+				<!-- 组织架构 -->
+				<view v-if="!searchValue" v-for="item in orgList" :key="item[orgPrimaryKey]" style="display: flex;">
+					<view @click="isCheckedOrg(item)" style="margin-left: 10px;">
+						<radio v-if="type !== 'user'" :value="item[orgPrimaryKey]" :checked="item._checked || false" />
+						<u-icon name="list" />{{' '}} {{ item.name }}
+					</view>
+					<!-- 下级按钮 -->
+					<view style="position: absolute; right: 10px; padding-left: 15px;" :key="item.loading"
+						@click="nextLayer(item)">
+						<u-icon name="arrow-right" />
+					</view>
+				</view>
+				<!-- 人员 -->
+				<view v-for="item in list" :key="item[primaryKey]">
+					<view @click="isCheckedUser(item)" style="margin-left: 10px;">
+						<radio :value="item[primaryKey]" :checked="item._checked || false" />
+						<u-icon name="account-fill" />{{' '}} {{ item.realname }}
+					</view>
+				</view>
+				<u-empty v-if="isEmpty">
+					<u-button slot="bottom" size="mini" plain @click="callback">返回上级</u-button>
+				</u-empty>
+				<u-loadmore v-else-if="type !== 'org'" :status="status" :loadText='loadText' margin-top="24"
+					margin-bottom="24" />
+			</scroll-view>
+		</u-popup>
+	</view>
+</template>
+
+<script>
+	import list from '@/mixins/list'
+	import {
+		dibootApi
+	} from '@/utils/dibootApi'
+	export default {
+		name: "di-person-picker",
+		mixins: [list],
+		props: {
+			// 类型
+			type: {
+				// 用户选择、组织机构选择、人员架构混选
+				type: 'user' | 'org' | 'default',
+				default: 'default'
+			},
+			// 初始值 (type为 'user' | 'org' 时应,初始值应为对应的对象列表;type为 'default' 时,初始值应为 { orgs?: 组织对象列表, users?: 用户对象列表 }  )
+			initValue: {
+				type: Array | Object,
+				default: () => []
+			}
+		},
+		data() {
+			return {
+				visible: false,
+				confirmLoading: false,
+				getListFromMixin: false,
+				baseApi: '/workflow/userGroup',
+				listApi: 'userList',
+				queryParam: {
+					orgId: '0'
+				},
+				oldQueryParam: null,
+				searchValue: '',
+				orgPrimaryKey: 'id',
+				breadList: [{
+					id: '0',
+					name: '组织结构',
+					children: []
+				}],
+				checkedOrgs: [],
+				checkedUsers: [],
+				oldCheckedOrgs: [],
+				oldCkedUsers: [],
+				topHeight: 50,
+				windowHeight: 500
+			};
+		},
+		created() {
+			switch (this.type) {
+				case 'user':
+					this.checkedUsers = this.initValue
+					break
+				case 'org':
+					this.checkedOrgs = this.initValue
+					break
+				default:
+					this.checkedOrgs = this.initValue.orgs || []
+					this.checkedUsers = this.initValue.users || []
+			}
+
+			// 获取组织数据
+			dibootApi.get(`${this.baseApi}/orgTree`).then(res => {
+				if (res.ok) {
+					this.breadList[0].children = this.dataFilter(res.data, this.checkedOrgs)
+				} else {
+					this.showToast(res.msg)
+				}
+			})
+
+			// 获取人员信息
+			this.type !== 'org' && this.getList(true)
+
+			// 获取屏幕可用高度
+			let self = this;
+			uni.getSystemInfo({
+				success: function(res) {
+					self.windowHeight = res.windowHeight
+				}
+			});
+		},
+		computed: {
+			// 滚动区域高度
+			rollHeight() {
+				return this.windowHeight - this.topHeight
+			},
+			orgList() {
+				return this.breadList[this.breadList.length - 1].children || []
+			},
+			isEmpty() {
+				return this.orgList.length + ((this.list || []).length || 0) === 0
+			}
+		},
+		watch: {
+			visible(value) {
+				value && this.calcHeight()
+			}
+		},
+		methods: {
+			async calcHeight() {
+				await this.$nextTick();
+				uni.createSelectorQuery().in(this).select('#top').boundingClientRect(data => {
+					this.topHeight = data.height
+				}).exec()
+			},
+			openPopup() {
+				this.oldCheckedOrgs = this.$u.deepClone(this.checkedOrgs)
+				this.oldCheckedUsers = this.$u.deepClone(this.checkedUsers)
+			},
+			activateBread(index) {
+				return this.breadList.length - 1 === index
+			},
+			isCheckedOrg(item) {
+				if (this.type === 'user') return
+				item._checked = !item._checked
+				if (item._checked)
+					this.checkedOrgs.push(item)
+				else
+					this.checkedOrgs.splice(this.checkedOrgs.indexOf(item), 1);
+				this.calcHeight()
+			},
+			isCheckedUser(item) {
+				item._checked = !item._checked
+				if (item._checked)
+					this.checkedUsers.push(item)
+				else
+					this.checkedUsers.splice(this.checkedUsers.indexOf(item), 1);
+				this.calcHeight()
+			},
+			async nextLayer(item) {
+				item.loading = true
+				this.$forceUpdate()
+				this.queryParam.orgId = item[this.orgPrimaryKey]
+				this.type !== 'org' && await this.getList(true)
+				this.breadList.push(item)
+				item.loading = false
+				this.$forceUpdate()
+				this.calcHeight()
+			},
+			goOrg(item) {
+				this.breadList.splice(this.breadList.indexOf(item) + 1)
+				if (this.type !== 'org') {
+					this.list = []
+					this.queryParam.orgId = item[this.orgPrimaryKey]
+					this.getList(true)
+				}
+				this.calcHeight()
+			},
+			callback() {
+				this.goOrg(this.breadList[this.breadList.length - 2])
+			},
+			remove(list, index) {
+				list.splice(index, 1)[0]._checked = false
+				this.calcHeight()
+			},
+			submit() {
+				switch (this.type) {
+					case 'user':
+						const userIds = this.checkedUsers.map(e => e[this.primaryKey])
+						this.$emit('input', userIds)
+						this.$emit('change', userIds)
+						break
+					case 'org':
+						const orgIds = this.checkedOrgs.map(e => e[this.orgPrimaryKey])
+						this.$emit('input', orgIds)
+						this.$emit('change', orgIds)
+						break
+					default:
+						const data = {
+							orgs: this.checkedOrgs,
+							users: this.checkedUsers
+						}
+						this.$emit('input', data)
+						this.$emit('change', data)
+				}
+				this.visible = false
+			},
+			cancel() {
+				this.visible = false
+				this.checkedOrgs = this.$u.deepClone(this.oldCheckedOrgs)
+				this.checkedUsers = this.$u.deepClone(this.oldCheckedUsers)
+			},
+			// 数据过滤(用于回显选项)
+			dataFilter(data, list) {
+				if (list.length === 0) return data
+				data.forEach(item => {
+					const index = list.findIndex(e => e[this.primaryKey] === item[this.primaryKey])
+					index >= 0 && (item._checked = true) && list.splice(index, 1, item);
+					item.children != null && this.dataFilter(item.children, list)
+				});
+				this.$forceUpdate()
+				return data
+			},
+			/**
+			 * 获取数据列表
+			 */
+			async getList(replace = false) {
+				try {
+					this.status = 'loading'
+					const res = await dibootApi.get(this.listApi ? `${this.baseApi}/${this.listApi}` :
+						`${this.baseApi}/list`, this.queryParam)
+					if (res.code === 0) {
+						const data = this.dataFilter(res.data, this.checkedUsers)
+						this.list = replace ? data : this.list.concat(data)
+						this.page = res.page
+						this.page.pageIndex++
+					} else {
+						this.showToast(res.msg)
+					}
+				} catch (e) {
+					//TODO handle the exception
+				} finally {
+					this.triggered = false
+					this.status = (this.list || []).length == this.page.totalCount ? 'nomore' : 'loadmore'
+				}
+			},
+			// 搜索用户(附带防抖)
+			search(value) {
+				clearTimeout(this.searchDebounce);
+				this.searchDebounce = setTimeout(() => {
+					if (value) {
+						if (this.oldQueryParam == null) this.oldQueryParam = this.$u.deepClone(this.queryParam)
+						this.queryParam = {
+							realname: value
+						}
+						this.getList(true)
+					} else {
+						this.queryParam = this.$u.deepClone(this.oldQueryParam)
+						this.oldQueryParam = null
+						this.handlePullDownRefresh()
+					}
+					this.$forceUpdate()
+				}, 300)
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.division {
+		padding-left: 10px;
+		border-bottom-style: solid;
+		border-width: 1px;
+		border-color: #cccccc;
+	}
+
+	.tag+.tag {
+		margin-left: 5px;
+	}
+</style>

+ 57 - 0
components/di-radio-list/di-radio-list.vue

@@ -0,0 +1,57 @@
+<template>
+	<u-radio-group v-model="tempVal" :active-color="activeColor" @change="handleChange">
+		<u-radio v-for="(item, index) in list" :key="index" :name="item.value">
+			{{ item.label }}
+		</u-radio>
+	</u-radio-group>
+</template>
+
+<script>
+	/**
+	* di-radio-list 单选列表
+	* @description 单选列表组件,基于uview,适应与diboot接口的radio列表
+	* @property  {String}  activeColor 激活时候的颜色
+	* @property  {String Boolean, Number}  value 支持字符串、布尔、数字
+	* @property  {Array}  list 传入labelValue列表
+	*/
+	export default {
+		name:"di-radio-list",
+		data() {
+			return {
+				tempVal: this.value
+			};
+		},
+		methods: {
+			/**
+			 * 切换后触发
+			 * @param {Object} val
+			 */
+			handleChange(val) {
+				this.$emit('input', val)
+			}
+		},
+		watch: {
+			value(value) {
+				this.tempVal = value
+			}
+		},
+		props: {
+			value: {
+				type: [String, Boolean, Number],
+				require: true
+			},
+			list: {
+				type: Array,
+				require: true
+			},
+			activeColor: {
+				type: String,
+				default: '#19be6b'
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 96 - 0
components/di-region-picker/di-region-picker.vue

@@ -0,0 +1,96 @@
+<template>
+	<view class="di-region-picker">
+		<u-input ref='diRegion' v-model="tempVal" @click="show = true" disabled :select-open="show"
+			type="select" :placeholder="placeholder" />
+		<u-picker confirm-color="#18b566" v-model="show" :params="pickerParams" mode="region" @confirm="handlePicker" :default-region="defaultRegion"></u-picker>
+	</view>
+</template>
+
+<script>
+	/**
+	 * di-region-picker 区域选择器
+	 * @description 基于u-picker mode='region'模式
+	 * @property {String} value 可以使用v-model双向绑定,省市区请使用“-”分割,提交后自行处理
+	 * @property {String} placeholder 提示信息
+	 * @property {Boolean} city 是否选择市 默认true
+	 * @property {Boolean} area 是否选择区 默认true
+	 * @event {Function} confirm 点击确定按钮,传递出所选的完整的label-value值对象
+	 */
+	export default {
+		data() {
+			return {
+				show: false,
+				tempVal: this.value
+			}
+		},
+		watch: {
+			value(val) {
+				this.tempVal = val
+			}
+		},
+		methods: {
+			/**
+			 * 确认选择
+			 * 
+			 * @param {Object} value
+			 */
+			handlePicker(value) {
+				const{province, city, area} = value
+				const result = [province.label]
+				city && result.push(city.label)
+				area && result.push(area.label)
+				this.tempVal = result.join('-')
+				this.handleInputEvent(this.tempVal)
+				this.$emit('confirm', value)
+			},
+			/**
+			 * 发送input消息
+			 * 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值
+			 * 但是微信小程序上 尚未更新到u-form-item,导致获取的值为空
+			 */
+			handleInputEvent(value) {
+				this.$emit('input', value)
+				this.$nextTick(function(){
+					// 将当前的值发送到 u-form-item 进行校验
+					this.$refs.diRegion.dispatch('u-form-item', 'on-form-change', value);
+				})
+			}
+		},
+		computed: {
+			pickerParams(){
+				return {
+					province: true,
+					city: this.city,
+					area: this.area
+				}
+			},
+			defaultRegion() {
+				return this.value && typeof this.value === 'string' && this.value.split('-') || this.value || []
+			}
+		},
+		props: {
+			placeholder: {
+				type: String,
+				default: '请选择'
+			},
+			value: {
+				type: String,
+				require: true
+			},
+			city: {
+				type: Boolean,
+				default: true
+			},
+			area: {
+				type: Boolean,
+				default: true
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.di-region-picker {
+	width: 100%;
+}
+</style>

+ 55 - 0
components/di-scroll-menu-list/di-scroll-menu-list.vue

@@ -0,0 +1,55 @@
+<template>
+	<scroll-view class="di-scroll-menu-list" :scroll-x="true">
+		<view class="di-scroll-menu-list__item" :style="{'width':menuList.length > 4 ? '23%' : '25%'}" v-for="(menu, index) in menuList"
+			:key="menu.title">
+			<view class="di-scroll-menu-list__item-body" @click="handleClick(menu)">
+				<u-image width="64rpx" height="64rpx" :src="menu.icon"></u-image>
+				<text class="u-m-t-24">{{menu.title}}</text>
+			</view>
+		</view>
+	</scroll-view>
+</template>
+
+<script>
+	/**
+	* di-scroll-menu-list 滚动菜单
+	* @description 单行滚动菜单,可以用在首页单行展示多个菜单
+	* @property  {Array}  menu-list= [{title: '', icon: '', path: ''}] 
+	* @event {Function} click 点击传入当前menu的参数 
+	*/
+	export default {
+		methods: {
+			/**
+			 * 点击触发
+			 * @param {Object} menu
+			 */
+			handleClick(menu) {
+				menu.path && uni.navigateTo({
+					url: menu.path
+				})
+				this.$emit('click', menu)
+			}
+		},
+		props: {
+			menuList: {
+				type: Array,
+				require: true
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.di-scroll-menu-list {
+		white-space: nowrap;
+		width: 100%;
+		&__item {
+			display: inline-block;
+			&-body {
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+			}
+		}
+	}
+</style>

+ 115 - 0
components/di-select/di-select.vue

@@ -0,0 +1,115 @@
+<template>
+	<view class="di-select">
+		<u-input ref="diSelect" v-model="label" @click="show = true" disabled :select-open="show"
+			type="select" :placeholder="placeholder" />
+		<u-select v-model="show" :mode='mode' :list="list" @confirm="handleSelectConfirm"></u-select>
+	</view>
+</template>
+
+<script>
+	/**
+	 * di-select选择框
+	 * @description select列表组件,基于u-select,适配form
+	 * @property {Number String Array} value 可以使用v-model双向绑定
+	 * @property {String} placeholder 提示信息
+	 * @property {Array} list select展示的列表数据,与u-select的list保持一直
+	 * @property {String} mode = [single-column|mutil-column|mutil-column-auto] 模式选择,"single-column"-单列模式,"mutil-column"-多列模式,"mutil-column-auto"-多列联动模式
+	 * @event {Function} confirm 点击确定按钮,返回u-select#confirm事件保持一致
+	 */
+	export default {
+		data() {
+			return {
+				show: false,
+				label: ''
+			};
+		},
+		methods: {
+			/**
+			 * 点击确认
+			 * 
+			 * @param {Object} e
+			 */
+			handleSelectConfirm(e) {
+				if(this.mode === 'mutil-column' || this.mode === 'mutil-column-auto') {
+					let labelList = []
+					let valueList = []
+					e.forEach(item => {
+						labelList.push(item.label)
+						valueList.push(item.value)
+					})
+					this.label = labelList.join('-')
+					this.handleInputEvent(valueList)
+				} else {
+					this.label = e[0].label
+					this.handleInputEvent(e[0].value)
+				}
+				this.$emit("confirm", e)
+			},
+			async __setLabel(val) {
+				let time = setTimeout(() => {
+					clearTimeout(time)
+					if(this.mode === 'mutil-column' || this.mode === 'mutil-column-auto') {
+						let valueList = this.value.split(',')
+						if(!!valueList) {
+							return
+						}
+						let labelList = []
+						valueList.forEach(valueItem => {
+							let selectItem = val.filter(item => item.value === valueItem)
+							selectItem && selectItem.length > 0 && labelList.push(selectItem[0].label)
+						})
+						this.label = labelList.join('-')
+					} else {
+						const selectItem = val.filter(item => item.value === this.value)
+						this.label = selectItem && selectItem.length > 0 && selectItem[0].label || ''
+					}
+					// H5下,label值会覆盖value值
+					this.value && this.handleInputEvent(this.value)
+				}, 0)
+			},
+		    /**
+		    * 发送input消息
+		    * 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值
+		    * 但是微信小程序上 尚未更新到u-form-item,导致获取的值为空
+		    */
+		    handleInputEvent(value) {
+				this.$emit('input', value)
+				setTimeout(() => {
+				  this.$refs.diSelect.dispatch('u-form-item', 'on-form-change', value);
+				}, 60)
+			}
+		},
+		watch: {
+			list: {
+				immediate: true,
+				handler(value) {
+					this.__setLabel(value)
+				}
+			}
+		},
+		props: {
+			placeholder: {
+				type: String,
+				default: '请选择'
+			},
+			value: {
+				type: [Number, String, Array],
+				require: true
+			},
+			mode: {
+				type: String,
+				default: 'single-column'
+			},
+			list: {
+				type: Array,
+				default: () => []
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+.di-select {
+	width: 100%;
+}
+</style>

+ 44 - 0
components/di-switch/di-switch.vue

@@ -0,0 +1,44 @@
+<template>
+	<view class="di-switch">
+		<u-switch v-model="tempVal" active-color="#19be6b"></u-switch>
+	</view>
+</template>
+
+<script>
+	/**
+	 * di-switch 开关组件
+	 * @description 开关组件,基于u-switch,适配form
+	 * @property {Boolean} value 可以使用v-model双向绑定
+	 */
+	export default {
+		name:"di-switch",
+		data() {
+			return {
+				tempVal: this.value
+			};
+		},
+		watch: {
+			tempVal(val) {
+				this.$emit('input', val)
+			},
+			//回显
+			value: {
+				immediate: true,
+				handler(value) {
+					this.tempVal = value
+				}
+			}
+			
+		},
+		props: {
+			value: {
+				type: Boolean,
+				default: false
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 206 - 0
components/di-upload/di-upload.vue

@@ -0,0 +1,206 @@
+<template>
+	<u-upload
+	class="di-upload"
+	:max-size="uploadConfig.maxSize"
+	:max-count="uploadConfig.limitCount"
+	:upload-text="uploadConfig.uploadText"
+	:width="uploadConfig.width"
+	:height="uploadConfig.height"
+	name="file"
+	:show-progress="uploadConfig.showProgress"
+	:deletable="uploadConfig.deletable"
+	:action="uploadAction"
+	@on-success="handleSuccess"
+	@on-remove="handleRemove"
+	:form-data="uploadFormData"
+	:header="header"
+	:file-list="tempFileList"
+	>
+	</u-upload>
+</template>
+
+<script>
+	/**
+	 * di-upload 上传组件
+	 * @description 上传组件,基于u-upload,适配form
+	 * @property {String} value 上传后的后端返回地址,支持v-model双向绑定
+	 * @property {String} action 向后端发送的请求地址,默认'/uploadFile/upload/dto'
+	 * @property {String} rel-obj-type 绑定的业务对象名,自动设置到form-data对象中
+	 * @property {String} rel-obj-field 绑定业务对象的属性,自动设置到form-data对象中
+	 * @property {Object} form-data 提交的form-data,优先级最低,单独的属性设置会覆盖对象的配置
+	 * @property {Array} file-list 文件存储位置
+	 * @property {String} limit-count 上传数量限制,默认1,自动设置到config对象中
+	 * @property {Boolean} show-progress 是否显示进度条,默认true,自动设置到config对象中
+	 * @property {String} max-size 选择单个文件的最大大小,单位B(byte),默认不限制,自动设置到config对象中
+	 * @property {Boolean} deletable 是否显示删除图片的按钮,默认true,自动设置到config对象中
+	 * @property {String} width 图片预览区域和添加图片按钮的宽度(单位:rpx),默认200 ,自动设置到config对象中
+	 * @property {String} height 图片预览区域和添加图片按钮的高度(单位:rpx) ,默认200,自动设置到config对象中
+	 * @property {String} upload-text 上传框里面的文本 ,默认 ‘上传’,自动设置到config对象中
+	 * @property {Object} config 上传组件配置,优先级最低,单独的属性设置会覆盖对象的配置
+	 */
+	import Emitter from '../../uview-ui/libs/util/emitter.js';
+	export default {
+		mixins: [Emitter],
+		computed: {
+			header() {
+				return {
+					authtoken: uni.getStorageSync('authtoken') || ''
+				}
+			},
+			// 更新回显
+			tempFileList: {
+				get() {
+					return this.fileList
+				},
+				set() {}
+			},
+			uploadFormData() {
+				let tempFormData = this.formData || {}
+				tempFormData.relObjType = this.relObjType
+				tempFormData.relObjField = this.relObjField
+				return tempFormData
+			},
+			uploadConfig() {
+				let tempConfig = this.config || {}
+				tempConfig.maxSize = this.maxSize
+				tempConfig.limitCount = this.limitCount
+				tempConfig.uploadText = this.uploadText
+				tempConfig.width = this.width
+				tempConfig.height = this.height
+				tempConfig.showProgress = this.showProgress
+				tempConfig.deletable = this.deletable
+				return tempConfig
+			},
+			uploadAction() {
+				return `${this.$cons.host()}${this.action}`
+			}
+		},
+		methods: {
+			/**
+			 * 上传成功触发
+			 * @param {Object} data
+			 * @param {Object} index
+			 * @param {Object} lists
+			 * @param {Object} name
+			 */
+			handleSuccess(data, index, lists, name) {
+				lists.length = 0
+				if (this.limitCount === 1) {
+				    this.fileList.length = 0
+				}
+				this.fileList.push(this.fileFormatter(data.data))
+				console.log('add end', this.fileList)
+				this.$emit('update:fileList', this.fileList)
+				this.$forceUpdate()
+				this.handleInputEvent()
+			},
+			/**
+			 * 删除图片触发
+			 * @param {Object} index
+			 * @param {Object} lists
+			 * @param {Object} name
+			 */
+			handleRemove(index, lists, name) {
+				const newFileList = this.fileList.slice()
+				newFileList.splice(index, 1)
+				this.fileList.length = 0
+				this.fileList.push(...newFileList)
+				this.$emit('update:fileList', this.fileList)
+				console.log('remove end', this.fileList)
+				this.handleInputEvent()
+			},
+			/**
+			 * 文件转化
+			 * 
+			 * @param {Object} data
+			 */
+			fileFormatter(data) {
+				return {
+					uid: data.uuid,
+					filePath: data.accessUrl,
+					url: `${this.$cons.host()}${data.accessUrl}/image`
+				}
+			},
+			/**
+			 * 发送input消息
+			 * 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值
+			 * 但是微信小程序上 尚未更新到u-form-item,导致获取的值为空
+			 */
+			handleInputEvent() {
+				const value = this.fileList.map(file => file.filePath).join(',')
+				this.$emit('input', value)
+				this.$nextTick(function(){
+					// 将当前的值发送到 u-form-item 进行校验
+					this.dispatch('u-form-item', 'on-form-change', value);
+				})
+			}
+		},
+		props: {
+			value: {
+				type: String,
+				require: true
+			},
+			action: {
+				type: String,
+				default: '/uploadFile/upload/dto'
+			},
+			relObjType: {
+			  type: String,
+			  required: true
+			},
+			relObjField: {
+			  type: String,
+			  required: true
+			},
+			// 回显操作
+			fileList: {
+				type: Array,
+				require: true
+			},
+			limitCount: {
+				type: Number,
+				default: 1
+			},
+			maxSize: {
+				type: Number,
+				default: Number.MAX_VALUE
+			},
+			height: {
+				type: [Number, String],
+				default: 200
+			},
+			width: {
+				type: [Number, String],
+				default: 200
+			},
+			deletable: {
+				type: Boolean,
+				default: true
+			},
+			showProgress: {
+				type: Boolean,
+				default: true
+			},
+			uploadText: {
+				type: String,
+				default: '上传'
+			},
+			formData: {
+				type: Object,
+				default: () => {}
+			},
+			config: {
+				type: Object,
+				default: () => {}
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+   .u-upload {
+	   /deep/ .u-list-item {
+		   display: flex;
+	   }
+   }
+</style>

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 44 - 0
main.js

@@ -0,0 +1,44 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import {setTip} from '@/utils/common.js'
+import color from '@/utils/color.js'
+Vue.config.productionTip = false
+App.mpType = 'app'
+
+import uView from "uview-ui";
+Vue.use(uView);
+
+import {dibootApi} from './utils/dibootApi.js'
+import constant from './utils/constant.js'
+import Member from './classes/class.member.js'
+import PwdLogin from './classes/class.pwd.login.js'
+// import MpLogin from './classes/class.mp.login.js'
+import MiniLogin from './classes/class.mini.login.js'
+
+Vue.prototype.$dibootApi = dibootApi
+Vue.prototype.$cons = constant
+Vue.prototype.$member = new Member
+Vue.prototype.$pwdLogin = new PwdLogin
+// Vue.prototype.$mpLogin = new MpLogin
+Vue.prototype.$miniLogin = new MiniLogin
+
+Vue.prototype.$tip = setTip
+Vue.prototype.$color = color
+
+const app = new Vue({
+    ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  return {
+    app
+  }
+}
+// #endif

+ 85 - 0
manifest.json

@@ -0,0 +1,85 @@
+{
+    "name" : "diboot-mobile-ui",
+    "appid" : "__UNI__3D45701",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {
+            "Geolocation" : {}
+        },
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {
+                "geolocation" : {
+                    "system" : {
+                        "__platform__" : [ "ios", "android" ]
+                    }
+                }
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false,
+            "minified" : false,
+            "es6" : true,
+            "postcss" : false
+        },
+        "usingComponents" : true,
+        "permission" : {}
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+	"vueVersion" : "2",
+    "h5" : {}
+}

+ 46 - 0
mixins/detail.js

@@ -0,0 +1,46 @@
+import {dibootApi} from '@/utils/dibootApi'
+
+export default {
+	data() {
+		return {
+			// 请求接口基础路径
+			baseApi: '/',
+			// 当前详情框详情数据
+			model: {}
+		}
+	},
+	/**
+	 * 打开详情
+	 * @param id ;/test?id=1
+	 */
+	onLoad(option) {
+		this.open(option.id)
+	},
+	methods: {
+		/**
+		 * 打开详情
+		 * @returns {Promise<void>}
+		 */
+		async open(id) {
+			const res = await dibootApi.get(`${this.baseApi}/${id}`)
+			if (res.code === 0) {
+				this.model = res.data
+			} else {
+				uni.showToast({
+					title: '获取数据失败',
+					icon: 'error'
+				});
+			}
+		},
+		/**
+		 * 预览保存图片
+		 * @param path
+		 */
+		previewImage(path) {
+			uni.previewImage({
+				urls: [path],
+				longPressActions: true
+			})
+		}
+	}
+}

+ 276 - 0
mixins/form.js

@@ -0,0 +1,276 @@
+import {dibootApi} from '@/utils/dibootApi'
+import more from './more'
+
+export default {
+	mixins: [more],
+	data() {
+		return {
+			// 主键字段名
+			primaryKey: 'id',
+			// 请求接口基础路径
+			baseApi: '/',
+			// 新建接口
+			createApi: '',
+			// 更新接口
+			updateApiPrefix: '',
+			// 标题
+			title: '',
+			// 存储当前对象form数据
+			form: {},
+			// 当前form是否包含上传
+			isUpload: false,
+			// 确认提交
+			confirmSubmit: false,
+			/**
+			 * 所有文件的集合都放置与fileWrapper对象中,提交的时候会自动遍历
+			 * 格式如下:
+			 * fileWrapper: {
+			 *  singleImageList: [],
+			 *  multiImageList: [],
+			 *  singleFileList: [],
+			 *  multiFileList: []
+			 * }
+			 */
+			fileWrapper: {},
+			/**
+			 * uuid集合
+			 */
+			fileUuidList: [],
+			/**
+			 *
+			 * 激活的颜色:主要用于checkbox、radio等,保持风格统一
+			 */
+			activeColor: this.$color.success
+		}
+	},
+	/**
+	 * 打开表单
+	 * @param id ;/test?id=1
+	 */
+	onLoad(option) {
+		this.open(option.id)
+	},
+	methods: {
+		/**
+		 * 打开
+		 * @param {Object} id
+		 */
+		async open(id) {
+			if (id === undefined) {
+				// 没有id数据则认为是新建
+				this.title = '新建'
+				this.afterOpen(id)
+			} else {
+				uni.showLoading({
+				    title: '加载中'
+				});
+				try{
+					// 否则作为更新处理
+					const res = await dibootApi.get(`${this.baseApi}/${id}`)
+					if (res.code === 0) {
+						this.form = res.data
+						this.title = '更新'
+						this.afterOpen(id)
+					} else {
+						uni.showToast({
+							title: res.msg
+						});
+					}
+				} finally {
+					uni.hideLoading()
+				}
+			}
+			await this.attachMore()
+		},
+		afterOpen(id) {
+		},
+		/** *
+		 * 提交前的验证流程
+		 * @returns {Promise<any>}
+		 */
+		validate() {
+			return new Promise((resolve, reject) => {
+				// rules存在,进行校验
+				if(this.$refs.uForm.rules && Object.keys(this.$refs.uForm.rules).length > 0) {
+					this.$refs.uForm.validate(valid => {
+						valid ? resolve(true) : reject(false)
+					});
+				} else {
+					resolve(true)
+				}
+			})
+		},
+		/** *
+		 * 提交前对数据的处理(在验证正确之后的处理)
+		 * @param values 提交的参数
+		 */
+		async enhance(values) {},
+		/** *
+		 * 新建记录的提交
+		 * @param values 提交的参数
+		 * @returns {Promise<string>}
+		 */
+		async add(values) {
+			const createApi = this.createApi ? this.createApi : '/'
+			const res = await dibootApi.post(`${this.baseApi}${createApi}`, values)
+			if (res.code === 0) {
+				return {
+					data: res.data,
+					msg: '添加成功'
+				}
+			} else {
+				throw new Error(res.msg)
+			}
+		},
+		/** *
+		 * 更新记录的提交
+		 * @param values
+		 * @returns {Promise<string>}
+		 */
+		async update(values) {
+			const updateApiPrefix = this.updateApiPrefix ? this.updateApiPrefix : ''
+			const res = await dibootApi.put(`${this.baseApi}${updateApiPrefix}/${this.form[this.primaryKey]}`, values)
+			if (res.code === 0) {
+				return {
+					data: res.data,
+					msg: '更新记录成功'
+				}
+			} else {
+				throw new Error(res.msg)
+			}
+		},
+		/** *
+		 * 表单提交事件
+		 * @returns {Promise<void>}
+		 */
+		async onSubmit() {
+			this.confirmSubmit = true
+			uni.showLoading({
+			    title: '提交中...'
+			});
+			try {
+				const valid = await this.validate()
+				if(!valid) {
+					uni.hideLoading()
+					return
+				}
+				await this.enhance()
+				let result = {}
+				if (this.form[this.primaryKey] === undefined) {
+					// 新增该记录
+					result = await this.add(this.form)
+				} else {
+					// 更新该记录
+					result = await this.update(this.form)
+				}
+				// 执行提交成功后的一系列后续操作
+				this.submitSuccess(result)
+			} catch (e) {
+				// 执行提交失败后的一系列后续操作
+				this.submitFailed(e)
+				console.log(e)
+			} finally {
+				uni.hideLoading()
+				this.confirmSubmit = false
+			}
+		},
+		/** *
+		 * 提交成功之后的处理
+		 * @param msg
+		 */
+		submitSuccess(result) {
+			uni.showToast({
+				title: '操作成功',
+				duration: 2000,
+				success: ()=>{
+					uni.navigateBack({
+					    delta: 1
+					});
+				}
+			});
+		},
+		/** *
+		 * 提交失败之后的处理
+		 * @param e
+		 */
+		submitFailed(e) {
+			// 如果是字符串,直接提示
+			let msg
+			if (typeof e === 'string') {
+				msg = e
+			} else if (typeof e === 'boolean') {
+				msg = ''
+			} else {
+				msg = e.message || e.msg
+			}
+			if(msg) {
+				uni.showToast({ title: msg, icon: 'error'})
+			}
+		},
+		/**
+		 * 文件转化
+		 *
+		 * @param {Object} data
+		 */
+		fileFormatter(data) {
+			return {
+				uid: data.uuid,
+				filePath: data.accessUrl,
+				url: `${this.$cons.host()}${data.accessUrl}/image`
+			}
+		},
+		/**
+		 * 将属性值转化为数组
+		 * @param fieldName
+		 * @param separator
+		 */
+		transformStr2Arr(fieldName, separator = ',') {
+			this.$set(this.form, fieldName, this.strSplit(this.form[fieldName], separator))
+		},
+		/**
+		 * 字符串分割
+		 * @param str
+		 * @param separator
+		 */
+		strSplit(str, separator = ',') {
+			return str ? str.split(',') : []
+		},
+		/**
+		 * 设置文件uuid
+		 * @private
+		 */
+		__setFileUuidList__() {
+			if (!this.isUpload) {
+				return
+			}
+			// 如果包含上传功能,那么设置uuid
+			this.fileUuidList = []
+			const fileWrapperKeys = Object.keys(this.fileWrapper)
+			if (fileWrapperKeys.length === 0) {
+				return
+			}
+			for (const fileWrapperKey of fileWrapperKeys) {
+				const tempFileList = this.fileWrapper[fileWrapperKey]
+				if (tempFileList && tempFileList.length && tempFileList.length > 0) {
+					this.fileUuidList.push(...tempFileList.map(item => item.uid))
+				}
+			}
+			this.form['fileUuidList'] = this.fileUuidList
+		},
+		/**
+		 * 初始化fileWrapper
+		 * @private
+		 */
+		__defaultFileWrapperKeys__() {
+			const fileWrapperKeys = Object.keys(this.fileWrapper)
+			if (fileWrapperKeys.length > 0) {
+				for (const fileWrapperKey of fileWrapperKeys) {
+					this.fileWrapper[fileWrapperKey] = []
+				}
+			} else {
+				this.fileWrapper = {}
+			}
+			this.fileUuidList = []
+		}
+	}
+}

+ 219 - 0
mixins/list.js

@@ -0,0 +1,219 @@
+import {dibootApi} from '@/utils/dibootApi'
+import more from './more'
+
+export default {
+	mixins: [more],
+	data() {
+		return {
+			primaryKey: 'id',
+			// 请求接口基础路径
+			baseApi: '/',
+			// 列表数据接口
+			listApi: '',
+			// 删除接口
+			deleteApiPrefix: '',
+			// 是否在页面初始化时自动加载列表数据
+			getListFromMixin: true,
+			// 与查询条件绑定的参数(会被查询表单重置和改变的参数)
+			queryParam: {},
+			// 用户自定义查询条件
+			customQueryParam: {},
+			// //下拉刷新的状态
+			triggered: false, 
+			// load状态
+			status: 'loadmore',
+			loadText: {
+				loadmore: '上拉加载更多',
+				loading: '努力加载中',
+				nomore: '没有更多了'
+			},
+			// 分页
+			page: {
+				pageIndex: 1,
+				pageSize: 20,
+				totalCount: 0,
+				totalPage: 0
+			},
+			// 激活的Index
+			activeIndex: -100,
+			// 是否弹出删除
+			deleteShow: false,
+			// 右滑菜单列表
+			actionOptions: [{
+				text: '编辑',
+				type: 'handleUpdate',
+				style: {
+					backgroundColor: this.$color.warning
+				}
+			}, {
+				text: '删除',
+				type: 'handleDelete',
+				style: {
+					backgroundColor: this.$color.error
+				}
+			}],
+			// 数据列表
+			list: [],
+			// 是否允许访问详情
+			allowGoDetail: true,
+			// 状态栏高度
+			diStatusBarHeight: 0,
+			// 阻止重复发送
+			keyMap: {}
+		}
+	},
+	onLoad() {
+		this.diStatusBarHeight = uni.getSystemInfoSync().statusBarHeight
+		
+	},
+	onShow() {
+		this.activeIndex = -100
+		this.getListFromMixin && this.getList(true)
+	},
+	methods: {
+		/**
+		 * 新增
+		 */
+		handleCreate() {
+			uni.navigateTo({
+				url: './form'
+			})
+		},
+		/*
+		 * 详情
+		 */
+		handleDetail(id) {
+			if(!this.allowGoDetail) {
+				return
+			}
+			uni.navigateTo({
+				url:`./detail?id=${id}`
+			})
+		},
+		/* 
+		 * 编辑
+		 */
+		handleUpdate(id) {
+			uni.navigateTo({
+				url: `./form?id=${id}`
+			})
+		},
+		/**
+		 * 删除
+		 */
+		handleDelete(id) {
+			this.deleteShow = true
+			this.activeIndex = id
+		},
+		/**
+		 * 确认删除
+		 * @param {Object} id
+		 */
+		async handleConfirmDel() {
+			try{
+				const deleteApiPrefix = this.deleteApiPrefix ? this.deleteApiPrefix : ''
+				const res = await dibootApi.delete(`${this.baseApi}${deleteApiPrefix}/${this.activeIndex}`)
+				this.showToast(res.msg, res.code === 0 ? 'success' : 'error')
+			}catch(e){
+				console.log(e)
+				this.showToast('网络异常!')
+			} finally {
+				this.page.pageIndex = 1
+				this.getList(true)
+				this.handleCancelDel()
+			}
+			
+		},
+		/**
+		 * 取消删除
+		 */
+		handleCancelDel() {
+			this.deleteShow = false
+			this.activeIndex = -100
+		},
+		/*
+		 * 打开左滑操作
+		 */
+		handleActiveSwipeAction(index) {
+			this.activeIndex = index
+		},
+		/**
+		 * 点击左滑按钮
+		 * @param {Number} index  所在列表的primaryKey
+		 * @param {Number} optionIdx  操作列表actionOptions的下标
+		 */
+		handleActionClick(index, optionIdx) {
+			this[this.actionOptions[optionIdx]['type']](index)
+		},
+		/**
+		 * 下拉刷新
+		 */
+		handlePullDownRefresh() {
+			if (this.triggered) return
+			this.triggered = true
+			this.page.pageIndex = 1
+			this.getList(true)
+		},
+		/**
+		 * 触底加载
+		 */
+		handleOnreachBottom() {
+			// 将当前pageIndex制作成下标,并查看缓存中是否存在指定下标的值
+			const key = `_${this.page.pageIndex}`
+			const value = this.keyMap[key]
+			// 如果value存在,表示缓存有值,那么阻止请求
+			if(value) {
+				return 
+			}
+			// value不存在,表示第一次请求,设置占位
+			this.keyMap[key] = 'temp'
+			this.status = 'nomore'
+			if (this.page.pageIndex <= this.page.totalPage) {
+				this.getList()
+			}
+		},
+		/**
+		 * 获取数据列表
+		 */
+		async getList(replace = false) {
+			try{
+				this.status = 'loading'
+				const tempQueryParam = this.buildQueryParamPage()
+				const res = await dibootApi.get(this.listApi ? `${this.baseApi}/${this.listApi}` : `${this.baseApi}/list`, tempQueryParam)
+				if (res.code === 0) {
+					this.list = replace ? res.data : this.list.concat(res.data)
+					this.page = res.page
+					this.page.pageIndex++
+				} else {
+					this.showToast(res.msg)
+				}
+			}catch(e){
+				//TODO handle the exception
+			} finally {
+				this.triggered = false
+				this.status = (this.list || []).length == this.page.totalCount ? 'nomore' : 'loadmore'
+			}
+			
+		},
+		buildQueryParamPage() {
+			this.queryParam.pageIndex = this.page.pageIndex
+			return Object.assign(this.queryParam, this.customQueryParam)
+		},
+		/**
+		 * 展示提示
+		 * @param {Object} title 提示内容
+		 * @param {Object} icon 提示icon, 默认使用error
+		 */
+		showToast(title, icon = 'error') {
+			uni.showToast({
+			    title,
+				icon
+			});
+		}
+	},
+	computed: {
+		listMargin() {
+			return `margin: ${this.list.length === 0 ? 0 : 20}rpx`
+		}
+	}
+}

+ 61 - 0
mixins/more.js

@@ -0,0 +1,61 @@
+import {dibootApi} from '@/utils/dibootApi'
+
+export default {
+	data() {
+		return {
+			// 请求接口基础路径
+			baseApi: '/',
+			// 是否从当前业务的attachMore接口中自动获取关联数据
+			getMore: false,
+			// 获取关联数据列表的配置列表
+			attachMoreList: [],
+			// 远程过滤关联数据列表的配置对象
+			attachMoreLoader: {},
+			// 远程过滤加载状态
+			attachMoreLoading: false,
+			// 关联相关的更多数据
+			more: {}
+		}
+	},
+	methods: {
+		/**
+		 * 加载当前页面关联的对象或者字典
+		 */
+		async attachMore() {
+			const reqList = []
+			// 个性化接口
+			this.getMore === true && reqList.push(dibootApi.get(`${this.baseApi}/attachMore`))
+			// 通用获取当前对象关联的数据的接口
+			this.attachMoreList.length > 0 && reqList.push(dibootApi.post('/common/attachMore', this
+				.attachMoreList))
+			if (reqList.length > 0) {
+				const resList = await Promise.all(reqList)
+				resList.forEach(res => res.ok ? Object.keys(res.data).forEach(key => this.$set(this.more, key, res.data[key]))
+					: uni.showToast({title: res.msg || '获取选项数据失败', icon: 'error'}))
+			}
+		},
+		/**
+		 * 远程过滤加载选项
+		 *
+		 * @param value 输入值
+		 * @param loader 加载器类型
+		 */
+		attachMoreFilter(value, loader) {
+			if (value == null || (value = value.trim()).length === 0) {
+				this.$set(this.more, `${loader}Options`, [])
+				return
+			}
+			this.attachMoreLoading = true
+			const moreLoader = this.attachMoreLoader[loader]
+			moreLoader.keyword = value
+			dibootApi.post('/common/attachMoreFilter', moreLoader).then(res => {
+				res.ok ? this.$set(this.more, `${loader}Options`, res.data)
+					: uni.showToast({title: res.msg || '获取选项数据失败', icon: 'error'})
+				this.attachMoreLoading = false
+			}).catch(() => {
+				uni.showToast({title: res.msg || '获取选项数据失败', icon: 'error'})
+				this.attachMoreLoading = false
+			})
+		}
+	}
+}

+ 126 - 0
pages.json

@@ -0,0 +1,126 @@
+{
+	"easycom": {
+		"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/login/index",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/home/home",
+			"style": {
+				"navigationBarTitleText": "首页",
+				"enablePullDownRefresh": false,
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/component-page/index",
+			"style": {
+				"navigationBarTitleText": "组件",
+				"enablePullDownRefresh": false
+			}
+
+		},{
+			"path": "pages/component-page/crud/list",
+			"style": {
+				"navigationBarTitleText": "列表",
+				"navigationStyle": "custom"
+			}
+		},{
+			"path": "pages/component-page/crud/detail",
+			"style": {
+				"navigationBarTitleText": "详细",
+				"enablePullDownRefresh": false
+			}
+		},{
+			"path": "pages/component-page/crud/form",
+			"style": {
+				"navigationBarTitleText": "表单",
+				"enablePullDownRefresh": false
+			}
+		},{
+			"path": "pages/personal/personal",
+			"style": {
+				"navigationBarTitleText": "个人中心",
+				"enablePullDownRefresh": false
+			}
+		}
+	    ,{
+            "path" : "pages/workflowTask/index",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "navigationStyle": "custom"
+            }
+            
+        }
+        ,{
+            "path" : "pages/workflowTask/webviewPages/dealTask",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/workflowTask/webviewPages/startFlow",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/workflowTask/webviewPages/flowDiagram",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+    ],
+	"tabBar": {
+		"color": "#606266",
+		"selectedColor": "#19be6b",
+		"borderStyle": "black",
+		"backgroundColor": "#ffffff",
+		"list": [{
+			"pagePath": "pages/home/home",
+			"iconPath": "static/images/home.png",
+			"selectedIconPath": "static/images/home_selected.png",
+			"text": "首页"
+		},{
+			"pagePath": "pages/component-page/index",
+			"iconPath": "static/images/component.png",
+			"selectedIconPath": "static/images/component_selected.png",
+			"text": "组件"
+		}, {
+			"pagePath": "pages/personal/personal",
+			"iconPath": "static/images/personal.png",
+			"selectedIconPath": "static/images/personal_selected.png",
+			"text": "我的"
+		}]
+	},
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"condition": { //模式配置,仅开发期间生效
+		"current": 0, //当前激活的模式(list 的索引项)
+		"list": [{
+			"name": "", //模式名称
+			"path": "", //启动页面,必选
+			"query": "" //启动参数,在页面的onLoad函数里面得到
+		}]
+	}
+}

+ 38 - 0
pages/component-page/crud/detail.vue

@@ -0,0 +1,38 @@
+<template>
+	<di-descriptions :title="model.title" :title-bottom="true">
+		<u-tag slot="right" :text="model.sexLabel" type="success" size="mini"/>
+		<di-descriptions-item label-col="0" value="12">
+			<u-image slot="value" width="160rpx" height="160rpx" :src="model.avatar" round />
+		</di-descriptions-item>
+		<di-descriptions-item label="姓名" :value="model.name"/>
+		<di-descriptions-item label="年龄" :value="model.age"/>
+		<di-descriptions-item label="链接" :ellipsis="true">
+			<u-link slot="value" href="https://www.diboot.com">Diboot 低代码开发平台:写的更少,性能更好 - 开发人员的低代码框架</u-link>
+		</di-descriptions-item>
+		<di-descriptions-item label="简介">
+			<u-parse slot="value" :html="model.description" :use-cache="true" :selectable="true" :lazy-load="true" loading-img="https://dibootm.oss-cn-shanghai.aliyuncs.com/loading-img.png"></u-parse>
+		</di-descriptions-item>
+	</di-descriptions>
+</template>
+
+<script>
+	import detail from '@/mixins/detail'
+	export default {
+		data() {
+			return {
+				model: {
+					title: '用户明细',
+					sexLabel: '男',
+					avatar: require('@/static/logo.png'),
+					name: 'diboot-mobile-ui',
+					age: '1',
+					description: '<p>简述:请点击链接查看更多:<img style="max-width:100%" alt="logo" src="https://dibootm.oss-cn-shanghai.aliyuncs.com/loading-img.png" data-img-size-val="900,500"/></p>'
+				}
+			}
+		},
+		mixins:[detail]
+	}
+</script>
+
+<style>
+</style>

+ 178 - 0
pages/component-page/crud/form.vue

@@ -0,0 +1,178 @@
+<template>
+	<view class="u-p-24 page-bg-color" style="min-height: 100%;">
+		<view class="page-card u-p-l-24 u-p-r-24 u-p-b-24">
+			<u-form :model="form" ref="uForm" :label-width="150">
+				<u-form-item label="姓名" prop="name">
+					<u-input v-model="form.name" placeholder="请输入姓名" />
+				</u-form-item>
+				<u-form-item label="性别" prop="sex">
+					<di-select v-model="form.sex" placeholder="请选择性别" :list="list"></di-select>
+				</u-form-item>
+				<u-form-item label="水果" prop="fruits">
+					<di-checkbox-list v-model="form.fruits" :list="checkboxList"></di-checkbox-list>
+				</u-form-item>
+				<u-form-item label="味道" prop="taste">
+					<di-radio-list v-model="form.taste" :list="radioList"/>
+				</u-form-item>
+				<u-form-item label="开关">
+					<template slot="right">
+						<di-switch v-model="form.switchVal"></di-switch>
+					</template>
+				</u-form-item>
+				<u-form-item label="日历" prop="calendarDate">
+					<di-calendar-picker v-model="form.calendarDate" placeholder="请选择日期范围"/>
+				</u-form-item>
+				<u-form-item label="日历范围" prop="calendarRange">
+					<di-calendar-picker v-model="form.calendarRange" mode="range" placeholder="请选择日期范围"/>
+				</u-form-item>
+				<u-form-item label="地区" prop="region">
+					<di-region-picker v-model="form.region" placeholder="请选择地区"/>
+				</u-form-item>
+				<u-form-item label="时间" prop="time">
+					<di-date-picker v-model="form.time" placeholder="请选择时间" mode="datetime"/>
+				</u-form-item>
+				<u-form-item label="上传图片" prop="picture">
+					<di-upload v-model="form.picture" :file-list="fileWrapper.pictureList" rel-obj-field="picture" :rel-obj-type="relObjType"/>
+				</u-form-item>
+			</u-form>
+			<view class="u-m-t-60">
+				<u-button type="success">提交</u-button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import form from '@/mixins/form'
+	export default {
+		data() {
+			return {
+				form: {
+					"name": "123",
+					"taste": "麻辣",
+					"sex": "1",
+					"switchVal": true,
+					"fruits": "apple,mango",
+					"calendarDate": "2021-12-24",
+					"calendarRange": "2021-12-14~2021-12-24",
+					"region": "北京市-市辖区-东城区",
+					"time": "2021-12-29 17:36:38",
+					"picture": ""
+				},
+				list: [{
+						value: '1',
+						label: '男'
+					},
+					{
+						value: '2',
+						label: '女'
+					}
+				],
+				checkboxList: [
+					{
+						value: 'apple',
+						label: '苹果'
+					},
+					{
+						value: 'banana',
+						label: '香蕉'
+					},
+					{
+						value: 'mango',
+						label: '芒果'
+					}
+				],
+				radioList: [{
+						label: '鲜甜',
+						value: '鲜甜'
+					},
+					{
+						label: '麻辣',
+						value: '麻辣'
+					}
+				],
+				relObjType: 'Demo',
+				fileWrapper: {
+				  pictureList: []
+				},
+				isUpload: true,
+				rules: {
+					name: [{
+						required: true,
+						message: '请输入姓名',
+						trigger: ['blur', 'change']
+					}],
+					sex: [{
+						required: true,
+						message: '请选择性别',
+						trigger: ['blur', 'change']
+					}],
+					fruits: [{
+						required: true,
+						message: '请选择水果',
+						trigger: ['blur', 'change']
+					}],
+					taste: [{
+						required: true,
+						message: '请选择味道',
+						trigger: ['blur', 'change']
+					}],
+					calendarDate: [{
+						required: true,
+						message: '请选择日历',
+						trigger: ['blur', 'change']
+					}],
+					calendarRange: [{
+						required: true,
+						message: '请选择日历范围',
+						trigger: ['blur', 'change']
+					}],
+					region: [{
+						required: true,
+						message: '请选择地区',
+						trigger: ['blur', 'change']
+					}],
+					time: [{
+						required: true,
+						message: '请选择时间',
+						trigger: ['blur', 'change']
+					}],
+					picture: [{
+						required: true,
+						message: '请上传图片',
+						trigger: ['blur', 'change']
+					}],
+				}
+			};
+		},
+		mixins:[form],
+		methods: {
+			enhance (values) {
+			  this.__setFileUuidList__(values)
+			},
+			
+			/****
+			 * 打开表单之后的操作
+			 * @param id
+			 */
+			afterOpen (id) {
+				// 回显图片
+				if(id) {
+					// this.$dibootApi.get(`/uploadFile/getList/${id}/${this.relObjType}/picture`).then(res => {
+					//   if (res.code === 0) {
+					// 	if (res.data && res.data.length > 0) {
+					// 	  res.data.forEach(data => {
+					// 		this.fileWrapper.pictureList.push(this.fileFormatter(data,true))
+					// 	  })
+					// 	}
+					//   }
+					// })
+				}
+			}
+		},
+		// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
+		onReady() {
+			this.$refs.uForm.setRules(this.rules);
+		}
+	};
+</script>

+ 68 - 0
pages/component-page/crud/list.vue

@@ -0,0 +1,68 @@
+<template>
+	<view class="h100 page-bg-color" :style="{paddingTop: diStatusBarHeight + 44 + 'px'}">
+		<u-navbar title="列表" height="44" :immersive="true" :background="{background: '#f8f8f8'}" :border-bottom="false">
+			<u-icon @click="handleCreate" style="margin-right: 40rpx;" slot="right" name="plus" size="28" label="新建" color="#19be6b" label-color="#19be6b"/>
+		</u-navbar>
+		<scroll-view class="di-scroll" scroll-y  @scrolltolower="handleOnreachBottom" :refresher-triggered="triggered"
+			refresher-enabled @refresherrefresh="handlePullDownRefresh">
+			<view class="di-scroll-list">
+				<!-- 右滑 -->
+				<u-swipe-action
+					v-for="(item, index) in list"
+					:show="activeIndex === item[primaryKey]"
+					:key="index" 
+					:index='item.id'
+					:options="actionOptions"
+					@content-click="handleDetail"
+					@click="handleActionClick"
+					@open="handleActiveSwipeAction">
+					<di-descriptions :title="item.title" label-col="4" value-col="8" :border-bottom="true" >
+						<di-descriptions-item label="姓名" :value="item.value" :ellipsis="true"/>
+						<di-descriptions-item label="创建时间" :value="item.createTime"/>
+					</di-descriptions>
+				</u-swipe-action>
+			</view>
+			<u-loadmore v-if="!triggered" :status="status" :loadText='loadText' margin-top="24" margin-bottom="24" />
+		</scroll-view>
+		<u-modal 
+		 v-model="deleteShow"
+		 title="删除"
+		 :show-cancel-button="true"
+		 :confirm-color="$color.error"
+		 confirm-content="确认要删除吗?"
+		 @confirm="handleConfirmDel"
+		 @cancel="handleCancelDel">
+		</u-modal>
+	</view>
+</template>
+
+<script>
+	import list from '@/mixins/list'
+	export default {
+		mixins: [list],
+		methods: {
+			/**
+			 * 获取数据列表 (重写函数)
+			 */
+			async getList(replace = false) {
+				let count = 10000
+				setTimeout(() => {
+					let list = []
+					for (let i = 1; i <= this.page.pageSize; i++) {
+						list.push({
+							id: ++count,
+							title: '标题' + count,
+							value: 'Didoot',
+							createTime: '2021-11-22 10:27'
+						})
+					}
+					this.list = replace ? list : this.list.concat(list)
+					this.page.pageIndex++
+					this.triggered = false
+				}, 2000)
+			}
+		}
+	}
+</script>
+<style scoped lang="scss">
+</style>

+ 77 - 0
pages/component-page/index.vue

@@ -0,0 +1,77 @@
+<template>
+	<view class="home page-bg-color u-rela u-p-24">
+		<view class="u-m-b-30 menu-color">
+			<di-scroll-menu-list :menu-list="menuList"></di-scroll-menu-list>
+		</view>
+		<view class="u-m-b-40 page-card">
+			<navigation></navigation>
+		</view>
+	</view>
+</template>
+
+<script>
+	import navigation from './navigation/index.vue'
+	export default {
+		components: {
+			navigation
+		},
+		data() {
+			return {
+				swiperBgColor: '',
+				menuList: [
+					{
+						icon: require('@/static/images/list.png'),
+						title: '列表示例',
+						path: '/pages/component-page/crud/list'
+					},
+					{
+						icon: require('@/static/images/form.png'),
+						title: '表单示例',
+						path:'/pages/component-page/crud/form'
+					},
+					{
+						icon: require('@/static/images/detail.png'),
+						title: '详情示例',
+						path: '/pages/component-page/crud/detail'
+					},
+					{
+						icon: require('@/static/logo.png'),
+						title: '测试',
+						path: '/pages/testPage/list'
+					}
+				]
+			}
+		},
+		onLoad() {
+			this.$member.getMemberInfo()
+		},
+		methods: {
+			/* 
+			 设置轮播图的背景颜色
+			 */
+			setSwiperBgColor(color) {
+				this.swiperBgColor = color
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	page {
+		height: 100%;
+	}
+	.home {
+		height: 100%;
+		&-bg-color {
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 560rpx;
+		}
+		.menu-color {
+			background-color: #fff;
+			border-radius: 10rpx;
+			padding: 30rpx 0;
+		}
+	}
+</style>

+ 93 - 0
pages/component-page/navigation/index.vue

@@ -0,0 +1,93 @@
+<template>
+	<view class="navigation">
+		<view class="navigation-header u-border-bottom">
+			<text class="navigation-header-title">标题</text>
+			<view class="u-tips-color" @click.native="toListPage">
+				<text>更多</text>
+				<u-icon name="arrow-right"></u-icon>
+			</view>
+		</view>
+		<view>
+			<di-card
+				v-for="(item,index) in list" 
+				:key="item.id"
+				:class="{'u-border-bottom':index !== list.length - 1}"
+				:index="item.id"
+				:title="item.title" 
+				:image-list="item.imageList" 
+				:mode="item.mode"
+				@click="toDetail"
+				>
+				<view style="" slot="footer">
+					{{item.date}}
+				</view>
+			</di-card>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				list: [{
+						id: 100,
+						title: 'diboot 是一套全新的基于"坚固地基(基础框架)+高效脚手架(devtools代码生成工具)"的低代码开发平台,致力于解决开发的质量效率和可维护难题。',
+						imageList: [require('@/static/images/diboot/diboot-lowcode.jpeg')],
+						date: '2022-03-22 10:27',
+						mode: 'pictureCard'
+					},
+					{
+						id: 200,
+						title: 'diboot-cloud,基于diboot spring boot版本打造,将diboot的优势延伸到微服务开发场景。',
+						imageList: [require('@/static/images/diboot/diboot-cloud.jpeg')],
+						date: '2022-03-22 10:27',
+						mode: 'card'
+					},
+					{
+						id: 300,
+						title: '结合diboot平台的架构设计经验优势,匠心打磨出了 diboot-workflow 项目,助力企业客户基于我们的源码,跳过沼泽地,高效无风险落地流程审批业务。',
+						imageList: [require('@/static/images/diboot/diboot-workflow.png'), require('@/static/images/diboot/diboot-workflow2.png')],
+						date: '2022-03-22 10:27',
+						mode: 'multiple'
+					}
+				]
+			}
+		},
+		methods:{
+			/* 
+		       跳转列表页面	 
+			 */
+			toListPage() {
+				uni.navigateTo({
+					url:'/pages/demo/list'
+				})
+			},
+			/**
+			 * 跳转详情页
+			 */
+			toDetail() {
+				uni.navigateTo({
+					url:'/pages/demo/detail'
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.navigation {
+		background-color: #fff;
+		height: 100%;
+		&-header {
+			padding: 24rpx;
+			display: flex;
+			justify-content: space-between;
+
+			&-title {
+				font-size: 28rpx;
+				font-weight: bold;
+			}
+		}
+	}
+</style>

+ 39 - 0
pages/home/banner/index.vue

@@ -0,0 +1,39 @@
+<template>
+	<view>
+		<u-swiper :height="390" :list="list" @change="change"></u-swiper>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				list: [
+					{
+						image: require('@/static/images/diboot/diboot-lowcode.jpeg'),
+					},
+					{
+						image: require('@/static/images/diboot/diboot-workflow2.png'),
+					},
+					{
+						image: require('@/static/images/diboot/diboot-cloud.jpeg'),
+					},
+					{
+						image: require('@/static/images/diboot/diboot-workflow.png'),
+					}
+				],
+			}
+		},
+		mounted() {
+			this.$emit('setSwiperBgColor',this.list[0]['bgColor'])
+		},
+		methods: {
+			change(index) {
+			   this.$emit('setSwiperBgColor',this.list[index]['bgColor'])
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 568 - 0
pages/home/calendar/calendar.vue

@@ -0,0 +1,568 @@
+<template>
+	<view class="u-calendar">
+		<view class="u-calendar__action u-flex u-row-center">
+			<view class="u-calendar__action__icon">
+				<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
+			</view>
+			<view class="u-calendar__action__icon">
+				<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
+			</view>
+			<view class="u-calendar__action__text">{{ showTitle }}</view>
+			<view class="u-calendar__action__icon">
+				<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
+			</view>
+			<view class="u-calendar__action__icon">
+				<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
+			</view>
+			<view class="u-calendar__action__back" @click="init">
+				回到今天
+			</view>
+		</view>
+		<view class="u-calendar__week-day">
+			<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
+		</view>
+		<view class="u-calendar__content">
+			<!-- 前置空白部分 -->
+			<block v-for="(item, index) in weekdayArr" :key="index">
+				<view class="u-calendar__content__item"></view>
+			</block>
+			<view class="u-calendar__content__item u-calendar__content--start-date u-calendar__content--end-date" :class="{
+				'u-hover-class':openDisAbled(year,month,index+1)
+			}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
+			 @tap="dateClick(index)">
+				<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
+					<view>{{ index + 1 }}</view>
+				</view>
+			</view>
+			<view class="u-calendar__content__bg-month">{{month}}</view>
+		</view>
+	</view>
+</template>
+<script>
+	/**
+	 * calendar 日历
+	 * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
+	 * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
+	 * @property {String Number} max-year 可切换的最大年份(默认2050)
+	 * @property {String Number} min-year 可切换的最小年份(默认1950)
+	 * @property {String Number} min-date 最小可选日期(默认1950-01-01)
+	 * @property {String Number} max-date 最大可选日期(默认当前日期)
+	 * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
+	 * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
+	 * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
+	 * @property {String} color 日期字体的默认颜色(默认#303133)
+	 * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
+	 * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
+	 */
+	export default {
+		name: 'calendar',
+		props: {
+			// 是否允许切换年份
+			changeYear: {
+				type: Boolean,
+				default: true
+			},
+			// 是否允许切换月份
+			changeMonth: {
+				type: Boolean,
+				default: true
+			},
+			// date-单个日期选择,range-开始日期+结束日期选择
+			mode: {
+				type: String,
+				default: 'date'
+			},
+			// 可切换的最大年份
+			maxYear: {
+				type: [Number, String],
+				default: 2050
+			},
+			// 可切换的最小年份
+			minYear: {
+				type: [Number, String],
+				default: 1950
+			},
+			// 最小可选日期(不在范围内日期禁用不可选)
+			minDate: {
+				type: [Number, String],
+				default: '1950-01-01'
+			},
+			/**
+			 * 最大可选日期
+			 * 默认最大值为今天,之后的日期不可选
+			 * 2030-12-31
+			 * */
+			maxDate: {
+				type: [Number, String],
+				default: '2030-12-31'
+			},
+			// 弹窗顶部左右两边的圆角值
+			borderRadius: {
+				type: [String, Number],
+				default: 20
+			},
+			// 月份切换按钮箭头颜色
+			monthArrowColor: {
+				type: String,
+				default: '#606266'
+			},
+			// 年份切换按钮箭头颜色
+			yearArrowColor: {
+				type: String,
+				default: '#909399'
+			},
+			// 默认日期字体颜色
+			color: {
+				type: String,
+				default: '#303133'
+			},
+			// 选中|起始结束日期背景色
+			activeBgColor: {
+				type: String,
+				default: '#19be6b'
+			},
+			// 选中|起始结束日期字体颜色
+			activeColor: {
+				type: String,
+				default: '#ffffff'
+			},
+			// 范围内日期背景色
+			rangeBgColor: {
+				type: String,
+				default: 'rgba(41,121,255,0.13)'
+			},
+			// 当前选中日期带选中效果
+			isActiveCurrent: {
+				type: Boolean,
+				default: true
+			},
+			// 切换年月是否触发事件 mode=date时生效
+			isChange: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				// 星期几,值为1-7
+				weekday: 1, 
+				weekdayArr:[],
+				// 当前月有多少天
+				days: 0, 
+				daysArr:[],
+				showTitle: '',
+				year: 2020,
+				month: 0,
+				day: 0,
+				startYear: 0,
+				startMonth: 0,
+				startDay: 0,
+				endYear: 0,
+				endMonth: 0,
+				endDay: 0,
+				today: '',
+				activeDate: '',
+				startDate: '',
+				endDate: '',
+				isStart: true,
+				min: null,
+				max: null,
+				weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
+			};
+		},
+		computed: {
+			dataChange() {
+				return `${this.mode}-${this.minDate}-${this.maxDate}`;
+			},
+			uZIndex() {
+				// 如果用户有传递z-index值,优先使用
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		watch: {
+			dataChange(val) {
+				this.init()
+			}
+		},
+		created() {
+			this.init()
+		},
+		methods: {
+			getColor(index, type) {
+				let color = type == 1 ? '' : this.color;
+				let day = index + 1
+				let date = `${this.year}-${this.month}-${day}`
+				let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+				let start = this.startDate.replace(/\-/g, '/')
+				let end = this.endDate.replace(/\-/g, '/')
+				if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+					color = type == 1 ? this.activeBgColor : this.activeColor;
+				} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+					color = type == 1 ? this.rangeBgColor : this.rangeColor;
+				}
+				return color;
+			},
+			init() {
+				let now = new Date();
+				this.year = now.getFullYear();
+				this.month = now.getMonth() + 1;
+				this.day = now.getDate();
+				this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+				this.activeDate = this.today;
+				this.min = this.initDate(this.minDate);
+				this.max = this.initDate(this.maxDate || this.today);
+				this.startDate = "";
+				this.startYear = 0;
+				this.startMonth = 0;
+				this.startDay = 0;
+				this.endYear = 0;
+				this.endMonth = 0;
+				this.endDay = 0;
+				this.endDate = "";
+				this.isStart = true;
+				this.changeData();
+			},
+			//日期处理
+			initDate(date) {
+				let fdate = date.split('-');
+				return {
+					year: Number(fdate[0] || 1920),
+					month: Number(fdate[1] || 1),
+					day: Number(fdate[2] || 1)
+				}
+			},
+			openDisAbled: function(year, month, day) {
+				let bool = true;
+				let date = `${year}/${month}/${day}`;
+				// let today = this.today.replace(/\-/g, '/');
+				let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+				let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+				let timestamp = new Date(date).getTime();
+				if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+					bool = false;
+				}
+				return bool;
+			},
+			generateArray: function(start, end) {
+				return Array.from(new Array(end + 1).keys()).slice(start);
+			},
+			formatNum: function(num) {
+				return num < 10 ? '0' + num : num + '';
+			},
+			//一个月有多少天
+			getMonthDay(year, month) {
+				let days = new Date(year, month, 0).getDate();
+				return days;
+			},
+			getWeekday(year, month) {
+				let date = new Date(`${year}/${month}/01 00:00:00`);
+				return date.getDay();
+			},
+			checkRange(year) {
+				let overstep = false;
+				if (year < this.minYear || year > this.maxYear) {
+					uni.showToast({
+						title: "日期超出范围啦~",
+						icon: 'none'
+					})
+					overstep = true;
+				}
+				return overstep;
+			},
+			changeMonthHandler(isAdd) {
+				if (isAdd) {
+					let month = this.month + 1;
+					let year = month > 12 ? this.year + 1 : this.year;
+					if (!this.checkRange(year)) {
+						this.month = month > 12 ? 1 : month;
+						this.year = year;
+						this.changeData();
+					}
+
+				} else {
+					let month = this.month - 1;
+					let year = month < 1 ? this.year - 1 : this.year;
+					if (!this.checkRange(year)) {
+						this.month = month < 1 ? 12 : month;
+						this.year = year;
+						this.changeData();
+					}
+				}
+			},
+			changeYearHandler(isAdd) {
+				let year = isAdd ? this.year + 1 : this.year - 1;
+				if (!this.checkRange(year)) {
+					this.year = year;
+					this.changeData();
+				}
+			},
+			changeData() {
+				this.days = this.getMonthDay(this.year, this.month);
+				this.daysArr=this.generateArray(1,this.days)
+				this.weekday = this.getWeekday(this.year, this.month);
+				this.weekdayArr=this.generateArray(1,this.weekday)
+				this.showTitle = `${this.year}年${this.month}月`;
+				if (this.isChange && this.mode == 'date') {
+					this.btnFix(true);
+				}
+			},
+			dateClick: function(day) {
+				day += 1;
+				if (!this.openDisAbled(this.year, this.month, day)) {
+					this.day = day;
+					let date = `${this.year}-${this.month}-${day}`;
+					if (this.mode == 'date') {
+						this.activeDate = date;
+					} else {
+						let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
+						if (this.isStart || compare) {
+							this.startDate = date;
+							this.startYear = this.year;
+							this.startMonth = this.month;
+							this.startDay = this.day;
+							this.endYear = 0;
+							this.endMonth = 0;
+							this.endDay = 0;
+							this.endDate = "";
+							this.activeDate = "";
+							this.isStart = false;
+						} else {
+							this.endDate = date;
+							this.endYear = this.year;
+							this.endMonth = this.month;
+							this.endDay = this.day;
+							this.isStart = true;
+						}
+					}
+				}
+			},
+			close() {
+				// 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
+				this.$emit('input', false);
+			},
+			getWeekText(date) {
+				date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+				let week = date.getDay();
+				return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+			},
+			btnFix(show) {
+				if (!show) {
+					this.close();
+				}
+				if (this.mode == 'date') {
+					let arr = this.activeDate.split('-')
+					let year = this.isChange ? this.year : Number(arr[0]);
+					let month = this.isChange ? this.month : Number(arr[1]);
+					let day = this.isChange ? this.day : Number(arr[2]);
+					//当前月有多少天
+					let days = this.getMonthDay(year, month);
+					let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+					let weekText = this.getWeekText(result);
+					let isToday = false;
+					if (`${year}-${month}-${day}` == this.today) {
+						//今天
+						isToday = true;
+					}
+					this.$emit('change', {
+						year: year,
+						month: month,
+						day: day,
+						days: days,
+						result: result,
+						week: weekText,
+						isToday: isToday,
+						// switch: show //是否是切换年月操作
+					});
+				} else {
+					if (!this.startDate || !this.endDate) return;
+					let startMonth = this.formatNum(this.startMonth);
+					let startDay = this.formatNum(this.startDay);
+					let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+					let startWeek = this.getWeekText(startDate)
+
+					let endMonth = this.formatNum(this.endMonth);
+					let endDay = this.formatNum(this.endDay);
+					let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+					let endWeek = this.getWeekText(endDate);
+					this.$emit('change', {
+						startYear: this.startYear,
+						startMonth: this.startMonth,
+						startDay: this.startDay,
+						startDate: startDate,
+						startWeek: startWeek,
+						endYear: this.endYear,
+						endMonth: this.endMonth,
+						endDay: this.endDay,
+						endDate: endDate,
+						endWeek: endWeek
+					});
+				}
+			}
+		}
+	};
+</script>
+
+<style scoped lang="scss">
+	// 定义混入指令,用于在非nvue环境下的flex定义,因为nvue没有display属性,会报错
+	@mixin vue-flex($direction: row) {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		flex-direction: $direction;
+		/* #endif */
+	}
+	.u-calendar {
+		color: $u-content-color;
+		&__header {
+			width: 100%;
+			box-sizing: border-box;
+			font-size: 30rpx;
+			background-color: #fff;
+			color: $u-main-color;
+			
+			&__text {
+				margin-top: 30rpx;
+				padding: 0 60rpx;
+				@include vue-flex;
+				justify-content: center;
+				align-items: center;
+			}
+		}
+		
+		&__action {
+			padding: 40rpx 0 40rpx 0;
+			position: relative;
+			&__icon {
+				margin: 0 16rpx;
+			}
+			
+			&__text {
+				padding: 0 16rpx;
+				color: $u-main-color;
+				font-size: 32rpx;
+				line-height: 32rpx;
+				font-weight: bold;
+			}
+		}
+	
+		&__week-day {
+			@include vue-flex;
+			align-items: center;
+			justify-content: center;
+			padding: 6px 0;
+			overflow: hidden;
+			
+			&__text {
+				flex: 1;
+				text-align: center;
+			}
+		}
+	
+		&__content {
+			width: 100%;
+			@include vue-flex;
+			flex-wrap: wrap;
+			padding: 6px 0;
+			box-sizing: border-box;
+			background-color: #fff;
+			position: relative;
+			
+			&--end-date {
+				border-top-right-radius: 8rpx;
+				border-bottom-right-radius: 8rpx;
+			}
+			
+			&--start-date {
+				border-top-left-radius: 8rpx;
+				border-bottom-left-radius: 8rpx;
+			}
+			
+			&__item {
+				width: 14.2857%;
+				@include vue-flex;
+				align-items: center;
+				justify-content: center;
+				padding: 6px 0;
+				overflow: hidden;
+				position: relative;
+				z-index: 2;
+				
+				&__inner {
+					height: 84rpx;
+					@include vue-flex;
+					align-items: center;
+					justify-content: center;
+					flex-direction: column;
+					font-size: 32rpx;
+					position: relative;
+					border-radius: 50%;
+					
+					&__desc {
+						width: 100%;
+						font-size: 24rpx;
+						line-height: 24rpx;
+						transform: scale(0.75);
+						transform-origin: center center;
+						position: absolute;
+						left: 0;
+						text-align: center;
+						bottom: 2rpx;
+					}
+				}
+				
+				&__tips {
+					width: 100%;
+					font-size: 24rpx;
+					line-height: 24rpx;
+					position: absolute;
+					left: 0;
+					transform: scale(0.8);
+					transform-origin: center center;
+					text-align: center;
+					bottom: 8rpx;
+					z-index: 2;
+				}
+			}
+			
+			&__bg-month {
+				position: absolute;
+				font-size: 130px;
+				line-height: 130px;
+				left: 50%;
+				top: 50%;
+				transform: translate(-50%, -50%);
+				color: #e4e7ed;
+				z-index: 1;
+			}
+		}
+	
+		&__bottom {
+			width: 100%;
+			@include vue-flex;
+			align-items: center;
+			justify-content: center;
+			flex-direction: column;
+			background-color: #fff;
+			padding: 0 40rpx 30rpx;
+			box-sizing: border-box;
+			font-size: 24rpx;
+			color: $u-tips-color;
+			
+			&__choose {
+				height: 50rpx;
+			}
+			
+			&__btn {
+				width: 100%;
+			}
+		}
+		.u-calendar__action__back {
+			position: absolute;
+			color: #fff;
+			font-size: 24rpx;
+			right: -10rpx;
+			background-color: #19be6b;
+			padding: 5rpx 12rpx;
+			border-radius: 20rpx 0 0 20rpx;
+		}
+	}
+</style>

+ 132 - 0
pages/home/home.vue

@@ -0,0 +1,132 @@
+<template>
+	<view class="home page-bg-color u-rela">
+		<view class="banner">
+			<banner/>
+		</view>
+		<u-card margin="30rpx 0" padding="10" :border-radius="0" :border="false" :show-foot="false">
+			<view class="" slot="head">
+				<u-icon name="list-dot" class="u-margin-right-10 text"></u-icon>
+				<text class="text">业务服务</text>
+			</view>
+			<u-grid slot="body" :col="4" :border="false">
+				<u-grid-item :key="index" v-for="(menu, index) in menuList" @click="handleMenuClick(menu)">
+					<u-image :class="{gray: menu.forbidClick}" width="64rpx" height="64rpx" :src="menu.icon"></u-image>
+					<view class="menu-text text">{{menu.title}}</view>
+				</u-grid-item>
+			</u-grid>
+		</u-card>
+		<u-card margin="30rpx 0" padding="10" :border-radius="0" :border="false" :show-foot="false" title-size="28">
+			<view class="" slot="head">
+				<u-icon name="calendar-fill" class="u-margin-right-10 text"></u-icon>
+				<text class="text">日程安排</text>
+			</view>
+			<calendar slot="body" mode="date"></calendar>
+		</u-card>
+		
+	</view>
+</template>
+
+<script>
+	import banner from './banner/index.vue'
+	import calendar from './calendar/calendar.vue'
+	export default {
+		data() {
+			return {
+				show: true,
+				menuList: [
+					{
+						title: '发起流程',
+						icon: require('@/static/images/workflow/start.png'),
+						options: {
+							type: 'start'
+						}
+					},
+					{
+						title: '我发起的',
+						icon: require('@/static/images/workflow/myLanuch.png'),
+						options: {
+							type: 'myLaunch'
+						}
+					},
+					{
+						title: '我的待办',
+						icon: require('@/static/images/workflow/todo.png'),
+						options: {
+							type: 'todo'
+						}
+					},
+					{
+						title: '我的已办',
+						icon: require('@/static/images/workflow/done.png'),
+						options: {
+							type: 'done'
+						}
+					},
+					{
+						title: '请假申请',
+						forbidClick: true,
+						icon: require('@/static/images/menu/leave.png')
+					},
+					{
+						title: '采购申请',
+						forbidClick: true,
+						icon: require('@/static/images/menu/buy.png')
+					},
+					{
+						title: '企业内训',
+						forbidClick: true,
+						icon: require('@/static/images/menu/train.png')
+					},
+					{
+						title: '邮件通知',
+						forbidClick: true,
+						icon: require('@/static/images/menu/email.png')
+					}
+				]
+			};
+		},
+		components: {
+			banner,
+			calendar
+		},
+		methods: {
+			handleMenuClick(menu) {
+				if(!menu.forbidClick) {
+					this.openPage(menu.options)
+				}
+			},
+			/**
+			 * 打开工作流页面
+			 */
+			openPage(options) {
+				uni.navigateTo({
+					url: `/pages/workflowTask/index?type=${options.type}`
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		height: unset;
+	}
+	.home {
+		&-bg-color {
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 560rpx;
+		}
+		.text {
+			font-weight: 400;
+			font-size: $main-font-size;
+		}
+		.menu-text {
+			margin-top: 5rpx;
+		}
+		.gray {
+			filter: grayscale(100%);
+		}
+	}
+</style>

+ 52 - 0
pages/index/index.vue

@@ -0,0 +1,52 @@
+<template>
+	<view class="content">
+		<image class="logo" src="/static/logo.png"></image>
+		<view class="text-area">
+			<text class="title">{{title}}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title: 'Hello'
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style>
+	.content {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.logo {
+		height: 200rpx;
+		width: 200rpx;
+		margin-top: 200rpx;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 50rpx;
+	}
+
+	.text-area {
+		display: flex;
+		justify-content: center;
+	}
+
+	.title {
+		font-size: 36rpx;
+		color: #8f8f94;
+	}
+</style>

+ 122 - 0
pages/login/index.vue

@@ -0,0 +1,122 @@
+<template>
+	<view class="container">
+		<view class="container-title u-font-40">diboot-workflow mobile-UI</view>
+		<view class="u-m-b-60 container-type u-flex">
+			<view v-for="(item,index) in typeList" :key="item.name">
+				<text :class="{'u-font-40': item.value === currentType}"
+					@click="currentType = item.value">{{item.name}}</text>
+				<text class="u-m-l-10 u-m-r-10" v-if="index !== typeList.length - 1">/</text>
+			</view>
+		</view>
+		<view>
+			<login-form v-if="currentType === 'login'" @login="login" @signUp="currentType = 'register'"></login-form>
+			<register-form v-if="currentType === 'register'" @register="register" @signIn="currentType = 'login'">
+			</register-form>
+		</view>
+		<view class="u-m-t-80 ">
+			<u-divider margin-bottom="40">其他方式登陆</u-divider>
+			<view class="u-flex u-row-center">
+				<u-icon @click="weiLogin" size="90" name="weixin-circle-fill" color="rgb(83,194,64)"></u-icon>
+			</view>
+		</view>
+		<u-top-tips ref="uTips" navbar-height="0"/>
+	</view>
+</template>
+
+<script>
+	import loginForm from './loginForm.vue'
+	import registerForm from './registerForm.vue'
+	export default {
+		components: {
+			loginForm,
+			registerForm
+		},
+		data() {
+			return {
+				typeList: [{
+						name: '登录',
+						value: 'login'
+					},
+					{
+						name: '注册',
+						value: 'register'
+					}
+				],
+				currentType: ''
+			}
+		},
+		onShow() {
+			if(uni.getStorageSync("authtoken")) {
+				uni.switchTab({
+						url: '/pages/home/home'
+				})
+			}
+		},
+		onLoad() {
+			this.currentType = 'login'
+		},
+		methods: {
+			/**
+			 * 注册
+			 */
+			register(data) {
+				// 注册逻辑
+				this.$tip(this.$refs.uTips, '注册成功,请重新登陆').then(() => {
+					this.currentType = 'login'
+				})
+			},
+			/**
+			 * 登陆成功
+			 */
+			login(data) {
+				// 密码登陆
+				this.$pwdLogin
+					// .setTip(this.$refs.uTips)
+					.go(data)
+					.then(() => {
+						// 跳转到首页
+						uni.switchTab({
+							url: '/pages/home/home'
+						})
+					})
+			},
+			/**
+			 * 微信登陆:
+			 */
+			weiLogin() {
+				// 小程序登陆
+				// #ifdef MP-WEIXIN
+				this.$miniLogin.setTip(this.$refs.uTips).setBindWx(false).setUrlPath('/pages/home/home').go()
+				//#endif
+				// 微信公众号登陆
+				// #ifdef H5
+				this.$mpLogin.redirect()
+				//#endif
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.container {
+		padding: 160rpx 24rpx 40rpx;
+		background-image: url('https://dibootm.oss-cn-shanghai.aliyuncs.com/login_bg3.png'),
+			url('https://dibootm.oss-cn-shanghai.aliyuncs.com/login_bg3.png'),
+			url('https://dibootm.oss-cn-shanghai.aliyuncs.com/login_bg3.png'),
+			linear-gradient($u-type-success-dark 0rpx, $u-type-success-light 400rpx, rgba(255, 255, 255, 0) 500rpx);
+		background-position: -360rpx 250rpx, -120rpx 250rpx, 0 250rpx, 0 0;
+		background-repeat: no-repeat;
+		background-size: 115% 500rpx;
+
+		&-title {
+			text-align: center;
+			margin-bottom: 160rpx;
+			font-weight: bold;
+			color: #fff;
+		}
+
+		&-type {
+			font-weight: bold;
+		}
+	}
+</style>

+ 71 - 0
pages/login/loginForm.vue

@@ -0,0 +1,71 @@
+<template>
+	<view>
+		<u-form :model="form" ref="uForm" label-width="auto">
+			<u-form-item prop="username">
+				<u-input v-model="form.username" placeholder="请输入用户名" />
+			</u-form-item>
+			<u-form-item prop="password">
+				<u-input v-model="form.password" type="password" placeholder="请输入密码" />
+			</u-form-item>
+		</u-form>
+		<view class="u-m-t-60">
+			<u-button type="success" @click="submit">登录</u-button>
+		</view>
+		<view class="u-m-t-40 u-text-right u-type-success">
+			<text @click="signUp">注册账号</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		mounted() {
+			this.$refs.uForm.setRules(this.rules)
+		},
+		data() {
+			return {
+				form: {
+					username: '',
+					password: '',
+				},
+				rules: {
+					username: [{
+						required: true,
+						message: '请输入用户名',
+						trigger: ['change', 'blur']
+					}],
+					password: [{
+						required: true,
+						message: '请输入密码',
+						trigger: ['change', 'blur']
+					}]
+				},
+			}
+		},
+		methods: {
+			/* 
+			  注册账号
+			 */
+			signUp() {
+			   this.$emit('signUp')	
+			},
+			/**
+			 * 校验
+			 */
+			validate() {
+				return new Promise((resolve, reject) => {
+					this.$refs.uForm.validate(valid => valid && resolve(true) || reject(false))
+		
+				})
+			},
+			async submit() {
+				// 校验
+				await this.validate()
+				this.$emit('login', this.form)
+			}
+		}
+	}
+</script>
+
+<style>
+</style>

+ 79 - 0
pages/login/registerForm.vue

@@ -0,0 +1,79 @@
+<template>
+	<view>
+		<u-form :model="form" ref="uForm" label-width="auto">
+			<u-form-item prop="username">
+				<u-input v-model="form.username" placeholder="请输入用户名" />
+			</u-form-item>
+			<u-form-item prop="password">
+				<u-input v-model="form.password" type="password" placeholder="请输入密码" />
+			</u-form-item>
+			<u-form-item prop="confirmPassword">
+				<u-input v-model="form.confirmPassword" type="password" placeholder="请输入确认密码" />
+			</u-form-item>
+		</u-form>
+		<view class="u-m-t-60">
+			<u-button type="success" @click="submit">确认注册</u-button>
+		</view>
+		<view class="u-m-t-40 u-text-right u-type-success">
+			<text @click="signIn">已有账号,去登录</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				form: {
+					username: '',
+					password: '',
+					confirmPassword: ''
+				},
+				rules: {
+					username: [{
+						required: true,
+						message: '请输入用户名',
+						trigger: ['change', 'blur']
+					}],
+					password: [{
+						required: true,
+						message: '请输入密码',
+						trigger: ['change', 'blur']
+					}],
+					confirmPassword: [{
+						required: true,
+						message: '请输入确认密码',
+						trigger: ['change', 'blur']
+					}]
+				},
+			}
+		},
+		methods: {
+			/* 
+			  登录
+			 */
+			signIn() {
+			   this.$emit('signIn')	
+			},
+			/**
+			 * 校验
+			 */
+			validated() {
+				return new Promise((resolve, reject) => {
+					this.$refs.uForm.validate(valid => valid ? resolve(true) : reject(false));
+				})
+			},
+			async submit() {
+				// 校验
+				await this.validated()
+				this.$emit('register', this.form)
+			}
+		},
+		mounted() {
+			this.$refs.uForm.setRules(this.rules)
+		}
+	}
+</script>
+
+<style>
+</style>

+ 89 - 0
pages/personal/personal.vue

@@ -0,0 +1,89 @@
+<template>
+	<view class="personal page-bg-color">
+		<view class="personal-header">
+			<u-avatar :src="require('@/static/logo.png')" size="large"></u-avatar>
+			<view class="personal-header-info">
+				<text class="personal-header-info-name">{{userInfo.displayName || '-'}}</text>
+				<text class="u-tips-color">{{userInfo.displayName || '-'}}</text>
+			</view>
+			<u-icon size="40" name="edit-pen"></u-icon>
+		</view>
+		<view class="u-m-t-20">
+			<u-cell-group>
+				<u-cell-item icon="weixin-fill" title="绑定微信" @click="weiBind"></u-cell-item>
+			</u-cell-group>
+		</view>
+		<view class="u-m-t-80">
+			<u-button type="error" @click="logout">退出登录</u-button>
+		</view>
+		<u-top-tips ref="uTips"></u-top-tips>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				userInfo: JSON.parse(uni.getStorageSync('userInfo') || '{}')
+			}
+		},
+		methods: {
+			logout() {
+				const that = this
+				that.$dibootApi.post('/h5/logout').then(() => {
+					that.$tip(that.$refs.uTips, '退出成功!').then(() => {
+						uni.clearStorageSync()
+						let timer = setTimeout(() => {
+							clearTimeout(timer)
+						      uni.reLaunch({
+						      	url: '/pages/login/index'
+						      });
+						  }, 0)
+						
+					})
+				})
+			},
+			//
+			/**
+			 * 微信绑定
+			 */
+			weiBind() {
+				// 小程序登陆
+				// #ifdef MP-WEIXIN
+				this.$miniLogin.setBindWx(true).setTip(this.$refs.uToast).setUrlPath('/pages/personal/personal').go()
+				//#endif
+				// 微信公众号登陆
+				// #ifdef H5
+				this.$mpLogin.redirect(true)
+				//#endif
+			}
+		},
+	}
+</script>
+
+<style scoped lang="scss">
+	.personal {
+		padding: 24rpx 24rpx 0;
+
+		&-header {
+			display: flex;
+			background-color: #fff;
+			padding: 40rpx;
+			border-radius: 20rpx;
+			margin-bottom: 40rpx;
+
+			&-info {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+				justify-content: space-around;
+				margin-left: 40rpx;
+
+				&-name {
+					font-weight: bold;
+					font-size: 32rpx;
+				}
+			}
+		}
+	}
+</style>

+ 32 - 0
pages/workflowTask/approveCenter/approveList.js

@@ -0,0 +1,32 @@
+import list from '@/mixins/list'
+export default {
+	mixins: [list],
+	created() {
+		this.activeIndex = -100
+		this.getList(true)
+	},
+	methods: {
+		handle2ProcessTask(id, instId, category, definitionId, allowDeal) {
+			uni.navigateTo({
+				url:`/pages/workflowTask/webviewPages/dealTask?id=${id}&definitionId=${definitionId}&instId=${instId}&category=${category}&allowDeal=${allowDeal}`
+			})
+		}
+	},
+	watch: {
+	  searchKeyWord: {
+	    handler: function (val) {
+		  this.list = []
+	      this.queryParam['name'] = val
+		  this.handlePullDownRefresh()
+	    }
+	  }
+	},
+	props: {
+		approveListHeight: {
+			type: Number
+		},
+		searchKeyWord: {
+			type: String
+		}
+	}
+}

+ 43 - 0
pages/workflowTask/approveCenter/doneList.vue

@@ -0,0 +1,43 @@
+<template>
+	<scroll-view :style="{height: `calc(${approveListHeight}px - 20rpx)`}" class="di-scroll" scroll-y @scrolltolower="handleOnreachBottom" :refresher-triggered="triggered"
+		refresher-enabled @refresherrefresh="handlePullDownRefresh">
+		<view class="di-scroll-list">
+			<!-- 右滑 -->
+			<u-swipe-action
+				v-for="(item, index) in list"
+				:show="activeIndex === item[primaryKey]"
+				:key="index" 
+				:index='item.id'
+				@content-click="(id) => handle2ProcessTask(id, item.procInstId, '', item.procDefId,false)">
+				<di-descriptions :title="item.processInstanceName" label-col="3" value-col="9" :border-bottom="true" >
+					<di-descriptions-item label="节点名称" :value="item.name" :ellipsis="true"/>
+					<di-descriptions-item label="开始时间" :value="item.startTime" :ellipsis="true"/>
+					<di-descriptions-item label="办结时间" :value="item.endTime" :ellipsis="true"/>
+				</di-descriptions>
+			</u-swipe-action>
+		</view>
+		<u-loadmore v-if="!triggered" :status="status" :loadText='loadText' margin-top="24" margin-bottom="20" />
+	</scroll-view>
+</template>
+
+<script>
+	import approveList from './approveList'
+	export default {
+		mixins: [approveList],
+		data() {
+			return {
+				baseApi: '/workflow',
+				listApi: 'task/taskList',
+				customQueryParam: {
+				    taskCategory: 'done',
+					findByCurrentUserId: true,
+					orderBy: 'startTime:DESC'
+				}
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 91 - 0
pages/workflowTask/approveCenter/index.vue

@@ -0,0 +1,91 @@
+<template>
+	<view class="approve-container">
+		<view class="approve-search">
+			<u-search
+			placeholder="查询流程"
+			v-model="keyword"
+			bg-color="#fff"
+			margin="0 0 10rpx 0"
+			:input-style="{fontSize: variables.mainFontSize}"
+			:action-style="{fontSize: variables.mainFontSize}"
+			:border-color="variables.mainColor"
+			@search="() => searchKeyWord = keyword"
+			@custom="() => searchKeyWord = keyword"
+			/>
+		</view>
+		<view class="approve-list">
+			<todo-list :approve-list-height="approveListHeight" :search-key-word="searchKeyWord" v-if="type === 'todo'"/>
+			<done-list :approve-list-height="approveListHeight" :search-key-word="searchKeyWord" v-else-if="type === 'done'"/>
+			<launch-list :approve-list-height="approveListHeight" :search-key-word="searchKeyWord" v-else/>
+		</view>
+	</view>
+</template>
+
+<script>
+	import variables from '@/styles/variables.scss'
+	import doneList from './doneList'
+	import launchList from './launchList'
+	import todoList from './todoList'
+	export default {
+		components: {
+				todoList,
+				launchList,
+				doneList
+		},
+		data() {
+			return {
+				keyword: '',
+				searchKeyWord: '',
+				current: 0,
+				variables,
+				searchHeight: 0,
+				approveContainerHeight: 0
+			};
+		},
+		created() {
+			this.computedHeight()
+		},
+		computed: {
+			approveListHeight() {
+				return this.approveContainerHeight - this.searchHeight
+			}
+		},
+		props: {
+			type: {
+				type: String,
+				required: true
+			}
+		},
+		methods: {
+			handleChange(index) {
+				this.current = index
+				this.keyword = ''
+				this.searchKeyWord = ''
+			},
+			async computedHeight() {
+				await this.$nextTick()
+				const query = uni.createSelectorQuery().in(this)
+				query.select(".approve-container")
+				.boundingClientRect(data => {
+					  
+					this.approveContainerHeight = data.height
+				}).exec()
+				
+				query.select(".approve-search")
+				.boundingClientRect(data => {
+					this.searchHeight = data.height
+				}).exec()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.approve-container {
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+		background-color: #fff;
+		padding: 10rpx;
+	}
+</style>

+ 46 - 0
pages/workflowTask/approveCenter/launchList.vue

@@ -0,0 +1,46 @@
+<template>
+	<scroll-view :style="{height: `calc(${approveListHeight}px - 20rpx)`}" class="di-scroll" scroll-y @scrolltolower="handleOnreachBottom" :refresher-triggered="triggered"
+		refresher-enabled @refresherrefresh="handlePullDownRefresh">
+		<view class="di-scroll-list">
+			<!-- 右滑 -->
+			<u-swipe-action
+				v-for="(item, index) in list"
+				:show="activeIndex === item[primaryKey]"
+				:key="index" 
+				:index='item.id'
+				@content-click="(id) => handle2ProcessTask(id, item.id, item.category, item.procDefId, false)">
+				<di-descriptions :title="item.name" label-col="3" value-col="9" :border-bottom="true" >
+					<u-tag slot="right" size="mini" :text="item.statusLabel" :type="statusColorMap[item.statusLabel]" mode="light" />
+					<di-descriptions-item label="流程名称" :value="item.processDefinitionName" :ellipsis="true"/>
+					<di-descriptions-item label="开始时间" :value="item.startTime" :ellipsis="true"/>
+					<di-descriptions-item label="结束时间" :value="item.endTime" :ellipsis="true"/>
+					<di-descriptions-item label="流程耗时" :value="item.durationLabel" :ellipsis="true"/>
+				</di-descriptions>
+			</u-swipe-action>
+		</view>
+		<u-loadmore v-if="!triggered" :status="status" :loadText='loadText' margin-top="24" margin-bottom="20" />
+	</scroll-view>
+</template>
+
+<script>
+	import approveList from './approveList'
+	export default {
+		mixins: [approveList],
+		data() {
+			return {
+				baseApi: '/workflow/processLaunch',
+				listApi: 'myProcessList',
+				statusColorMap: {
+				  '已完成': 'success',
+				  '进行中': 'warning',
+				  '已撤销': 'info',
+				  '不通过': 'error'
+				}
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 71 - 0
pages/workflowTask/approveCenter/todoList.vue

@@ -0,0 +1,71 @@
+<template>
+	<scroll-view :style="{height: `calc(${approveListHeight}px - 20rpx)`}" class="di-scroll" scroll-y @scrolltolower="handleOnreachBottom" :refresher-triggered="triggered"
+		refresher-enabled @refresherrefresh="handlePullDownRefresh">
+		<view class="di-scroll-list">
+			<!-- 右滑 -->
+			<u-swipe-action
+				v-for="(item, index) in list"
+				:show="activeIndex === item[primaryKey]"
+				:key="index" 
+				:index='item.id'
+				@content-click="(id) => handleClickContext(id, item)">
+				<di-descriptions :title="item.processInstanceName" label-col="3" value-col="9" :border-bottom="true" >
+					<template slot="right" v-if="categoryMap[item.category]">
+						<u-tag size="mini" :text="categoryMap[item.category].label" :type="categoryMap[item.category].type" mode="light" />
+					</template>
+					<di-descriptions-item label="节点名称" :value="item.name" :ellipsis="true"/>
+					<di-descriptions-item label="开始时间" :value="item.createTime" :ellipsis="true"/>
+				</di-descriptions>
+			</u-swipe-action>
+		</view>
+		<u-loadmore v-if="!triggered" :status="status" :loadText='loadText' margin-top="24" margin-bottom="20" />
+	</scroll-view>
+</template>
+
+<script>
+	import approveList from './approveList'
+	export default {
+		mixins: [approveList],
+		data() {
+			return {
+				baseApi: '/workflow',
+				listApi: 'task/runTaskList',
+				customQueryParam: {
+				    category: 'todo'
+				},
+				categoryMap: {
+				  todo: {
+				    label: '待办',
+				    type: 'primary'
+				  },
+				  hold: {
+				    label: '暂存',
+				    type: 'warning'
+				  },
+				  cc: {
+				    label: '抄送',
+				    type: 'success'
+				  }
+				}
+			}
+		},
+    methods: {
+      async handleClickContext (id, item) {
+        if (!item.assignee) {
+          await this.claim(item.id)
+        }
+        this.handle2ProcessTask(id, item.procInstId, item.category, item.procDefId, true)
+      },
+      async claim (taskId) {
+        await this.$dibootApi.put('/workflow/task/taskOperate', {
+          taskId,
+          taskOperate: 'claim'
+        })
+      }
+    }
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 58 - 0
pages/workflowTask/index.vue

@@ -0,0 +1,58 @@
+<template>
+	<view class="h100vh page-bg-color">
+		<u-navbar :customBack="handleBackHome" :title="typeLabel[currentType]"></u-navbar>
+		<view class="workflow-page" :style="{height: `calc(100vh - ${diStatusBarHeight}px - 48px)`}">
+			<view class="workflow-main">
+				<start-workflow v-if="currentType === 'start'"></start-workflow>
+				<approve-center v-else :type='currentType'></approve-center>
+			</view>
+		</view>
+	</view>	
+</template>
+
+<script>
+	import startWorkflow from './startWorkflow/startWorkflow'
+	import approveCenter from './approveCenter/index'
+	export default {
+		data() {
+			return {
+				currentType: 'start',
+				typeLabel: {
+					start: '发起申请',
+					myLaunch: '我发起的',
+					todo: '我的待办',
+					done: '我的已办',
+				},
+				diStatusBarHeight: 0
+			};
+		},
+		onLoad(options) {
+			this.diStatusBarHeight = uni.getSystemInfoSync().statusBarHeight
+			this.currentType = options.type
+		},
+		components: {
+			startWorkflow,
+			approveCenter
+		},
+		methods: {
+			handleBackHome() {
+				console.log('000000')
+				uni.switchTab({
+					url: '/pages/home/home'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.h100vh {
+		height: 100vh;
+	}
+	.workflow-page {
+		.workflow-main {
+			height: 100%;
+			overflow: hidden;
+		}
+	}
+</style>

+ 92 - 0
pages/workflowTask/startWorkflow/processDefinitionList.vue

@@ -0,0 +1,92 @@
+<template>
+	<scroll-view :style="{height: `calc(${processDefinitionHeight}px - 20rpx)`}" class="di-scroll" scroll-y @scrolltolower="handleOnreachBottom" :refresher-triggered="triggered"
+		refresher-enabled @refresherrefresh="handlePullDownRefresh">
+		<view class="di-scroll-list">
+			<!-- 右滑 -->
+			<u-swipe-action
+				v-for="(item, index) in list"
+				:show="activeIndex === item[primaryKey]"
+				:key="index" 
+				:index='item.id'
+				:options="actionOptions"
+				@content-click="(id) => handle2StartFlow(item)"
+				@click="handleActionClick"
+				@open="handleActiveSwipeAction">
+				<di-descriptions value-col="12" :border-bottom="true" >
+					<di-descriptions-item :value="item.name" :ellipsis="true"/>
+				</di-descriptions>
+			</u-swipe-action>
+		</view>
+		<u-loadmore v-if="!triggered" :status="status" :loadText='loadText' margin-top="24" margin-bottom="20" />
+	</scroll-view>
+</template>
+
+<script>
+	import list from '@/mixins/list'
+	export default {
+		mixins: [list],
+		data() {
+			return {
+				baseApi: '/mobile/workflow/processDefinition',
+				listApi: 'getAuthorizedProcDefList',
+				actionOptions: [{
+					text: '流程图',
+					type: 'handleViewDiagram',
+					style: {
+						backgroundColor: this.$color.warning
+					}
+				}]
+			};
+		},
+		created() {
+			this.activeIndex = -100
+			this.getList()
+		},
+		methods: {
+			handleViewDiagram(id) {
+				uni.navigateTo({
+					url: `/pages/workflowTask/webviewPages/flowDiagram?id=${id}`
+				})
+			},
+			handle2StartFlow(item) {
+				uni.navigateTo({
+					url: `/pages/workflowTask/webviewPages/startFlow?id=${item.id}&processInstanceId=${item.processInstanceId}&defaultTitle=${item.defaultTitle}&name=${item.name}`
+				})
+			}
+		},
+		watch: {
+		  category: {
+		    handler: function (val) {
+			  this.list = []
+			  delete this.queryParam['name']
+		      this.queryParam['category'] = val
+			  this.handlePullDownRefresh()
+		    }
+		  },
+		  searchKeyWord: {
+		    handler: function (val) {
+			  this.list = []
+		      this.queryParam['name'] = val
+			  this.handlePullDownRefresh()
+		    }
+		  }
+		},
+		props: {
+		  category: {
+		    type: String,
+		    default: ''
+		  },
+		  processDefinitionHeight: {
+			  type: Number,
+			  default: 0
+		  },
+		  searchKeyWord: {
+			  type: String,
+			  default: ''
+		  }
+		}
+	}
+</script>
+
+<style lang="scss">
+</style>

+ 166 - 0
pages/workflowTask/startWorkflow/startWorkflow.vue

@@ -0,0 +1,166 @@
+<template>
+	<view class="start-workflow">
+		<view class="start-workflow-search">
+			<u-search
+			placeholder="查询流程"
+			v-model="keyword"
+			bg-color="#fff"
+			margin="0 0 10rpx 0"
+			:input-style="{fontSize: variables.mainFontSize}"
+			:action-style="{fontSize: variables.mainFontSize}"
+			:border-color="variables.mainColor"
+			@search="() => searchKeyWord = keyword"
+			@custom="() => searchKeyWord = keyword"
+			/>
+			<u-tabs
+			font-size="24"
+			:active-color="variables.mainColor"
+			inactive-color="#606266"
+			:current="current"
+			:list="tabList"
+			@change="handleChange"/>
+			<!-- 移动端只分两级, -->
+			<u-tabs
+			font-size="24"
+			:active-color="variables.mainColor"
+			inactive-color="#606266"
+			:current="childCurrent"
+			:list="tabChildrenList"
+			@change="handleChildChange"/>
+		</view>
+		<view class="process-definition-list" >
+			<process-definition-list :search-key-word="searchKeyWord" :category="category" :process-definition-height='processDefinitionHeight'/>
+		</view>
+	</view>
+</template>
+
+<script>
+	import processDefinitionList from './processDefinitionList.vue'
+	import variables from '@/styles/variables.scss'
+	export default {
+		components: {
+			processDefinitionList
+		},
+		computed: {
+			processDefinitionHeight() {
+				return this.startWorkflowHeight - this.searchHeight
+			}
+		},
+		data() {
+			return {
+				category: '',
+				parentCategoryInfo: {},
+				current:0,
+				childrenCurrent: '',
+				keyword: '',
+				categoryList: [],
+				tabList: [],
+				childCurrent: 0,
+				tabChildrenList: [],
+				childCartegoryList: [],
+				startWorkflowHeight: 0,
+				searchHeight: 0,
+				variables,
+				searchKeyWord: ''
+			};
+		},
+		created() {
+			this.loadCategory()
+		},
+		methods: {
+			async loadCategory() {
+				try{
+					uni.showLoading({
+						title: '加载中'
+					})
+					const res = await this.$dibootApi.get('/mobile/workflow/processDefinition/getAuthorizedCategoryTree')
+					if(res.code === 0 && res.data && res.data.length > 0) {
+						this.categoryList = res.data
+						this.tabList = [{
+							name: '全部'
+						}, ...this.categoryList.map(item => {
+							return {
+								name: item.name
+							}
+						})]
+						this.handleChange(0)
+					}
+				} catch(e){
+					//TODO handle the exception
+				} finally {
+					uni.hideLoading()
+				}
+				
+			},
+			/**
+			 * 切换一级分类
+			 * @param {Object} index
+			 */
+			handleChange(index) {
+				this.keyword = ''
+				this.searchKeyWord = ''
+				this.current = index
+				this.tabChildrenList = []
+				this.childCartegoryList = []
+				this.childrenCurrent = 0
+				this.resetHeight()
+				if(this.tabList[index].name === '全部') {
+					this.parentCategoryInfo = {}
+					this.category = ''
+					return
+				}
+				this.parentCategoryInfo = this.categoryList[index - 1]
+				this.category = this.parentCategoryInfo.id
+				this.childCartegoryList = this.parentCategoryInfo.children || []
+				if(this.childCartegoryList && this.childCartegoryList.length > 0) {
+					this.tabChildrenList = [{
+						name: '全部' 
+					}, ...this.childCartegoryList.map(item => {
+						return {
+							name: item.name
+						}
+					})]
+				}
+			},
+			/**
+			 * 切换二级分类
+			 * @param {Object} index
+			 */
+			handleChildChange(index) {
+				this.keyword = ''
+				this.searchKeyWord = ''
+				this.childCurrent = index
+				if(this.tabChildrenList[index].name === '全部') {
+					this.category = this.parentCategoryInfo.id
+					return
+				}
+				const categoryInfo =  this.childCartegoryList[index - 1]
+				this.category = categoryInfo.id
+				this.resetHeight()
+			},
+			async resetHeight() {
+				await this.$nextTick()
+				const query = uni.createSelectorQuery().in(this)
+				query.select(".start-workflow")
+				.boundingClientRect(data => {
+					this.startWorkflowHeight = data && data.height || 0
+				}).exec()
+				
+				query.select(".start-workflow-search")
+				.boundingClientRect(data => {
+					this.searchHeight = data && data.height || 0
+				}).exec()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.start-workflow {
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+		background-color: #fff;
+		padding: 10rpx;
+	}
+</style>

+ 30 - 0
pages/workflowTask/webviewPages/dealTask.vue

@@ -0,0 +1,30 @@
+<template>
+	<web-view :src="`${webviewServer}/mobile/dealTask?definitionId=${definitionId}&category=${category}&taskId=${taskId}&withDealTask=${withDealTask}&processInstanceId=${processInstanceId}&fromMobile=true&authtoken=${authtoken}`"></web-view>
+</template>
+
+<script>
+	import webview from './webview'
+	export default {
+		mixins: [webview],
+		data() {
+			return {
+				definitionId: '',
+				processInstanceId: '',
+				taskId: '',
+				category: '',
+				withDealTask: '',
+			};
+		},
+		onLoad(options) {
+			this.processInstanceId = options.instId
+			this.definitionId = options.definitionId
+			this.taskId = options.id
+			this.category = options.category
+			this.withDealTask = options.allowDeal
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 22 - 0
pages/workflowTask/webviewPages/flowDiagram.vue

@@ -0,0 +1,22 @@
+<template>
+	<web-view :src="`${webviewServer}/mobile/flowDiagram?id=${id}&fromMobile=true`"></web-view>
+</template>
+
+<script>
+	import webview from './webview'
+	export default {
+		mixins: [webview],
+		data() {
+			return {
+				id: ''
+			}
+		},
+		onLoad(options) {
+			this.id = options.id
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 64 - 0
pages/workflowTask/webviewPages/startFlow.vue

@@ -0,0 +1,64 @@
+<template>
+	<web-view
+		:src="`${webviewServer}/mobile/startFlow?id=${id}&name=${name}&processInstanceId=${processInstanceId}&defaultTitle=${defaultTitle}&authtoken=${authtoken}&longitude=${longitude}&latitude=${latitude}&scanResult=${scanResult}&fromMobile=true`">
+	</web-view>
+</template>
+
+<script>
+	import webview from './webview'
+	export default {
+		mixins: [webview],
+		data() {
+			return {
+				defaultTitle: '',
+				processInstanceId: '',
+				id: '',
+				name: '',
+				longitude: '',
+				latitude: '',
+				scanResult: ''
+			};
+		},
+		onLoad(options) {
+			this.processInstanceId = options.processInstanceId
+			this.defaultTitle = options.defaultTitle
+			this.id = options.id
+			this.name = options.name
+			this.getLocation()
+			if (options.scanCode) this.scanCode()
+		},
+		methods: {
+			getLocation() {
+				let that = this
+				uni.getLocation({
+					type: 'gcj02',
+					success: function(res) {
+						that.longitude = res.longitude
+						that.latitude = res.latitude
+					},
+					fail: function(err) {
+						console.log('获取位置信息失败', err)
+						// uni.showToast({
+						// 	title: '获取位置信息失败',
+						// 	icon: 'none',
+						// 	duration: 3000
+						// });
+					}
+				});
+			},
+			scanCode() {
+				let that = this
+				uni.scanCode({
+					onlyFromCamera: true,
+					success: function(res) {
+						that.scanResult = res.result
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 9 - 0
pages/workflowTask/webviewPages/webview.js

@@ -0,0 +1,9 @@
+export default {
+	data() {
+		return {
+			webviewServer: 'http://192.168.31.119:9529/workflow/',
+			// webviewServer: 'http://localhost:9529',
+			authtoken: uni.getStorageSync('authtoken')
+		};
+	}
+}

BIN
static/images/component.png


BIN
static/images/component_selected.png


BIN
static/images/detail.png


BIN
static/images/diboot/diboot-cloud.jpeg


BIN
static/images/diboot/diboot-lowcode.jpeg


BIN
static/images/diboot/diboot-workflow.png


BIN
static/images/diboot/diboot-workflow2.png


BIN
static/images/form.png


BIN
static/images/home.png


BIN
static/images/home_selected.png


BIN
static/images/list.png


BIN
static/images/menu/buy.png


BIN
static/images/menu/email.png


BIN
static/images/menu/expense.png


BIN
static/images/menu/leave.png


BIN
static/images/menu/signIn.png


BIN
static/images/menu/startflow.png


BIN
static/images/menu/todo.png


BIN
static/images/menu/train.png


BIN
static/images/menu/travel.png


BIN
static/images/personal.png


BIN
static/images/personal_selected.png


BIN
static/images/workflow/done.png


BIN
static/images/workflow/myLanuch.png


BIN
static/images/workflow/start.png


BIN
static/images/workflow/todo.png


BIN
static/logo.png


+ 8 - 0
styles/variables.scss

@@ -0,0 +1,8 @@
+$main-color: #19be6b;
+$main-font-size: 24rpx;
+$title-font-size: 28rpx;
+:export {
+	mainColor: $main-color;
+	mainFontSize: $main-font-size;
+	titleFontSize: $title-font-size
+}

+ 77 - 0
uni.scss

@@ -0,0 +1,77 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+@import "uview-ui/theme.scss";
+@import "styles/variables.scss";
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:24rpx;
+$uni-font-size-base:28rpx;
+$uni-font-size-lg:32rpx;
+
+/* 图片尺寸 */
+$uni-img-size-sm:40rpx;
+$uni-img-size-base:52rpx;
+$uni-img-size-lg:80rpx;
+
+/* Border Radius */
+$uni-border-radius-sm: 4rpx;
+$uni-border-radius-base: 6rpx;
+$uni-border-radius-lg: 12rpx;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 10px;
+$uni-spacing-row-base: 20rpx;
+$uni-spacing-row-lg: 30rpx;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 8rpx;
+$uni-spacing-col-base: 16rpx;
+$uni-spacing-col-lg: 24rpx;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:40rpx;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:36rpx;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:30rpx;

+ 0 - 0
unpackage/dist/build/.automator/h5/.automator.json


BIN
unpackage/dist/build/h5.zip


+ 2 - 0
unpackage/dist/build/h5/index.html

@@ -0,0 +1,2 @@
+<!DOCTYPE html><html lang=zh-CN><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><title>diboot-mobile-ui</title><script>var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
+            document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')</script><link rel=stylesheet href=/workflow-h5/static/index.2da1efab.css></head><body><noscript><strong>Please enable JavaScript to continue.</strong></noscript><div id=app></div><script src=/workflow-h5/static/js/chunk-vendors.990d7a44.js></script><script src=/workflow-h5/static/js/index.dedc9069.js></script></body></html>

BIN
unpackage/dist/build/h5/static/images/component.png


BIN
unpackage/dist/build/h5/static/images/component_selected.png


BIN
unpackage/dist/build/h5/static/images/detail.png


BIN
unpackage/dist/build/h5/static/images/diboot/diboot-cloud.jpeg


BIN
unpackage/dist/build/h5/static/images/diboot/diboot-lowcode.jpeg


BIN
unpackage/dist/build/h5/static/images/diboot/diboot-workflow.png


BIN
unpackage/dist/build/h5/static/images/diboot/diboot-workflow2.png


BIN
unpackage/dist/build/h5/static/images/form.png


BIN
unpackage/dist/build/h5/static/images/home.png


BIN
unpackage/dist/build/h5/static/images/home_selected.png


BIN
unpackage/dist/build/h5/static/images/list.png


BIN
unpackage/dist/build/h5/static/images/menu/buy.png


BIN
unpackage/dist/build/h5/static/images/menu/email.png


BIN
unpackage/dist/build/h5/static/images/menu/expense.png


BIN
unpackage/dist/build/h5/static/images/menu/leave.png


BIN
unpackage/dist/build/h5/static/images/menu/signIn.png


BIN
unpackage/dist/build/h5/static/images/menu/startflow.png


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