瀏覽代碼

小程序代码

lijj 2 年之前
父節點
當前提交
1162921f73
共有 100 個文件被更改,包括 8164 次插入0 次删除
  1. 80 0
      admin/src/main/java/org/springblade/modules/ycwh/controller/wx/WXFamousDishController.java
  2. 1 0
      admin/src/main/resources/application.yml
  3. 3 0
      miniapp/.eslintignore
  4. 27 0
      miniapp/.gitignore
  5. 168 0
      miniapp/App.vue
  6. 34 0
      miniapp/LICENSE
  7. 2 0
      miniapp/README.md
  8. 19 0
      miniapp/api/biz/biz.js
  9. 19 0
      miniapp/api/dish.js
  10. 66 0
      miniapp/api/member.js
  11. 18 0
      miniapp/api/saber.js
  12. 31 0
      miniapp/common/setting.js
  13. 800 0
      miniapp/components/HM-dragSorts/HM-dragSorts.vue
  14. 366 0
      miniapp/components/HM-dragSorts/drag.wxs
  15. 27 0
      miniapp/components/page-theme/page-theme.vue
  16. 46 0
      miniapp/components/qiun-error/qiun-error.vue
  17. 162 0
      miniapp/components/qiun-loading/loading1.vue
  18. 170 0
      miniapp/components/qiun-loading/loading2.vue
  19. 173 0
      miniapp/components/qiun-loading/loading3.vue
  20. 222 0
      miniapp/components/qiun-loading/loading4.vue
  21. 229 0
      miniapp/components/qiun-loading/loading5.vue
  22. 36 0
      miniapp/components/qiun-loading/qiun-loading.vue
  23. 172 0
      miniapp/components/self-audio/index.scss
  24. 225 0
      miniapp/components/self-audio/self-audio.vue
  25. 二進制
      miniapp/components/self-audio/static/loading.png
  26. 二進制
      miniapp/components/self-audio/static/loading2.png
  27. 二進制
      miniapp/components/self-audio/static/pausebtn.png
  28. 二進制
      miniapp/components/self-audio/static/playbtn.png
  29. 187 0
      miniapp/components/uni-datetime-picker/calendar-item.vue
  30. 924 0
      miniapp/components/uni-datetime-picker/calendar.vue
  31. 22 0
      miniapp/components/uni-datetime-picker/i18n/en.json
  32. 8 0
      miniapp/components/uni-datetime-picker/i18n/index.js
  33. 22 0
      miniapp/components/uni-datetime-picker/i18n/zh-Hans.json
  34. 22 0
      miniapp/components/uni-datetime-picker/i18n/zh-Hant.json
  35. 45 0
      miniapp/components/uni-datetime-picker/keypress.js
  36. 946 0
      miniapp/components/uni-datetime-picker/time-picker.vue
  37. 1011 0
      miniapp/components/uni-datetime-picker/uni-datetime-picker.vue
  38. 410 0
      miniapp/components/uni-datetime-picker/util.js
  39. 145 0
      miniapp/http/api.js
  40. 46 0
      miniapp/http/config.js
  41. 32 0
      miniapp/http/install.js
  42. 34 0
      miniapp/main.js
  43. 123 0
      miniapp/manifest.json
  44. 13 0
      miniapp/package.json
  45. 57 0
      miniapp/pages.json
  46. 45 0
      miniapp/pages/components/card.vue
  47. 69 0
      miniapp/pages/components/dropdown.vue
  48. 64 0
      miniapp/pages/components/helper.vue
  49. 56 0
      miniapp/pages/components/icon.vue
  50. 137 0
      miniapp/pages/components/list.vue
  51. 36 0
      miniapp/pages/components/radio.vue
  52. 44 0
      miniapp/pages/components/search.vue
  53. 76 0
      miniapp/pages/components/table.vue
  54. 314 0
      miniapp/pages/components/tooltip.vue
  55. 102 0
      miniapp/pages/index.vue
  56. 32 0
      miniapp/pages/yuecai/dish/detail.vue
  57. 24 0
      miniapp/pages/yuecai/home/index.vue
  58. 22 0
      miniapp/pages/yuecai/mixins/themeMixins.js
  59. 二進制
      miniapp/static/images/blocktime.png
  60. 二進制
      miniapp/static/images/book.png
  61. 二進制
      miniapp/static/images/digit/digit.png
  62. 二進制
      miniapp/static/images/digit/normal/0.png
  63. 二進制
      miniapp/static/images/digit/normal/1.png
  64. 二進制
      miniapp/static/images/digit/normal/2.png
  65. 二進制
      miniapp/static/images/digit/normal/3.png
  66. 二進制
      miniapp/static/images/digit/normal/4.png
  67. 二進制
      miniapp/static/images/digit/normal/5.png
  68. 二進制
      miniapp/static/images/digit/normal/6.png
  69. 二進制
      miniapp/static/images/digit/normal/7.png
  70. 二進制
      miniapp/static/images/digit/normal/8.png
  71. 二進制
      miniapp/static/images/digit/normal/9.png
  72. 二進制
      miniapp/static/images/digit/normal/colon.png
  73. 二進制
      miniapp/static/images/digit/shadow/0.png
  74. 二進制
      miniapp/static/images/digit/shadow/1.png
  75. 二進制
      miniapp/static/images/digit/shadow/2.png
  76. 二進制
      miniapp/static/images/digit/shadow/3.png
  77. 二進制
      miniapp/static/images/digit/shadow/4.png
  78. 二進制
      miniapp/static/images/digit/shadow/5.png
  79. 二進制
      miniapp/static/images/digit/shadow/6.png
  80. 二進制
      miniapp/static/images/digit/shadow/7.png
  81. 二進制
      miniapp/static/images/digit/shadow/8.png
  82. 二進制
      miniapp/static/images/digit/shadow/9.png
  83. 二進制
      miniapp/static/images/digit/shadow/colon.png
  84. 二進制
      miniapp/static/images/dunbar.png
  85. 二進制
      miniapp/static/images/empty.png
  86. 二進制
      miniapp/static/images/face.png
  87. 二進制
      miniapp/static/images/icon/add.png
  88. 二進制
      miniapp/static/images/icon/calendar.png
  89. 二進制
      miniapp/static/images/icon/checked.png
  90. 二進制
      miniapp/static/images/icon/export.png
  91. 二進制
      miniapp/static/images/icon/fav.png
  92. 二進制
      miniapp/static/images/icon/fav2.png
  93. 二進制
      miniapp/static/images/icon/fly.png
  94. 二進制
      miniapp/static/images/icon/help.png
  95. 二進制
      miniapp/static/images/icon/history.png
  96. 二進制
      miniapp/static/images/icon/light.png
  97. 二進制
      miniapp/static/images/icon/music.png
  98. 二進制
      miniapp/static/images/icon/power.png
  99. 二進制
      miniapp/static/images/icon/save.png
  100. 0 0
      miniapp/static/images/icon/search.png

+ 80 - 0
admin/src/main/java/org/springblade/modules/ycwh/controller/wx/WXFamousDishController.java

@@ -0,0 +1,80 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.modules.ycwh.controller.wx;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.AllArgsConstructor;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.excel.util.ExcelUtil;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.constant.BladeConstant;
+import org.springblade.core.tool.utils.DateUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.modules.ycwh.entity.FamousDishEntity;
+import org.springblade.modules.ycwh.excel.FamousDishExcel;
+import org.springblade.modules.ycwh.service.IFamousDishService;
+import org.springblade.modules.ycwh.vo.FamousDishVO;
+import org.springblade.modules.ycwh.wrapper.FamousDishWrapper;
+import org.springframework.web.bind.annotation.*;
+import springfox.documentation.annotations.ApiIgnore;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 名菜
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("yuecai-wx/famous_dish")
+@Api(value = "名菜", tags = "名菜接口")
+public class WXFamousDishController extends BladeController {
+
+	private final IFamousDishService famousDishService;
+
+	/**
+	 * 名菜 详情
+	 */
+	@GetMapping("/detail")
+	@ApiOperationSupport(order = 1)
+	@ApiOperation(value = "详情", notes = "传入famousDish")
+	public R<FamousDishVO> detail(FamousDishEntity famousDish) {
+		FamousDishEntity detail = famousDishService.getOne(Condition.getQueryWrapper(famousDish));
+		return R.data(FamousDishWrapper.build().entityVO(detail));
+	}
+
+	/**
+	 * 名菜 分页
+	 */
+	@GetMapping("/list")
+	@ApiOperationSupport(order = 2)
+	@ApiOperation(value = "分页", notes = "传入famousDish")
+	public R<IPage<FamousDishVO>> list(@ApiIgnore @RequestParam Map<String, Object> famousDish, Query query) {
+		IPage<FamousDishEntity> pages = famousDishService.page(Condition.getPage(query), Condition.getQueryWrapper(famousDish, FamousDishEntity.class));
+		return R.data(FamousDishWrapper.build().pageVO(pages));
+	}
+}

+ 1 - 0
admin/src/main/resources/application.yml

@@ -208,6 +208,7 @@ blade:
     #接口放行
     skip-url:
       - /blade-test/**
+      - /yuecai-wx/**
     #授权认证配置
     auth:
       - method: ALL

+ 3 - 0
miniapp/.eslintignore

@@ -0,0 +1,3 @@
+unpackage
+node_modules
+uview-ui

+ 27 - 0
miniapp/.gitignore

@@ -0,0 +1,27 @@
+.DS_Store
+node_modules/
+dist/
+unpackage/
+.hbuilderx/
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+.env.development
+
+package-lock.json
+yarn.lock

+ 168 - 0
miniapp/App.vue

@@ -0,0 +1,168 @@
+<script>
+	export default {
+		globalData: {
+			theme: '',
+			timer: {}, //倒计时模式
+			watch: {}, //默认模式
+		},
+		methods: {
+			watch(method, name) {
+				Object.defineProperty(this.globalData, name, {
+					configurable: true,
+					enumerable: true,
+					set: function(value) {
+						this._customValue = value
+						method(value)
+					},
+					get: function(value) {
+						return this._customValue
+					}
+				})
+			},
+			getTheme() {
+				const appBaseInfo = wx.getAppBaseInfo();
+				var followSystem = !wx.getStorageSync('selfTheme');
+				var theme = wx.getStorageSync('theme') || 'light';
+				if (followSystem) { //跟随系统
+					theme = appBaseInfo.theme || 'light';
+				}
+				this.globalData.theme = theme;
+				if (theme == 'light') {
+					uni.setNavigationBarColor({
+						frontColor: '#000000',
+						backgroundColor: '#f6f6f6'
+					});
+					uni.setBackgroundTextStyle({
+						textStyle: 'light'
+					})
+					uni.setBackgroundColor({
+						backgroundColor: '#f4f4f4',
+						backgroundColorTop: '#eeeeee',
+						backgroundColorBottom: '#efefef'
+					});
+					uni.setTabBarStyle({
+						color: '#999999',
+						selectedColor: '#000000',
+						backgroundColor: '#ffffff',
+						borderStyle: 'black'
+					});
+					uni.setTabBarItem({
+						index: 0,
+						iconPath: 'static/images/tabbar/todo.png',
+						selectedIconPath: 'static/images/tabbar/todo_selected.png'
+					});
+					uni.setTabBarItem({
+						index: 1,
+						iconPath: 'static/images/tabbar/statistics.png',
+						selectedIconPath: 'static/images/tabbar/statistics_selected.png'
+					});
+					uni.setTabBarItem({
+						index: 2,
+						iconPath: 'static/images/tabbar/record.png',
+						selectedIconPath: 'static/images/tabbar/record_selected.png'
+					});
+					uni.setTabBarItem({
+						index: 3,
+						iconPath: 'static/images/tabbar/my.png',
+						selectedIconPath: 'static/images/tabbar/my_selected.png'
+					});
+				} else {
+					uni.setNavigationBarColor({
+						frontColor: '#ffffff',
+						backgroundColor: '#000000'
+					});
+					uni.setBackgroundTextStyle({
+						textStyle: 'dark'
+					})
+					uni.setBackgroundColor({
+						backgroundColor: '#1f1f1f',
+						backgroundColorTop: '#000000',
+						backgroundColorBottom: '#1f1f1f'
+					});
+					uni.setTabBarStyle({
+						color: '#f0f0f0',
+						selectedColor: '#ffffff',
+						backgroundColor: '#000000',
+						borderStyle: 'white'
+					});
+					uni.setTabBarItem({
+						index: 0,
+						iconPath: 'static/images/tabbar/todo_dark.png',
+						selectedIconPath: 'static/images/tabbar/todo_selected_dark.png'
+					});
+					uni.setTabBarItem({
+						index: 1,
+						iconPath: 'static/images/tabbar/statistics_dark.png',
+						selectedIconPath: 'static/images/tabbar/statistics_selected_dark.png'
+					});
+					uni.setTabBarItem({
+						index: 2,
+						iconPath: 'static/images/tabbar/record_dark.png',
+						selectedIconPath: 'static/images/tabbar/record_selected_dark.png'
+					});
+					uni.setTabBarItem({
+						index: 3,
+						iconPath: 'static/images/tabbar/my_dark.png',
+						selectedIconPath: 'static/images/tabbar/my_selected_dark.png'
+					});
+				}
+				return theme;
+			},
+			checkForUpdate() { //检查是否有新版本,有则更新
+				const updateManager = uni.getUpdateManager() // 小程序版本更新管理器
+				updateManager.onCheckForUpdate(res => { // 检测新版本后的回调
+					if (res.hasUpdate) { // 如果有新版本提醒并进行强制升级
+						console.log('有新版本,强制升级')
+					}
+				})
+				updateManager.onUpdateReady(() => { // 新版本下载完成的回调
+					this.$u.func.logout(true) //登出但是不跳转页面,为了保证新版本用新登录逻辑
+					updateManager.applyUpdate() // 强制更新版本并重启
+				})
+				updateManager.onUpdateFailed(() => { // 新版本下载失败的回调
+					// 新版本下载失败,提示用户删除后通过冷启动重新打开
+					uni.showModal({
+						content: '下载失败,请删除当前小程序后重新搜索打开',
+						showCancel: false,
+						confirmText: '我知道了',
+						success: () => {
+							this.$u.func.logout() //登出
+						}
+					})
+				})
+			},
+		},
+		onLaunch: function() {
+			console.log('App Launch')
+			this.checkForUpdate() //检查是否有新版本,有则更新
+		},
+		onShow: function() {
+			// uni.hideTabBar()//需要手动隐藏原生的tabbar
+			console.log('App Show')
+			if (this.$u.func.checkLogin()) { //已登录
+				//原来是会员,但是现在过期了
+				if (this.userInfo.isVIP && !this.$u.func.checkVIP(this.userInfo.equityEffective)) {
+					console.log('会员过期,强制登出并且跳转到登录页')
+					this.$u.func.logout() //强制登出并且跳转到登录页
+				}
+			}
+		},
+		onThemeChange: function() {
+			this.getTheme();
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*uview全局样式*/
+	@import "uview-ui/index.scss";
+	/*app全局样式*/
+	@import 'static/style/app.scss';
+
+	@import 'static/style/person_colors.scss';
+	/*主题样式*/
+	@import 'static/style/theme.scss';
+</style>

文件差異過大導致無法顯示
+ 34 - 0
miniapp/LICENSE


+ 2 - 0
miniapp/README.md

@@ -0,0 +1,2 @@
+## 说明
+## 邓巴人脉

+ 19 - 0
miniapp/api/biz/biz.js

@@ -0,0 +1,19 @@
+export const bizConst = {
+	basicPrefix: '/yuecai-wx',
+  memberPrefix: '/yuecai-wx/member',
+	pageSize: 10,//默认每页数据条数
+	superVip: '2099-12-31',//终身会员的日期
+}
+
+export const bizColor = {
+	high: '#E56104',
+	medium: '#5C86AD',
+	low: '#7F7F7F',
+}
+
+export const colorMode = {//颜色模式
+	system: 0, //跟随系统
+	common: 1, //普通模式
+	night: 2, //深夜模式
+}
+

+ 19 - 0
miniapp/api/dish.js

@@ -0,0 +1,19 @@
+import http from '@/http/api.js'
+import {bizConst} from './biz/biz.js'
+import {clientId,tenantId} from '@/common/setting'
+
+// 获取token
+const getDishList = () => {
+	return http.request({
+		url: bizConst.basicPrefix + '/famous_dish/list',
+		method: 'GET',
+		header: {
+			'Tenant-Id': tenantId
+		},
+		params: {},
+	})
+} 
+
+export default {
+	getDishList
+}

+ 66 - 0
miniapp/api/member.js

@@ -0,0 +1,66 @@
+import http from '@/http/api.js'
+import {bizConst} from './biz/biz.js'
+import {clientId,tenantId} from '@/common/setting'
+
+// 获取token
+const wxUserLogin = (code) => {
+	return http.request({
+		url: bizConst.memberPrefix + '/login/miniapp',//?clientCode=' + clientId + '&code=' + code,
+		method: 'POST',
+		header: {
+			'Tenant-Id': tenantId
+		},
+		params: {
+			clientCode: clientId,
+			code,
+		},
+	})
+}
+
+//刷新token
+const refreshToken = (refreshToken) => {
+	return http.request({
+		url: bizConst.memberPrefix + '/login/refresh',
+		method: 'POST',
+		params: {
+			refreshToken,
+		},
+	})
+}
+
+//获取随机名称
+const getRandomName = () => {
+	return http.request({
+		url: bizConst.memberPrefix + '/member/info/random/name',
+		method: 'GET',
+	})
+}
+
+//获取随机头像
+const getRandomAvatar = () => {
+	return http.request({
+		url: bizConst.memberPrefix + '/member/info/random/avatar',
+		method: 'GET',
+	})
+}
+
+//保存昵称和头像
+const saveNicknameAndAvatar = (name, avatar) => {
+	return http.request({
+		url: bizConst.memberPrefix + '/member/info/random/save',
+		method: 'POST',
+		params: {
+			name,
+			avatar,
+		},
+	})
+}
+ 
+
+export default {
+	wxUserLogin,
+	refreshToken,
+	getRandomName,
+	getRandomAvatar,
+	saveNicknameAndAvatar
+}

+ 18 - 0
miniapp/api/saber.js

@@ -0,0 +1,18 @@
+import http from '@/http/api.js'
+import {bizConst} from './biz/biz.js'
+
+const prefixUrl = bizConst.basicPrefix
+
+const getDictionary = (code) => {//获取业务字典
+	return http.request({
+		url: prefixUrl + '/dict-biz/dictionary',
+		method: 'GET',
+		params: {
+			code,
+		}
+	})
+}
+
+export default {
+	getDictionary,
+}

+ 31 - 0
miniapp/common/setting.js

@@ -0,0 +1,31 @@
+/**
+ * 全局变量配置
+ */
+module.exports = {
+	// 应用名
+	name: 'Yuecai',
+	// 应用logo,支持本地路径和网络路径
+	logo: '/static/images/blocktime.png',
+	// 版本号
+	version: '0.1.0',
+	// 开发环境接口Url
+	devUrl: 'http://localhost:8210',//因为是微服务架构,所以开发环境也全部调服务器,需要调本地服务的配置到biz.js里
+	// 线上环境接口Url
+	prodUrl: 'http://ycwh.pj.farwalker.cn/api',
+	// 后端数据的接收方式application/json;charset=UTF-8或者application/x-www-form-urlencoded;charset=UTF-8
+	contentType: 'application/json;charset=UTF-8',
+	// 后端返回状态码
+	codeName: 'code',
+	// 操作正常code
+	successCode: 200,
+	// 登录失效code
+	invalidCode: 401,
+	// 客户端ID
+	clientId: 'yuecai',
+	// 客户端密钥
+	clientSecret: 'yuecai_secret',
+	// 是否mock数据
+	isMockData: false,
+	// 租户ID
+	tenantId: '940067',
+}

文件差異過大導致無法顯示
+ 800 - 0
miniapp/components/HM-dragSorts/HM-dragSorts.vue


+ 366 - 0
miniapp/components/HM-dragSorts/drag.wxs

