Эх сурвалжийг харах

Signed-off-by: ljx <809268652@qq.com>高校分流系统小程序代码

ljx 9 сар өмнө
commit
2db0484d25
100 өөрчлөгдсөн 7653 нэмэгдсэн , 0 устгасан
  1. 19 0
      .gitignore
  2. 23 0
      App.vue
  3. 193 0
      common/ajax.js
  4. 96 0
      common/formChecker.js
  5. 10 0
      common/index.js
  6. 41 0
      common/mixin.js
  7. 442 0
      components/sin-signature/sin-signature.vue
  8. 55 0
      components/u-class/u-class.vue
  9. 112 0
      components/u-des-row/u-des-row.vue
  10. 147 0
      components/u-drawer-input/u-drawer-input.vue
  11. 115 0
      components/u-drawer/u-drawer.vue
  12. 154 0
      components/u-input/u-input.vue
  13. 86 0
      components/u-nav-bar/u-nav-bar.vue
  14. 57 0
      components/u-panel/u-panel.vue
  15. 65 0
      components/u-picker/u-picker.vue
  16. 249 0
      components/u-sign/u-sign.vue
  17. 20 0
      index.html
  18. 38 0
      main.js
  19. 78 0
      manifest.json
  20. 462 0
      package-lock.json
  21. 19 0
      package.json
  22. 52 0
      pages.json
  23. 299 0
      pages/apps/change-major/change-major.vue
  24. 151 0
      pages/apps/my-grades/my-grades.vue
  25. 222 0
      pages/apps/progress-query/progress-query.vue
  26. 201 0
      pages/apps/shunt-places/shunt-places.vue
  27. 373 0
      pages/apps/special-application/special-application.vue
  28. 15 0
      pages/apps/test/test.vue
  29. 39 0
      pages/common/webview/webview.vue
  30. 6 0
      pages/error/403/403.vue
  31. 92 0
      pages/index/components/apps.vue
  32. 182 0
      pages/index/components/schedule.vue
  33. 175 0
      pages/index/components/top-panel.vue
  34. 34 0
      pages/index/index.vue
  35. 115 0
      pages/my/my.vue
  36. 202 0
      pages/my/problem-feedback/problem-feedback.vue
  37. 179 0
      pages/my/user-info/user-info.vue
  38. BIN
      static/403.png
  39. BIN
      static/achievement_label.png
  40. BIN
      static/bg_fail.png
  41. BIN
      static/bg_icon.png
  42. BIN
      static/bg_plan.png
  43. BIN
      static/choose_icon.png
  44. BIN
      static/edit.png
  45. BIN
      static/home_label.png
  46. BIN
      static/home_label_red.png
  47. BIN
      static/index/card-title-icon.png
  48. BIN
      static/index/diversion_icon.png
  49. BIN
      static/index/diversion_quota_icon.png
  50. BIN
      static/index/end_state_choose.png
  51. BIN
      static/index/into_icon.png
  52. BIN
      static/index/my_grades_icon.png
  53. BIN
      static/index/not_state_choose.png
  54. BIN
      static/index/progress_query_icon.png
  55. BIN
      static/index/schedule_list_bg.png
  56. BIN
      static/index/special_icon.png
  57. BIN
      static/index/start_state_choose.png
  58. BIN
      static/logo.png
  59. BIN
      static/menber_info_icon.png
  60. BIN
      static/my_grades_bg.png
  61. BIN
      static/no_choose_icon.png
  62. BIN
      static/no_list.png
  63. BIN
      static/no_open.png
  64. BIN
      static/not_state_choose.png
  65. BIN
      static/num_icon.png
  66. BIN
      static/open_icon.png
  67. BIN
      static/schedule_list_bg.png
  68. BIN
      static/shenhe-中.png
  69. BIN
      static/shenhe-失败.png
  70. BIN
      static/shenhe-成功.png
  71. BIN
      static/sign_icon.png
  72. BIN
      static/success_bg.png
  73. BIN
      static/tabbar/grid.png
  74. BIN
      static/tabbar/grid_active.png
  75. BIN
      static/tabbar/im-contacts.png
  76. BIN
      static/tabbar/im-contacts_active.png
  77. BIN
      static/tabbar/list.png
  78. BIN
      static/tabbar/list_active.png
  79. BIN
      static/tabbar/me.png
  80. BIN
      static/tabbar/me_active.png
  81. BIN
      static/time_icon.png
  82. BIN
      static/top_panel_bg.png
  83. BIN
      static/upload_icon.png
  84. BIN
      static/xpc.png
  85. 24 0
      store/index.js
  86. 96 0
      store/modules/user.js
  87. 186 0
      uni.scss
  88. 29 0
      uni_modules/uni-badge/changelog.md
  89. 268 0
      uni_modules/uni-badge/components/uni-badge/uni-badge.vue
  90. 10 0
      uni_modules/uni-badge/readme.md
  91. 33 0
      uni_modules/uni-easyinput/changelog.md
  92. 56 0
      uni_modules/uni-easyinput/components/uni-easyinput/common.js
  93. 461 0
      uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue
  94. 11 0
      uni_modules/uni-easyinput/readme.md
  95. 61 0
      uni_modules/uni-file-picker/changelog.md
  96. 224 0
      uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js
  97. 650 0
      uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue
  98. 325 0
      uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue
  99. 292 0
      uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue
  100. 109 0
      uni_modules/uni-file-picker/components/uni-file-picker/utils.js

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+# Build and Release Folders
+bin-debug/
+bin-release/
+[Oo]bj/
+[Bb]in/
+
+# Other files and folders
+.settings/
+
+# Executables
+*.swf
+*.air
+*.ipa
+*.apk
+
+# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
+# should NOT be excluded as they contain compiler settings and other important
+# information for Eclipse / Flash Builder.
+node_modules

+ 23 - 0
App.vue

