Browse Source

初始化项目

haveyou 1 year ago
parent
commit
c28294be6f
100 changed files with 7426 additions and 0 deletions
  1. 23 0
      .hbuilderx/launch.json
  2. 40 0
      App.vue
  3. 13 0
      README.md
  4. 115 0
      components/cy-upload/index.vue
  5. 135 0
      components/pageScroll/index.vue
  6. 5 0
      config/api.js
  7. 262 0
      config/hooks.js
  8. 69 0
      config/request.js
  9. 29 0
      config/share.js
  10. 20 0
      index.html
  11. 42 0
      main.js
  12. 104 0
      manifest.json
  13. BIN
      node_modules/.pnpm/@esbuild+win32-x64@0.20.2/node_modules/@esbuild/win32-x64/esbuild.exe
  14. BIN
      node_modules/.pnpm/@rollup+rollup-win32-x64-msvc@4.18.0/node_modules/@rollup/rollup-win32-x64-msvc/rollup.win32-x64-msvc.node
  15. 290 0
      pages.json
  16. 192 0
      pages/book/components/feedbackPopup.vue
  17. 216 0
      pages/book/index.vue
  18. 3 0
      pages/index/audit/confirm-receipt.vue
  19. 3 0
      pages/index/audit/express-order.vue
  20. 3 0
      pages/index/audit/scan-order.vue
  21. 3 0
      pages/index/audit/sender.vue
  22. 3 0
      pages/index/entry/book-weight.vue
  23. 3 0
      pages/index/entry/scan-book.vue
  24. 228 0
      pages/index/express/components/SelectWarehouse.vue
  25. 3 0
      pages/index/express/quick-check.vue
  26. 3 0
      pages/index/express/quick-unpack.vue
  27. 3 0
      pages/index/express/route-exception.vue
  28. 181 0
      pages/index/express/transfer-sign.vue
  29. 3 0
      pages/index/express/warehouse-sign.vue
  30. 3 0
      pages/index/express/weight-modify.vue
  31. 300 0
      pages/index/index.vue
  32. 3 0
      pages/index/offline/check-order.vue
  33. 3 0
      pages/index/offline/check-record.vue
  34. 3 0
      pages/index/statistic/after-sale.vue
  35. 3 0
      pages/index/statistic/audit.vue
  36. 3 0
      pages/index/statistic/package.vue
  37. 3 0
      pages/index/wms/bad-in.vue
  38. 3 0
      pages/index/wms/bad-off.vue
  39. 3 0
      pages/index/wms/bad-out.vue
  40. 3 0
      pages/index/wms/good-in.vue
  41. 3 0
      pages/index/wms/location-order.vue
  42. 3 0
      pages/index/wms/medium-in.vue
  43. 3 0
      pages/index/wms/order-query.vue
  44. 3 0
      pages/index/wms/secondary-in.vue
  45. 3 0
      pages/index/wms/speedy-check.vue
  46. 163 0
      pages/my/components/orderItem.vue
  47. 153 0
      pages/my/my.vue
  48. 18 0
      pages/my/page/audit-unfinished.vue
  49. 90 0
      pages/my/page/book-display.vue
  50. 225 0
      pages/my/page/password.vue
  51. 127 0
      pages/my/page/school.vue
  52. 140 0
      pages/my/page/user-info.vue
  53. 51 0
      pages/my/page/version.vue
  54. 420 0
      pages/my/page/volume.vue
  55. 130 0
      pages/my/page/warehouse.vue
  56. 198 0
      pages/order/index.vue
  57. 13 0
      pages/order/stat/pending-audit.vue
  58. 13 0
      pages/order/stat/pending-confirm.vue
  59. 13 0
      pages/order/stat/pending-payment.vue
  60. 13 0
      pages/order/stat/pending-pick.vue
  61. 13 0
      pages/order/stat/pending-review.vue
  62. 13 0
      pages/order/stat/pending-sign.vue
  63. 13 0
      pages/order/stat/receive-stat.vue
  64. 5 0
      static/css/index.scss
  65. 148 0
      static/css/mystyle.css
  66. BIN
      static/tabbar/book.png
  67. BIN
      static/tabbar/bookHl.png
  68. BIN
      static/tabbar/home.png
  69. BIN
      static/tabbar/homeHl.png
  70. BIN
      static/tabbar/mine.png
  71. BIN
      static/tabbar/mineHl.png
  72. BIN
      static/tabbar/order.png
  73. BIN
      static/tabbar/orderHl.png
  74. 10 0
      store/index.js
  75. 34 0
      store/uses.js
  76. 10 0
      uni.promisify.adaptor.js
  77. 78 0
      uni.scss
  78. 8 0
      uni_modules/mescroll-uni/changelog.md
  79. 19 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css
  80. 400 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue
  81. 122 0
      uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue
  82. BIN
      uni_modules/mescroll-uni/components/mescroll-empty/no-result.png
  83. 55 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.css
  84. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue
  85. 99 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue
  86. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.css
  87. 39 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue
  88. 15 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js
  89. 46 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js
  90. 64 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js
  91. 39 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.css
  92. 799 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.js
  93. 480 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue
  94. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js
  95. 57 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js
  96. 77 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js
  97. 109 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/mixins.js
  98. 92 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/renderjs.js
  99. 269 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs
  100. 66 0
      uni_modules/mescroll-uni/hooks/useMescroll.js

+ 23 - 0
.hbuilderx/launch.json

@@ -0,0 +1,23 @@
+{
+    // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+    // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version" : "0.0",
+    "configurations" : [
+        {
+            "app-plus" : {
+                "launchtype" : "local"
+            },
+            "default" : {
+                "launchtype" : "local"
+            },
+            "mp-weixin" : {
+                "launchtype" : "local"
+            },
+            "type" : "uniCloud"
+        },
+        {
+            "playground" : "custom",
+            "type" : "uni-app:app-android"
+        }
+    ]
+}

+ 40 - 0
App.vue

@@ -0,0 +1,40 @@
+<script setup>
+	import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
+	import {
+		store
+	} from '@/store/index.js'
+	onLaunch(() => {
+		console.log('App Launch')
+		// 尝试去获取本地的 token 和用户信息,填充到 store 中
+		if(!store.token) {
+			let token = uni.getStorageSync('token')
+			if(token) {
+				store.setToken(token)
+				let userStr = uni.getStorageSync('userInfo')
+				if(userStr) {
+					let userInfo = JSON.parse(userStr)
+					if(userInfo.userId) {
+						store.setUserInfo(userInfo)
+					}
+				}
+			}
+		}
+	})
+	onShow(() => {
+		console.log('App Show');
+	})
+	onHide(() => {
+		console.log('App Hide');
+	})
+</script>
+
+<style lang="scss">
+	@import "@/uni_modules/uview-plus/index.scss";
+	@import "@/static/css/mystyle.css";
+	@import "@/static/css/index.scss";
+	/*每个页面公共css */
+	page{
+		background: #F5F6FA;
+		height: 100%;
+	}
+</style>

+ 13 - 0
README.md

@@ -0,0 +1,13 @@
+# uniapp-vue3-uview-plus3.0-template
+
+#### 介绍
+基于 uniapp + vue3 + pinia + uview-plus 3.0 的框架搭建的移动端小程序、app开发框架,脚本使用的是 js
+
+#### 技术文档
+
+1.  [uni-app官网](https://uniapp.dcloud.net.cn/api/)
+
+2.  [Vue.js](https://cn.vuejs.org/)
+
+3.  [uview-plus 3.0](https://uiadmin.net/uview-plus/)
+

+ 115 - 0
components/cy-upload/index.vue

@@ -0,0 +1,115 @@
+<template>
+	<u-upload v-bind="$attrs" :fileList="fileList" @afterRead="afterRead" @delete="deletePic" :notFile="notFile" :disabled="disabled">
+	</u-upload>
+	<view class="tips" v-if="tips">{{tips}}</view>
+</template>
+
+<script setup name="cyUpload">
+	import {
+		watch,
+		ref
+	} from 'vue';
+	import baseURL from '@/config/request.js';
+	const emit = defineEmits(['update:filename', 'update:keyValue', 'success'])
+
+	const props = defineProps({
+		filename: {
+			type: [Array, String],
+			default: () => []
+		},
+		url: {
+			type: String
+		},
+		notFile: {
+			type: Boolean,
+			default: false
+		},
+		tips: {
+			type: String
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		isUpdate: {
+			type: Boolean,
+			default: false
+		}
+	})
+	const fileList = ref([])
+
+	watch(() => props.filename, (filename) => {
+		if (!filename) return fileList.value = []
+		if (typeof filename == 'string') {
+			fileList.value = filename.split(',').map(v => ({
+				url: baseURL + v,
+				fileName: v
+			}))
+		} else {
+			fileList.value = filename
+			console.log(fileList.value,'dsadsa')
+		}
+	}, {
+		immediate: true,
+		deep: true
+	})
+
+	const afterRead = async (e) => {
+		const {
+			file
+		} = e
+		fileList.value.push({
+			...file,
+			status: 'uploading',
+			message: '上传中...',
+		});
+		const result = await uploadFilePromise(file.url);
+
+		fileList.value.forEach(v => {
+			v.url = result.url
+			v.fileName = result.fileName
+			v.status = 'success'
+			v.message = ''
+		})
+		//更新外部参数
+		emit('update:filename', fileList)
+		emit('success', result)
+	}
+
+	const deletePic = (event) => {
+		fileList.value.splice(event.index, 1);
+	}
+
+	const uploadFilePromise = (url, baseStr = '/common/upload') => {
+		let token = uni.getStorageSync('token')
+		return new Promise((resolve, reject) => {
+			let a = uni.uploadFile({
+				url: baseURL + baseStr, // 图片上传地址
+				filePath: url,
+				name: 'file',
+				header: {
+					"Authorization": token
+				},
+				success: (res) => {
+					let respic = JSON.parse(res.data)
+					resolve(respic)
+				},
+			});
+		});
+	};
+</script>
+
+<style lang="scss" scoped>
+	.tips {
+		font-size: 24rpx;
+		font-family: PingFangSC, PingFang SC;
+		font-weight: 400;
+		color: #969696;
+		line-height: 48rpx;
+		margin-top: 10rpx;
+	}
+
+	::v-deep .align-right .u-upload__wrap {
+		justify-content: flex-end;
+	}
+</style>

+ 135 - 0
components/pageScroll/index.vue

@@ -0,0 +1,135 @@
+<template>
+	<mescroll-uni @init="mescrollInit" @down="downCallback" @up="upCallback" :height="height||windowHeight+'px'"
+		:up="{textNoMore:'-- 没有更多了 --'}">
+		<slot></slot>
+	</mescroll-uni>
+</template>
+
+<script setup>
+	import {
+		computed,
+		onMounted,
+		ref,
+		watch
+	} from 'vue';
+	import {
+		onShow
+	} from '@dcloudio/uni-app';
+	import useMescroll from "@/uni_modules/mescroll-uni/hooks/useMescroll.js";
+	import {
+		store
+	} from "@/store/index.js"
+
+
+	// 调用mescroll的hook (注: mescroll-uni不用传onPageScroll,onReachBottom, 而mescroll-body必传)
+	const {
+		mescrollInit,
+		downCallback,
+		getMescroll
+	} = useMescroll()
+
+	const windowHeight = computed(() => {
+		return store.clientHeight - props.diffHeight
+	})
+
+	const emit = defineEmits(['updateList', 'update:total'])
+	const props = defineProps({
+		request: {
+			type: Function
+		},
+		requestStr: {
+			type: String
+		},
+		isRefresh: {
+			type: [Number, Boolean],
+			default: 0
+		},
+		height: {
+			type: [String, Number]
+		},
+		total: {
+			type: [Number],
+			default: 0
+		},
+		otherParams: {
+			type: Object,
+			default: () => ({})
+		},
+		diffHeight: {
+			type: Number,
+			default: 0
+		},
+		immediate: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	const list = ref([])
+
+	const getList = (mescroll) => {
+		return new Promise((resolve, reject) => {
+			let params = {
+				pageNum: mescroll.num,
+				pageSize: mescroll.size,
+				...props.otherParams
+			}
+			uni.$u.http.get(props.requestStr, {
+				params
+			}).then(res => {
+				emit('update:total', res.total)
+				resolve(res)
+			})
+		})
+	}
+
+	// 上拉加载的回调: 其中num:当前页 从1开始, size:每页数据条数,默认10
+	const upCallback = (mescroll) => {
+		if (!props.requestStr) return;
+		getList(mescroll).then(res => {
+			const curPageData = res.rows || res.data || [] // 当前页数据
+			if (mescroll.num == 1) list.value = []; // 第一页需手动制空列表
+			list.value = list.value.concat(curPageData); //追加新数据
+			emit('updateList', list.value)
+			//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+			//mescroll会根据传的参数,自动判断列表如果无任何数据,则提示空;列表无下一页数据,则提示无更多数据;
+
+			//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+			//mescroll.endByPage(curPageData.length, totalPage); //必传参数(当前页的数据个数, 总页数)
+
+			//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+			mescroll.endBySize(curPageData.length, res.total); //必传参数(当前页的数据个数, 总数据量)
+
+			//方法三(推荐): 您有其他方式知道是否有下一页 hasNext
+			//mescroll.endSuccess(curPageData.length, hasNext); //必传参数(当前页的数据个数, 是否有下一页true/false)
+
+			//方法四 (不推荐),会存在一个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据当前页的数据个数判断,则需翻到第三页才会知道无更多数据.
+			// mescroll.endSuccess(curPageData.length); // 请求成功, 结束加载
+		}).catch(() => {
+			mescroll.endErr(); // 请求失败, 结束加载
+		})
+	}
+
+	//重置列表
+	const resetUpScroll = () => {
+		emit('updateList', [])
+		setTimeout(() => {
+			getMescroll().resetUpScroll()
+		}, 100)
+	}
+
+	const scroll = (e) => {
+		emit('scroll', e)
+	}
+
+	defineExpose({
+		resetUpScroll
+	})
+</script>
+
+<style lang="scss" scoped>
+	.item {
+		line-height: 150rpx;
+		border-bottom: 1px solid #ccc;
+	}
+</style>

+ 5 - 0
config/api.js

@@ -0,0 +1,5 @@
+// post请求
+export const weixinLogin = (params, config = {}) => uni.$u.http.post('/ai-auth/wechat/login', params, config)
+
+// get请求,注意:get请求的配置等,都在第二个参数中
+export const getUserInfo = (data) => uni.$u.http.get('/system/user/getInfo', data)

+ 262 - 0
config/hooks.js

@@ -0,0 +1,262 @@
+import {
+	weixinLogin,
+} from './api.js'
+import {
+	store
+} from '@/store/index.js'
+
+/**
+ * 吊起微信支付
+ * @param {Object} data 支付参数对象至少包含:timeStamp,nonceStr,package,signType,paySign
+ * @param {String} successTip 支付成功后的提示文本,默认:支付成功
+ * @param {String} failTip 支付失败 后的提示文本,默认:支付未完成
+ */
+export function wxPay(data, successTip = '支付成功', failTip = '支付未完成') {
+	return new Promise((resolve, reject) => {
+		if (!data?.timeStamp || !data?.nonceStr || !data?.package || !data?.signType || !data?.paySign)
+			return resolve(false)
+		uni.requestPayment({
+			provider: 'wxpay',
+			timeStamp: data.timeStamp,
+			nonceStr: data.nonceStr,
+			package: data.package,
+			signType: data.signType,
+			paySign: data.paySign,
+			success(result) {
+				console.log('wxPay-支付完成', result)
+				uni.showToast({
+					icon: 'success',
+					title: successTip
+				})
+				resolve(true)
+			},
+			fail(e) {
+				console.error('wxPay-支付未完成', e)
+				uni.$u.toast(failTip)
+				resolve(false)
+			}
+		})
+	})
+}
+
+
+// 封装的微信登录方法
+export async function useWXLogin() {
+	// 先尝试从本地存储中读取用户登录后保存的 token ,如果读取到了则表示已经登录过了
+	// if (store.token) return true
+	// 未登录则执行以下登录的逻辑
+	let params = {}
+	// 1.调用 uni.getUserProfile() 询问用户是否同意授权登录
+	/*
+	try {
+		const res = await uni.getUserProfile({
+			desc: '用户登录',
+			lang: 'zh_CN'
+		})
+		// console.log('获取信息', res); // res[1].userInfo 即可获取到用户的基本信息:头像、名称等
+		params.nickname = res.userInfo.nickName
+		params.gender = res.userInfo.gender;
+		params.avatar = res.userInfo.avatarUrl
+	} catch (e) {
+		// console.error(e);
+		// 用户取消授权的场景
+		uni.showToast({
+			title: '已取消授权',
+			duration: 2000,
+			icon: 'none'
+		})
+		return false
+	}
+	*/
+	uni.showLoading({
+		mask: true,
+		title: '登录中...'
+	})
+	// console.log(JSON.stringify(res.userInfo)); // res 中就包含了 code 字段值
+	// 2.调用 uni.login() 获取用户 code
+	const res2 = await uni.login({
+		provider: 'weixin', // 登录服务提供商,这里是微信
+	})
+	// console.log('获取code', res2)
+	params.code = res2.code
+	console.log(params);
+	// 3.调用登录 API 接口进行登录
+	const res3 = await weixinLogin(params)
+	if (res3.code !== 200) {
+		uni.hideLoading()
+		return false
+	}
+	console.log('登录接口回调', res3);
+	// 保存返回的 token 到 store 中(同时也会存储到本地存储中)
+	store.setToken(res3.data.access_token)
+	let data = await getUserDataDetail()
+	if (data.code == 200) {
+		// 保存返回的 用户信息 到 store 中(同时也会存储到本地存储中)
+		store.setUserInfo(data.data)
+	}
+	uni.hideLoading()
+	return true
+}
+
+/**
+ * u-upload 组件上传图片的方法
+ * @param {Object} event 是选择的图片对象,对象的 url 属性为图片的本地路径,如果选择的是多张图片请使用便利逐一传入
+ */
+export async function uploadImg(event) {
+	if (event.hasOwnProperty('file')) event = event.file
+	if (typeof event != 'object' || (!event?.url && !event[0]?.url)) return console.error(
+		'hooks.js function uploadImg 接收到的参数有误!', event);
+	console.log('上传图片', event);
+	uni.showLoading({
+		title: '上传中'
+	})
+	let url = event.length == undefined ? event.url : event[0].url
+	return uploadFilePromise(url)
+}
+// 将图片上传到服务器
+const uploadFilePromise = (url) => {
+	return new Promise((resolve, reject) => {
+		let a = uni.uploadFile({
+			url: store.uploadUrl,
+			filePath: url,
+			name: 'file',
+			formData: {
+				user: 'test'
+			},
+			header: {
+				Authorization: store.token ? `Bearer ${store.token}` : ''
+			},
+			success: (res) => {
+				if (res.statusCode === 200) {
+					let data = JSON.parse(res.data)
+					if (data.code === 200) {
+						// console.log('上传图片成功', data.data);
+						resolve(data.data.url)
+					}
+				}
+			},
+			complete() {
+				uni.hideLoading()
+			}
+		});
+	})
+}
+
+// 每次随机生成一个可以当做 id 的不会重复的值:a4168e6598df6
+export function getId() {
+	return Math.random().toString(16).slice(2);
+}
+
+// 格式化时间函数,接收一个长度为 10 位 或者 13 位 的时间戳
+export function getDate(time = Number(new Date()), isGetText = true) {
+	// 如果时间戳是 10位 的就 * 1000 转换为 毫秒
+	const _time = time.toString().length > 10 ? time : time * 1000
+	// 这里传入的时间戳是 10 位的,表示为秒数,所以要乘以 1000 转换为毫秒
+	var date = new Date(_time);
+	var Y = date.getFullYear();
+	var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
+	var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate());
+	var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours());
+	var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
+	var s = (date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds());
+	// 处理与当前时间相比较的时间格式,比如:3分钟前、1小时前、1天前...
+	let text = ''
+	if (isGetText) {
+		let nd = getDate(undefined, false) // 获取现在的时间信息
+		if (Y == nd.Y) {
+			if (M == nd.M) {
+				if (D == nd.D) {
+					if (h == nd.h) { // 同一个小时内
+						let mc = nd.m - m
+						if (mc > 0) {
+							text = `${mc}分钟前`
+						} else {
+							text = `刚刚` // 一分钟内的时间返回 "刚刚"
+						}
+					} else {
+						text = `${nd.h - h}小时前`
+					}
+				} else {
+					// 判断是否为昨天
+					if (D == nd.D - 1) {
+						text = `昨天 ${h}:${m}`
+					} else {
+						text = `${Y}年${M}月${D}日 ${h}:${m}`
+					}
+				}
+			} else {
+				text = `${Y}年${M}月${D}日 ${h}:${m}`
+			}
+		} else {
+			text = `${Y}年${M}月${D}日 ${h}:${m}`
+		}
+	}
+
+
+	// 返回一个对象
+	let obj = {
+		date: `${Y}年${M}月${D}日`,
+		time: `${h}时${m}分${s}秒`,
+		Y,
+		M,
+		D,
+		h,
+		m,
+		s,
+		text // 这里返回的是类似于 1分钟前 这种文字
+	}
+
+	return obj;
+}
+
+// 封装一个验证手机号的方法
+export function isPhone(phone) {
+	// 校验手机号的正则
+	let reg = /^1[3|4|5|6|7|8][0-9]{9}$/;
+	if (reg.test(phone)) {
+		return true
+	}
+	// uni.$u.toast('手机号格式错误')
+	return false
+}
+// 拨号
+export function call(phone) {
+	if (isPhone(phone)) {
+		uni.makePhoneCall({
+			phoneNumber: phone
+		})
+	}
+}
+// 复制
+export function copy(text) {
+	if (text) {
+		uni.setClipboardData({
+			data: text,
+			showToast: true // 弹出提示
+		})
+	}
+}
+/**
+ * 图片预览
+ * current 为当前显示图片的链接/索引值,不填或填写的值无效则为 urls 的第一张
+ * urls 需要预览的图片链接列表。注:如果传入的是一个对象数据,则将图片链接在对象中的字段名作为第三个字段传入
+ * key 如果urls是对象数组则会使用 key 在对象中取出链接字段的值
+ */
+export function previewImgs(current, urls, key = '') {
+	if (key) {
+		if (urls.length && typeof urls[0] == 'object') {
+			let list = [...urls]
+			urls = []
+			list.forEach((item, index) => {
+				if (item[key]) urls.push(item[key])
+			})
+		}
+		if (typeof current == 'object') {
+			current = current[key]
+		}
+	}
+	uni.previewImage({
+		urls,
+		current
+	})
+}

+ 69 - 0
config/request.js

@@ -0,0 +1,69 @@
+import {
+	store
+} from '@/store/index.js'
+/**
+ * 文档地址:https://uiadmin.net/uview-plus/js/http.html
+ */
+export function initRequest() {
+	console.log('初始化了 http 请求代码')
+	// 初始化请求配置
+	uni.$u.http.setConfig((config) => {
+		/* config 为默认全局配置*/
+		// config.baseURL = `http://192.168.1.2:8080`; /* 根域名 */
+		config.baseURL = `http://tf.hqzf100.com/dev-api/`; /* 根域名 */
+		config.custom.toast = true // 默认消息有msg会显示出来
+		return config
+	})
+	// 请求拦截
+	uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+		// console.log('请求拦截', config)
+		// 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
+		config.data = config.data || {}
+		// 挂载 token
+		let token = store.token
+		config.header.Authorization = token ? `Bearer ${token}` : ''
+		return config
+	}, config => { // 可使用async await 做异步操作
+		return Promise.reject(config)
+	})
+
+	// 响应拦截
+	uni.$u.http.interceptors.response.use((response) => {
+		/* 对响应成功做点什么 可使用async await 做异步操作*/
+		// console.log('响应拦截', response)
+		const data = response.data
+		// 自定义参数
+		const custom = response.config?.custom
+		if (data.code !== 200) {
+			// 身份认证失败,需要重新登录
+			if (data.code === 401) {
+				uni.$u.toast(data.msg || '身份认证失败,请先登录')
+				store.setToken('') // 清空token
+				setTimeout(() => {
+					uni.$u.route({
+						type: 'reLaunch',
+						url: '/pages/login/login' // 跳转到登录页
+					})
+				}, 1000)
+			} else {
+				// 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示
+				if (custom.toast !== false) {
+					uni.$u.toast(data.msg)
+				}
+				// 如果需要catch返回,则进行reject
+				// if (custom?.catch) {
+				//     return Promise.reject(data)
+				// } else {
+				//     // 否则返回一个pending中的promise,请求不会进入catch中
+				//     return new Promise(() => { })
+				// }
+				return data
+			}
+		}
+		// return data.data === undefined ? {} : data.data
+		return data // 这里视不同的后端返回数据格式而定
+	}, (response) => {
+		// 对响应错误做点什么 (statusCode !== 200)
+		return Promise.reject(response)
+	})
+}

+ 29 - 0
config/share.js

@@ -0,0 +1,29 @@
+export default {
+	data() {
+		return {
+			// 默认的全局分享内容
+			share: {
+				title: '标题',
+				path: '/pages/home/home', // 这里是分享的页面
+				imageUrl: '' // 这里是分析的图片,如果不写则默认截取当前分享页为分享图片
+			}
+		}
+	},
+	// 定义全局分享
+	// 1.发送给朋友
+	onShareAppMessage(res) {
+		return {
+			title: this.share.title,
+			path: this.share.path,
+			imageUrl: this.share.imageUrl,
+		}
+	},
+	//2.分享到朋友圈
+	onShareTimeline(res) {
+		return {
+			title: this.share.title,
+			path: this.share.path,
+			imageUrl: this.share.imageUrl,
+		}
+	},
+}

+ 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>

+ 42 - 0
main.js

@@ -0,0 +1,42 @@
+import App from './App'
+// 这个框架引入了 uview-plus UI组件库,该组件库中的所有组件和方法均可使用
+// uview-plus文档:https://uiadmin.net/uview-plus/
+import uviewPlus from '@/uni_modules/uview-plus'
+// 全局配置微信小程序分享
+import share from '/config/share.js'
+import {
+	initRequest
+} from '@/config/request.js'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+	...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import {
+	createSSRApp
+} from 'vue'
+// 导入 pinia 全局状态管理
+import {
+	createPinia
+} from 'pinia'
+export function createApp() {
+	const app = createSSRApp(App)
+	// 引入请求封装方法并执行
+	initRequest()
+	const pinia = createPinia()
+	app.use(pinia).use(uviewPlus)
+	app.mixin(share)
+	return {
+		app,
+		pinia
+	}
+}
+// #endif

+ 104 - 0
manifest.json

@@ -0,0 +1,104 @@
+{
+    "name" : "PDS",
+    "appid" : "__UNI__65C1C73",
+    "description" : "",
+    "versionName" : "1.0.1",
+    "versionCode" : 101,
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {
+            "Speech" : {}
+        },
+        /* 应用发布信息 */
+        "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\"/>",
+                    "<uses-permission android:name=\"android.permission.INTERNET\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {
+                "privacyDescription" : {
+                    "NSMicrophoneUsageDescription" : "需要访问麦克风权限,用于语音播报功能",
+                    "NSSpeechRecognitionUsageDescription" : "需要访问语音识别权限,用于语音播报功能"
+                },
+                "dSYMs" : false
+            },
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        },
+        "nativePlugins" : {
+            "nrb-tts-plugin" : {
+                "__plugin_info__" : {
+                    "name" : "语音合成TTS - [试用版,仅用于自定义调试基座]",
+                    "description" : "原生语音合成TTS",
+                    "platforms" : "Android,iOS",
+                    "url" : "https://ext.dcloud.net.cn/plugin?id=4133",
+                    "android_package_name" : "",
+                    "ios_bundle_id" : "",
+                    "isCloud" : true,
+                    "bought" : 0,
+                    "pid" : "4133",
+                    "parameters" : {}
+                }
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wx4b39b6f6e6caab96",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "postcss" : true,
+            "minified" : true
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

BIN
node_modules/.pnpm/@esbuild+win32-x64@0.20.2/node_modules/@esbuild/win32-x64/esbuild.exe


BIN
node_modules/.pnpm/@rollup+rollup-win32-x64-msvc@4.18.0/node_modules/@rollup/rollup-win32-x64-msvc/rollup.win32-x64-msvc.node


+ 290 - 0
pages.json

@@ -0,0 +1,290 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "首页"
+			}
+		},
+		{
+			"path": "pages/my/my",
+			"style": {
+				"navigationBarTitleText": "个人中心"
+			}
+		},
+		{
+			"path": "pages/order/index",
+			"style": {
+				"navigationBarTitleText": "订单统计"
+			}
+		},
+		{
+			"path": "pages/book/index",
+			"style": {
+				"navigationBarTitleText": "图书管理"
+			}
+		}
+	],
+	"subPackages": [{
+		"root": "pages/index",
+		"pages": [{
+			"path": "express/transfer-sign",
+			"style": {
+				"navigationBarTitleText": "中转签收"
+			}
+		}, {
+			"path": "express/quick-check",
+			"style": {
+				"navigationBarTitleText": "快速验收"
+			}
+		}, {
+			"path": "express/quick-unpack",
+			"style": {
+				"navigationBarTitleText": "快速拆包"
+			}
+		}, {
+			"path": "express/route-exception",
+			"style": {
+				"navigationBarTitleText": "路由签收异常"
+			}
+		}, {
+			"path": "express/warehouse-sign",
+			"style": {
+				"navigationBarTitleText": "到仓签收"
+			}
+		}, {
+			"path": "express/weight-modify",
+			"style": {
+				"navigationBarTitleText": "重量修改"
+			}
+		}, {
+			"path": "audit/confirm-receipt",
+			"style": {
+				"navigationBarTitleText": "确认收货"
+			}
+		}, {
+			"path": "audit/scan-order",
+			"style": {
+				"navigationBarTitleText": "扫书查单"
+			}
+		}, {
+			"path": "audit/express-order",
+			"style": {
+				"navigationBarTitleText": "根据快递单或订单"
+			}
+		}, {
+			"path": "audit/sender",
+			"style": {
+				"navigationBarTitleText": "根据发件人"
+			}
+		}, {
+			"path": "statistic/audit",
+			"style": {
+				"navigationBarTitleText": "审核统计"
+			}
+		}, {
+			"path": "statistic/after-sale",
+			"style": {
+				"navigationBarTitleText": "售后统计"
+			}
+		}, {
+			"path": "statistic/package",
+			"style": {
+				"navigationBarTitleText": "打包统计"
+			}
+		}, {
+			"path": "wms/medium-in",
+			"style": {
+				"navigationBarTitleText": "中等入库"
+			}
+		}, {
+			"path": "wms/good-in",
+			"style": {
+				"navigationBarTitleText": "良品入库"
+			}
+		}, {
+			"path": "wms/secondary-in",
+			"style": {
+				"navigationBarTitleText": "次品入库"
+			}
+		}, {
+			"path": "wms/bad-in",
+			"style": {
+				"navigationBarTitleText": "不良入库"
+			}
+		}, {
+			"path": "wms/bad-out",
+			"style": {
+				"navigationBarTitleText": "不良出库"
+			}
+		}, {
+			"path": "wms/bad-off",
+			"style": {
+				"navigationBarTitleText": "不良下架"
+			}
+		}, {
+			"path": "wms/order-query",
+			"style": {
+				"navigationBarTitleText": "订单查询"
+			}
+		}, {
+			"path": "wms/location-order",
+			"style": {
+				"navigationBarTitleText": "库位订单"
+			}
+		}, {
+			"path": "wms/speedy-check",
+			"style": {
+				"navigationBarTitleText": "快速盘点"
+			}
+		}, {
+			"path": "offline/check-order",
+			"style": {
+				"navigationBarTitleText": "线下核单"
+			}
+		}, {
+			"path": "offline/check-record",
+			"style": {
+				"navigationBarTitleText": "核单记录"
+			}
+		}, {
+			"path": "entry/scan-book",
+			"style": {
+				"navigationBarTitleText": "扫码查书"
+			}
+		}, {
+			"path": "entry/book-weight",
+			"style": {
+				"navigationBarTitleText": "录入书籍重量"
+			}
+		}]
+	}, {
+		"root": "pages/my",
+		"pages": [{
+				"path": "page/user-info",
+				"style": {
+					"navigationBarTitleText": "用户信息"
+				}
+			},
+			{
+				"path": "page/warehouse",
+				"style": {
+					"navigationBarTitleText": "默认仓库"
+				}
+			}, {
+				"path": "page/school",
+				"style": {
+					"navigationBarTitleText": "学校设置"
+				}
+			}, {
+				"path": "page/audit-unfinished",
+				"style": {
+					"navigationBarTitleText": "审核未完成"
+				}
+			}, {
+				"path": "page/version",
+				"style": {
+					"navigationBarTitleText": "版本设置"
+				}
+
+			}, {
+				"path": "page/volume",
+				"style": {
+					"navigationBarTitleText": "音频设置"
+				}
+			}, {
+				"path": "page/book-display",
+				"style": {
+					"navigationBarTitleText": "图书显示设置"
+				}
+			}, {
+				"path": "page/password",
+				"style": {
+					"navigationBarTitleText": "修改密码"
+				}
+			}
+		]
+	}, {
+		"root": "pages/order/stat",
+		"pages": [{
+				"path": "pending-review",
+				"style": {
+					"navigationBarTitleText": "待初审统计"
+				}
+			},
+			{
+				"path": "pending-pick",
+				"style": {
+					"navigationBarTitleText": "待拣件统计"
+				}
+			},
+			{
+				"path": "pending-sign",
+				"style": {
+					"navigationBarTitleText": "已拣件待签收"
+				}
+			},
+			{
+				"path": "pending-confirm",
+				"style": {
+					"navigationBarTitleText": "待确认收货"
+				}
+			},
+			{
+				"path": "pending-audit",
+				"style": {
+					"navigationBarTitleText": "已到货待审核"
+				}
+			},
+			{
+				"path": "pending-payment",
+				"style": {
+					"navigationBarTitleText": "待付款"
+				}
+			},
+			{
+				"path": "receive-stat",
+				"style": {
+					"navigationBarTitleText": "收货统计"
+				}
+			}
+
+		]
+	}],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "",
+		"navigationBarBackgroundColor": "#ffffff"
+	},
+	"tabBar": {
+		"backgroundColor": "#ffffff",
+		"color": "#767676",
+		"selectedColor": "#31B4FF",
+		"list": [{
+				"pagePath": "pages/index/index",
+				"iconPath": "/static/tabbar/home.png",
+				"selectedIconPath": "/static/tabbar/homeHl.png",
+				"text": "首页"
+			},
+			{
+				"pagePath": "pages/order/index",
+				"iconPath": "/static/tabbar/order.png",
+				"selectedIconPath": "/static/tabbar/orderHl.png",
+				"text": "订单"
+			},
+			{
+				"pagePath": "pages/book/index",
+				"iconPath": "/static/tabbar/book.png",
+				"selectedIconPath": "/static/tabbar/bookHl.png",
+				"text": "图书"
+			},
+			{
+				"pagePath": "pages/my/my",
+				"iconPath": "/static/tabbar/mine.png",
+				"selectedIconPath": "/static/tabbar/mineHl.png",
+				"text": "我的"
+			}
+		]
+	},
+	"uniIdRouter": {}
+}

+ 192 - 0
pages/book/components/feedbackPopup.vue

@@ -0,0 +1,192 @@
+<template>
+	<u-popup mode="center" :show="show" @close="handleClose" :closeOnClickOverlay="false">
+		<view class="popup-container">
+			<u-form ref="formRef" :model="formData" :rules="rules" labelPosition="top" labelWidth="100%">
+
+				<!-- 反馈信息 -->
+				<u-form-item label="反馈图书信息" prop="feedbackText" :borderBottom="true">
+					<u-input v-model="formData.feedbackText" type="textarea" placeholder="请输入反馈信息" :border="true"
+						height="100"></u-input>
+				</u-form-item>
+
+				<!-- 图片上传 -->
+				<u-form-item label="上传照片" prop="images" :borderBottom="false">
+					<u-upload :fileList="formData.images" @afterRead="afterRead" @delete="deletePic" name="1" multiple
+						:maxCount="1" :imageMode="true">
+						<view class="upload-slot">
+							<u-icon name="plus" size="30"></u-icon>
+							<text class="upload-text">上传照片</text>
+						</view>
+					</u-upload>
+				</u-form-item>
+			</u-form>
+
+			<!-- 按钮组 -->
+			<view class="button-group">
+				<u-button type="info" text="取消" :plain="true" @click="handleCancel" :disabled="submitting"></u-button>
+				<u-button type="primary" text="确认" @click="handleConfirm" :loading="submitting"></u-button>
+			</view>
+		</view>
+	</u-popup>
+</template>
+
+<script setup>
+	import {
+		ref,
+		reactive
+	} from 'vue'
+
+	const props = defineProps({
+		show: {
+			type: Boolean,
+			default: false
+		}
+	})
+
+	const emit = defineEmits(['update:show', 'submit'])
+
+	// 表单引用
+	const formRef = ref(null)
+
+	// 提交状态
+	const submitting = ref(false)
+
+	// 表单数据
+	const formData = reactive({
+		feedbackText: '',
+		images: []
+	})
+
+	// 表单验证规则
+	const rules = {
+		feedbackText: [{
+			required: true,
+			message: '请输入反馈信息',
+			trigger: ['blur', 'change']
+		}, {
+			min: 5,
+			message: '反馈信息不能少于5个字',
+			trigger: ['blur', 'change']
+		}],
+		images: [{
+			required: true,
+			message: '请上传照片',
+			trigger: ['change']
+		}]
+	}
+
+	// 上传图片后的处理
+	function afterRead(event) {
+		const {
+			file
+		} = event
+		// 这里可以处理图片上传到服务器的逻辑
+		formData.images.push({
+			url: file.url,
+			status: 'success',
+			message: '上传成功'
+		})
+	}
+
+	// 删除图片
+	function deletePic(event) {
+		const index = event.index
+		formData.images.splice(index, 1)
+	}
+
+	// 取消操作
+	function handleCancel() {
+		resetForm()
+		emit('update:show', false)
+	}
+
+	// 确认操作
+	async function handleConfirm() {
+		try {
+			// 表单验证
+			await formRef.value?.validate()
+
+			submitting.value = true
+
+			// 模拟提交操作
+			await new Promise(resolve => setTimeout(resolve, 1000))
+
+			// 提交成功
+			uni.showToast({
+				title: '反馈已提交',
+				icon: 'success'
+			})
+
+			// 触发提交事件
+			emit('submit', {
+				feedbackText: formData.feedbackText,
+				images: formData.images
+			})
+
+			// 重置表单并关闭弹窗
+			resetForm()
+			emit('update:show', false)
+		} catch (error) {
+			console.error('表单验证失败:', error)
+		} finally {
+			submitting.value = false
+		}
+	}
+
+	// 重置表单
+	function resetForm() {
+		formData.feedbackText = ''
+		formData.images = []
+		formRef.value?.resetFields()
+	}
+</script>
+
+<style lang="scss" scoped>
+	.popup-container {
+		width: 600rpx;
+		background-color: #ffffff;
+		border-radius: 12rpx;
+		padding: 30rpx;
+
+		:deep(.u-form) {
+			.u-form-item {
+				&__body {
+					padding: 20rpx 0;
+				}
+
+				&__body__left__content {
+					padding-bottom: 8rpx;
+				}
+			}
+		}
+
+		.upload-slot {
+			width: 160rpx;
+			height: 160rpx;
+			border: 2rpx dashed #dcdfe6;
+			border-radius: 8rpx;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			justify-content: center;
+
+			.upload-text {
+				font-size: 24rpx;
+				color: #909399;
+				margin-top: 10rpx;
+			}
+		}
+
+		.button-group {
+			display: flex;
+			justify-content: space-between;
+			margin-top: 40rpx;
+			padding: 0 20rpx;
+
+			:deep(.u-button) {
+				width: 45%;
+				border-radius: 8rpx;
+			}
+		}
+	}
+</style>

+ 216 - 0
pages/book/index.vue

@@ -0,0 +1,216 @@
+<template>
+    <page-scroll ref="pageScrollRef" :loadmore="loadMore" :loading="loading" :empty="isEmpty" emptyText="暂无搜索结果">
+        <!-- 搜索框 -->
+        <view class="search-header">
+            <view class="search-box">
+                <u-icon name="scan" size="24" color="#ffffff" @click="scanCode"></u-icon>
+                <u-search v-model="searchText" placeholder="请输入ISBN" :showAction="false" :clearabled="true"
+                    @search="handleSearch" @clear="handleClear" bgColor="#ffffff"></u-search>
+            </view>
+        </view>
+
+        <!-- 搜索结果列表 -->
+        <view class="book-list" v-if="bookList.length > 0">
+            <view class="book-item" v-for="(book, index) in bookList" :key="index">
+                <view class="book-info">
+                    <image class="book-image" :src="book.coverUrl" mode="aspectFill"></image>
+                    <view class="book-details">
+                        <text class="book-title">{{ book.title }}</text>
+                        <text class="book-isbn">ISBN: {{ book.isbn }}</text>
+                        <text class="book-price">定价: {{ book.price }}</text>
+                        <text class="book-set">套装: {{ book.isSet ? '是' : '否' }}</text>
+                    </view>
+                </view>
+                <view class="book-action">
+                    <u-icon name="edit-pen" size="20" color="#4cd964" @click="editBook(book)"></u-icon>
+                </view>
+            </view>
+        </view>
+
+        <feedback-popup v-model:show="showFeedback" @submit="handleFeedbackSubmit"></feedback-popup>
+    </page-scroll>
+</template>
+
+<script setup>
+import {
+    ref,
+    computed
+} from 'vue'
+import PageScroll from '@/components/pageScroll/index.vue'
+import FeedbackPopup from './components/feedbackPopup.vue';
+
+const showFeedback = ref(false)
+
+function handleFeedbackSubmit(data) {
+    console.log('反馈数据:', data)
+}
+
+// 页面滚动组件引用
+const pageScrollRef = ref(null)
+
+// 搜索相关
+const searchText = ref('')
+const loading = ref(false)
+const bookList = ref([{
+    id: 1,
+    title: '公文写作教程',
+    isbn: '9787040515555',
+    price: '49.5',
+    isSet: false,
+    coverUrl: 'https://img20.360buyimg.com/da/jfs/t1/141592/25/8861/261559/5f68d8c1E33ed78ab/698ad655bfcfbaed.png'
+}])
+
+// 是否为空
+const isEmpty = computed(() => {
+    return !loading.value && bookList.value.length === 0
+})
+
+// 处理搜索
+async function handleSearch() {
+    if (!searchText.value) {
+        uni.showToast({
+            title: '请输入ISBN',
+            icon: 'none'
+        })
+        return
+    }
+
+    await loadData()
+}
+
+// 处理清除
+function handleClear() {
+    bookList.value = []
+}
+
+// 加载数据
+async function loadData() {
+    loading.value = true
+    try {
+        // 模拟API调用
+        const res = await mockSearchBooks(searchText.value)
+        bookList.value = res
+    } catch (error) {
+        uni.showToast({
+            title: '加载失败',
+            icon: 'none'
+        })
+    } finally {
+        loading.value = false
+    }
+}
+
+// 加载更多
+async function loadMore() {
+    if (loading.value) return
+    page.value++
+    await loadData()
+}
+
+// 扫码
+function scanCode() {
+    uni.scanCode({
+        scanType: ['barCode'],
+        success: (res) => {
+            searchText.value = res.result
+            handleSearch()
+        },
+        fail: () => {
+            uni.showToast({
+                title: '扫码失败',
+                icon: 'none'
+            })
+        }
+    })
+}
+
+// 编辑图书
+function editBook(book) {
+    // 处理编辑逻辑
+    showFeedback.value = true
+}
+
+// 模拟搜索API
+function mockSearchBooks(isbn) {
+    return new Promise((resolve) => {
+        setTimeout(() => {
+            resolve([{
+                id: 1,
+                title: '公文写作教程',
+                isbn: '9787040515555',
+                price: '49.5',
+                isSet: false,
+                coverUrl: '/static/book-cover.jpg'
+            }])
+        }, 1000)
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+.search-header {
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    background-color: #4cd964;
+    padding: 20rpx;
+
+    .search-box {
+        display: flex;
+        align-items: center;
+        border-radius: 8rpx;
+        padding: 0 20rpx;
+    }
+}
+
+.book-list {
+    padding: 20rpx;
+
+    .book-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        background-color: #ffffff;
+        border-radius: 12rpx;
+        padding: 20rpx;
+        margin-bottom: 20rpx;
+        box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+
+        .book-info {
+            display: flex;
+            flex: 1;
+
+            .book-image {
+                width: 160rpx;
+                height: 200rpx;
+                border-radius: 8rpx;
+                margin-right: 20rpx;
+            }
+
+            .book-details {
+                display: flex;
+                flex-direction: column;
+                justify-content: space-between;
+
+                .book-title {
+                    font-size: 32rpx;
+                    font-weight: bold;
+                    color: #333;
+                }
+
+                .book-isbn,
+                .book-price,
+                .book-set {
+                    font-size: 26rpx;
+                    color: #666;
+                    margin-top: 8rpx;
+                }
+            }
+        }
+
+        .book-action {
+            padding: 20rpx;
+        }
+    }
+}
+</style>

+ 3 - 0
pages/index/audit/confirm-receipt.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/audit/express-order.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/audit/scan-order.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/audit/sender.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/entry/book-weight.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/entry/scan-book.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 228 - 0
pages/index/express/components/SelectWarehouse.vue

@@ -0,0 +1,228 @@
+<template>
+  <wd-popup
+    v-model="visible"
+    position="bottom"
+    class="warehouse-popup"
+    :close-on-click-modal="false"
+    :safe-area-inset-bottom="true"
+    :closable="true"
+  >
+    <view class="popup-content">
+      <!-- 标题 -->
+      <view class="popup-header">
+        <view class="header-left">
+          <wd-icon name="warehouse" size="32rpx" color="#333"></wd-icon>
+          <text>绑定仓库</text>
+        </view>
+      </view>
+
+      <!-- 搜索框 -->
+      <view class="search-box">
+        <wd-input
+          v-model="searchKey"
+          placeholder="搜索仓库"
+          clearable
+          class="search-input"
+          no-border
+        >
+          <template #suffix>
+            <wd-button type="success" class="search-btn" @click="handleSearch">搜索</wd-button>
+          </template>
+        </wd-input>
+      </view>
+
+      <!-- 搜索结果 -->
+      <view class="search-result" v-if="searchResults.length">
+        <text class="section-title">搜索结果</text>
+        <view class="tag-group">
+          <wd-tag
+            v-for="item in searchResults"
+            :key="item.id"
+            class="warehouse-tag"
+            :class="{ active: selectedWarehouse === item.name }"
+            @click="selectWarehouse(item.name)"
+          >
+            {{ item.name }}
+          </wd-tag>
+        </view>
+      </view>
+
+      <!-- 历史绑定 -->
+      <view class="history-section">
+        <text class="section-title">历史绑定</text>
+        <view class="tag-group">
+          <wd-tag
+            v-for="item in historyWarehouses"
+            :key="item.id"
+            class="warehouse-tag"
+            :class="{ active: selectedWarehouse === item.name }"
+            @click="selectWarehouse(item.name)"
+          >
+            {{ item.name }}
+          </wd-tag>
+        </view>
+      </view>
+
+      <!-- 底部按钮 -->
+      <view class="popup-footer">
+        <wd-button class="footer-btn cancel-btn" @click="handleCancel">取消</wd-button>
+        <wd-button type="success" class="footer-btn confirm-btn" @click="handleConfirm">
+          确定
+        </wd-button>
+      </view>
+    </view>
+  </wd-popup>
+</template>
+
+<script setup>
+import { ref, defineProps, defineEmits } from 'vue'
+
+const visible = ref(false)
+
+const emit = defineEmits(['confirm'])
+
+// 搜索相关
+const searchKey = ref('')
+const searchResults = ref([
+  { id: 1, name: '河南仓' },
+  { id: 2, name: '湖北仓' },
+  { id: 3, name: '河北仓' },
+  { id: 4, name: '涨涨涨' },
+  { id: 5, name: '涨涨' },
+])
+
+// 历史记录
+const historyWarehouses = ref([{ id: 1, name: '河南仓' }])
+
+// 选中的仓库
+const selectedWarehouse = ref('')
+
+// 搜索处理
+const handleSearch = () => {
+  // TODO: 实现搜索逻辑
+  console.log('搜索关键词:', searchKey.value)
+}
+
+// 选择仓库
+const selectWarehouse = (name) => {
+  selectedWarehouse.value = name
+}
+
+// 取消
+const handleCancel = () => {
+  visible.value = false
+  selectedWarehouse.value = ''
+  searchKey.value = ''
+}
+
+// 确认
+const handleConfirm = () => {
+  if (!selectedWarehouse.value) {
+    uni.showToast({
+      title: '请选择仓库',
+      icon: 'none',
+    })
+    return
+  }
+  emit('confirm', selectedWarehouse.value)
+  handleCancel()
+}
+
+const open = () => {
+  visible.value = true
+}
+
+defineExpose({ open })
+</script>
+
+<style lang="scss" scoped>
+:deep(.wd-popup) {
+  border-radius: 24rpx 24rpx 0 0;
+}
+
+.popup-content {
+  padding: 32rpx;
+  padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
+}
+
+.popup-header {
+  margin-bottom: 32rpx;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 12rpx;
+    font-size: 32rpx;
+    font-weight: 500;
+    color: #333;
+  }
+}
+
+.search-box {
+  margin-bottom: 32rpx;
+
+  .search-input {
+    :deep(.wd-input__inner) {
+      height: 80rpx;
+      background: #f5f6fa;
+      border-radius: 40rpx;
+      padding: 0 24rpx;
+      font-size: 28rpx;
+    }
+
+    .search-btn {
+      margin-left: 20rpx;
+      border-radius: 32rpx;
+      font-size: 26rpx;
+      min-width: 140rpx;
+    }
+  }
+}
+
+.section-title {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 20rpx;
+  display: block;
+}
+
+.tag-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+  margin-bottom: 32rpx;
+
+  .warehouse-tag.wd-tag {
+    padding: 10rpx 24rpx;
+    border-radius: 32rpx;
+    font-size: 26rpx;
+    background: #f5f6fa;
+    border: none;
+    color: #333;
+
+    &.active {
+      background: #e8f7ea;
+      color: #4cd964;
+    }
+  }
+}
+
+.popup-footer {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 40rpx;
+
+  .footer-btn {
+    flex: 1;
+    height: 88rpx;
+    border-radius: 44rpx;
+    font-size: 32rpx;
+  }
+
+  .cancel-btn {
+    background: #f5f6fa;
+    color: #333;
+    border: none;
+  }
+}
+</style>

+ 3 - 0
pages/index/express/quick-check.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/express/quick-unpack.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/express/route-exception.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 181 - 0
pages/index/express/transfer-sign.vue

@@ -0,0 +1,181 @@
+
+<template>
+  <view class="scan-page">
+    <!-- 仓库选择 -->
+    <view class="input-group">
+      <wd-input
+        v-model="warehouse"
+        placeholder="请选择仓库"
+        readonly
+        :no-border="true"
+        right-icon="arrow-right"
+        @click="handleOpenWarehousePicker"
+      />
+      <wd-button size="small" class="btn-select" @click="handleOpenWarehousePicker">选择</wd-button>
+    </view>
+
+    <!-- 物流单号输入 -->
+    <view class="input-group">
+      <wd-input
+        v-model="trackingNumber"
+        placeholder="扫描/输入物流单号"
+        clearable
+        :no-border="true"
+      >
+        <template #prefix>
+          <wd-icon name="scan" size="20px" color="#666" @click="scanTrackingNumber"></wd-icon>
+        </template>
+      </wd-input>
+      <wd-button size="small" class="btn-confirm" @click="confirmTrackingNumber">确定</wd-button>
+    </view>
+
+    <!-- 底部扫码按钮 -->
+    <view class="bottom-button">
+      <wd-button type="success" block @click="handleScan">扫码</wd-button>
+    </view>
+
+    <SelectWarehouse ref="selectRef" />
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import SelectWarehouse from '@/pages/index/express/components/SelectWarehouse.vue'
+
+// 仓库相关
+const warehouse = ref('')
+// 物流单号
+const trackingNumber = ref('')
+
+const selectRef = ref(null)
+//选择仓库
+const handleOpenWarehousePicker = () => {
+  selectRef.value?.open()
+}
+
+// 扫描物流单号
+const scanTrackingNumber = () => {
+  uni.scanCode({
+    scanType: ['barCode'],
+    success: (res) => {
+      trackingNumber.value = res.result
+    },
+    fail: () => {
+      uni.showToast({
+        title: '扫码失败',
+        icon: 'none',
+      })
+    },
+  })
+}
+
+// 确认物流单号
+const confirmTrackingNumber = () => {
+  if (!warehouse.value) {
+    uni.showToast({
+      title: '请选择仓库',
+      icon: 'none',
+    })
+    return
+  }
+  if (!trackingNumber.value) {
+    uni.showToast({
+      title: '请输入物流单号',
+      icon: 'none',
+    })
+    return
+  }
+  // TODO: 处理确认逻辑
+  console.log('确认物流单号:', {
+    warehouse: warehouse.value,
+    trackingNumber: trackingNumber.value,
+  })
+}
+
+// 底部扫码按钮
+const handleScan = () => {
+  if (!warehouse.value) {
+    uni.showToast({
+      title: '请先选择仓库',
+      icon: 'none',
+    })
+    return
+  }
+  uni.scanCode({
+    scanType: ['barCode', 'qrCode'],
+    success: (res) => {
+      console.log('扫码结果:', res.result)
+      // TODO: 处理扫码结果
+    },
+    fail: () => {
+      uni.showToast({
+        title: '扫码失败',
+        icon: 'none',
+      })
+    },
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.scan-page {
+  padding: 32rpx;
+  box-sizing: border-box;
+
+  .input-group {
+    display: flex;
+    align-items: center;
+    gap: 20rpx;
+    margin-bottom: 32rpx;
+
+    :deep(.wd-input) {
+      flex: 1;
+      background: #ffffff;
+      border-radius: 8rpx;
+
+      .wd-input__inner {
+        height: 80rpx;
+        font-size: 28rpx;
+        padding-left: 24rpx;
+        border: none;
+      }
+
+      .wd-input__prefix {
+        padding-left: 24rpx;
+      }
+    }
+
+    .btn-select,
+    .btn-confirm {
+      width: 120rpx;
+      height: 80rpx;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+      background: #999999;
+      color: #ffffff;
+      border: none;
+
+      &:active {
+        opacity: 0.8;
+      }
+    }
+
+    .btn-confirm {
+      background: #4cd964;
+    }
+  }
+
+  .bottom-button {
+    position: fixed;
+    left: 32rpx;
+    right: 32rpx;
+    bottom: calc(32rpx + env(safe-area-inset-bottom));
+
+    :deep(.wd-button) {
+      height: 88rpx;
+      font-size: 32rpx;
+      border-radius: 44rpx;
+    }
+  }
+}
+</style>

+ 3 - 0
pages/index/express/warehouse-sign.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/express/weight-modify.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 300 - 0
pages/index/index.vue

@@ -0,0 +1,300 @@
+<template>
+	<view class="operation-container">
+		<!-- 快递操作区域 -->
+		<view class="section">
+			<view class="section-title">快递</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in expressOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+
+		<!-- 审核操作区域 -->
+		<view class="section">
+			<view class="section-title">审核</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in auditOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+
+		<!-- 统计操作区域 -->
+		<view class="section">
+			<view class="section-title">统计</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in statisticsOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+
+		<!-- WMS操作区域 -->
+		<view class="section">
+			<view class="section-title">WMS操作</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in wmsOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+
+		<!-- 线下核单 -->
+		<view class="section">
+			<view class="section-title">线下核单</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in offlineOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+
+		<!-- 录入信息 -->
+		<view class="section">
+			<view class="section-title">录入信息</view>
+			<view class="grid-container">
+				<view v-for="(item, index) in entryOperations" :key="index" class="grid-item" :class="item.type"
+					@click="handleNavigation(item.path)">
+					{{ item.name }}
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	// 快递操作列表
+	const expressOperations = ref([{
+			name: '中转签收',
+			path: '/pages/index/express/transfer-sign',
+			type: 'primary'
+		},
+		{
+			name: '到仓签收',
+			path: '/pages/index/express/warehouse-sign',
+			type: 'warning'
+		},
+		{
+			name: '重量修改',
+			path: '/pages/index/express/weight-modify',
+			type: 'primary'
+		},
+		{
+			name: '快速验收',
+			path: '/pages/index/express/quick-check',
+			type: 'warning'
+		},
+		{
+			name: '路由签收异常',
+			path: '/pages/index/express/route-exception',
+			type: 'primary'
+		},
+		{
+			name: '快速拆包',
+			path: '/pages/index/express/quick-unpack',
+			type: 'warning'
+		},
+	])
+
+	// 审核操作列表
+	const auditOperations = ref([{
+			name: '确认收货',
+			path: '/pages/index/audit/confirm-receipt',
+			type: 'primary'
+		},
+		{
+			name: '扫书查单',
+			path: '/pages/index/audit/scan-order',
+			type: 'warning'
+		},
+		{
+			name: '根据快递单或订单',
+			path: '/pages/index/audit/express-order',
+			type: 'warning'
+		},
+		{
+			name: '根据发件人',
+			path: '/pages/index/audit/sender',
+			type: 'primary'
+		},
+	])
+
+	// WMS操作列表
+	const wmsOperations = ref([{
+			name: '中等入库',
+			path: '/pages/index/wms/medium-in',
+			type: 'primary'
+		},
+		{
+			name: '良品入库',
+			path: '/pages/index/wms/good-in',
+			type: 'warning'
+		},
+		{
+			name: '次品入库',
+			path: '/pages/index/wms/secondary-in',
+			type: 'primary'
+		},
+		{
+			name: '不良入库',
+			path: '/pages/index/wms/bad-in',
+			type: 'warning'
+		},
+		{
+			name: '不良出库',
+			path: '/pages/index/wms/bad-out',
+			type: 'primary'
+		},
+		{
+			name: '不良下架',
+			path: '/pages/index/wms/bad-off',
+			type: 'warning'
+		},
+		{
+			name: '订单查询',
+			path: '/pages/index/wms/order-query',
+			type: 'primary'
+		},
+		{
+			name: '库位订单',
+			path: '/pages/index/wms/location-order',
+			type: 'warning'
+		},
+		{
+			name: '快速盘点',
+			path: '/pages/index/wms/speedy-check',
+			type: 'primary',
+			span: 24
+		},
+	])
+
+	// 统计操作列表
+	const statisticsOperations = ref([{
+			name: '审核统计',
+			path: '/pages/index/statistics/audit',
+			type: 'warning',
+			span: 24
+		},
+		{
+			name: '售后统计',
+			path: '/pages/index/statistics/after-sale',
+			type: 'primary'
+		},
+		{
+			name: '打包统计',
+			path: '/pages/index/statistics/package',
+			type: 'warning'
+		},
+	])
+	//线下核单
+	const offlineOperations = ref([{
+			name: '线下核单',
+			path: '/pages/index/offline/check-order',
+			type: 'primary'
+		},
+		{
+			name: '核单记录',
+			path: '/pages/index/offline/check-record',
+			type: 'warning'
+		},
+	])
+
+	//录入信息
+	const entryOperations = ref([{
+			name: '扫码查书',
+			path: '/pages/index/entry/scan-book',
+			type: 'primary'
+		},
+		{
+			name: '录入书籍重量',
+			path: '/pages/index/entry/book-weight',
+			type: 'warning'
+		},
+	])
+
+	// 页面跳转方法
+	const handleNavigation = (path) => {
+		uni.navigateTo({
+			url: path,
+			fail: () => {
+				uni.showToast({
+					title: '页面跳转失败',
+					icon: 'none',
+				})
+			},
+		})
+	}
+</script>
+
+<style lang="scss" scoped>
+	.operation-container {
+		padding: 20rpx;
+		background: #f5f6fa;
+		min-height: 100vh;
+	}
+
+	.section {
+		margin-bottom: 30rpx;
+		background: #ffffff;
+		border-radius: 16rpx;
+		padding: 20rpx;
+		box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+		.section-title {
+			font-size: 32rpx;
+			font-weight: 600;
+			color: #333;
+			padding: 20rpx;
+			border-bottom: 2rpx solid #f0f0f0;
+			margin-bottom: 20rpx;
+		}
+
+		.grid-container {
+			display: grid;
+			grid-template-columns: repeat(2, 1fr);
+			gap: 20rpx;
+			padding: 10rpx;
+
+			.grid-item {
+				height: 88rpx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				font-size: 28rpx;
+				border-radius: 12rpx;
+				color: #ffffff;
+				transition: all 0.3s;
+
+				&.primary {
+					background: linear-gradient(135deg, #4cd964, #3ac555);
+
+					&:active {
+						background: linear-gradient(135deg, #3ac555, #2fb548);
+					}
+				}
+
+				&.warning {
+					background: linear-gradient(135deg, #ff9500, #ff8000);
+
+					&:active {
+						background: linear-gradient(135deg, #ff8000, #e67300);
+					}
+				}
+
+				&:active {
+					transform: scale(0.98);
+				}
+			}
+		}
+	}
+</style>

+ 3 - 0
pages/index/offline/check-order.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/offline/check-record.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/statistic/after-sale.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/statistic/audit.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/statistic/package.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/bad-in.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/bad-off.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/bad-out.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/good-in.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/location-order.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/medium-in.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/order-query.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/secondary-in.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 3 - 0
pages/index/wms/speedy-check.vue

@@ -0,0 +1,3 @@
+<template>
+	<view>1</view>
+</template>

+ 163 - 0
pages/my/components/orderItem.vue

@@ -0,0 +1,163 @@
+<template>
+	<view class="order-card">
+		<!-- 用户信息 -->
+		<view class="user-info">
+			<image class="avatar" src="https://img20.360buyimg.com/da/jfs/t1/141592/25/8861/261559/5f68d8c1E33ed78ab/698ad655bfcfbaed.png" mode="aspectFill"></image>
+			<view class="user-details">
+				<text class="username">微信用户(南**)</text>
+				<text class="date">共卖出7本书</text>
+				<text class="date">来自河南省郑州市中牟县</text>
+			</view>
+			
+			<view class="user-details right-items">
+				<text class="date">2024-06-01 17:00:00</text>
+				<text class="status" :style="{ color: statusColor }">[待收货审核]</text>
+			</view>
+		</view>
+
+		<!-- 订单信息 -->
+		<view class="order-info">
+			
+		</view>
+
+		<!-- 订单详情 -->
+		<view class="order-details">
+			<view class="detail-row">
+				<text>订单ID:</text>
+				<text class="link" @click="copyToClipboard(orderId)">{{ orderId }}</text>
+				<text>运单号:</text>
+				<text class="link" @click="copyToClipboard(trackingId)">{{ trackingId }}</text>
+			</view>
+			<view class="detail-row">
+				<text>预估金额:{{ estimatedAmount }}</text>
+				<text>审核金额:{{ auditAmount }}</text>
+			</view>
+			<view class="detail-row">
+				<text>内部备注:{{ internalNote }}</text>
+			</view>
+		</view>
+
+		<!-- 操作按钮 -->
+		<view class="action-buttons">
+			<u-button size="small" @click="handleAudit">到货审核</u-button>
+			<u-button size="small" @click="handleView">查看</u-button>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	// 模拟数据
+	const orderId = ref('66478425')
+	const trackingId = ref('DPK202356410215')
+	const estimatedAmount = ref('66.6')
+	const auditAmount = ref('待核')
+	const internalNote = ref('已反馈')
+	const statusColor = ref('#FF4D4F')
+
+	// 复制到剪贴板
+	const copyToClipboard = (text) => {
+		uni.setClipboardData({
+			data: text,
+			success: () => {
+				uni.showToast({
+					title: '复制成功',
+					icon: 'success'
+				})
+			}
+		})
+	}
+
+	// 到货审核
+	const handleAudit = () => {
+		console.log('到货审核')
+		// 处理到货审核逻辑
+	}
+
+	// 查看
+	const handleView = () => {
+		console.log('查看')
+		// 处理查看逻辑
+	}
+</script>
+
+<style lang="scss" scoped>
+	.order-card {
+		background: #FFFFFF;
+		border-radius: 8rpx;
+		padding: 20rpx;
+		margin-bottom: 20rpx;
+		box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+
+		.user-info {
+			display: flex;
+			align-items: flex-start;
+			margin-bottom: 20rpx;
+
+			.avatar {
+				width: 80rpx;
+				height: 80rpx;
+				border-radius: 40rpx;
+				margin-right: 20rpx;
+			}
+
+			.user-details {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+				&.right-items{
+					align-items: flex-end;
+				}
+
+				.username {
+					font-size: 28rpx;
+					color: #333333;
+					margin-bottom: 4rpx;
+				}
+
+				.date {
+					font-size: 24rpx;
+					color: #999999;
+				}
+
+				.status {
+					font-size: 24rpx;
+					font-weight: bold;
+				}
+			}
+		}
+
+		.order-details {
+			font-size: 26rpx;
+			color: #333333;
+			margin-bottom: 20rpx;
+
+			.detail-row {
+				display: flex;
+				justify-content: space-between;
+				margin-bottom: 8rpx;
+
+				.link {
+					color: #007BFF;
+					text-decoration: underline;
+					cursor: pointer;
+				}
+			}
+		}
+
+		.action-buttons {
+			display: flex;
+			justify-content: space-between;
+
+			:deep(.u-button) {
+				flex: 1;
+				margin: 0 10rpx;
+				height: 60rpx;
+				font-size: 26rpx;
+			}
+		}
+	}
+</style>

+ 153 - 0
pages/my/my.vue

@@ -0,0 +1,153 @@
+<template>
+	<view class="settings-page">
+		<!-- 顶部用户信息 -->
+		<view class="user-header">
+			<view class="user-info">
+				<u-avatar :size="50"
+					src="https://img20.360buyimg.com/da/jfs/t1/141592/25/8861/261559/5f68d8c1E33ed78ab/698ad655bfcfbaed.png"></u-avatar>
+
+				<view class="greeting">
+					<text class="time">下午好!</text>
+					<text class="name">涨涨涨</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 设置列表 -->
+		<view class="settings-list">
+			<u-cell-group :border="false">
+				<u-cell v-for="(item, index) in settingsList" :key="index" :title="item.title" :isLink="true"
+					@click="handleClick(item)" :border="index==settingsList.length-1?false:true">
+					<template #icon>
+						<view class="cell-icon">
+							<u-icon :name="item.icon||'setting'" size="32rpx" color="#333"></u-icon>
+						</view>
+					</template>
+				</u-cell>
+			</u-cell-group>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	const settingsList = ref([{
+			title: '用户信息',
+			path: '/pages/my/page/user-info',
+			icon: "account"
+		},
+		{
+			title: '默认仓库',
+			path: '/pages/my/page/warehouse',
+			icon: 'home'
+		},
+		{
+			title: '学校设置',
+			path: '/pages/my/page/school'
+		},
+		{
+			title: '审核未完成',
+			path: '/pages/my/page/audit-unfinished',
+			icon: "order"
+		},
+		{
+			title: '版本设置',
+			path: '/pages/my/page/version',
+			icon: 'tags'
+		},
+		{
+			title: '音频设置',
+			path: '/pages/my/page/volume',
+			icon: 'volume'
+		},
+		{
+			title: '图书显示设置',
+			path: '/pages/my/page/book-display',
+			icon: 'bookmark'
+		},
+		{
+			title: '修改密码',
+			icon: 'lock',
+			path: '/pages/my/page/password'
+		},
+		{
+			title: '退出账号',
+			path: '/pages/my/page/logout',
+			type: 'logout'
+		}
+	])
+
+	const handleClick = (item) => {
+		if (item.type === 'logout') {
+			uni.showModal({
+				title: '提示',
+				content: '确定要退出登录吗?',
+				success: (res) => {
+					if (res.confirm) {
+						// 执行退出登录逻辑
+						uni.clearStorageSync()
+						uni.reLaunch({
+							url: '/pages/login/index'
+						})
+					}
+				}
+			})
+			return
+		}
+		uni.navigateTo({
+			url: item.path
+		})
+	}
+</script>
+
+<style lang="scss" scoped>
+	.settings-page {
+
+		.user-header {
+			background: #4CD964;
+			padding: 40rpx 32rpx;
+
+			.user-info {
+				display: flex;
+				align-items: center;
+
+				.greeting {
+					display: flex;
+					flex-direction: column;
+					margin-left: 20rpx;
+
+					.time {
+						font-size: 32rpx;
+						color: #FFFFFF;
+						margin-bottom: 8rpx;
+					}
+
+					.name {
+						font-size: 36rpx;
+						color: #FFFFFF;
+						font-weight: 500;
+					}
+				}
+			}
+		}
+
+		:deep(.u-cell) {
+			.u-cell__body {
+				padding: 15px;
+			}
+		}
+
+		.settings-list {
+			padding: 20rpx;
+
+			:deep(.u-cell-group) {
+				border-radius: 16rpx;
+				overflow: hidden;
+				background: #FFFFFF;
+			}
+		}
+	}
+</style>

+ 18 - 0
pages/my/page/audit-unfinished.vue

@@ -0,0 +1,18 @@
+<template>
+	<view class="audit-list">
+		<order-item></order-item>
+		<order-item></order-item>
+		<order-item></order-item>
+	</view>
+</template>
+
+<script setup>
+	import orderItem from '../components/orderItem.vue';
+</script>
+
+<style lang="scss">
+	.audit-list{
+		padding: 20rpx;
+		box-sizing: border-box;
+	}
+</style>

+ 90 - 0
pages/my/page/book-display.vue

@@ -0,0 +1,90 @@
+<template>
+    <view class="settings-container">
+        <!-- 设置列表 -->
+        <u-cell-group :border="false" class="settings-group">
+            <u-cell title="是否展示回收价" :border-bottom="true">
+                <template #right-icon>
+                    <u-switch v-model="settings.showRecyclePrice" activeColor="#4cd964"></u-switch>
+                </template>
+            </u-cell>
+
+            <u-cell title="是否展示回收状态" :border-bottom="true">
+                <template #right-icon>
+                    <u-switch v-model="settings.showRecycleStatus" activeColor="#4cd964"></u-switch>
+                </template>
+            </u-cell>
+
+            <u-cell title="是否展示库存信息" :border-bottom="true">
+                <template #right-icon>
+                    <u-switch v-model="settings.showInventoryInfo" activeColor="#4cd964"></u-switch>
+                </template>
+            </u-cell>
+
+            <u-cell title="是否默认展开商品详情" :border-bottom="false">
+                <template #right-icon>
+                    <u-switch v-model="settings.expandProductDetails" activeColor="#4cd964"></u-switch>
+                </template>
+            </u-cell>
+        </u-cell-group>
+    </view>
+</template>
+
+<script setup>
+import { reactive, onMounted } from 'vue'
+
+// 设置状态
+const settings = reactive({
+    showRecyclePrice: false,
+    showRecycleStatus: false,
+    showInventoryInfo: false,
+    expandProductDetails: false
+})
+
+// 加载保存的设置
+function loadSettings() {
+    try {
+        const savedSettings = uni.getStorageSync('display_settings')
+        if (savedSettings) {
+            Object.assign(settings, JSON.parse(savedSettings))
+        }
+    } catch (error) {
+        console.error('加载设置失败:', error)
+    }
+}
+
+// 保存设置
+function saveSettings() {
+    try {
+        uni.setStorageSync('display_settings', JSON.stringify(settings))
+        uni.showToast({
+            title: '设置已保存',
+            icon: 'success'
+        })
+    } catch (error) {
+        console.error('保存设置失败:', error)
+        uni.showToast({
+            title: '保存设置失败',
+            icon: 'error'
+        })
+    }
+}
+// 生命周期钩子
+onMounted(() => {
+    loadSettings()
+})
+</script>
+
+<style lang="scss" scoped>
+.settings-container {
+    min-height: 100vh;
+    background-color: #f5f5f5;
+    padding: 20rpx;
+
+    .settings-group {
+        background-color: #ffffff;
+        border-radius: 12rpx;
+        overflow: hidden;
+        box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+    }
+}
+</style>

+ 225 - 0
pages/my/page/password.vue

@@ -0,0 +1,225 @@
+<template>
+	<view class="password-container">
+		<u-form 
+			:model="formData" 
+			:rules="rules" 
+			ref="formRef"
+			labelPosition="top"
+			:borderBottom="true"
+			labelWidth="300px"
+		>
+			<u-form-item
+				label="原密码"
+				prop="oldPassword"
+				borderBottom
+			>
+				<u-input
+					v-model="formData.oldPassword"
+					type="password"
+					placeholder="请输入原密码"
+					:password="!showPassword.old"
+					:border="false"
+				>
+					<template #suffix>
+						<u-icon
+							:name="showPassword.old ? 'eye-fill' : 'eye-off'"
+							size="20"
+							color="#909399"
+							@click="togglePasswordVisible('old')"
+						></u-icon>
+					</template>
+				</u-input>
+			</u-form-item>
+
+			<u-form-item
+				label="新密码"
+				prop="newPassword"
+				borderBottom
+			>
+				<u-input
+					v-model="formData.newPassword"
+					type="password"
+					placeholder="请输入新密码"
+					:password="!showPassword.new"
+					:border="false"
+				>
+					<template #suffix>
+						<u-icon
+							:name="showPassword.new ? 'eye-fill' : 'eye-off'"
+							size="20"
+							color="#909399"
+							@click="togglePasswordVisible('new')"
+						></u-icon>
+					</template>
+				</u-input>
+			</u-form-item>
+
+			<u-form-item
+				label="确认新密码"
+				prop="confirmPassword"
+				borderBottom
+			>
+				<u-input
+					v-model="formData.confirmPassword"
+					type="password"
+					placeholder="请再次输入新密码"
+					:password="!showPassword.confirm"
+					:border="false"
+				>
+					<template #suffix>
+						<u-icon
+							:name="showPassword.confirm ? 'eye-fill' : 'eye-off'"
+							size="20"
+							color="#909399"
+							@click="togglePasswordVisible('confirm')"
+						></u-icon>
+					</template>
+				</u-input>
+			</u-form-item>
+		</u-form>
+
+		<view class="submit-btn">
+			<u-button
+				type="primary"
+				text="修改密码"
+				@click="submitForm"
+				:loading="loading"
+			></u-button>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+
+// 表单引用
+const formRef = ref(null)
+
+// 加载状态
+const loading = ref(false)
+
+// 密码显示状态
+const showPassword = reactive({
+	old: false,
+	new: false,
+	confirm: false
+})
+
+// 表单数据
+const formData = reactive({
+	oldPassword: '',
+	newPassword: '',
+	confirmPassword: ''
+})
+
+// 表单验证规则
+const rules = {
+	oldPassword: [{
+		required: true,
+		message: '请输入原密码',
+		trigger: ['blur', 'change']
+	}, {
+		min: 6,
+		message: '密码长度不能小于6位',
+		trigger: ['blur', 'change']
+	}],
+	newPassword: [{
+		required: true,
+		message: '请输入新密码',
+		trigger: ['blur', 'change']
+	}, {
+		min: 6,
+		message: '密码长度不能小于6位',
+		trigger: ['blur', 'change']
+	}, {
+		validator: (rule, value, callback) => {
+			if (value === formData.oldPassword) {
+				callback(new Error('新密码不能与原密码相同'))
+			} else {
+				callback()
+			}
+		},
+		trigger: ['blur', 'change']
+	}],
+	confirmPassword: [{
+		required: true,
+		message: '请再次输入新密码',
+		trigger: ['blur', 'change']
+	}, {
+		validator: (rule, value, callback) => {
+			if (value !== formData.newPassword) {
+				callback(new Error('两次输入的密码不一致'))
+			} else {
+				callback()
+			}
+		},
+		trigger: ['blur', 'change']
+	}]
+}
+
+// 切换密码显示状态
+function togglePasswordVisible(field) {
+	showPassword[field] = !showPassword[field]
+}
+
+// 提交表单
+function submitForm() {
+	if (!formRef.value) return
+	
+	formRef.value.validate(valid => {
+		if (valid) {
+			loading.value = true
+			// 这里添加修改密码的API调用
+			setTimeout(() => {
+				uni.showToast({
+					title: '密码修改成功',
+					icon: 'success'
+				})
+				loading.value = false
+				// 重置表单
+				resetForm()
+				// 可以添加成功后的跳转
+				// uni.navigateBack()
+			}, 1500)
+		}
+	})
+}
+
+// 重置表单
+function resetForm() {
+	if (!formRef.value) return
+	formRef.value.resetFields()
+}
+</script>
+
+<style>
+	page{
+		background-color: #ffffff;
+	}
+</style>
+<style lang="scss" scoped>
+.password-container {
+	padding: 30rpx;
+	box-sizing: border-box;
+
+	:deep(.u-form) {
+		.u-input {
+			&__content {
+				background-color: #f5f5f5;
+				border-radius: 8rpx;
+				padding: 20rpx;
+			}
+		}
+	}
+
+	.submit-btn {
+		margin-top: 60rpx;
+		padding: 0 20rpx;
+		
+		:deep(.u-button) {
+			height: 88rpx;
+			border-radius: 44rpx;
+		}
+	}
+}
+</style>

+ 127 - 0
pages/my/page/school.vue

@@ -0,0 +1,127 @@
+<template>
+	<view class="school-add">
+		<!-- 输入区域 -->
+		<view class="input-section">
+			<view class="input-header">
+				<text class="required">*</text>
+				<text class="label">学校名称:</text>
+				<u-input v-model="schoolName" placeholder="请输入一个学校名称" border="false" class="input">
+					<template #suffix>
+						<u-icon name="plus" size="24" color="#666" @click="handleAddSchool"></u-icon>
+					</template>
+				</u-input>
+			</view>
+		</view>
+
+		<!-- 学校列表 -->
+		<view class="school-list">
+			<view class="school-item" v-for="(item, index) in schoolList" :key="index">
+				<u-tag :text="item.name" closable></u-tag>
+			</view>
+		</view>
+
+		<!-- 底部提交按钮 -->
+		<view class="submit-btn">
+			<u-button type="success" block @click="handleSubmit">提交</u-button>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	// 学校名称输入
+	const schoolName = ref('')
+
+	// 学校列表
+	const schoolList = ref([{
+		name: '河南大学'
+	}, {
+		name: '清华大学'
+	}, {
+		name: '北京大学'
+	}])
+
+	// 添加学校
+	const handleAddSchool = () => {
+		if (!schoolName.value.trim()) {
+			uni.$u.toast('请输入学校名称')
+			return
+		}
+
+		schoolList.value.push({
+			name: schoolName.value
+		})
+		schoolName.value = ''
+	}
+
+	// 移除学校
+	const handleRemoveSchool = (index) => {
+		schoolList.value.splice(index, 1)
+	}
+
+	// 提交
+	const handleSubmit = () => {
+		if (schoolList.value.length === 0) {
+			uni.$u.toast('请至少添加一所学校')
+			return
+		}
+
+		// 这里处理提交逻辑
+		console.log('提交的学校列表:', schoolList.value)
+	}
+</script>
+
+<style lang="scss" scoped>
+	.school-add {
+		height: 100%;
+		padding: 30rpx;
+		box-sizing: border-box;
+		display: flex;
+		flex-direction: column;
+
+		.input-section {
+			background: #FFFFFF;
+			border-radius: 8rpx;
+			padding: 20rpx;
+			margin-bottom: 20rpx;
+
+			.input-header {
+				display: flex;
+				align-items: center;
+
+				.required {
+					color: #FF4D4F;
+					font-size: 28rpx;
+					margin-right: 4rpx;
+				}
+
+				.label {
+					font-size: 28rpx;
+					color: #333333;
+					white-space: nowrap;
+				}
+
+				.input {
+					flex: 1;
+				}
+			}
+		}
+
+		.school-list {
+			display: flex;
+			flex-wrap: wrap;
+			gap: 10rpx;
+		}
+
+		.submit-btn {
+			padding: 40rpx 0rpx;
+			padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
+			position: fixed;
+			width: calc(100% - 60rpx);
+			bottom: 0;
+		}
+	}
+</style>

+ 140 - 0
pages/my/page/user-info.vue

@@ -0,0 +1,140 @@
+<template>
+	<view class="user-info-page">
+		<!-- 头像区域 -->
+		<view class="avatar-section">
+			<view class="avatar-wrap">
+				<text>头像:</text>
+				<u-avatar :size="50"
+					src="https://img20.360buyimg.com/da/jfs/t1/141592/25/8861/261559/5f68d8c1E33ed78ab/698ad655bfcfbaed.png"></u-avatar>
+				<u-button type="success" style="width:200rpx" size="small" @click="handleEditAvatar">去修改</u-button>
+			</view>
+		</view>
+
+		<!-- 基本信息区域 -->
+		<view class="info-section">
+			<view class="section-title">基本信息</view>
+			<view class="info-card">
+				<u-cell-group :border="false">
+					<u-cell title="姓名" :value="userInfo.name || '无'" />
+					<u-cell title="性别" :value="userInfo.gender || '无'" />
+					<u-cell title="生日" :value="userInfo.birthday || '无'" />
+					<u-cell title="学历" :value="userInfo.education || '无'" />
+					<u-cell title="毕业学校" :value="userInfo.school || '无'" :border="false" />
+				</u-cell-group>
+			</view>
+		</view>
+
+		<!-- 账号信息区域 -->
+		<view class="info-section">
+			<view class="section-title">用户账号</view>
+			<view class="info-card">
+				<u-cell-group :border="false">
+					<u-cell title="账号" :value="userInfo.account" />
+					<u-cell title="创建日期" :value="userInfo.createTime" />
+					<u-cell title="最近登录" :value="userInfo.lastLogin || '--'" :border="false"  />
+				</u-cell-group>
+			</view>
+		</view>
+
+		<!-- 员工资料区域 -->
+		<view class="info-section">
+			<view class="section-title">员工资料</view>
+			<view class="info-card">
+				<u-cell-group :border="false">
+					<u-cell title="公司" :value="userInfo.company || '无'" />
+					<u-cell title="员工编号" :value="userInfo.employeeId || '无'" />
+					<u-cell title="职位" :value="userInfo.position || '无'" :border="false" />
+				</u-cell-group>
+			</view>
+		</view>
+
+		<!-- 联系方式区域 -->
+		<view class="info-section">
+			<view class="section-title">联系方式</view>
+			<view class="info-card">
+				<u-cell-group :border="false">
+					<u-cell title="电话" :value="userInfo.phone" />
+					<u-cell title="邮箱" :value="userInfo.email || '无'" />
+					<u-cell title="住址" :value="userInfo.address || '无'" :border="false" />
+				</u-cell-group>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	const userInfo = ref({
+		avatar: '/static/images/avatar.jpg',
+		name: '无',
+		gender: '无',
+		birthday: '无',
+		education: '无',
+		school: '无',
+		username: '用户账号',
+		account: 'fxt',
+		createTime: '2024-06-12 12:00:00',
+		lastLogin: '',
+		company: '无',
+		employeeId: '无',
+		position: '无',
+		phone: '18888888888',
+		email: '无',
+		address: '无'
+	})
+
+	const handleEditAvatar = () => {
+		uni.chooseImage({
+			count: 1,
+			success: (res) => {
+				// 处理头像上传逻辑
+				console.log('选择的图片:', res.tempFilePaths[0])
+			}
+		})
+	}
+</script>
+
+<style>
+	page {
+		background: #F5F6FA;
+	}
+</style>
+<style lang="scss" scoped>
+	.user-info-page {
+		padding: 20rpx;
+		box-sizing: border-box;
+
+		.avatar-section {
+			background: #FFFFFF;
+			border-radius: 16rpx;
+			padding: 40rpx;
+			margin-bottom: 20rpx;
+
+			.avatar-wrap {
+				display: flex;
+				align-items: center;
+				gap: 30rpx;
+			}
+		}
+
+		.info-section {
+			margin-bottom: 20rpx;
+
+			.section-title {
+				font-size: 28rpx;
+				color: #666;
+				padding: 20rpx 10rpx;
+				font-weight: 500;
+			}
+
+			.info-card {
+				background: #FFFFFF;
+				border-radius: 16rpx;
+				overflow: hidden;
+			}
+		}
+	}
+</style>

+ 51 - 0
pages/my/page/version.vue

@@ -0,0 +1,51 @@
+<template>
+	<view class="version-info-page">
+		<u-cell-group :border="false" customStyle="background:#ffffff">
+			<u-cell title="当前版本号(正式版)" :value="userInfo.version || '无'" />
+			<u-cell 
+				title="检测更新" 
+				:value="userInfo.updateStatus || '无'" 
+				:border="false" 
+				@click="checkForUpdates" 
+			/>
+		</u-cell-group>
+	</view>
+</template>
+
+<script setup>
+	import { reactive } from 'vue'
+
+	const userInfo = reactive({
+		version: 'v_2024050400_1', // Example version number
+		updateStatus: '已经是新版本' // Example update status
+	})
+
+	function checkForUpdates() {
+		// Simulate checking for updates
+		setTimeout(() => {
+			const newVersionAvailable = true; // Simulate a new version check
+
+			if (newVersionAvailable) {
+				userInfo.updateStatus = '发现新版本,正在下载...';
+				downloadNewVersion();
+			} else {
+				userInfo.updateStatus = '已经是新版本';
+			}
+		}, 1000);
+	}
+
+	function downloadNewVersion() {
+		// Simulate downloading the new version
+		setTimeout(() => {
+			userInfo.updateStatus = '下载完成,准备安装';
+			// Add logic to install the new version if needed
+		}, 2000);
+	}
+</script>
+
+<style>
+	.version-info-page {
+		padding: 20rpx;
+		border-radius: 20rpx;
+	}
+</style>

+ 420 - 0
pages/my/page/volume.vue

@@ -0,0 +1,420 @@
+<template>
+	<view class="settings-container">
+		<!-- 设置选项组 -->
+		<view class="settings-section">
+			<u-cell-group :border="false">
+				<!-- 语音播报设置 -->
+				<u-cell title="语音播报" label="控制除app启动提示外所有语音播报的开关" :border-bottom="true">
+					<template #right-icon>
+						<u-switch v-model="settings.voiceBroadcast" activeColor="#4cd964"></u-switch>
+					</template>
+				</u-cell>
+
+				<!-- 点击音效设置 -->
+				<u-cell title="点击音效" :border-bottom="true">
+					<template #right-icon>
+						<u-switch v-model="settings.clickSound" activeColor="#4cd964"></u-switch>
+					</template>
+				</u-cell>
+
+				<!-- 启动提示语设置 -->
+				<u-cell title="启动提示语" :border-bottom="true">
+					<template #right-icon>
+						<u-switch v-model="settings.startupPrompt" activeColor="#4cd964"></u-switch>
+					</template>
+				</u-cell>
+			</u-cell-group>
+
+			<!-- 启动提示语输入框 -->
+			<view class="input-section">
+				<u-textarea v-model="settings.startupText" placeholder="输入启动时要说的内容,比如:欢迎使用书嗨/很开心又见到你啦"
+					height="100"></u-textarea>
+			</view>
+
+			<!-- 语速设置 -->
+			<view class="slider-section">
+				<view class="slider-title">
+					<text>设置语速</text>
+					<text class="subtitle">(部分机型不支持)</text>
+				</view>
+				<u-slider v-model="settings.speechRate" :min="0" :max="10" :step="1" showValue></u-slider>
+			</view>
+
+			<!-- 音调设置 -->
+			<view class="slider-section">
+				<view class="slider-title">
+					<text>设置音调</text>
+					<text class="subtitle">(部分机型不支持)</text>
+				</view>
+				<u-slider v-model="settings.pitch" :min="0" :max="10" :step="1" showValue></u-slider>
+			</view>
+
+			<!-- 操作按钮 -->
+			<view class="button-group">
+				<u-button type="primary" text="保存设置并试听" @click="saveAndTest"></u-button>
+				<u-button type="warning" text="停止播放" @click="stopPlayback" :plain="true"></u-button>
+				<u-button type="error" text="重启app" @click="restartApp" :plain="true"></u-button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		onMounted
+	} from 'vue'
+
+	// TTS 引擎实例
+	let innerAudioContext = null;
+
+	// 设置状态
+	const settings = reactive({
+		voiceBroadcast: true,
+		clickSound: true,
+		startupPrompt: true,
+		startupText: '书嗨,不辜负每一个爱书的人',
+		speechRate: 10,
+		pitch: 10
+	})
+
+	// 检查并请求权限
+	async function checkAndRequestPermissions() {
+		// #ifdef APP-PLUS
+		try {
+			const status = await requestPermissions()
+			if (!status) {
+				uni.showModal({
+					title: '提示',
+					content: '请在系统设置中允许应用访问麦克风等权限,以便使用语音功能',
+					confirmText: '去设置',
+					success: (res) => {
+						if (res.confirm) {
+							// 跳转到应用设置页面
+							if (uni.getSystemInfoSync().platform === 'android') {
+								const main = plus.android.runtimeMainActivity()
+								const Intent = plus.android.importClass('android.content.Intent')
+								const Settings = plus.android.importClass('android.provider.Settings')
+								const Uri = plus.android.importClass('android.net.Uri')
+								const intent = new Intent()
+								intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+								intent.setData(Uri.fromParts('package', main.getPackageName(), null))
+								main.startActivity(intent)
+							} else {
+								// iOS
+								const UIApplication = plus.ios.import("UIApplication")
+								const NSURL = plus.ios.import("NSURL")
+								const setting = NSURL.URLWithString("app-settings:")
+								const application = UIApplication.sharedApplication()
+								application.openURL(setting)
+							}
+						}
+					}
+				})
+				return false
+			}
+			return true
+		} catch (error) {
+			console.error('权限请求失败:', error)
+			return false
+		}
+		// #endif
+
+		// #ifdef H5
+		// H5端检查Web Speech API权限
+		if ('speechSynthesis' in window) {
+			return true
+		}
+		uni.showToast({
+			title: '当前浏览器不支持语音功能',
+			icon: 'none'
+		})
+		return false
+		// #endif
+
+		// #ifdef MP-WEIXIN
+		// 微信小程序一般不需要特殊权限
+		return true
+		// #endif
+	}
+
+	// 请求权限
+	function requestPermissions() {
+		return new Promise((resolve, reject) => {
+			// #ifdef APP-PLUS
+			plus.android.requestPermissions(
+				[
+					'android.permission.RECORD_AUDIO',
+					'android.permission.MODIFY_AUDIO_SETTINGS',
+					'android.permission.WRITE_EXTERNAL_STORAGE',
+					'android.permission.READ_EXTERNAL_STORAGE'
+				],
+				function(resultObj) {
+					if (resultObj.granted.length === 4) {
+						resolve(true)
+					} else {
+						resolve(false)
+					}
+				},
+				function(error) {
+					reject(error)
+				}
+			)
+			// #endif
+
+			// 其他平台直接返回true
+			resolve(true)
+		})
+	}
+
+	// 初始化 TTS 时检查权限
+	async function initTTS() {
+		const hasPermission = await checkAndRequestPermissions()
+		if (!hasPermission) {
+			return
+		}
+
+		// 检查平台是否支持 TTS
+		if (!uni.createInnerAudioContext) {
+			uni.showToast({
+				title: '当前设备不支持语音功能',
+				icon: 'none'
+			})
+			return
+		}
+
+		try {
+			// 创建音频实例
+			innerAudioContext = uni.createInnerAudioContext()
+
+			// 监听错误
+			innerAudioContext.onError((res) => {
+				console.error('音频播放错误:', res.errMsg)
+				uni.showToast({
+					title: '语音播放失败',
+					icon: 'none'
+				})
+			})
+		} catch (error) {
+			console.error('TTS初始化失败:', error)
+		}
+	}
+
+	// 文字转语音
+	async function textToSpeech(text) {
+		const hasPermission = await checkAndRequestPermissions()
+		if (!hasPermission) {
+			return
+		}
+
+		if (!settings.voiceBroadcast) return
+
+		try {
+			// 构建 TTS 参数
+			const ttsParams = {
+				lang: 'zh-CN',
+				speed: settings.speechRate / 10, // 将 0-10 转换为 0-1
+				pitch: settings.pitch / 10, // 将 0-10 转换为 0-1
+				volume: 1,
+				text: text
+			}
+
+			// #ifdef APP-PLUS
+			// App 端使用原生 TTS
+			const TTSModule = uni.requireNativePlugin('nrb-tts-plugin')
+			TTSModule && TTSModule.init({
+				"lang": "ZH",
+				"country": "CN"
+			}, res => {
+				if (res.success == 0) {
+					TTSModule.speak(text, ttsParams, (e) => {
+						console.log(e, 'dsadsads')
+					})
+				}
+			})
+			// #endif
+
+			// #ifdef H5
+			// H5端使用Web Speech API
+			if ('speechSynthesis' in window) {
+				const utterance = new SpeechSynthesisUtterance(text)
+				utterance.lang = ttsParams.lang
+				utterance.rate = ttsParams.speed
+				utterance.pitch = ttsParams.pitch
+				utterance.volume = ttsParams.volume
+				speechSynthesis.speak(utterance)
+			}
+			// #endif
+
+			// #ifdef MP-WEIXIN
+			// 微信小程序使用微信同声传译插件
+			const plugin = requirePlugin("WechatSI")
+			plugin.textToSpeech({
+				lang: "zh_CN",
+				tts: text,
+				success: function(res) {
+					innerAudioContext.src = res.filename
+					innerAudioContext.play()
+				},
+				fail: function(res) {
+					console.error("转换失败", res)
+				}
+			})
+			// #endif
+		} catch (error) {
+			console.error('TTS播放错误:', error)
+			uni.showToast({
+				title: '语音播放失败',
+				icon: 'none'
+			})
+		}
+	}
+
+	// 保存设置并试听
+	function saveAndTest() {
+		// 保存设置到本地存储
+		uni.setStorageSync('tts_settings', settings)
+
+		// 试听当前设置
+		textToSpeech(settings.startupText)
+
+		uni.showToast({
+			title: '设置已保存',
+			icon: 'success'
+		})
+	}
+
+	// 停止播放
+	function stopPlayback() {
+		// #ifdef APP-PLUS
+		const TTSModule = uni.requireNativePlugin('nrb-tts-plugin')
+		if (TTSModule && TTSModule.stop) {
+			TTSModule.stop()
+		}
+		// #endif
+
+		// #ifdef H5
+		if ('speechSynthesis' in window) {
+			speechSynthesis.cancel()
+		}
+		// #endif
+
+		// #ifdef MP-WEIXIN
+		if (innerAudioContext) {
+			innerAudioContext.stop()
+		}
+		// #endif
+
+		uni.showToast({
+			title: '已停止播放',
+			icon: 'none'
+		})
+	}
+
+	// 重启应用
+	function restartApp() {
+		uni.showModal({
+			title: '提示',
+			content: '确定要重启应用吗?',
+			success: function(res) {
+				if (res.confirm) {
+					// #ifdef APP-PLUS
+					plus.runtime.restart()
+					// #endif
+
+					// #ifdef H5 || MP-WEIXIN
+					uni.reLaunch({
+						url: '/pages/index/index'
+					})
+					// #endif
+				}
+			}
+		})
+	}
+
+	// 加载保存的设置
+	function loadSettings() {
+		try {
+			const savedSettings = uni.getStorageSync('tts_settings')
+			if (savedSettings) {
+				Object.assign(settings, savedSettings)
+			}
+		} catch (error) {
+			console.error('加载设置失败:', error)
+		}
+	}
+
+	// 播放点击音效
+	function playClickSound() {
+		if (!settings.clickSound) return
+
+		const audio = uni.createInnerAudioContext()
+		audio.src = '/static/audio/click.mp3' // 确保添加点击音效文件
+		audio.play()
+	}
+
+	// 生命周期钩子
+	onMounted(async () => {
+		await initTTS()
+		loadSettings()
+	})
+</script>
+
+<style lang="scss" scoped>
+	.settings-container {
+		min-height: 100vh;
+		background-color: #f5f5f5;
+		padding-bottom: 40rpx;
+
+		.card-head {
+			display: flex;
+			align-items: center;
+			padding: 10rpx 0;
+
+			.head-title {
+				margin-left: 10rpx;
+				font-size: 32rpx;
+				font-weight: bold;
+			}
+		}
+
+		.settings-section {
+			margin: 20rpx;
+			background-color: #ffffff;
+			border-radius: 12rpx;
+			padding: 20rpx;
+			box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+		}
+
+		.input-section {
+			margin: 30rpx 0;
+			padding: 0 20rpx;
+		}
+
+		.slider-section {
+			margin: 30rpx 0;
+			padding: 0 20rpx;
+
+			.slider-title {
+				margin-bottom: 20rpx;
+				font-size: 28rpx;
+				color: #333;
+
+				.subtitle {
+					font-size: 24rpx;
+					color: #999;
+					margin-left: 10rpx;
+				}
+			}
+		}
+
+		.button-group {
+			margin-top: 50rpx;
+			padding: 0 20rpx;
+
+			:deep(.u-button) {
+				margin-bottom: 20rpx;
+			}
+		}
+	}
+</style>

+ 130 - 0
pages/my/page/warehouse.vue

@@ -0,0 +1,130 @@
+<template>
+	<view class="warehouse-select">
+		<view class="select-wrap" @click="showPicker = true">
+			<text class="required-label">*</text>
+			<text class="label">默认仓库</text>
+			<view class="picker-value">
+				<text :class="['value-text', !selectedWarehouse && 'placeholder']">
+					{{ selectedWarehouse || '请选择仓库' }}
+				</text>
+				<u-icon name="arrow-right" color="#999" size="16"></u-icon>
+			</view>
+		</view>
+
+		<!-- 仓库选择器 -->
+		<u-picker :show="showPicker" :columns="[warehouseList]" @confirm="onConfirm" @cancel="showPicker = false"
+			@close="showPicker = false" title="选择仓库" confirmText="确定" cancelText="取消"></u-picker>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+
+	// 选择器显示状态
+	const showPicker = ref(false)
+
+	// 选中的仓库
+	const selectedWarehouse = ref('')
+
+
+	// 仓库列表
+	const warehouseList = ref([{
+			text: '北京仓库',
+			value: '1'
+		},
+		{
+			text: '上海仓库',
+			value: '2'
+		},
+		{
+			text: '广州仓库',
+			value: '3'
+		},
+		{
+			text: '深圳仓库',
+			value: '4'
+		},
+	])
+
+	// 确认选择
+	const onConfirm = (e) => {
+		const [{
+			value,
+			text
+		}] = e.value
+		selectedWarehouse.value = text
+		showPicker.value = false
+		// 触发选择事件
+		emit('change', value)
+	}
+
+	// 定义事件
+	const emit = defineEmits(['change'])
+</script>
+
+<style lang="scss" scoped>
+	.warehouse-select {
+		padding: 20rpx;
+
+		.select-wrap {
+			display: flex;
+			align-items: center;
+			background: #FFFFFF;
+			min-height: 88rpx;
+			padding: 0 20rpx;
+			position: relative;
+
+			.required-label {
+				color: #FF4D4F;
+				font-size: 28rpx;
+				margin-right: 4rpx;
+			}
+
+			.label {
+				font-size: 28rpx;
+				color: #333333;
+				margin-right: 20rpx;
+				white-space: nowrap;
+			}
+
+			.picker-value {
+				flex: 1;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+
+				.value-text {
+					font-size: 28rpx;
+					color: #333333;
+
+					&.placeholder {
+						color: #999999;
+					}
+				}
+
+				.count {
+					font-size: 24rpx;
+					color: #999999;
+					margin-right: 10rpx;
+				}
+			}
+
+			&::after {
+				content: '';
+				position: absolute;
+				left: 20rpx;
+				right: 20rpx;
+				bottom: 0;
+				height: 1px;
+				background: #EEEEEE;
+				transform: scaleY(0.5);
+			}
+
+			&:active {
+				background: #F5F5F5;
+			}
+		}
+	}
+</style>

+ 198 - 0
pages/order/index.vue

@@ -0,0 +1,198 @@
+<template>
+	<view class="order-statistics">
+		<!-- 顶部统计卡片 -->
+		<view class="overview-card">
+			<view class="overview-grid">
+				<view class="grid-item">
+					<text class="item-label">今日订单</text>
+					<text class="item-value">1361</text>
+				</view>
+				<view class="grid-item">
+					<text class="item-label">昨日订单</text>
+					<text class="item-value">1361</text>
+				</view>
+				<view class="grid-item">
+					<text class="item-label">本月订单</text>
+					<text class="item-value">2.77<text class="unit">万</text></text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 订单状态网格 -->
+		<view class="status-grid">
+			<view v-for="(item, index) in statusList" :key="index" class="status-item"
+				@tap="navigateToDetail(item.path)">
+				<view class="status-content">
+					<text class="status-label">{{ item.label }}</text>
+					<view class="status-value-wrap">
+						<text class="status-value">{{ item.value }}</text>
+						<u-icon name="arrow-right" color="#999" size="14"></u-icon>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 底部操作项 -->
+		<view class="bottom-actions">
+			<u-cell-group :border="false">
+				<u-cell title="收货统计" isLink @click="navigateToDetail('/pages/statistics/receive')"></u-cell>
+				<u-cell title="全部订单" isLink @click="navigateToDetail('/pages/order/stat/all')" :border="false"></u-cell>
+			</u-cell-group>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import PageScroll from '@/components/pageScroll/index.vue'
+	import {
+		ref
+	} from 'vue'
+
+	// 订单状态数据
+	const statusList = ref([{
+			label: '待初审',
+			value: '1361',
+			path: '/pages/order/stat/pending-review'
+		},
+		{
+			label: '待拣件',
+			value: '1361',
+			path: '/pages/order/stat/pending-pick'
+		},
+		{
+			label: '已拣件待签收',
+			value: '1361',
+			path: '/pages/order/stat/pending-sign'
+		},
+		{
+			label: '待确认收货',
+			value: '1361',
+			path: '/pages/order/stat/pending-confirm'
+		},
+		{
+			label: '已到货待审核',
+			value: '1361',
+			path: '/pages/order/stat/pending-audit'
+		},
+		{
+			label: '待付款',
+			value: '1361',
+			path: '/pages/order/stat/pending-payment'
+		}
+	])
+
+	// 页面跳转
+	const navigateToDetail = (path) => {
+		uni.navigateTo({
+			url: path
+		})
+	}
+</script>
+
+<style>
+	page {
+		background: #F5F6FA;
+	}
+</style>
+<style lang="scss" scoped>
+	.order-statistics {
+		padding: 20rpx;
+
+		.overview-card {
+			background: #FFFFFF;
+			border-radius: 16rpx;
+			padding: 30rpx 20rpx;
+			margin-bottom: 20rpx;
+
+			.overview-grid {
+				display: flex;
+				justify-content: space-between;
+
+				.grid-item {
+					flex: 1;
+					text-align: center;
+					position: relative;
+
+					&:not(:last-child)::after {
+						content: '';
+						position: absolute;
+						right: 0;
+						top: 50%;
+						transform: translateY(-50%);
+						height: 60%;
+						width: 2rpx;
+						background: #EEEEEE;
+					}
+
+					.item-label {
+						font-size: 28rpx;
+						color: #666666;
+						margin-bottom: 12rpx;
+						display: block;
+					}
+
+					.item-value {
+						font-size: 40rpx;
+						color: #333333;
+						font-weight: 500;
+
+						.unit {
+							font-size: 24rpx;
+							margin-left: 4rpx;
+						}
+					}
+				}
+			}
+		}
+
+		:deep(.u-cell) {
+			.u-cell__body {
+				padding: 15px;
+			}
+		}
+
+		.status-grid {
+			display: grid;
+			grid-template-columns: repeat(3, 1fr);
+			gap: 20rpx;
+			margin-bottom: 20rpx;
+
+			.status-item {
+				background: #FFFFFF;
+				border-radius: 16rpx;
+				padding: 24rpx 20rpx;
+
+				.status-content {
+					.status-label {
+						font-size: 28rpx;
+						color: #666666;
+						margin-bottom: 16rpx;
+						display: block;
+					}
+
+					.status-value-wrap {
+						display: flex;
+						align-items: center;
+						justify-content: space-between;
+
+						.status-value {
+							font-size: 36rpx;
+							color: #333333;
+							font-weight: 500;
+						}
+					}
+				}
+
+				&:active {
+					opacity: 0.8;
+				}
+			}
+		}
+
+		.bottom-actions {
+			background: #FFFFFF;
+			border-radius: 16rpx;
+			overflow: hidden;
+		}
+	}
+</style>

+ 13 - 0
pages/order/stat/pending-audit.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/pending-confirm.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/pending-payment.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/pending-pick.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/pending-review.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/pending-sign.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 13 - 0
pages/order/stat/receive-stat.vue

@@ -0,0 +1,13 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script setup>
+	
+</script>
+
+<style>
+	       
+</style>

+ 5 - 0
static/css/index.scss

@@ -0,0 +1,5 @@
+:deep(.u-cell) {
+	.u-cell__body {
+		padding: 15px;
+	}
+}

+ 148 - 0
static/css/mystyle.css

@@ -0,0 +1,148 @@
+.w100 {
+	width: 100%!important;
+}
+.h100 {
+	height: 100%!important;
+}
+
+/* 定位 */
+.pos-re {
+	position: relative;
+}
+.pos-ab {
+	position: absolute;
+}
+
+.box-s {
+	box-sizing: border-box;
+}
+.flex {
+	display: flex;
+}
+.flex-1 {
+	flex: 1;
+}
+
+.flex-a {
+	display: flex;
+	align-items: center;
+}
+
+.flex-a-s {
+	align-items: flex-start;
+}
+.flex-a-c {
+	align-items: center;
+}
+.flex-a-e {
+	align-items: flex-end;
+}
+
+.flex-c {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.flex-d {
+	display: flex;
+	flex-direction: column;
+}
+.flex-w {
+	display: flex;
+	flex-wrap: wrap;
+}
+
+.flex-j-a {
+	justify-content: space-around;
+}
+
+.flex-j-c {
+	justify-content: center;
+}
+
+.flex-j-b {
+	justify-content: space-between;
+}
+
+/* 文本仅显示 1 行 */
+.line-1 {
+	width: 100%;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	overflow: hidden;
+	word-break: break-all;
+}
+
+/* 文本仅显示 2 行 */
+.line-2 {
+	text-overflow: -o-ellipsis-lastline;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	line-clamp: 2;
+	-webkit-box-orient: vertical;
+}
+
+.m-2 {
+	margin: 20rpx;
+}
+.m-l-1 {
+	margin-left: 10rpx;
+}
+.m-l-2 {
+	margin-left: 20rpx;
+}
+.m-l-3 {
+	margin-left: 30rpx;
+}
+.m-r-1 {
+	margin-right: 10rpx;
+}
+.m-r-2 {
+	margin-right: 20rpx;
+}
+.m-r-3 {
+	margin-right: 30rpx;
+}
+.m-t-0 {
+	margin-top: 0;
+}
+.m-t-1 {
+	margin-top: 10rpx;
+}
+.m-t-2 {
+	margin-top: 20rpx;
+}
+.m-t-3 {
+	margin-top: 30rpx;
+}
+.m-b-2 {
+	margin-bottom: 20rpx;
+}
+.p-2 {
+	padding: 20rpx;
+}
+.p-l-2 {
+	padding-left: 20rpx;
+}
+.p-r-2 {
+	padding-right: 20rpx;
+}
+.p-t-2 {
+	padding-top: 20rpx;
+}
+.p-b-2 {
+	padding-bottom: 20rpx;
+}
+
+.text-l {
+	text-align: left;
+}
+.text-c {
+	text-align: center;
+}
+.text-r {
+	text-align: right;
+}

BIN
static/tabbar/book.png


BIN
static/tabbar/bookHl.png


BIN
static/tabbar/home.png


BIN
static/tabbar/homeHl.png


BIN
static/tabbar/mine.png


BIN
static/tabbar/mineHl.png


BIN
static/tabbar/order.png


BIN
static/tabbar/orderHl.png


+ 10 - 0
store/index.js

@@ -0,0 +1,10 @@
+import {useStore} from '@/store/uses.js'
+import { createPinia } from 'pinia';
+const pinia = createPinia();
+
+/**
+ * 导出 store 需要使用的页面直接
+ * import { store } from '@/store'
+ * 即可使用 store 中的属于和方法
+ */
+export const store = useStore(pinia)

+ 34 - 0
store/uses.js

@@ -0,0 +1,34 @@
+import {
+	defineStore
+} from 'pinia'
+
+export const useStore = defineStore('user', {
+	state: () => ({
+		userInfo: {},		// 用户信息
+		token: '',
+		// uploadUrl: 'http://192.168.1.3:8080/ai-file/upload/oss', // 上传资源的服务器网址
+		uploadUrl: 'https://gc.sdzcq.com/prod-api/ai-file/upload/oss', // 上传资源的服务器网址
+	}),
+	getters: {},
+	actions: {
+		setUserInfo(data) {
+			if (data.userId) {
+				console.log('更新全局的用户信息-也更新本地存储', data);
+				this.userInfo = data
+				uni.setStorageSync('userInfo', JSON.stringify(data))
+			} else {
+				uni.removeStorageSync('userInfo')		// 清空
+			}
+		},
+		setToken(token) {
+			if (token) {
+				console.log('更新全局的 token -也更新本地存储', token);
+				this.token = token
+				uni.setStorageSync('token', token)
+			} else {
+				uni.removeStorageSync('token')		// 清空
+			}
+		}
+	}
+
+})

+ 10 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,10 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
+    });
+  },
+});

+ 78 - 0
uni.scss

@@ -0,0 +1,78 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+// 在根目录uni.scss中引入uView的主题样式
+@import '@/uni_modules/uview-plus/theme.scss';
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#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;

+ 8 - 0
uni_modules/mescroll-uni/changelog.md

@@ -0,0 +1,8 @@
+## 1.3.8(2023-03-27)
+1. 新增useMescroll的hook, 支持vue3 script setup的写法  
+2. 新增vue3 script setup的示例 ( 根据vue2的示例,全部重写了一遍 )  
+3. mescroll-body 和 mescroll-uni 无需再写 ref="mescrollRef"  
+4. 解决mescroll-uni在页面渲染之后,无法动态设置height的问题  
+5. 解决renderjs在h5返回有时候无法正常滑动的问题  
+6. 修复小程序编辑器提示 Cannot read property 'nv_optDown' of undefined 的错误  
+-by 小瑾同学

+ 19 - 0
uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css

@@ -0,0 +1,19 @@
+.mescroll-body {
+	position: relative; /* 下拉刷新区域相对自身定位 */
+	height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+	overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+	overflow: unset !important
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 400 - 0
uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue

@@ -0,0 +1,400 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+	@touchstart="wxsBiz.touchstartEvent" 
+	@touchmove="wxsBiz.touchmoveEvent" 
+	@touchend="wxsBiz.touchendEvent" 
+	@touchcancel="wxsBiz.touchendEvent"
+	:change:prop="wxsBiz.propObserver"
+	:prop="wxsProp"
+	>
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+					<view class="downwarp-tip">{{downText}}</view>
+				</view>
+			</view>
+	
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+			<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+			<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+				<view v-show="upLoadType===1">
+					<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+				</view>
+				<!-- 无数据 -->
+				<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+			</view>
+		</view>
+		
+		<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+		<!-- #ifdef H5 -->
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from "../mescroll-uni/wxs/renderjs.js";
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from "../mescroll-uni/mescroll-uni.js";
+	// 引入全局配置
+	import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
+	// 引入国际化工具类
+	import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from "../mescroll-uni/wxs/mixins.js";
+	
+	/**
+	 * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
+	 * @event {Function} init 初始化完成的回调 
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调 
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
+	 */
+	export default {
+		name: 'mescroll-body',
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				windowHeight: 0, // 可使用窗口的高度
+				windowBottom: 0, // 可使用窗口的底部位置
+				statusBarHeight: 0 // 状态栏高度
+			};
+		},
+		computed: {
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			minHeight(){
+				return this.toPx(this.height || '100%') + 'px'
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			padTop() {
+				return this.numTop + 'px';
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom);
+			},
+			padBottom() {
+				return this.numBottom + 'px';
+			},
+			// 是否为重置下拉的状态
+			isDownReset() {
+				return this.downLoadType === 3 || this.downLoadType === 4;
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num) {
+				if (typeof num === 'string') {
+					if (num.indexOf('px') !== -1) {
+						if (num.indexOf('rpx') !== -1) {
+							// "10rpx"
+							num = num.replace('rpx', '');
+						} else if (num.indexOf('upx') !== -1) {
+							// "10upx"
+							num = num.replace('upx', '');
+						} else {
+							// "10px"
+							return Number(num.replace('px', ''));
+						}
+					} else if (num.indexOf('%') !== -1) {
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace('%', '')) / 100;
+						return this.windowHeight * rate;
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0;
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll);
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+							if(vm.downLoadType === 4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll);
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+					}
+				}
+			};
+			
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../mescroll-body/mescroll-body.css";
+	@import "../mescroll-uni/components/mescroll-down.css";
+	@import "../mescroll-uni/components/mescroll-up.css";
+</style>

+ 122 - 0
uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue

@@ -0,0 +1,122 @@
+<!--空布局:
+遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+-->
+<template>
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+		<!-- <view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view> -->
+		
+		<view> <image v-if="icon" class="empty-icon" src="./no-result.png" mode="widthFix" /> </view>
+		<view v-if="tip" class="empty-tip">{{ tip }}</view>
+		<view v-if="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
+// 引入国际化工具类
+import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
+				return this.option.icon
+			} else{
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				if (this.option.i18n) {
+					return this.option.i18n[i18nType].icon
+				} else{
+					return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
+				}
+			}
+		},
+		// 文本提示
+		tip() {
+			if (this.option.tip != null) { // 支持传空串不显示文本提示
+				return this.option.tip
+			} else{
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				if (this.option.i18n) {
+					return this.option.i18n[i18nType].tip
+				} else{
+					return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
+				}
+			}
+		},
+		// 按钮文本
+		btnText() {
+			if (this.option.i18n) {
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				return this.option.i18n[i18nType].btnText
+			} else{
+				return this.option.btnText
+			}
+		}
+	},
+	methods: {
+		// 点击按钮
+		emptyClick() {
+			this.$emit('emptyclick');
+		}
+	}
+};
+</script>
+
+<style>
+/* 无任何数据的空布局 */
+.mescroll-empty {
+	box-sizing: border-box;
+	width: 100%;
+	padding: 100rpx 50rpx;
+	text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+	z-index: 99;
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+	top: 100rpx;
+	left: 0;
+}
+
+.mescroll-empty .empty-icon {
+	width: 280rpx;
+	height: 280rpx;
+}
+
+.mescroll-empty .empty-tip {
+	margin-top: 20rpx;
+	height: 40rpx;
+	font-size: 28rpx;
+	font-family: PingFangSC;
+	font-weight: 500;
+	color: #666666;
+	line-height: 40rpx;
+}
+
+.mescroll-empty .empty-btn {
+	display: inline-block;
+	margin-top: 40rpx;
+	min-width: 200rpx;
+	padding: 18rpx;
+	font-size: 28rpx;
+	border: 1rpx solid #e04b28;
+	border-radius: 60rpx;
+	color: #e04b28;
+}
+
+.mescroll-empty .empty-btn:active {
+	opacity: 0.75;
+}
+</style>

BIN
uni_modules/mescroll-uni/components/mescroll-empty/no-result.png


+ 55 - 0
uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.css

@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+	position: absolute;
+	top: -100%;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	text-align: center;
+}
+
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	min-height: 60rpx;
+	padding: 20rpx 0;
+	text-align: center;
+}
+
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	margin-left: 16rpx;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+	animation: mescrollDownRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollDownRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 47 - 0
uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue

@@ -0,0 +1,47 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+			<view class="downwarp-tip">{{downText}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+		rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return 'rotate(' + 360 * this.rate + 'deg)'
+		},
+		// 文本提示
+		downText(){
+			switch (this.type){
+				case 1: return this.mOption.textInOffset;
+				case 2: return this.mOption.textOutOffset;
+				case 3: return this.mOption.textLoading;
+				case 4: return this.mOption.textLoading;
+				default: return this.mOption.textInOffset;
+			}
+		}
+	}
+};
+</script>
+
+<style>
+@import "./mescroll-down.css";
+</style>

+ 99 - 0
uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue

@@ -0,0 +1,99 @@
+<!-- 回到顶部的按钮 -->
+<template>
+	<image
+		v-if="option.src"
+		class="mescroll-totop"
+		:class="[isShow ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': option.safearea}]"
+		:style="{'z-index':option.zIndex, 'left': left, 'right': right, 'bottom':addUnit(option.bottom), 'width':addUnit(option.width), 'border-radius':addUnit(option.radius)}"
+		:src="option.src"
+		mode="widthFix"
+		@click="toTopClick"
+	/>
+</template>
+
+<script>
+export default {
+	props: {
+		// up.toTop的配置项
+		option: {
+			type: Object,
+			default(){
+				return {}
+			}
+		},
+		// 是否显示
+		value: false, // vue2
+		modelValue: false // vue3
+	},
+	computed: {
+		// 优先显示左边
+		left(){
+			return this.option.left ? this.addUnit(this.option.left) : 'auto';
+		},
+		// 右边距离 (优先显示左边)
+		right() {
+			return this.option.left ? 'auto' : this.addUnit(this.option.right);
+		},
+		// 是否显示
+		isShow(){
+			// #ifdef VUE3
+			return this.modelValue
+			// #endif
+			// #ifdef VUE2
+			return this.value
+			// #endif
+		}
+	},
+	methods: {
+		addUnit(num){
+			if(!num) return 0;
+			if(typeof num === 'number') return num + 'rpx';
+			return num
+		},
+		toTopClick() {
+			// #ifdef VUE3
+			this.$emit("update:modelValue", false); // 使v-model生效 vue3
+			// #endif
+			// #ifdef VUE2
+			this.$emit('input', false); // 使v-model生效 vue2
+			// #endif
+			this.$emit('click'); // 派发点击事件
+		}
+	}
+};
+</script>
+
+<style>
+/* 回到顶部的按钮 */
+.mescroll-totop {
+	z-index: 9990;
+	position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+	right: 20rpx;
+	bottom: 120rpx;
+	width: 72rpx;
+	height: auto;
+	border-radius: 50%;
+	opacity: 0;
+	transition: opacity 0.5s; /* 过渡 */
+	margin-bottom: var(--window-bottom); /* css变量 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-totop-safearea {
+		margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+		margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+	}
+}
+
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+	opacity: 1;
+}
+
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+	opacity: 0;
+	pointer-events: none;
+}
+</style>

+ 47 - 0
uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.css

@@ -0,0 +1,47 @@
+/* 上拉加载区域 */
+.mescroll-upwarp {
+	box-sizing: border-box;
+	min-height: 110rpx;
+	padding: 30rpx 0;
+	text-align: center;
+	clear: both;
+}
+
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+.mescroll-upwarp .upwarp-tip {
+	margin-left: 16rpx;
+}
+
+/*旋转进度条 */
+.mescroll-upwarp .upwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-upwarp .mescroll-rotate {
+	animation: mescrollUpRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollUpRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 39 - 0
uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue

@@ -0,0 +1,39 @@
+<!-- 上拉加载区域 -->
+<template>
+	<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+		<view v-show="isUpLoading">
+			<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
+		</view>
+		<!-- 无数据 -->
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object, // up的配置项
+		type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 加载中
+		isUpLoading() {
+			return this.type === 1;
+		},
+		// 没有更多了
+		isUpNoMore() {
+			return this.type === 2;
+		}
+	}
+};
+</script>
+
+<style>
+@import './mescroll-up.css';
+</style>

+ 15 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js

@@ -0,0 +1,15 @@
+// 国际化工具类
+const mescrollI18n = {
+	// 默认语言
+	def: "zh",
+	// 获取当前语言类型
+	getType(){
+		return uni.getStorageSync("mescroll-i18n") || this.def
+	},
+	// 设置当前语言类型
+	setType(type){
+		uni.setStorageSync("mescroll-i18n", type)
+	}
+}
+
+export default mescrollI18n

+ 46 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js

@@ -0,0 +1,46 @@
+// mescroll-body 和 mescroll-uni 通用
+const MescrollMixin = {
+	data() {
+		return {
+			mescroll: null //mescroll实例对象
+		}
+	},
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	onPullDownRefresh(){
+		this.mescroll && this.mescroll.onPullDownRefresh();
+	},
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onPageScroll(e) {
+		this.mescroll && this.mescroll.onPageScroll(e);
+	},
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onReachBottom() {
+		this.mescroll && this.mescroll.onReachBottom();
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+		},
+		// 下拉刷新的回调 (mixin默认resetUpScroll)
+		downCallback() {
+			if(this.mescroll.optUp.use){
+				this.mescroll.resetUpScroll()
+			}else{
+				setTimeout(()=>{
+					this.mescroll.endSuccess();
+				}, 500)
+			}
+		},
+		// 上拉加载的回调
+		upCallback() {
+			// mixin默认延时500自动结束加载
+			setTimeout(()=>{
+				this.mescroll.endErr();
+			}, 500)
+		}
+	}
+	
+}
+
+export default MescrollMixin;

+ 64 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js

@@ -0,0 +1,64 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+			right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+		}
+	},
+	// 国际化配置
+	i18n: {
+		// 中文
+		zh: {
+			down: {
+				textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+				textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textSuccess: '加载成功', // 加载成功的文本
+				textErr: '加载失败', // 加载失败的文本
+			},
+			up: {
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textNoMore: '-- END --', // 没有更多数据的提示文本
+				empty: {
+					tip: '暂无数据' // 空提示
+				}
+			}
+		},
+		// 英文
+		en: {
+			down: {
+				textInOffset: 'drop down refresh',
+				textOutOffset: 'release updates',
+				textLoading: 'loading ...',
+				textSuccess: 'loaded successfully',
+				textErr: 'loading failed'
+			},
+			up: {
+				textLoading: 'loading ...',
+				textNoMore: '-- END --',
+				empty: {
+					tip: '~ absolutely empty ~'
+				}
+			}
+		}
+	}
+}
+
+export default GlobalOption

+ 39 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.css

@@ -0,0 +1,39 @@
+.mescroll-uni-warp {
+	height: 100%;
+}
+
+.mescroll-uni-content {
+	height: 100%;
+}
+
+.mescroll-uni {
+	position: relative;
+	width: 100%;
+	height: 100%;
+	min-height: 200rpx;
+	overflow-y: auto;
+	box-sizing: border-box;
+	/* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed {
+	z-index: 1;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	width: auto;
+	/* 使right生效 */
+	height: auto;
+	/* 使bottom生效 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 799 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.js

@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.7
+ * 2021-04-12 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.7'; // mescroll版本号
+	me.options = options || {}; // 配置
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+
+	// 初始化下拉刷新
+	me.initDownScroll();
+	// 初始化上拉加载,则初始化
+	me.initUpScroll();
+
+	// 自动加载
+	setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+		// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+		if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+			if (me.optDown.autoShowLoading) {
+				me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
+			} else {
+				me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
+			}
+		}
+		// 自动触发上拉加载
+		if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+			setTimeout(function(){
+				me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
+			},100)
+		}
+	}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
+}
+
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function(optDown) {
+	// 下拉刷新的配置
+	MeScroll.extend(optDown, {
+		use: true, // 是否启用下拉刷新; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+		native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+		autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+		isLock: false, // 是否锁定下拉刷新,默认false;
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+		inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+		minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 下拉刷新初始化完毕的回调
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+		showLoading: null, // 显示下拉刷新进度的回调
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+		endDownScroll: null, // 结束下拉刷新的回调
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+		callback: function(mescroll) {
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
+			mescroll.resetUpScroll();
+		}
+	})
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+	// 上拉加载的配置
+	MeScroll.extend(optUp, {
+		use: true, // 是否启用上拉加载; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+		isLock: false, // 是否锁定上拉加载,默认false;
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
+		page: {
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+			size: 10, // 每页数据的数量
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+		},
+		noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 初始化完毕的回调
+		showLoading: null, // 显示加载中的回调
+		showNoMore: null, // 显示无更多数据的回调
+		hideUpScroll: null, // 隐藏上拉加载的回调
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: null, // 图片路径,默认null (绝对路径或网络图)
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			zIndex: 9990, // fixed定位z-index值
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: null, // 图标路径
+			tip: '~ 暂无相关数据 ~', // 提示
+			btnText: '', // 按钮
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+			zIndex: 99 // fixed定位z-index值
+		},
+		onScroll: false // 是否监听滚动事件
+	})
+}
+
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+	if (!userOption) return defaultOption;
+	for (let key in defaultOption) {
+		if (userOption[key] == null) {
+			let def = defaultOption[key];
+			if (def != null && typeof def === 'object') {
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
+			} else {
+				userOption[key] = def;
+			}
+		} else if (typeof userOption[key] === 'object') {
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+		}
+	}
+	return userOption;
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+	if(!color) return false;
+	let c = color.toLowerCase();
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optDown = me.options.down || {};
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendDownScroll(me.optDown);
+	
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+	if(me.isScrollBody && me.optDown.native){
+		me.optDown.use = false
+	}else{
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+	}
+	
+	me.downHight = 0; // 下拉区域的高度
+
+	// 在页面中加入下拉布局
+	if (me.optDown.use && me.optDown.inited) {
+		// 初始化完毕的回调
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optDown.inited(me);
+		}, 0)
+	}
+}
+
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function(e) {
+	if (!this.optDown.use) return;
+
+	this.startPoint = this.getPoint(e); // 记录起点
+	this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
+	this.startAngle = 0; // 初始角度
+	this.lastPoint = this.startPoint; // 重置上次move的点
+	this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	this.inTouchend = false; // 标记不是touchend
+}
+
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function(e) {
+	if (!this.optDown.use) return;
+	let me = this;
+
+	let scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	let curPoint = me.getPoint(e); // 当前点
+
+	let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.optUp.isBoth))) {
+
+			// 下拉的初始角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
+
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				me.touchendEvent(); // 提前触发touchend
+				return;
+			}
+			
+			me.preventDefault(e); // 阻止默认事件
+
+			let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
+					me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+		}
+	}
+
+	me.lastPoint = curPoint; // 记录本次移动的点
+}
+
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function(e) {
+	if (!this.optDown.use) return;
+	// 如果下拉区域高度已改变,则需重置回来
+	if (this.isMoveDown) {
+		if (this.downHight >= this.optDown.offset) {
+			// 符合触发刷新的条件
+			this.triggerDownScroll();
+		} else {
+			// 不符合的话 则重置
+			this.downHight = 0;
+			this.endDownScrollCall(this);
+		}
+		this.movetype = 0;
+		this.isMoveDown = false;
+	} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				this.triggerUpScroll(true);
+			}
+		}
+	}
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function(e) {
+	if (!e) {
+		return {
+			x: 0,
+			y: 0
+		}
+	}
+	if (e.touches && e.touches[0]) {
+		return {
+			x: e.touches[0].pageX,
+			y: e.touches[0].pageY
+		}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {
+			x: e.changedTouches[0].pageX,
+			y: e.changedTouches[0].pageY
+		}
+	} else {
+		return {
+			x: e.clientX,
+			y: e.clientY
+		}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+MeScroll.prototype.getAngle = function(p1, p2) {
+	let x = Math.abs(p1.x - p2.x);
+	let y = Math.abs(p1.y - p2.y);
+	let z = Math.sqrt(x * x + y * y);
+	let angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function() {
+	if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+		//return true则处于完全自定义状态
+	} else {
+		this.showDownScroll(); // 下拉刷新中...
+		!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	if (this.optDown.native) {
+		uni.startPullDownRefresh(); // 系统自带的下拉刷新
+		this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	} else{
+		this.downHight = this.optDown.offset; // 更新下拉区域高度
+		this.showDownLoadingCall(this.downHight); // 下拉刷新中...
+	}
+}
+
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
+	this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
+	this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
+}
+
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+}
+
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function() {
+	if (this.optDown.native) { // 结束原生下拉刷新
+		this.isDownScrolling = false;
+		this.endDownScrollCall(this);
+		uni.stopPullDownRefresh();
+		return
+	}
+	let me = this;
+	// 结束下拉刷新的方法
+	let endScroll = function() {
+		me.downHight = 0;
+		me.isDownScrolling = false;
+		me.endDownScrollCall(me);
+		if(!me.isScrollBody){
+			me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+			me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+		}
+	}
+	// 结束下拉刷新时的回调
+	let delay = 0;
+	if (me.optDown.beforeEndDownScroll) {
+		delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
+		if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
+	}
+	if (typeof delay === 'number' && delay > 0) {
+		setTimeout(endScroll, delay);
+	} else {
+		endScroll();
+	}
+}
+
+MeScroll.prototype.endDownScrollCall = function() {
+	this.optDown.endDownScroll && this.optDown.endDownScroll(this);
+	this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
+}
+
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optDown.isLock = isLock;
+}
+
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optUp.isLock = isLock;
+}
+
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optUp = me.options.up || {use: false}
+	if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendUpScroll(me.optUp);
+
+	if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
+	me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
+	me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
+
+	// 初始化完毕的回调
+	if (me.optUp.inited) {
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optUp.inited(me);
+		}, 0)
+	}
+}
+
+/*滚动到底部的事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onReachBottom = function() {
+	if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+		if (!this.optUp.isLock && this.optUp.hasNext) {
+			this.triggerUpScroll();
+		}
+	}
+}
+
+/*列表滚动事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onPageScroll = function(e) {
+	if (!this.isScrollBody) return;
+	
+	// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+	this.setScrollTop(e.scrollTop);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+}
+
+/*列表滚动事件*/
+MeScroll.prototype.scroll = function(e, onScroll) {
+	// 更新滚动条的位置
+	this.setScrollTop(e.scrollTop);
+	// 更新滚动内容高度
+	this.setScrollHeight(e.scrollHeight);
+
+	// 向上滑还是向下滑动
+	if (this.preScrollY == null) this.preScrollY = 0;
+	this.isScrollUp = e.scrollTop - this.preScrollY > 0;
+	this.preScrollY = e.scrollTop;
+
+	// 上滑 && 检查并触发上拉
+	this.isScrollUp && this.triggerUpScroll(true);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+
+	// 滑动监听
+	this.optUp.onScroll && onScroll && onScroll()
+}
+
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
+	if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+		// 是否校验在底部; 默认不校验
+		if (isCheck === true) {
+			let canUp = false;
+			// 还有下一页 && 没有锁定 && 不在下拉中
+			if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+				if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+					canUp = true; // 标记可上拉
+				}
+			}
+			if (canUp === false) return;
+		}
+		this.showUpScroll(); // 上拉加载中...
+		this.optUp.page.num++; // 预先加一页,如果失败则减回
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function() {
+	this.isUpScrolling = true; // 标记上拉加载中
+	this.optUp.showLoading && this.optUp.showLoading(this); // 回调
+}
+
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function() {
+	this.optUp.hasNext = false; // 标记无更多数据
+	this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
+}
+
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function() {
+	this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
+}
+
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
+	if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+		if (isShowNoMore) {
+			this.showNoMore(); // isShowNoMore=true,显示无更多数据
+		} else {
+			this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
+		}
+	}
+	this.isUpScrolling = false; // 标记结束上拉加载
+}
+
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+ */
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
+	if (this.optUp && this.optUp.use) {
+		let page = this.optUp.page;
+		this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
+		this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
+		page.num = this.startNum; // 重置为第一页
+		page.time = null; // 重置时间为空
+		if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+			if (isShowLoading == null) {
+				this.removeEmpty(); // 移除空布局
+				this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
+			} else {
+				this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
+			}
+		}
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
+	}
+}
+
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function(num) {
+	this.optUp.page.num = num - 1;
+}
+
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function(size) {
+	this.optUp.page.size = size;
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
+	let hasNext;
+	if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalSize: 列表所有数据总数量(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
+	let hasNext;
+	if (this.optUp.use && totalSize != null) {
+		let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
+		hasNext = loadSize < totalSize; // 是否还有下一页
+	}
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+ */
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
+	let me = this;
+	// 结束下拉刷新
+	if (me.isDownScrolling) {
+		me.isDownEndSuccess = true
+		me.endDownScroll();
+	}
+
+	// 结束上拉加载
+	if (me.optUp.use) {
+		let isShowNoMore; // 是否已无更多数据
+		if (dataSize != null) {
+			let pageNum = me.optUp.page.num; // 当前页码
+			let pageSize = me.optUp.page.size; // 每页长度
+			// 如果是第一页
+			if (pageNum === 1) {
+				if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
+			}
+			if (dataSize < pageSize || hasNext === false) {
+				// 返回的数据不满一页时,则说明已无更多数据
+				me.optUp.hasNext = false;
+				if (dataSize === 0 && pageNum === 1) {
+					// 如果第一页无任何数据且配置了空布局
+					isShowNoMore = false;
+					me.showEmpty();
+				} else {
+					// 总列表数少于配置的数量,则不显示无更多数据
+					let allDataSize = (pageNum - 1) * pageSize + dataSize;
+					if (allDataSize < me.optUp.noMoreSize) {
+						isShowNoMore = false;
+					} else {
+						isShowNoMore = true;
+					}
+					me.removeEmpty(); // 移除空布局
+				}
+			} else {
+				// 还有下一页
+				isShowNoMore = false;
+				me.optUp.hasNext = true;
+				me.removeEmpty(); // 移除空布局
+			}
+		}
+
+		// 隐藏上拉
+		me.endUpScroll(isShowNoMore);
+	}
+}
+
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function(errDistance) {
+	// 结束下拉,回调失败重置回原来的页码和时间
+	if (this.isDownScrolling) {
+		this.isDownEndSuccess = false
+		let page = this.optUp.page;
+		if (page && this.prePageNum) {
+			page.num = this.prePageNum;
+			page.time = this.prePageTime;
+		}
+		this.endDownScroll();
+	}
+	// 结束上拉,回调失败重置回原来的页码
+	if (this.isUpScrolling) {
+		this.optUp.page.num--;
+		this.endUpScroll(false);
+		// 如果是mescroll-body,则需往回滚一定距离
+		if(this.isScrollBody && errDistance !== 0){ // 不处理0
+			if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
+			this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+		}
+	}
+}
+
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+}
+
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+}
+
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function() {
+	if (!this.topBtnShow) {
+		this.topBtnShow = true;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
+	}
+}
+
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function() {
+	if (this.topBtnShow) {
+		this.topBtnShow = false;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
+	}
+}
+
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function() {
+	return this.scrollTop || 0
+}
+
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function(y) {
+	this.scrollTop = y;
+}
+
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function(y, t) {
+	this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+}
+
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
+	this.myScrollTo = myScrollTo
+}
+
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function() {
+	return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+}
+
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+ * */
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
+	let diff = end - star; // 差值
+	if (t === 0 || diff === 0) {
+		callback && callback(end);
+		return;
+	}
+	t = t || 300; // 时长 300ms
+	rate = rate || 30; // 周期 30ms
+	let count = t / rate; // 次数
+	let step = diff / count; // 步长
+	let i = 0; // 计数
+	let timer = setInterval(function() {
+		if (i < count - 1) {
+			star += step;
+			callback && callback(star, timer);
+			i++;
+		} else {
+			callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
+			clearInterval(timer);
+		}
+	}, rate);
+}
+
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function(isReal) {
+	let h = this.clientHeight || 0
+	if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+		h = this.getBodyHeight()
+	}
+	return h
+}
+MeScroll.prototype.setClientHeight = function(h) {
+	this.clientHeight = h;
+}
+
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function() {
+	return this.scrollHeight || 0;
+}
+MeScroll.prototype.setScrollHeight = function(h) {
+	this.scrollHeight = h;
+}
+
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function() {
+	return this.bodyHeight || 0;
+}
+MeScroll.prototype.setBodyHeight = function(h) {
+	this.bodyHeight = h;
+}
+
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function(e) {
+	// 小程序不支持e.preventDefault, 已在wxs中禁止
+	// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+	// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+	if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
+}

+ 480 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,480 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content">
+							<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+							<view class="downwarp-tip">{{downText}}</view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+					<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+					<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+						<view v-show="upLoadType===1">
+							<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+						</view>
+						<!-- 无数据 -->
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+					</view>
+				</view>
+			
+				<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+				<!-- #ifdef H5 -->
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins:[renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入国际化工具类
+	import mescrollI18n from './mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	/**
+	 * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+	 * @event {Function} init 初始化完成的回调 
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调 
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-uni @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+	 */
+	export default {
+		name: 'mescroll-uni',
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			fixed: {
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			disableScroll: Boolean
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		watch: {
+			height() {
+				// 设置容器的高度
+				this.setClientHeight()
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			isDownReset(){
+				return this.downLoadType===3 || this.downLoadType===4
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				if(this.disableScroll) return false
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num){
+				if(typeof num === "string"){
+					if (num.indexOf('px') !== -1) {
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
+							num = num.replace('rpx', '');
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
+							num = num.replace('upx', '');
+						} else { // "10px"
+							return Number(num.replace('px', ''))
+						}
+					}else if (num.indexOf('%') !== -1){
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace("%","")) / 100
+						return this.windowHeight * rate
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (!this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						this.getClientInfo(data=>{
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery().in(this);
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).exec();
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							if(vm.downLoadType===4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll)
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			vm.mescroll.i18n = i18nOption; // 挂载语言包
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let curY = vm.mescroll.getScrollTop()
+								let top = rect.top - mescrollTop
+								top += curY
+								if(!vm.isFixed) top -= vm.numTop
+								vm.scrollTop = curY;
+								vm.$nextTick(function() {
+									vm.scrollTop = top
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, t)
+				}
+			})
+			
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 47 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,47 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollCompMixin = {
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	data() {
+		return {
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	methods:{
+		handlePageScroll(e){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+		}
+	}
+}
+
+export default MescrollCompMixin;

+ 57 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js

@@ -0,0 +1,57 @@
+/**
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+ */
+const MescrollMoreItemMixin = {
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
+	// #ifndef MP-ALIPAY || MP-DINGTALK
+	props:{
+		i: Number, // 每个tab页的专属下标
+		index: { // 当前tab的下标
+			type: Number,
+			default(){
+				return 0
+			}
+		}
+	},
+	// #endif
+	data() {
+		return {
+			downOption:{
+				auto:false // 不自动加载
+			},
+			upOption:{
+				auto:false // 不自动加载
+			},
+			isInit: false // 当前tab是否已初始化
+		}
+	},
+	watch:{
+		// 监听下标的变化
+		index(val){
+			if (this.i === val && !this.isInit) this.mescrollTrigger()
+		}
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			// 自动加载当前tab的数据
+			if(this.i === this.index){
+				this.mescrollTrigger()
+			}
+		},
+		// 主动触发加载
+		mescrollTrigger(){
+			this.isInit = true; // 标记为true
+			if (this.mescroll) {
+				if (this.mescroll.optDown.use) {
+					this.mescroll.triggerDownScroll();
+				} else{
+					this.mescroll.triggerUpScroll();
+				}
+			}
+		}
+	}
+}
+
+export default MescrollMoreItemMixin;

+ 77 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js

@@ -0,0 +1,77 @@
+/**
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollMoreMixin = {
+	data() {
+		return {
+			tabIndex: 0, // 当前tab下标
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	methods:{
+		handlePageScroll(e){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPullDownRefresh();
+		},
+		// 根据下标获取对应子组件的mescroll
+		getMescroll(i){
+			if(!this.mescrollItems) this.mescrollItems = [];
+			if(!this.mescrollItems[i]) {
+				// v-for中的refs
+				let vForItem = this.$refs["mescrollItem"];
+				if(vForItem){
+					this.mescrollItems[i] = vForItem[i]
+				}else{
+					// 普通的refs,不可重复
+					this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+				}
+			}
+			let item = this.mescrollItems[i]
+			return item ? item.mescroll : null
+		},
+		// 切换tab,恢复滚动条位置
+		tabChange(i){
+			let mescroll = this.getMescroll(i);
+			if(mescroll){
+				// 恢复上次滚动条的位置
+				let y = mescroll.getScrollTop()
+				mescroll.scrollTo(y, 0)
+				// 再次恢复上次滚动条的位置, 确保元素已渲染
+				setTimeout(()=>{
+					mescroll.scrollTo(y, 0)
+				},30)
+			}
+		}
+	}
+}
+
+export default MescrollMoreMixin;

+ 109 - 0
uni_modules/mescroll-uni/components/mescroll-uni/wxs/mixins.js

@@ -0,0 +1,109 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+	data() {
+		return {
+			// 传入wxs视图层的数据 (响应式)
+			wxsProp: {
+				optDown:{}, // 下拉刷新的配置
+				scrollTop:0, // 滚动条的距离
+				bodyHeight:0, // body的高度
+				isDownScrolling:false, // 是否正在下拉刷新中
+				isUpScrolling:false, // 是否正在上拉加载中
+				isScrollBody:true, // 是否为mescroll-body滚动
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 标记调用wxs视图层的方法
+			callProp: {
+				callType: '', // 方法名
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+			// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+			wxsBiz: {
+				//注册列表touchstart事件,用于下拉刷新
+				touchstartEvent: e=> {
+					this.mescroll.touchstartEvent(e);
+				},
+				//注册列表touchmove事件,用于下拉刷新
+				touchmoveEvent: e=> {
+					this.mescroll.touchmoveEvent(e);
+				},
+				//注册列表touchend事件,用于下拉刷新
+				touchendEvent: e=> {
+					this.mescroll.touchendEvent(e);
+				},
+				propObserver(){}, // 抹平wxs的写法
+				callObserver(){} // 抹平wxs的写法
+			},
+			// #endif
+			
+			// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+			// #ifndef APP-PLUS || H5
+			renderBiz: {
+				propObserver(){} // 抹平renderjs的写法
+			}
+			// #endif
+		}
+	},
+	methods: {
+		// wxs视图层调用逻辑层的回调
+		wxsCall(msg){
+			if(msg.type === 'setWxsProp'){
+				// 更新wxsProp数据 (值改变才触发更新)
+				this.wxsProp = {
+					optDown: this.mescroll.optDown,
+					scrollTop: this.mescroll.getScrollTop(),
+					bodyHeight: this.mescroll.getBodyHeight(),
+					isDownScrolling: this.mescroll.isDownScrolling,
+					isUpScrolling: this.mescroll.isUpScrolling,
+					isUpBoth: this.mescroll.optUp.isBoth,
+					isScrollBody:this.mescroll.isScrollBody,
+					t: Date.now()
+				}
+			}else if(msg.type === 'setLoadType'){
+				// 设置inOffset,outOffset的状态
+				this.downLoadType = msg.downLoadType
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+				// 重置是否加载成功的状态
+				this.$set(this.mescroll, 'isDownEndSuccess', null)
+			}else if(msg.type === 'triggerDownScroll'){
+				// 主动触发下拉刷新
+				this.mescroll.triggerDownScroll();
+			}else if(msg.type === 'endDownScroll'){
+				// 结束下拉刷新
+				this.mescroll.endDownScroll();
+			}else if(msg.type === 'triggerUpScroll'){
+				// 主动触发上拉加载
+				this.mescroll.triggerUpScroll(true);
+			}
+		}
+	},
+	mounted() {
+		// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+		// 配置主动触发wxs显示加载进度的回调
+		this.mescroll.optDown.afterLoading = ()=>{
+			this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+		}
+		// 配置主动触发wxs隐藏加载进度的回调
+		this.mescroll.optDown.afterEndDownScroll = ()=>{
+			this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+			let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
+			setTimeout(()=>{
+				if(this.downLoadType === 4 || this.downLoadType === 0){
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+				}
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+			}, delay)
+		}
+		// 初始化wxs的数据
+		this.wxsCall({type: 'setWxsProp'})
+		// #endif
+	}
+}
+
+export default WxsMixin;

+ 92 - 0
uni_modules/mescroll-uni/components/mescroll-uni/wxs/renderjs.js

@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+
+// 与wxs的me实例一致
+var me = {}
+
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+	window.$mescrollRenderInit = true
+	
+	
+	window.addEventListener('touchstart', function(e){
+		if (me.disabled()) return;
+		me.startPoint = me.getPoint(e); // 记录起点
+	}, {passive: true})
+	
+	
+	window.addEventListener('touchmove', function(e){
+		if (me.disabled()) return;
+		if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+		
+		var curPoint = me.getPoint(e); // 当前点
+		var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 向下拉
+		if (moveY > 0) {
+			// 可下拉的条件
+			if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+				
+				// 只有touch在mescroll的view上面,才禁止bounce
+				var el = e.target;
+				var isMescrollTouch = false;
+				while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+					var cls = el.classList;
+					if (cls && cls.contains('mescroll-render-touch')) {
+						isMescrollTouch = true
+						break;
+					}
+					el = el.parentNode; // 继续检查其父元素
+				}
+				// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+				if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+			}
+		}
+	}, {passive: false})
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || document.documentElement.scrollTop || document.body.scrollTop || 0
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+}
+
+/* 导出模块 */
+const renderBiz = {
+	data() {
+		return {
+			propObserver: propObserver,
+		}
+	}
+}
+
+export default renderBiz;

+ 269 - 0
uni_modules/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs

@@ -0,0 +1,269 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
+
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+var me = {}
+
+// ------ 自定义下拉刷新动画 start ------
+
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
+			'transform': 'translateY(' + downHight + 'px)',
+			'transition': ''
+		})
+		// 环形进度条
+		var progress = ins.selectComponent('.mescroll-wxs-progress')
+		progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+	})
+}
+
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+	me.downHight = me.optDown.offset
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(' + me.downHight + 'px)',
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+	me.downHight = 0;
+	me.isDownScrolling = false;
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': '',
+			'transform': '',
+			'transition': ''
+		})
+	})
+}
+
+// ------ 自定义下拉刷新动画 end ------
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	if(!wxsProp) return
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.bodyHeight = wxsProp.bodyHeight
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+	me.isScrollBody = wxsProp.isScrollBody
+	me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+}
+
+/**
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+ */
+function callObserver(callProp, oldValue, ins) {
+	if (me.disabled()) return;
+	if(callProp.callType){
+		// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+		if(callProp.callType === 'showLoading'){
+			me.showLoading(ins)
+		}else if(callProp.callType === 'endDownScroll'){
+			me.endDownScroll(ins)
+		}else if(callProp.callType === 'clearTransform'){
+			me.clearTransform(ins)
+		}
+	}
+}
+
+/**
+ * touch事件
+ */
+function touchstartEvent(e, ins) {
+	me.downHight = 0; // 下拉的距离
+	me.startPoint = me.getPoint(e); // 记录起点
+	me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+	me.startAngle = 0; // 初始角度
+	me.lastPoint = me.startPoint; // 重置上次move的点
+	me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	me.inTouchend = false; // 标记不是touchend
+	
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+function touchmoveEvent(e, ins) {
+	var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+	
+	if (me.disabled()) return isPrevent;
+	
+	var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	var curPoint = me.getPoint(e); // 当前点
+	
+	var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+	
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.isUpBoth))) {
+	
+			// 下拉的角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+	
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				touchendEvent(e, ins); // 提前触发touchend
+				return isPrevent;
+			}
+			
+			isPrevent = false // 小程序是return false
+	
+			var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+	
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+	
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+			me.onMoving(ins, rate, me.downHight)
+		}
+	}
+	
+	me.lastPoint = curPoint; // 记录本次移动的点
+	
+	return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+}
+
+function touchendEvent(e, ins) {
+	// 如果下拉区域高度已改变,则需重置回来
+	if (me.isMoveDown) {
+		if (me.downHight >= me.optDown.offset) {
+			// 符合触发刷新的条件
+			me.downHight = me.optDown.offset; // 更新下拉区域高度
+			// me.triggerDownScroll();
+			me.callMethod(ins, {type: 'triggerDownScroll'})
+		} else {
+			// 不符合的话 则重置
+			me.downHight = 0;
+			// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+			me.callMethod(ins, {type: 'endDownScroll'})
+		}
+		me.movetype = 0;
+		me.isMoveDown = false;
+	} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				// me.triggerUpScroll(true);
+				me.callMethod(ins, {type: 'triggerUpScroll'})
+			}
+		}
+	}
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+me.getAngle = function (p1, p2) {
+	var x = Math.abs(p1.x - p2.x);
+	var y = Math.abs(p1.y - p2.y);
+	var z = Math.sqrt(x * x + y * y);
+	var angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+	return me.bodyHeight || 0;
+}
+
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+	if(ins) ins.callMethod('wxsCall', param)
+}
+
+/* 导出模块 */
+module.exports = {
+	propObserver: propObserver,
+	callObserver: callObserver,
+	touchstartEvent: touchstartEvent,
+	touchmoveEvent: touchmoveEvent,
+	touchendEvent: touchendEvent
+}

+ 66 - 0
uni_modules/mescroll-uni/hooks/useMescroll.js

@@ -0,0 +1,66 @@
+// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
+// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
+
+/** 
+ * 初始化mescroll, 相当于vue2的mescroll-mixins.js文件 (mescroll-body 和 mescroll-uni 通用) 
+ * mescroll-body需传入onPageScroll, onReachBottom
+ * mescroll-uni无需传onPageScroll, onReachBottom
+ * 当down.native为true时,需传入onPullDownRefresh
+ */ 
+function useMescroll(onPageScroll, onReachBottom, onPullDownRefresh){
+	// mescroll实例对象
+	let mescroll = null;
+	
+	// mescroll组件初始化的回调,可获取到mescroll对象
+	const mescrollInit = (e)=> {
+		mescroll = e;
+	}
+	
+	// 获取mescroll对象, mescrollInit执行之后会有值, 生命周期created中会有值
+	const getMescroll = ()=>{
+		return mescroll
+	}
+	
+	// 下拉刷新的回调 (mixin默认resetUpScroll)
+	const downCallback = ()=> {
+		if(mescroll.optUp.use){
+			mescroll.resetUpScroll()
+		}else{
+			setTimeout(()=>{
+				mescroll.endSuccess();
+			}, 500)
+		}
+	}
+	
+	// 上拉加载的回调
+	const upCallback = ()=> {
+		// mixin默认延时500自动结束加载
+		setTimeout(()=>{
+			mescroll.endErr();
+		}, 500)
+	}
+	
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	onPullDownRefresh && onPullDownRefresh(() => {
+	  mescroll && mescroll.onPullDownRefresh();
+	})
+	
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onPageScroll && onPageScroll(e=>{
+		mescroll && mescroll.onPageScroll(e);
+	})
+	
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onReachBottom && onReachBottom(()=>{
+		mescroll && mescroll.onReachBottom();
+	}) 
+	
+	return {
+		getMescroll,
+		mescrollInit,
+		downCallback,
+		upCallback
+	}
+}
+
+export default useMescroll

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