@@ -0,0 +1,366 @@
+
+
+var scrollTop = {}; //滚动条位置
+// 排序列表
+var sortList={};
+var isMove = false; //是否可拖动 长按事件控制切换这个状态
+var touchTimer = false; //长按事件定时器
+// 当页面有多个当前组件时,guid用来识别当前的列表的。因为一个页面内多个组件的wxs作用域相同。
+
+
+function setScrollTop(tmpGuid) {
+	if (typeof scrollTop[tmpGuid] == "undefined") {
+		scrollTop[tmpGuid] = 0;
+	}
+}
+
+function scroll(event, instance) {
+	var dataView = instance.selectComponent('#dataView');
+	var viewData = dataView.getDataset();
+	setScrollTop(viewData.guid)
+	scrollTop[viewData.guid] = event.detail.scrollTop;
+}
+
+function initVar(state, instance) {
+	var dataView = instance.selectComponent('#dataView');
+	var viewData = dataView.getDataset();
+	// 读取配置项
+	// 获取scroll-view id
+	config = All_Config[viewData.guid];
+	
+	setScrollTop(config.guid);
+	state.initscrollTop = scrollTop[config.guid];
+	
+}
+function getRowSort(findId,instance){
+	for (var i = 0; i < sortList[config.guid].length; i++) {
+		if(sortList[config.guid][i].id==findId){
+			currentRowView = sortList[config.guid][i].rowView;
+			return sortList[config.guid][i].lastSort;
+		}
+	}
+}
+var shadowRowBoxView=null;
+var shadowRowView = null;
+var currentRowView=null;
+var rowSort=0;
+var sorting = false;
+function touchstart(event, instance) {
+	if(sorting){
+		return ;
+	}
+	sorting = true;
+	// 兼容setTimeout
+	if(typeof setTimeout ==="undefined" && typeof instance.setTimeout !== 'undefined'){
+		setTimeout = instance.setTimeout;
+		clearTimeout = instance.clearTimeout;
+	}
+	
+	isMove = false;
+	var rowData = event.instance.getDataset();
+	var state = instance.getState();
+	if (event.touches.length == 1) {
+		state.point = event.touches[0];
+
+		state.islongTap = true;
+		state.rowData = rowData;
+		//读取数据
+		initVar(state, instance);
+	}
+	var rowStyle = event.instance.getComputedStyle(['height']);
+	config.rowHeight = parseFloat(rowStyle.height); //获取行高
+	// 计算shadowRow.style.top
+	
+	rowSort = getRowSort(rowData.id,instance);
+	var shadowRowTop = rowSort * config.rowHeight;
+	shadowRowTop = shadowRowTop - scrollTop[config.guid];
+	// 加载shadowRow数据
+	instance.callMethod("loadShadowRow", {
+		rowSort: rowSort
+	});
+	state.shadowRowTop = shadowRowTop;
+	// 设置shadowRow初始位置
+	shadowRowBoxView = instance.selectComponent('#shadowRowBox');
+	shadowRowBoxView.setStyle({
+		'top': shadowRowTop + 'px'
+	})
+	shadowRowView = instance.selectComponent('#shadowRow')
+	//长按事件
+	if (config.longTouch) {
+		touchTimer && clearTimeout(touchTimer);
+		touchTimer = setTimeout(function() {
+			longpress(event, instance);
+		}, config.longTouchTime)
+	}
+}
+
+function longpress(event, instance) {
+	if (config.longTouch) {
+		isMove = true;
+		moveRow(instance, 0)
+	}
+}
+
+function touchmove(event, instance) {
+	var state = instance.getState();
+	var rowData = event.instance.getDataset();
+	var movePoint = event.touches[0];
+	var initPoint = state.point;
+	var moveY = movePoint.pageY - initPoint.pageY;
+	if (config.longTouch) {
+		if (Math.abs(moveY) > 10) {
+			clearTimeout(touchTimer);
+		}
+		if (!isMove) {
+			return;
+		}
+	} 
+	moveRow(instance, moveY);
+	//阻止滚动页面
+	if (event.preventDefault) {
+		event.preventDefault();
+	}
+	return false;
+}
+function touchend(event, instance) {
+	if (config.longTouch) {
+		clearTimeout(touchTimer);
+	}
+	if (lastCommand != "stop") {
+		lastCommand = "stop";
+		config.autoScroll && instance.callMethod("pageScroll", {
+			'guid': config.guid,
+			'command': "stop"
+		});
+	}
+	var state = instance.getState();
+	// 把隐藏的行重新显示
+	resetRowStyle(instance,state.rowData.id)
+	// 隐藏ShadowRow
+	resetShadowRowStyle(instance,state.offset)
+	if (typeof state.offset !== "undefined" && rowSort != state.offset && state.offset != null) {
+		var sortArray=[];
+		for (var i = 0; i < sortList[config.guid].length; i++) {
+			sortList[config.guid][i].lastSort = sortList[config.guid][i].newSort;
+			sortArray.push(sortList[config.guid][i].newSort);
+			sortList[config.guid][i].rowView.removeClass('ani');
+		}
+		instance.callMethod("sort", {
+			index: rowSort,
+			offset: state.offset,
+			sortArray:sortArray
+		});
+	} else {
+		sorting = false;
+		triggerFeedbackGenerator(instance); //震动反馈
+		return false;
+	}
+	state.offset = null;
+	oldOffset = null;
+	sorting = false;
+	triggerFeedbackGenerator(instance); //震动反馈
+	return false;
+}
+// 重置列表行
+function resetRowStyle(instance,id) {
+	currentRowView.removeClass('hide');
+}
+// 重置拖拽行
+function resetShadowRowStyle(instance,offset) {
+	shadowRowBoxView.removeClass('show');
+	shadowRowBoxView.addClass('hide');
+	shadowRowBoxView.setStyle({});	
+}
+var lastCommand = '';
+
+// move Row
+function moveRow(instance, moveY) {
+	var state = instance.getState();
+
+	// 显示shadowRowBox
+	shadowRowBoxView.removeClass('hide');
+	shadowRowBoxView.hasClass('show') || shadowRowBoxView.addClass('show');
+	// 移动shadowRowBox里面的shadowRow
+	shadowRowView.setStyle({
+		'transform': 'translate3d(0,' + moveY + 'px,10px)',
+		'-webkit-transform': 'translate3d(0,' + moveY + 'px,10px)'
+	});
+	// 隐藏列表对应行
+	currentRowView.hasClass('hide') || currentRowView.addClass('hide');
+	currentRowView.removeClass('ani')
+	var listClientY = state.shadowRowTop + moveY + config.rowHeight/2;
+	var tmpscrollListTop = scrollTop[config.guid];
+	
+	// 拖拽至边缘滚动视图 距离顶部距离1.5行高触发上滚动 下滚动同理
+	var callMethodData = {
+		guid: config.guid,
+		command: listClientY < config.ListHeight * 0.2 ? "up" : listClientY > config.ListHeight - (config.ListHeight * 0.2) ? "down" :
+			"stop",
+		scrollTop: tmpscrollListTop,
+	}
+	// 把滚动指令发给逻辑层
+	if (lastCommand != callMethodData.command) {
+		lastCommand = callMethodData.command;
+		config.autoScroll && instance.callMethod("pageScroll", callMethodData);
+	}
+
+	var moveOffset = moveY + scrollTop[config.guid] - state.initscrollTop;
+	var offset = calcOffset(rowSort, moveOffset);
+	if (offset <= 2 || offset >= config.listLength - 2) {
+		callMethodData.command = 'stop';
+	}
+	// 为减少卡顿,微信小程序端,在滚动视图期间不进行列表位置交换
+	if (config.autoScroll && (!config.isAppH5) && callMethodData.command != 'stop') {
+		return;
+	}
+	oldOffset = oldOffset == null ? rowSort : oldOffset;
+	if (offset < 0 || offset >= config.listLength) {
+		return;
+	}
+	if (offset == oldOffset) {
+		return;
+	}
+
+	oldOffset = offset;
+	state.offset = offset;
+	//触发change事件 并交换列表位置
+	instance.callMethod("change", {
+		index: rowSort,
+		moveTo: state.offset
+	});
+	for (var i = 0; i < sortList[config.guid].length; i++) {
+		var sort = sortList[config.guid][i].lastSort;
+		var newSort = sortList[config.guid][i].newSort;
+		if ((sort >= offset && sort <= rowSort) || (sort <= offset && sort >= rowSort)) {
+			if(sort == rowSort) {
+				newSort = offset;
+			}else{
+				newSort = sort < rowSort ? sort+1 : sort-1;
+			}
+		}else{
+			newSort = sort;
+		}
+		if(sortList[config.guid][i].newSort == newSort){
+			continue;
+		}
+		sortList[config.guid][i].newSort = newSort;
+		var translateY = (sortList[config.guid][i].newSort-sortList[config.guid][i].sort) * 100;
+		sortList[config.guid][i].rowView.hasClass('ani') || sortList[config.guid][i].rowView.addClass('ani');
+		sortList[config.guid][i].rowView.setStyle({
+			'transform': 'translate3d(0,' + translateY + '%,0)',
+			'-webkit-transform': 'translate3d(0,' + translateY + '%,0)'
+		});
+	}
+	triggerFeedbackGenerator(instance); //震动反馈
+}
+//计算偏移index
+var oldOffset = null;
+function calcOffset(initSort, moveY) {
+	var offset = initSort + parseInt(moveY / config.rowHeight); //偏移 行高的倍数
+	var rest = moveY % config.rowHeight;
+	if (rest > 0) {
+		offset = offset + (rest / config.rowHeight >= 0.6 ? 1 : 0);
+		if (offset < oldOffset) {
+			offset = rest / config.rowHeight <= 0.4 ? offset : oldOffset;
+		}
+	} else 
+	{
+		offset = offset + (rest / config.rowHeight <= -0.6 ? -1 : 0);
+		if (offset > oldOffset) {
+			offset = rest / config.rowHeight >= -0.4 ? offset : oldOffset;
+		}
+	}
+	return offset;
+}
+
+//触感反馈
+//wxs 不支持条件编译,所以用此方法判断
+var isiOSAPP = typeof plus != "undefined" && plus.os.name == 'iOS';
+var UISelectionFeedbackGenerator;
+var impact
+
+if (isiOSAPP) {
+	UISelectionFeedbackGenerator = plus.ios.importClass("UISelectionFeedbackGenerator");
+	impact = new UISelectionFeedbackGenerator();
+	impact.init();
+}
+function triggerFeedbackGenerator(instance) {
+	if (!config.feedbackGenerator) {
+		//关闭触感反馈
+		return;
+	}
+	if (isiOSAPP) {
+		//异步,避免与点击事件冲突
+		setTimeout(function(){
+			impact.selectionChanged();
+		},0)
+	} else {
+		if (typeof plus != "undefined") {
+			plus.device.vibrate(12)
+		} else {
+			instance.callMethod("vibrate");
+		}
+	}
+}
+var All_Config={};
+var config = {};
+function receiveData(e,state, instance){
+	var data = JSON.parse(e);
+	var tmp_config = {};
+	var hasGuid = false;
+	var sortArray=[];
+	for(var i=0;i<data.length;i++){
+		var arr = data[i];
+		switch (arr[0]){
+			case "sortArray":
+				sortArray = arr[1];
+				break;
+			default:
+				if(arr[0]=='guid'){
+					hasGuid = true;
+				}
+				tmp_config[arr[0]] = arr[1];
+				break;
+		}
+	}
+	if(!hasGuid){
+		return;
+	}
+	var isUpdateList = false;
+	if(typeof All_Config[tmp_config.guid] == "undefined" ||typeof All_Config[tmp_config.guid].lastInitTime == "undefined" ||  All_Config[tmp_config.guid].lastInitTime!=tmp_config.lastInitTime){
+		isUpdateList = true;
+	}
+	All_Config[tmp_config.guid] = tmp_config;
+	if(isUpdateList){
+		updateSortList(tmp_config.guid, instance,sortArray);
+	}
+}
+// 更新guid对应的排序列表
+function updateSortList(guid, instance,sortArray) {
+	
+	sortList[guid]=[];
+	var pageSortList = instance.selectAllComponents('.hm-row');
+	for (var i = 0; i < pageSortList.length; i++){
+		var tmp_row = {id:pageSortList[i].getDataset().id,sort:i,lastSort:parseInt(pageSortList[i].getDataset().sort),newSort:i,rowView:pageSortList[i]};
+		if(sortArray.length>0){
+			tmp_row.lastSort = sortArray[i];
+			tmp_row.newSort = tmp_row.lastSort;
+		}
+		sortList[guid].push(tmp_row);
+		var translateY = (tmp_row.lastSort-tmp_row.sort) * 100;
+		tmp_row.rowView.setStyle({
+			'transform': 'translate3d(0,' + translateY + '%,0)',
+			'-webkit-transform': 'translate3d(0,' + translateY + '%,0)'
+		});
+	}
+
+}
+// 输出
+module.exports = {
+	receiveData:receiveData,
+	scroll: scroll,
+	longpress: longpress,
+	touchstart: touchstart,
+	touchmove: touchmove,
+	touchend: touchend
+}

+ 27 - 0
miniapp/components/page-theme/page-theme.vue

@@ -0,0 +1,27 @@
+<template>
+	<view class="page-theme" :class="'page-theme-'+theme"
+		:style="isflex?'height:100%;display:flex;flex-direction:column;':''">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: ['theme','isflex'],
+		data() {
+			return {};
+		},
+		watch: {},
+		mounted() {},
+		onReady() {},
+		methods: {}
+	}
+</script>
+
+<style scoped lang="scss">
+	.page-theme {
+		width: 100%;
+		min-height: 100%;
+		overflow-x: hidden;
+	}
+</style>

文件差異過大導致無法顯示
+ 46 - 0
miniapp/components/qiun-error/qiun-error.vue


+ 162 - 0
miniapp/components/qiun-loading/loading1.vue

@@ -0,0 +1,162 @@
+<template>
+	 <view class="container loading1">
+		<view class="shape shape1"></view>
+		<view class="shape shape2"></view>
+		<view class="shape shape3"></view>
+		<view class="shape shape4"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'loading1',
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+
+<style scoped="true">
+.container {
+  width: 30px;
+  height: 30px;
+  position: relative;
+}
+.container.loading1 {
+  -webkit-transform: rotate(45deg);
+          transform: rotate(45deg);
+}
+
+.container .shape {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 1px;
+}
+.container .shape.shape1 {
+  left: 0;
+  background-color: #1890FF;
+}
+.container .shape.shape2 {
+  right: 0;
+  background-color: #91CB74;
+}
+.container .shape.shape3 {
+  bottom: 0;
+  background-color: #FAC858;
+}
+.container .shape.shape4 {
+  bottom: 0;
+  right: 0;
+  background-color: #EE6666;
+}
+
+.loading1 .shape1 {
+  -webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
+          animation: animation1shape1 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation1shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(16px, 16px);
+            transform: translate(16px, 16px);
+  }
+}
+
+@keyframes animation1shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(16px, 16px);
+            transform: translate(16px, 16px);
+  }
+}
+.loading1 .shape2 {
+  -webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
+          animation: animation1shape2 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation1shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-16px, 16px);
+            transform: translate(-16px, 16px);
+  }
+}
+
+@keyframes animation1shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-16px, 16px);
+            transform: translate(-16px, 16px);
+  }
+}
+.loading1 .shape3 {
+  -webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
+          animation: animation1shape3 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation1shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(16px, -16px);
+            transform: translate(16px, -16px);
+  }
+}
+
+@keyframes animation1shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(16px, -16px);
+            transform: translate(16px, -16px);
+  }
+}
+.loading1 .shape4 {
+  -webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
+          animation: animation1shape4 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation1shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-16px, -16px);
+            transform: translate(-16px, -16px);
+  }
+}
+
+@keyframes animation1shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-16px, -16px);
+            transform: translate(-16px, -16px);
+  }
+}
+
+
+</style>

+ 170 - 0
miniapp/components/qiun-loading/loading2.vue

@@ -0,0 +1,170 @@
+<template>
+	 <view class="container loading2">
+		<view class="shape shape1"></view>
+		<view class="shape shape2"></view>
+		<view class="shape shape3"></view>
+		<view class="shape shape4"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'loading2',
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+
+<style scoped="true">
+.container {
+  width: 30px;
+  height: 30px;
+  position: relative;
+}
+
+.container.loading2 {
+  -webkit-transform: rotate(10deg);
+          transform: rotate(10deg);
+}
+.container.loading2 .shape {
+  border-radius: 5px;
+}
+.container.loading2{
+  -webkit-animation: rotation 1s infinite;
+          animation: rotation 1s infinite;
+}
+
+.container .shape {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 1px;
+}
+.container .shape.shape1 {
+  left: 0;
+  background-color: #1890FF;
+}
+.container .shape.shape2 {
+  right: 0;
+  background-color: #91CB74;
+}
+.container .shape.shape3 {
+  bottom: 0;
+  background-color: #FAC858;
+}
+.container .shape.shape4 {
+  bottom: 0;
+  right: 0;
+  background-color: #EE6666;
+}
+
+
+.loading2 .shape1 {
+  -webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
+          animation: animation2shape1 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation2shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(20px, 20px);
+            transform: translate(20px, 20px);
+  }
+}
+
+@keyframes animation2shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(20px, 20px);
+            transform: translate(20px, 20px);
+  }
+}
+.loading2 .shape2 {
+  -webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
+          animation: animation2shape2 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation2shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-20px, 20px);
+            transform: translate(-20px, 20px);
+  }
+}
+
+@keyframes animation2shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-20px, 20px);
+            transform: translate(-20px, 20px);
+  }
+}
+.loading2 .shape3 {
+  -webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
+          animation: animation2shape3 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation2shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(20px, -20px);
+            transform: translate(20px, -20px);
+  }
+}
+
+@keyframes animation2shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(20px, -20px);
+            transform: translate(20px, -20px);
+  }
+}
+.loading2 .shape4 {
+  -webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
+          animation: animation2shape4 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation2shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-20px, -20px);
+            transform: translate(-20px, -20px);
+  }
+}
+
+@keyframes animation2shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-20px, -20px);
+            transform: translate(-20px, -20px);
+  }
+}
+
+</style>

+ 173 - 0
miniapp/components/qiun-loading/loading3.vue

@@ -0,0 +1,173 @@
+<template>
+	 <view class="container loading3">
+		<view class="shape shape1"></view>
+		<view class="shape shape2"></view>
+		<view class="shape shape3"></view>
+		<view class="shape shape4"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'loading3',
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+
+<style scoped="true">
+.container {
+  width: 30px;
+  height: 30px;
+  position: relative;
+}
+
+ .container.loading3 {
+  -webkit-animation: rotation 1s infinite;
+          animation: rotation 1s infinite;
+}
+.container.loading3 .shape1 {
+  border-top-left-radius: 10px;
+}
+.container.loading3 .shape2 {
+  border-top-right-radius: 10px;
+}
+.container.loading3 .shape3 {
+  border-bottom-left-radius: 10px;
+}
+.container.loading3 .shape4 {
+  border-bottom-right-radius: 10px;
+}
+
+.container .shape {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 1px;
+}
+.container .shape.shape1 {
+  left: 0;
+  background-color: #1890FF;
+}
+.container .shape.shape2 {
+  right: 0;
+  background-color: #91CB74;
+}
+.container .shape.shape3 {
+  bottom: 0;
+  background-color: #FAC858;
+}
+.container .shape.shape4 {
+  bottom: 0;
+  right: 0;
+  background-color: #EE6666;
+}
+
+.loading3 .shape1 {
+  -webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
+          animation: animation3shape1 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation3shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(5px, 5px);
+            transform: translate(5px, 5px);
+  }
+}
+
+@keyframes animation3shape1 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(5px, 5px);
+            transform: translate(5px, 5px);
+  }
+}
+.loading3 .shape2 {
+  -webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
+          animation: animation3shape2 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation3shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-5px, 5px);
+            transform: translate(-5px, 5px);
+  }
+}
+
+@keyframes animation3shape2 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-5px, 5px);
+            transform: translate(-5px, 5px);
+  }
+}
+.loading3 .shape3 {
+  -webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
+          animation: animation3shape3 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation3shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(5px, -5px);
+            transform: translate(5px, -5px);
+  }
+}
+
+@keyframes animation3shape3 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(5px, -5px);
+            transform: translate(5px, -5px);
+  }
+}
+.loading3 .shape4 {
+  -webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
+          animation: animation3shape4 0.5s ease 0s infinite alternate;
+}
+
+@-webkit-keyframes animation3shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-5px, -5px);
+            transform: translate(-5px, -5px);
+  }
+}
+
+@keyframes animation3shape4 {
+  from {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  to {
+    -webkit-transform: translate(-5px, -5px);
+            transform: translate(-5px, -5px);
+  }
+}
+</style>

+ 222 - 0
miniapp/components/qiun-loading/loading4.vue

@@ -0,0 +1,222 @@
+<template>
+	 <view class="container loading5">
+		<view class="shape shape1"></view>
+		<view class="shape shape2"></view>
+		<view class="shape shape3"></view>
+		<view class="shape shape4"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'loading5',
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+
+<style scoped="true">
+.container {
+  width: 30px;
+  height: 30px;
+  position: relative;
+}
+
+.container.loading5 .shape {
+  width: 15px;
+  height: 15px;
+}
+
+.container .shape {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 1px;
+}
+.container .shape.shape1 {
+  left: 0;
+  background-color: #1890FF;
+}
+.container .shape.shape2 {
+  right: 0;
+  background-color: #91CB74;
+}
+.container .shape.shape3 {
+  bottom: 0;
+  background-color: #FAC858;
+}
+.container .shape.shape4 {
+  bottom: 0;
+  right: 0;
+  background-color: #EE6666;
+}
+
+.loading5 .shape1 {
+  animation: animation5shape1 2s ease 0s infinite reverse;
+}
+
+@-webkit-keyframes animation5shape1 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, 15px);
+            transform: translate(0, 15px);
+  }
+  50% {
+    -webkit-transform: translate(15px, 15px);
+            transform: translate(15px, 15px);
+  }
+  75% {
+    -webkit-transform: translate(15px, 0);
+            transform: translate(15px, 0);
+  }
+}
+
+@keyframes animation5shape1 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, 15px);
+            transform: translate(0, 15px);
+  }
+  50% {
+    -webkit-transform: translate(15px, 15px);
+            transform: translate(15px, 15px);
+  }
+  75% {
+    -webkit-transform: translate(15px, 0);
+            transform: translate(15px, 0);
+  }
+}
+.loading5 .shape2 {
+  animation: animation5shape2 2s ease 0s infinite reverse;
+}
+
+@-webkit-keyframes animation5shape2 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(-15px, 0);
+            transform: translate(-15px, 0);
+  }
+  50% {
+    -webkit-transform: translate(-15px, 15px);
+            transform: translate(-15px, 15px);
+  }
+  75% {
+    -webkit-transform: translate(0, 15px);
+            transform: translate(0, 15px);
+  }
+}
+
+@keyframes animation5shape2 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(-15px, 0);
+            transform: translate(-15px, 0);
+  }
+  50% {
+    -webkit-transform: translate(-15px, 15px);
+            transform: translate(-15px, 15px);
+  }
+  75% {
+    -webkit-transform: translate(0, 15px);
+            transform: translate(0, 15px);
+  }
+}
+.loading5 .shape3 {
+  animation: animation5shape3 2s ease 0s infinite reverse;
+}
+
+@-webkit-keyframes animation5shape3 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(15px, 0);
+            transform: translate(15px, 0);
+  }
+  50% {
+    -webkit-transform: translate(15px, -15px);
+            transform: translate(15px, -15px);
+  }
+  75% {
+    -webkit-transform: translate(0, -15px);
+            transform: translate(0, -15px);
+  }
+}
+
+@keyframes animation5shape3 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(15px, 0);
+            transform: translate(15px, 0);
+  }
+  50% {
+    -webkit-transform: translate(15px, -15px);
+            transform: translate(15px, -15px);
+  }
+  75% {
+    -webkit-transform: translate(0, -15px);
+            transform: translate(0, -15px);
+  }
+}
+.loading5 .shape4 {
+  animation: animation5shape4 2s ease 0s infinite reverse;
+}
+
+@-webkit-keyframes animation5shape4 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, -15px);
+            transform: translate(0, -15px);
+  }
+  50% {
+    -webkit-transform: translate(-15px, -15px);
+            transform: translate(-15px, -15px);
+  }
+  75% {
+    -webkit-transform: translate(-15px, 0);
+            transform: translate(-15px, 0);
+  }
+}
+
+@keyframes animation5shape4 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, -15px);
+            transform: translate(0, -15px);
+  }
+  50% {
+    -webkit-transform: translate(-15px, -15px);
+            transform: translate(-15px, -15px);
+  }
+  75% {
+    -webkit-transform: translate(-15px, 0);
+            transform: translate(-15px, 0);
+  }
+}
+
+</style>

+ 229 - 0
miniapp/components/qiun-loading/loading5.vue

@@ -0,0 +1,229 @@
+<template>
+	 <view class="container loading6">
+		<view class="shape shape1"></view>
+		<view class="shape shape2"></view>
+		<view class="shape shape3"></view>
+		<view class="shape shape4"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'loading6',
+		data() {
+			return {
+				
+			};
+		}
+	}
+</script>
+<style scoped="true">
+.container {
+  width: 30px;
+  height: 30px;
+  position: relative;
+}
+
+.container.loading6 {
+  -webkit-animation: rotation 1s infinite;
+          animation: rotation 1s infinite;
+}
+.container.loading6 .shape {
+  width: 12px;
+  height: 12px;
+  border-radius: 2px;
+}
+.container .shape {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  border-radius: 1px;
+}
+.container .shape.shape1 {
+  left: 0;
+  background-color: #1890FF;
+}
+.container .shape.shape2 {
+  right: 0;
+  background-color: #91CB74;
+}
+.container .shape.shape3 {
+  bottom: 0;
+  background-color: #FAC858;
+}
+.container .shape.shape4 {
+  bottom: 0;
+  right: 0;
+  background-color: #EE6666;
+}
+
+
+.loading6 .shape1 {
+  -webkit-animation: animation6shape1 2s linear 0s infinite normal;
+          animation: animation6shape1 2s linear 0s infinite normal;
+}
+
+@-webkit-keyframes animation6shape1 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, 18px);
+            transform: translate(0, 18px);
+  }
+  50% {
+    -webkit-transform: translate(18px, 18px);
+            transform: translate(18px, 18px);
+  }
+  75% {
+    -webkit-transform: translate(18px, 0);
+            transform: translate(18px, 0);
+  }
+}
+
+@keyframes animation6shape1 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, 18px);
+            transform: translate(0, 18px);
+  }
+  50% {
+    -webkit-transform: translate(18px, 18px);
+            transform: translate(18px, 18px);
+  }
+  75% {
+    -webkit-transform: translate(18px, 0);
+            transform: translate(18px, 0);
+  }
+}
+.loading6 .shape2 {
+  -webkit-animation: animation6shape2 2s linear 0s infinite normal;
+          animation: animation6shape2 2s linear 0s infinite normal;
+}
+
+@-webkit-keyframes animation6shape2 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(-18px, 0);
+            transform: translate(-18px, 0);
+  }
+  50% {
+    -webkit-transform: translate(-18px, 18px);
+            transform: translate(-18px, 18px);
+  }
+  75% {
+    -webkit-transform: translate(0, 18px);
+            transform: translate(0, 18px);
+  }
+}
+
+@keyframes animation6shape2 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(-18px, 0);
+            transform: translate(-18px, 0);
+  }
+  50% {
+    -webkit-transform: translate(-18px, 18px);
+            transform: translate(-18px, 18px);
+  }
+  75% {
+    -webkit-transform: translate(0, 18px);
+            transform: translate(0, 18px);
+  }
+}
+.loading6 .shape3 {
+  -webkit-animation: animation6shape3 2s linear 0s infinite normal;
+          animation: animation6shape3 2s linear 0s infinite normal;
+}
+
+@-webkit-keyframes animation6shape3 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(18px, 0);
+            transform: translate(18px, 0);
+  }
+  50% {
+    -webkit-transform: translate(18px, -18px);
+            transform: translate(18px, -18px);
+  }
+  75% {
+    -webkit-transform: translate(0, -18px);
+            transform: translate(0, -18px);
+  }
+}
+
+@keyframes animation6shape3 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(18px, 0);
+            transform: translate(18px, 0);
+  }
+  50% {
+    -webkit-transform: translate(18px, -18px);
+            transform: translate(18px, -18px);
+  }
+  75% {
+    -webkit-transform: translate(0, -18px);
+            transform: translate(0, -18px);
+  }
+}
+.loading6 .shape4 {
+  -webkit-animation: animation6shape4 2s linear 0s infinite normal;
+          animation: animation6shape4 2s linear 0s infinite normal;
+}
+
+@-webkit-keyframes animation6shape4 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, -18px);
+            transform: translate(0, -18px);
+  }
+  50% {
+    -webkit-transform: translate(-18px, -18px);
+            transform: translate(-18px, -18px);
+  }
+  75% {
+    -webkit-transform: translate(-18px, 0);
+            transform: translate(-18px, 0);
+  }
+}
+
+@keyframes animation6shape4 {
+  0% {
+    -webkit-transform: translate(0, 0);
+            transform: translate(0, 0);
+  }
+  25% {
+    -webkit-transform: translate(0, -18px);
+            transform: translate(0, -18px);
+  }
+  50% {
+    -webkit-transform: translate(-18px, -18px);
+            transform: translate(-18px, -18px);
+  }
+  75% {
+    -webkit-transform: translate(-18px, 0);
+            transform: translate(-18px, 0);
+  }
+}
+</style>

+ 36 - 0
miniapp/components/qiun-loading/qiun-loading.vue

@@ -0,0 +1,36 @@
+<template>
+	<view>
+	 <Loading1 v-if="loadingType==1"/>
+	 <Loading2 v-if="loadingType==2"/>
+	 <Loading3 v-if="loadingType==3"/>
+	 <Loading4 v-if="loadingType==4"/>
+	 <Loading5 v-if="loadingType==5"/>
+	</view>
+</template>
+
+<script>
+	import Loading1 from "./loading1.vue";
+	import Loading2 from "./loading2.vue";
+	import Loading3 from "./loading3.vue";
+	import Loading4 from "./loading4.vue";
+	import Loading5 from "./loading5.vue";
+	export default {
+		components:{Loading1,Loading2,Loading3,Loading4,Loading5},
+		name: 'qiun-loading',
+		props: {
+			loadingType: {
+				type: Number,
+				default: 2
+			},
+		},
+		data() {
+			return {
+				
+			};
+		},
+	}
+</script>
+
+<style>
+
+</style>

+ 172 - 0
miniapp/components/self-audio/index.scss

@@ -0,0 +1,172 @@
+@mixin textoverflow() {
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+}
+
+@keyframes rowup {
+  0% {
+    -webkit-transform: translate(-50%, -50%) rotate(0deg);
+    transform-origin: center center;
+  }
+
+  100% {
+    -webkit-transform: translate(-50%, -50%) rotate(360deg);
+    transform-origin: center center;
+  }
+}
+
+.imt-audio {
+  background: #f7f7f7;
+  width: 100%;
+  padding: 20px 20px 20px 10px;
+  position: relative;
+  overflow: hidden;
+  box-sizing: border-box;
+  color: #666666;
+  border-radius: 8rpx;
+
+  .audio-top {
+    display: flex;
+    justify-content: space-between;
+    box-sizing: border-box;
+    width: 100%;
+
+    .audio-left {
+      width: 140rpx;
+      position: relative;
+
+      .cover {
+        width: 120rpx;
+        height: 120rpx;
+        /* box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2); */
+        position: absolute;
+        top: calc(50%);
+        left: calc(50%);
+        transform: translate(-50%, -50%);
+        animation-fill-mode: forwards;
+        -webkit-animation-fill-mode: forwards;
+      }
+    }
+
+    .audio-right {
+      padding-left: 20rpx;
+      box-sizing: border-box;
+    }
+  }
+
+  .audio-right {
+    padding-left: 20rpx;
+    width: calc(100% - 140rpx);
+  }
+
+  .audio-wrapper {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+
+    .titlebox {
+      display: flex;
+      margin-bottom: 10rpx;
+
+      .title {
+        font-size: 30rpx;
+        max-width: 60%;
+        @include textoverflow;
+        color: #333;
+      }
+
+      .singer {
+        margin-left: 20rpx;
+        font-size: 28rpx;
+        max-width: 50%;
+        @include textoverflow;
+      }
+    }
+  }
+
+  .slidebox {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    font-size: 20rpx;
+    color: #787878;
+    margin-top: 6rpx;
+  }
+
+  /deep/ .uni-slider-tap-area {
+    padding: 0;
+  }
+
+  /deep/ .uni-slider-wrapper {
+    min-height: 0;
+  }
+
+  /deep/ .uni-slider-handle-wrapper {
+    height: 6px;
+  }
+
+  .audio-slider {
+    width: 100%;
+    margin: 0;
+  }
+
+  .play {
+    width: 80rpx;
+    height: 80rpx;
+    z-index: 99;
+    background: rgba(0, 0, 0, 0.4);
+    border-radius: 50%;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+
+    &.loading {
+      width: 60rpx;
+      height: 60rpx;
+      animation: rotating_theme3 2s linear infinite;
+    }
+  }
+}
+
+@keyframes rotating {
+  0% {
+    transform: rotateZ(0deg)
+  }
+
+  100% {
+    transform: rotateZ(360deg)
+  }
+}
+
+@keyframes rotating_theme3 {
+  0% {
+    transform: translate(-50%, -50%) rotateZ(0deg)
+  }
+
+  100% {
+    transform: translate(-50%, -50%) rotateZ(360deg)
+  }
+}
+
+.hItem {
+  margin-left: 16rpx;
+}
+
+.extrButton {
+  font-size: 36rpx;
+}
+
+.play-btns {
+  display: flex;
+  padding: 10px 50px 0 50px;
+  justify-content: space-between;
+  align-items: center;
+
+  .play-speed {
+    text-align: center;
+  }
+}

+ 225 - 0
miniapp/components/self-audio/self-audio.vue

@@ -0,0 +1,225 @@
+<template>
+  <!-- 
+    引用方式
+   onLoad() {
+     var audioPlayer = this.$refs.audioPlayer;
+     audioPlayer.setSrc(
+       "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3");
+     // audioPlayer.setPoster(
+     //   "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-uni-app-doc/7fbf26a0-4f4a-11eb-b680-7980c8a877b8.png"); //海报图片
+     audioPlayer.setSinger("贝多芬"); //设置歌手
+     audioPlayer.setName("致爱丽丝"); //设置作品名字
+     //
+   },
+  -->
+  <view class="imt-audio">
+    <view class="audio-top">
+      <view class="audio-left">
+        <view class="audio-control-wrapper">
+          <image :src="poster" mode="aspectFill" class="cover"></image>
+          <!-- loading存在逻辑问题,注释掉不用了 -->
+          <!-- <image :src="require('./static/loading.png')" v-if="playState=='loading'" class="play loading"></image> -->
+          <!-- <template v-else> -->
+          <image :src="require('./static/playbtn.png')" alt="play" @click="play" class="play" v-if="playState=='pause'">
+          </image>
+          <image :src="require('./static/pausebtn.png')" alt="pause" @click="pause" class="play" v-else></image>
+          <!-- </template> -->
+        </view>
+      </view>
+      <view class="audio-right">
+        <view class="audio-wrapper">
+          <view class="titlebox">
+            <view class="title">{{name}}</view>
+            <view v-if="singer" class="singer">{{singer}}</view>
+          </view>
+        </view>
+        <slider class="audio-slider" block-size="12" block-color="#49679c" activeColor="#49679c"
+          backgroundColor="#dddddd" :max="duration" :value="currentTime" @change="sliderChange"
+          @changing="sliderChanging"></slider>
+        <view class="slidebox">
+          <view>{{formatSeconds(currentTime)}}</view>
+          <view>{{formatSeconds(duration)}}</view>
+        </view>
+      </view>
+    </view>
+    <view v-if="playState=='playing'" class="play-btns">
+      <view @click.stop="sliderTo(-15)">
+        <u-icon name="rewind-left" color="#898989" size="22"></u-icon>
+      </view>
+      <view class="play-speed" @click.stop="changePlaySpeed">
+        {{currentSpeed==1?'倍数': currentSpeed + ' 倍'}}
+      </view>
+      <view @click.stop="sliderTo(15)">
+        <u-icon name="rewind-right" color="#898989" size="22"></u-icon>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  /*
+	createInnerAudioContext()是audio组件的内部实现,不能熄屏播放、不能后台播放、不能倍速播放。
+	getBackgroundAudioManager() 可以熄屏播放、后台播放,不能倍速播放。缺点是响应速度很慢,无法实现精细、及时的进度控制,而且可能被别的程序占用。
+  
+	因此这里只能用video来实现,video能倍速播放,不能熄屏播放、不能后台播放。而且避免了用createInnerAudioContext()实现的跳转到别的页面,还在播放的问题
+	因此应用程序可以在需要后台播放的时候(需要用户操作触发),再暂停这个控件的播放,然后自己用getBackgroundAudioManager实现后台播放
+	*/
+
+  import Vue from 'vue';
+  export default {
+    props: {},
+    data() {
+      return {
+        bgAudio: null,
+        //
+        src: "",
+        poster: "",
+        name: "...",
+        singer: "...",
+        duration: 0,
+        currentTime: 0,
+        nextTime: null,
+        playState: "pause", //"loading"/"playing"/"pause"
+        isSliderChanging: false,
+        currentSpeed: 1
+      };
+    },
+    created: function() {
+    },
+    methods: {
+      // 后台播放相关
+      operateBgAudio(opearteType) {
+        const that = this;
+        let bgAudio = this.bgAudio;
+        if (!bgAudio) {
+          // 进度条恢复可动
+          this.isSliderChanging = false;
+          //
+          bgAudio = uni.getBackgroundAudioManager();
+          bgAudio.paused = true;
+          bgAudio.currentTime = this.currentTime || 0;
+          bgAudio.title = "致爱丽丝"; //设置作品名字
+          bgAudio.src = 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3';
+          bgAudio.onPlay(e => {
+            this.playState = "playing";
+          })
+          bgAudio.onPause(e => {
+            this.isSliderChanging = false;
+            this.playState = "pause";
+          })
+          bgAudio.onStop(e => {
+            // 清空
+            that.stop();
+          })
+          bgAudio.onEnded(e => {
+            // 清空
+            that.bgAudio = '';
+            this.isSliderChanging = false;
+            that.stop();
+          })
+          bgAudio.onTimeUpdate(e => {
+            if (that.nextTime === 0 || that.nextTime > 0) {
+              that.currentTime = that.nextTime;
+              setTimeout(function() {
+                that.nextTime = null;
+                that.isSliderChanging = false;
+              })
+            } else {
+              if (that.isSliderChanging == false) {
+                that.currentTime = that.bgAudio.currentTime || 0;
+                that.duration = that.bgAudio.duration || 0;
+              }
+            }
+          })
+          this.bgAudio = bgAudio;
+        }
+        switch (opearteType) {
+          case 'play':
+            bgAudio.play();
+            break;
+          case 'pause':
+            bgAudio.pause();
+            break;
+          case 'sliderChange':
+            bgAudio.seek(this.nextTime)
+            this.isSliderChanging = false;
+            break;
+          case 'changePlaySpeed':
+            bgAudio.playbackRate = this.currentSpeed;
+            break;
+        }
+      },
+      // 切换播放速度
+      changePlaySpeed() {
+        if (this.currentSpeed < 2) {
+          this.currentSpeed += 0.25;
+        } else {
+          this.currentSpeed = 1;
+        }
+        //
+        this.operateBgAudio('changePlaySpeed');
+      },
+      setSrc: function(value) {
+        this.src = value;
+      },
+      setPoster: function(value) {
+        this.poster = value;
+      },
+      setName: function(value) {
+        this.name = value;
+      },
+      setSinger: function(value) {
+        this.singer = value;
+      },
+      formatSeconds: function(seconds) {
+        var result = typeof seconds === "string" ? parseFloat(seconds) : seconds;
+        if (isNaN(result))
+          return "";
+        let h = Math.floor(result / 3600) < 10 ?
+          "0" + Math.floor(result / 3600) :
+          Math.floor(result / 3600);
+        let m = Math.floor((result / 60) % 60) < 10 ?
+          "0" + Math.floor((result / 60) % 60) :
+          Math.floor((result / 60) % 60) + h * 60;
+        let s = Math.floor(result % 60) < 10 ?
+          "0" + Math.floor(result % 60) :
+          Math.floor(result % 60);
+        return `${m}:${s}`;
+      },
+
+      play: function() {
+        this.playState = "playing";
+        //
+        this.operateBgAudio("play");
+      },
+      pause: function() {
+        this.playState = "pause";
+        this.operateBgAudio("pause");
+      },
+      sliderTo(time) {
+        this.isSliderChanging = true;
+        this.currentTime += time;
+        this.nextTime = this.currentTime;
+        this.operateBgAudio("sliderChange");
+      },
+      sliderChange: function(e) {
+        this.isSliderChanging = true;
+        // 进度条下次变化时间
+        this.nextTime = e.detail.value;
+        this.operateBgAudio("sliderChange");
+      },
+      sliderChanging: function() {
+        this.isSliderChanging = true;
+      },
+      stop: function() {
+        this.isSliderChanging = true;
+        this.currentTime = 0;
+        this.playState = "pause";
+      },
+    },
+  }
+</script>
+
+<style lang="scss">
+  @import './index.scss';
+</style>

二進制
miniapp/components/self-audio/static/loading.png


二進制
miniapp/components/self-audio/static/loading2.png


二進制
miniapp/components/self-audio/static/pausebtn.png


二進制
miniapp/components/self-audio/static/playbtn.png


+ 187 - 0
miniapp/components/uni-datetime-picker/calendar-item.vue

@@ -0,0 +1,187 @@
+<template>
+	<view class="uni-calendar-item__weeks-box" :class="{
+		'uni-calendar-item--disable':weeks.disable,
+		'uni-calendar-item--before-checked-x':weeks.beforeMultiple,
+		'uni-calendar-item--multiple': weeks.multiple,
+		'uni-calendar-item--after-checked-x':weeks.afterMultiple,
+		}" @click="choiceDate(weeks)" @mouseenter="handleMousemove(weeks)">
+		<view class="uni-calendar-item__weeks-box-item" :class="{
+				'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && (calendar.userChecked || !checkHover),
+				'uni-calendar-item--checked-range-text': checkHover,
+				'uni-calendar-item--before-checked':weeks.beforeMultiple,
+				'uni-calendar-item--multiple': weeks.multiple,
+				'uni-calendar-item--after-checked':weeks.afterMultiple,
+				'uni-calendar-item--disable':weeks.disable,
+				}">
+			<text v-if="selected&&weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
+			<text class="uni-calendar-item__weeks-box-text uni-calendar-item__weeks-box-text-disable uni-calendar-item--checked-text">{{weeks.date}}</text>
+		</view>
+		<view :class="{'uni-calendar-item--isDay': weeks.isDay}"></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			weeks: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			calendar: {
+				type: Object,
+				default: () => {
+					return {}
+				}
+			},
+			selected: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+			lunar: {
+				type: Boolean,
+				default: false
+			},
+			checkHover: {
+				type: Boolean,
+				default: false
+			}
+		},
+		methods: {
+			choiceDate(weeks) {
+				this.$emit('change', weeks)
+			},
+			handleMousemove(weeks) {
+				this.$emit('handleMouse', weeks)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" >
+	$uni-primary: #007aff !default;
+
+	.uni-calendar-item__weeks-box {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		margin: 1px 0;
+		position: relative;
+	}
+
+	.uni-calendar-item__weeks-box-text {
+		font-size: 14px;
+		// font-family: Lato-Bold, Lato;
+		font-weight: bold;
+		color: darken($color: $uni-primary, $amount: 40%);
+	}
+
+	.uni-calendar-item__weeks-lunar-text {
+		font-size: 12px;
+		color: #333;
+	}
+
+	.uni-calendar-item__weeks-box-item {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		width: 40px;
+		height: 40px;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+
+	.uni-calendar-item__weeks-box-circle {
+		position: absolute;
+		top: 5px;
+		right: 5px;
+		width: 8px;
+		height: 8px;
+		border-radius: 8px;
+		background-color: #dd524d;
+
+	}
+
+	.uni-calendar-item__weeks-box .uni-calendar-item--disable {
+		// background-color: rgba(249, 249, 249, $uni-opacity-disabled);
+		cursor: default;
+	}
+
+	.uni-calendar-item--disable .uni-calendar-item__weeks-box-text-disable {
+		color: #D1D1D1;
+	}
+
+	.uni-calendar-item--isDay {
+		position: absolute;
+		top: 10px;
+		right: 17%;
+		background-color: #dd524d;
+		width:6px;
+		height: 6px;
+		border-radius: 50%;
+	}
+
+	.uni-calendar-item--extra {
+		color: #dd524d;
+		opacity: 0.8;
+	}
+
+	.uni-calendar-item__weeks-box .uni-calendar-item--checked {
+		background-color: $uni-primary;
+		border-radius: 50%;
+		box-sizing: border-box;
+		border: 3px solid #fff;
+	}
+
+	.uni-calendar-item--checked .uni-calendar-item--checked-text {
+		color: #fff;
+	}
+
+	.uni-calendar-item--multiple .uni-calendar-item--checked-range-text {
+		color: #333;
+	}
+
+	.uni-calendar-item--multiple {
+		background-color:  #F6F7FC;
+		// color: #fff;
+	}
+
+	.uni-calendar-item--multiple .uni-calendar-item--before-checked,
+	.uni-calendar-item--multiple .uni-calendar-item--after-checked {
+		background-color: $uni-primary;
+		border-radius: 50%;
+		box-sizing: border-box;
+		border: 3px solid #F6F7FC;
+	}
+
+	.uni-calendar-item--before-checked .uni-calendar-item--checked-text,
+	.uni-calendar-item--after-checked .uni-calendar-item--checked-text {
+		color: #fff;
+	}
+
+	.uni-calendar-item--before-checked-x {
+		border-top-left-radius: 50px;
+		border-bottom-left-radius: 50px;
+		box-sizing: border-box;
+		background-color: #F6F7FC;
+	}
+
+	.uni-calendar-item--after-checked-x {
+		border-top-right-radius: 50px;
+		border-bottom-right-radius: 50px;
+		background-color: #F6F7FC;
+	}
+</style>

+ 924 - 0
miniapp/components/uni-datetime-picker/calendar.vue

@@ -0,0 +1,924 @@
+<template>
+	<view class="uni-calendar" @mouseleave="leaveCale">
+		<view v-if="!insert&&show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}"
+			@click="clean();maskClick()"></view>
+		<view v-if="insert || show" class="uni-calendar__content"
+			:class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow, 'uni-calendar__content-mobile': aniMaskShow}">
+			<view class="uni-calendar__header" :class="{'uni-calendar__header-mobile' :!insert}">
+				<view v-if="left" class="uni-calendar__header-btn-box" @click.stop="pre">
+					<view class="uni-calendar__header-btn uni-calendar--left"></view>
+				</view>
+				<picker mode="date" :value="date" fields="month" @change="bindDateChange">
+					<text
+						class="uni-calendar__header-text">{{ (nowDate.year||'') + yearText + ( nowDate.month||'') + monthText}}</text>
+				</picker>
+				<view v-if="right" class="uni-calendar__header-btn-box" @click.stop="next">
+					<view class="uni-calendar__header-btn uni-calendar--right"></view>
+				</view>
+				<view v-if="!insert" class="dialog-close" @click="clean">
+					<view class="dialog-close-plus" data-id="close"></view>
+					<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
+				</view>
+
+				<!-- <text class="uni-calendar__backtoday" @click="backtoday">回到今天</text> -->
+			</view>
+			<view class="uni-calendar__box">
+				<view v-if="showMonth" class="uni-calendar__box-bg">
+					<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
+				</view>
+				<view class="uni-calendar__weeks" style="padding-bottom: 7px;">
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{MONText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
+					</view>
+					<view class="uni-calendar__weeks-day">
+						<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
+					</view>
+				</view>
+				<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
+					<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
+						<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar"
+							:selected="selected" :lunar="lunar" :checkHover="range" @change="choiceDate"
+							@handleMouse="handleMouse">
+						</calendar-item>
+					</view>
+				</view>
+			</view>
+			<view v-if="!insert && !range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top"
+				style="padding: 0 80px;">
+				<view class="uni-date-changed--time-date">{{tempSingleDate ? tempSingleDate : selectDateText}}</view>
+				<time-picker type="time" :start="reactStartTime" :end="reactEndTime" v-model="time"
+					:disabled="!tempSingleDate" :border="false" :hide-second="hideSecond" class="time-picker-style">
+				</time-picker>
+			</view>
+
+			<view v-if="!insert && range && typeHasTime" class="uni-date-changed uni-calendar--fixed-top">
+				<view class="uni-date-changed--time-start">
+					<view class="uni-date-changed--time-date">{{tempRange.before ? tempRange.before : startDateText}}
+					</view>
+					<time-picker type="time" :start="reactStartTime" v-model="timeRange.startTime" :border="false"
+						:hide-second="hideSecond" :disabled="!tempRange.before" class="time-picker-style">
+					</time-picker>
+				</view>
+				<view style="line-height: 50px;">
+					<uni-icons type="arrowthinright" color="#999"></uni-icons>
+				</view>
+				<view class="uni-date-changed--time-end">
+					<view class="uni-date-changed--time-date">{{tempRange.after ? tempRange.after : endDateText}}</view>
+					<time-picker type="time" :end="reactEndTime" v-model="timeRange.endTime" :border="false"
+						:hide-second="hideSecond" :disabled="!tempRange.after" class="time-picker-style">
+					</time-picker>
+				</view>
+			</view>
+			<view v-if="!insert" class="uni-date-changed uni-date-btn--ok">
+				<!-- <view class="uni-calendar__header-btn-box">
+					<text class="uni-calendar__button-text uni-calendar--fixed-width">{{okText}}</text>
+				</view> -->
+				<view class="uni-datetime-picker--btn" @click="confirm">{{confirmText}}</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import Calendar from './util.js';
+	import calendarItem from './calendar-item.vue'
+	import timePicker from './time-picker.vue'
+	import {
+		initVueI18n
+	} from '@dcloudio/uni-i18n'
+	import messages from './i18n/index.js'
+	const {
+		t
+	} = initVueI18n(messages)
+	/**
+	 * Calendar 日历
+	 * @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=56
+	 * @property {String} date 自定义当前时间,默认为今天
+	 * @property {Boolean} lunar 显示农历
+	 * @property {String} startDate 日期选择范围-开始日期
+	 * @property {String} endDate 日期选择范围-结束日期
+	 * @property {Boolean} range 范围选择
+	 * @property {Boolean} insert = [true|false] 插入模式,默认为false
+	 * 	@value true 弹窗模式
+	 * 	@value false 插入模式
+	 * @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
+	 * @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
+	 * @property {Boolean} showMonth 是否选择月份为背景
+	 * @event {Function} change 日期改变,`insert :ture` 时生效
+	 * @event {Function} confirm 确认选择`insert :false` 时生效
+	 * @event {Function} monthSwitch 切换月份时触发
+	 * @example <uni-calendar :insert="true":lunar="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
+	 */
+	export default {
+		components: {
+			calendarItem,
+			timePicker
+		},
+		props: {
+			date: {
+				type: String,
+				default: ''
+			},
+			defTime: {
+				type: [String, Object],
+				default: ''
+			},
+			selectableTimes: {
+				type: [Object],
+				default () {
+					return {}
+				}
+			},
+			selected: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			lunar: {
+				type: Boolean,
+				default: false
+			},
+			startDate: {
+				type: String,
+				default: ''
+			},
+			endDate: {
+				type: String,
+				default: ''
+			},
+			range: {
+				type: Boolean,
+				default: false
+			},
+			typeHasTime: {
+				type: Boolean,
+				default: false
+			},
+			insert: {
+				type: Boolean,
+				default: true
+			},
+			showMonth: {
+				type: Boolean,
+				default: true
+			},
+			clearDate: {
+				type: Boolean,
+				default: true
+			},
+			left: {
+				type: Boolean,
+				default: true
+			},
+			right: {
+				type: Boolean,
+				default: true
+			},
+			checkHover: {
+				type: Boolean,
+				default: true
+			},
+			hideSecond: {
+				type: [Boolean],
+				default: false
+			},
+			pleStatus: {
+				type: Object,
+				default () {
+					return {
+						before: '',
+						after: '',
+						data: [],
+						fulldate: ''
+					}
+				}
+			}
+		},
+		data() {
+			return {
+				show: false,
+				weeks: [],
+				calendar: {},
+				nowDate: '',
+				aniMaskShow: false,
+				firstEnter: true,
+				time: '',
+				timeRange: {
+					startTime: '',
+					endTime: ''
+				},
+				tempSingleDate: '',
+				tempRange: {
+					before: '',
+					after: ''
+				}
+			}
+		},
+		watch: {
+			date: {
+				immediate: true,
+				handler(newVal, oldVal) {
+					if (!this.range) {
+						this.tempSingleDate = newVal
+						setTimeout(() => {
+							this.init(newVal)
+						}, 100)
+					}
+				}
+			},
+			defTime: {
+				immediate: true,
+				handler(newVal, oldVal) {
+					if (!this.range) {
+						this.time = newVal
+					} else {
+						// console.log('-----', newVal);
+						this.timeRange.startTime = newVal.start
+						this.timeRange.endTime = newVal.end
+					}
+				}
+			},
+			startDate(val) {
+				this.cale.resetSatrtDate(val)
+				this.cale.setDate(this.nowDate.fullDate)
+				this.weeks = this.cale.weeks
+			},
+			endDate(val) {
+				this.cale.resetEndDate(val)
+				this.cale.setDate(this.nowDate.fullDate)
+				this.weeks = this.cale.weeks
+			},
+			selected(newVal) {
+				this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
+				this.weeks = this.cale.weeks
+			},
+			pleStatus: {
+				immediate: true,
+				handler(newVal, oldVal) {
+					const {
+						before,
+						after,
+						fulldate,
+						which
+					} = newVal
+					this.tempRange.before = before
+					this.tempRange.after = after
+					setTimeout(() => {
+						if (fulldate) {
+							this.cale.setHoverMultiple(fulldate)
+							if (before && after) {
+								this.cale.lastHover = true
+								if (this.rangeWithinMonth(after, before)) return
+								this.setDate(before)
+							} else {
+								this.cale.setMultiple(fulldate)
+								this.setDate(this.nowDate.fullDate)
+								this.calendar.fullDate = ''
+								this.cale.lastHover = false
+							}
+						} else {
+							this.cale.setDefaultMultiple(before, after)
+							if (which === 'left') {
+								this.setDate(before)
+								this.weeks = this.cale.weeks
+							} else {
+								this.setDate(after)
+								this.weeks = this.cale.weeks
+							}
+							this.cale.lastHover = true
+						}
+					}, 16)
+				}
+			}
+		},
+		computed: {
+			reactStartTime() {
+				const activeDate = this.range ? this.tempRange.before : this.calendar.fullDate
+				const res = activeDate === this.startDate ? this.selectableTimes.start : ''
+				return res
+			},
+			reactEndTime() {
+				const activeDate = this.range ? this.tempRange.after : this.calendar.fullDate
+				const res = activeDate === this.endDate ? this.selectableTimes.end : ''
+				return res
+			},
+			/**
+			 * for i18n
+			 */
+			selectDateText() {
+				return t("uni-datetime-picker.selectDate")
+			},
+			startDateText() {
+				return this.startPlaceholder || t("uni-datetime-picker.startDate")
+			},
+			endDateText() {
+				return this.endPlaceholder || t("uni-datetime-picker.endDate")
+			},
+			okText() {
+				return t("uni-datetime-picker.ok")
+			},
+			yearText() {
+				return t("uni-datetime-picker.year")
+			},
+			monthText() {
+				return t("uni-datetime-picker.month")
+			},
+			MONText() {
+				return t("uni-calender.MON")
+			},
+			TUEText() {
+				return t("uni-calender.TUE")
+			},
+			WEDText() {
+				return t("uni-calender.WED")
+			},
+			THUText() {
+				return t("uni-calender.THU")
+			},
+			FRIText() {
+				return t("uni-calender.FRI")
+			},
+			SATText() {
+				return t("uni-calender.SAT")
+			},
+			SUNText() {
+				return t("uni-calender.SUN")
+			},
+			confirmText() {
+				return t("uni-calender.confirm")
+			},
+		},
+		created() {
+			// 获取日历方法实例
+			this.cale = new Calendar({
+				// date: new Date(),
+				selected: this.selected,
+				startDate: this.startDate,
+				endDate: this.endDate,
+				range: this.range,
+				// multipleStatus: this.pleStatus
+			})
+			// 选中某一天
+			// this.cale.setDate(this.date)
+			this.init(this.date)
+			// this.setDay
+		},
+		methods: {
+			leaveCale() {
+				this.firstEnter = true
+			},
+			handleMouse(weeks) {
+				if (weeks.disable) return
+				if (this.cale.lastHover) return
+				let {
+					before,
+					after
+				} = this.cale.multipleStatus
+				if (!before) return
+				this.calendar = weeks
+				// 设置范围选
+				this.cale.setHoverMultiple(this.calendar.fullDate)
+				this.weeks = this.cale.weeks
+				// hover时,进入一个日历,更新另一个
+				if (this.firstEnter) {
+					this.$emit('firstEnterCale', this.cale.multipleStatus)
+					this.firstEnter = false
+				}
+			},
+			rangeWithinMonth(A, B) {
+				const [yearA, monthA] = A.split('-')
+				const [yearB, monthB] = B.split('-')
+				return yearA === yearB && monthA === monthB
+			},
+
+			// 取消穿透
+			clean() {
+				this.close()
+			},
+
+			// 蒙版点击事件
+			maskClick() {
+				this.$emit('maskClose')
+			},
+
+			clearCalender() {
+				if (this.range) {
+					this.timeRange.startTime = ''
+					this.timeRange.endTime = ''
+					this.tempRange.before = ''
+					this.tempRange.after = ''
+					this.cale.multipleStatus.before = ''
+					this.cale.multipleStatus.after = ''
+					this.cale.multipleStatus.data = []
+					this.cale.lastHover = false
+				} else {
+					this.time = ''
+					this.tempSingleDate = ''
+				}
+				this.calendar.fullDate = ''
+				this.setDate()
+			},
+
+			bindDateChange(e) {
+				const value = e.detail.value + '-1'
+				this.init(value)
+			},
+			/**
+			 * 初始化日期显示
+			 * @param {Object} date
+			 */
+			init(date) {
+				this.cale.setDate(date)
+				this.weeks = this.cale.weeks
+				this.nowDate = this.calendar = this.cale.getInfo(date)
+			},
+			// choiceDate(weeks) {
+			// 	if (weeks.disable) return
+			// 	this.calendar = weeks
+			// 	// 设置多选
+			// 	this.cale.setMultiple(this.calendar.fullDate, true)
+			// 	this.weeks = this.cale.weeks
+			// 	this.tempSingleDate = this.calendar.fullDate
+			// 	this.tempRange.before = this.cale.multipleStatus.before
+			// 	this.tempRange.after = this.cale.multipleStatus.after
+			// 	this.change()
+			// },
+			/**
+			 * 打开日历弹窗
+			 */
+			open() {
+				// 弹窗模式并且清理数据
+				if (this.clearDate && !this.insert) {
+					this.cale.cleanMultipleStatus()
+					// this.cale.setDate(this.date)
+					this.init(this.date)
+				}
+				this.show = true
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.aniMaskShow = true
+					}, 50)
+				})
+			},
+			/**
+			 * 关闭日历弹窗
+			 */
+			close() {
+				this.aniMaskShow = false
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.show = false
+						this.$emit('close')
+					}, 300)
+				})
+			},
+			/**
+			 * 确认按钮
+			 */
+			confirm() {
+				this.setEmit('confirm')
+				this.close()
+			},
+			/**
+			 * 变化触发
+			 */
+			change() {
+				if (!this.insert) return
+				this.setEmit('change')
+			},
+			/**
+			 * 选择月份触发
+			 */
+			monthSwitch() {
+				let {
+					year,
+					month
+				} = this.nowDate
+				this.$emit('monthSwitch', {
+					year,
+					month: Number(month)
+				})
+			},
+			/**
+			 * 派发事件
+			 * @param {Object} name
+			 */
+			setEmit(name) {
+				let {
+					year,
+					month,
+					date,
+					fullDate,
+					lunar,
+					extraInfo
+				} = this.calendar
+				this.$emit(name, {
+					range: this.cale.multipleStatus,
+					year,
+					month,
+					date,
+					time: this.time,
+					timeRange: this.timeRange,
+					fulldate: fullDate,
+					lunar,
+					extraInfo: extraInfo || {}
+				})
+			},
+			/**
+			 * 选择天触发
+			 * @param {Object} weeks
+			 */
+			choiceDate(weeks) {
+				if (weeks.disable) return
+				this.calendar = weeks
+				this.calendar.userChecked = true
+				// 设置多选
+				this.cale.setMultiple(this.calendar.fullDate, true)
+				this.weeks = this.cale.weeks
+				this.tempSingleDate = this.calendar.fullDate
+				const beforeStatus = this.cale.multipleStatus.before
+				const beforeDate = new Date(this.cale.multipleStatus.before).getTime()
+				const afterDate = new Date(this.cale.multipleStatus.after).getTime()
+				if (beforeDate > afterDate && afterDate) {
+					this.tempRange.before = this.cale.multipleStatus.after
+					this.tempRange.after = this.cale.multipleStatus.before
+				} else {
+					this.tempRange.before = this.cale.multipleStatus.before
+					this.tempRange.after = this.cale.multipleStatus.after
+				}
+				this.change()
+			},
+			/**
+			 * 回到今天
+			 */
+			backtoday() {
+				let date = this.cale.getDate(new Date()).fullDate
+				// this.cale.setDate(date)
+				this.init(date)
+				this.change()
+			},
+			/**
+			 * 比较时间大小
+			 */
+			dateCompare(startDate, endDate) {
+				// 计算截止时间
+				startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
+				// 计算详细项的截止时间
+				endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
+				if (startDate <= endDate) {
+					return true
+				} else {
+					return false
+				}
+			},
+			/**
+			 * 上个月
+			 */
+			pre() {
+				const preDate = this.cale.getDate(this.nowDate.fullDate, -1, 'month').fullDate
+				this.setDate(preDate)
+				this.monthSwitch()
+
+			},
+			/**
+			 * 下个月
+			 */
+			next() {
+				const nextDate = this.cale.getDate(this.nowDate.fullDate, +1, 'month').fullDate
+				this.setDate(nextDate)
+				this.monthSwitch()
+			},
+			/**
+			 * 设置日期
+			 * @param {Object} date
+			 */
+			setDate(date) {
+				this.cale.setDate(date)
+				this.weeks = this.cale.weeks
+				this.nowDate = this.cale.getInfo(date)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" >
+	$uni-primary: #007aff !default;
+
+	.uni-calendar {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+	}
+
+	.uni-calendar__mask {
+		position: fixed;
+		bottom: 0;
+		top: 0;
+		left: 0;
+		right: 0;
+		background-color: rgba(0, 0, 0, 0.4);
+		transition-property: opacity;
+		transition-duration: 0.3s;
+		opacity: 0;
+		/* #ifndef APP-NVUE */
+		z-index: 99;
+		/* #endif */
+	}
+
+	.uni-calendar--mask-show {
+		opacity: 1
+	}
+
+	.uni-calendar--fixed {
+		position: fixed;
+		bottom: calc(var(--window-bottom));
+		left: 0;
+		right: 0;
+		transition-property: transform;
+		transition-duration: 0.3s;
+		transform: translateY(460px);
+		/* #ifndef APP-NVUE */
+		z-index: 99;
+		/* #endif */
+	}
+
+	.uni-calendar--ani-show {
+		transform: translateY(0);
+	}
+
+	.uni-calendar__content {
+		background-color: #fff;
+	}
+
+	.uni-calendar__content-mobile {
+		border-top-left-radius: 10px;
+		border-top-right-radius: 10px;
+		box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.1);
+	}
+
+	.uni-calendar__header {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		height: 50px;
+	}
+
+	.uni-calendar__header-mobile {
+		padding: 10px;
+		padding-bottom: 0;
+	}
+
+	.uni-calendar--fixed-top {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: space-between;
+		border-top-color: rgba(0, 0, 0, 0.4);
+		border-top-style: solid;
+		border-top-width: 1px;
+	}
+
+	.uni-calendar--fixed-width {
+		width: 50px;
+	}
+
+	.uni-calendar__backtoday {
+		position: absolute;
+		right: 0;
+		top: 25rpx;
+		padding: 0 5px;
+		padding-left: 10px;
+		height: 25px;
+		line-height: 25px;
+		font-size: 12px;
+		border-top-left-radius: 25px;
+		border-bottom-left-radius: 25px;
+		color: #fff;
+		background-color: #f1f1f1;
+	}
+
+	.uni-calendar__header-text {
+		text-align: center;
+		width: 100px;
+		font-size: 15px;
+		color: #666;
+	}
+
+	.uni-calendar__button-text {
+		text-align: center;
+		width: 100px;
+		font-size: 14px;
+		color: $uni-primary;
+		/* #ifndef APP-NVUE */
+		letter-spacing: 3px;
+		/* #endif */
+	}
+
+	.uni-calendar__header-btn-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		width: 50px;
+		height: 50px;
+	}
+
+	.uni-calendar__header-btn {
+		width: 9px;
+		height: 9px;
+		border-left-color: #808080;
+		border-left-style: solid;
+		border-left-width: 1px;
+		border-top-color: #555555;
+		border-top-style: solid;
+		border-top-width: 1px;
+	}
+
+	.uni-calendar--left {
+		transform: rotate(-45deg);
+	}
+
+	.uni-calendar--right {
+		transform: rotate(135deg);
+	}
+
+
+	.uni-calendar__weeks {
+		position: relative;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.uni-calendar__weeks-item {
+		flex: 1;
+	}
+
+	.uni-calendar__weeks-day {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		height: 40px;
+		border-bottom-color: #F5F5F5;
+		border-bottom-style: solid;
+		border-bottom-width: 1px;
+	}
+
+	.uni-calendar__weeks-day-text {
+		font-size: 12px;
+		color: #B2B2B2;
+	}
+
+	.uni-calendar__box {
+		position: relative;
+		// padding: 0 10px;
+		padding-bottom: 7px;
+	}
+
+	.uni-calendar__box-bg {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+	}
+
+	.uni-calendar__box-bg-text {
+		font-size: 200px;
+		font-weight: bold;
+		color: #999;
+		opacity: 0.1;
+		text-align: center;
+		/* #ifndef APP-NVUE */
+		line-height: 1;
+		/* #endif */
+	}
+
+	.uni-date-changed {
+		padding: 0 10px;
+		// line-height: 50px;
+		text-align: center;
+		color: #333;
+		border-top-color: #DCDCDC;
+		;
+		border-top-style: solid;
+		border-top-width: 1px;
+		flex: 1;
+	}
+
+	.uni-date-btn--ok {
+		padding: 20px 15px;
+	}
+
+	.uni-date-changed--time-start {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.uni-date-changed--time-end {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		align-items: center;
+	}
+
+	.uni-date-changed--time-date {
+		color: #999;
+		line-height: 50px;
+		margin-right: 5px;
+		// opacity: 0.6;
+	}
+
+	.time-picker-style {
+		// width: 62px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center
+	}
+
+	.mr-10 {
+		margin-right: 10px;
+	}
+
+	.dialog-close {
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		padding: 0 25px;
+		margin-top: 10px;
+	}
+
+	.dialog-close-plus {
+		width: 16px;
+		height: 2px;
+		background-color: #737987;
+		border-radius: 2px;
+		transform: rotate(45deg);
+	}
+
+	.dialog-close-rotate {
+		position: absolute;
+		transform: rotate(-45deg);
+	}
+
+	.uni-datetime-picker--btn {
+		border-radius: 100px;
+		height: 40px;
+		line-height: 40px;
+		background-color: $uni-primary;
+		color: #fff;
+		font-size: 16px;
+		letter-spacing: 2px;
+	}
+
+	/* #ifndef APP-NVUE */
+	.uni-datetime-picker--btn:active {
+		opacity: 0.7;
+	}
+	/* #endif */
+</style>

+ 22 - 0
miniapp/components/uni-datetime-picker/i18n/en.json

@@ -0,0 +1,22 @@
+{
+	"uni-datetime-picker.selectDate": "select date",
+	"uni-datetime-picker.selectTime": "select time",
+	"uni-datetime-picker.selectDateTime": "select datetime",
+	"uni-datetime-picker.startDate": "start date",
+	"uni-datetime-picker.endDate": "end date",
+	"uni-datetime-picker.startTime": "start time",
+	"uni-datetime-picker.endTime": "end time",
+	"uni-datetime-picker.ok": "ok",
+	"uni-datetime-picker.clear": "clear",
+	"uni-datetime-picker.cancel": "cancel",
+	"uni-datetime-picker.year": "-",
+	"uni-datetime-picker.month": "",
+	"uni-calender.MON": "MON",
+	"uni-calender.TUE": "TUE",
+	"uni-calender.WED": "WED",
+	"uni-calender.THU": "THU",
+	"uni-calender.FRI": "FRI",
+	"uni-calender.SAT": "SAT",
+	"uni-calender.SUN": "SUN",
+	"uni-calender.confirm": "confirm"
+}

+ 8 - 0
miniapp/components/uni-datetime-picker/i18n/index.js

@@ -0,0 +1,8 @@
+import en from './en.json'
+import zhHans from './zh-Hans.json'
+import zhHant from './zh-Hant.json'
+export default {
+	en,
+	'zh-Hans': zhHans,
+	'zh-Hant': zhHant
+}

+ 22 - 0
miniapp/components/uni-datetime-picker/i18n/zh-Hans.json

@@ -0,0 +1,22 @@
+{
+	"uni-datetime-picker.selectDate": "选择日期",
+	"uni-datetime-picker.selectTime": "选择时间",
+	"uni-datetime-picker.selectDateTime": "选择日期时间",
+	"uni-datetime-picker.startDate": "开始日期",
+	"uni-datetime-picker.endDate": "结束日期",
+	"uni-datetime-picker.startTime": "开始时间",
+	"uni-datetime-picker.endTime": "结束时间",
+	"uni-datetime-picker.ok": "确定",
+	"uni-datetime-picker.clear": "清除",
+	"uni-datetime-picker.cancel": "取消",
+	"uni-datetime-picker.year": "年",
+	"uni-datetime-picker.month": "月",
+	"uni-calender.SUN": "日",
+	"uni-calender.MON": "一",
+	"uni-calender.TUE": "二",
+	"uni-calender.WED": "三",
+	"uni-calender.THU": "四",
+	"uni-calender.FRI": "五",
+	"uni-calender.SAT": "六",
+	"uni-calender.confirm": "确认"
+}

+ 22 - 0
miniapp/components/uni-datetime-picker/i18n/zh-Hant.json

@@ -0,0 +1,22 @@
+{
+  "uni-datetime-picker.selectDate": "選擇日期",
+  "uni-datetime-picker.selectTime": "選擇時間",
+  "uni-datetime-picker.selectDateTime": "選擇日期時間",
+  "uni-datetime-picker.startDate": "開始日期",
+  "uni-datetime-picker.endDate": "結束日期",
+  "uni-datetime-picker.startTime": "開始时间",
+  "uni-datetime-picker.endTime": "結束时间",
+  "uni-datetime-picker.ok": "確定",
+  "uni-datetime-picker.clear": "清除",
+  "uni-datetime-picker.cancel": "取消",
+  "uni-datetime-picker.year": "年",
+  "uni-datetime-picker.month": "月",
+  "uni-calender.SUN": "日",
+  "uni-calender.MON": "一",
+  "uni-calender.TUE": "二",
+  "uni-calender.WED": "三",
+  "uni-calender.THU": "四",
+  "uni-calender.FRI": "五",
+  "uni-calender.SAT": "六",
+  "uni-calender.confirm": "確認"
+}

+ 45 - 0
miniapp/components/uni-datetime-picker/keypress.js

@@ -0,0 +1,45 @@
+// #ifdef H5
+export default {
+  name: 'Keypress',
+  props: {
+    disable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  mounted () {
+    const keyNames = {
+      esc: ['Esc', 'Escape'],
+      tab: 'Tab',
+      enter: 'Enter',
+      space: [' ', 'Spacebar'],
+      up: ['Up', 'ArrowUp'],
+      left: ['Left', 'ArrowLeft'],
+      right: ['Right', 'ArrowRight'],
+      down: ['Down', 'ArrowDown'],
+      delete: ['Backspace', 'Delete', 'Del']
+    }
+    const listener = ($event) => {
+      if (this.disable) {
+        return
+      }
+      const keyName = Object.keys(keyNames).find(key => {
+        const keyName = $event.key
+        const value = keyNames[key]
+        return value === keyName || (Array.isArray(value) && value.includes(keyName))
+      })
+      if (keyName) {
+        // 避免和其他按键事件冲突
+        setTimeout(() => {
+          this.$emit(keyName, {})
+        }, 0)
+      }
+    }
+    document.addEventListener('keyup', listener)
+    this.$once('hook:beforeDestroy', () => {
+      document.removeEventListener('keyup', listener)
+    })
+  },
+	render: () => {}
+}
+// #endif

+ 946 - 0
miniapp/components/uni-datetime-picker/time-picker.vue

@@ -0,0 +1,946 @@
+<template>
+	<view class="uni-datetime-picker">
+		<view @click="initTimePicker">
+			<slot>
+				<view class="uni-datetime-picker-timebox-pointer"
+					:class="{'uni-datetime-picker-disabled': disabled, 'uni-datetime-picker-timebox': border}">
+					<text class="uni-datetime-picker-text">{{time}}</text>
+					<view v-if="!time" class="uni-datetime-picker-time">
+						<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
+					</view>
+				</view>
+			</slot>
+		</view>
+		<view v-if="visible" id="mask" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view>
+		<view v-if="visible" class="uni-datetime-picker-popup" :class="[dateShow && timeShow ? '' : 'fix-nvue-height']"
+			:style="fixNvueBug">
+			<view class="uni-title">
+				<text class="uni-datetime-picker-text">{{selectTimeText}}</text>
+			</view>
+			<view v-if="dateShow" class="uni-datetime-picker__container-box">
+				<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd"
+					@change="bindDateChange">
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+				<!-- 兼容 nvue 不支持伪类 -->
+				<text class="uni-datetime-picker-sign sign-left">-</text>
+				<text class="uni-datetime-picker-sign sign-right">-</text>
+			</view>
+			<view v-if="timeShow" class="uni-datetime-picker__container-box">
+				<picker-view class="uni-datetime-picker-view" :class="[hideSecond ? 'time-hide-second' : '']"
+					:indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange">
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column>
+						<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+					<picker-view-column v-if="!hideSecond">
+						<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">
+							<text class="uni-datetime-picker-item">{{lessThanTen(item)}}</text>
+						</view>
+					</picker-view-column>
+				</picker-view>
+				<!-- 兼容 nvue 不支持伪类 -->
+				<text class="uni-datetime-picker-sign" :class="[hideSecond ? 'sign-center' : 'sign-left']">:</text>
+				<text v-if="!hideSecond" class="uni-datetime-picker-sign sign-right">:</text>
+			</view>
+			<view class="uni-datetime-picker-btn">
+				<view @click="clearTime">
+					<text class="uni-datetime-picker-btn-text">{{clearText}}</text>
+				</view>
+				<view class="uni-datetime-picker-btn-group">
+					<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">
+						<text class="uni-datetime-picker-btn-text">{{cancelText}}</text>
+					</view>
+					<view @click="setTime">
+						<text class="uni-datetime-picker-btn-text">{{okText}}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+		<!-- #ifdef H5 -->
+		<!-- <keypress v-if="visible" @esc="tiggerTimePicker" @enter="setTime" /> -->
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	// #ifdef H5
+	import keypress from './keypress'
+	// #endif
+	import {
+		initVueI18n
+	} from '@dcloudio/uni-i18n'
+	import messages from './i18n/index.js'
+	const {	t	} = initVueI18n(messages)
+
+	/**
+	 * DatetimePicker 时间选择器
+	 * @description 可以同时选择日期和时间的选择器
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
+	 * @property {String} type = [datetime | date | time] 显示模式
+	 * @property {Boolean} multiple = [true|false] 是否多选
+	 * @property {String|Number} value 默认值
+	 * @property {String|Number} start 起始日期或时间
+	 * @property {String|Number} end 起始日期或时间
+	 * @property {String} return-type = [timestamp | string]
+	 * @event {Function} change  选中发生变化触发
+	 */
+
+	export default {
+		name: 'UniDatetimePicker',
+		components: {
+			// #ifdef H5
+			keypress
+			// #endif
+		},
+		data() {
+			return {
+				indicatorStyle: `height: 50px;`,
+				visible: false,
+				fixNvueBug: {},
+				dateShow: true,
+				timeShow: true,
+				title: '日期和时间',
+				// 输入框当前时间
+				time: '',
+				// 当前的年月日时分秒
+				year: 1920,
+				month: 0,
+				day: 0,
+				hour: 0,
+				minute: 0,
+				second: 0,
+				// 起始时间
+				startYear: 1920,
+				startMonth: 1,
+				startDay: 1,
+				startHour: 0,
+				startMinute: 0,
+				startSecond: 0,
+				// 结束时间
+				endYear: 2120,
+				endMonth: 12,
+				endDay: 31,
+				endHour: 23,
+				endMinute: 59,
+				endSecond: 59,
+			}
+		},
+		props: {
+			type: {
+				type: String,
+				default: 'datetime'
+			},
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			modelValue: {
+				type: [String, Number],
+				default: ''
+			},
+			start: {
+				type: [Number, String],
+				default: ''
+			},
+			end: {
+				type: [Number, String],
+				default: ''
+			},
+			returnType: {
+				type: String,
+				default: 'string'
+			},
+			disabled: {
+				type: [Boolean, String],
+				default: false
+			},
+			border: {
+				type: [Boolean, String],
+				default: true
+			},
+			hideSecond: {
+				type: [Boolean, String],
+				default: false
+			}
+		},
+		watch: {
+			// #ifndef VUE3
+			value: {
+				handler(newVal, oldVal) {
+					if (newVal) {
+						this.parseValue(this.fixIosDateFormat(newVal)) //兼容 iOS、safari 日期格式
+						this.initTime(false)
+					} else {
+						this.time = ''
+						this.parseValue(Date.now())
+					}
+				},
+				immediate: true
+			},
+			// #endif
+			// #ifdef VUE3
+			modelValue: {
+				handler(newVal, oldVal) {
+					if (newVal) {
+						this.parseValue(this.fixIosDateFormat(newVal)) //兼容 iOS、safari 日期格式
+						this.initTime(false)
+					} else {
+						this.time = ''
+						this.parseValue(Date.now())
+					}
+				},
+				immediate: true
+			},
+			// #endif
+			type: {
+				handler(newValue) {
+					if (newValue === 'date') {
+						this.dateShow = true
+						this.timeShow = false
+						this.title = '日期'
+					} else if (newValue === 'time') {
+						this.dateShow = false
+						this.timeShow = true
+						this.title = '时间'
+					} else {
+						this.dateShow = true
+						this.timeShow = true
+						this.title = '日期和时间'
+					}
+				},
+				immediate: true
+			},
+			start: {
+				handler(newVal) {
+					this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'start') //兼容 iOS、safari 日期格式
+				},
+				immediate: true
+			},
+			end: {
+				handler(newVal) {
+					this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'end') //兼容 iOS、safari 日期格式
+				},
+				immediate: true
+			},
+
+			// 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项
+			months(newVal) {
+				this.checkValue('month', this.month, newVal)
+			},
+			days(newVal) {
+				this.checkValue('day', this.day, newVal)
+			},
+			hours(newVal) {
+				this.checkValue('hour', this.hour, newVal)
+			},
+			minutes(newVal) {
+				this.checkValue('minute', this.minute, newVal)
+			},
+			seconds(newVal) {
+				this.checkValue('second', this.second, newVal)
+			}
+		},
+		computed: {
+			// 当前年、月、日、时、分、秒选择范围
+			years() {
+				return this.getCurrentRange('year')
+			},
+
+			months() {
+				return this.getCurrentRange('month')
+			},
+
+			days() {
+				return this.getCurrentRange('day')
+			},
+
+			hours() {
+				return this.getCurrentRange('hour')
+			},
+
+			minutes() {
+				return this.getCurrentRange('minute')
+			},
+
+			seconds() {
+				return this.getCurrentRange('second')
+			},
+
+			// picker 当前值数组
+			ymd() {
+				return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay]
+			},
+			hms() {
+				return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond]
+			},
+
+			// 当前 date 是 start
+			currentDateIsStart() {
+				return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay
+			},
+
+			// 当前 date 是 end
+			currentDateIsEnd() {
+				return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay
+			},
+
+			// 当前年、月、日、时、分、秒的最小值和最大值
+			minYear() {
+				return this.startYear
+			},
+			maxYear() {
+				return this.endYear
+			},
+			minMonth() {
+				if (this.year === this.startYear) {
+					return this.startMonth
+				} else {
+					return 1
+				}
+			},
+			maxMonth() {
+				if (this.year === this.endYear) {
+					return this.endMonth
+				} else {
+					return 12
+				}
+			},
+			minDay() {
+				if (this.year === this.startYear && this.month === this.startMonth) {
+					return this.startDay
+				} else {
+					return 1
+				}
+			},
+			maxDay() {
+				if (this.year === this.endYear && this.month === this.endMonth) {
+					return this.endDay
+				} else {
+					return this.daysInMonth(this.year, this.month)
+				}
+			},
+			minHour() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart) {
+						return this.startHour
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					return this.startHour
+				}
+			},
+			maxHour() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd) {
+						return this.endHour
+					} else {
+						return 23
+					}
+				}
+				if (this.type === 'time') {
+					return this.endHour
+				}
+			},
+			minMinute() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart && this.hour === this.startHour) {
+						return this.startMinute
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.startHour) {
+						return this.startMinute
+					} else {
+						return 0
+					}
+				}
+			},
+			maxMinute() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd && this.hour === this.endHour) {
+						return this.endMinute
+					} else {
+						return 59
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.endHour) {
+						return this.endMinute
+					} else {
+						return 59
+					}
+				}
+			},
+			minSecond() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) {
+						return this.startSecond
+					} else {
+						return 0
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.startHour && this.minute === this.startMinute) {
+						return this.startSecond
+					} else {
+						return 0
+					}
+				}
+			},
+			maxSecond() {
+				if (this.type === 'datetime') {
+					if (this.currentDateIsEnd && this.hour === this.endHour && this.minute === this.endMinute) {
+						return this.endSecond
+					} else {
+						return 59
+					}
+				}
+				if (this.type === 'time') {
+					if (this.hour === this.endHour && this.minute === this.endMinute) {
+						return this.endSecond
+					} else {
+						return 59
+					}
+				}
+			},
+
+			/**
+			 * for i18n
+			 */
+			selectTimeText() {
+				return t("uni-datetime-picker.selectTime")
+			},
+			okText() {
+				return t("uni-datetime-picker.ok")
+			},
+			clearText() {
+				return t("uni-datetime-picker.clear")
+			},
+			cancelText() {
+				return t("uni-datetime-picker.cancel")
+			}
+		},
+
+		mounted() {
+			// #ifdef APP-NVUE
+			const res = uni.getSystemInfoSync();
+			this.fixNvueBug = {
+				top: res.windowHeight / 2,
+				left: res.windowWidth / 2
+			}
+			// #endif
+		},
+
+		methods: {
+			/**
+			 * @param {Object} item
+			 * 小于 10 在前面加个 0
+			 */
+
+			lessThanTen(item) {
+				return item < 10 ? '0' + item : item
+			},
+
+			/**
+			 * 解析时分秒字符串,例如:00:00:00
+			 * @param {String} timeString
+			 */
+			parseTimeType(timeString) {
+				if (timeString) {
+					let timeArr = timeString.split(':')
+					this.hour = Number(timeArr[0])
+					this.minute = Number(timeArr[1])
+					this.second = Number(timeArr[2])
+				}
+			},
+
+			/**
+			 * 解析选择器初始值,类型可以是字符串、时间戳,例如:2000-10-02、'08:30:00'、 1610695109000
+			 * @param {String | Number} datetime
+			 */
+			initPickerValue(datetime) {
+				let defaultValue = null
+				if (datetime) {
+					defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end)
+				} else {
+					defaultValue = Date.now()
+					defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end)
+				}
+				this.parseValue(defaultValue)
+			},
+
+			/**
+			 * 初始值规则:
+			 * - 用户设置初始值 value
+			 * 	- 设置了起始时间 start、终止时间 end,并 start < value < end,初始值为 value, 否则初始值为 start
+			 * 	- 只设置了起始时间 start,并 start < value,初始值为 value,否则初始值为 start
+			 * 	- 只设置了终止时间 end,并 value < end,初始值为 value,否则初始值为 end
+			 * 	- 无起始终止时间,则初始值为 value
+			 * - 无初始值 value,则初始值为当前本地时间 Date.now()
+			 * @param {Object} value
+			 * @param {Object} dateBase
+			 */
+			compareValueWithStartAndEnd(value, start, end) {
+				let winner = null
+				value = this.superTimeStamp(value)
+				start = this.superTimeStamp(start)
+				end = this.superTimeStamp(end)
+
+				if (start && end) {
+					if (value < start) {
+						winner = new Date(start)
+					} else if (value > end) {
+						winner = new Date(end)
+					} else {
+						winner = new Date(value)
+					}
+				} else if (start && !end) {
+					winner = start <= value ? new Date(value) : new Date(start)
+				} else if (!start && end) {
+					winner = value <= end ? new Date(value) : new Date(end)
+				} else {
+					winner = new Date(value)
+				}
+
+				return winner
+			},
+
+			/**
+			 * 转换为可比较的时间戳,接受日期、时分秒、时间戳
+			 * @param {Object} value
+			 */
+			superTimeStamp(value) {
+				let dateBase = ''
+				if (this.type === 'time' && value && typeof value === 'string') {
+					const now = new Date()
+					const year = now.getFullYear()
+					const month = now.getMonth() + 1
+					const day = now.getDate()
+					dateBase = year + '/' + month + '/' + day + ' '
+				}
+				if (Number(value) && typeof value !== NaN) {
+					value = parseInt(value)
+					dateBase = 0
+				}
+				return this.createTimeStamp(dateBase + value)
+			},
+
+			/**
+			 * 解析默认值 value,字符串、时间戳
+			 * @param {Object} defaultTime
+			 */
+			parseValue(value) {
+				if (!value) {
+					return
+				}
+				if (this.type === 'time' && typeof value === "string") {
+					this.parseTimeType(value)
+				} else {
+					let defaultDate = null
+					defaultDate = new Date(value)
+					if (this.type !== 'time') {
+						this.year = defaultDate.getFullYear()
+						this.month = defaultDate.getMonth() + 1
+						this.day = defaultDate.getDate()
+					}
+					if (this.type !== 'date') {
+						this.hour = defaultDate.getHours()
+						this.minute = defaultDate.getMinutes()
+						this.second = defaultDate.getSeconds()
+					}
+				}
+				if (this.hideSecond) {
+					this.second = 0
+				}
+			},
+
+			/**
+			 * 解析可选择时间范围 start、end,年月日字符串、时间戳
+			 * @param {Object} defaultTime
+			 */
+			parseDatetimeRange(point, pointType) {
+				// 时间为空,则重置为初始值
+				if (!point) {
+					if (pointType === 'start') {
+						this.startYear = 1920
+						this.startMonth = 1
+						this.startDay = 1
+						this.startHour = 0
+						this.startMinute = 0
+						this.startSecond = 0
+					}
+					if (pointType === 'end') {
+						this.endYear = 2120
+						this.endMonth = 12
+						this.endDay = 31
+						this.endHour = 23
+						this.endMinute = 59
+						this.endSecond = 59
+					}
+					return
+				}
+				if (this.type === 'time') {
+					const pointArr = point.split(':')
+					this[pointType + 'Hour'] = Number(pointArr[0])
+					this[pointType + 'Minute'] = Number(pointArr[1])
+					this[pointType + 'Second'] = Number(pointArr[2])
+				} else {
+					if (!point) {
+						pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60
+						return
+					}
+					if (Number(point) && Number(point) !== NaN) {
+						point = parseInt(point)
+					}
+					// datetime 的 end 没有时分秒, 则不限制
+					const hasTime = /[0-9]:[0-9]/
+					if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test(
+							point)) {
+						point = point + ' 23:59:59'
+					}
+					const pointDate = new Date(point)
+					this[pointType + 'Year'] = pointDate.getFullYear()
+					this[pointType + 'Month'] = pointDate.getMonth() + 1
+					this[pointType + 'Day'] = pointDate.getDate()
+					if (this.type === 'datetime') {
+						this[pointType + 'Hour'] = pointDate.getHours()
+						this[pointType + 'Minute'] = pointDate.getMinutes()
+						this[pointType + 'Second'] = pointDate.getSeconds()
+					}
+				}
+			},
+
+			// 获取 年、月、日、时、分、秒 当前可选范围
+			getCurrentRange(value) {
+				const range = []
+				for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) {
+					range.push(i)
+				}
+				return range
+			},
+
+			// 字符串首字母大写
+			capitalize(str) {
+				return str.charAt(0).toUpperCase() + str.slice(1)
+			},
+
+			// 检查当前值是否在范围内,不在则当前值重置为可选范围第一项
+			checkValue(name, value, values) {
+				if (values.indexOf(value) === -1) {
+					this[name] = values[0]
+				}
+			},
+
+			// 每个月的实际天数
+			daysInMonth(year, month) { // Use 1 for January, 2 for February, etc.
+				return new Date(year, month, 0).getDate();
+			},
+
+			//兼容 iOS、safari 日期格式
+			fixIosDateFormat(value) {
+				if (typeof value === 'string') {
+					value = value.replace(/-/g, '/')
+				}
+				return value
+			},
+
+			/**
+			 * 生成时间戳
+			 * @param {Object} time
+			 */
+			createTimeStamp(time) {
+				if (!time) return
+				if (typeof time === "number") {
+					return time
+				} else {
+					time = time.replace(/-/g, '/')
+					if (this.type === 'date') {
+						time = time + ' ' + '00:00:00'
+					}
+					return Date.parse(time)
+				}
+			},
+
+			/**
+			 * 生成日期或时间的字符串
+			 */
+			createDomSting() {
+				const yymmdd = this.year +
+					'-' +
+					this.lessThanTen(this.month) +
+					'-' +
+					this.lessThanTen(this.day)
+
+				let hhmmss = this.lessThanTen(this.hour) +
+					':' +
+					this.lessThanTen(this.minute)
+
+				if (!this.hideSecond) {
+					hhmmss = hhmmss + ':' + this.lessThanTen(this.second)
+				}
+
+				if (this.type === 'date') {
+					return yymmdd
+				} else if (this.type === 'time') {
+					return hhmmss
+				} else {
+					return yymmdd + ' ' + hhmmss
+				}
+			},
+
+			/**
+			 * 初始化返回值,并抛出 change 事件
+			 */
+			initTime(emit = true) {
+				this.time = this.createDomSting()
+				if (!emit) return
+				if (this.returnType === 'timestamp' && this.type !== 'time') {
+					this.$emit('change', this.createTimeStamp(this.time))
+					this.$emit('input', this.createTimeStamp(this.time))
+					this.$emit('update:modelValue', this.createTimeStamp(this.time))
+				} else {
+					this.$emit('change', this.time)
+					this.$emit('input', this.time)
+					this.$emit('update:modelValue', this.time)
+				}
+			},
+
+			/**
+			 * 用户选择日期或时间更新 data
+			 * @param {Object} e
+			 */
+			bindDateChange(e) {
+				const val = e.detail.value
+				this.year = this.years[val[0]]
+				this.month = this.months[val[1]]
+				this.day = this.days[val[2]]
+			},
+			bindTimeChange(e) {
+				const val = e.detail.value
+				this.hour = this.hours[val[0]]
+				this.minute = this.minutes[val[1]]
+				this.second = this.seconds[val[2]]
+			},
+
+			/**
+			 * 初始化弹出层
+			 */
+			initTimePicker() {
+				if (this.disabled) return
+				const value = this.fixIosDateFormat(this.value)
+				this.initPickerValue(value)
+				this.visible = !this.visible
+			},
+
+			/**
+			 * 触发或关闭弹框
+			 */
+			tiggerTimePicker(e) {
+				this.visible = !this.visible
+			},
+
+			/**
+			 * 用户点击“清空”按钮,清空当前值
+			 */
+			clearTime() {
+				this.time = ''
+				this.$emit('change', this.time)
+				this.$emit('input', this.time)
+				this.$emit('update:modelValue', this.time)
+				this.tiggerTimePicker()
+			},
+
+			/**
+			 * 用户点击“确定”按钮
+			 */
+			setTime() {
+				this.initTime()
+				this.tiggerTimePicker()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	$uni-primary: #007aff !default;
+
+	.uni-datetime-picker {
+		/* #ifndef APP-NVUE */
+		/* width: 100%; */
+		/* #endif */
+	}
+
+	.uni-datetime-picker-view {
+		height: 130px;
+		width: 270px;
+		/* #ifndef APP-NVUE */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-item {
+		height: 50px;
+		line-height: 50px;
+		text-align: center;
+		font-size: 14px;
+	}
+
+	.uni-datetime-picker-btn {
+		margin-top: 60px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		cursor: pointer;
+		/* #endif */
+		flex-direction: row;
+		justify-content: space-between;
+	}
+
+	.uni-datetime-picker-btn-text {
+		font-size: 14px;
+		color: $uni-primary;
+	}
+
+	.uni-datetime-picker-btn-group {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.uni-datetime-picker-cancel {
+		margin-right: 30px;
+	}
+
+	.uni-datetime-picker-mask {
+		position: fixed;
+		bottom: 0px;
+		top: 0px;
+		left: 0px;
+		right: 0px;
+		background-color: rgba(0, 0, 0, 0.4);
+		transition-duration: 0.3s;
+		z-index: 998;
+	}
+
+	.uni-datetime-picker-popup {
+		border-radius: 8px;
+		padding: 30px;
+		width: 270px;
+		/* #ifdef APP-NVUE */
+		height: 500px;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 330px;
+		/* #endif */
+		background-color: #fff;
+		position: fixed;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		transition-duration: 0.3s;
+		z-index: 999;
+	}
+
+	.fix-nvue-height {
+		/* #ifdef APP-NVUE */
+		height: 330px;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-time {
+		color: grey;
+	}
+
+	.uni-datetime-picker-column {
+		height: 50px;
+	}
+
+	.uni-datetime-picker-timebox {
+
+		border: 1px solid #E5E5E5;
+		border-radius: 5px;
+		padding: 7px 10px;
+		/* #ifndef APP-NVUE */
+		box-sizing: border-box;
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-timebox-pointer {
+		/* #ifndef APP-NVUE */
+		cursor: pointer;
+		/* #endif */
+	}
+
+
+	.uni-datetime-picker-disabled {
+		opacity: 0.4;
+		/* #ifdef H5 */
+		cursor: not-allowed !important;
+		/* #endif */
+	}
+
+	.uni-datetime-picker-text {
+		font-size: 14px;
+		line-height: 50px
+	}
+
+	.uni-datetime-picker-sign {
+		position: absolute;
+		top: 53px;
+		/* 减掉 10px 的元素高度,兼容nvue */
+		color: #999;
+		/* #ifdef APP-NVUE */
+		font-size: 16px;
+		/* #endif */
+	}
+
+	.sign-left {
+		left: 86px;
+	}
+
+	.sign-right {
+		right: 86px;
+	}
+
+	.sign-center {
+		left: 135px;
+	}
+
+	.uni-datetime-picker__container-box {
+		position: relative;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		margin-top: 40px;
+	}
+
+	.time-hide-second {
+		width: 180px;
+	}
+</style>

文件差異過大導致無法顯示
+ 1011 - 0
miniapp/components/uni-datetime-picker/uni-datetime-picker.vue


+ 410 - 0
miniapp/components/uni-datetime-picker/util.js

@@ -0,0 +1,410 @@
+class Calendar {
+	constructor({
+		date,
+		selected,
+		startDate,
+		endDate,
+		range,
+		// multipleStatus
+	} = {}) {
+		// 当前日期
+		this.date = this.getDate(new Date()) // 当前初入日期
+		// 打点信息
+		this.selected = selected || [];
+		// 范围开始
+		this.startDate = startDate
+		// 范围结束
+		this.endDate = endDate
+		this.range = range
+		// 多选状态
+		this.cleanMultipleStatus()
+		// 每周日期
+		this.weeks = {}
+		// this._getWeek(this.date.fullDate)
+		// this.multipleStatus = multipleStatus
+		this.lastHover = false
+	}
+	/**
+	 * 设置日期
+	 * @param {Object} date
+	 */
+	setDate(date) {
+		this.selectDate = this.getDate(date)
+		this._getWeek(this.selectDate.fullDate)
+	}
+
+	/**
+	 * 清理多选状态
+	 */
+	cleanMultipleStatus() {
+		this.multipleStatus = {
+			before: '',
+			after: '',
+			data: []
+		}
+	}
+
+	/**
+	 * 重置开始日期
+	 */
+	resetSatrtDate(startDate) {
+		// 范围开始
+		this.startDate = startDate
+
+	}
+
+	/**
+	 * 重置结束日期
+	 */
+	resetEndDate(endDate) {
+		// 范围结束
+		this.endDate = endDate
+	}
+
+	/**
+	 * 获取任意时间
+	 */
+	getDate(date, AddDayCount = 0, str = 'day') {
+		if (!date) {
+			date = new Date()
+		}
+		if (typeof date !== 'object') {
+			date = date.replace(/-/g, '/')
+		}
+		const dd = new Date(date)
+		switch (str) {
+			case 'day':
+				dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期
+				break
+			case 'month':
+				if (dd.getDate() === 31) {
+					dd.setDate(dd.getDate() + AddDayCount)
+				} else {
+					dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期
+				}
+				break
+			case 'year':
+				dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期
+				break
+		}
+		const y = dd.getFullYear()
+		const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0
+		const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0
+		return {
+			fullDate: y + '-' + m + '-' + d,
+			year: y,
+			month: m,
+			date: d,
+			day: dd.getDay()
+		}
+	}
+
+
+	/**
+	 * 获取上月剩余天数
+	 */
+	_getLastMonthDays(firstDay, full) {
+		let dateArr = []
+		for (let i = firstDay; i > 0; i--) {
+			const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate()
+			dateArr.push({
+				date: beforeDate,
+				month: full.month - 1,
+				disable: true
+			})
+		}
+		return dateArr
+	}
+	/**
+	 * 获取本月天数
+	 */
+	_currentMonthDys(dateData, full) {
+		let dateArr = []
+		let fullDate = this.date.fullDate
+		for (let i = 1; i <= dateData; i++) {
+			let isinfo = false
+			let nowDate = full.year + '-' + (full.month < 10 ?
+				full.month : full.month) + '-' + (i < 10 ?
+				'0' + i : i)
+			// 是否今天
+			let isDay = fullDate === nowDate
+			// 获取打点信息
+			let info = this.selected && this.selected.find((item) => {
+				if (this.dateEqual(nowDate, item.date)) {
+					return item
+				}
+			})
+
+			// 日期禁用
+			let disableBefore = true
+			let disableAfter = true
+			if (this.startDate) {
+				// let dateCompBefore = this.dateCompare(this.startDate, fullDate)
+				// disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate)
+				disableBefore = this.dateCompare(this.startDate, nowDate)
+			}
+
+			if (this.endDate) {
+				// let dateCompAfter = this.dateCompare(fullDate, this.endDate)
+				// disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate)
+				disableAfter = this.dateCompare(nowDate, this.endDate)
+			}
+			let multiples = this.multipleStatus.data
+			let checked = false
+			let multiplesStatus = -1
+			if (this.range) {
+				if (multiples) {
+					multiplesStatus = multiples.findIndex((item) => {
+						return this.dateEqual(item, nowDate)
+					})
+				}
+				if (multiplesStatus !== -1) {
+					checked = true
+				}
+			}
+			let data = {
+				fullDate: nowDate,
+				year: full.year,
+				date: i,
+				multiple: this.range ? checked : false,
+				beforeMultiple: this.isLogicBefore(nowDate, this.multipleStatus.before, this.multipleStatus.after),
+				afterMultiple: this.isLogicAfter(nowDate, this.multipleStatus.before, this.multipleStatus.after),
+				month: full.month,
+				disable: !(disableBefore && disableAfter),
+				isDay,
+				userChecked: false
+			}
+			if (info) {
+				data.extraInfo = info
+			}
+
+			dateArr.push(data)
+		}
+		return dateArr
+	}
+	/**
+	 * 获取下月天数
+	 */
+	_getNextMonthDays(surplus, full) {
+		let dateArr = []
+		for (let i = 1; i < surplus + 1; i++) {
+			dateArr.push({
+				date: i,
+				month: Number(full.month) + 1,
+				disable: true
+			})
+		}
+		return dateArr
+	}
+
+	/**
+	 * 获取当前日期详情
+	 * @param {Object} date
+	 */
+	getInfo(date) {
+		if (!date) {
+			date = new Date()
+		}
+		const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate)
+		return dateInfo
+	}
+
+	/**
+	 * 比较时间大小
+	 */
+	dateCompare(startDate, endDate) {
+		// 计算截止时间
+		startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
+		// 计算详细项的截止时间
+		endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
+		if (startDate <= endDate) {
+			return true
+		} else {
+			return false
+		}
+	}
+
+	/**
+	 * 比较时间是否相等
+	 */
+	dateEqual(before, after) {
+		// 计算截止时间
+		before = new Date(before.replace('-', '/').replace('-', '/'))
+		// 计算详细项的截止时间
+		after = new Date(after.replace('-', '/').replace('-', '/'))
+		if (before.getTime() - after.getTime() === 0) {
+			return true
+		} else {
+			return false
+		}
+	}
+
+	/**
+	 *  比较真实起始日期
+	 */
+
+	isLogicBefore(currentDay, before, after) {
+		let logicBefore = before
+		if (before && after) {
+			logicBefore = this.dateCompare(before, after) ? before : after
+		}
+		return this.dateEqual(logicBefore, currentDay)
+	}
+
+	isLogicAfter(currentDay, before, after) {
+		let logicAfter = after
+		if (before && after) {
+			logicAfter = this.dateCompare(before, after) ? after : before
+		}
+		return this.dateEqual(logicAfter, currentDay)
+	}
+
+	/**
+	 * 获取日期范围内所有日期
+	 * @param {Object} begin
+	 * @param {Object} end
+	 */
+	geDateAll(begin, end) {
+		var arr = []
+		var ab = begin.split('-')
+		var ae = end.split('-')
+		var db = new Date()
+		db.setFullYear(ab[0], ab[1] - 1, ab[2])
+		var de = new Date()
+		de.setFullYear(ae[0], ae[1] - 1, ae[2])
+		var unixDb = db.getTime() - 24 * 60 * 60 * 1000
+		var unixDe = de.getTime() - 24 * 60 * 60 * 1000
+		for (var k = unixDb; k <= unixDe;) {
+			k = k + 24 * 60 * 60 * 1000
+			arr.push(this.getDate(new Date(parseInt(k))).fullDate)
+		}
+		return arr
+	}
+
+	/**
+	 *  获取多选状态
+	 */
+	setMultiple(fullDate) {
+		let {
+			before,
+			after
+		} = this.multipleStatus
+		if (!this.range) return
+		if (before && after) {
+			if (!this.lastHover) {
+				this.lastHover = true
+				return
+			}
+			this.multipleStatus.before = fullDate
+			this.multipleStatus.after = ''
+			this.multipleStatus.data = []
+			this.multipleStatus.fulldate = ''
+			this.lastHover = false
+		} else {
+			if (!before) {
+				this.multipleStatus.before = fullDate
+				this.lastHover = false
+			} else {
+				this.multipleStatus.after = fullDate
+				if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
+					this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus
+						.after);
+				} else {
+					this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus
+						.before);
+				}
+				this.lastHover = true
+			}
+		}
+		this._getWeek(fullDate)
+	}
+
+	/**
+	 *  鼠标 hover 更新多选状态
+	 */
+	setHoverMultiple(fullDate) {
+		let {
+			before,
+			after
+		} = this.multipleStatus
+
+		if (!this.range) return
+		if (this.lastHover) return
+
+		if (!before) {
+			this.multipleStatus.before = fullDate
+		} else {
+			this.multipleStatus.after = fullDate
+			if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
+				this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after);
+			} else {
+				this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before);
+			}
+		}
+		this._getWeek(fullDate)
+	}
+
+	/**
+	 * 更新默认值多选状态
+	 */
+	setDefaultMultiple(before, after) {
+		this.multipleStatus.before = before
+		this.multipleStatus.after = after
+		if (before && after) {
+			if (this.dateCompare(before, after)) {
+				this.multipleStatus.data = this.geDateAll(before, after);
+				this._getWeek(after)
+			} else {
+				this.multipleStatus.data = this.geDateAll(after, before);
+				this._getWeek(before)
+			}
+		}
+	}
+
+	/**
+	 * 获取每周数据
+	 * @param {Object} dateData
+	 */
+	_getWeek(dateData) {
+		const {
+			fullDate,
+			year,
+			month,
+			date,
+			day
+		} = this.getDate(dateData)
+		let firstDay = new Date(year, month - 1, 1).getDay()
+		let currentDay = new Date(year, month, 0).getDate()
+		let dates = {
+			lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天
+			currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数
+			nextMonthDays: [], // 下个月开始几天
+			weeks: []
+		}
+		let canlender = []
+		const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length)
+		dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData))
+		canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays)
+		let weeks = {}
+		// 拼接数组  上个月开始几天 + 本月天数+ 下个月开始几天
+		for (let i = 0; i < canlender.length; i++) {
+			if (i % 7 === 0) {
+				weeks[parseInt(i / 7)] = new Array(7)
+			}
+			weeks[parseInt(i / 7)][i % 7] = canlender[i]
+		}
+		this.canlender = canlender
+		this.weeks = weeks
+	}
+
+	//静态方法
+	// static init(date) {
+	// 	if (!this.instance) {
+	// 		this.instance = new Calendar(date);
+	// 	}
+	// 	return this.instance;
+	// }
+}
+
+
+export default Calendar

+ 145 - 0
miniapp/http/api.js

@@ -0,0 +1,145 @@
+import {clientId, clientSecret, successCode, invalidCode, tenantId} from '@/common/setting'
+import {options} from '@/http/config.js'
+import {Base64} from '@/utils/base64.js'
+import {throttle} from '@/utils/util.js'
+import Request from '@/utils/luch-request/index.js'
+
+const http = new Request(options);
+const ignorePaths = ['mb/login/miniapp', 'mb/login/refresh']//"blade-auth/oauth/token";//更新token的api不需要检查token
+const preExpire = 5 //token超时之前几分钟提前刷新token
+
+const wxLogin = () => {//微信小程序获取wxcode静默登录,并获取token
+	uni.hideLoading()
+	console.log('api.js -- wxLogin')
+	uni.reLaunch({
+		url: '/pages/index?f=1',//强制重新登录
+	})
+}
+// var throttleHandle = false
+const reLogin = (isShowLoading) => {//重新登录
+	if (isShowLoading) {
+		uni.showLoading({
+			title: '未登录,即将跳至登录页',
+			mask: true,
+		})
+	}
+	// if (!throttleHandle) {
+	// 	throttleHandle = throttle(()=>{
+	// 		// throttleHandle = null
+	// 		wxLogin()
+	// 	}, 1500)
+	// 	throttleHandle()
+	// }
+	// throttle(wxLogin, 1500)
+	uni.$u.throttle(wxLogin, 1500)
+}
+const isGetTokenPath = (url) => {//判断是否请求token或刷新token的url
+	// for (let i = 0; i < ignorePaths.length; i++) {
+	// 	if (url.indexOf(ignorePaths[i]) >= 0) {
+	// 		return true
+	// 	}
+	// }
+	return true
+}
+
+http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+	// 假设有token值需要在头部需要携带
+	let accessToken = uni.getStorageSync('accessToken');
+	if (accessToken) {
+		config.header['Blade-Auth'] = 'bearer ' + accessToken;
+	}
+	config.header['Tenant-Id'] = tenantId;//添加一个默认租户ID
+	// 客户端认证参数
+	config.header['Authorization'] = 'Basic ' + Base64.encode(clientId + ':' + clientSecret);
+	
+	//token超时前5分钟重新默认静态登录,3秒内只允许一次静态登录
+	let expireTime = uni.getStorageSync('tokenExpire') || 0
+	if(!isGetTokenPath(config.url) && (!accessToken || (new Date().getTime() > expireTime - preExpire*60*1000)) ) {
+		/* #ifdef MP-WEIXIN */
+		console.log('token超时前' + preExpire + '分钟重新默认静态登录')
+		console.log('new Date().getTime()=' + new Date().getTime())
+		console.log('expireTime=' + expireTime)
+		reLogin()//只要有请求发现token不存在或快超时了就去静默获取一次token
+		/* #endif */
+		return Promise.reject(config)
+	}
+	
+	if(config.restParams) {//额外参数处理restParams
+		let restPath = ""
+		for(var key in config.restParams){
+		  restPath = "/" + key + "/" + config.restParams[key]
+		}
+		if(restPath.length > 0) {
+			config.url = config.url + restPath
+		}
+	}
+	
+	if (config.custom && config.custom.debug ){
+		//console.log("ggg debug",config.custom)
+		config.baseURL = ""
+		config.url = config.custom.url
+	}
+	
+	if (config.hasOwnProperty('custom') && config.custom.hasOwnProperty('loading') && config.custom.loading == false ) {
+		//
+	} else {
+		uni.showLoading({
+			title:"请稍后...",
+			mask: true
+		})
+	}
+	
+	//console.log("extra",config)
+	// config.data = config.data || {};
+	// config.data.pf = uni.getSystemInfoSync().platform;
+	// config.data.sys = uni.getSystemInfoSync().system;
+
+	// 演示custom 用处
+	// if (config.custom.auth) {
+	//   config.header.token = 'token'
+	// }
+	// if (config.custom.loading) {
+	//  uni.showLoading()
+	// }
+	/**
+	 /* 演示
+	 if (!token) { // 如果token不存在,return Promise.reject(config) 会取消本次请求
+	    return Promise.reject(config)
+	  }
+	 **/
+	return config
+}, config => { // 可使用async await 做异步操作
+	return Promise.reject(config)
+})
+http.interceptors.response.use((response) => {
+	// 若有数据返回则通过
+	uni.hideLoading()
+	if (response.data.access_token || response.data.key) {
+		return response.data
+	}
+    // 服务端返回的状态码等于401(请求未授权),则跳到登录页
+	if (response.data.code == invalidCode) {
+		console.log('response.success ' + invalidCode)
+		reLogin()
+		return Promise.reject(response);
+    }
+	// 服务端返回的状态码不等于200,则reject()
+	if (response.data.code !== successCode) {
+		return Promise.reject(response);
+	}
+	return response.data;
+}, (response) => {
+    // 服务端返回的状态码等于401(请求未授权),则跳到登录页
+    if (response.data.code == invalidCode) {
+		console.log('response.error ' + invalidCode)
+		reLogin(true)
+    } else {
+		/*  对响应错误做点什么 (statusCode !== successCode)*/
+		uni.showToast({
+			title: response.data.msg,
+			icon: 'none'
+		})
+	}
+	return Promise.reject(response)
+})
+export default http;

+ 46 - 0
miniapp/http/config.js

@@ -0,0 +1,46 @@
+import {
+	devUrl,
+	prodUrl,
+	contentType
+} from '@/common/setting'
+
+var options = {
+	baseURL: process.env.NODE_ENV === 'development' ? devUrl : prodUrl,
+	header: {
+		'Content-Type': contentType
+	},
+	method: 'POST',
+	dataType: 'json',
+	// #ifndef MP-ALIPAY || APP-PLUS
+	responseType: 'text',
+	// #endif
+	// 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部)
+	custom: {}, // 全局自定义参数默认值
+	
+	restParams: {}, //restfull方式的参数,拼成名/值这样的路径方式
+	// #ifdef MP-ALIPAY || MP-WEIXIN
+	timeout: 30000,
+	// #endif
+	// #ifdef APP-PLUS
+	sslVerify: true,
+	// #endif
+	// #ifdef H5
+	// 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+)
+	//withCredentials: false,
+	// #endif
+	// #ifdef APP-PLUS
+	firstIpv4: false, // DNS解析时优先使用ipv4 仅 App-Android 支持 (HBuilderX 2.8.0+)
+	// #endif
+	// 局部优先级高于全局,返回当前请求的task,options。请勿在此处修改options。非必填
+	// getTask: (task, options) => {
+	// 相当于设置了请求超时时间500ms
+	//   setTimeout(() => {
+	//     task.abort()
+	//   }, 500)
+	// },
+	// 全局自定义验证器。参数为statusCode 且必存在,不用判断空情况。
+	// validateStatus: (statusCode) => { // statusCode 必存在。此处示例为全局默认配置
+	// 	return statusCode >= 200 && statusCode < 300
+	// }
+};
+export { options };

+ 32 - 0
miniapp/http/install.js

@@ -0,0 +1,32 @@
+import {isMockData} from '@/common/setting.js'
+
+// 获取api目录所有js文件
+const files = require.context('@/api', false, /\.js$/)
+const mockFiles = require.context('@/api/mock', false, /\.js$/)
+
+// 此处第二个参数vm,就是我们在页面使用的this,可以通过vm获取vuex等操作
+const install = (Vue, vm) => {
+	// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api下(因为vm就是this,也即this.$u.api)
+	// 自动将所有api挂载到vm.$u.api中
+	vm.$u.api = {}
+	files.keys().forEach(key => {
+		const api = files(key).default
+		for (let item in api) {
+			vm.$u.api[item] = api[item]
+		}
+	})
+	
+	//如果有mock接口,则把mock接口覆盖原接口
+	if (isMockData) {
+		mockFiles.keys().forEach(key => {
+			const api = mockFiles(key).default
+			for (let item in api) {
+				vm.$u.api[item] = api[item]
+			}
+		})
+	}
+}
+
+export default {
+	install
+}

+ 34 - 0
miniapp/main.js

@@ -0,0 +1,34 @@
+import Vue from 'vue'
+import App from './App'
+import store from '@/store';
+
+Vue.config.productionTip = false;
+
+App.mpType = 'app';
+
+// 引入全局uView
+import uView from 'uview-ui'
+Vue.use(uView);
+
+// 引入vuex
+const vuexStore = require("@/store/$u.mixin.js");
+Vue.mixin(vuexStore);
+
+// 创建对象
+const app = new Vue({
+  store,
+  ...App
+});
+
+// 接口集中管理
+import httpInstall from '@/http/install.js'
+Vue.use(httpInstall, app)
+
+// 公共函数
+import globalFunc from '@/utils/func.js'
+Vue.use(globalFunc, app);
+
+import pageTheme from './components/page-theme/page-theme.vue'
+Vue.component('pageTheme', pageTheme)
+
+app.$mount()

+ 123 - 0
miniapp/manifest.json

@@ -0,0 +1,123 @@
+{
+    "name" : "粤菜",
+    "appid" : "__UNI__CDB6659",
+    "description" : "",
+    "versionName" : "1.0.6",
+    "versionCode" : 10006,
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        /** 音频自动播放的问题 */
+        "mediaPlaybackRequiresUserAction" : false,
+        "safearea" : {
+            "bottom" : {
+                "offset" : "none"
+            }
+        },
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {
+            "Share" : {}
+        },
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {
+                "UIBackgroundModes" : [ "audio" ]
+            },
+            /* SDK配置 */
+            "sdkConfigs" : {
+                "ad" : {},
+                "share" : {
+                    "weixin" : {
+                        "appid" : "wx4517e7bae4f224e5",
+                        "UniversalLinks" : ""
+                    }
+                },
+                "push" : {}
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wxcffc5049c0ccb9ef",
+        "setting" : {
+            "urlCheck" : false,
+            "postcss" : false,
+            "es6" : false,
+            "minified" : true
+        },
+        "usingComponents" : true,
+        "lazyCodeLoading" : "requiredComponents",
+        "permission" : {},
+        "requiredBackgroundModes" : [ "audio" ]
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "h5" : {
+        "template" : "template.h5.html",
+        "router" : {
+            "mode" : "hash",
+            "base" : "/"
+        },
+        "devServer" : {
+            "port" : 2888,
+            "disableHostCheck" : true, // 开启可以用自己的域名
+            "proxy" : {
+                "/api" : {
+                    "target" : "http://localhost:8826",
+                    "changeOrigin" : true,
+                    "secure" : false,
+                    "pathRewrite" : {
+                        "^/api" : ""
+                    }
+                }
+            },
+            "https" : false
+        }
+    },
+    "locale" : "zh-Hans"
+}

+ 13 - 0
miniapp/package.json

@@ -0,0 +1,13 @@
+{
+  "id": "yz-audio",
+  "name": "倍速播放的音频播放器yz-audio",
+  "version": "1.1.0",
+  "description": "支持音频的变速播放;界面中内置了三个可以自定义内容的文本区域,可以响应点击事件,也可以使用slot自",
+  "keywords": [
+    "音频播放器",
+    "变速"
+  ],
+  "dependencies": {
+    "uview-ui": "^2.0.36"
+  }
+}

+ 57 - 0
miniapp/pages.json

@@ -0,0 +1,57 @@
+{
+  "easycom": {
+    "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+  },
+  "subPackages": [],
+  // "requiredBackgroundModes": ["audio", "location"],
+  "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+   {
+        "path": "pages/yuecai/home/index",
+        "style": {
+          "navigationBarTitleText": "首页",
+          "enablePullDownRefresh": false
+        }
+    },{
+      "path": "pages/yuecai/dish/detail",
+      "style": {
+        "navigationBarTitleText": "菜品详情",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/index",
+      "style": {
+        "navigationBarTitleText": "粤菜文化",
+        "enablePullDownRefresh": false
+      }
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTitleText": "",
+    "navigationBarBackgroundColor": "@navBgColor",
+    "navigationBarTextStyle": "@navTxtStyle",
+    "backgroundColor": "@bgColor",
+    "backgroundTextStyle": "@bgTxtStyle",
+    "backgroundColorTop": "@bgColorTop",
+    "backgroundColorBottom": "@bgColorBottom"
+  },
+  "tabBar": {
+    "color": "@tabFontColor",
+    "selectedColor": "@tabSelectedColor",
+    "backgroundColor": "@tabBgColor",
+    "borderStyle": "@tabBorderStyle",
+    "list": [{
+      "iconPath": "@iconPath1",
+      "selectedIconPath": "@selectedIconPath1",
+      "text": "首页",
+      "pagePath": "pages/yuecai/home/index"
+    },{
+      "iconPath": "@iconPath2",
+      "selectedIconPath": "@selectedIconPath2",
+      "text": "菜品详情2",
+      "pagePath": "pages/yuecai/dish/detail"
+    }]
+  },
+  "darkmode": true,
+  "themeLocation": "theme.json"
+}

+ 45 - 0
miniapp/pages/components/card.vue

@@ -0,0 +1,45 @@
+<template>
+<view class="k-card-">
+	<!-- <view class="p-20 f-30 t-bold c-g-primary-l">{{title}}</view>
+	<u-cell-group>
+		<u-cell v-for="(item, index) in list" :key="index" :title="item.value" isLink/>
+	</u-cell-group> -->
+	<view class="f-36 t-bold m-t-20">{{title}}</view>
+	<view class="k-card-plain-p m-t-10" v-for="(item,index) in list" :key="index">
+		<u-cell is-link :border="false" :title="item.value" @click="clickCell(item)"/>
+	</view>
+</view>
+</template>
+
+<script>
+export default {
+	props: {
+		title: String,
+		list: Array,
+	},
+	data() {
+		return {
+		}
+	},
+	methods: {
+		clickCell(item) {
+			this.$emit("click", item)
+		}
+	},
+}
+</script>
+
+<style>
+.k-card {
+	display: flex;
+	flex-direction: column;
+	/* align-items: center; */
+	width: 100%;
+	/* padding-top: 20rpx; */
+	/* padding-bottom: 20rpx; */
+	/* margin-left: 5rpx; */
+	/* margin-right: 5rpx; */
+	border-radius: 10rpx;
+	overflow: hidden;
+}
+</style>

+ 69 - 0
miniapp/pages/components/dropdown.vue

@@ -0,0 +1,69 @@
+<template>
+<u-overlay :show="isShow" @click="isShow = false" opacity="0">
+	<view :class="'k-' + position" :style="[myStyle]">
+		<slot/>
+	</view>
+</u-overlay>
+</template>
+
+<script>
+export default {
+	props: {
+		top: {
+			type: [String, Number],
+			default: 0,
+		},
+		position: {
+			type: String,
+			default: 'right',//left
+		},
+		left: {
+			type: [String, Number],
+			default: 0,
+		},
+		right: {
+			type: [String, Number],
+			default: 0,
+		},
+	},
+	computed: {
+	    myStyle() {
+			return {
+				marginTop: this.$u.addUnit(this.top, 'px'),
+				marginLeft: this.$u.addUnit(this.left, 'px'),
+				marginRight: this.$u.addUnit(this.right, 'px'),
+			}
+		},
+	},
+	data() {
+		return {
+			isShow: false,
+		}
+	},
+	methods: {
+		show() {
+			this.isShow = true
+		},
+		hide() {
+			this.isShow = false
+		},
+	},
+}
+</script>
+
+<style>
+.k-top {
+	background-color: #333333;
+	width: fit-content;
+}
+.k-left {
+	display: flex;
+	flex-direction: row;
+	justify-content: flex-start;
+}
+.k-right {
+	display: flex;
+	flex-direction: row;
+	justify-content: flex-end;
+}
+</style>

+ 64 - 0
miniapp/pages/components/helper.vue

@@ -0,0 +1,64 @@
+<template>
+<u-overlay :show="step > 0" @click="showHelp" opacity="0.7">
+	<view class="width-100 height-100">
+		<view class="opacity-06 c-g-white p-a border-round-20" :style="[helpRectStyle]"></view>
+		<view class="p-a f-40 col-white t-bold width-100 padding-box" :style="[helpTextStyle]">{{helpText}}</view>
+	</view>
+</u-overlay>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			step: 0,//第几步,0则还未开始
+			helpRectStyle: {},
+			helpText: '',
+			helpTextStyle: {},
+			options: [],
+		}
+	},
+	methods: {
+		showHelp() {//显示帮助信息
+			this.step++//先往前走一步
+			if (this.step > this.options.length) {//到底了,就关闭
+				this.close()
+			} else {//还有,则继续显示
+				let option = this.options[this.step - 1]//取到配置
+				this.showHelpRect(option)//显示高亮框
+				this.showHelpText(option.text)//显示帮助文字
+			}
+		},
+		showHelpRect(option) {//显示高亮框
+			uni.createSelectorQuery().in(option.ref).select(option.selector).boundingClientRect(res => {
+				this.helpRectStyle = {
+					width: (res.width + 20) + 'px',
+					height: (res.height + 20) + 'px',
+					top: (res.top - 10) + 'px',
+					left: (res.left - 10) + 'px',
+				}
+				let top = option.text.length / 16 * 35//每16个字占一行,一行35个像素
+				top = top < 70 ? 70 : top //至少70个像素
+				this.helpTextStyle = {
+					top: (res.top - top) + 'px',
+				}
+			}).exec()
+		},
+		showHelpText(text) {//显示文字部分
+			this.helpText = text
+		},
+		
+		// ------- 对外的接口 --------
+		open(options) {
+			this.options = options
+			this.showHelp()
+		},
+		close() {
+			this.step = 0
+		},
+	},
+}
+</script>
+
+<style>
+</style>

+ 56 - 0
miniapp/pages/components/icon.vue

@@ -0,0 +1,56 @@
+<template>
+<view :style="[style]">
+	<u--image :src="imgSrc" :width="imgWidth" :height="imgHeight" @click="clickImg" 
+		:showMenuByLongpress="false"/>
+</view>
+</template>
+
+<script>
+export default {
+	props: {
+		img: String,
+		size: [Number,String],//如果有宽和高,则size失效
+		width: [Number,String],
+		height: [Number,String],
+		color: String,
+		left: [Number,String],
+		right: [Number,String],
+	},
+	computed: {
+		imgWidth() {
+			return parseInt(this.width || this.size) / 2
+		},
+		imgHeight() {
+			return parseInt(this.height || this.size) / 2
+		},
+		imgSrc() {
+			return '/static/images/icon/' + this.img + '.png'
+		},
+		style() {
+			let style = {}
+			if (this.color) {
+				style.backgroundColor = this.color
+			}
+			if (this.left) {
+				style.marginLeft = this.left + 'rpx'
+			}
+			if (this.right) {
+				style.marginRight = this.right + 'rpx'
+			}
+			return style
+		},
+	},
+	data() {
+		return {
+		}
+	},
+	methods: {
+		clickImg() {
+			this.$emit('click')
+		},
+	},
+}
+</script>
+
+<style>
+</style>

+ 137 - 0
miniapp/pages/components/list.vue

@@ -0,0 +1,137 @@
+<template>
+<view>
+	<u-empty v-if="!list || list.length == 0" marginTop="100"/>
+	<block v-else>
+		<u-swipe-action v-if="edit || del" ref="swipes">
+			<u-swipe-action-item v-for="(item,i) in list" :key="item.id" :options="swipes" @click="clickSwipe($event,item,i)">
+				<view :class="border ? 'border-b-i-m' + (i==0?' border-t-i-m':'') : ''">
+					<slot :item="item"/>
+				</view>
+			</u-swipe-action-item>
+		</u-swipe-action>
+		<view v-else v-for="(item,i) in list" :key="i" :class="border ? 'border-b-i-m' + (i==0?' border-t-i-m':'') : ''">
+			<slot :item="item"/>
+		</view>
+		<view class="p-10"><u-loadmore :status="page.status" line/></view>
+		<u-back-top :scroll-top="scrollTop" bottom="30" duration="300"/>
+	</block>
+</view>
+</template>
+
+<script>
+import {bizConst} from '@/api/biz/biz.js'
+
+export default {
+	props: {
+		func: String,
+		params: {
+			type: Object,
+			default() {
+				return {}
+			},
+		},
+		pageSize: {
+			type: Number,
+			default: bizConst.pageSize,
+		},
+		border: {//是否显示中间的分隔线
+			type: Boolean,
+			default: false,
+		},
+		edit: {//是否可以划出编辑按钮
+			type: Boolean,
+			default: false,
+		},
+		del: {//是否可以划出删除按钮
+			type: Boolean,
+			default: false,
+		},
+	},
+	computed: {
+		swipes() {
+			let swipes = []
+			if (this.edit) {
+				swipes.push({
+					text:'编辑', 
+					style: {
+						backgroundColor:'#4F8477',
+					},
+				})
+			}
+			if (this.del) {
+				swipes.push({
+					text:'删除', 
+					style: {
+						backgroundColor:'#DC545A',
+					},
+				})
+			}
+			console.log(swipes)
+			return swipes
+		},
+	},
+	watch: {
+		params: {
+			handler: function(newValue) {
+				console.log('watch:params=' + newValue)
+				this.refresh()
+			},
+			deep: true,
+		},
+	},
+	data() {
+		return {
+			list: [],
+			page: {
+				current: 0,
+				status: 'loadmore',//loading,nomore
+			},
+			scrollTop: 0, // 滚动条位置,距顶为0
+		}
+	},
+	methods: {
+		refresh(params) {//刷新
+			this.getDatas(false, params)
+		},
+		getDatas(isNextPage, params) {//翻页
+			if (this.page.status == 'loading') {//如果在加载过程中再次请求,那么不处理
+				return
+			}
+			if (!isNextPage) {//重新刷新
+				this.list = []
+				this.page.current = 0
+			} else if (this.page.status == 'nomore') {//请求下一页时,如果没数据了,直接退出
+				return
+			}
+			this.params = params || this.params
+			this.page.status = 'loading'
+			this.$u.api[this.func].call(this, this.page.current + 1, this.pageSize, this.params).then(res => {
+				this.list = this.list.concat(res.data.records)
+				this.page.current++ //加一页
+				this.page.status = this.page.current < res.data.pages ? 'loadmore' : 'nomore'
+				console.log('加载完第' + this.page.current + '页')
+			})
+		},
+		clickSwipe(e, item, index) {
+			if (e.index == 0) {//编辑
+				this.$emit("toEdit", item, index)
+			} else{//删除
+				this.$emit("toDel", item, index)
+			}
+			this.$refs.swipes.closeOther()//直接调组件的未公布的方法
+		},
+		delOne(index) {//删除前端数据
+			this.list.splice(index, 1)
+		},
+		scroll(scrollTop) {
+			this.scrollTop = scrollTop
+		},
+		reachBottom() {
+			this.getDatas(true)
+		},
+	},
+}
+</script>
+
+<style>
+</style>

+ 36 - 0
miniapp/pages/components/radio.vue

@@ -0,0 +1,36 @@
+<template>
+	<icon-comp :img="checkedValue ? 'checked' : 'unchecked'" size="32" @click.stop="clickRadio"/>
+</template>
+
+<script>
+import iconComp from './icon.vue'
+
+export default {
+	components: {iconComp},
+	props: {
+		checked: {//默认是否选中
+			type: Boolean,
+			default: false,
+		},
+	},
+	computed: {
+		checkedValue() {//要放到这里,这样才会和外部的数据同步
+			return this.checked
+		}
+	},
+	data() {
+		return {
+			// checkedValue: this.checked,
+		}
+	},
+	methods: {
+		clickRadio() {
+			// this.checked = !this.checked //不能修改prop,不然会报错
+			this.$emit("change", !this.checked) //由外部去同步这个数据
+		},
+	},
+}
+</script>
+
+<style>
+</style>

+ 44 - 0
miniapp/pages/components/search.vue

@@ -0,0 +1,44 @@
+<template>
+<view>
+	<u-search placeholder="搜索人脉、联络记录、人脉指南" :show-action="!global" v-model="key"
+		:disabled="global" @click="cLick" @custom="search" @search="search"/>
+</view>
+</template>
+
+<script>
+export default {
+	props: {
+		global: {//是否全局搜索
+			type: Boolean,
+			default: false,
+		},
+		searchKey: {//搜索条件
+			type: String,
+			default: '',
+		},
+	},
+	data() {
+		return {
+			key: this.searchKey
+		}
+	},
+	methods: {
+		cLick() {
+			if (this.global) {
+				uni.navigateTo({
+					url: '/pages/home/search',
+				})
+			}
+		},
+		search() {
+			if (!this.global) {
+				this.$emit('update:searchKey', this.key)
+				this.$emit('search', this.key)
+			}
+		},
+	},
+}
+</script>
+
+<style>
+</style>

+ 76 - 0
miniapp/pages/components/table.vue

@@ -0,0 +1,76 @@
+<template>
+<u-grid :col="columns.length" :customStyle="{alignItems:'stretch'}">
+	<u-grid-item v-for="(item,i) in gridItems" :key="i">
+		<view class="t-c p-b-t-10 m-b-5 width-100 k-col height-100" 
+			:class="item.type=='top'?'c-g-info-m col-info-d':(item.type=='total'?'c-g-info-m col-white':'c-g-info-l col-info')">
+			{{item.text}}
+		</view>
+	</u-grid-item>
+</u-grid>
+</template>
+
+<script>
+export default {
+	props: {
+		columns: {
+			type: Array,
+			default() {
+				return []
+			},
+		},
+		datas: {
+			type: Array,
+			default() {
+				return []
+			},
+		},
+	},
+	computed: {
+		gridItems() {
+			let list = []
+			for (let i = 0; i < this.columns.length; i++) {//先加表头
+				list.push({
+					text: this.columns[i].name,
+					type: 'top',
+				})
+			}
+			for (let i = 0; i < this.datas.length; i++) {//再加数据,先遍历行
+				let data = this.datas[i]
+				for (let j = 0; j < this.columns.length; j++) {//再遍历列
+					list.push({
+						text: data[this.columns[j].key],
+						type: data.rowType,
+					})
+				}
+			}
+			return list
+		},
+	},
+	data() {
+		return {
+			// gridItems: [],
+		}
+	},
+	methods: {
+		refresh(columns, datas) {//这个方法好像没用
+			let list = []
+			for (let i = 0; i < columns.length; i++) {//先加表头
+				list.push(columns[i].name)
+			}
+			for (let i = 0; i < datas.length; i++) {//再加数据,先遍历行
+				let data = datas[i]
+				for (let j = 0; j < columns.length; j++) {//再遍历列
+					list.push(data[columns[j].key])
+				}
+			}
+			this.gridItems = list
+		},
+	},
+}
+</script>
+
+<style>
+.k-col {
+	/* line-height: 20px; */
+}
+</style>

+ 314 - 0
miniapp/pages/components/tooltip.vue

@@ -0,0 +1,314 @@
+<template>
+<view
+	class="u-tooltip"
+	:style="[$u.addStyle(customStyle)]"
+>
+	<u-overlay
+		:show="showTooltip && tooltipTop !== -10000 && overlay"
+		customStyle="backgroundColor: rgba(0, 0, 0, 0)"
+		@click="overlayClickHandler"
+	></u-overlay>
+	<view class="u-tooltip__wrapper">
+		<view
+			class="u-tooltip__wrapper__text"
+			:id="textId"
+			:ref="textId"
+			:userSelect="false"
+			:selectable="false"
+			@longpress.stop="longpressHandler"
+			:style="{
+				color: color,
+				backgroundColor: bgColor && showTooltip && tooltipTop !== -10000 ? bgColor : 'transparent'
+			}"
+		><slot></slot></view>
+		<u-transition
+			mode="fade"
+			:show="showTooltip"
+			duration="200"
+			:customStyle="{
+				position: 'absolute', 
+				top: $u.addUnit(tooltipTop),
+				zIndex: zIndex,
+				...tooltipStyle
+			}"
+		>
+			<view
+				class="u-tooltip__wrapper__popup"
+				:id="tooltipId"
+				:ref="tooltipId"
+			>
+				<view
+					class="u-tooltip__wrapper__popup__indicator"
+					hover-class="u-tooltip__wrapper__popup__indicator--hover"
+					v-if="buttons.length"
+					:style="[indicatorStyle, {
+						width: $u.addUnit(indicatorWidth),
+						height: $u.addUnit(indicatorWidth),
+					}]"
+				>
+					<!-- 由于nvue不支持三角形绘制,这里就做一个四方形,再旋转45deg,得到露出的一个三角 -->
+				</view>
+				<view class="u-tooltip__wrapper__popup__list">
+					<block v-for="(item , index) in buttons" :key="index">
+						<view
+							class="u-tooltip__wrapper__popup__list__btn"
+							hover-class="u-tooltip__wrapper__popup__list__btn--hover"
+						>
+							<text
+								class="u-tooltip__wrapper__popup__list__btn__text"
+								@tap="btnClickHandler(index)"
+							>{{ item }}</text>
+						</view>
+						<u-line
+							direction="column"
+							color="#8d8e90"
+							v-if="index < buttons.length - 1"
+							length="18"
+						></u-line>
+					</block>
+				</view>
+			</view>
+		</u-transition>
+	</view>
+</view>
+</template>
+
+<script>
+// import props from './props.js';
+// #ifdef APP-NVUE 
+const dom = uni.requireNativePlugin('dom')
+// #endif
+export default {
+	// mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+	props: {
+		// 文本大小
+		size: {
+			type: [String, Number],
+			default: 14,
+		},
+		// 字体颜色
+		color: {
+			type: String,
+			default: 'black',
+		},
+		// 弹出提示框时,文本的背景色
+		bgColor: {
+			type: String,
+			default: 'transparent',
+		},
+		// 弹出提示的方向,top-上方,bottom-下方
+		direction: {
+			type: String,
+			default: 'top',
+		},
+		// 扩展的按钮组
+		buttons: {
+			type: Array,
+			default: [],
+		},
+	},
+	data() {
+		return {
+			overlay: true,// 是否显示透明遮罩以防止触摸穿透
+			zIndex: 10071,// 弹出提示的z-index,nvue无效(默认 10071 )
+			
+			// 是否展示气泡
+			showTooltip: true,
+			// 生成唯一id,防止一个页面多个组件,造成干扰
+			textId: uni.$u.guid(),
+			tooltipId: uni.$u.guid(),
+			// 初始时甚至为很大的值,让其移到屏幕外面,为了计算元素的尺寸
+			tooltipTop: -10000,
+			// 气泡的位置信息
+			tooltipInfo: {
+				width: 0,
+				left: 0
+			},
+			// 文本的位置信息
+			textInfo: {
+				width: 0,
+				left: 0
+			},
+			// 三角形指示器的样式
+			indicatorStyle: {},
+			// 气泡在可能超出屏幕边沿范围时,重新定位后,距离屏幕边沿的距离
+			screenGap: 12,
+			// 三角形指示器的宽高,由于对元素进行了角度旋转,精确计算指示器位置时,需要用到其尺寸信息
+			indicatorWidth: 14,
+		}
+	},
+	watch: {
+		propsChange() {
+			this.getElRect()
+		}
+	},
+	computed: {
+		// 特别处理H5的复制,因为H5浏览器是自带系统复制功能的,在H5环境
+		// 当一些依赖参数变化时,需要重新计算气泡和指示器的位置信息
+		propsChange() {
+			// return [this.text, this.buttons]
+			return [this.buttons]
+		},
+		// 计算气泡和指示器的位置信息
+		tooltipStyle() {
+			const style = {
+					transform: `translateY(${this.direction === 'top' ? '-100%' : '100%'})`,
+				},
+				sys = uni.$u.sys(),
+				getPx = uni.$u.getPx,
+				addUnit = uni.$u.addUnit
+			if (this.tooltipInfo.width / 2 > this.textInfo.left + this.textInfo.width / 2 - this.screenGap) {
+				this.indicatorStyle = {}
+				style.left = `-${addUnit(this.textInfo.left - this.screenGap)}`
+				this.indicatorStyle.left = addUnit(this.textInfo.width / 2 - getPx(style.left) - this.indicatorWidth /
+					2)
+			} else if (this.tooltipInfo.width / 2 > sys.windowWidth - this.textInfo.right + this.textInfo.width / 2 -
+				this.screenGap) {
+				this.indicatorStyle = {}
+				style.right = `-${addUnit(sys.windowWidth - this.textInfo.right - this.screenGap)}`
+				this.indicatorStyle.right = addUnit(this.textInfo.width / 2 - getPx(style.right) - this
+					.indicatorWidth / 2)
+			} else {
+				const left = Math.abs(this.textInfo.width / 2 - this.tooltipInfo.width / 2)
+				style.left = this.textInfo.width > this.tooltipInfo.width ? addUnit(left) : -addUnit(left)
+				this.indicatorStyle = {}
+			}
+			if (this.direction === 'top') {
+				style.marginTop = '-10px'
+				this.indicatorStyle.bottom = '-4px'
+			} else {
+				style.marginBottom = '-10px'
+				this.indicatorStyle.top = '-4px'
+			}
+			return style
+		}
+	},
+	mounted() {
+		this.init()
+	},
+	methods: {
+		init() {
+			this.getElRect()
+		},
+		// 长按触发事件
+		async longpressHandler() {
+			this.tooltipTop = 0
+			this.showTooltip = true
+		},
+		// 点击透明遮罩
+		overlayClickHandler() {
+			this.showTooltip = false
+		},
+		// 点击弹出按钮
+		btnClickHandler(index) {
+			this.showTooltip = false
+			// 如果需要展示复制按钮,此处index需要加1,因为复制按钮在第一个位置
+			this.$emit('click', index)
+		},
+		// 查询内容高度
+		queryRect(ref) {
+			// #ifndef APP-NVUE
+			// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+			// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
+			return new Promise(resolve => {
+				this.$uGetRect(`#${ref}`).then(size => {
+					resolve(size)
+				})
+			})
+			// #endif
+
+			// #ifdef APP-NVUE
+			// nvue下,使用dom模块查询元素高度
+			// 返回一个promise,让调用此方法的主体能使用then回调
+			return new Promise(resolve => {
+				dom.getComponentRect(this.$refs[ref], res => {
+					resolve(res.size)
+				})
+			})
+			// #endif
+		},
+		// 元素尺寸
+		getElRect() {
+			// 调用之前,先将指示器调整到屏幕外,方便获取尺寸
+			this.showTooltip = true
+			this.tooltipTop = -10000
+			uni.$u.sleep(500).then(() => {
+				this.queryRect(this.tooltipId).then(size => {
+					this.tooltipInfo = size
+					// 获取气泡尺寸之后,将其隐藏,为了让下次切换气泡显示与隐藏时,有淡入淡出的效果
+					this.showTooltip = false
+				})
+				this.queryRect(this.textId).then(size => {
+					this.textInfo = size
+				})
+			})
+		},
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+@import "@/uview-ui/libs/css/components.scss";
+
+.u-tooltip {
+	position: relative;
+	@include flex;
+
+	&__wrapper {
+		@include flex;
+		justify-content: center;
+		/* #ifndef APP-NVUE */
+		white-space: nowrap;
+		/* #endif */
+
+		&__text {
+			font-size: 14px;
+		}
+
+		&__popup {
+			@include flex;
+			justify-content: center;
+
+			&__list {
+				background-color: #060607;
+				position: relative;
+				flex: 1;
+				border-radius: 5px;
+				padding: 0px 0;
+				@include flex(row);
+				align-items: center;
+				overflow: hidden;
+
+				&__btn {
+					padding: 11px 13px;
+
+					&--hover {
+						background-color: #58595B;
+					}
+
+					&__text {
+						line-height: 12px;
+						font-size: 13px;
+						color: #FFFFFF;
+					}
+				}
+			}
+
+			&__indicator {
+				position: absolute;
+				background-color: #060607;
+				width: 14px;
+				height: 14px;
+				bottom: -4px;
+				transform: rotate(45deg);
+				border-radius: 2px;
+				z-index: -1;
+
+				&--hover {
+					background-color: #58595B;
+				}
+			}
+		}
+	}
+}
+</style>

+ 102 - 0
miniapp/pages/index.vue

@@ -0,0 +1,102 @@
+<template>
+  <page-theme :theme="theme">
+    <template>
+      <view class="f-30 p-40 flex-dir-column flex-x-center">
+        <view class="col-info-d t-c m-b-30">欢迎使用</view>
+      </view>
+    </template>
+  </page-theme>
+</template>
+
+<script>
+  import themeMixins from '@/pages/yuecai/mixins/themeMixins.js';
+  export default {
+    mixins: [themeMixins],
+    data() {
+      return {
+        isForcelogin: false, //是否强制重新登录
+        checkeds: [],
+        btnEnabled: false,
+        isScaned: false,
+        content: '<p>创建您的账户,您必须同意我们的' +
+          '<a name="userAgreement" style="text-decoration:none"><strong><span style="color: black;">《用户协议》</span></strong></a>' +
+          '和' +
+          '<a name="privacyPolicy" style="text-decoration:none"><strong><span style="color: black;">《隐私协议》</span></strong></a></p>',
+      }
+    },
+    methods: {
+      gotoHome() {
+        if (!this.btnEnabled) {
+          return
+        }
+        if (this.isForcelogin || !this.$u.func.checkLogin()) {
+          //强制重登或未登录,就登录一次
+          console.log('强制重登或未登录,就登录一次')
+          this.$u.func.wxLogin()
+        } else {
+          this.$u.func.toHome()
+        }
+      },
+      gotoFollow() {
+        uni.navigateTo({
+          url: "./blocktime/follow"
+        })
+      },
+      change(e) {
+        this.btnEnabled = e.length > 0
+      },
+      clickLink(e) {
+        if (e.name == 'userAgreement') {
+          this.openUserAgreement()
+        } else {
+          this.openPrivacyPolicy()
+        }
+      },
+      openUserAgreement() {
+        uni.navigateTo({
+          url: './blocktime/protocol'
+        })
+      },
+      openPrivacyPolicy() {
+        uni.navigateTo({
+          url: './blocktime/privacy'
+        })
+      },
+
+      sceneInfo() {
+        let options = uni.getLaunchOptionsSync()
+
+        console.log("options:" + JSON.stringify(options))
+        let scanScene = [
+          1011, //扫描二维码
+          1047 //扫描小程序码
+        ]
+        if (scanScene.indexOf(options.scene) > -1) {
+          this.isScaned = true
+        }
+      },
+    },
+    onLoad(options) {
+      wx.showLoading();
+      /** 扫码进入 */
+      this.sceneInfo()
+      this.isForcelogin = options.f == 1
+      console.log("index onload")
+      uni.$u.sleep(500).then(() => {
+        wx.hideLoading();
+        this.$u.func.toHome()
+        // if (this.$u.func.checkLogin()) {
+        //   this.$u.func.toHome()
+        // } else {
+        //   this.$u.func.wxLogin()
+        // }
+      })
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .u-checkbox-group {
+    justify-content: center !important;
+  }
+</style>

+ 32 - 0
miniapp/pages/yuecai/dish/detail.vue

@@ -0,0 +1,32 @@
+<template>
+  <page-theme :theme="theme">
+    <template>
+      <view class="f-30 p-40 flex-dir-column flex-x-center">
+        <view class="col-info-d t-c m-b-30">详情</view>
+      </view>
+    </template>
+  </page-theme>
+</template>
+
+<script>
+  import themeMixins from '@/pages/yuecai/mixins/themeMixins.js';
+  export default {
+    mixins: [themeMixins],
+    data() {
+      return {}
+    },
+    methods: {
+      getDishList() {
+        this.$u.api.getDishList().then(res=> {
+          //res.data.records
+        })
+      }
+    },
+    onLoad(options) {
+      this.getDishList();
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+</style>

+ 24 - 0
miniapp/pages/yuecai/home/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <page-theme :theme="theme">
+    <template>
+      <view class="f-30 p-40 flex-dir-column flex-x-center">
+        <view class="col-info-d t-c m-b-30">首页</view>
+      </view>
+    </template>
+  </page-theme>
+</template>
+
+<script>
+  import themeMixins from '@/pages/yuecai/mixins/themeMixins.js';
+  export default {
+    mixins: [themeMixins],
+    data() {
+      return {}
+    },
+    methods: {},
+    onLoad(options) {},
+  }
+</script>
+
+<style scoped lang="scss">
+</style>

+ 22 - 0
miniapp/pages/yuecai/mixins/themeMixins.js

@@ -0,0 +1,22 @@
+//这个mixins是给切换主题使用
+module.exports = {
+	data() {
+		return {
+			theme: ''
+		}
+	},
+	methods: {
+		refreshTheme(theme) {
+			this.theme = theme;
+		}
+	},
+	onLoad() {
+		getApp().watch(this.refreshTheme, 'theme');
+	},
+	onShow() {
+		this.theme = getApp().getTheme();
+	},
+	onReady() {
+		this.theme = getApp().getTheme();
+	}
+}

二進制
miniapp/static/images/blocktime.png


二進制
miniapp/static/images/book.png


二進制
miniapp/static/images/digit/digit.png


二進制
miniapp/static/images/digit/normal/0.png


二進制
miniapp/static/images/digit/normal/1.png


二進制
miniapp/static/images/digit/normal/2.png


二進制
miniapp/static/images/digit/normal/3.png


二進制
miniapp/static/images/digit/normal/4.png


二進制
miniapp/static/images/digit/normal/5.png


二進制
miniapp/static/images/digit/normal/6.png


二進制
miniapp/static/images/digit/normal/7.png


二進制
miniapp/static/images/digit/normal/8.png


二進制
miniapp/static/images/digit/normal/9.png


二進制
miniapp/static/images/digit/normal/colon.png


二進制
miniapp/static/images/digit/shadow/0.png


二進制
miniapp/static/images/digit/shadow/1.png


二進制
miniapp/static/images/digit/shadow/2.png


二進制
miniapp/static/images/digit/shadow/3.png


二進制
miniapp/static/images/digit/shadow/4.png


二進制
miniapp/static/images/digit/shadow/5.png


二進制
miniapp/static/images/digit/shadow/6.png


二進制
miniapp/static/images/digit/shadow/7.png


二進制
miniapp/static/images/digit/shadow/8.png


二進制
miniapp/static/images/digit/shadow/9.png


二進制
miniapp/static/images/digit/shadow/colon.png


二進制
miniapp/static/images/dunbar.png


二進制
miniapp/static/images/empty.png


二進制
miniapp/static/images/face.png


二進制
miniapp/static/images/icon/add.png


二進制
miniapp/static/images/icon/calendar.png


二進制
miniapp/static/images/icon/checked.png


二進制
miniapp/static/images/icon/export.png


二進制
miniapp/static/images/icon/fav.png


二進制
miniapp/static/images/icon/fav2.png


二進制
miniapp/static/images/icon/fly.png


二進制
miniapp/static/images/icon/help.png


二進制
miniapp/static/images/icon/history.png


二進制
miniapp/static/images/icon/light.png


二進制
miniapp/static/images/icon/music.png


二進制
miniapp/static/images/icon/power.png


二進制
miniapp/static/images/icon/save.png


+ 0 - 0
miniapp/static/images/icon/search.png


部分文件因文件數量過多而無法顯示