@@ -0,0 +1,23 @@
+<script>
+	export default {
+		onLaunch: function() {
+			// console.log('App Launch')
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	/*每个页面公共css */
+	body {
+
+		max-width: 677px;
+		margin: 0 auto;
+
+	}
+</style>

+ 193 - 0
common/ajax.js

@@ -0,0 +1,193 @@
+// ajax.js
+
+// 引入 uni-ajax 模块
+import ajax from 'uni-ajax'
+import store from '@/store'
+
+// 错误log上传
+function errorApiLogUpload(response) {
+	const code = response?.data?.code || ''
+	const isLog = response?.config?.header?.isLog // 上报问题接口
+	if (!isLog && code && code !== 200) {
+		const url = response?.config?.url
+		console.log('errorApiLogUpload', url, code, response)
+		const content = '【前端接口日志】:' + JSON.stringify(response)
+		instance.post('/shunt/proposal-apply', {
+			content
+		}, {
+			header: {
+				// 避免死循环
+				isLog: 1,
+				// 上传log 专属token
+				token: 'eyJhbGciOiJIUzUxMiJ9.eyJ5ZWFySWQiOiIyMDIyIiwiY29sbGVnZUlkIjotMSwic3R1ZGVudE5vIjoiLTEiLCJpc3N1ZXIiOiJzaHVudCBhdXRoIEpXVCBJc3N1ZXIgMS4wIiwiZXhwIjozNzY1NDEzMDU3Mn0.9OkmMbwt7tFRdgOopNVPYCq0VNXE-f-Y-yv57kfRZQDjDy_THGvsu_UQyOyGkgV9d8WoPklagBB5K2Bbj5OFdA'
+			}
+		})
+	}
+}
+// 创建请求实例
+const instance = ajax.create({
+	// 初始配置
+	baseURL: '',
+	timeout: 6000,
+	// responseType: 'json',
+	// withCredentials: true
+})
+/* 无感刷新 Token */
+let isRefreshing = false // 当前是否在请求刷新 Token
+let requestQueue = [] // 将在请求刷新 Token 中的请求暂存起来,等刷新 Token 后再重新请求
+
+// 执行暂存起来的请求
+const executeQueue = error => {
+	requestQueue.forEach(promise => {
+		if (error) {
+			promise.reject(error)
+		} else {
+			promise.resolve()
+		}
+	})
+
+	requestQueue = []
+}
+
+// 刷新 Token 请求
+const refreshToken = () => instance.post('/shunt/login', {
+	code: uni.getStorageSync('code')
+})
+// 刷新 Token 请求处理,参数为刷新成功后的回调函数
+const refreshTokenHandler = afresh => {
+	// 如果当前是在请求刷新 Token 中,则将期间的请求暂存起来
+	if (isRefreshing) {
+		return new Promise((resolve, reject) => {
+			requestQueue.push({
+				resolve,
+				reject
+			})
+		}).then(afresh)
+	}
+
+	isRefreshing = true
+
+	return new Promise((resolve, reject) => {
+		uni.showLoading({
+			title: '登录中',
+			mask: true
+		})
+		let token = ''
+		store.commit('user/setToken', token)
+		refreshToken()
+			.then(res => {
+				errorApiLogUpload(res)
+				token = res?.data?.token || ''
+				// if (token) {
+				// 	uni.setStorageSync('TOKEN', token)
+				// 	resolve(afresh?.())
+				// 	executeQueue(null)
+				// } else {
+				// 	return Promise.reject(res)
+				// }
+			})
+			.catch(err => {
+				// uni.removeStorageSync('TOKEN')
+				// reject(err)
+				// executeQueue(err)
+				console.error('login', error)
+			})
+			.finally(() => {
+				isRefreshing = false
+				uni.hideLoading()
+
+				store.commit('user/setToken', token)
+				if (token) {
+					store.dispatch('user/updateUserInfo')
+					// 重新获取接口的话 旧的接口会保留问题token,导致登录死循环
+					// setTimeout(() => {
+					// 	resolve(afresh?.())
+					// 	executeQueue(null)
+					// }, 5000)
+					uni.reLaunch({
+						url: '/pages/index/index'
+					});
+				} else {
+					store.commit('user/logout', '')
+					uni.reLaunch({
+						url: '/pages/error/403/403'
+					});
+					reject(err)
+					executeQueue(err)
+				}
+
+			})
+	})
+}
+
+// 添加请求拦截器
+instance.interceptors.request.use(
+	config => {
+		// 给每条请求赋值 Token 请求头
+		config.header['token'] = config.header['token'] || store.state.user.token || uni.getStorageSync(
+			'TOKEN') // token令牌
+
+		const requestUid = new Date().getTime() // 请求requestUid,时间戳
+		config.data = {
+			data: Object.assign({}, config?.data || {}),
+			requestUid
+		}
+		// 在发送请求前做些什么
+		return config
+	},
+	error => {
+		// 对请求错误做些什么
+		return Promise.reject(error)
+	}
+)
+
+// 添加响应拦截器
+instance.interceptors.response.use(
+	response => {
+		if (response?.config?.header?.isLog) { // 上报问题接口 无需处理
+			return
+		}
+		errorApiLogUpload(response)
+		const {
+			code,
+			data,
+			msg
+		} = response.data || {}
+
+		if (code === 200) {
+			return response.data
+		} else if (code === 201 || code === 202 || code === 203) { // 刷新 Token
+			store.commit('user/logout', '')
+			uni.reLaunch({
+				url: '/pages/error/403/403'
+			});
+			return Promise.reject(response.data)
+			// return refreshTokenHandler(() => instance(response.config))
+		} else {
+			msg && uni.showToast({
+				title: msg,
+				position: 'top',
+				icon: 'none',
+				duration: 2500
+			});
+			return Promise.reject(response.data)
+		}
+	},
+	error => {
+		// 对响应错误做些什么
+		return Promise.reject(error)
+	}
+)
+
+// 导出 create 创建后的实例
+export default instance
+
+// 200=成功时状态码
+// 201=token超时
+// 202=token为空
+// 203=token非法
+// 402=业务数据配置缺失时错误码
+// 403=超出服务范围时,禁止提供服务(熔断,高并发限流)时,错误码
+// 500=未定义的系统异常(Exception)时,错误码
+// 401=业务未授权时进行访问,401(权限校验)
+// 402=方法参数错误

+ 96 - 0
common/formChecker.js

@@ -0,0 +1,96 @@
+/**
+数据验证(表单验证)
+作者 ocl
+*/
+export default {
+	error:'',
+	check : function (data, rule){
+		for(var i = 0; i < rule.length; i++){
+			if (!rule[i].checkType){return true;}
+			if (!rule[i].name) {return true;}
+			if (!rule[i].errorMsg) {return true;}
+			if (!data[rule[i].name]) {this.error = rule[i].errorMsg; return false;}
+			switch (rule[i].checkType){
+				case 'string':
+					var reg = new RegExp('^.{' + rule[i].checkRule + '}$');
+					if(!reg.test(data[rule[i].name])) {this.error = rule[i].errorMsg; return false;}
+				break;
+				case 'int':
+					var reg = new RegExp('^(-[1-9]|[1-9])[0-9]{' + rule[i].checkRule + '}$');
+					if(!reg.test(data[rule[i].name])) {this.error = rule[i].errorMsg; return false;}
+					break;
+				break;
+				case 'between':
+					if (!this.isNumber(data[rule[i].name])){
+						this.error = rule[i].errorMsg;
+						return false;
+					}
+					var minMax = rule[i].checkRule.split(',');
+					minMax[0] = Number(minMax[0]);
+					minMax[1] = Number(minMax[1]);
+					if (data[rule[i].name] > minMax[1] || data[rule[i].name] < minMax[0]) {
+						this.error = rule[i].errorMsg;
+						return false;
+					}
+				break;
+				case 'betweenD':
+					var reg = /^-?[1-9][0-9]?$/;
+					if (!reg.test(data[rule[i].name])) { this.error = rule[i].errorMsg; return false; }
+					var minMax = rule[i].checkRule.split(',');
+					minMax[0] = Number(minMax[0]);
+					minMax[1] = Number(minMax[1]);
+					if (data[rule[i].name] > minMax[1] || data[rule[i].name] < minMax[0]) {
+						this.error = rule[i].errorMsg;
+						return false;
+					}
+				break;
+				case 'betweenF': 
+					var reg = /^-?[0-9][0-9]?.+[0-9]+$/;
+					if (!reg.test(data[rule[i].name])){this.error = rule[i].errorMsg; return false;}
+					var minMax = rule[i].checkRule.split(',');
+					minMax[0] = Number(minMax[0]);
+					minMax[1] = Number(minMax[1]);
+					if (data[rule[i].name] > minMax[1] || data[rule[i].name] < minMax[0]) {
+						this.error = rule[i].errorMsg;
+						return false;
+					}
+				break;
+				case 'same':
+					if (data[rule[i].name] != rule[i].checkRule) { this.error = rule[i].errorMsg; return false;}
+				break;
+				case 'notsame':
+					if (data[rule[i].name] == rule[i].checkRule) { this.error = rule[i].errorMsg; return false; }
+				break;
+				case 'email':
+					var reg = /^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
+					if (!reg.test(data[rule[i].name])) { this.error = rule[i].errorMsg; return false; }
+				break;
+				case 'phoneno':
+					var reg = /^1[0-9]{10,10}$/;
+					if (!reg.test(data[rule[i].name])) { this.error = rule[i].errorMsg; return false; }
+				break;
+				case 'zipcode':
+					var reg = /^[0-9]{6}$/;
+					if (!reg.test(data[rule[i].name])) { this.error = rule[i].errorMsg; return false; }
+				break;
+				case 'reg':
+					var reg = new RegExp(rule[i].checkRule);
+					if (!reg.test(data[rule[i].name])) { this.error = rule[i].errorMsg; return false; }
+				break;
+				case 'in':
+					if(rule[i].checkRule.indexOf(data[rule[i].name]) == -1){
+						this.error = rule[i].errorMsg; return false;
+					}
+				break;
+				case 'notnull':
+					if(data[rule[i].name] == null || data[rule[i].name].length < 1){this.error = rule[i].errorMsg; return false;}
+				break;
+			}
+		}
+		return true;
+	},
+	isNumber : function (checkVal){
+		var reg = /^-?[1-9][0-9]?.?[0-9]*$/;
+		return reg.test(checkVal);
+	}
+}

+ 10 - 0
common/index.js

@@ -0,0 +1,10 @@
+// 关键信息隐藏
+export function hideCode(str, frontLen, endLen) {
+	str = str || ''
+	var len = str.length - frontLen - endLen;
+	var xing = '';
+	for (var i = 0; i < len; i++) {
+		xing += '*';
+	}
+	return str.substring(0, frontLen) + xing + str.substring(str.length - endLen);
+}

+ 41 - 0
common/mixin.js

@@ -0,0 +1,41 @@
+import {
+	mapState
+} from 'vuex'
+
+export default {
+	computed: {
+		...mapState({
+			userInfo: state => state.user.userInfo,
+		})
+	},
+	async onReady() {
+		const whiteList = ['pages/error/403/403'] // 路由白名单
+		const pages = getCurrentPages() // 获取栈实例
+		const page = pages[0] || {}
+
+		// set code
+		const code = page?.options?.code || ''
+		if (code) {
+			console.log('code: ', code)
+			this.$store.commit('user/setCode', code)
+			// code 重新登录
+			await this.$store.dispatch('user/login', {
+				test: 111
+			})
+		}
+		// 白名单 无需登录
+		if (whiteList.indexOf(page.route) >= 0) {
+			return
+		}
+		setTimeout(function() {
+			const token = uni.getStorageSync('TOKEN')
+			// 获取用户信息
+			token && !this.userInfo.studentName && this.$store.dispatch('user/updateUserInfo')
+				// 获取token
+				!token && this.$store.dispatch('user/login', {
+					test: 222
+				})
+		}, 500);
+
+	}
+}

+ 442 - 0
components/sin-signature/sin-signature.vue

@@ -0,0 +1,442 @@
+<template>
+	<view class="signature-wrap">
+		<view class="img-wrap" @tap="showSignature()" @touchstart="touchSignature()">
+			<image :src="absPrevView" mode="scaleToFill"></image>
+		</view>
+		<view v-if="!disabled" v-show="show" class="signature-contain">
+			<view class="signature-main" style="z-index: 3000;">
+				<view class="signature-title"><text v-for="t in titles">{{t}}</text></view>
+				<canvas disable-scroll="true" class="signature" :class="cid" canvas-id="cvs" @touchstart="touchstart"
+					@touchmove="touchmove" @touchend="touchend"></canvas>
+				<view class="signature-btns">
+					<view class="btn btn-cancel cu-btn bg-main margin-tb-sm text-white" @tap="cancelSignature()">
+						<text>取</text><text>消</text>
+					</view>
+					<view class="btn btn-clear cu-btn bg-main margin-tb-sm text-white" @tap="clearSignature();">
+						<text>清</text><text>空</text>
+					</view>
+					<view class="btn btn-ok cu-btn bg-main margin-tb-sm text-white" @tap="onOK()">
+						<text>确</text><text>定</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	let _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+	var _utf8_encode = function(string) {
+		string = string.replace(/\r\n/g, "\n");
+		var utftext = "";
+		for (var n = 0; n < string.length; n++) {
+			var c = string.charCodeAt(n);
+			if (c < 128) {
+				utftext += String.fromCharCode(c);
+			} else if ((c > 127) && (c < 2048)) {
+				utftext += String.fromCharCode((c >> 6) | 192);
+				utftext += String.fromCharCode((c & 63) | 128);
+			} else {
+				utftext += String.fromCharCode((c >> 12) | 224);
+				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+				utftext += String.fromCharCode((c & 63) | 128);
+			}
+
+		}
+		return utftext;
+	}
+
+	let base64encode = function(input) {
+		var output = "";
+		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
+		var i = 0;
+		input = _utf8_encode(input);
+		while (i < input.length) {
+			chr1 = input.charCodeAt(i++);
+			chr2 = input.charCodeAt(i++);
+			chr3 = input.charCodeAt(i++);
+			enc1 = chr1 >> 2;
+			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+			enc4 = chr3 & 63;
+			if (isNaN(chr2)) {
+				enc3 = enc4 = 64;
+			} else if (isNaN(chr3)) {
+				enc4 = 64;
+			}
+			output = output +
+				_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
+				_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
+		}
+		return output;
+	}
+	export default {
+		cxt: null,
+		data() {
+			return {
+				VERSION: '1.0.0',
+				cid: 'cvs',
+				show: false,
+				ctrl: null,
+				listeners: [],
+				prevView: '',
+
+				draws: [],
+				lines: [],
+				line: null,
+			};
+		},
+		props: {
+			value: {
+				default: '',
+			},
+			title: {
+				type: String,
+				default: '请签字',
+			},
+			disabled: {
+				type: Boolean,
+				default: false,
+			}
+		},
+		watch: {
+			value() {
+				this.prevView = this.value;
+			}
+		},
+		computed: {
+			titles() {
+				return this.title.split('')
+			},
+			absPrevView() {
+				var pv = this.prevView;
+				// if(pv){
+				// 	pv = this.$wrapUrl(pv)
+				// }
+				return pv;
+			}
+		},
+		mounted() {
+			this.prevView = this.value;
+			console.log('dx')
+		},
+		methods: {
+			onOK() {
+				let data = this.ctrl.getValue();
+				this.$emit('input', data);
+				this.prevView = data;
+				this.hideSignature();
+				let f = this.listeners.shift();
+				if (f) {
+					f(data);
+				}
+			},
+			touchSignature() {
+				let sig = this.prevView
+				if (!sig || !sig.length) {
+					this.showSignature()
+				}
+			},
+			showSignature() {
+				if (this.disabled)
+					return;
+				if (!this.ctrl) {
+					this.initCtrl();
+				} else if (!this.show) {
+					this.clearSignature();
+					this.show = true;
+				}
+			},
+			async getSyncSignature() {
+				this.showSignature();
+				return await new Promise(async (resolve, reject) => {
+					this.listeners.push((res) => {
+						resolve(res);
+					});
+				});
+			},
+			cancelSignature() {
+				this.listeners.map((f) => {
+					f(null);
+				})
+				this.hideSignature();
+			},
+			hideSignature() {
+				this.ctrl && this.ctrl.clear();
+				this.show = false;
+			},
+			clearSignature() {
+				this.ctrl && this.ctrl.clear();
+			},
+			async initCtrl() {
+				this.show = true;
+				let cxt = uni.createCanvasContext(this.cid, this);
+				this.cxt = cxt;
+				// cxt.clearRect(0,0,c.width,c.height);
+				this.ctrl = {
+					width: 0,
+					height: 0,
+					clear: () => {
+						this.lines = [];
+						let info = uni.createSelectorQuery().in(this).select("." + this.cid);
+						info.boundingClientRect((data) => {
+							if (data) {
+								cxt.clearRect(0, 0, data.width, data.height);
+								if (data.width && data.height) {
+									this.ctrl.width = data.width;
+									this.ctrl.height = data.height;
+								}
+							}
+						}).exec();
+						this.redraw();
+					},
+					getValue: () => {
+						if (!this.lines.length)
+							return '';
+						let svg = this._get_svg();
+						// new Buff
+						let b64 = base64encode(svg);
+						let data = 'data:image/svg+xml;base64,' + b64;
+						// console.log(svg);
+						// console.log(data);
+						return data;
+					},
+				};
+				this.$nextTick(function() {
+					this.ctrl.clear();
+				})
+			},
+			_get_svg() {
+				let r = -90;
+				let paths = [];
+				let raww = this.ctrl.width;
+				let rawh = this.ctrl.height;
+				let width = Math.abs(r) != 90 ? raww : rawh;
+				let height = Math.abs(r) == 90 ? raww : rawh;
+				let cx = raww / 2;
+				let cy = rawh / 2;
+				let PI = Math.PI;
+				let R = (r || 0) % 360;
+				let cosv = Math.cos(R * PI / 180);
+				let sinv = Math.sin(R * PI / 180);
+				let dcx = (width - raww) / 2;
+				let dcy = (height - rawh) / 2;
+				let trans = function(p) {
+					if (!R) {
+						return p;
+					} else {
+						let nx = (p.x - cx) * cosv - (p.y - cy) * sinv + cx;
+						let ny = (p.x - cx) * sinv + (p.y - cy) * cosv + cy;
+						return {
+							x: nx + dcx,
+							y: ny + dcy
+						};
+					}
+					return p;
+				}
+				this.lines.map(l => {
+					if (l.points.length < 2) {
+						return;
+					}
+					let sp = trans(l.start)
+					let pts = [`M ${sp.x} ${Number(sp.y)}`];
+					l.points.map(p => {
+						let np = trans(p)
+						pts.push(`L ${np.x} ${Number(np.y)}`);
+					});
+					paths.push(
+						`<path stroke-linejoin="round" stroke-linecap="round" stroke-width="3" stroke="rgb(0,0,0)" fill="none" d="${pts.join(' ')}"/>`
+					);
+				})
+				let svg =
+					`<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}" height="${height}">${paths.join('\n')}</svg>`;
+				return svg;
+			},
+			_get_svg_raw() {
+				let paths = [];
+				this.lines.map(l => {
+					if (l.points.length < 2) {
+						return;
+					}
+					let pts = [`M ${l.start.x} ${Number(l.start.y)}`];
+					l.points.map(p => {
+						pts.push(`L ${p.x} ${Number(p.y)}`);
+					});
+					paths.push(
+						`<path stroke-linejoin="round" stroke-linecap="round" stroke-width="3" stroke="rgb(0,0,0)" fill="none" d="${pts.join(' ')}"/>`
+					);
+				})
+				let width = this.ctrl.width;
+				let height = this.ctrl.height;
+				let svg =
+					`<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}" height="${height}" transform="rotate(-90)">${paths.join('\n')}</svg>`;
+				return svg;
+			},
+			_get_point(e) {
+				return {
+					x: e.changedTouches[0].x.toFixed(1),
+					y: e.changedTouches[0].y.toFixed(1),
+				}
+			},
+			touchstart(e) {
+				let p = this._get_point(e);
+				this.line = {
+					start: p,
+					points: [p],
+				}
+				this.lines.push(this.line);
+			},
+			touchmove(e) {
+				let p = this._get_point(e);
+				this.line.points.push(p)
+				if (!this.tm) {
+					this.tm = setTimeout(() => {
+						this.redraw();
+						this.tm = 0;
+					}, 10)
+				}
+			},
+			touchend(e) {
+				let p = this._get_point(e);
+				this.line.points.push(p)
+				this.line.end = p
+				this.redraw()
+			},
+			redraw() {
+				let cxt = this.cxt;
+				cxt.setStrokeStyle("#000");
+				cxt.setLineWidth(3);
+				var last = null;
+				this.lines.map(l => {
+					cxt.beginPath();
+					if (l.points.length < 2) {
+						return;
+					}
+					cxt.moveTo(l.start.x, l.start.y);
+					l.points.map(p => {
+						cxt.lineTo(p.x, p.y)
+					})
+					cxt.stroke()
+				})
+
+				cxt.draw()
+			},
+			canvasIdErrorCallback: function(e) {
+				console.error(e.detail.errMsg)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.signature-wrap {
+		height: 100%;
+		width: 100%;
+		// padding: 0 5px;
+
+		// min-width: 60vw;
+		.img-wrap {
+			width: 100%;
+			min-height: 200rpx;
+			display: flex;
+			align-items: center;
+			text-align: center;
+			align-content: center;
+			justify-content: center;
+
+			image {
+				width: 100%;
+			}
+
+			// background: red;
+		}
+	}
+
+	.signature-contain {
+		z-index: 9000;
+		position: fixed;
+		left: 0;
+		top: 0;
+		width: 100%;
+
+		.signature-main {
+			background: white;
+			flex-direction: row-reverse;
+			display: flex;
+			align-items: stretch;
+			height: 101%;
+			overflow: scroll;
+		}
+
+		.signature-title {
+			font-weight: bold;
+			font-size: 18px;
+			display: flex;
+			padding: 0 20rpx;
+			flex-direction: column;
+			justify-content: center;
+			height: 100vh;
+			color: $uni-text-color;
+
+			text {
+				transform: rotate(90deg);
+			}
+		}
+
+		.signature {
+			border: 1px dotted black;
+			border-bottom: 1px dotted black;
+			background: #FFF;
+			margin: 10px 0;
+			width: 90vw;
+			height: 90vh;
+			align-self: center;
+			// pointer-events:none;
+		}
+
+		.signature-btns {
+			display: flex;
+			padding: 2px;
+			// margin-right: 5px;
+			flex-direction: column;
+
+			.btn {
+				flex-grow: 1;
+				flex-shrink: 0;
+				padding: 20rpx;
+				font-size: 20px;
+				margin: 0;
+				text-align: center;
+				text-decoration: none;
+				height: 30vh;
+				display: flex;
+				align-content: center;
+				justify-content: center;
+				flex-direction: column;
+
+				text {
+					transform: rotate(90deg);
+				}
+
+				&+.btn {
+					border-top: 1px solid #eee;
+				}
+
+				&.btn-clear {
+					// background-color: #fc2a07;
+					color: $uni-color-success;
+				}
+
+				&.btn-cancel {
+					// background-color: #eff4f4;
+					color: $uni-color-warning;
+				}
+
+				&.btn-ok {
+					// background-color: $uni-color-success;
+					color: $uni-color-primary;
+				}
+			}
+		}
+	}
+</style>

+ 55 - 0
components/u-class/u-class.vue

@@ -0,0 +1,55 @@
+<template>
+	<view class="u-class">
+		<view class="title" :title="title">
+			{{title}}
+			<image class="icon" src="/static/index/card-title-icon.png"></image>
+		</view>
+		<view class="content">
+			<slot />
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:"u-class",
+		props: {
+			title: {
+				type: String,
+				default: ''
+			},
+		},
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-class {
+		.title {
+			font-size: 36rpx;
+			
+			font-weight: bold;
+			color: #333333;
+			padding: 0 32rpx 0 40rpx;
+			position: relative;
+
+			.icon {
+				width: 7rpx;
+				height: 30rpx;
+				position: absolute;
+				left: 0;
+				top: 50%;
+				transform: translateY(-50%);
+			}
+		}
+		.content {
+			padding: 16rpx 0;
+			margin-bottom: 32rpx;
+		}
+	}
+
+</style>

+ 112 - 0
components/u-des-row/u-des-row.vue

@@ -0,0 +1,112 @@
+<template>
+	<view class="u-des-row" :class="{'border': border, 'click': isCilck}" @click="$emit('click')">
+		<slot name="left" />
+		<view class="label">
+			{{label}}
+		</view>
+		<view class="value">
+			<slot name="value" :isOpen="isOpen">
+				{{value}}
+			</slot>
+		</view>
+		<!--  -->
+		<view class="right-icon" v-if="forward">
+			<uni-icons type="forward" size="12" color="#9FA5BA" />
+		</view>
+		<view class="right-icon" v-else-if="eye" @click="isOpen=!isOpen">
+			<image v-if="isOpen" src="/static/open_icon.png" style="width: 28rpx;height: 20rpx;" mode=""></image>
+			<image v-else src="/static/no_open.png" style="width: 28rpx;height: 11rpx;" mode=""></image>
+		</view>
+
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-des-row",
+		props: {
+			label: {
+				type: [String, Number],
+				default: ''
+			},
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			border: {
+				type: Boolean,
+				default: false
+			},
+			forward: { // 向右箭头
+				type: Boolean,
+				default: false
+			},
+			eye: { // 眼睛 可视?
+				type: Boolean,
+				default: false
+			},
+		},
+		data() {
+			return {
+				isOpen: false
+			}
+		},
+		computed: {
+			isCilck() {
+				return Boolean(this.$listeners['click'])
+			}
+		}
+
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-des-row {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		height: 100rpx;
+
+
+		&.border {
+			border-bottom: 1px solid #F5F7FA;
+		}
+
+		&.click {
+			cursor: pointer;
+
+			&:hover {
+				background: #F5F7FA;
+			}
+		}
+
+		.label {
+			font-size: 28rpx;
+			margin-right: 16rpx;
+			color: #333333;
+			font-weight: bold;
+		}
+
+		.value {
+			flex: 1;
+			text-align: right;
+			font-size: 28rpx;
+			font-weight: 400;
+			color: #333333;
+
+			//超过一行省略号 one-line
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+
+		.right-icon {
+			height: 36rpx;
+			width: 36rpx;
+			display: flex;
+			align-items: center;
+			justify-content: flex-end;
+			cursor: pointer;
+		}
+	}
+</style>

+ 147 - 0
components/u-drawer-input/u-drawer-input.vue

@@ -0,0 +1,147 @@
+<template>
+	<view class="u-drawer " :class="visible ? 'u-drawer-open' : ''">
+		<view class="u-drawer-mask" @click="cancel" />
+		<view class="u-drawer-content">
+			<view class="header u-flex">
+				<view class="cancel" @click="cancel">取消</view>
+				<view v-if="config.title" class="title">{{config.title}}</view>
+				<view class="u-button" @click="sumbit">完成</view>
+			</view>
+			<slot></slot>
+			<uni-easyinput :focus="true" v-model="inputValue" :placeholder="config.placeholder"></uni-easyinput>
+		</view>
+	</view>
+</template>
+
+<script>
+	import formChecker from '@/common/formChecker.js'
+	export default {
+		name: "u-drawer-input",
+		props: {
+			visible: {
+				type: Boolean,
+				default: false
+			},
+			config: {
+				type: Object,
+				default: () => {
+					return {}
+				}
+			},
+		},
+		data() {
+			return {
+				inputValue: ''
+			}
+		},
+		watch: {
+			visible: {
+				handler() {
+					this.visible && this.init()
+				},
+				deep: true,
+				immediate: true
+			}
+		},
+		methods: {
+			init() {
+				this.inputValue = this.config.value || ''
+			},
+			cancel() {
+				!Boolean(this.$listeners['click']) && this.$emit('update:visible', false)
+			},
+			sumbit() {
+				//定义表单规则
+				var rule = this.config.rule || []
+				//进行表单检查
+				var checkRes = formChecker.check({
+					value: this.inputValue
+				}, rule);
+				if (!checkRes) {
+					uni.showToast({
+						title: formChecker.error,
+						icon: "none"
+					});
+					return
+				}
+				this.$emit('sumbit', this.inputValue)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.input-container {}
+
+	.u-drawer>* {
+		transition: transform .3s cubic-bezier(.7, .3, .1, 1), box-shadow .3s cubic-bezier(.7, .3, .1, 1);
+	}
+
+	.u-drawer {
+		position: fixed;
+		z-index: 99;
+
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 0%;
+	}
+
+	.u-drawer-mask {
+		position: absolute;
+		top: 0;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		background-color: rgba(0, 0, 0, .45);
+		display: none;
+		// pointer-events: none;
+	}
+
+	.u-drawer-open {
+		height: 100%;
+
+		.u-drawer-content,
+		.u-drawer-mask {
+			display: inherit;
+			transform: translateY(0);
+		}
+	}
+
+
+	.u-drawer-content {
+		position: absolute;
+		width: 100%;
+		bottom: 0;
+		top: 0;
+		// box-shadow: 0 -2px 8px rgb(0 0 0 / 15%);
+		padding: 28rpx 32rpx;
+		min-height: 100rpx;
+		background: #FFFFFF;
+		border-radius: 28rpx 28rpx 0px 0px;
+		// display: none;
+		transform: translateY(100%);
+		// animation: mymove 0.3s;
+		// animation-iteration-count: 1;
+
+		.header {
+			margin-bottom: 50rpx;
+			height: auto;
+
+			.title {
+				font-size: 36rpx;
+				font-weight: bold;
+				color: #333333;
+			}
+
+			.cancel {
+				min-width: 150rpx;
+				padding: 12rpx 0;
+				font-size: 28rpx;
+				font-weight: 400;
+				color: #333333;
+				cursor: pointer;
+			}
+		}
+	}
+</style>

+ 115 - 0
components/u-drawer/u-drawer.vue

@@ -0,0 +1,115 @@
+<template>
+	<view class="u-drawer " :class="visible ? 'u-drawer-open' : ''">
+		<view class="u-drawer-mask" @click="cancel"/>
+		<view class="u-drawer-content" >
+			<view class="header u-flex">
+				<view class="cancel" @click="cancel">取消</view>
+				<view v-if="title" class="title">{{title}}</view>
+				<view class="u-button" @click="sumbit">完成</view>
+			</view>
+			 <slot></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-drawer",
+		props: {
+			visible: {
+				type: Boolean,
+				default: false
+			},
+			title: {
+				type: String,
+				default: ''
+			},
+			
+		},
+		methods: {
+			test(e) {
+				console.log(e)
+			},
+			cancel() {
+				this.$emit('update:visible', false)
+			},
+			sumbit() {
+				this.$emit('sumbit')
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-drawer>* {
+		transition: transform .3s cubic-bezier(.7, .3, .1, 1), box-shadow .3s cubic-bezier(.7, .3, .1, 1);
+	}
+
+	.u-drawer {
+		position: fixed;
+		z-index: 1000;
+
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 0%;
+	}
+
+	.u-drawer-mask {
+		position: absolute;
+		top: 0;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		background-color: rgba(0, 0, 0, .45);
+		display: none;
+		// pointer-events: none;
+	}
+	.u-drawer-open {
+		height: 100%;
+	
+		.u-drawer-content,
+		.u-drawer-mask {
+			display: inherit;
+			transform: translateY(0);
+		}
+	}
+	
+
+	.u-drawer-content {
+		position: absolute;
+		width: 100%;
+		bottom: 0;
+		// box-shadow: 0 -2px 8px rgb(0 0 0 / 15%);
+		padding: 28rpx 32rpx;
+		min-height: 100rpx;
+		background: #FFFFFF;
+		border-radius: 28rpx 28rpx 0px 0px;
+		// display: none;
+		transform: translateY(1000%);
+		// animation: mymove 0.3s;
+		// animation-iteration-count: 1;
+		
+		.header {
+			margin-bottom: 50rpx;
+		
+			.title {
+				font-size: 36rpx;
+				font-weight: bold;
+				color: #333333;
+			}
+		
+			.cancel {
+				min-width: 150rpx;
+				padding: 12rpx 0;
+				font-size: 28rpx;
+				font-weight: 400;
+				color: #333333;
+				cursor: pointer;
+			}
+		}
+	}
+
+
+
+</style>

+ 154 - 0
components/u-input/u-input.vue

@@ -0,0 +1,154 @@
+<template>
+	<view class="u-input-container" :class="{'has-sumbit-button': sumbitButton}">
+		<textarea v-if="type==='textarea'" type="digit" style="min-height:100rpx" @blur="blur" auto-height
+			@focus="isFocus=true" class="u-input" :value="inputValue" :focus="isFocus"
+			@input="e=>change(e.detail.value)" :placeholder="placeholder">
+		</textarea>
+		<input v-else-if="type==='digit'" type="digit" @blur="blur" @focus="isFocus=true" class="u-input"
+			:value="inputValue" :focus="isFocus" @input="e=>change(e.detail.value)" :placeholder="placeholder">
+		</input>
+		<input v-else-if="type==='number'" type="number" @blur="blur" @focus="isFocus=true" class="u-input"
+			:value="inputValue" :focus="isFocus" @input="e=>change(e.detail.value)" :placeholder="placeholder">
+		</input>
+		<input v-else @blur="blur" @focus="isFocus=true" class="u-input" :value="inputValue" :focus="isFocus"
+			@input="e=>change(e.detail.value)" :placeholder="placeholder">
+		</input>
+
+		<uni-icons v-if="inputValue && isFocus" class="input-clear" @click="clear" type="clear" size="16"
+			color="#9FA5BA" />
+		<button v-if="sumbitButton" :disabled="!inputValue" @click="sumbit" class="sumbit-button" size="mini"
+			type="primary">完成</button>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-input",
+		model: {
+			prop: 'inputValue',
+			event: 'change'
+		},
+		props: {
+			inputValue: {
+				type: String,
+				default: ''
+			},
+			placeholder: {
+				type: String,
+				default: ''
+			},
+			sumbitButton: {
+				type: Boolean,
+				default: false
+			},
+			type: {
+				type: String,
+				default: ''
+			},
+
+		},
+		data() {
+			return {
+				value: '',
+				isFocus: false
+			}
+		},
+		watch: {
+			value: {
+				handler() {
+					this.$emit('change', this.value)
+				},
+				deep: false,
+				immediate: true
+			},
+		},
+		mounted() {
+			this.value = this.inputValue
+		},
+		methods: {
+			change(value) {
+				this.$emit('change', value)
+			},
+			clear() {
+				this.change('')
+				const that = this
+				setTimeout(function() {
+					that.isFocus = true
+				}, 200);
+			},
+			blur() {
+				const that = this
+				setTimeout(function() {
+					that.isFocus = false
+					that.setBlur()
+				}, 100);
+			},
+			setBlur() {
+				const that = this
+				setTimeout(function() {
+					that.isFocus === false && that.$emit('blur')
+				}, 400);
+			},
+			sumbit() {
+				this.$emit('sumbit', this.inputValue)
+				const that = this
+				setTimeout(function() {
+					that.isFocus = true
+				}, 200);
+			}
+
+		}
+	}
+</script>
+
+<style lang="scss">
+	.u-input-container {
+		width: 100%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		align-items: center;
+
+		.u-input {
+			width: 100%;
+			height: 100%;
+			position: relative;
+			padding-top: 4rpx;
+
+		}
+
+		.input-clear {
+			padding: 12rpx 0 12rpx 12rpx;
+			cursor: pointer;
+		}
+
+		.sumbit-button {
+			display: flex;
+			justify-content: center;
+			align-items: center;
+
+			font-size: 28rpx;
+			color: #FFFFFF;
+			padding: 12rpx 16rpx;
+			white-space: nowrap;
+			line-height: 1;
+			margin-left: 16rpx;
+		}
+	}
+
+	.has-sumbit-button {}
+</style>
+<style lang="scss">
+	input,
+	textarea {
+		font-size: 28rpx;
+		font-weight: 400;
+		color: #333;
+	}
+
+	.uni-input-placeholder {
+		font-size: 28rpx;
+		font-weight: 400;
+		color: #A3ABBF;
+	}
+</style>

+ 86 - 0
components/u-nav-bar/u-nav-bar.vue

@@ -0,0 +1,86 @@
+<template>
+	<view class="u-nav-bar" :style="{color: color,background: bg}">
+		<uni-icons type="arrow-left" class="left-icon" size="30" :color="color" @click="toBack"></uni-icons>
+		<view class="title" :title="title" v-if="title">
+			{{title}}
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-nav-bar",
+		props: {
+			title: { // 页面标题
+				type: String,
+				default: ''
+			},
+			color: { // 页面颜色
+				type: String,
+				default: 'white'
+			},
+			bg: { // 底色颜色
+				type: String,
+				default: ''
+			},
+		},
+		computed: {
+			showNavBar() {
+				return Boolean(this.$listeners['on-left']) && this.prevPage
+			}
+		},
+		data() {
+			return {
+				prevPage: null
+			};
+		},
+		mounted() {
+			let pages = getCurrentPages() // 获取栈实例
+			let page = pages[pages.length - 1] // 获取当前页面的数据,包含页面路由
+			this.prevPage = pages[pages.length - 2] // 获取上个页面的数据,包含页面路由
+		},
+		methods: {
+			toBack() {
+				if (Boolean(this.$listeners['on-left'])) {
+					this.$emit('on-left')
+				} else if (this.prevPage) {
+					uni.navigateBack()
+				} else {
+					console.log('不存在上一页面')
+					uni.switchTab({
+						url: '/pages/index/index'
+					});
+				}
+
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-nav-bar {
+		position: sticky;
+		top: 0;
+		left: 0;
+		z-index: 5;
+		width: 100%;
+		height: $u-nav-bar-height;
+		// padding: 0 32rpx;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-around;
+		align-items: flex-start;
+
+		.left-icon {
+			display: inline-block;
+			cursor: pointer;
+			padding: 0 24rpx;
+		}
+
+		.title {
+			font-size: 36rpx;
+			font-weight: bold;
+			padding: 0 32rpx;
+		}
+	}
+</style>

+ 57 - 0
components/u-panel/u-panel.vue

@@ -0,0 +1,57 @@
+<template>
+	<view class="u-panel" :class="theme">
+		<view class="u-panel-title u-flex" v-if="title">
+			<image v-if="theme==='red'" class="icon" src="/static/home_label_red.png"></image>
+			<image v-else class="icon" src="/static/home_label.png"></image>
+			<view class="">{{title}}</view>
+			<view style="flex:1" />
+		</view>
+		<slot />
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-panel",
+		props: {
+			title: {
+				type: String,
+				default: ''
+			},
+			theme: {
+				type: String,
+				default: ''
+			},
+		},
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-panel {
+		background: #FFFFFF;
+		margin-bottom: 24rpx;
+		box-shadow: 0px 4rpx 22rpx 0px rgba(232, 237, 251, 0.8);
+		border-radius: 6rpx;
+		padding: 12rpx 32rpx 24rpx;
+
+		&.red {
+			.u-panel-title {
+				color: #EF6960;
+			}
+		}
+
+		.u-panel-title {
+			padding: 24rpx 0;
+			text-align: left;
+			font-size: 30rpx;
+			font-weight: bold;
+			color: #078EF7;
+
+			.icon {
+				width: 7rpx;
+				height: 30rpx;
+				margin-right: 20rpx;
+			}
+		}
+	}
+</style>

+ 65 - 0
components/u-picker/u-picker.vue

@@ -0,0 +1,65 @@
+<template>
+	<picker @change="change" range-key="pickerName" :value="selectedItem.valueIndex" :range="sourceOptions">
+		<slot :data="selectedItem"/>
+	</picker>
+</template>
+
+<script>
+	export default {
+		model: {
+			prop: 'value',
+			event: 'changeValue'
+		},
+		props: {
+			value: {
+				type: [Number, String],
+				default: ''
+			},
+			options: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			valueKey: {
+				type: String,
+				default: 'value'
+			},
+			labelKey: {
+				type: String,
+				default: 'label'
+			},
+		},
+		computed: {
+			sourceOptions(){
+				const list = this.options.map(item=>{
+					const pickerName = item.disabled===true? item[this.labelKey] + '(已选)' : item[this.labelKey]
+				return {...item, pickerName}
+				})
+				// const list = this.options.filter(item=>item.disabled!==true)
+				return list
+			},
+			selectedItem() {
+				const valueIndex = this.options.findIndex(element => element[this.valueKey] == this.value)
+				const item = valueIndex>=0 ? this.options[valueIndex] : {}
+				item.valueIndex = valueIndex
+				return item
+			}
+		},
+		methods: {
+			change(e) {
+				const index = e.detail.value
+				const item = this.options[index]
+				this.$emit('changeValue', item[this.valueKey])
+				const that = this
+				setTimeout(function() {
+					that.$emit('change', that.selectedItem[that.valueKey], that.selectedItem)
+				}, 10);
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 249 - 0
components/u-sign/u-sign.vue

@@ -0,0 +1,249 @@
+<template>
+	<view class="u-sign" :class="{'is-null': isNull}">
+		<view class="u-sign-content">
+			<canvas class="handWriting" :disable-scroll="true" @touchstart="uploadScaleStart"
+				@touchmove="uploadScaleMove" canvas-id="handWriting"></canvas>
+		</view>
+		<view class="re-draw" @click="retDraw">
+			<image style="width: 37rpx;height: 35rpx;" src="/static/xpc.png" mode="widthFix" />
+		</view>
+		<view v-if="false" class="handBtn">
+			<image @click="selectColorEvent('black','#1A1A1A')"
+				:src="selectColor === 'black' ? '/static/other/color_black_selected.png' : '/static/other/color_black.png'"
+				:class="[selectColor === 'black' ? 'color_select' : '', 'black-select']"></image>
+			<image @click="selectColorEvent('red','#ca262a')"
+				:src="selectColor === 'red' ? '/static/other/color_red_selected.png' : '/static/other/color_red.png'"
+				:class="[selectColor === 'red' ? 'color_select' : '', 'black-select']"></image>
+			<button @click="retDraw" class="delBtn">重写</button>
+			<button @click="saveCanvasAsImg" class="saveBtn">保存</button>
+			<button @click="previewCanvasImg" class="previewBtn">预览</button>
+			<button @click="subCanvas" class="subBtn">完成</button>
+		</view>
+
+	</view>
+
+</template>
+
+<script>
+	export default {
+		name: 'u-sign',
+		data() {
+			return {
+				canvasName: 'handWriting',
+				ctx: '',
+				startX: null,
+				startY: null,
+				canvasWidth: 0,
+				canvasHeight: 0,
+				selectColor: 'black',
+				lineColor: '#1A1A1A', // 颜色
+				lineSize: 5, // 笔记倍数
+				isNull: true
+			};
+		},
+		mounted() {
+			this.ctx = uni.createCanvasContext("handWriting");
+			console.log(111, this.ctx)
+			this.$nextTick(() => {
+				uni.createSelectorQuery().select('.u-sign-content').boundingClientRect(rect => {
+						this.canvasWidth = rect.width;
+						this.canvasHeight = rect.height;
+						/* 将canvas背景设置为 白底,不设置  导出的canvas的背景为透明 */
+						this.setCanvasBg('#FAFAFA');
+					})
+					.exec();
+			});
+		},
+		methods: {
+			// 笔迹开始
+			uploadScaleStart(e) {
+				this.isNull = false
+				this.startX = e.changedTouches[0].x
+				this.startY = e.changedTouches[0].y
+				//设置画笔参数
+				//画笔颜色
+				this.ctx.setStrokeStyle(this.lineColor)
+				//设置线条粗细
+				this.ctx.setLineWidth(this.lineSize)
+				//设置线条的结束端点样式
+				this.ctx.setLineCap("round") //'butt'、'round'、'square'
+				//开始画笔
+				this.ctx.beginPath()
+			},
+			// 笔迹移动
+			uploadScaleMove(e) {
+				//取点
+				let temX = e.changedTouches[0].x
+				let temY = e.changedTouches[0].y
+				//画线条
+				this.ctx.moveTo(this.startX, this.startY)
+				this.ctx.lineTo(temX, temY)
+				this.ctx.stroke()
+				this.startX = temX
+				this.startY = temY
+				this.ctx.draw(true)
+			},
+			/**
+			 * 重写
+			 */
+			retDraw() {
+				this.isNull = true
+				this.ctx.clearRect(0, 0, 700, 730);
+				this.ctx.draw();
+				//设置canvas背景
+				this.setCanvasBg('#fff');
+			},
+			/**
+			 * @param {Object} str
+			 * @param {Object} color
+			 * 选择颜色
+			 */
+			selectColorEvent(str, color) {
+				this.selectColor = str;
+				this.lineColor = color;
+			},
+			getBase64(success) {
+				if (this.isNull) {
+					success(null)
+				} else {
+					uni.canvasToTempFilePath({
+						canvasId: 'handWriting',
+						fileType: 'png',
+						quality: 1, //图片质量
+						success,
+					})
+
+				}
+			},
+			//完成
+			subCanvas() {
+				uni.canvasToTempFilePath({
+					canvasId: 'handWriting',
+					fileType: 'png',
+					quality: 1, //图片质量
+					success(res) {
+						// console.log(res.tempFilePath, 'canvas生成图片地址');
+						uni.showToast({
+							title: '以保存'
+						});
+						//保存到系统相册
+						uni.saveImageToPhotosAlbum && uni.saveImageToPhotosAlbum({
+							filePath: res.tempFilePath,
+							success(res) {
+								uni.showToast({
+									title: '已成功保存到相册',
+									duration: 2000
+								});
+							}
+						});
+					}
+				});
+			},
+			//保存到相册
+			saveCanvasAsImg() {
+				uni.canvasToTempFilePath({
+					canvasId: 'handWriting',
+					fileType: 'png',
+					quality: 1, //图片质量
+					success(res) {
+						console.log(res.tempFilePath, 'canvas生成图片地址');
+						uni.saveImageToPhotosAlbum && uni.saveImageToPhotosAlbum({
+							filePath: res.tempFilePath,
+							success(res) {
+								uni.showToast({
+									title: '已保存到相册',
+									duration: 2000
+								});
+							}
+						});
+					}
+				});
+			},
+			//预览
+			previewCanvasImg() {
+				uni.canvasToTempFilePath({
+					canvasId: 'handWriting',
+					fileType: 'jpg',
+					quality: 1, //图片质量
+					success(res) {
+						uni.previewImage({
+							urls: [res.tempFilePath] //预览图片 数组
+						});
+					}
+				});
+			},
+			//设置canvas背景色  不设置  导出的canvas的背景为透明
+			//@params:字符串  color
+			setCanvasBg(color) {
+
+				/* 将canvas背景设置为 白底,不设置  导出的canvas的背景为透明 */
+				//rect() 参数说明  矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
+				//这里是 canvasHeight - 4 是因为下边盖住边框了,所以手动减了写
+				this.ctx.rect(0, 0, this.canvasWidth, this.canvasHeight - 4);
+				// ctx.setFillStyle('red')
+				this.ctx.setFillStyle(color);
+				this.ctx.fill(); //设置填充
+				this.ctx.draw(); //开画
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.u-sign {
+		overflow: hidden;
+		position: relative;
+
+		&:after {
+			position: absolute;
+			top: 0;
+			right: 0;
+			left: 0;
+			bottom: 0;
+			content: '';
+			font-size: 36rpx;
+			font-weight: bold;
+			color: #CCCCCC;
+			padding-top: 200rpx;
+			text-align: center;
+			border: 1rpx solid #CCCCCC;
+			pointer-events: none;
+			opacity: 0.5;
+		}
+	}
+
+	.is-null {
+		&:after {
+			content: '请在此签名';
+		}
+	}
+
+	.handWriting {
+		background: #fff;
+		width: 100%;
+		height: 100%;
+	}
+
+	.handRight {
+		display: inline-flex;
+		align-items: center;
+	}
+
+	.u-sign-content {
+		width: 100%;
+		height: 454rpx;
+		overflow: hidden;
+		background: #FAFAFA;
+		// border: 1rpx solid #CCCCCC;
+		border-radius: 8rpx;
+		// margin-bottom: 32rpx;
+	}
+
+	.re-draw {
+		position: absolute;
+		bottom: 0;
+		right: 0;
+		padding: 16rpx;
+		cursor: pointer;
+	}
+</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>

+ 38 - 0
main.js

@@ -0,0 +1,38 @@
+import App from './App'
+import ajax from './common/ajax' 
+import mixin from './common/mixin' 
+import store from './store'
+// #ifndef VUE3
+import Vue from 'vue'
+// Vue2:挂载在 Vue 原型链上,则通过 this.$ajax 调用
+Vue.prototype.$ajax = ajax
+Vue.mixin(mixin)
+
+
+
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+    ...App,
+	store,
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  // Vue3 (Options API):挂载在当前应用上(app 为 createSSRApp 后的应用),也是通过 this.$ajax 调用
+  app.config.globalProperties.$ajax = ajax
+  	app.use(store)
+  return {
+    app
+  }
+}
+// #endif
+
+
+// 如果你在项目中有用到 nvue 页面,是无法通过 this.$ajax 调用
+// 需要将请求方法添加到 uni 对象上,然后通过 uni.$ajax 调用
+uni.$ajax = ajax

+ 78 - 0
manifest.json

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

+ 462 - 0
package-lock.json

@@ -0,0 +1,462 @@
+{
+  "name": "shunt-app",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "shunt-app",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "uni-ajax": "^2.4.3",
+        "vuex": "^4.0.2"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.18.11",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.11.tgz",
+      "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
+      "peer": true,
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
+      "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.16.4",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "source-map": "^0.6.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
+      "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-core": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
+      "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.16.4",
+        "@vue/compiler-core": "3.2.37",
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/compiler-ssr": "3.2.37",
+        "@vue/reactivity-transform": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.25.7",
+        "postcss": "^8.1.10",
+        "source-map": "^0.6.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
+      "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz",
+      "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ=="
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz",
+      "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
+      "peer": true,
+      "dependencies": {
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "node_modules/@vue/reactivity-transform": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
+      "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.16.4",
+        "@vue/compiler-core": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.25.7"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
+      "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
+      "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
+      "peer": true,
+      "dependencies": {
+        "@vue/runtime-core": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "csstype": "^2.6.8"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
+      "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-ssr": "3.2.37",
+        "@vue/shared": "3.2.37"
+      },
+      "peerDependencies": {
+        "vue": "3.2.37"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz",
+      "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+      "peer": true
+    },
+    "node_modules/csstype": {
+      "version": "2.6.20",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz",
+      "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==",
+      "peer": true
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "peer": true
+    },
+    "node_modules/magic-string": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz",
+      "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+      "peer": true,
+      "dependencies": {
+        "sourcemap-codec": "^1.4.8"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz",
+      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "peer": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "peer": true
+    },
+    "node_modules/postcss": {
+      "version": "8.4.16",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.16.tgz",
+      "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.4",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "peer": true
+    },
+    "node_modules/uni-ajax": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmmirror.com/uni-ajax/-/uni-ajax-2.4.3.tgz",
+      "integrity": "sha512-ny+6KoXs6D7KWiLQ1vEzNig0QgfkTDlP3Hx8Z6hcX2lqSsjq6P2Q70818gA+YUh5NrsuhH1M4F4wt4eQsRo8yw=="
+    },
+    "node_modules/vue": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz",
+      "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/compiler-sfc": "3.2.37",
+        "@vue/runtime-dom": "3.2.37",
+        "@vue/server-renderer": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "node_modules/vuex": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",
+      "integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.0.0-beta.11"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.2"
+      }
+    }
+  },
+  "dependencies": {
+    "@babel/parser": {
+      "version": "7.18.11",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.11.tgz",
+      "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
+      "peer": true
+    },
+    "@vue/compiler-core": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
+      "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
+      "peer": true,
+      "requires": {
+        "@babel/parser": "^7.16.4",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "source-map": "^0.6.1"
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
+      "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
+      "peer": true,
+      "requires": {
+        "@vue/compiler-core": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
+      "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
+      "peer": true,
+      "requires": {
+        "@babel/parser": "^7.16.4",
+        "@vue/compiler-core": "3.2.37",
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/compiler-ssr": "3.2.37",
+        "@vue/reactivity-transform": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.25.7",
+        "postcss": "^8.1.10",
+        "source-map": "^0.6.1"
+      }
+    },
+    "@vue/compiler-ssr": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
+      "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
+      "peer": true,
+      "requires": {
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz",
+      "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ=="
+    },
+    "@vue/reactivity": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz",
+      "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
+      "peer": true,
+      "requires": {
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "@vue/reactivity-transform": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
+      "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
+      "peer": true,
+      "requires": {
+        "@babel/parser": "^7.16.4",
+        "@vue/compiler-core": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.25.7"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
+      "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
+      "peer": true,
+      "requires": {
+        "@vue/reactivity": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
+      "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
+      "peer": true,
+      "requires": {
+        "@vue/runtime-core": "3.2.37",
+        "@vue/shared": "3.2.37",
+        "csstype": "^2.6.8"
+      }
+    },
+    "@vue/server-renderer": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
+      "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
+      "peer": true,
+      "requires": {
+        "@vue/compiler-ssr": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz",
+      "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+      "peer": true
+    },
+    "csstype": {
+      "version": "2.6.20",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz",
+      "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==",
+      "peer": true
+    },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "peer": true
+    },
+    "magic-string": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz",
+      "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+      "peer": true,
+      "requires": {
+        "sourcemap-codec": "^1.4.8"
+      }
+    },
+    "nanoid": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz",
+      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "peer": true
+    },
+    "picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "peer": true
+    },
+    "postcss": {
+      "version": "8.4.16",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.16.tgz",
+      "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
+      "peer": true,
+      "requires": {
+        "nanoid": "^3.3.4",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "peer": true
+    },
+    "source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "peer": true
+    },
+    "sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "peer": true
+    },
+    "uni-ajax": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmmirror.com/uni-ajax/-/uni-ajax-2.4.3.tgz",
+      "integrity": "sha512-ny+6KoXs6D7KWiLQ1vEzNig0QgfkTDlP3Hx8Z6hcX2lqSsjq6P2Q70818gA+YUh5NrsuhH1M4F4wt4eQsRo8yw=="
+    },
+    "vue": {
+      "version": "3.2.37",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz",
+      "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
+      "peer": true,
+      "requires": {
+        "@vue/compiler-dom": "3.2.37",
+        "@vue/compiler-sfc": "3.2.37",
+        "@vue/runtime-dom": "3.2.37",
+        "@vue/server-renderer": "3.2.37",
+        "@vue/shared": "3.2.37"
+      }
+    },
+    "vuex": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",
+      "integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
+      "requires": {
+        "@vue/devtools-api": "^6.0.0-beta.11"
+      }
+    }
+  }
+}

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "shunt-app",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git@gitee.com:ouchulun/shunt-app.git"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "uni-ajax": "^2.4.3",
+    "vuex": "^4.0.2"
+  }
+}

+ 52 - 0
pages.json

@@ -0,0 +1,52 @@
+{
+	//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+	"pages": [{
+		"path": "pages/index/index" // 首页
+	}, {
+		"path": "pages/my/my" // 我的
+	}, {
+		"path": "pages/apps/shunt-places/shunt-places" // 分流名额
+	}, {
+		"path": "pages/apps/my-grades/my-grades" // 本人成绩
+	}, {
+		"path": "pages/apps/special-application/special-application" // 特别申请
+	}, {
+		"path": "pages/apps/change-major/change-major" // 专业分流
+	}, {
+		"path": "pages/apps/progress-query/progress-query" // 进度查询
+	}, {
+		"path": "pages/my/user-info/user-info" // 个人信息
+	},{
+		"path": "pages/my/problem-feedback/problem-feedback" // 个人信息
+	},  {
+		"path": "pages/common/webview/webview" // 第三方网址页面
+	}, {
+		"path": "pages/error/403/403"
+	} ],
+	"globalStyle": {
+		"navigationBarTextStyle": "white",
+		"navigationStyle": "custom",
+		"navigationBarTitleText": "",
+		"navigationBarBackgroundColor": "#fff"
+		// "backgroundColor": "#fff"
+	},
+	"tabBar": {
+		"color": "#7A7E83",
+		"selectedColor": "#007AFF",
+		"borderStyle": "black",
+		"backgroundColor": "#FFFFFF",
+		"list": [{
+				"pagePath": "pages/index/index",
+				"iconPath": "static/tabbar/list.png",
+				"selectedIconPath": "static/tabbar/list_active.png",
+				"text": "首页"
+			},
+			{
+				"pagePath": "pages/my/my",
+				"iconPath": "static/tabbar/me.png",
+				"selectedIconPath": "static/tabbar/me_active.png",
+				"text": "我的"
+			}
+		]
+	}
+}

+ 299 - 0
pages/apps/change-major/change-major.vue

@@ -0,0 +1,299 @@
+<template>
+	<view class="page-container">
+		<u-nav-bar title="专业分流志愿填报" bg="#007aff" ref="uNavBar" />
+		<view class="u-bg" />
+		<view class="main-content">
+			<u-panel v-if="form.notice">
+				<u-des-row label="注意事项" />
+				<view class="notice-tip" v-html="form.notice" />
+			</u-panel>
+			<u-panel title="个人信息">
+				<u-des-row label="学号" :value="userInfo.studentNO" border />
+				<u-des-row label="姓名" :value="userInfo.studentName" border />
+				<u-des-row label="班级" :value="userInfo.gradeName" border />
+				<u-des-row label="联系电话" eye>
+					<view slot="value" slot-scope="{isOpen}">
+						{{isOpen ? userInfo.mobileNo : hideCode(userInfo.mobileNo, 3,4) }}
+					</view>
+				</u-des-row>
+			</u-panel>
+			<u-panel title="专业志愿">
+				<u-picker v-model="form.majorId1st" label-key="majorName" value-key="majorId" :options="majorOptions"
+					@change="e=>pickerMajor(e, 'majorId1st')">
+					<u-des-row slot-scope="{data}" label="第一专业志愿" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" border />
+				</u-picker>
+				<u-picker v-model="form.majorId2nd" label-key="majorName" value-key="majorId" :options="majorOptions"
+					@change="e=>pickerMajor(e, 'majorId2nd')">
+					<u-des-row slot-scope="{data}" label="第二专业志愿" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" border />
+				</u-picker>
+				<u-picker v-model="form.majorId3rd" label-key="majorName" value-key="majorId" :options="majorOptions"
+					@change="e=>pickerMajor(e, 'majorId3rd')">
+					<u-des-row slot-scope="{data}" label="第三专业志愿" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" border />
+				</u-picker>
+				<u-picker v-model="form.majorId4th" label-key="majorName" value-key="majorId" :options="majorOptions"
+					@change="e=>pickerMajor(e, 'majorId4th')">
+					<u-des-row slot-scope="{data}" label="第四专业志愿" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" border />
+				</u-picker>
+				<u-picker v-model="form.majorId5th" label-key="majorName" value-key="majorId" :options="majorOptions"
+					@change="e=>pickerMajor(e, 'majorId5th')">
+					<u-des-row slot-scope="{data}" label="第五专业志愿" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" />
+				</u-picker>
+			</u-panel>
+			<u-panel>
+				<u-des-row label="学生签名" @click="visible=true">
+					<image slot="value" style="width: 34rpx;height: 30rpx;" src="/static/sign_icon.png"
+						mode="widthFix" />
+				</u-des-row>
+				<view v-if="form.studentSign" class="u-sign-preview">
+					<image :src="form.studentSign" style="width: 100%;height: 100%;" mode="widthFix"></image>
+					<view class="icon-clear u-flex-center" @click="form.studentSign=''">
+						<uni-icons type="closeempty" size="15" color="white" />
+					</view>
+				</view>
+				<view v-else class="sign-tip">填写完信息请点击签字笔完成签名哦~</view>
+			</u-panel>
+			<view class="u-flex" style="margin: 96rpx 0 32rpx;">
+				<view class="u-button save-button" :class="{'disabled':disabledSave}" @click="!disabledSave && onSumbit(1)">
+					暂存
+				</view>
+				<view style="width:20rpx">
+				</view>
+				<button class="u-button sumbit-button" :class="{'disabled':disabledSumbit}" @click="!disabledSumbit && onSumbit(2)">
+					{{form.applyStatus >= 1 ? '已提交' : '提交'}}
+				</button>
+			</view>
+		</view>
+		<u-drawer :visible.sync="visible" title="手写签名" @sumbit="signSumbit" @cancel="visible=false">
+			<view class="drawer-tip">
+				<text class="empty-2"></text>
+				<text>本人已仔细阅读专业分流并理解了有关要求和规定,在此我郑重承诺:“本人自愿按以上顺序选择专业,服从调剂,且专业分流后不再变更。
+					对违反以上承诺所造成的后果,本人自愿承担相应责任。”</text>
+			</view>
+			<u-sign ref="uSign" v-if="visible" />
+		</u-drawer>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+	import {
+		hideCode
+	} from '@/common'
+	import formChecker from '@/common/formChecker.js'
+	export default {
+		data() {
+			return {
+				form: {
+					notice: '',
+					majorId1st: '',
+					majorId2nd: '',
+					majorId3rd: '',
+					majorId4th: '',
+					majorId5th: '',
+
+					studentSign: '',
+					//
+					applyStatus: '',
+					majorApplyId: ''
+				},
+				visible: false, // 签名 抽屉
+				majorOptions: []
+			}
+		},
+		computed: {
+			...mapState({
+				userInfo: state => state.user.userInfo,
+			}),
+			disabledSave() {
+				return this.form.applyStatus >= 1 // 已提交
+			},
+			disabledSumbit() {
+				return this.form.applyStatus >= 1 || !this.form.majorApplyId // 已提交 或 未缓存
+			},
+		},
+		mounted() {
+			this.$ajax.get('/shunt/major-info').then(res => {
+				this.majorOptions = res.data.map(item => ({
+					majorId: item.majorId,
+					majorName: item.majorName,
+					disabled: false
+				}))
+				this.majorOptionsDisabled()
+			})
+			this.init()
+		},
+		methods: {
+			hideCode,
+			async init() {
+				// 志愿填报浏览
+				this.$ajax.post('/shunt/major-apply-view').then(res => {
+					Object.assign(this.form, res.data)
+				})
+			},
+			majorOptionsDisabled() {
+				const selectedList = []
+				Object.keys(this.form).forEach(key => {
+					key.indexOf('majorId') >= 0 && this.form[key] && selectedList.push(this.form[key])
+				})
+				this.majorOptions.forEach(element => {
+					element.disabled = selectedList.indexOf(element.majorId) >= 0
+				})
+				console.log(1111, selectedList, this.majorOptions)
+			},
+			pickerMajor(val, prop) {
+				console.log(222, val, prop)
+				Object.keys(this.form).forEach(key => {
+					if (key != prop && key.indexOf('majorId') >= 0 && this.form[key] === val) {
+						this.form[key] = ''
+					}
+				})
+				// this.form[majorKey] = this.majorOptions[index].label
+				// this[indexKey] = index
+				this.majorOptionsDisabled()
+			},
+			signSumbit() {
+				this.$refs.uSign.getBase64(res => {
+					this.form.studentSign = res?.tempFilePath || ''
+					this.visible = false
+				})
+			},
+			// api 
+			onSumbit(type) { // 1:暂存 2:提交
+				//定义表单规则
+				var rule = [{
+					name: "majorId1st",
+					checkType: "notnull",
+					errorMsg: "请输入第一专业志愿"
+				}, {
+					name: "majorId2nd",
+					checkType: "notnull",
+					errorMsg: "请输入第二专业志愿"
+				}, {
+					name: "majorId3rd",
+					checkType: "notnull",
+					errorMsg: "请输入第三专业志愿"
+				}, {
+					name: "majorId4th",
+					checkType: "notnull",
+					errorMsg: "请输入第四专业志愿"
+				}, {
+					name: "majorId5th",
+					checkType: "notnull",
+					errorMsg: "请输入第五专业志愿"
+				}, {
+					name: "studentSign",
+					checkType: "notnull",
+					errorMsg: "请填写签名"
+				}, ];
+				//进行表单检查
+				var checkRes = formChecker.check(this.form, rule);
+				if (!checkRes) {
+					uni.showToast({
+						title: formChecker.error,
+						icon: "none"
+					});
+					return
+				}
+				const that = this
+
+				uni.showLoading()
+				this.$ajax.post('/shunt/major-apply', this.form).then(res => {
+					if (type === 1) {
+						uni.showToast({
+							title: '暂存成功',
+							icon: "success"
+						});
+						that.init()
+					}
+					uni.hideLoading()
+				}).catch(error => {
+					uni.hideLoading()
+					uni.showModal({
+						title: '错误',
+						content: error.msg || error,
+						showCancel: false
+					})
+				})
+				if (type === 2) {
+					uni.showLoading()
+					this.$ajax.post('/shunt/major-apply-submit').then(res => {
+						uni.showToast({
+							title: '提交成功',
+							icon: "success"
+						});
+						that.init()
+						uni.hideLoading()
+					}).catch(error => {
+						uni.hideLoading()
+						uni.showModal({
+							title: '错误',
+							content: error.msg || error,
+							showCancel: false
+						})
+					})
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.page-container {
+		.u-bg {
+			height: 317rpx;
+			background: $uni-color-primary;
+		}
+
+		.main-content {
+			padding: 60rpx 32rpx 32rpx 32rpx;
+		}
+
+		.notice-tip {
+			font-size: 24rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 1.5;
+			padding-bottom: 32rpx;
+		}
+
+		.sign-tip {
+			height: 144rpx;
+			font-size: 20rpx;
+			font-weight: 400;
+			color: #A3ABBF;
+		}
+
+		.drawer-tip {
+			font-size: 24rpx;
+			font-weight: 400;
+			line-height: 1.5;
+			color: #999999;
+			margin-bottom: 36rpx;
+		}
+
+		.save-button,
+		.sumbit-button {
+			flex: 1;
+			height: 88rpx;
+			background: linear-gradient(90deg, #FFCF18 0%, #FFB300 100%);
+			border-radius: 8rpx;
+
+			&.disabled {
+				cursor: not-allowed;
+				opacity: 0.3;
+			}
+		}
+
+		.sumbit-button {
+			background: linear-gradient(90deg, #36ACFC 0%, #078EF7 100%);
+		}
+
+	}
+</style>

+ 151 - 0
pages/apps/my-grades/my-grades.vue

@@ -0,0 +1,151 @@
+<template>
+	<view class="page-container">
+		<u-nav-bar title="本人成绩" ></u-nav-bar>
+		<view class="u-bg"/>
+		<view class="main-content">
+			<view class="des">
+				<view style="margin-bottom: 12rpx;">您的成绩绩点 {{myGrades.avgScore.toFixed(2) || '--'}} 已超越了</view>
+				<view class="">
+					<text class="value">{{myGrades.avgScoreMaxRank || '--'}}</text> 位同学
+				</view>
+			</view>
+			<view class="statistics-item u-flex">
+				<view class="name-grade">
+					<view class="name">
+						{{myGrades.studentName}}
+					</view>
+					<view class="grade">
+						<!-- {{myGrades.genderId || '--'}} {{myGrades.majorName || '--'}} {{myGrades.gradeName}} -->
+						{{myGrades.gradeName || '--'}} 
+					</view>
+				</view>
+				<view class="avg-score">
+					平均绩点:<text class="value">{{myGrades.avgScore.toFixed(2)}}</text>
+				</view>
+
+				<view class="icon-container u-flex-center">
+					<view class="name">
+						{{ Math.min(myGrades.avgScoreRank, myGrades.avgScoreMaxRank) }}
+					</view>
+				</view>
+
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				myGrades: {},
+			}
+		},
+		mounted() {
+		
+			this.init()
+		},
+		methods: {
+			async init() {
+				uni.showLoading({
+					title: '加载中'
+				});
+				this.$ajax.post('/shunt/score').then(res => {
+					this.myGrades = res.data || {}
+				}).catch(error => {
+					this.myGrades = {}
+				}).finally(() => {
+					uni.hideLoading();
+				})
+			},
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$bg-height: 560rpx;
+	.page-container {
+		.u-bg {
+			height: $bg-height;
+			background: url('/static/my_grades_bg.png');
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			background-origin: content-box;
+		}
+		.main-content {
+			padding: 0 32rpx;
+		}
+		
+		.des {
+			margin-top: $bg-height - 112rpx - 230rpx - $u-nav-bar-height;
+			height: 112rpx;
+			font-size: 36rpx;
+			font-weight: 400;
+			color: #FFFFFF;
+			.value {
+				font-weight: bold;
+				font-size: 64rpx;
+				margin-right: 16rpx;
+			}
+		}
+
+		.statistics-item {
+			height: 230rpx;
+			background: #FFFFFF;
+			box-shadow: 0px 4px 22px 0px rgba(232, 237, 251, 0.8);
+			border-radius: 16px;
+			padding: 20rpx 20rpx 20rpx 126rpx;
+			position: relative;
+			transform: translateY(50%);
+
+			.name-grade {
+				.name {
+					font-size: 36rpx;
+					font-weight: bold;
+					color: #333333;
+					margin-bottom: 40rpx;
+				}
+
+				.grade {
+					font-size: 24rpx;
+
+					font-weight: bold;
+					color: #A3ABBF;
+				}
+			}
+
+			.avg-score {
+
+				font-size: 28rpx;
+
+				font-weight: 400;
+				color: #A3ABBF;
+
+				.value {
+					font-weight: bold;
+					color: #ED6E4E;
+				}
+			}
+			.icon-container {
+				position: absolute;
+				top: 1rpx;
+				left: 20rpx;
+				width: 74rpx;
+				height: 85rpx;
+				background: url('/static/achievement_label.png');
+				background-size: 100% 100%;
+				background-repeat: no-repeat;
+				background-position: center;
+				background-origin: content-box;
+
+				font-size: 32rpx;
+				font-weight: bold;
+				color: #FFFFFF;
+
+			}
+		}
+
+	}
+</style>

+ 222 - 0
pages/apps/progress-query/progress-query.vue

@@ -0,0 +1,222 @@
+<template>
+	<view class="page-container" :class="'check-type-' + detailObj.applyStatus">
+		<u-nav-bar title="进度查询"></u-nav-bar>
+		<view class="u-bg" />
+		<view class="main-content">
+			<template v-if="detailObj.applyStatus==1">
+				<image src="/static/shenhe-中.png" mode="" class="icon"></image>
+				<view class="des">等待审核</view>
+				<view class="tip">
+					<view style="margin-bottom: 15rpx;">审核通过后录取结果将在本页面显示</view>
+					<view>请耐心等待~</view>
+				</view>
+			</template>
+			<template v-else-if="detailObj.applyStatus==2">
+				<image src="/static/shenhe-成功.png" mode="" class="icon"></image>
+				<view class="des">审核通过</view>
+				<u-panel title="录取结果">
+					<u-des-row label="班级" :value="detailObj.gradeName" border />
+					<u-des-row label="学号" :value="detailObj.studentNO" border />
+					<u-des-row label="姓名" :value="detailObj.studentName" border />
+					<u-des-row v-if="queryType==1" label="是否通过" value="是" border />
+					<u-des-row v-else-if="queryType==2" label="是否需要调剂" :value="detailObj.test" border />
+					<u-des-row label="录取专业" :value="detailObj.majorName" />
+				</u-panel>
+			</template>
+			<template v-else-if="detailObj.applyStatus==3">
+				<image src="/static/shenhe-失败.png" mode="" class="icon"></image>
+				<view class="des">审核不通过</view>
+				<u-panel title="录取结果" theme="red">
+					<u-des-row label="班级" :value="detailObj.gradeName" border />
+					<u-des-row label="学号" :value="detailObj.studentNO" border />
+					<u-des-row label="姓名" :value="detailObj.studentName" border />
+					<u-des-row label="是否通过">
+						<view style="color: #EF6960;" slot="value">否</view>
+					</u-des-row>
+				</u-panel>
+			</template>
+
+			<template v-else>
+				<image src="/static/shenhe-中.png" mode="" class="icon"></image>
+				<view class="des">未提交</view>
+			</template>
+			<!-- test -->
+			<!-- <view style="text-align: left;">
+				<view v-for="(item, index) in applyStatusList" :key="item.value" @click="detailObj.applyStatus = item.value">
+					<radio :value="item.label" :checked="detailObj.applyStatus == item.value" />
+					{{item.label}} {{item.value}}
+				</view>
+			</view> -->
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				//
+				detailObj: {
+					applyStatus: 0,
+				},
+				queryType: 1, // 1:专业分流 2:特别申请  默认专业分流
+
+				applyStatusList: [{
+						value: 1,
+						label: '审核中'
+					},
+					{
+						value: 2,
+						label: '特别申请-审核成功通过'
+					},
+					// {
+					// 	value: 2,
+					// 	label: '专业分流-审核成功通过'
+					// },
+					{
+						value: 3,
+						label: '审核失败驳回'
+					},
+				],
+
+			}
+		},
+		onLoad(option) { //option为object类型,会序列化上个页面传递的参数
+			this.queryType = option.type==2?2:1
+			console.log(444, this.queryType, option); //打印出上个页面传递的参数。
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			async init() {
+				uni.showLoading({
+					title: '加载中'
+				});
+				
+			this.queryType===1	&& this.$ajax.post('/shunt/major-apply-result').then(res => {
+					Object.assign(this.detailObj, res.data || {})
+				}).catch(error => {}).finally(() => {
+					uni.hideLoading();
+				})
+			this.queryType===2	&& this.$ajax.post('/shunt/special-apply-result').then(res => {
+					Object.assign(this.detailObj, res.data || {})
+				}).catch(error => {}).finally(() => {
+					uni.hideLoading();
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.page-container {
+		.u-bg {
+			height: 603rpx;
+			background: url('/static/success_bg.png');
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			background-origin: content-box;
+		}
+
+		.main-content {
+			padding: 32rpx 32rpx 32rpx 32rpx;
+			text-align: center;
+		}
+
+		.icon {
+			width: 229rpx;
+			height: 214rpx;
+			margin-bottom: 26rpx;
+		}
+
+		.des {
+			font-size: 36rpx;
+			font-weight: 400;
+			color: #FFFFFF;
+			margin-bottom: 50rpx;
+		}
+
+		.res-container {
+			.content {
+				background: #FFFFFF;
+
+				box-shadow: 0px 4rpx 22rpx 0px rgba(232, 237, 251, 0.8);
+				border-radius: 6rpx;
+				padding: 32rpx;
+			}
+
+			.title {
+				padding: 0 32rpx 0 28rpx;
+				position: relative;
+				text-align: left;
+				font-size: 30rpx;
+
+				font-weight: bold;
+				color: #078EF7;
+				margin-bottom: 24rpx;
+
+				.icon {
+					width: 7rpx;
+					height: 30rpx;
+					position: absolute;
+					left: 0;
+					top: 50%;
+					transform: translateY(-50%);
+				}
+			}
+
+
+
+			.list-item {
+				height: 100rpx;
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+
+				font-size: 28rpx;
+
+				font-weight: 400;
+				color: #333333;
+				border-bottom: 1px solid #F5F7FA;
+
+				&:nth-last-child(1) {
+					border-bottom: none;
+				}
+
+				.label {
+					font-weight: bold;
+				}
+
+			}
+		}
+
+		.tip {
+			font-size: 28rpx;
+			font-weight: 400;
+			color: #A3ABBF;
+			margin-top: 160rpx;
+		}
+	}
+
+	.check-type-1 {
+		.u-bg {
+			background: url('/static/bg_plan.png');
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			background-origin: content-box;
+		}
+	}
+
+	.check-type-3 {
+		.u-bg {
+			background: url('/static/bg_fail.png');
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			background-origin: content-box;
+		}
+	}
+</style>

+ 201 - 0
pages/apps/shunt-places/shunt-places.vue

@@ -0,0 +1,201 @@
+<template>
+	<view class="page-container">
+		<u-nav-bar title="专业分流名额" />
+		<view class="u-bg" />
+		<view class="main-content">
+			<view class="statistics-container u-flex">
+				<view v-for="(item,index) in statisticsList" :key="index" class="statistics-item u-flex-column">
+					<view class="icon-container">
+						<image :src="item.icon" mode="widthFix" class="icon"></image>
+					</view>
+					<view class="value">{{item.value}}</view>
+					<view class="label">{{item.label}}</view>
+				</view>
+			</view>
+			<view class="rank-container">
+				<view v-for="(item,index) in rankList" :key="index" class="rank-item u-flex">
+					<view class="rank">{{index+1}}</view>
+					<view class="label">{{item.majorName}}</view>
+					<view class="value">{{item.sinUpStudentsNum}}/{{item.totalStudentsNum}}</view>
+				</view>
+			</view>
+			<view class="tip">
+				注:各专业报名人数为第一志愿已报人数,正式报名阶段每小时 更新一次~
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				statisticsList: [{
+						prop: 'totalStudents',
+						value: 0,
+						label: '学生总人数',
+						icon: '/static/num_icon.png'
+					},
+					{
+						prop: 'sinUpStudents',
+						value: 0,
+						label: '已报人数',
+						icon: '/static/choose_icon.png'
+					},
+					{
+						prop: 'unSinUpStudents',
+						value: 0,
+						label: '未报名人数',
+						icon: '/static/no_choose_icon.png'
+					},
+
+				],
+				rankList: []
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			async init() {
+				uni.showLoading({
+					title: '加载中'
+				});
+				this.$ajax.post('/shunt/quota').then(res => {
+					const data = res.data || {}
+					this.statisticsList.forEach(element => {
+						element.value = data[element.prop]
+					})
+					this.rankList = data.majorShuntCountVOList || []
+				}).catch(error => {
+					this.statisticsList.forEach(element => {
+						element.value = 0
+					})
+					this.rankList = []
+				}).finally(() => {
+					console.log(this.statisticsList)
+					uni.hideLoading();
+				})
+			},
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$bg-height: 317rpx;
+	.page-container {
+		.u-bg {
+			height: $bg-height;
+			background: $uni-color-primary;
+		}
+
+		.main-content {
+			padding: 0 32rpx;
+		}
+
+		.statistics-container {
+			margin: $bg-height - $u-nav-bar-height 0 0 0;
+			transform: translateY(-50%);
+
+			.statistics-item {
+				height: 170rpx;
+				flex: 1;
+				background: #FFFFFF;
+				box-shadow: 0px 4px 22px 0px rgba(232, 237, 251, 0.8);
+				border-radius: 16px;
+				padding: 16rpx 20rpx;
+
+				&:nth-child(2) {
+					margin: 0 24rpx;
+				}
+
+				.value {
+					font-size: 43rpx;
+
+					font-weight: bold;
+					color: #333333;
+					width: 100%;
+				}
+
+				.label {
+					font-size: 24rpx;
+
+					font-weight: 400;
+					color: #333333;
+					width: 100%;
+				}
+
+				.icon {
+					width: 44rpx;
+					height: 44rpx;
+				}
+
+				.icon-container {
+					width: 100%;
+					text-align: right;
+				}
+			}
+
+		}
+
+		.rank-container {
+			.rank-item {
+				margin-bottom: 60rpx;
+
+				.rank {
+					font-size: 41rpx;
+
+					font-weight: bold;
+					color: #A3ABBF;
+					margin-right: 30rpx;
+
+
+				}
+
+				&:nth-child(1) {
+					.rank {
+						color: #FFB300;
+					}
+				}
+
+				&:nth-child(2) {
+					.rank {
+						color: #ED6E4E;
+					}
+				}
+
+				&:nth-child(3) {
+					.rank {
+						color: #078EF7;
+					}
+				}
+
+				.label {
+
+					font-size: 32rpx;
+
+					font-weight: bold;
+					color: #333333;
+					flex: 1;
+				}
+
+				.value {
+					font-size: 24rpx;
+
+					font-weight: 400;
+					color: #333333;
+
+				}
+			}
+		}
+
+		.tip {
+			font-size: 24rpx;
+			font-weight: 400;
+			color: #FFB300;
+			line-height: 1.5;
+
+		}
+	}
+</style>

+ 373 - 0
pages/apps/special-application/special-application.vue

@@ -0,0 +1,373 @@
+<template>
+	<view class="page-container ">
+		<u-nav-bar title="特别申请" bg="#007aff" ref="uNavBar" />
+		<view class="u-bg" />
+		<view class="u-disabled-mask" v-if="false" />
+		<view class="main-content">
+			<u-panel v-if="form.notice">
+				<u-des-row label="注意事项" />
+				<view class="notice-tip" v-html="form.notice" />
+			</u-panel>
+			<u-panel>
+				<u-des-row label="姓名" :value="userInfo.studentName" border />
+				<u-des-row label="性别" :value="userInfo.genderName" border />
+				<u-des-row label="班级" :value="userInfo.gradeName" border />
+				<u-des-row label="学号" :value="userInfo.studentNO" border />
+				<u-des-row label="联系电话" eye>
+					<view slot="value" slot-scope="{isOpen}">
+						{{isOpen ? userInfo.mobileNo : hideCode(userInfo.mobileNo, 3,4) }}
+					</view>
+				</u-des-row>
+			</u-panel>
+			<u-panel>
+				<u-des-row label="绩点" :value="form.avgScore.toFixed(2)" border />
+				<u-des-row label="专业群排名" :value="form.avgScoreRank" border />
+				<u-picker v-model="form.majorId" label-key="majorName" value-key="majorId" :options="majorOptions">
+					<u-des-row slot-scope="{data}" label="申请专业" :value="data.majorName || '请选择'"
+						:style="{opacity: data.majorName?1:0.5}" border />
+				</u-picker>
+				<u-des-row label="申请原因" />
+				<uni-easyinput type="textarea" autoHeight  :maxlength="300" v-model="form.applyReason" placeholder="请输入详细的申请原因哦(最多300字)~">
+				</uni-easyinput>
+			</u-panel>
+			<u-panel style="padding-top:24rpx;">
+				<!-- <u-des-row label="附件" :value="form.attachmentList.length + '/9'" /> -->
+				<uni-file-picker v-model="form.attachmentList" @select="imgSelect" fileMediatype="image"
+					file-extname="png,jpg" :auto-upload="false" mode="grid" :limit="9" title="附件">
+					<view class="choose-image u-flex-center">
+						<image src="/static/upload_icon.png" style="width: 70rpx;height: 60rpx;" mode=""></image>
+						<view class="choose-image-tip">上传图片</view>
+					</view>
+				</uni-file-picker>
+			</u-panel>
+			<u-panel>
+				<u-des-row label="学生签名" @click="toSign(1)">
+					<image slot="value" style="width: 34rpx;height: 30rpx;" src="/static/sign_icon.png"
+						mode="widthFix" />
+				</u-des-row>
+				<view v-if="form.studentSign" class="u-sign-preview">
+					<image :src="form.studentSign" style="width: 100%;height: 100%;" mode="widthFix"></image>
+					<view class="icon-clear u-flex-center" @click="form.studentSign=''">
+						<uni-icons type="closeempty" size="15" color="white" />
+					</view>
+				</view>
+				<view v-else class="sign-tip">填写完信息请点击签字笔完成签名哦~</view>
+			</u-panel>
+			<view class="u-flex" style="margin: 96rpx 0 32rpx;">
+				<view class="u-button save-button" :class="{'disabled':disabledSave}" @click="!disabledSave && onSumbit(1)">
+					暂存
+				</view>
+				<view style="width:20rpx">
+				</view>
+				<button class="u-button sumbit-button" :class="{'disabled':disabledSumbit}" @click="!disabledSumbit && onSumbit(2)">
+					{{form.applyStatus >= 1 ? '已提交' : '提交'}}
+				</button>
+			</view>
+		</view>
+		<u-drawer :visible.sync="visible" title="手写签名" @sumbit="signSumbit" @cancel="visible=false">
+			<view v-if="false" class="drawer-tip">
+				<text class="empty-2"></text>
+				<text>本人已仔细阅读专业分流并理解了有关要求和规定,在此我郑重承诺:“本人自愿按以上顺序选择专业,服从调剂,且专业分流后不再变更。
+					对违反以上承诺所造成的后果,本人自愿承担相应责任。”</text>
+			</view>
+			<u-sign ref="uSign" v-if="visible" />
+		</u-drawer>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+	import {
+		hideCode
+	} from '@/common'
+	import formChecker from '@/common/formChecker.js'
+	export default {
+		data() {
+			return {
+				majorOptions: [], // majorId majorName
+				form: {
+					notice: '',
+					majorId: '',
+					majorName: '',
+					applyReason: '',
+					attachmentList: [],
+					studentSign: '',
+					specialApplyId: '',
+					applyStatus: '', //0未提交 1已提交 2通过 3驳回
+				},
+				signType: '',
+				visible: false // 签名 抽屉
+			}
+		},
+		computed: {
+			...mapState({
+				userInfo: state => state.user.userInfo,
+			}),
+			disabledSave() {
+				return this.form.applyStatus >= 1 // 已提交
+			},
+			disabledSumbit() {
+				return this.form.applyStatus >= 1 || !this.form.specialApplyId // 已提交 或 未缓存
+			}
+		},
+		mounted() {
+			this.$ajax.get('/shunt/major-info').then(res => {
+				this.majorOptions = res.data || []
+			})
+			this.init()
+		},
+		methods: {
+			hideCode,
+			async init() {
+				// 特别申请浏览
+				this.$ajax.post('/shunt/special-apply-view').then(res => {
+					let attachmentList = res.data.attachmentList || []
+					attachmentList = attachmentList.map(item => {
+						return {
+							path: item,
+							url: item,
+						}
+					})
+					Object.assign(this.form, res.data, {
+						attachmentList
+					})
+					console.log(111, this.form)
+				})
+			},
+			imgSelect(res) {
+				let error2M = 0
+				const newImgs = res.tempFiles.filter(file => {
+					const isLt2M = file.size / 1024 / 1024 < 2 
+					if(!isLt2M) {
+						error2M++
+					}
+					return isLt2M
+				})
+				if (error2M) {
+					uni.showToast({
+						title: `当前选择了${res.tempFiles.length}个文件,${error2M}个文件大于2M`,
+						icon: "none"
+					});
+				}
+				console.log(111, newImgs)
+				this.form.attachmentList = this.form.attachmentList.concat(newImgs)
+			},
+			toSign(type) { // 1学生 2家长
+				this.signType = type
+				this.visible = true
+			},
+			signSumbit() {
+				this.$refs.uSign.getBase64(res => {
+					if (this.signType === 1) {
+						this.form.studentSign = res?.tempFilePath || ''
+					} else {
+						this.form.test7 = res?.tempFilePath || ''
+					}
+					this.visible = false
+				})
+			},
+			// api 
+			async onSumbit(type) { // 1:暂存 2:提交
+				//定义表单规则
+				var rule = [
+					// 	{
+					// 	name: "avgScore",
+					// 	checkType: "notnull",
+					// 	errorMsg: "请输入绩点"
+					// }, {
+					// 	name: "avgScoreRank",
+					// 	checkType: "notnull",
+					// 	errorMsg: "请输入专业群排名"
+					// }, 
+					// 
+					{
+						name: "majorId",
+						checkType: "notnull",
+						errorMsg: "请输入申请专业"
+					}, {
+						name: "applyReason",
+						checkType: "notnull",
+						errorMsg: "请输入申请原因"
+					}, {
+						name: "attachmentList",
+						checkType: "notnull",
+						errorMsg: "请添加附件"
+					}, {
+						name: "studentSign",
+						checkType: "notnull",
+						errorMsg: "请填写学生签名"
+					}
+				];
+
+				//进行表单检查
+				var checkRes = formChecker.check(this.form, rule);
+				if (!checkRes) {
+					uni.showToast({
+						title: formChecker.error,
+						icon: "none"
+					});
+					return
+				}
+				await this.uploadFiles()
+				const that = this
+				uni.showLoading()
+				const params = {
+					...that.form
+				}
+				params.attachmentList = params.attachmentList.map(item => item.path)
+				that.$ajax.post('/shunt/special-apply', params).then(res => {
+					if (type === 1) {
+						uni.showToast({
+							title: '暂存成功',
+							icon: "success"
+						});
+						that.init()
+					}
+					uni.hideLoading()
+				}).catch(error => {
+					uni.hideLoading()
+					uni.showModal({
+						title: '错误',
+						content: error.msg || error,
+						showCancel: false
+					})
+				})
+				if (type === 2) {
+					uni.showLoading()
+					that.$ajax.post('/shunt/special-apply-submit').then(res => {
+						uni.showToast({
+							title: '提交成功',
+							icon: "success"
+						});
+						that.init()
+						uni.hideLoading()
+					}).catch(error => {
+						uni.hideLoading()
+						uni.showModal({
+							title: '错误',
+								content: error.msg || error,
+							showCancel: false
+						})
+					})
+				}
+
+			},
+			async uploadFiles(success) {
+				uni.showLoading({
+					title: '上传文件中...'
+				});
+				const promiseList = []
+				const indexList = []
+				const that = this
+				that.form.attachmentList.forEach((element, index) => {
+					if (element.status) {
+						indexList.push(index)
+						promiseList.push(uni.uploadFile({
+							url: `/shunt/upload`,
+							filePath: element.url,
+							// fileType: 'image',
+							name: 'file',
+							header: {
+								token: uni.getStorageSync('TOKEN')
+							},
+							formData: {
+								// file: res.tempFiles[0],
+								fileType: 'APPLY',
+							},
+							// success(uploadFileRes) {
+							// 	const data = JSON.parse(uploadFileRes.data)
+							// 	const filePath = data.data.filePath
+							// 	console.log(1111, filePath)
+							// 	that.form.attachmentList[index] = filePath
+							// }
+						}))
+					}
+				})
+				await Promise.all(promiseList).then(resList => {
+					resList.forEach((element, index) => {
+						let uploadFileRes = element[1]
+						const data = JSON.parse(uploadFileRes.data)
+						const filePath = data.data.filePath
+						const formIndex = indexList[index]
+						that.form.attachmentList[formIndex].path = filePath
+					})
+				})
+				uni.hideLoading()
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.page-container {
+		.u-bg {
+			height: 317rpx;
+			background: $uni-color-primary;
+		}
+
+		.main-content {
+			padding: 60rpx 32rpx 32rpx 32rpx;
+		}
+
+		.notice-tip {
+			font-size: 24rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 1.5;
+			padding-bottom: 32rpx;
+		}
+
+		.sign-tip {
+			height: 144rpx;
+			font-size: 20rpx;
+			font-weight: 400;
+			color: #A3ABBF;
+		}
+
+		.drawer-tip {
+			font-size: 24rpx;
+			font-weight: 400;
+			line-height: 1.5;
+			color: #999999;
+			margin-bottom: 36rpx;
+		}
+
+		.save-button,
+		.sumbit-button {
+			flex: 1;
+			height: 88rpx;
+			background: linear-gradient(90deg, #FFCF18 0%, #FFB300 100%);
+			border-radius: 8rpx;
+
+			&.disabled {
+				cursor: not-allowed;
+				opacity: 0.3;
+			}
+		}
+
+		.sumbit-button {
+			background: linear-gradient(90deg, #36ACFC 0%, #078EF7 100%);
+		}
+
+		.choose-image {
+			width: 100%;
+			height: 100%;
+			background: #FFFFFF;
+			cursor: pointer;
+			text-align: center;
+			border: 1rpx dashed #A3ABBF;
+			flex-direction: column;
+
+			.choose-image-tip {
+				font-size: 24rpx;
+				font-weight: 400;
+				color: #078EF7;
+				margin-top: 24rpx;
+			}
+
+		}
+
+	}
+</style>

+ 15 - 0
pages/apps/test/test.vue

@@ -0,0 +1,15 @@
+<template>
+	<u-sign>
+
+	</u-sign>
+</template>
+
+<script>
+	export default {
+		
+	}
+</script>
+
+<style scoped>
+
+</style>

+ 39 - 0
pages/common/webview/webview.vue

@@ -0,0 +1,39 @@
+<template>
+	<view>
+		<web-view v-if="url" :src="url"></web-view>
+	</view>
+</template>
+
+<script>
+	export default {
+		onLoad({url='',title}) {
+			if(url.substring(0, 4) != 'http'){
+				uni.showModal({
+					title:"错误",
+					content: '不是一个有效的网站链接,'+'"'+url+'"',
+					showCancel: false,
+					confirmText:"知道了",
+					complete: () => {
+						uni.navigateBack()
+					}
+				});
+				title = "页面路径错误"
+			}else{
+				console.log(url,title);
+				this.url = url;
+			}
+			if(title){
+				uni.setNavigationBarTitle({title});
+			}
+		},
+		data() {
+			return {
+				url:null
+			};
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

+ 6 - 0
pages/error/403/403.vue

@@ -0,0 +1,6 @@
+<template>
+	<view style="text-align: center;padding: 80rpx;">
+		<image src="/static/403.png" mode="widthFix" style="width: 80%;"></image>
+	</view>
+</template>
+

+ 92 - 0
pages/index/components/apps.vue

@@ -0,0 +1,92 @@
+<template>
+	<view style="padding: 0 32rpx">
+		<u-class title="专业分流">
+			<view class="content">
+				<view v-for="(item,index) in appList" :key="index" class="app-item" @click="onClick(item)">
+					<image class="icon" :src="item.icon" />
+					<view class="label">
+						{{item.label}}
+					</view>
+				</view>
+			</view>
+		</u-class>
+	</view>
+</template>
+<script>
+	export default {
+		data() {
+			return {
+				appList: [{
+						label: '分流名额',
+						icon: '/static/index/diversion_quota_icon.png',
+						url: '/pages/apps/shunt-places/shunt-places'
+					},
+					{
+						label: '本人成绩',
+						icon: '/static/index/my_grades_icon.png',
+						url: '/pages/apps/my-grades/my-grades'
+					},
+					{
+						label: '特别申请',
+						icon: '/static/index/special_icon.png',
+						url: '/pages/apps/special-application/special-application'
+					},
+					{
+						label: '专业分流',
+						icon: '/static/index/diversion_icon.png',
+						url: '/pages/apps/change-major/change-major'
+					},
+					{
+						label: '进度查询',
+						icon: '/static/index/progress_query_icon.png',
+						url: '/pages/apps/progress-query/progress-query'
+					},
+					// {
+					// 	label: 'test',
+					// 	icon: '/static/index/progress_query_icon.png',
+					// 	url: '/pages/apps/test/test'
+					// }
+				]
+			}
+		},
+		methods: {
+			onClick(item) {
+				console.log(item);
+				item.url && uni.navigateTo({
+					url: item.url
+				})
+			},
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.content {
+		display: flex;
+		flex-wrap: wrap;
+		justify-content: space-between;
+		flex-direction: row;
+
+		.app-item {
+			display: flex;
+			align-items: center;
+			flex-direction: column;
+			font-size: 29rpx;
+			background: white;
+
+			font-weight: 400;
+			color: #333333;
+			cursor: pointer;
+			padding: 16rpx 0;
+			&:hover {
+				opacity: 0.8;
+			}
+
+			.icon {
+				width: 142rpx;
+				height: 142rpx;
+			}
+		}
+	}
+</style>

+ 182 - 0
pages/index/components/schedule.vue

@@ -0,0 +1,182 @@
+<template>
+	<view style="padding: 0 32rpx">
+		<u-class title="日程安排">
+			<view v-if="list.length" class="timeline">
+				<view v-for="(item,index) in list" :key="index" class="item-container">
+					<view class="item-main u-flex" @click="toQuery(item)">
+						<view class="left">
+							<view class="left-label">{{item.labelTitle}}</view>
+							<view class="left-time">{{item.labelStartDate}} ~ {{item.labelEndDate}}</view>
+						</view>
+						<!-- labelStatus	日程状态:-1:已结束,0-未开始,1-进行中 -->
+						<view class="right-label">
+							<image v-if="item.labelStatus==1" class="right-icon" src="/static/index/into_icon.png" />
+							<view>{{item.labeltatusName ||item.labelStatusName || '--'}}</view>
+						
+						</view>
+					</view>
+					<view class="line" />
+					<image v-if="item.labelStatus==-1" class="head-icon" src="/static/index/end_state_choose.png" />
+					<image v-else-if="item.labelStatus==1" class="head-icon"
+						src="/static/index/start_state_choose.png" />
+					<image v-else class="head-icon not_state" src="/static/index/end_state_choose.png" />
+				</view>
+			</view>
+			<view v-else class="no-data">
+				<image class="icon" src="/static/no_list.png" mode="widthFix"></image>
+				<view class="tip">暂无日程安排哦~</view>
+			</view>
+		</u-class>
+	</view>
+</template>
+<script>
+	export default {
+		data() {
+			return {
+				activeIndex: 1,
+				list: [],
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			async init() {
+				this.$ajax.post('/shunt/schedule').then(res => {
+					this.list = res.data || []
+				}).catch(error => {
+					this.list = []
+				})
+			},
+
+			toQuery(item) {
+				const {
+					path,
+					labelStatus,
+				} = item || {}
+				if(labelStatus!==1) {
+					console.log(item)
+					return 
+				}
+				if (path && path.indexOf('/')>=0) {
+					['/', '/pages/index/index', '/pages/my/my'].indexOf(path) >= 0 ?
+						uni.switchTab({
+							url: path
+						}) :
+						uni.navigateTo({
+							url: path // `/pages/apps/progress-query/progress-query`
+						})
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.timeline {
+		max-width: 400px;
+		margin: 0 auto;
+
+		.item-container {
+			padding-left: 56rpx;
+			position: relative;
+
+			.item-main {
+				width: 100%;
+				height: 209rpx;
+				background: url('/static/schedule_list_bg.png');
+				background-size: 100% 100%;
+				background-repeat: no-repeat;
+				background-position: right center;
+				background-origin: content-box;
+			}
+
+			.left {
+				padding-left: 60rpx;
+			}
+
+			.left-label {
+				font-size: 30rpx;
+				font-weight: bold;
+				color: #333333;
+				margin-bottom: 36rpx;
+			}
+
+			.left-time {
+				font-size: 24rpx;
+				font-weight: 400;
+				color: #A3ABBF;
+			}
+
+			.right-label {
+				font-size: 28rpx;
+				font-weight: 400;
+				color: #FFFFFF;
+				text-align: center;
+				padding-right: 60rpx;
+			}
+
+			.right-icon {
+				cursor: pointer;
+				width: 52rpx;
+				height: 52rpx;
+				margin-bottom: 22rpx;
+			}
+
+			.line {
+				position: absolute;
+				top: 0;
+				left: 16rpx;
+				width: 6rpx;
+				height: 100%;
+				background: #D1E3FA;
+				transform: translateX(-50%);
+			}
+
+			&:nth-child(1) {
+				.line {
+					top: 50%;
+					height: 50%;
+				}
+			}
+
+			&:nth-last-child(1) {
+				.line {
+					height: 50%;
+				}
+			}
+
+			.head-icon {
+				position: absolute;
+				top: 50%;
+				left: 0;
+				transform: translateY(-50%);
+				z-index: 1;
+				width: 32rpx;
+				height: 32rpx;
+
+				&.not_state {
+					width: 22rpx;
+					height: 22rpx;
+					left: 5rpx
+				}
+			}
+
+		}
+	}
+
+	.no-data {
+		text-align: center;
+
+		.icon {
+			width: 183rpx;
+			height: 167rpx;
+			margin-top: 80rpx;
+			margin-bottom: 40rpx;
+		}
+
+		font-size: 20rpx;
+		font-weight: 400;
+		color: #A3ABBF;
+	}
+</style>

+ 175 - 0
pages/index/components/top-panel.vue

@@ -0,0 +1,175 @@
+<template>
+	<view class="page-container" :class="{'is-apply': isApply}">
+		<view class="u-bg" />
+		<view class="main-content">
+			<view class="title">
+				高校学院
+			</view>
+			<view v-if="isApply" class="special-apply-result">
+				<view class="u-flex" style="margin-bottom: 54rpx;">
+					<view class="apply-title">特别申请结果查询</view>
+					<view class="apply-status">{{applyObj.applyStatusName}}</view>
+				</view>
+				<view class="daterange u-flex" style="margin-bottom: 24rpx;">
+					<image src="/static/time_icon.png" style="width: 32rpx;height: 32rpx;margin-right: 16rpx;" mode="">
+					</image>
+					<view style="flex: 1;">{{applyObj.time || '22-04-20 ~ 22- 04-30'}}</view>
+				</view>
+				<view class="u-flex">
+					<view class="">
+
+					</view>
+					<view class="u-button ">立即查询</view>
+				</view>
+			</view>
+
+			<swiper v-else class="swiper" indicator-active-color="#007aff" circular :indicator-dots="true" :autoplay="true" :interval="3000"
+				:duration="500">
+				<swiper-item @click="change(item)" v-for="(item,index) in list" :key="index">
+					<image :src="item.bannerImageUrl" style="width:100%" mode="widthFix"></image>
+				</swiper-item>
+			</swiper>
+		</view>
+	</view>
+</template>
+<script>
+	export default {
+		data() {
+			return {
+				list: [],
+				applyObj: {},
+			}
+		},
+		computed: {
+			isApply() {
+				return this.applyObj.applyStatus >= 1 && false // 不需要自定义
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			async init() {
+				this.$ajax.post('/shunt/banner').then(res => {
+					this.list = res.data || []
+				}).catch(error => {
+					this.list = []
+				})
+				// 
+				this.$ajax.post('/shunt/special-apply-result').then(res => {
+					this.applyObj = res.data || {}
+				}).catch(error => {
+					this.applyObj = {}
+				})
+			},
+			change(item) {
+				const {
+					jumType,
+					jumUrl,
+					bannerTitle = '123'
+				} = item || {}
+				console.log(jumType, jumUrl);
+				//0-无跳转,1-内部跳转体质,2-http跳转地址
+				if (jumType == 1 && jumUrl) { // 
+					['/', '/pages/index/index', '/pages/my/my'].indexOf(jumUrl) >= 0 ?
+						uni.switchTab({
+							url: jumUrl
+						}) :
+						uni.navigateTo({
+							url: jumUrl
+						})
+				} else if (jumType == 2 && jumUrl) {
+					console.log(111, jumUrl)
+					uni.navigateTo({
+						url: `/pages/common/webview/webview?url=${jumUrl}&title=${bannerTitle}`,
+					});
+				}
+			},
+		},
+
+	}
+</script>
+
+<style scoped lang='scss'>
+	.u-bg {
+		display: none;
+		height: 353rpx;
+		background: url('/static/top_panel_bg.png');
+		background-size: 100% 100%;
+		background-repeat: no-repeat;
+		background-position: center;
+		background-origin: content-box;
+	}
+
+	.main-content {
+		padding: 46rpx 32rpx 72rpx 32rpx;
+
+		.title {
+			font-size: 40rpx;
+			font-weight: bold;
+
+			color: #333333;
+			margin-bottom: 52rpx;
+		}
+
+		.special-apply-result {
+			background: #FFFFFF;
+			box-shadow: 0px 4rpx 22rpx 0rpx rgba(232, 237, 251, 0.8);
+			border-radius: 16rpx;
+			padding: 32rpx;
+
+			.apply-title {
+				font-size: 36rpx;
+				font-weight: bold;
+				color: #333333;
+			}
+
+			.apply-status {
+				font-size: 28rpx;
+				font-weight: bold;
+				color: #FFB300;
+			}
+
+			.daterange {
+				font-size: 28rpx;
+				font-weight: bold;
+				color: #078EF7;
+			}
+
+			.u-button {
+				background: rgba($color: #078EF7, $alpha: .18);
+				font-size: 28rpx;
+				font-weight: bold;
+				color: #078EF7;
+				height: 84rpx;
+				border-radius: 40rpx;
+				width: 250rpx;
+
+			}
+		}
+	}
+
+
+
+	.swiper {
+		height: 300rpx;
+	}
+
+	.swiper-item {
+		display: block;
+		height: 300rpx;
+		line-height: 300rpx;
+		text-align: center;
+		border-radius: 12rpx;
+	}
+
+	.is-apply {
+		.u-bg {
+			display: inherit;
+		}
+
+		.title {
+			color: #FFFFFF;
+		}
+	}
+</style>

+ 34 - 0
pages/index/index.vue

@@ -0,0 +1,34 @@
+<template>
+	<view v-if="token" class="page-container">
+		<topPanel />
+		<apps />
+		<schedule />
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+	import topPanel from './components/top-panel.vue';
+	import apps from './components/apps.vue';
+	import schedule from './components/schedule.vue';
+	export default {
+		components: {
+			topPanel,
+			apps,
+			schedule,
+		},
+		computed: {
+			...mapState({
+				token: state => state.user.token,
+			})
+		},
+	}
+</script>
+<style scoped lang='scss'>
+	.page-container {
+		/* background: white;
+		padding: 0 32rpx; */
+	}
+</style>

+ 115 - 0
pages/my/my.vue

@@ -0,0 +1,115 @@
+<template>
+	<view class="page-container">
+		<view class="bg" />
+		<view class="main-content">
+			<image :src="userInfo.avatar" class="avatar" mode=""></image>
+			<view class="studentName">
+				{{userInfo.studentName}}
+			</view>
+			<view class="studentNO">
+				学号:{{userInfo.studentNO}}
+			</view>
+			<view class="majorName">
+				班级:{{userInfo.majorName}} {{userInfo.gradeName}}
+			</view>
+			<u-panel class="list-container">
+				<u-des-row label="个人信息" forward @click="toUserInfo">
+					<image slot="left" style="width: 44rpx;height: 44rpx;margin-right: 40rpx;"
+						src="/static/menber_info_icon.png" mode="widthFix"></image>
+				</u-des-row>
+				<u-des-row label="问题反馈" forward @click="toProblemFeedback">
+					<div slot="left" style="width: 44rpx;height: 44rpx;margin-right: 40rpx;"
+						src="/static/menber_info_icon.png" mode="widthFix"></div>
+				</u-des-row>
+			</u-panel>
+		</view>
+	</view>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+	export default {
+		computed: {
+			...mapState({
+				userInfo: state => state.user.userInfo,
+			})
+		},
+		methods: {
+			toUserInfo() {
+				uni.navigateTo({
+					url: '/pages/my/user-info/user-info'
+				})
+			},
+			toProblemFeedback() {
+				uni.navigateTo({
+					url: '/pages/my/problem-feedback/problem-feedback'
+				})
+			},
+		},
+		onShow: function() {
+			 this.$store.dispatch('user/updateUserInfo')
+		},
+	}
+</script>
+<style lang="scss" scoped>
+	.page-container {
+		.bg {
+			width: 100%;
+			height: 885rpx;
+			background: url('/static/bg_icon.png');
+			background-size: 100% 100%;
+			background-repeat: no-repeat;
+			background-position: center;
+			background-origin: content-box;
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			z-index: -1;
+		}
+
+		.main-content {
+			padding: 182rpx 0 0 0;
+			text-align: center;
+
+			.avatar {
+				width: 149rpx;
+				height: 149rpx;
+				background: white;
+				border-radius: 100%;
+				margin-bottom: 50rpx;
+			}
+
+			.studentName {
+				font-size: 49rpx;
+				font-weight: bold;
+				color: #FFFFFF;
+				margin-bottom: 40rpx;
+			}
+
+			.studentNO,
+			.majorName {
+				font-size: 28rpx;
+				font-weight: bold;
+				color: #FFFFFF;
+				margin-bottom: 40rpx;
+			}
+
+			.list-container {
+				position: absolute;
+				top: 828rpx;
+				left: 0;
+				right: 0;
+				bottom: 0;
+				background: white;
+				border-radius: 26rpx 26rpx 0 0;
+				padding-top: 48rpx;
+				margin: 0;
+			}
+		}
+
+
+	}
+</style>

+ 202 - 0
pages/my/problem-feedback/problem-feedback.vue

@@ -0,0 +1,202 @@
+<template>
+	<view class="page-container ">
+		<u-nav-bar title="问题反馈" color="#333" bg="white" ref="uNavBar"></u-nav-bar>
+		<view class="main-content">
+			<u-des-row label="问题描述" />
+			<uni-easyinput type="textarea" :maxlength="300" autoHeight v-model="form.content" placeholder="请输入问题描述(最多300字)">
+			</uni-easyinput>
+			<!-- <u-des-row label="附件" :value="form.attachmentList.length + '/9'" /> -->
+			<uni-file-picker v-model="form.attachmentList" @select="imgSelect" fileMediatype="image" style="margin-top: 24rpx;"
+				file-extname="png,jpg" :auto-upload="false" mode="grid" :limit="9" title="附件">
+				<view class="choose-image u-flex-center">
+					<image src="/static/upload_icon.png" style="width: 70rpx;height: 60rpx;" mode=""></image>
+					<view class="choose-image-tip">上传图片</view>
+				</view>
+			</uni-file-picker>
+			<view class="u-flex" style="margin-top: 56rpx;">
+				<button class="u-button sumbit-button" @click="onSumbit">
+					提交
+				</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import formChecker from '@/common/formChecker.js'
+	export default {
+		data() {
+			return {
+				form: {
+					content: '',
+					attachmentList: [],
+				},
+			}
+		},
+		mounted() {},
+		methods: {
+			imgSelect(res) {
+				let error2M = 0
+				const newImgs = res.tempFiles.filter(file => {
+					const isLt2M = file.size / 1024 / 1024 < 2
+					if (!isLt2M) {
+						error2M++
+					}
+					return isLt2M
+				})
+				if (error2M) {
+					uni.showToast({
+						title: `当前选择了${res.tempFiles.length}个文件,${error2M}个文件大于2M`,
+						icon: "none"
+					});
+				}
+				console.log(111, newImgs)
+				this.form.attachmentList = this.form.attachmentList.concat(newImgs)
+			},
+
+			// api 
+			async onSumbit(type) { // 1:暂存 2:提交
+				//定义表单规则
+				var rule = [{
+					name: "content",
+					checkType: "notnull",
+					errorMsg: "请输入问题描述"
+				}, {
+					name: "attachmentList",
+					// checkType: "notnull",
+					errorMsg: "请添加附件"
+				}];
+
+				//进行表单检查
+				var checkRes = formChecker.check(this.form, rule);
+				if (!checkRes) {
+					uni.showToast({
+						title: formChecker.error,
+						icon: "none"
+					});
+					return
+				}
+
+				await this.uploadFiles()
+				const that = this
+				uni.showLoading()
+				const params = {
+					...that.form
+				}
+				params.attachmentList = params.attachmentList.map(item => item.path)
+				uni.showLoading()
+				that.$ajax.post('/shunt/proposal-apply', params).then(res => {
+					uni.hideLoading()
+					uni.showToast({
+						title: '提交成功',
+						duration: 2000
+					});
+					setTimeout(function() {
+						that.$refs.uNavBar.toBack()
+					}, 2000);
+
+				}).catch(error => {
+					uni.hideLoading()
+					uni.showModal({
+						title: '错误',
+						content: error.msg || error,
+						showCancel: false
+					})
+				})
+
+			},
+			async uploadFiles(success) {
+				uni.showLoading({
+					title: '上传文件中...'
+				});
+				const promiseList = []
+				const indexList = []
+				const that = this
+				that.form.attachmentList.forEach((element, index) => {
+					if (element.status) {
+						indexList.push(index)
+						promiseList.push(uni.uploadFile({
+							url: `/shunt/upload`,
+							filePath: element.url,
+							// fileType: 'image',
+							name: 'file',
+							header: {
+								token: uni.getStorageSync('TOKEN')
+							},
+							formData: {
+								// file: res.tempFiles[0],
+								fileType: 'APPLY',
+							},
+							// success(uploadFileRes) {
+							// 	const data = JSON.parse(uploadFileRes.data)
+							// 	const filePath = data.data.filePath
+							// 	console.log(1111, filePath)
+							// 	that.form.attachmentList[index] = filePath
+							// }
+						}))
+					}
+				})
+				await Promise.all(promiseList).then(resList => {
+					resList.forEach((element, index) => {
+						let uploadFileRes = element[1]
+						const data = JSON.parse(uploadFileRes.data)
+						const filePath = data.data.filePath
+						const formIndex = indexList[index]
+						that.form.attachmentList[formIndex].path = filePath
+					})
+				})
+				uni.hideLoading()
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.page-container {
+
+		// background-color: #F5f5f5;
+		// min-height: 100vh;
+
+		.main-content {
+			background-color: white;
+			padding: 32rpx;
+		}
+
+
+		.save-button,
+		.sumbit-button {
+			flex: 1;
+			height: 88rpx;
+			background: linear-gradient(90deg, #FFCF18 0%, #FFB300 100%);
+			border-radius: 8rpx;
+
+			&.disabled {
+				cursor: not-allowed;
+				opacity: 0.3;
+			}
+		}
+
+		.sumbit-button {
+			background: linear-gradient(90deg, #36ACFC 0%, #078EF7 100%);
+		}
+
+		.choose-image {
+			width: 100%;
+			height: 100%;
+			background: #FFFFFF;
+			cursor: pointer;
+			text-align: center;
+			border: 1rpx dashed #A3ABBF;
+			flex-direction: column;
+
+			.choose-image-tip {
+				font-size: 24rpx;
+				font-weight: 400;
+				color: #078EF7;
+				margin-top: 24rpx;
+			}
+
+		}
+
+	}
+</style>

+ 179 - 0
pages/my/user-info/user-info.vue

@@ -0,0 +1,179 @@
+<template>
+	<view class="page-container">
+		<u-nav-bar title="个人信息" color="#333" bg="white"></u-nav-bar>
+		<view class="main-content">
+			<u-panel>
+				<u-des-row label="头像" border forward @click="editAvatar">
+					<view slot="value" class="u-flex" style="justify-content: flex-end;">
+						<image :style="{background: form.avatar?'':'#9FA5BA'}" :src="form.avatar" class="avatar"
+							mode="aspectFill"></image>
+					</view>
+				</u-des-row>
+				<u-des-row label="姓名" :value="form.studentName" border />
+				<u-des-row label="性别" :value="form.genderName || '--'" border />
+				<u-des-row label="专业群名称" :value="form.majorName || '--'" border />
+				<u-des-row label="学号" :value="form.studentNO || '--'" border />
+				<!-- <u-des-row label="年级" :value="form.test" border />s -->
+				<u-des-row label="班级" :value="form.gradeName  || '--'" border />
+				<u-des-row label="电子邮箱" :value="form.test" border @click="editEmail">
+					<view slot="value" class="u-flex" style="justify-content: flex-end;">
+						{{form.studentEmail}}
+						<view class="u-right-icon">
+							<image style="width:24rpx;height:24rpx;" src="/static/edit.png" />
+						</view>
+					</view>
+				</u-des-row>
+				<u-des-row label="手机" eye>
+					<view slot="value" slot-scope="{isOpen}">{{isOpen ? userInfo.mobileNo : hideCode(userInfo.mobileNo, 3,4) }}</view>
+				</u-des-row>
+			</u-panel>
+			<!-- <button  type="primary" class="button" @click="onSumbit">立即修改</button> -->
+		</view>
+		<u-drawer-input :visible.sync="visible" :config="inputConfig" @sumbit="setEmail" @cancel="visible=false" />
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex'
+	import {
+		hideCode
+	} from '@/common'
+	export default {
+		data() {
+			return {
+				genderNameOptions: [{
+					label: '男',
+					vlaue: '男'
+				}, {
+					label: '女',
+					value: '女'
+				}],
+				form: {
+					avatar: '',
+					genderName: '',
+					studentEmail: '',
+				},
+				inputConfig: {},
+				visible: false //  抽屉
+			}
+		},
+		computed: {
+			...mapState({
+				userInfo: state => state.user.userInfo,
+			})
+		},
+		watch: {
+			userInfo: {
+				handler(xy) {
+					this.init()
+				},
+				deep: true,
+				immediate: true
+			},
+		},
+		methods: {
+			hideCode,
+			async init() {
+				Object.assign(this.form, this.userInfo || {})
+			},
+			editAvatar() {
+				const that = this
+				uni.chooseImage({
+					count: 1, //默认9
+					sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
+					sourceType: ['album', 'camera'], //从相册选择
+					success(res) {
+						const filePath = res.tempFilePaths[0]
+						console.log(111, res)
+						uni.uploadFile({
+							url: `/shunt/upload`,
+							filePath: filePath,
+							// fileType: 'image',
+							name: 'file',
+							header: {
+								token: uni.getStorageSync('TOKEN')
+							},
+							formData: {
+							// file: res.tempFiles[0],
+								fileType: 'AVATAR',
+							},
+							success(uploadFileRes) {
+								const data = JSON.parse(uploadFileRes.data)
+								const filePath = data.data.filePath
+								that.setAvatar(filePath)
+							}
+						});
+					}
+				});
+			},
+			setAvatar(avatar) {
+				this.setUserInfo('/shunt/set-avatar', {
+					avatar
+				})
+			},
+			editEmail() {
+				this.inputConfig = {
+					title: '电子邮箱',
+					value: this.form.studentEmail,
+					placeholder: '请输入电子邮箱',
+					rule: [{
+						name: "value",
+						checkType: "email",
+						errorMsg: "请输入正确的邮箱"
+					}]
+				}
+				this.visible = true
+			},
+			setEmail(email) {
+				this.setUserInfo('/shunt/set-email', {
+					email
+				})
+			},
+			async setUserInfo(url, data) {
+				try {
+					uni.showLoading()
+					await this.$ajax.post(url, data)
+					await this.$store.dispatch('user/updateUserInfo')
+					uni.showToast({
+						title: '修改成功',
+						icon: "success"
+					});
+					this.visible = false
+				} catch (e) {
+					console.log('setUserInfo', e)
+					//TODO handle the exception
+				} finally {
+					uni.hideLoading()
+				}
+			},
+		},
+	}
+</script>
+<style lang="scss" scoped>
+	.page-container {
+
+		.main-content {
+			padding: 32rpx 32rpx 32rpx 32rpx; // 188
+
+			.avatar {
+				width: 81rpx;
+				height: 81rpx;
+				// background: #9FA5BA;
+				border-radius: 100%;
+				//border: 1rpx solid  #9FA5BA;
+			}
+
+			.button {
+				background: linear-gradient(90deg, #36ACFC 0%, #078EF7 100%);
+				border-radius: 45rpx;
+				font-size: 32rpx;
+				font-weight: 400;
+				color: #FFFFFF;
+				padding: 28rpx;
+				margin-top: 80rpx;
+			}
+		}
+	}
+</style>

BIN
static/403.png


BIN
static/achievement_label.png


BIN
static/bg_fail.png


BIN
static/bg_icon.png


BIN
static/bg_plan.png


BIN
static/choose_icon.png


BIN
static/edit.png


BIN
static/home_label.png


BIN
static/home_label_red.png


BIN
static/index/card-title-icon.png


BIN
static/index/diversion_icon.png


BIN
static/index/diversion_quota_icon.png


BIN
static/index/end_state_choose.png


BIN
static/index/into_icon.png


BIN
static/index/my_grades_icon.png


BIN
static/index/not_state_choose.png


BIN
static/index/progress_query_icon.png


BIN
static/index/schedule_list_bg.png


BIN
static/index/special_icon.png


BIN
static/index/start_state_choose.png


BIN
static/logo.png


BIN
static/menber_info_icon.png


BIN
static/my_grades_bg.png


BIN
static/no_choose_icon.png


BIN
static/no_list.png


BIN
static/no_open.png


BIN
static/not_state_choose.png


BIN
static/num_icon.png


BIN
static/open_icon.png


BIN
static/schedule_list_bg.png


BIN
static/shenhe-中.png


BIN
static/shenhe-失败.png


BIN
static/shenhe-成功.png


BIN
static/sign_icon.png


BIN
static/success_bg.png


BIN
static/tabbar/grid.png


BIN
static/tabbar/grid_active.png


BIN
static/tabbar/im-contacts.png


BIN
static/tabbar/im-contacts_active.png


BIN
static/tabbar/list.png


BIN
static/tabbar/list_active.png


BIN
static/tabbar/me.png


BIN
static/tabbar/me_active.png


BIN
static/time_icon.png


BIN
static/top_panel_bg.png


BIN
static/upload_icon.png


BIN
static/xpc.png


+ 24 - 0
store/index.js

@@ -0,0 +1,24 @@
+import user from '@/store/modules/user.js'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import Vuex from 'vuex'
+Vue.use(Vuex)
+const store = new Vuex.Store({
+	modules: {
+		user
+	},
+	strict: true
+})
+// #endif
+
+// #ifdef VUE3
+import {createStore} from 'vuex'
+const store = createStore({
+	modules: {
+		user
+	}
+})
+// #endif
+
+export default store

+ 96 - 0
store/modules/user.js

@@ -0,0 +1,96 @@
+import ajax from '@/common/ajax'
+import store from '@/store'
+
+let state = {
+		token: uni.getStorageSync('TOKEN'), //是否已经登录
+		userInfo: uni.getStorageSync('userInfo') || {}, //用户信息
+		code: uni.getStorageSync('code'), //企微 code
+	},
+	getters = {
+		userInfo(state) {
+			return state.userInfo;
+		},
+		token(state) {
+			return state.token;
+		}
+	},
+	mutations = {
+		setUserInfo(state, info) { //登录成功后的操作
+			//原有的结合传来的参数
+			state.userInfo = info || {};
+			state.token = uni.getStorageSync('TOKEN') || '';
+			uni.setStorageSync('userInfo', info);
+		},
+		logout(state) {
+			state.info = {};
+			state.token = '';
+			state.code = '';
+			uni.setStorageSync('userInfo', {});
+			uni.removeStorageSync('TOKEN');
+			uni.setStorageSync('code', '');
+		},
+		setToken(state, token) { 
+			state.token = token;
+			uni.setStorageSync('TOKEN', token);
+		},
+		setCode(state, code) {
+			state.code = code;
+			uni.setStorageSync('code', code);
+		},
+	},
+	actions = {
+		login({
+			commit,
+			state
+		},params) {
+			uni.showLoading({
+				title: '登录中',
+				mask: true
+			})
+			let token = ''
+			ajax.post('/shunt/login', {code: state.code, ...params }).then(res => {
+				token = res?.data?.token || ''
+			}).catch(error => {
+				console.error('login', error)
+			}).finally(() => {
+				uni.hideLoading()
+				commit('setToken', token)
+				if (token) {
+					store.dispatch('user/updateUserInfo')
+					uni.reLaunch({
+						url: '/pages/index/index'
+					});
+				} else {
+					commit('logout', '')
+					uni.reLaunch({
+						url: '/pages/error/403/403'
+					});
+				}
+			})
+		},
+		updateUserInfo({
+			commit,
+			state
+		}) {
+			uni.showLoading({
+				mask: true
+			})
+			state.token && ajax.post('/shunt/userinfo').then(res => {
+				const info = res.data || {}
+				commit('setUserInfo', info)
+			}).catch(error => {
+				console.error('updateUserInfo', error)
+				commit('setUserInfo', {})
+			}).finally(() => {
+				uni.hideLoading()
+			})
+		},
+
+	}
+export default {
+	namespaced: true,
+	state,
+	getters,
+	mutations,
+	actions
+}

+ 186 - 0
uni.scss

@@ -0,0 +1,186 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$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:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;
+//
+$u-nav-bar-height: 160rpx;
+.u-nav-content {
+	margin-top: $u-nav-bar-height;
+}
+
+
+// 全局初始化样式
+* {
+	line-height: 1;
+	padding: 0;
+	margin: 0;
+	box-sizing: border-box;
+    font-family: Microsoft YaHei;
+}
+// u-flex
+@mixin u-flex {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	height: 100%;
+	justify-content: space-between;
+}
+
+.u-flex {
+	@include u-flex;
+}
+.u-flex-column {
+	@include u-flex;
+	flex-direction: column;
+}
+.u-flex-center {
+	@include u-flex;
+	justify-content: center;
+}
+
+// 
+	//
+	.u-bg {
+		width: 100%;
+
+		// sbackground: $uni-color-primary;
+		// background: url('/static/my_grades_bg.png');
+		// background-size: 100% 100%;
+		// background-repeat: no-repeat;
+		// background-position: center;
+		// background-origin: content-box;
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		z-index: -1;
+	}
+
+	.u-button {
+		width: auto;
+		padding: 0 40rpx;
+		height: 70rpx;
+		background: linear-gradient(90deg, #36ACFC 0%, #078EF7 100%);
+		border-radius: 36rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 32rpx;
+		font-weight: bold;
+		color: #FFFFFF;
+		cursor: pointer;
+		&:hover {
+			opacity: 0.8;
+		}
+	}
+	.u-right-icon {
+		cursor: pointer;
+		padding: 8rpx 0 8rpx 12rpx;
+	}
+	
+	// 空两格 
+	.empty-2 {
+		width: 44rpx;  height: 1px;  display: inline-block;
+	}
+	
+	//
+	.u-sign-preview{
+		width: 128rpx;
+		height: auto;
+		position: relative;
+	
+		.icon-clear {
+			position: absolute;
+			top: 0;
+			right: 0;
+			width: 36rpx;
+			height: 36rpx;
+			background: rgba($color: #333333, $alpha: 0.55);
+			z-index: 1;
+			border-radius: 0px 8rpx 0px 8rpx;
+			cursor: pointer;
+	
+		}
+	}
+	
+	.u-disabled-mask {
+		position: fixed;
+			top: 0;
+			left: 0;
+			bottom: 0;
+			right: 0;
+			z-index: 99;
+			cursor: not-allowed;
+			background-color: rgba(0, 0, 0, 0.05);
+	}

+ 29 - 0
uni_modules/uni-badge/changelog.md

@@ -0,0 +1,29 @@
+## 1.2.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
+## 1.1.7(2021-11-08)
+- 优化 升级ui
+- 修改 size 属性默认值调整为 small
+- 修改 type 属性,默认值调整为 error,info 替换 default
+## 1.1.6(2021-09-22)
+- 修复 在字节小程序上样式不生效的 bug
+## 1.1.5(2021-07-30)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.1.4(2021-07-29)
+- 修复 去掉 nvue 不支持css 的 align-self 属性,nvue 下不暂支持 absolute 属性
+## 1.1.3(2021-06-24)
+- 优化 示例项目
+## 1.1.1(2021-05-12)
+- 新增 组件示例地址
+## 1.1.0(2021-05-12)
+- 新增 uni-badge 的 absolute 属性,支持定位
+- 新增 uni-badge 的 offset 属性,支持定位偏移
+- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点
+- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+
+- 优化 uni-badge 属性 custom-style, 支持以对象形式自定义样式
+## 1.0.7(2021-05-07)
+- 修复 uni-badge 在 App 端,数字小于10时不是圆形的bug
+- 修复 uni-badge 在父元素不是 flex 布局时,宽度缩小的bug
+- 新增 uni-badge 属性 custom-style, 支持自定义样式
+## 1.0.6(2021-02-04)
+- 调整为uni_modules目录规范

+ 268 - 0
uni_modules/uni-badge/components/uni-badge/uni-badge.vue

@@ -0,0 +1,268 @@
+<template>
+	<view class="uni-badge--x">
+		<slot />
+		<text v-if="text" :class="classNames" :style="[badgeWidth, positionStyle, customStyle, dotStyle]"
+			class="uni-badge" @click="onClick()">{{displayValue}}</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * Badge 数字角标
+	 * @description 数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=21
+	 * @property {String} text 角标内容
+	 * @property {String} size = [normal|small] 角标内容
+	 * @property {String} type = [info|primary|success|warning|error] 颜色类型
+	 * 	@value info 灰色
+	 * 	@value primary 蓝色
+	 * 	@value success 绿色
+	 * 	@value warning 黄色
+	 * 	@value error 红色
+	 * @property {String} inverted = [true|false] 是否无需背景颜色
+	 * @property {Number} maxNum 展示封顶的数字值,超过 99 显示 99+
+	 * @property {String} absolute = [rightTop|rightBottom|leftBottom|leftTop] 开启绝对定位, 角标将定位到其包裹的标签的四角上		
+	 * 	@value rightTop 右上
+	 * 	@value rightBottom 右下
+	 * 	@value leftTop 左上
+	 * 	@value leftBottom 左下
+	 * @property {Array[number]} offset	距定位角中心点的偏移量,只有存在 absolute 属性时有效,例如:[-10, -10] 表示向外偏移 10px,[10, 10] 表示向 absolute 指定的内偏移 10px
+	 * @property {String} isDot = [true|false] 是否显示为一个小点
+	 * @event {Function} click 点击 Badge 触发事件
+	 * @example <uni-badge text="1"></uni-badge>
+	 */
+
+	export default {
+		name: 'UniBadge',
+		emits: ['click'],
+		props: {
+			type: {
+				type: String,
+				default: 'error'
+			},
+			inverted: {
+				type: Boolean,
+				default: false
+			},
+			isDot: {
+				type: Boolean,
+				default: false
+			},
+			maxNum: {
+				type: Number,
+				default: 99
+			},
+			absolute: {
+				type: String,
+				default: ''
+			},
+			offset: {
+				type: Array,
+				default () {
+					return [0, 0]
+				}
+			},
+			text: {
+				type: [String, Number],
+				default: ''
+			},
+			size: {
+				type: String,
+				default: 'small'
+			},
+			customStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			}
+		},
+		data() {
+			return {};
+		},
+		computed: {
+			width() {
+				return String(this.text).length * 8 + 12
+			},
+			classNames() {
+				const {
+					inverted,
+					type,
+					size,
+					absolute
+				} = this
+				return [
+					inverted ? 'uni-badge--' + type + '-inverted' : '',
+					'uni-badge--' + type,
+					'uni-badge--' + size,
+					absolute ? 'uni-badge--absolute' : ''
+				].join(' ')
+			},
+			positionStyle() {
+				if (!this.absolute) return {}
+				let w = this.width / 2,
+					h = 10
+				if (this.isDot) {
+					w = 5
+					h = 5
+				}
+				const x = `${- w  + this.offset[0]}px`
+				const y = `${- h + this.offset[1]}px`
+
+				const whiteList = {
+					rightTop: {
+						right: x,
+						top: y
+					},
+					rightBottom: {
+						right: x,
+						bottom: y
+					},
+					leftBottom: {
+						left: x,
+						bottom: y
+					},
+					leftTop: {
+						left: x,
+						top: y
+					}
+				}
+				const match = whiteList[this.absolute]
+				return match ? match : whiteList['rightTop']
+			},
+			badgeWidth() {
+				return {
+					width: `${this.width}px`
+				}
+			},
+			dotStyle() {
+				if (!this.isDot) return {}
+				return {
+					width: '10px',
+					height: '10px',
+					borderRadius: '10px'
+				}
+			},
+			displayValue() {
+				const {
+					isDot,
+					text,
+					maxNum
+				} = this
+				return isDot ? '' : (Number(text) > maxNum ? `${maxNum}+` : text)
+			}
+		},
+		methods: {
+			onClick() {
+				this.$emit('click');
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	$uni-primary: #2979ff !default;
+	$uni-success: #4cd964 !default;
+	$uni-warning: #f0ad4e !default;
+	$uni-error: #dd524d !default;
+	$uni-info: #909399 !default;
+
+
+	$bage-size: 12px;
+	$bage-small: scale(0.8);
+
+	.uni-badge--x {
+		/* #ifdef APP-NVUE */
+		// align-self: flex-start;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: inline-block;
+		/* #endif */
+		position: relative;
+	}
+
+	.uni-badge--absolute {
+		position: absolute;
+	}
+
+	.uni-badge--small {
+		transform: $bage-small;
+		transform-origin: center center;
+	}
+
+	.uni-badge {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		overflow: hidden;
+		box-sizing: border-box;
+		/* #endif */
+		justify-content: center;
+		flex-direction: row;
+		height: 20px;
+		line-height: 18px;
+		color: #fff;
+		border-radius: 100px;
+		background-color: $uni-info;
+		background-color: transparent;
+		border: 1px solid #fff;
+		text-align: center;
+		font-family: 'Helvetica Neue', Helvetica, sans-serif;
+		font-size: $bage-size;
+		/* #ifdef H5 */
+		z-index: 999;
+		cursor: pointer;
+		/* #endif */
+
+		&--info {
+			color: #fff;
+			background-color: $uni-info;
+		}
+
+		&--primary {
+			background-color: $uni-primary;
+		}
+
+		&--success {
+			background-color: $uni-success;
+		}
+
+		&--warning {
+			background-color: $uni-warning;
+		}
+
+		&--error {
+			background-color: $uni-error;
+		}
+
+		&--inverted {
+			padding: 0 5px 0 0;
+			color: $uni-info;
+		}
+
+		&--info-inverted {
+			color: $uni-info;
+			background-color: transparent;
+		}
+
+		&--primary-inverted {
+			color: $uni-primary;
+			background-color: transparent;
+		}
+
+		&--success-inverted {
+			color: $uni-success;
+			background-color: transparent;
+		}
+
+		&--warning-inverted {
+			color: $uni-warning;
+			background-color: transparent;
+		}
+
+		&--error-inverted {
+			color: $uni-error;
+			background-color: transparent;
+		}
+
+	}
+</style>

+ 10 - 0
uni_modules/uni-badge/readme.md

@@ -0,0 +1,10 @@
+## Badge 数字角标
+> **组件名:uni-badge**
+> 代码块: `uBadge`
+
+数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景,
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 
+
+

+ 33 - 0
uni_modules/uni-easyinput/changelog.md

@@ -0,0 +1,33 @@
+## 1.0.1(2022-04-02)
+- 修复 value不能为0的bug
+## 1.0.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
+## 0.1.4(2021-08-20)
+- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug
+## 0.1.3(2021-08-11)
+- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
+## 0.1.2(2021-07-30)
+- 优化 vue3下事件警告的问题
+## 0.1.1
+- 优化 errorMessage 属性支持 Boolean 类型
+## 0.1.0(2021-07-13)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 0.0.16(2021-06-29)
+- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug
+## 0.0.15(2021-06-21)
+- 修复 passwordIcon 属性拼写错误的 bug
+## 0.0.14(2021-06-18)
+- 新增 passwordIcon 属性,当type=password时是否显示小眼睛图标
+- 修复 confirmType 属性不生效的问题
+## 0.0.13(2021-06-04)
+- 修复 disabled 状态可清出内容的 bug
+## 0.0.12(2021-05-12)
+- 新增 组件示例地址
+## 0.0.11(2021-05-07)
+- 修复 input-border 属性不生效的问题
+## 0.0.10(2021-04-30)
+- 修复 ios 遮挡文字、显示一半的问题
+## 0.0.9(2021-02-05)
+- 调整为uni_modules目录规范
+- 优化 兼容 nvue 页面

+ 56 - 0
uni_modules/uni-easyinput/components/uni-easyinput/common.js

@@ -0,0 +1,56 @@
+/**
+ * @desc 函数防抖
+ * @param func 目标函数
+ * @param wait 延迟执行毫秒数
+ * @param immediate true - 立即执行, false - 延迟执行
+ */
+export const debounce = function(func, wait = 1000, immediate = true) {
+	let timer;
+	console.log(1);
+	return function() {
+		console.log(123);
+		let context = this,
+			args = arguments;
+		if (timer) clearTimeout(timer);
+		if (immediate) {
+			let callNow = !timer;
+			timer = setTimeout(() => {
+				timer = null;
+			}, wait);
+			if (callNow) func.apply(context, args);
+		} else {
+			timer = setTimeout(() => {
+				func.apply(context, args);
+			}, wait)
+		}
+	}
+}
+/**
+ * @desc 函数节流
+ * @param func 函数
+ * @param wait 延迟执行毫秒数
+ * @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发
+ */
+export const throttle = (func, wait = 1000, type = 1) => {
+	let previous = 0;
+	let timeout;
+	return function() {
+		let context = this;
+		let args = arguments;
+		if (type === 1) {
+			let now = Date.now();
+
+			if (now - previous > wait) {
+				func.apply(context, args);
+				previous = now;
+			}
+		} else if (type === 2) {
+			if (!timeout) {
+				timeout = setTimeout(() => {
+					timeout = null;
+					func.apply(context, args)
+				}, wait)
+			}
+		}
+	}
+}

+ 461 - 0
uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue

@@ -0,0 +1,461 @@
+<template>
+	<view class="uni-easyinput" :class="{'uni-easyinput-error':msg}" :style="{color:inputBorder && msg?'#e43d33':styles.color}">
+		<view class="uni-easyinput__content" :class="{'is-input-border':inputBorder ,'is-input-error-border':inputBorder && msg,'is-textarea':type==='textarea','is-disabled':disabled}"
+		 :style="{'border-color':inputBorder && msg?'#dd524d':styles.borderColor,'background-color':disabled?styles.disableColor:''}">
+			<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')"></uni-icons>
+			<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{'input-padding':inputBorder}"
+			 :name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class"
+			 :maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" @input="onInput" @blur="onBlur"  @focus="onFocus"
+			 @confirm="onConfirm"></textarea>
+			<input v-else :type="type === 'password'?'text':type" class="uni-easyinput__content-input" :style="{
+				 'padding-right':type === 'password' ||clearable || prefixIcon?'':'10px',
+				 'padding-left':prefixIcon?'':'10px'
+			 }"
+			 :name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder"
+			 :placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" @focus="onFocus"
+			 @blur="onBlur" @input="onInput" @confirm="onConfirm" />
+			<template v-if="type === 'password' && passwordIcon" >
+				<uni-icons v-if="val != '' " class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" :type="showPassword?'eye-slash-filled':'eye-filled'"
+				 :size="18" color="#c0c4cc" @click="onEyes"></uni-icons>
+			</template>
+			<template v-else-if="suffixIcon">
+				<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')"></uni-icons>
+			</template>
+			<template v-else>
+				<uni-icons class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" type="clear" :size="clearSize"
+				 v-if="clearable && (val !== '') && !disabled" color="#c0c4cc" @click="onClear"></uni-icons>
+			</template>
+			<slot name="right"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	// import {
+	// 	debounce,
+	// 	throttle
+	// } from './common.js'
+	/**
+	 * Easyinput 输入框
+	 * @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=3455
+	 * @property {String}	value	输入内容
+	 * @property {String }	type	输入框的类型(默认text) password/text/textarea/..
+	 * 	@value text			文本输入键盘
+	 * 	@value textarea	多行文本输入键盘
+	 * 	@value password	密码输入键盘
+	 * 	@value number		数字输入键盘,注意iOS上app-vue弹出的数字键盘并非9宫格方式
+	 * 	@value idcard		身份证输入键盘,信、支付宝、百度、QQ小程序
+	 * 	@value digit		带小数点的数字键盘	,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持
+	 * @property {Boolean}	clearable	是否显示右侧清空内容的图标控件,点击可清空输入框内容(默认true)
+	 * @property {Boolean}	autoHeight	是否自动增高输入区域,type为textarea时有效(默认false)
+	 * @property {String }	placeholder	输入框的提示文字
+	 * @property {String }	placeholderStyle	placeholder的样式(内联样式,字符串),如"color: #ddd"
+	 * @property {Boolean}	focus	是否自动获得焦点(默认false)
+	 * @property {Boolean}	disabled	是否禁用(默认false)
+	 * @property {Number }	maxlength	最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+	 * @property {String }	confirmType	设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+	 * @property {Number }	clearSize	清除图标的大小,单位px(默认15)
+	 * @property {String}	prefixIcon	输入框头部图标
+	 * @property {String}	suffixIcon	输入框尾部图标
+	 * @property {Boolean}	trim	是否自动去除两端的空格
+	 * @value both	去除两端空格
+	 * @value left	去除左侧空格
+	 * @value right	去除右侧空格
+	 * @value start	去除左侧空格
+	 * @value end		去除右侧空格
+	 * @value all		去除全部空格
+	 * @value none	不去除空格
+	 * @property {Boolean}	inputBorder	是否显示input输入框的边框(默认true)
+	 * @property {Boolean}	passwordIcon	type=password时是否显示小眼睛图标
+	 * @property {Object}	styles	自定义颜色
+	 * @event {Function}	input	输入框内容发生变化时触发
+	 * @event {Function}	focus	输入框获得焦点时触发
+	 * @event {Function}	blur	输入框失去焦点时触发
+	 * @event {Function}	confirm	点击完成按钮时触发
+	 * @event {Function}	iconClick	点击图标时触发
+	 * @example <uni-easyinput v-model="mobile"></uni-easyinput>
+	 */
+
+	 export default {
+		name: 'uni-easyinput',
+		emits:['click','iconClick','update:modelValue','input','focus','blur','confirm'],
+		model:{
+			prop:'modelValue',
+			event:'update:modelValue'
+		},
+		props: {
+			name: String,
+			value: [Number, String],
+			modelValue: [Number, String],
+			type: {
+				type: String,
+				default: 'text'
+			},
+			clearable: {
+				type: Boolean,
+				default: true
+			},
+			autoHeight: {
+				type: Boolean,
+				default: false
+			},
+			placeholder: String,
+			placeholderStyle: String,
+			focus: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			maxlength: {
+				type: [Number, String],
+				default: 140
+			},
+			confirmType: {
+				type: String,
+				default: 'done'
+			},
+			clearSize: {
+				type: [Number, String],
+				default: 15
+			},
+			inputBorder: {
+				type: Boolean,
+				default: true
+			},
+			prefixIcon: {
+				type: String,
+				default: ''
+			},
+			suffixIcon: {
+				type: String,
+				default: ''
+			},
+			trim: {
+				type: [Boolean, String],
+				default: true
+			},
+			passwordIcon:{
+				type: Boolean,
+				default: true
+			},
+			styles: {
+				type: Object,
+				default () {
+					return {
+						color: '#333',
+						disableColor: '#F7F6F6',
+						borderColor: '#e5e5e5'
+					}
+				}
+			},
+			errorMessage:{
+				type:[String,Boolean],
+				default:''
+			}
+		},
+		data() {
+			return {
+				focused: false,
+				errMsg: '',
+				val: '',
+				showMsg: '',
+				border: false,
+				isFirstBorder: false,
+				showClearIcon: false,
+				showPassword: false
+			};
+		},
+		computed: {
+			msg() {
+				return this.errorMessage || this.errMsg;
+			},
+			// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,用户可以传入字符串数值
+			inputMaxlength() {
+				return Number(this.maxlength);
+			},
+		},
+		watch: {
+			value(newVal) {
+				if (this.errMsg) this.errMsg = ''
+				this.val = newVal
+				// fix by mehaotian is_reset 在 uni-forms 中定义
+				if (this.form && this.formItem &&!this.is_reset) {
+					this.is_reset = false
+					this.formItem.setValue(newVal)
+				}
+			},
+			modelValue(newVal) {
+				if (this.errMsg) this.errMsg = ''
+				this.val = newVal
+				if (this.form && this.formItem &&!this.is_reset) {
+					this.is_reset = false
+					this.formItem.setValue(newVal)
+				}
+			},
+			focus(newVal) {
+				this.$nextTick(() => {
+					this.focused = this.focus
+				})
+			}
+		},
+		created() {
+			if(!this.value && this.value !== 0){
+				this.val = this.modelValue
+			}
+			if(!this.modelValue === '' && this.modelValue !== 0){
+				this.val = this.value
+			}
+			this.form = this.getForm('uniForms')
+			this.formItem = this.getForm('uniFormsItem')
+			if (this.form && this.formItem) {
+				if (this.formItem.name) {
+					if(!this.is_reset){
+						this.is_reset = false
+						this.formItem.setValue(this.val)
+					}
+					this.rename = this.formItem.name
+					this.form.inputChildrens.push(this)
+				}
+			}
+		},
+		mounted() {
+			this.$nextTick(() => {
+				this.focused = this.focus
+			})
+		},
+		methods: {
+			/**
+			 * 初始化变量值
+			 */
+			init() {
+
+			},
+			onClickIcon(type) {
+				this.$emit('iconClick', type)
+			},
+			/**
+			 * 获取父元素实例
+			 */
+			getForm(name = 'uniForms') {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== name) {
+					parent = parent.$parent;
+					if (!parent) return false;
+					parentName = parent.$options.name;
+				}
+				return parent;
+			},
+
+			onEyes() {
+				this.showPassword = !this.showPassword
+			},
+			onInput(event) {
+				let value = event.detail.value;
+				// 判断是否去除空格
+				if (this.trim) {
+					if (typeof(this.trim) === 'boolean' && this.trim) {
+						value = this.trimStr(value)
+					}
+					if (typeof(this.trim) === 'string') {
+						value = this.trimStr(value, this.trim)
+					}
+				};
+				if (this.errMsg) this.errMsg = ''
+				this.val = value
+				// TODO 兼容 vue2
+				this.$emit('input', value);
+				// TODO 兼容 vue3
+				this.$emit('update:modelValue',value)
+			},
+
+			onFocus(event) {
+				this.$emit('focus', event);
+			},
+			onBlur(event) {
+				let value = event.detail.value;
+				this.$emit('blur', event);
+			},
+			onConfirm(e) {
+				this.$emit('confirm', e.detail.value);
+			},
+			onClear(event) {
+				this.val = '';
+				// TODO 兼容 vue2
+				this.$emit('input', '');
+				// TODO 兼容 vue2
+				// TODO 兼容 vue3
+				this.$emit('update:modelValue','')
+			},
+			fieldClick() {
+				this.$emit('click');
+			},
+			trimStr(str, pos = 'both') {
+				if (pos === 'both') {
+					return str.trim();
+				} else if (pos === 'left') {
+					return str.trimLeft();
+				} else if (pos === 'right') {
+					return str.trimRight();
+				} else if (pos === 'start') {
+					return str.trimStart()
+				} else if (pos === 'end') {
+					return str.trimEnd()
+				} else if (pos === 'all') {
+					return str.replace(/\s+/g, '');
+				} else if (pos === 'none') {
+					return str;
+				}
+				return str;
+			}
+		}
+	};
+</script>
+
+<style lang="scss" >
+	$uni-error: #e43d33;
+	$uni-border-1: #DCDFE6 !default;
+	.uni-easyinput {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+		flex: 1;
+		position: relative;
+		text-align: left;
+		color: #333;
+		font-size: 14px;
+	}
+
+	.uni-easyinput__content {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		min-height: 36px;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.uni-easyinput__content-input {
+		/* #ifndef APP-NVUE */
+		width: auto;
+		/* #endif */
+		position: relative;
+		overflow: hidden;
+		flex: 1;
+		line-height: 1;
+		font-size: 14px;
+	}
+	.uni-easyinput__placeholder-class {
+		color: #999;
+		font-size: 12px;
+		font-weight: 200;
+	}
+	.is-textarea {
+		align-items: flex-start;
+	}
+
+	.is-textarea-icon {
+		margin-top: 5px;
+	}
+
+	.uni-easyinput__content-textarea {
+		position: relative;
+		overflow: hidden;
+		flex: 1;
+		line-height: 1.5;
+		font-size: 14px;
+		padding-top: 6px;
+		padding-bottom: 10px;
+		height: 80px;
+		/* #ifndef APP-NVUE */
+		min-height: 80px;
+		width: auto;
+		/* #endif */
+	}
+
+	.input-padding {
+		padding-left: 10px;
+	}
+
+	.content-clear-icon {
+		padding: 0 5px;
+	}
+
+	.label-icon {
+		margin-right: 5px;
+		margin-top: -1px;
+	}
+
+	// 显示边框
+	.is-input-border {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		border: 1px solid $uni-border-1;
+		border-radius: 4px;
+	}
+
+	.uni-error-message {
+		position: absolute;
+		bottom: -17px;
+		left: 0;
+		line-height: 12px;
+		color: $uni-error;
+		font-size: 12px;
+		text-align: left;
+	}
+
+	.uni-error-msg--boeder {
+		position: relative;
+		bottom: 0;
+		line-height: 22px;
+	}
+
+	.is-input-error-border {
+		border-color: $uni-error;
+		.uni-easyinput__placeholder-class {
+			color: mix(#fff, $uni-error, 50%);;
+		}
+	}
+
+
+	.uni-easyinput--border {
+		margin-bottom: 0;
+		padding: 10px 15px;
+		// padding-bottom: 0;
+		border-top: 1px #eee solid;
+	}
+
+	.uni-easyinput-error {
+		padding-bottom: 0;
+	}
+
+	.is-first-border {
+		/* #ifndef APP-NVUE */
+		border: none;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		border-width: 0;
+		/* #endif */
+	}
+
+	.is-disabled {
+		border-color: red;
+		background-color: #F7F6F6;
+		color: #D5D5D5;
+		.uni-easyinput__placeholder-class {
+			color: #D5D5D5;
+			font-size: 12px;
+		}
+	}
+</style>

+ 11 - 0
uni_modules/uni-easyinput/readme.md

@@ -0,0 +1,11 @@
+
+
+### Easyinput 增强输入框
+> **组件名:uni-easyinput**
+> 代码块: `uEasyinput`
+
+
+easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的,easyinput 内置了边框,图标等,同时包含 input 所有功能
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 61 - 0
uni_modules/uni-file-picker/changelog.md

@@ -0,0 +1,61 @@
+## 1.0.1(2021-11-23)
+- 修复 参数为对象的情况下,url在某些情况显示错误的bug
+## 1.0.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker)
+## 0.2.16(2021-11-08)
+- 修复 传入空对象 ,显示错误的Bug
+## 0.2.15(2021-08-30)
+- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug
+## 0.2.14(2021-08-23)
+- 新增 参数中返回 fileID 字段
+## 0.2.13(2021-08-23)
+- 修复 腾讯云传入fileID 不能回显的bug
+- 修复 选择图片后,不能放大的问题
+## 0.2.12(2021-08-17)
+- 修复 由于 0.2.11 版本引起的不能回显图片的Bug
+## 0.2.11(2021-08-16)
+- 新增 clearFiles(index) 方法,可以手动删除指定文件
+- 修复 v-model 值设为 null 报错的Bug
+## 0.2.10(2021-08-13)
+- 修复 return-type="object" 时,无法删除文件的Bug
+## 0.2.9(2021-08-03)
+- 修复 auto-upload 属性失效的Bug
+## 0.2.8(2021-07-31)
+- 修复 fileExtname属性不指定值报错的Bug
+## 0.2.7(2021-07-31)
+- 修复 在某种场景下图片不回显的Bug
+## 0.2.6(2021-07-30)
+- 修复 return-type为object下,返回值不正确的Bug
+## 0.2.5(2021-07-30)
+- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题
+## 0.2.3(2021-07-28)
+- 优化 调整示例代码
+## 0.2.2(2021-07-27)
+- 修复 vue3 下赋值错误的Bug
+- 优化 h5平台下上传文件导致页面卡死的问题
+## 0.2.0(2021-07-13)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 0.1.1(2021-07-02)
+- 修复 sourceType 缺少默认值导致 ios 无法选择文件
+## 0.1.0(2021-06-30)
+- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改
+## 0.0.11(2021-06-30)
+- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题
+## 0.0.10(2021-06-29)
+- 优化 文件上传后进度条消失时机
+## 0.0.9(2021-06-29)
+- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug
+## 0.0.8(2021-06-15)
+- 修复 删除文件时无法触发 v-model 的Bug
+## 0.0.7(2021-05-12)
+- 新增 组件示例地址
+## 0.0.6(2021-04-09)
+- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug
+## 0.0.5(2021-04-09)
+- 优化 更新组件示例
+## 0.0.4(2021-04-09)
+- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔
+## 0.0.3(2021-02-05)
+- 调整为uni_modules目录规范
+- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug

+ 224 - 0
uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js

@@ -0,0 +1,224 @@
+'use strict';
+
+const ERR_MSG_OK = 'chooseAndUploadFile:ok';
+const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
+
+function chooseImage(opts) {
+	const {
+		count,
+		sizeType = ['original', 'compressed'],
+		sourceType = ['album', 'camera'],
+		extension
+	} = opts
+	return new Promise((resolve, reject) => {
+		uni.chooseImage({
+			count,
+			sizeType,
+			sourceType,
+			extension,
+			success(res) {
+				resolve(normalizeChooseAndUploadFileRes(res, 'image'));
+			},
+			fail(res) {
+				reject({
+					errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL),
+				});
+			},
+		});
+	});
+}
+
+function chooseVideo(opts) {
+	const {
+		camera,
+		compressed,
+		maxDuration,
+		sourceType = ['album', 'camera'],
+		extension
+	} = opts;
+	return new Promise((resolve, reject) => {
+		uni.chooseVideo({
+			camera,
+			compressed,
+			maxDuration,
+			sourceType,
+			extension,
+			success(res) {
+				const {
+					tempFilePath,
+					duration,
+					size,
+					height,
+					width
+				} = res;
+				resolve(normalizeChooseAndUploadFileRes({
+					errMsg: 'chooseVideo:ok',
+					tempFilePaths: [tempFilePath],
+					tempFiles: [
+					{
+						name: (res.tempFile && res.tempFile.name) || '',
+						path: tempFilePath,
+						size,
+						type: (res.tempFile && res.tempFile.type) || '',
+						width,
+						height,
+						duration,
+						fileType: 'video',
+						cloudPath: '',
+					}, ],
+				}, 'video'));
+			},
+			fail(res) {
+				reject({
+					errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL),
+				});
+			},
+		});
+	});
+}
+
+function chooseAll(opts) {
+	const {
+		count,
+		extension
+	} = opts;
+	return new Promise((resolve, reject) => {
+		let chooseFile = uni.chooseFile;
+		if (typeof wx !== 'undefined' &&
+			typeof wx.chooseMessageFile === 'function') {
+			chooseFile = wx.chooseMessageFile;
+		}
+		if (typeof chooseFile !== 'function') {
+			return reject({
+				errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。',
+			});
+		}
+		chooseFile({
+			type: 'all',
+			count,
+			extension,
+			success(res) {
+				resolve(normalizeChooseAndUploadFileRes(res));
+			},
+			fail(res) {
+				reject({
+					errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL),
+				});
+			},
+		});
+	});
+}
+
+function normalizeChooseAndUploadFileRes(res, fileType) {
+	res.tempFiles.forEach((item, index) => {
+		if (!item.name) {
+			item.name = item.path.substring(item.path.lastIndexOf('/') + 1);
+		}
+		if (fileType) {
+			item.fileType = fileType;
+		}
+		item.cloudPath =
+			Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.'));
+	});
+	if (!res.tempFilePaths) {
+		res.tempFilePaths = res.tempFiles.map((file) => file.path);
+	}
+	return res;
+}
+
+function uploadCloudFiles(files, max = 5, onUploadProgress) {
+	files = JSON.parse(JSON.stringify(files))
+	const len = files.length
+	let count = 0
+	let self = this
+	return new Promise(resolve => {
+		while (count < max) {
+			next()
+		}
+
+		function next() {
+			let cur = count++
+			if (cur >= len) {
+				!files.find(item => !item.url && !item.errMsg) && resolve(files)
+				return
+			}
+			const fileItem = files[cur]
+			const index = self.files.findIndex(v => v.uuid === fileItem.uuid)
+			fileItem.url = ''
+			delete fileItem.errMsg
+
+			uniCloud
+				.uploadFile({
+					filePath: fileItem.path,
+					cloudPath: fileItem.cloudPath,
+					fileType: fileItem.fileType,
+					onUploadProgress: res => {
+						res.index = index
+						onUploadProgress && onUploadProgress(res)
+					}
+				})
+				.then(res => {
+					fileItem.url = res.fileID
+					fileItem.index = index
+					if (cur < len) {
+						next()
+					}
+				})
+				.catch(res => {
+					fileItem.errMsg = res.errMsg || res.message
+					fileItem.index = index
+					if (cur < len) {
+						next()
+					}
+				})
+		}
+	})
+}
+
+
+
+
+
+function uploadFiles(choosePromise, {
+	onChooseFile,
+	onUploadProgress
+}) {
+	return choosePromise
+		.then((res) => {
+			if (onChooseFile) {
+				const customChooseRes = onChooseFile(res);
+				if (typeof customChooseRes !== 'undefined') {
+					return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ?
+						res : chooseRes);
+				}
+			}
+			return res;
+		})
+		.then((res) => {
+			if (res === false) {
+				return {
+					errMsg: ERR_MSG_OK,
+					tempFilePaths: [],
+					tempFiles: [],
+				};
+			}
+			return res
+		})
+}
+
+function chooseAndUploadFile(opts = {
+	type: 'all'
+}) {
+	if (opts.type === 'image') {
+		return uploadFiles(chooseImage(opts), opts);
+	}
+	else if (opts.type === 'video') {
+		return uploadFiles(chooseVideo(opts), opts);
+	}
+	return uploadFiles(chooseAll(opts), opts);
+}
+
+export {
+	chooseAndUploadFile,
+	uploadCloudFiles
+};

+ 650 - 0
uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue

@@ -0,0 +1,650 @@
+<template>
+	<view class="uni-file-picker">
+		<view v-if="title" class="uni-file-picker__header">
+			<text class="file-title">{{ title }}</text>
+			<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
+		</view>
+		<upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly"
+			:image-styles="imageStyles" :files-list="filesList" :limit="limitLength" :disablePreview="disablePreview"
+			:delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
+			<slot>
+				<view class="is-add">
+					<view class="icon-add"></view>
+					<view class="icon-add rotate"></view>
+				</view>
+			</slot>
+		</upload-image>
+		<upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly"
+			:list-styles="listStyles" :files-list="filesList" :showType="showType" :delIcon="delIcon"
+			@uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
+			<slot><button type="primary" size="mini">选择文件</button></slot>
+		</upload-file>
+	</view>
+</template>
+
+<script>
+	import {
+		chooseAndUploadFile,
+		uploadCloudFiles
+	} from './choose-and-upload-file.js'
+	import {
+		get_file_ext,
+		get_extname,
+		get_files_and_is_max,
+		get_file_info,
+		get_file_data
+	} from './utils.js'
+	import uploadImage from './upload-image.vue'
+	import uploadFile from './upload-file.vue'
+	let fileInput = null
+	/**
+	 * FilePicker 文件选择上传
+	 * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=4079
+	 * @property {Object|Array}	value	组件数据,通常用来回显 ,类型由return-type属性决定
+	 * @property {Boolean}	disabled = [true|false]	组件禁用
+	 * 	@value true 	禁用
+	 * 	@value false 	取消禁用
+	 * @property {Boolean}	readonly = [true|false]	组件只读,不可选择,不显示进度,不显示删除按钮
+	 * 	@value true 	只读
+	 * 	@value false 	取消只读
+	 * @property {String}	return-type = [array|object]	限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖
+	 * 	@value array	规定 value 属性的类型为数组
+	 * 	@value object	规定 value 属性的类型为对象
+	 * @property {Boolean}	disable-preview = [true|false]	禁用图片预览,仅 mode:grid 时生效
+	 * 	@value true 	禁用图片预览
+	 * 	@value false 	取消禁用图片预览
+	 * @property {Boolean}	del-icon = [true|false]	是否显示删除按钮
+	 * 	@value true 	显示删除按钮
+	 * 	@value false 	不显示删除按钮
+	 * @property {Boolean}	auto-upload = [true|false]	是否自动上传,值为true则只触发@select,可自行上传
+	 * 	@value true 	自动上传
+	 * 	@value false 	取消自动上传
+	 * @property {Number|String}	limit	最大选择个数 ,h5 会自动忽略多选的部分
+	 * @property {String}	title	组件标题,右侧显示上传计数
+	 * @property {String}	mode = [list|grid]	选择文件后的文件列表样式
+	 * 	@value list 	列表显示
+	 * 	@value grid 	宫格显示
+	 * @property {String}	file-mediatype = [image|video|all]	选择文件类型
+	 * 	@value image	只选择图片
+	 * 	@value video	只选择视频
+	 * 	@value all		选择所有文件
+	 * @property {Array}	file-extname	选择文件后缀,根据 file-mediatype 属性而不同
+	 * @property {Object}	list-style	mode:list 时的样式
+	 * @property {Object}	image-styles	选择文件后缀,根据 file-mediatype 属性而不同
+	 * @event {Function} select 	选择文件后触发
+	 * @event {Function} progress 文件上传时触发
+	 * @event {Function} success 	上传成功触发
+	 * @event {Function} fail 		上传失败触发
+	 * @event {Function} delete 	文件从列表移除时触发
+	 */
+	export default {
+		name: 'uniFilePicker',
+		components: {
+			uploadImage,
+			uploadFile
+		},
+		emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'],
+		props: {
+			// #ifdef VUE3
+			modelValue: {
+				type: [Array, Object],
+				default () {
+					return []
+				}
+			},
+			// #endif
+
+			// #ifndef VUE3
+			value: {
+				type: [Array, Object],
+				default () {
+					return []
+				}
+			},
+			// #endif
+
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			disablePreview: {
+				type: Boolean,
+				default: false
+			},
+			delIcon: {
+				type: Boolean,
+				default: true
+			},
+			// 自动上传
+			autoUpload: {
+				type: Boolean,
+				default: true
+			},
+			// 最大选择个数 ,h5只能限制单选或是多选
+			limit: {
+				type: [Number, String],
+				default: 9
+			},
+			// 列表样式 grid | list | list-card
+			mode: {
+				type: String,
+				default: 'grid'
+			},
+			// 选择文件类型  image/video/all
+			fileMediatype: {
+				type: String,
+				default: 'image'
+			},
+			// 文件类型筛选
+			fileExtname: {
+				type: [Array, String],
+				default () {
+					return []
+				}
+			},
+			title: {
+				type: String,
+				default: ''
+			},
+			listStyles: {
+				type: Object,
+				default () {
+					return {
+						// 是否显示边框
+						border: true,
+						// 是否显示分隔线
+						dividline: true,
+						// 线条样式
+						borderStyle: {}
+					}
+				}
+			},
+			imageStyles: {
+				type: Object,
+				default () {
+					return {
+						width: 'auto',
+						height: 'auto'
+					}
+				}
+			},
+			readonly: {
+				type: Boolean,
+				default: false
+			},
+			returnType: {
+				type: String,
+				default: 'array'
+			},
+			sizeType: {
+				type: Array,
+				default () {
+					return ['original', 'compressed']
+				}
+			}
+		},
+		data() {
+			return {
+				files: [],
+				localValue: []
+			}
+		},
+		watch: {
+			// #ifndef VUE3
+			value: {
+				handler(newVal, oldVal) {
+					this.setValue(newVal, oldVal)
+				},
+				immediate: true
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				handler(newVal, oldVal) {
+					this.setValue(newVal, oldVal)
+				},
+				immediate: true
+			},
+			// #endif
+		},
+		computed: {
+			filesList() {
+				let files = []
+				this.files.forEach(v => {
+					files.push(v)
+				})
+				return files
+			},
+			showType() {
+				if (this.fileMediatype === 'image') {
+					return this.mode
+				}
+				return 'list'
+			},
+			limitLength() {
+				if (this.returnType === 'object') {
+					return 1
+				}
+				if (!this.limit) {
+					return 1
+				}
+				if (this.limit >= 9) {
+					return 9
+				}
+				return this.limit
+			}
+		},
+		created() {
+			// TODO 兼容不开通服务空间的情况
+			if (!(uniCloud.config && uniCloud.config.provider)) {
+				this.noSpace = true
+				uniCloud.chooseAndUploadFile = chooseAndUploadFile
+			}
+			this.form = this.getForm('uniForms')
+			this.formItem = this.getForm('uniFormsItem')
+			if (this.form && this.formItem) {
+				if (this.formItem.name) {
+					this.rename = this.formItem.name
+					this.form.inputChildrens.push(this)
+				}
+			}
+		},
+		methods: {
+			/**
+			 * 公开用户使用,清空文件
+			 * @param {Object} index
+			 */
+			clearFiles(index) {
+				if (index !== 0 && !index) {
+					this.files = []
+					this.$nextTick(() => {
+						this.setEmit()
+					})
+				} else {
+					this.files.splice(index, 1)
+				}
+				this.$nextTick(() => {
+					this.setEmit()
+				})
+			},
+			/**
+			 * 公开用户使用,继续上传
+			 */
+			upload() {
+				let files = []
+				this.files.forEach((v, index) => {
+					if (v.status === 'ready' || v.status === 'error') {
+						files.push(Object.assign({}, v))
+					}
+				})
+				this.uploadFiles(files)
+			},
+			async setValue(newVal, oldVal) {
+				const newData =  async (v) => {
+					const reg = /cloud:\/\/([\w.]+\/?)\S*/
+					let url = ''
+					if(v.fileID){
+						url = v.fileID
+					}else{
+						url = v.url
+					}
+					if (reg.test(url)) {
+						v.fileID = url
+						v.url = await this.getTempFileURL(url)
+					}
+					if(v.url) v.path = v.url
+					return v
+				}
+				if (this.returnType === 'object') {
+					if (newVal) {
+						await newData(newVal)
+					} else {
+						newVal = {}
+					}
+				} else {
+					if (!newVal) newVal = []
+					for(let i =0 ;i < newVal.length ;i++){
+						let v = newVal[i]
+						await newData(v)
+					}
+				}
+				this.localValue = newVal
+				if (this.form && this.formItem &&!this.is_reset) {
+					this.is_reset = false
+					this.formItem.setValue(this.localValue)
+				}
+				let filesData = Object.keys(newVal).length > 0 ? newVal : [];
+				this.files = [].concat(filesData)
+			},
+
+			/**
+			 * 选择文件
+			 */
+			choose() {
+
+				if (this.disabled) return
+				if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType ===
+					'array') {
+					uni.showToast({
+						title: `您最多选择 ${this.limitLength} 个文件`,
+						icon: 'none'
+					})
+					return
+				}
+				this.chooseFiles()
+			},
+
+			/**
+			 * 选择文件并上传
+			 */
+			chooseFiles() {
+				const _extname = get_extname(this.fileExtname)
+				// 获取后缀
+				uniCloud
+					.chooseAndUploadFile({
+						type: this.fileMediatype,
+						compressed: false,
+						sizeType: this.sizeType,
+						// TODO 如果为空,video 有问题
+						extension: _extname.length > 0 ? _extname : undefined,
+						count: this.limitLength - this.files.length, //默认9
+						onChooseFile: this.chooseFileCallback,
+						onUploadProgress: progressEvent => {
+							this.setProgress(progressEvent, progressEvent.index)
+						}
+					})
+					.then(result => {
+						this.setSuccessAndError(result.tempFiles)
+					})
+					.catch(err => {
+						console.log('选择失败', err)
+					})
+			},
+
+			/**
+			 * 选择文件回调
+			 * @param {Object} res
+			 */
+			async chooseFileCallback(res) {
+				const _extname = get_extname(this.fileExtname)
+				const is_one = (Number(this.limitLength) === 1 &&
+						this.disablePreview &&
+						!this.disabled) ||
+					this.returnType === 'object'
+				// 如果这有一个文件 ,需要清空本地缓存数据
+				if (is_one) {
+					this.files = []
+				}
+
+				let {
+					filePaths,
+					files
+				} = get_files_and_is_max(res, _extname)
+				if (!(_extname && _extname.length > 0)) {
+					filePaths = res.tempFilePaths
+					files = res.tempFiles
+				}
+
+				let currentData = []
+				for (let i = 0; i < files.length; i++) {
+					if (this.limitLength - this.files.length <= 0) break
+					files[i].uuid = Date.now()
+					let filedata = await get_file_data(files[i], this.fileMediatype)
+					filedata.progress = 0
+					filedata.status = 'ready'
+					this.files.push(filedata)
+					currentData.push({
+						...filedata,
+						file: files[i]
+					})
+				}
+				this.$emit('select', {
+					tempFiles: currentData,
+					tempFilePaths: filePaths
+				})
+				res.tempFiles = files
+				// 停止自动上传
+				if (!this.autoUpload || this.noSpace) {
+					res.tempFiles = []
+				}
+			},
+
+			/**
+			 * 批传
+			 * @param {Object} e
+			 */
+			uploadFiles(files) {
+				files = [].concat(files)
+				uploadCloudFiles.call(this, files, 5, res => {
+						this.setProgress(res, res.index, true)
+					})
+					.then(result => {
+						this.setSuccessAndError(result)
+					})
+					.catch(err => {
+						console.log(err)
+					})
+			},
+
+			/**
+			 * 成功或失败
+			 */
+			async setSuccessAndError(res, fn) {
+				let successData = []
+				let errorData = []
+				let tempFilePath = []
+				let errorTempFilePath = []
+				for (let i = 0; i < res.length; i++) {
+					const item = res[i]
+					const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index
+
+					if (index === -1 || !this.files) break
+					if (item.errMsg === 'request:fail') {
+						this.files[index].url = item.path
+						this.files[index].status = 'error'
+						this.files[index].errMsg = item.errMsg
+						// this.files[index].progress = -1
+						errorData.push(this.files[index])
+						errorTempFilePath.push(this.files[index].url)
+					} else {
+						this.files[index].errMsg = ''
+						this.files[index].fileID = item.url
+						const reg = /cloud:\/\/([\w.]+\/?)\S*/
+						if (reg.test(item.url)) {
+							this.files[index].url = await this.getTempFileURL(item.url)
+						}else{
+							this.files[index].url = item.url
+						}
+
+						this.files[index].status = 'success'
+						this.files[index].progress += 1
+						successData.push(this.files[index])
+						tempFilePath.push(this.files[index].fileID)
+					}
+				}
+
+				if (successData.length > 0) {
+					this.setEmit()
+					// 状态改变返回
+					this.$emit('success', {
+						tempFiles: this.backObject(successData),
+						tempFilePaths: tempFilePath
+					})
+				}
+
+				if (errorData.length > 0) {
+					this.$emit('fail', {
+						tempFiles: this.backObject(errorData),
+						tempFilePaths: errorTempFilePath
+					})
+				}
+			},
+
+			/**
+			 * 获取进度
+			 * @param {Object} progressEvent
+			 * @param {Object} index
+			 * @param {Object} type
+			 */
+			setProgress(progressEvent, index, type) {
+				const fileLenth = this.files.length
+				const percentNum = (index / fileLenth) * 100
+				const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+				let idx = index
+				if (!type) {
+					idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid)
+				}
+				if (idx === -1 || !this.files[idx]) return
+				// fix by mehaotian 100 就会消失,-1 是为了让进度条消失
+				this.files[idx].progress = percentCompleted - 1
+				// 上传中
+				this.$emit('progress', {
+					index: idx,
+					progress: parseInt(percentCompleted),
+					tempFile: this.files[idx]
+				})
+			},
+
+			/**
+			 * 删除文件
+			 * @param {Object} index
+			 */
+			delFile(index) {
+				this.$emit('delete', {
+					tempFile: this.files[index],
+					tempFilePath: this.files[index].url
+				})
+				this.files.splice(index, 1)
+				this.$nextTick(() => {
+					this.setEmit()
+				})
+			},
+
+			/**
+			 * 获取文件名和后缀
+			 * @param {Object} name
+			 */
+			getFileExt(name) {
+				const last_len = name.lastIndexOf('.')
+				const len = name.length
+				return {
+					name: name.substring(0, last_len),
+					ext: name.substring(last_len + 1, len)
+				}
+			},
+
+			/**
+			 * 处理返回事件
+			 */
+			setEmit() {
+				let data = []
+				if (this.returnType === 'object') {
+					data = this.backObject(this.files)[0]
+					this.localValue = data?data:null
+				} else {
+					data = this.backObject(this.files)
+					if (!this.localValue) {
+						this.localValue = []
+					}
+					this.localValue = [...data]
+				}
+				// #ifdef VUE3
+				this.$emit('update:modelValue', this.localValue)
+				// #endif
+				// #ifndef VUE3
+				this.$emit('input', this.localValue)
+				// #endif
+			},
+
+			/**
+			 * 处理返回参数
+			 * @param {Object} files
+			 */
+			backObject(files) {
+				let newFilesData = []
+				files.forEach(v => {
+					newFilesData.push({
+						extname: v.extname,
+						fileType: v.fileType,
+						image: v.image,
+						name: v.name,
+						path: v.path,
+						size: v.size,
+						fileID:v.fileID,
+						url: v.url
+					})
+				})
+				return newFilesData
+			},
+			async getTempFileURL(fileList) {
+				fileList = {
+					fileList: [].concat(fileList)
+				}
+				const urls = await uniCloud.getTempFileURL(fileList)
+				return urls.fileList[0].tempFileURL || ''
+			},
+			/**
+			 * 获取父元素实例
+			 */
+			getForm(name = 'uniForms') {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== name) {
+					parent = parent.$parent;
+					if (!parent) return false;
+					parentName = parent.$options.name;
+				}
+				return parent;
+			}
+		}
+	}
+</script>
+
+<style>
+	.uni-file-picker {
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		overflow: hidden;
+		/* #endif */
+	}
+
+	.uni-file-picker__header {
+		padding-top: 5px;
+		padding-bottom: 10px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: space-between;
+	}
+
+	.file-title {
+		font-size: 14px;
+		color: #333;
+	}
+
+	.file-count {
+		font-size: 14px;
+		color: #999;
+	}
+
+	.is-add {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+	}
+
+	.icon-add {
+		width: 50px;
+		height: 5px;
+		background-color: #f1f1f1;
+		border-radius: 2px;
+	}
+
+	.rotate {
+		position: absolute;
+		transform: rotate(90deg);
+	}
+</style>

+ 325 - 0
uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue

@@ -0,0 +1,325 @@
+<template>
+	<view class="uni-file-picker__files">
+		<view v-if="!readonly" class="files-button" @click="choose">
+			<slot></slot>
+		</view>
+		<!-- :class="{'is-text-box':showType === 'list'}" -->
+		<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
+			<!-- ,'is-list-card':showType === 'list-card' -->
+
+			<view class="uni-file-picker__lists-box" v-for="(item ,index) in list" :key="index" :class="{
+				'files-border':index !== 0 && styles.dividline}"
+			 :style="index !== 0 && styles.dividline &&borderLineStyle">
+				<view class="uni-file-picker__item">
+					<!-- :class="{'is-text-image':showType === 'list'}" -->
+					<!-- 	<view class="files__image is-text-image">
+						<image class="header-image" :src="item.logo" mode="aspectFit"></image>
+					</view> -->
+					<view class="files__name">{{item.name}}</view>
+					<view v-if="delIcon&&!readonly" class="icon-del-box icon-files" @click="delFile(index)">
+						<view class="icon-del icon-files"></view>
+						<view class="icon-del rotate"></view>
+					</view>
+				</view>
+				<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
+					<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
+					 :backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
+				</view>
+				<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
+					点击重试
+				</view>
+			</view>
+
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "uploadFile",
+		emits:['uploadFiles','choose','delFile'],
+		props: {
+			filesList: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			delIcon: {
+				type: Boolean,
+				default: true
+			},
+			limit: {
+				type: [Number, String],
+				default: 9
+			},
+			showType: {
+				type: String,
+				default: ''
+			},
+			listStyles: {
+				type: Object,
+				default () {
+					return {
+						// 是否显示边框
+						border: true,
+						// 是否显示分隔线
+						dividline: true,
+						// 线条样式
+						borderStyle: {}
+					}
+				}
+			},
+			readonly:{
+				type:Boolean,
+				default:false
+			}
+		},
+		computed: {
+			list() {
+				let files = []
+				this.filesList.forEach(v => {
+					files.push(v)
+				})
+				return files
+			},
+			styles() {
+				let styles = {
+					border: true,
+					dividline: true,
+					'border-style': {}
+				}
+				return Object.assign(styles, this.listStyles)
+			},
+			borderStyle() {
+				let {
+					borderStyle,
+					border
+				} = this.styles
+				let obj = {}
+				if (!border) {
+					obj.border = 'none'
+				} else {
+					let width = (borderStyle && borderStyle.width) || 1
+					width = this.value2px(width)
+					let radius = (borderStyle && borderStyle.radius) || 5
+					radius = this.value2px(radius)
+					obj = {
+						'border-width': width,
+						'border-style': (borderStyle && borderStyle.style) || 'solid',
+						'border-color': (borderStyle && borderStyle.color) || '#eee',
+						'border-radius': radius
+					}
+				}
+				let classles = ''
+				for (let i in obj) {
+					classles += `${i}:${obj[i]};`
+				}
+				return classles
+			},
+			borderLineStyle() {
+				let obj = {}
+				let {
+					borderStyle
+				} = this.styles
+				if (borderStyle && borderStyle.color) {
+					obj['border-color'] = borderStyle.color
+				}
+				if (borderStyle && borderStyle.width) {
+					let width = borderStyle && borderStyle.width || 1
+					let style = borderStyle && borderStyle.style || 0
+					if (typeof width === 'number') {
+						width += 'px'
+					} else {
+						width = width.indexOf('px') ? width : width + 'px'
+					}
+					obj['border-width'] = width
+
+					if (typeof style === 'number') {
+						style += 'px'
+					} else {
+						style = style.indexOf('px') ? style : style + 'px'
+					}
+					obj['border-top-style'] = style
+				}
+				let classles = ''
+				for (let i in obj) {
+					classles += `${i}:${obj[i]};`
+				}
+				return classles
+			}
+		},
+
+		methods: {
+			uploadFiles(item, index) {
+				this.$emit("uploadFiles", {
+					item,
+					index
+				})
+			},
+			choose() {
+				this.$emit("choose")
+			},
+			delFile(index) {
+				this.$emit('delFile', index)
+			},
+			value2px(value) {
+				if (typeof value === 'number') {
+					value += 'px'
+				} else {
+					value = value.indexOf('px') !== -1 ? value : value + 'px'
+				}
+				return value
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-file-picker__files {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: flex-start;
+	}
+
+	.files-button {
+		// border: 1px red solid;
+	}
+
+	.uni-file-picker__lists {
+		position: relative;
+		margin-top: 5px;
+		overflow: hidden;
+	}
+
+	.file-picker__mask {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center;
+		position: absolute;
+		right: 0;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		color: #fff;
+		font-size: 14px;
+		background-color: rgba(0, 0, 0, 0.4);
+	}
+
+	.uni-file-picker__lists-box {
+		position: relative;
+	}
+
+	.uni-file-picker__item {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+		padding: 8px 10px;
+		padding-right: 5px;
+		padding-left: 10px;
+	}
+
+	.files-border {
+		border-top: 1px #eee solid;
+	}
+
+	.files__name {
+		flex: 1;
+		font-size: 14px;
+		color: #666;
+		margin-right: 25px;
+		/* #ifndef APP-NVUE */
+		word-break: break-all;
+		word-wrap: break-word;
+		/* #endif */
+	}
+
+	.icon-files {
+		/* #ifndef APP-NVUE */
+		position: static;
+		background-color: initial;
+		/* #endif */
+	}
+
+	// .icon-files .icon-del {
+	// 	background-color: #333;
+	// 	width: 12px;
+	// 	height: 1px;
+	// }
+
+
+	.is-list-card {
+		border: 1px #eee solid;
+		margin-bottom: 5px;
+		border-radius: 5px;
+		box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
+		padding: 5px;
+	}
+
+	.files__image {
+		width: 40px;
+		height: 40px;
+		margin-right: 10px;
+	}
+
+	.header-image {
+		width: 100%;
+		height: 100%;
+	}
+
+	.is-text-box {
+		border: 1px #eee solid;
+		border-radius: 5px;
+	}
+
+	.is-text-image {
+		width: 25px;
+		height: 25px;
+		margin-left: 5px;
+	}
+
+	.rotate {
+		position: absolute;
+		transform: rotate(90deg);
+	}
+
+	.icon-del-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		margin: auto 0;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+		position: absolute;
+		top: 0px;
+		bottom: 0;
+		right: 5px;
+		height: 26px;
+		width: 26px;
+		// border-radius: 50%;
+		// background-color: rgba(0, 0, 0, 0.5);
+		z-index: 2;
+		transform: rotate(-45deg);
+	}
+
+	.icon-del {
+		width: 15px;
+		height: 1px;
+		background-color: #333;
+		// border-radius: 1px;
+	}
+
+	/* #ifdef H5 */
+	@media all and (min-width: 768px) {
+		.uni-file-picker__files {
+			max-width: 375px;
+		}
+	}
+
+	/* #endif */
+</style>

+ 292 - 0
uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue

@@ -0,0 +1,292 @@
+<template>
+	<view class="uni-file-picker__container">
+		<view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle">
+			<view class="file-picker__box-content" :style="borderStyle">
+				<image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image>
+				<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
+					<view class="icon-del"></view>
+					<view class="icon-del rotate"></view>
+				</view>
+				<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
+					<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
+					 :backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
+				</view>
+				<view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
+					点击重试
+				</view>
+			</view>
+		</view>
+		<view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
+			<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
+				<slot>
+					<view class="icon-add"></view>
+					<view class="icon-add rotate"></view>
+				</slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "uploadImage",
+		emits:['uploadFiles','choose','delFile'],
+		props: {
+			filesList: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			disabled:{
+				type: Boolean,
+				default: false
+			},
+			disablePreview: {
+				type: Boolean,
+				default: false
+			},
+			limit: {
+				type: [Number, String],
+				default: 9
+			},
+			imageStyles: {
+				type: Object,
+				default () {
+					return {
+						width: 'auto',
+						height: 'auto',
+						border: {}
+					}
+				}
+			},
+			delIcon: {
+				type: Boolean,
+				default: true
+			},
+			readonly:{
+				type:Boolean,
+				default:false
+			}
+		},
+		computed: {
+			styles() {
+				let styles = {
+					width: 'auto',
+					height: 'auto',
+					border: {}
+				}
+				return Object.assign(styles, this.imageStyles)
+			},
+			boxStyle() {
+				const {
+					width = 'auto',
+						height = 'auto'
+				} = this.styles
+				let obj = {}
+				if (height === 'auto') {
+					if (width !== 'auto') {
+						obj.height = this.value2px(width)
+						obj['padding-top'] = 0
+					} else {
+						obj.height = 0
+					}
+				} else {
+					obj.height = this.value2px(height)
+					obj['padding-top'] = 0
+				}
+
+				if (width === 'auto') {
+					if (height !== 'auto') {
+						obj.width = this.value2px(height)
+					} else {
+						obj.width = '33.3%'
+					}
+				} else {
+					obj.width = this.value2px(width)
+				}
+
+				let classles = ''
+				for(let i in obj){
+					classles+= `${i}:${obj[i]};`
+				}
+				return classles
+			},
+			borderStyle() {
+				let {
+					border
+				} = this.styles
+				let obj = {}
+				const widthDefaultValue = 1
+				const radiusDefaultValue = 3
+				if (typeof border === 'boolean') {
+					obj.border = border ? '1px #eee solid' : 'none'
+				} else {
+					let width = (border && border.width) || widthDefaultValue
+					width = this.value2px(width)
+					let radius = (border && border.radius) || radiusDefaultValue
+					radius = this.value2px(radius)
+					obj = {
+						'border-width': width,
+						'border-style': (border && border.style) || 'solid',
+						'border-color': (border && border.color) || '#eee',
+						'border-radius': radius
+					}
+				}
+				let classles = ''
+				for(let i in obj){
+					classles+= `${i}:${obj[i]};`
+				}
+				return classles
+			}
+		},
+		methods: {
+			uploadFiles(item, index) {
+				this.$emit("uploadFiles", item)
+			},
+			choose() {
+				this.$emit("choose")
+			},
+			delFile(index) {
+				this.$emit('delFile', index)
+			},
+			prviewImage(img, index) {
+				let urls = []
+				if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){
+					this.$emit("choose")
+				}
+				if(this.disablePreview) return
+				this.filesList.forEach(i => {
+					urls.push(i.url)
+				})
+
+				uni.previewImage({
+					urls: urls,
+					current: index
+				});
+			},
+			value2px(value) {
+				if (typeof value === 'number') {
+					value += 'px'
+				} else {
+					if (value.indexOf('%') === -1) {
+						value = value.indexOf('px') !== -1 ? value : value + 'px'
+					}
+				}
+				return value
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.uni-file-picker__container {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-wrap: wrap;
+		margin: -5px;
+	}
+
+	.file-picker__box {
+		position: relative;
+		// flex: 0 0 33.3%;
+		width: 33.3%;
+		height: 0;
+		padding-top: 33.33%;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		/* #endif */
+	}
+
+	.file-picker__box-content {
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		margin: 5px;
+		border: 1px #eee solid;
+		border-radius: 5px;
+		overflow: hidden;
+	}
+
+	.file-picker__progress {
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		/* border: 1px red solid; */
+		z-index: 2;
+	}
+
+	.file-picker__progress-item {
+		width: 100%;
+	}
+
+	.file-picker__mask {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center;
+		position: absolute;
+		right: 0;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		color: #fff;
+		font-size: 12px;
+		background-color: rgba(0, 0, 0, 0.4);
+	}
+
+	.file-image {
+		width: 100%;
+		height: 100%;
+	}
+
+	.is-add {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+	}
+
+	.icon-add {
+		width: 50px;
+		height: 5px;
+		background-color: #f1f1f1;
+		border-radius: 2px;
+	}
+
+	.rotate {
+		position: absolute;
+		transform: rotate(90deg);
+	}
+
+	.icon-del-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+		justify-content: center;
+		position: absolute;
+		top: 3px;
+		right: 3px;
+		height: 26px;
+		width: 26px;
+		border-radius: 50%;
+		background-color: rgba(0, 0, 0, 0.5);
+		z-index: 2;
+		transform: rotate(-45deg);
+	}
+
+	.icon-del {
+		width: 15px;
+		height: 2px;
+		background-color: #fff;
+		border-radius: 2px;
+	}
+</style>

+ 109 - 0
uni_modules/uni-file-picker/components/uni-file-picker/utils.js

@@ -0,0 +1,109 @@
+/**
+ * 获取文件名和后缀
+ * @param {String} name
+ */
+export const get_file_ext = (name) => {
+	const last_len = name.lastIndexOf('.')
+	const len = name.length
+	return {
+		name: name.substring(0, last_len),
+		ext: name.substring(last_len + 1, len)
+	}
+}
+
+/**
+ * 获取扩展名
+ * @param {Array} fileExtname
+ */
+export const get_extname = (fileExtname) => {
+	if (!Array.isArray(fileExtname)) {
+		let extname = fileExtname.replace(/(\[|\])/g, '')
+		return extname.split(',')
+	} else {
+		return fileExtname
+	}
+	return []
+}
+
+/**
+ * 获取文件和检测是否可选
+ */
+export const get_files_and_is_max = (res, _extname) => {
+	let filePaths = []
+	let files = []
+	if(!_extname || _extname.length === 0){
+		return {
+			filePaths,
+			files
+		}
+	}
+	res.tempFiles.forEach(v => {
+		let fileFullName = get_file_ext(v.name)
+		const extname = fileFullName.ext.toLowerCase()
+		if (_extname.indexOf(extname) !== -1) {
+			files.push(v)
+			filePaths.push(v.path)
+		}
+	})
+	if (files.length !== res.tempFiles.length) {
+		uni.showToast({
+			title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`,
+			icon: 'none',
+			duration: 5000
+		})
+	}
+
+	return {
+		filePaths,
+		files
+	}
+}
+
+
+/**
+ * 获取图片信息
+ * @param {Object} filepath
+ */
+export const get_file_info = (filepath) => {
+	return new Promise((resolve, reject) => {
+		uni.getImageInfo({
+			src: filepath,
+			success(res) {
+				resolve(res)
+			},
+			fail(err) {
+				reject(err)
+			}
+		})
+	})
+}
+/**
+ * 获取封装数据
+ */
+export const get_file_data = async (files, type = 'image') => {
+	// 最终需要上传数据库的数据
+	let fileFullName = get_file_ext(files.name)
+	const extname = fileFullName.ext.toLowerCase()
+	let filedata = {
+		name: files.name,
+		uuid: files.uuid,
+		extname: extname || '',
+		cloudPath: files.cloudPath,
+		fileType: files.fileType,
+		url: files.path || files.path,
+		size: files.size, //单位是字节
+		image: {},
+		path: files.path,
+		video: {}
+	}
+	if (type === 'image') {
+		const imageinfo = await get_file_info(files.path)
+		delete filedata.video
+		filedata.image.width = imageinfo.width
+		filedata.image.height = imageinfo.height
+		filedata.image.location = imageinfo.path
+	} else {
+		delete filedata.image
+	}
+	return filedata
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно