Selaa lähdekoodia

📦 引入答题插件代码

快乐的梦鱼 2 kuukautta sitten
vanhempi
commit
dff36ffb31
86 muutettua tiedostoa jossa 10244 lisäystä ja 2 poistoa
  1. 3 0
      src/App.vue
  2. 3 1
      src/common/config/AppCofig.ts
  3. 161 0
      src/pages.json
  4. 1 0
      src/pages/index.vue
  5. 1 0
      src/pages/third/yunexamine/App.d.ts
  6. 26 0
      src/pages/third/yunexamine/App.js
  7. 81 0
      src/pages/third/yunexamine/App.vue
  8. 1 0
      src/pages/third/yunexamine/README.md
  9. 75 0
      src/pages/third/yunexamine/common/cache.js
  10. 362 0
      src/pages/third/yunexamine/common/util.js
  11. 124 0
      src/pages/third/yunexamine/components/area-picker/area-picker.vue
  12. 1 0
      src/pages/third/yunexamine/components/area-picker/pca-code.json
  13. 204 0
      src/pages/third/yunexamine/components/load-more/load-more.vue
  14. 36 0
      src/pages/third/yunexamine/components/none-data/none-data.vue
  15. 182 0
      src/pages/third/yunexamine/components/upload-avatar/upload-avatar.vue
  16. 16 0
      src/pages/third/yunexamine/mixins/auth.js
  17. 77 0
      src/pages/third/yunexamine/mixins/common.js
  18. 544 0
      src/pages/third/yunexamine/pages/examine/answer.vue
  19. 183 0
      src/pages/third/yunexamine/pages/examine/history.vue
  20. 478 0
      src/pages/third/yunexamine/pages/examine/review.vue
  21. 265 0
      src/pages/third/yunexamine/pages/examine/summary.vue
  22. 394 0
      src/pages/third/yunexamine/pages/examine/top.vue
  23. 247 0
      src/pages/third/yunexamine/pages/examine/wrong.vue
  24. 303 0
      src/pages/third/yunexamine/pages/gift/detail.vue
  25. 488 0
      src/pages/third/yunexamine/pages/gift/exchange.vue
  26. 231 0
      src/pages/third/yunexamine/pages/gift/express_feed.vue
  27. 295 0
      src/pages/third/yunexamine/pages/gift/index.vue
  28. 363 0
      src/pages/third/yunexamine/pages/gift/order.vue
  29. 480 0
      src/pages/third/yunexamine/pages/gift/order_detail.vue
  30. 67 0
      src/pages/third/yunexamine/pages/home/activity.vue
  31. 267 0
      src/pages/third/yunexamine/pages/home/dashboard.vue
  32. 463 0
      src/pages/third/yunexamine/pages/home/index.vue
  33. 33 0
      src/pages/third/yunexamine/pages/home/webview.vue
  34. 139 0
      src/pages/third/yunexamine/pages/home/welcome.vue
  35. 102 0
      src/pages/third/yunexamine/pages/user/auto_login.vue
  36. 145 0
      src/pages/third/yunexamine/pages/user/change_code.vue
  37. 256 0
      src/pages/third/yunexamine/pages/user/change_phone.vue
  38. 466 0
      src/pages/third/yunexamine/pages/user/feedback.vue
  39. 210 0
      src/pages/third/yunexamine/pages/user/feedback_detail.vue
  40. 561 0
      src/pages/third/yunexamine/pages/user/login.vue
  41. 283 0
      src/pages/third/yunexamine/pages/user/mine.vue
  42. 455 0
      src/pages/third/yunexamine/pages/user/register.vue
  43. 58 0
      src/pages/third/yunexamine/pages/user/tips.vue
  44. 49 0
      src/pages/third/yunexamine/service/api/common.js
  45. 127 0
      src/pages/third/yunexamine/service/api/examine.js
  46. 44 0
      src/pages/third/yunexamine/service/api/feedback.js
  47. 98 0
      src/pages/third/yunexamine/service/api/gift.js
  48. 57 0
      src/pages/third/yunexamine/service/api/mock.js
  49. 13 0
      src/pages/third/yunexamine/service/api/page.js
  50. 174 0
      src/pages/third/yunexamine/service/api/user.js
  51. 6 0
      src/pages/third/yunexamine/service/config.js
  52. 159 0
      src/pages/third/yunexamine/service/request/request.js
  53. 119 0
      src/pages/third/yunexamine/static/iconfont.css
  54. BIN
      src/pages/third/yunexamine/static/iconfont.ttf
  55. BIN
      src/pages/third/yunexamine/static/iconfont.woff
  56. BIN
      src/pages/third/yunexamine/static/iconfont.woff2
  57. BIN
      src/pages/third/yunexamine/static/imgs/activity_loading.jpg
  58. BIN
      src/pages/third/yunexamine/static/imgs/gift_top_bg.jpg
  59. BIN
      src/pages/third/yunexamine/static/imgs/home_top_loading.jpg
  60. BIN
      src/pages/third/yunexamine/static/imgs/line-bg.png
  61. BIN
      src/pages/third/yunexamine/static/imgs/loading_image.png
  62. BIN
      src/pages/third/yunexamine/static/imgs/login_top_loading.jpg
  63. BIN
      src/pages/third/yunexamine/static/imgs/result_right.png
  64. BIN
      src/pages/third/yunexamine/static/imgs/result_timeout.png
  65. BIN
      src/pages/third/yunexamine/static/imgs/result_timeout_end.png
  66. BIN
      src/pages/third/yunexamine/static/imgs/result_wrong.png
  67. BIN
      src/pages/third/yunexamine/static/imgs/top_1.png
  68. BIN
      src/pages/third/yunexamine/static/imgs/top_2.png
  69. BIN
      src/pages/third/yunexamine/static/imgs/top_3.png
  70. BIN
      src/pages/third/yunexamine/static/imgs/top_bg.png
  71. BIN
      src/pages/third/yunexamine/static/imgs/user.png
  72. BIN
      src/pages/third/yunexamine/static/tabs/gift.png
  73. BIN
      src/pages/third/yunexamine/static/tabs/gift_active.png
  74. BIN
      src/pages/third/yunexamine/static/tabs/home.png
  75. BIN
      src/pages/third/yunexamine/static/tabs/home_active.png
  76. BIN
      src/pages/third/yunexamine/static/tabs/info.png
  77. BIN
      src/pages/third/yunexamine/static/tabs/info_active.png
  78. BIN
      src/pages/third/yunexamine/static/tabs/mine.png
  79. BIN
      src/pages/third/yunexamine/static/tabs/mine_active.png
  80. BIN
      src/pages/third/yunexamine/static/tabs/top.png
  81. BIN
      src/pages/third/yunexamine/static/tabs/top_active.png
  82. 101 0
      src/pages/third/yunexamine/store/index.js
  83. 20 0
      src/pages/third/yunexamine/store/module/examine.js
  84. 32 0
      src/pages/third/yunexamine/store/module/user.js
  85. 109 0
      src/pages/third/yunexamine/uni.scss
  86. 6 1
      src/pages/user/index.vue

+ 3 - 0
src/App.vue

@@ -9,6 +9,7 @@ import { configAppTheme } from './common/config/Theme';
 import { getCurrentPageUrl, navTo } from './components/utils/PageAction';
 import { onError, onLaunch } from '@dcloudio/uni-app'
 import { RequestApiConfig, RequestApiError } from '@imengyu/imengyu-utils';
+import { doLaunch } from './pages/third/yunexamine/App.js';
 import ApiCofig from './common/config/ApiCofig';
 import MemoryTimeOut from './components/composeabe/MemoryTimeOut';
 import BugReporter from './common/BugReporter';
@@ -42,6 +43,8 @@ onLaunch(async () => {
     
   await BugReporter.checkAndReportBug();
   await init();
+
+  doLaunch();
 });
 
 //配置BugReporter

+ 3 - 1
src/common/config/AppCofig.ts

@@ -41,7 +41,9 @@ export const isDev = import.meta.env.DEV;
  */
 export const isProd = import.meta.env.PROD;
 
-const accountInfo = uni.getAccountInfoSync();
+const accountInfo = typeof uni.getAccountInfoSync === 'function' ? uni.getAccountInfoSync() : {
+  miniProgram: {} as Record<string, unknown>
+};
 
 /**
  * 获取当前环境版本

+ 161 - 0
src/pages.json

@@ -443,6 +443,167 @@
           }
         }
       ]
+    },
+    {
+      "root": "pages/third/yunexamine",
+      "pages": [
+        {
+          "path": "pages/home/index",
+          "style": {
+            "navigationBarTitleText": "首页"
+          }
+        },
+        {
+          "path": "pages/home/dashboard",
+          "style": {
+            "navigationBarTitleText": "活动中心"
+          }
+        },
+        {
+          "path": "pages/home/welcome",
+          "style": {
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "pages/home/activity",
+          "style": {
+            "navigationBarTitleText": "活动"
+          }
+        },
+        {
+          "path": "pages/home/webview",
+          "style": {
+            "navigationBarTitleText": "新页面"
+          }
+        },
+        {
+          "path": "pages/examine/answer",
+          "style": {
+            "navigationBarTitleText": "进行答题"
+          }
+        },
+        {
+          "path": "pages/examine/summary",
+          "style": {
+            "navigationBarTitleText": "答题结果"
+          }
+        },
+        {
+          "path": "pages/examine/history",
+          "style": {
+            "navigationBarTitleText": "答题历史"
+          }
+        },
+        {
+          "path": "pages/examine/wrong",
+          "style": {
+            "navigationBarTitleText": "错题记录"
+          }
+        },
+        {
+          "path": "pages/examine/review",
+          "style": {
+            "navigationBarTitleText": "回顾答题"
+          }
+        },
+        {
+          "path": "pages/examine/top",
+          "style": {
+            "navigationBarTitleText": "排行榜"
+          }
+        },
+        {
+          "path": "pages/gift/index",
+          "style": {
+            "navigationBarTitleText": "活动奖品"
+          }
+        },
+        {
+          "path": "pages/gift/detail",
+          "style": {
+            "navigationBarTitleText": "奖品详情"
+          }
+        },
+        {
+          "path": "pages/gift/exchange",
+          "style": {
+            "navigationBarTitleText": "奖品兑换"
+          }
+        },
+        {
+          "path": "pages/gift/order",
+          "style": {
+            "navigationBarTitleText": "兑换订单"
+          }
+        },
+        {
+          "path": "pages/gift/order_detail",
+          "style": {
+            "navigationBarTitleText": "订单详情"
+          }
+        },
+        {
+          "path": "pages/gift/express_feed",
+          "style": {
+            "navigationBarTitleText": "物流动态"
+          }
+        },
+        {
+          "path": "pages/user/register",
+          "style": {
+            "navigationBarTitleText": "注册"
+          }
+        },
+        {
+          "path": "pages/user/login",
+          "style": {
+            "navigationBarTitleText": "登录"
+          }
+        },
+        {
+          "path": "pages/user/mine",
+          "style": {
+            "navigationBarTitleText": "我的"
+          }
+        },
+        {
+          "path": "pages/user/change_phone",
+          "style": {
+            "navigationBarTitleText": "修改手机号"
+          }
+        },
+        {
+          "path": "pages/user/change_code",
+          "style": {
+            "navigationBarTitleText": "修改登录码"
+          }
+        },
+        {
+          "path": "pages/user/feedback",
+          "style": {
+            "navigationBarTitleText": "意见反馈"
+          }
+        },
+        {
+          "path": "pages/user/feedback_detail",
+          "style": {
+            "navigationBarTitleText": "反馈详情"
+          }
+        },
+        {
+          "path": "pages/user/auto_login",
+          "style": {
+            "navigationBarTitleText": "登录检查"
+          }
+        },
+        {
+          "path": "pages/user/tips",
+          "style": {
+            "navigationBarTitleText": "系统提示"
+          }
+        }
+      ]
     }
   ],
   "globalStyle": {

+ 1 - 0
src/pages/index.vue

@@ -125,6 +125,7 @@ async function paySuccessAndRefresh() {
   }
 }
 
+
 defineExpose({
   onPageBack: (name: string, data: Record<string, unknown>) => {
     if (name === 'goVillage') {

+ 1 - 0
src/pages/third/yunexamine/App.d.ts

@@ -0,0 +1 @@
+export declare function doLaunch(): void;

+ 26 - 0
src/pages/third/yunexamine/App.js

@@ -0,0 +1,26 @@
+import {
+  getConfig
+} from './service/api/common.js'
+import { useAppStore } from './store/index.js'
+
+export function	doLaunch() {
+  console.log('App Launch')
+  const appStore = useAppStore()
+  getConfig().then(([err, res]) => {
+    console.log('getConfig', err, res);
+    if (!err) {
+      appStore.setGlobalConfig(res)
+      // 当前活动无效
+      if (res.activity_invalid) {
+        appStore.delActivityId()
+        // 重新加载配置
+        getConfig().then(([err, res]) => {
+          console.log('getConfig reload', err, res)
+          if (!err) {
+            appStore.setGlobalConfig(res)
+          }
+        })
+      }
+    }
+  })
+}

+ 81 - 0
src/pages/third/yunexamine/App.vue

@@ -0,0 +1,81 @@
+<script>
+
+	export default {
+		onLaunch: function() {
+			console.log('App Launch')
+			const appStore = useAppStore()
+			getConfig().then(([err, res]) => {
+				console.log('getConfig', err, res);
+				if (!err) {
+					appStore.setGlobalConfig(res)
+					// 当前活动无效
+					if (res.activity_invalid) {
+						appStore.delActivityId()
+						// 重新加载配置
+						getConfig().then(([err, res]) => {
+							console.log('getConfig reload', err, res)
+							if (!err) {
+								appStore.setGlobalConfig(res)
+							}
+						})
+					}
+				}
+			})
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		},
+		created() {
+			console.log('App created')
+		},
+		mounted() {
+			console.log('App mounted')
+		},
+		updated() {
+			console.log('App updated')
+		},
+		activated() {
+			console.log('App activated')
+		},
+		deactivated() {
+			console.log('App deactivated')
+		},
+		unmounted() {
+			console.log('App unmounted')
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import '.https://xy.wenlvti.net/app_static/yunexamine/iconfont.css';
+
+	page {
+		font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+	}
+
+	/* input 样式 */
+	.input-placeholder {
+		color: #999999;
+	}
+
+	.placeholder {
+		color: #999999;
+	}
+
+	/* 重置 open-data 的 button 样式 */
+	.open-data-btn {
+		background: none;
+		margin-left: inherit;
+		margin-right: inherit;
+		line-height: inherit;
+		padding-left: 0;
+		padding-right: 0;
+
+		&::after {
+			border: none;
+		}
+	}
+</style>

+ 1 - 0
src/pages/third/yunexamine/README.md

@@ -0,0 +1 @@
+由PHP云答题插件导入,额外对接本项目架构,更新时注意同步修改

+ 75 - 0
src/pages/third/yunexamine/common/cache.js

@@ -0,0 +1,75 @@
+// 缓存前缀 
+var postfix = 'yuncache';
+
+/**
+ * 设置缓存 
+ * 
+ * @param  {[type]} k [键名]
+ * @param  {[type]} v [键值]
+ * @param  {[type]} t [时间、单位秒]
+ */
+function put(k, v, t) {
+	uni.setStorageSync(k, v)
+	var seconds = parseInt(t);
+	if (seconds > 0) {
+		var timestamp = Date.parse(new Date());
+		timestamp = timestamp / 1000 + seconds;
+		uni.setStorageSync(k + postfix, timestamp + "")
+	} else {
+		uni.removeStorageSync(k + postfix)
+	}
+}
+
+/**
+ * 获取缓存 
+ * 
+ * @param  {[type]} k   [键名]
+ * @param  {[type]} def [获取为空时默认]
+ */
+function get(k, def) {
+	var deadtime = parseInt(uni.getStorageSync(k + postfix))
+	if (deadtime) {
+		if (parseInt(deadtime) < Date.parse(new Date()) / 1000) {
+			if (def) {
+				return def;
+			} else {
+				return false;
+			}
+		}
+	}
+	var res = uni.getStorageSync(k);
+	if (res) {
+		return res;
+	} else {
+		if (def == undefined || def == "") {
+			def = false;
+		}
+		return def;
+	}
+}
+
+/**
+ * 清理指定缓存
+ * 
+ * @return {[type]} [description]
+ */
+function remove(k) {
+	uni.removeStorageSync(k);
+	uni.removeStorageSync(k + postfix);
+}
+
+/**
+ * 清理所有缓存
+ * 
+ * @return {[type]} [description]
+ */
+function clear() {
+	uni.clearStorageSync();
+}
+
+export default {
+	put,
+	get,
+	remove,
+	clear,
+}

+ 362 - 0
src/pages/third/yunexamine/common/util.js

@@ -0,0 +1,362 @@
+const extend = function extend(target) {
+	var sources = Array.prototype.slice.call(arguments, 1)
+
+	for (var i = 0; i < sources.length; i += 1) {
+		var source = sources[i]
+		for (var key in source) {
+			if (source.hasOwnProperty(key)) {
+				target[key] = source[key]
+			}
+		}
+	}
+
+	return target
+}
+
+/**
+ * 获取当前时间戳
+ */
+const getTimestamp = (date) => {
+	date = date || new Date()
+	return Math.round(date / 1000)
+}
+
+/**
+ * 保留指定小数位
+ */
+const toDecimal = (float, num) => {
+	if (typeof float !== 'number') float = parseFloat(float)
+	return parseFloat(float.toFixed(num))
+}
+
+/**
+ * 时间戳转 yyyy-MM-dd hh:mm
+ */
+export function formatDate(timestamp, format) {
+	const date = timestamp ? new Date(timestamp * 1000) : new Date();
+	const data = {
+		'M+': date.getMonth() + 1,
+		'd+': date.getDate(),
+		'h+': date.getHours(),
+		'm+': date.getMinutes(),
+		's+': date.getSeconds(),
+		'q+': Math.floor((date.getMonth() + 3) / 3),
+		'S+': date.getMilliseconds()
+	}
+	if (/(y+)/i.test(format)) {
+		format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+	}
+	for (var key in data) {
+		if (new RegExp('(' + key + ')').test(format)) {
+			format = format.replace(RegExp.$1, RegExp.$1.length == 1 ?
+				data[key] : ('00' + data[key]).substr(('' + data[key]).length))
+		}
+	}
+
+	return format
+}
+
+/**
+ * 分钟、小时、天前的处理
+ */
+const timeAgo = function(timestamp, format) {
+	const minute = 1000 * 60 // 把分,时,天,周,半个月,一个月用毫秒表示
+	const hour = minute * 60
+	const day = hour * 24
+	const now = new Date().getTime() // 获取当前时间毫秒
+	const spreadTime = now - timestamp * 1000 // 时间差
+	if (spreadTime < 0) {
+		return
+	}
+
+
+	const minC = spreadTime / minute
+	const hourC = spreadTime / hour
+	const dayC = spreadTime / day
+	let result
+
+	if (spreadTime >= 0 && spreadTime <= minute) {
+		result = '刚刚'
+	} else if (minC >= 1 && minC <= 59) {
+		result = parseInt(minC) + '分钟前'
+	} else if (hourC >= 1 && hourC <= 23) {
+		result = parseInt(hourC) + '小时前'
+	} else if (dayC >= 1 && dayC < 3) {
+		result = parseInt(dayC) + '天前'
+	} else if (dayC >= 3 && dayC <= 7) {
+		result = '3天前'
+	} else {
+		result = formatDate(timestamp, format)
+	}
+
+	return result
+}
+
+/**
+ * param 将要转为URL参数字符串的对象
+ * key URL参数字符串的前缀
+ * encode true/false 是否进行URL编码,默认为true
+ *
+ * return URL参数字符串
+ */
+const urlEncode = (param, key, encode) => {
+	if (param == null) return ''
+	const t = typeof(param)
+	let paramStr = ''
+	if (t === 'string' || t === 'number' || t === 'boolean') {
+		paramStr += '&' + key + '=' + ((encode == null || encode) ? encodeURIComponent(param) : param)
+	} else {
+		for (let i in param) {
+			let k = key == null ? i : key + (param instanceof Array ? '[' + i + ']' : '[' + i + ']')
+			paramStr += urlEncode(param[i], k, encode)
+		}
+	}
+
+	return paramStr
+}
+
+/**
+ * return
+ */
+const urlDecode = (query) => {
+	if (!query) return null
+	let hash, object = {}
+	const hashes = query.slice(query.indexOf('?') + 1).split('&')
+	for (let i = 0; i < hashes.length; i++) {
+		hash = hashes[i].split('=')
+		object[hash[0]] = hash[1]
+	}
+
+	return object;
+}
+
+/**
+ * 对象合并
+ */
+const deepAssign = (origin, source) => {
+	for (let key in source) {
+		if (source.hasOwnProperty(key)) {
+			if (source[key] instanceof Object) {
+				deepAssign(origin[key], source[key])
+			} else {
+				origin[key] = source[key]
+			}
+		}
+	}
+}
+
+/**
+ * 状态数据
+ */
+const display = {
+	toast: false, // 无用
+	modal: false,
+	loading: false,
+}
+
+// 显示消息提示
+export  function showToast(text, icon) {
+	wx.showToast({
+		title: text || '加载中',
+		icon: icon || 'none',
+		mask: true,
+		duration: 2000
+	})
+}
+
+// 隐藏消息提示
+export function hideToast() {
+	wx.hideToast()
+}
+
+// 显示 loading
+export function showLoading(text) {
+	if (!display.loading) {
+		display.loading = true
+		wx.showLoading({
+			title: text || '加载中',
+			mask: true
+		})
+	}
+}
+
+// 隐藏 loading
+export function hideLoading() {
+	if (display.loading) {
+		display.loading = false
+		wx.hideLoading()
+	}
+}
+
+// 显示失败提示
+const showModal = (title, content) => {
+	if (display.toast) hideToast()
+	if (display.loading) hideLoading()
+
+	if (!display.modal) {
+		title = title || '信息提示'
+		content = content || '未知错误' // content 为 null 时会报错
+		display.modal = true
+		wx.showModal({
+			title,
+			content: typeof content === 'object' ? JSON.stringify(content) : content,
+			showCancel: false,
+			confirmColor: '#3B99FC',
+			complete: function(e) {
+				display.modal = false
+			}
+		})
+	}
+}
+
+export function buildPageUrl(page, options) {
+	let pageUrl = page;
+	if (options) {
+		let params = urlEncode(options)
+		pageUrl += '?' + params.substring(1, params.length);
+	}
+
+	return pageUrl;
+}
+
+export function encodeRedirectUrl(page, options) {
+	return encodeURIComponent(buildPageUrl(page, options));
+}
+
+export function decodeRedirectUrl(url) {
+	return decodeURIComponent(url);
+}
+
+/**
+ * 处理富文本里的图片宽度自适应
+ * 1.去掉img标签里的style、width、height属性
+ * 2.img标签添加style属性:max-width:100%;height:auto
+ * 3.修改所有style里的width属性为max-width:100%
+ * 4.去掉<br/>标签
+ * 
+ * @param html
+ * @returns {void|string|*}
+ */
+export function formatRichText(html) {
+	let newContent = html.replace(/<img[^>]*>/gi, function(match, capture) {
+		match = match.replace(/style="[^"]+"/gi, '').replace(/style='[^']+'/gi, '');
+		match = match.replace(/width="[^"]+"/gi, '').replace(/width='[^']+'/gi, '');
+		match = match.replace(/height="[^"]+"/gi, '').replace(/height='[^']+'/gi, '');
+		return match;
+	});
+	newContent = newContent.replace(/style="[^"]+"/gi, function(match, capture) {
+		match = match.replace(/width:[^;]+;/gi, 'max-width:100%;').replace(/width:[^;]+;/gi, 'max-width:100%;');
+		return match;
+	});
+	newContent = newContent.replace(/<br[^>]*\/>/gi, '');
+	newContent = newContent.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;margin:10px 0;"');
+	return newContent;
+}
+
+/** 
+ * H5图片压缩 
+ */
+export function compressImage(file, maxWidth, maxHeight) {
+	const setting = {
+		maxWidth: maxWidth || 800,
+		maxHeight: maxHeight || 800,
+		quality: 0.9
+	}
+	console.log('compressImage', file, setting)
+
+	return new Promise((resolve, reject) => {
+		// 判断是否需要压缩, 没有超过限制
+		if (file.size <= 1024 * 1024) {
+			console.log('no compress')
+			return resolve([null, file.path])
+		} else {
+			console.log('yes compress')
+			uni.getImageInfo({
+				src: file.path
+			}).then(([err, res]) => {
+				let img = new Image()
+				img.src = res.path
+				// 图片高宽
+				let originWidth = res.width
+				let originHeight = res.height
+				let targetWidth = originWidth
+				let targetHeight = originHeight
+				let maxWidth = setting.maxWidth
+				let maxHeight = setting.maxHeight
+				if (originWidth > maxWidth || originHeight > maxHeight) {
+					if (originWidth / originHeight > maxWidth / maxHeight) {
+						targetWidth = maxWidth
+						targetHeight = Math.round(maxWidth * (originHeight / originWidth))
+					} else {
+						targetHeight = maxHeight
+						targetWidth = Math.round(maxHeight * (originWidth / originHeight))
+					}
+				}
+
+				// 图片尺寸调整
+				let canvas = document.createElement('canvas')
+				canvas.width = targetWidth
+				canvas.height = targetHeight
+				let ctx = canvas.getContext('2d')
+				ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
+
+				// 图片压缩导出
+				canvas.toBlob(function(fileSrc) {
+					let imgSrc = window.URL.createObjectURL(fileSrc)
+					console.log('after compress', imgSrc)
+
+					return resolve([null, imgSrc])
+				}, 'image/jpeg', setting.quality)
+			})
+		}
+	})
+}
+
+let systemInfo = null
+
+/**
+ * 全局方法 - px2rpx 
+ *
+ * @param {Object} px
+ */
+export function px2rpx(px) {
+  if (systemInfo === null) {
+    systemInfo = uni.getSystemInfoSync()
+  }
+
+  //  官方规定屏幕宽为750rpx
+  return px / systemInfo.windowWidth * 750;
+};
+
+/**
+ * 全局方法 - rpx2px
+ * 
+ * @param {Object} rpx
+ */
+export function rpx2px(rpx) {
+  if (systemInfo === null) {
+    systemInfo = uni.getSystemInfoSync()
+  }
+
+  //  官方规定屏幕宽为750rpx
+  return rpx / 750 * systemInfo.windowWidth;
+}
+
+export default {
+	extend,
+	toDecimal,
+	getTimestamp,
+	formatDate,
+	timeAgo,
+	urlEncode,
+	urlDecode,
+	deepAssign,
+	showToast,
+	hideToast,
+	showLoading,
+	hideLoading,
+	showModal,
+	formatRichText,
+  px2rpx,
+  rpx2px,
+}

+ 124 - 0
src/pages/third/yunexamine/components/area-picker/area-picker.vue

@@ -0,0 +1,124 @@
+<template>
+	<view class="area-picker">
+		<picker mode="multiSelector" @columnchange="bindMultiPickerColumnChange" @change="change" :value="multiIndexArr"
+			:range="multiArray">
+			<slot></slot>
+		</picker>
+	</view>
+</template>
+
+<script>
+	let _self;
+	import jsonData from './pca-code.json'
+	export default {
+		name: "area-picker",
+		// 此处定义传入的数据
+		props: {
+			addressList: {
+				type: Array,
+				default: []
+			}
+		},
+		computed: {
+			multiIndexArr() {
+				return [this.provinceIndex, this.cityIndex, this.countyIndex];
+			},
+			province() {
+				return this.provinceList[this.provinceIndex] || {};
+			},
+			provinceList() {
+				return this.data;
+			},
+			provinceNameList() {
+				return this.provinceList.map((item, index) => item.name || '');
+			},
+			city() {
+				return this.cityList[this.cityIndex] || {};
+			},
+			cityList() {
+				return this.province.children || [];
+			},
+			cityNameList() {
+				return this.cityList.map((item, index) => item.name || '');
+			},
+			county() {
+				return this.countyList[this.countyIndex] || {};
+			},
+			countyList() {
+				return this.city.children || [];
+			},
+			countyNameList() {
+				return this.countyList.map((item, index) => item.name || '');
+			},
+			multiArray() {
+				return [this.provinceNameList, this.cityNameList, this.countyNameList];
+			},
+			addressResultObj() {
+				return {
+					province: this.province,
+					city: this.city,
+					county: this.county
+				};
+			},
+			addressResultList() {
+				return Object.values(this.addressResultObj).map((item, index) => item.name || '');
+			}
+		},
+		data() {
+			return {
+				data: [],
+				provinceIndex: 0,
+				cityIndex: 0,
+				countyIndex: 0,
+				keys: ['province', 'city', 'county']
+			};
+		},
+		//在实例创建完成后被立即调用。
+		mounted() {
+			_self = this;
+			this.loadData();
+		},
+		methods: {
+			loadData() {
+				_self.data = jsonData;
+				_self.init();
+			},
+			init() {
+				if (this.addressList.length === 3) {
+					let key, index;
+					for (let i in this.keys) {
+						key = this.keys[i];
+						index = this[`${key}NameList`].indexOf(this.addressList[i]);
+						if (index <= -1) {
+							index = 0;
+						}
+						this.$data[`${key}Index`] = index;
+					}
+					this.change();
+				}
+			},
+			bindMultiPickerColumnChange: function(e) {
+				let index = e.detail.column;
+				let key = this.keys[index];
+				this.$data[`${key}Index`] = e.detail.value;
+				while (index < this.keys.length) {
+					index++;
+					key = this.keys[index];
+					this.$data[`${key}Index`] = 0;
+				}
+			},
+			change(e) {
+				// console.log(e)
+				this.$emit('update:addressList', this.addressResultList);
+				this.$emit('getAddressResultObj', this.addressResultObj);
+			},
+			getAddressResultObj() {
+				return this.getAddressResultObj;
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+
+</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 0
src/pages/third/yunexamine/components/area-picker/pca-code.json


+ 204 - 0
src/pages/third/yunexamine/components/load-more/load-more.vue

@@ -0,0 +1,204 @@
+<template>
+	<view class="load-more" v-if="loadingType !== -1" :style="{ marginTop: marginTop + 'px' }">
+		<view class="loading-img" v-show="loadingType === 1 && showImage">
+			<view class="load1">
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+			</view>
+			<view class="load2">
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+			</view>
+			<view class="load3">
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+				<view :style="{ background: color }"></view>
+			</view>
+		</view>
+		<text class="loading-text" :style="{ color: color }">
+			{{ loadingText ? loadingText : loadingType === 0 ? loadingTexts.down : loadingType === 1 ? loadingTexts.refresh : loadingType === 2 ? loadingTexts.noMore : loadingTexts.wrong }}
+		</text>
+	</view>
+</template>
+
+<script>
+import { rpx2px } from '../../common/util.js'
+export default {
+	name: 'load-more',
+	props: {
+		// 上拉的状态:0-loading前;1-loading中;2-没有更多了;3-没有更多了
+		loadingType: {
+			type: Number,
+			default: -1
+		},
+		loadingText: {
+			type: String,
+			default: ''
+		},
+		loadingTexts: {
+			type: Object,
+			default: () => {
+				return {
+					down: '上拉显示更多',
+					refresh: '正在加载...',
+					noMore: '没有更多数据了',
+					wrong: '网络繁忙,请稍后再试'
+				};
+			}
+		},
+		showImage: {
+			type: Boolean,
+			default: true
+		},
+		color: {
+			type: String,
+			default: '#777777'
+		},
+		top: {
+			type: Number,
+			default: 20
+		}
+	},
+	data() {
+		return {
+			marginTop: 0
+		};
+	},
+	created() {
+		this.marginTop = rpx2px(this.top);
+	},
+};
+</script>
+
+<style>
+.load-more {
+	display: flex;
+	flex-direction: row;
+	height: 80upx;
+	align-items: center;
+	justify-content: center;
+}
+.loading-img {
+	height: 48rpx;
+	width: 48rpx;
+	margin-right: 20rpx;
+}
+.loading-text {
+	font-size: 30rpx;
+	color: #777777;
+}
+
+.loading-img > view {
+	position: absolute;
+}
+.load1,
+.load2,
+.load3 {
+	height: 50rpx;
+	width: 50rpx;
+}
+.load2 {
+	transform: rotate(30deg);
+}
+
+.load3 {
+	transform: rotate(60deg);
+}
+
+.loading-img > view view {
+	width: 12rpx;
+	height: 4rpx;
+	border-top-left-radius: 1px;
+	border-bottom-left-radius: 1px;
+	background: #777;
+	position: absolute;
+	opacity: 0.2;
+	transform-origin: 50%;
+	-webkit-animation: load 1.56s ease infinite;
+}
+
+.loading-img > view view:nth-child(1) {
+	transform: rotate(90deg);
+	top: 2px;
+	left: 9px;
+}
+
+.loading-img > view view:nth-child(2) {
+	-webkit-transform: rotate(180deg);
+	top: 11px;
+	right: 0px;
+}
+
+.loading-img > view view:nth-child(3) {
+	transform: rotate(270deg);
+	bottom: 4rpx;
+	left: 18rpx;
+}
+
+.loading-img > view view:nth-child(4) {
+	top: 22rpx;
+	left: 0px;
+}
+
+.load1 view:nth-child(1) {
+	animation-delay: 0s;
+}
+
+.load2 view:nth-child(1) {
+	animation-delay: 0.13s;
+}
+
+.load3 view:nth-child(1) {
+	animation-delay: 0.26s;
+}
+
+.load1 view:nth-child(2) {
+	animation-delay: 0.39s;
+}
+
+.load2 view:nth-child(2) {
+	animation-delay: 0.52s;
+}
+
+.load3 view:nth-child(2) {
+	animation-delay: 0.65s;
+}
+
+.load1 view:nth-child(3) {
+	animation-delay: 0.78s;
+}
+
+.load2 view:nth-child(3) {
+	animation-delay: 0.91s;
+}
+
+.load3 view:nth-child(3) {
+	animation-delay: 1.04s;
+}
+
+.load1 view:nth-child(4) {
+	animation-delay: 1.17s;
+}
+
+.load2 view:nth-child(4) {
+	animation-delay: 1.3s;
+}
+
+.load3 view:nth-child(4) {
+	animation-delay: 1.43s;
+}
+
+@-webkit-keyframes load {
+	0% {
+		opacity: 1;
+	}
+	100% {
+		opacity: 0.2;
+	}
+}
+</style>

+ 36 - 0
src/pages/third/yunexamine/components/none-data/none-data.vue

@@ -0,0 +1,36 @@
+<template>
+	<view class="none">
+		<text class="s-icon iconfont icon-warn"></text>
+		<text class="s-text">暂无数据</text>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {},
+	data() {
+		return {};
+	},
+	methods: {}
+};
+</script>
+
+<style lang="scss">
+.none {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	width: 100%;
+	height: 750upx;
+	.s-icon {
+		color: #ddd;
+		font-size: 100upx;
+	}
+	.s-text {
+		margin-top: 20upx;
+		color: #999;
+		font-size: 24upx;
+	}
+}
+</style>

+ 182 - 0
src/pages/third/yunexamine/components/upload-avatar/upload-avatar.vue

@@ -0,0 +1,182 @@
+<template>
+	<view class="component" v-if="show">
+		<view class="sub-mask" @click="onClose"></view>
+		<view class="sub-block">
+			<view class="b-main">
+				<view class="s-title">设置头像</view>
+				<view class="s-avatar">
+					<image :src="image || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/user.png'" mode="aspectFill"></image>
+				</view>
+				<!-- #ifdef  MP-WEIXIN -->
+				<button class="s-upload open-data-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">上传头像</button>
+				<!-- #endif -->
+				<!-- #ifdef  H5 -->
+				<view class="s-upload" @click="onUploadAvatar">上传头像</view>
+				<!-- #endif -->
+			</view>
+			<view class="b-save" @click="onClose">
+				<view class="s-icon iconfont icon-roundclose"></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		updateUserInfo
+	} from '../../service/api/user.js'
+	import {
+		uploadImage
+	} from '../../service/api/common.js'
+	import {
+		compressImage
+	} from '../../common/util.js'
+	export default {
+		props: {
+			show: {
+				type: Boolean,
+				default: false
+			},
+			image: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {};
+		},
+		methods: {
+			onChange(image) {
+				this.$emit('bindChange', image);
+			},
+			onClose() {
+				this.$emit('bindClose');
+			},
+			onChooseAvatar(e) {
+				console.log('onChooseAvatar', e)
+				let imageUrl = e.detail.avatarUrl
+				uploadImage(imageUrl).then(([err, res]) => {
+					console.log('uploadImage', err, res)
+					if (!err) {
+						updateUserInfo(res.url).then(([err, res]) => {
+							console.log('updateUserInfo', err, res)
+							if (!err) {
+								this.onChange(imageUrl)
+							}
+						})
+					}
+				})
+			},
+			onUploadAvatar(e) {
+				console.log('onUploadAvatar', e)
+				uni.chooseImage({
+					count: 1, // 1张
+					sizeType: ['compressed'], // 压缩图
+				}).then(([err, res]) => {
+					console.log('chooseImage', err, res)
+					if (!err) {
+						compressImage(res.tempFiles[0], 500, 500).then(([err, res]) => {
+							console.log('compressImage', err, res)
+							if (!err) {
+								let imageUrl = res
+								uploadImage(imageUrl).then(([err, res]) => {
+									console.log('uploadImage', err, res)
+									if (!err) {
+										updateUserInfo(res.url).then(([err, res]) => {
+											console.log('updateUserInfo', err, res)
+											if (!err) {
+												this.onChange(imageUrl)
+											}
+										})
+									}
+								})
+							}
+						})
+					}
+				})
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	.sub-mask {
+		position: fixed;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		background: rgba($color: #000000, $alpha: 0.6);
+	}
+
+	.sub-block {
+		position: fixed;
+		left: 50%;
+		top: 48%;
+		transform: translate(-50%, -50%);
+		z-index: 100;
+		width: 400upx;
+
+		image {
+			width: 100%;
+		}
+	}
+
+	.b-main {
+		padding: 40upx 0;
+		background: #fff;
+		border-radius: 20upx;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.s-title {
+			font-size: 28upx;
+			color: #383838;
+		}
+
+		.s-avatar {
+			margin-top: 40upx;
+			width: 160upx;
+			height: 160upx;
+
+			image {
+				width: 160upx;
+				height: 160upx;
+				border-radius: 80upx;
+			}
+		}
+
+		.s-upload {
+			padding: 0 40upx;
+			margin-top: 40upx;
+			height: 80upx;
+			line-height: 80upx;
+			border-radius: 40upx;
+			text-align: center;
+			color: #fff;
+			font-size: 28upx;
+			background: #DA5650;
+
+			&.disable {
+				background: rgba($color: #DA5650, $alpha: 0.2);
+			}
+		}
+	}
+
+	.b-save {
+		position: absolute;
+		bottom: -120upx;
+		left: 0;
+		width: 100%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.s-icon {
+			padding: 20upx;
+			color: #fff;
+			font-size: 56upx;
+		}
+	}
+</style>

+ 16 - 0
src/pages/third/yunexamine/mixins/auth.js

@@ -0,0 +1,16 @@
+import {
+	silentReload
+} from '../service/request/main.js'
+import { useAppStore } from '../store/index.js'
+
+export default {
+	onLoad(options) {
+		console.log('onload mixin auth')
+		const appStore = useAppStore()
+		if (!appStore.getAccessToken) {
+			console.info('needLogin onload')
+			console.info('needLogin onload', 'silentReload')
+			silentReload()
+		}
+	}
+}

+ 77 - 0
src/pages/third/yunexamine/mixins/common.js

@@ -0,0 +1,77 @@
+import {
+	buildPageUrl,
+	formatDate
+} from '../common/util.js'
+
+export default {
+	data() {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1]
+		const currentPageUrl = buildPageUrl(currentPage.route, currentPage.options)
+		return {
+			currentPage,
+			currentPageUrl
+		}
+	},
+	onLoad(options) {
+		console.log('page onLoad', this.currentPageUrl);
+	},
+	methods: {
+		onCopy(text) {
+			uni.setClipboardData({
+				data: text + ''
+			}).then(([err, res]) => {
+				console.log('uni.setClipboardData', err, res);
+				if (!err) {
+					uni.showToast({
+						title: '复制成功',
+						icon: 'none',
+					});
+				}
+			});
+		}
+	},
+	computed: {
+		costTime() {
+			return (seconds, type) => {
+				let second = parseInt(seconds)
+				if (second <= 0) {
+					if (type === 'countdown') {
+						return '已结束'
+					}
+					return '0秒'
+				}
+				let minute = 0 // 初始化分
+				let hour = 0 // 初始化小时
+				if (second >= 60) { // 如果秒数大于等于60,将秒数转换成分钟
+					minute = parseInt(second / 60) // 获取分钟
+					second = parseInt(second % 60) // 获取秒数
+					if (minute >= 60) { // 如果分钟大于等于60,将分钟转换成小时
+						hour = parseInt(minute / 60) // 获取小时
+						minute = parseInt(minute % 60) // 获取小时后取余的分
+					}
+				}
+				if (type === 'countdown') {
+					return `${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}:${second.toString().padStart(2,'0')}`
+				}
+				let desc = ''
+				if (hour > 0) {
+					desc = hour + '小时'
+				}
+				if (minute > 0 || hour > 0) {
+					desc += minute + '分'
+				}
+				desc += second + '秒'
+				return desc
+			}
+		},
+		timeFormat() {
+			return (timestamp) => {
+				if (!timestamp) {
+					return ''
+				}
+				return formatDate(timestamp, 'yyyy-MM-dd hh:mm:ss')
+			}
+		},
+	},
+}

+ 544 - 0
src/pages/third/yunexamine/pages/examine/answer.vue

@@ -0,0 +1,544 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="process">
+				<view class="b-state">
+					<view class="b-count">
+						<view class="s-tit1">答题进度</view>
+						<view class="s-num">{{state.session_seq}}</view>
+						<view class="s-tit2">/{{state.total_count}}</view>
+					</view>
+					<view class="b-time" v-if="state.session_expired != -1">
+						<view class="s-tit">剩余时间</view>
+						<view class="s-time">{{costTime(state.session_expired, 'countdown')}}</view>
+					</view>
+				</view>
+				<view class="b-bar">
+					<view class="s-percent" :style="barPercent"></view>
+				</view>
+			</view>
+			<view class="answer">
+				<view class="b-head">
+					<view class="s-type">{{state.type_text}}</view>
+					<view class="s-time"></view>
+				</view>
+				<view class="b-title">
+					<view class="s-num">{{state.session_seq}}.</view>
+					<text class="s-title">{{state.title}}<text class="s-score">({{state.score}}分)</text></text>
+				</view>
+				<view class="b-options">
+					<view class="b-item" v-for="(item, index) in state.optionList" :key="index" @click="onChoose(index)"
+						:class="optionState(index)">
+						<view class="s-num">{{optionNum(index)}}.</view>
+						<view class="s-cont">{{item.label}}</view>
+						<view class="s-state iconfont" :class="optionIcon(index)"></view>
+					</view>
+				</view>
+				<view class="b-result" :class="resultIcon" v-if="state.state == 2">
+					<view class="b-right" v-if="state.show_answer">
+						<view class="s-tit">正确答案:</view>
+						<view class="s-cont">{{trueAnswer}}</view>
+					</view>
+					<view class="b-mine" v-if="state.show_answer">
+						<view class="s-tit">已选答案:</view>
+						<view class="s-cont">{{userAnswer}}</view>
+					</view>
+				</view>
+				<view class="b-explain" v-if="state.state == 2 && state.show_answer && state.explain">
+					<text>解析:{{state.explain}}</text>
+				</view>
+				<view class="b-action">
+					<view class="b-next" @click="onFinish" v-if="state.session_status == 2">查看成绩</view>
+					<view class="b-next" @click="onFinish" v-else-if="state.session_status == 3">超时结束</view>
+					<view class="b-submit" :class="btnState" @click="onSubmitAnswer" v-else-if="state.state == 1">
+						提交答案</view>
+					<view class="b-next" :class="btnState" @click="onGetNew" v-else-if="state.state == 2">
+						进入下一题</view>
+					<view class="b-home" @click="onHome">稍后再答</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" color="#ffffff" :top="500"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getNew,
+		getResult,
+		submitAnswer
+	} from '../../service/api/examine.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+  import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				hasLoad: false,
+				session_id: 0,
+				state: {
+					optionList: [],
+					trueAnswer: [],
+					userAnswer: []
+				},
+				disable: {
+					submit: false
+				},
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			console.log('onLoad', options)
+			this.from = options.from
+			switch (this.from) {
+				case 'exercise':
+					uni.setNavigationBarTitle({
+						title: '进行练习'
+					})
+					break
+				case 'review':
+					uni.setNavigationBarTitle({
+						title: '进行复习'
+					})
+					break
+			}
+			this.session_id = options.session_id
+
+			this.onGetNew()
+
+			// 倒计时
+			if (this.state.session_expired != -1) {
+				setInterval(() => {
+					if (this.state.session_expired > 0) {
+						this.state.session_expired--
+					}
+				}, 1000)
+			}
+		},
+		computed: {
+			barPercent() {
+				return 'width: ' + (this.state.session_seq * 100 / this.state.total_count) + '%'
+			},
+			optionNum() {
+				return (index) => {
+					return String.fromCharCode(64 + (index + 1))
+				}
+			},
+			optionState() {
+				return (index) => {
+					console.log('optionState', index)
+					let optionRight = false
+					let optionWrong = false
+					if (this.state.state == 2) {
+						if (this.state.trueAnswer.length > 0) {
+							if (this.state.type === 'radio' || this.state.type === 'yesorno') {
+								if (this.state.trueAnswer.includes(index)) {
+									optionRight = true
+								} else if (this.state.userAnswer.includes(index)) {
+									optionWrong = true
+								}
+							} else {
+								if (this.state.trueAnswer.includes(index)) {
+									optionRight = true
+								} else if ((this.state.userAnswer.includes(index) && !this.state.trueAnswer.includes(
+										index)) ||
+									(this.state.trueAnswer.includes(index) && !this.state.userAnswer.includes(index))
+								) {
+									optionWrong = true
+								}
+							}
+						} else {
+							if (this.state.userAnswer.includes(index)) {
+								if (this.state.result == 2) {
+									optionRight = true
+								} else {
+									optionWrong = true
+								}
+							}
+						}
+					}
+
+					return {
+						'f-select': this.state.userAnswer.includes(index),
+						'f-right': optionRight,
+						'f-wrong': optionWrong
+					}
+				}
+			},
+			optionIcon() {
+				return (index) => {
+					console.log('optionIcon', index)
+					return {
+						'icon-roundcheck': this.state.userAnswer.includes(index),
+						// 'icon-roundcheckfill': this.state.trueAnswer.includes(index),
+					}
+				}
+			},
+			resultIcon() {
+				return {
+					'f-right': this.state.result == 2,
+					'f-wrong': this.state.result == 3,
+					'f-timeout': this.state.result == 4,
+					'f-timeout-end': this.state.result == 5,
+				}
+			},
+			trueAnswer() {
+				let options = this.state.trueAnswer.map(item => {
+					return String.fromCharCode(64 + (parseInt(item) + 1))
+				})
+				return options.join('、')
+			},
+			userAnswer() {
+				let options = this.state.userAnswer.map(item => {
+					return String.fromCharCode(64 + (parseInt(item) + 1))
+				})
+				return options.join('、')
+			},
+			btnState() {
+				return {
+					'disable': this.disable.submit === true || this.state.userAnswer.length === 0
+				}
+			},
+		},
+		methods: {
+			onChoose(index) {
+				if (this.state.state != 1) {
+					return
+				}
+				let valueIndex = this.state.userAnswer.indexOf(index)
+				if (valueIndex !== -1) {
+					this.state.userAnswer.splice(valueIndex, 1)
+				} else {
+					if (this.state.type === 'checkbox') {
+						this.state.userAnswer.push(index)
+					} else {
+						this.state.userAnswer = [index]
+					}
+				}
+				if (this.state.userAnswer.length === 0) {
+					this.disable.submit = true
+				} else {
+					this.disable.submit = false
+				}
+			},
+			onGetNew() {
+				getNew(this.session_id).then(([err, res]) => {
+					console.log('getNew', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+						this.state.userAnswer = [] // 初始化提交的答案
+					} else {
+						if (err.data.code == 502) { // 超时结束
+							this.onFinish()
+						} else {
+							if (!this.hasLoad) {
+								this.loadingType = 3
+								this.loadingText = err.data.msg || '加载失败'
+							}
+						}
+					}
+				})
+			},
+			onSubmitAnswer() {
+				if (this.disable.submit) {
+					return
+				}
+				if (this.state.userAnswer.length === 0) {
+					return showToast('还未选择答案')
+				}
+				this.disable.submit = true
+				submitAnswer(this.state.id, this.state.userAnswer.join(',')).then(([err, res]) => {
+					console.log('submitAnswer', err, res)
+					this.disable.submit = false
+					if (!err) {
+						this.state = res
+						if (this.state.session_status == 3) {
+							showToast('答题会话已超时结束')
+						}
+					}
+				})
+			},
+			onFinish() {
+				uni.reLaunch({
+					url: '/pages/third/yunexamine/pages/examine/summary?from=' + this.from + '&session_id=' + this.session_id
+				})
+			},
+			onHome() {
+				uni.reLaunch({
+					url: '/pages/third/yunexamine/pages/home/index'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #DA5650;
+	}
+
+	.wrap {}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.process {
+		display: flex;
+		flex-direction: column;
+
+		.b-state {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			color: #fff;
+
+			.b-count {
+				display: flex;
+				align-items: center;
+
+				.s-tit1,
+				.s-tit2 {
+					font-size: 24upx;
+				}
+
+				.s-num {
+					margin: 0 10upx;
+					font-size: 32upx;
+				}
+			}
+
+			.b-time {
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 24upx;
+				}
+
+				.s-time {
+					margin-left: 10upx;
+					font-size: 28upx;
+				}
+			}
+		}
+
+		.b-bar {
+			margin-top: 10upx;
+			background: rgba(255, 255, 255, 0.5);
+			width: 100%;
+			height: 24upx;
+			position: relative;
+
+			.s-percent {
+				position: absolute;
+				left: 0;
+				bottom: 0;
+				width: 30%;
+				height: 24upx;
+				display: inline-block;
+				background: #fff;
+			}
+		}
+	}
+
+	.answer {
+		margin-top: 50upx;
+		padding: 40upx;
+		background: #fff;
+		border-radius: 20upx;
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+
+		.b-head {
+			display: flex;
+			align-items: center;
+			// justify-content: space-around;
+
+			.s-type {
+				width: fit-content;
+				background: #DA5650;
+				padding: 0 20upx;
+				font-size: 28upx;
+				color: #fff;
+				height: 50upx;
+				line-height: 50upx;
+			}
+
+			.s-time {
+				/* 题目剩余时间 */
+			}
+		}
+
+		.b-title {
+			margin-top: 20upx;
+			font-size: 30upx;
+			display: flex;
+
+			.s-num {
+				font-weight: bold;
+			}
+
+			.s-title {
+				margin-left: 10upx;
+				flex: 1;
+			}
+
+			.s-score {
+				margin-left: 20upx;
+				color: #666;
+			}
+		}
+
+		.b-options {
+			margin-top: 40upx;
+
+			.b-item {
+				margin-bottom: 20upx;
+				padding: 20upx;
+				font-size: 28upx;
+				display: flex;
+				// align-items: center;
+				background: #F7F7F7;
+
+				&:last-child {
+					margin-bottom: 0;
+				}
+
+				&.f-select {
+					background: #ECF3FE;
+					color: #338ADA;
+				}
+
+				&.f-right {
+					background: #ECF6F0;
+					color: #1EAB69;
+				}
+
+				&.f-wrong {
+					background: #FFEBEB;
+					color: #D13B42;
+				}
+
+				.s-num {
+					font-weight: bold;
+				}
+
+				.s-cont {
+					margin-left: 10upx;
+					flex: 1;
+				}
+
+				.s-state {
+					margin-top: 2upx;
+					margin-left: 10upx;
+					font-size: 32upx;
+				}
+			}
+		}
+
+		.b-result {
+			padding: 80upx 0;
+			min-height: 40upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			background-repeat: no-repeat;
+			background-size: 180upx auto;
+			background-position: center center;
+
+			&.f-right {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_right.png);
+			}
+
+			&.f-wrong {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_wrong.png);
+			}
+
+			&.f-timeout {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_timeout.png);
+			}
+
+			&.f-timeout-end {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_timeout_end.png);
+			}
+
+			.b-right,
+			.b-mine {
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 26upx;
+					color: #333;
+				}
+
+				.s-cont {
+					margin-left: 10upx;
+					font-weight: bold;
+					color: #DA5650;
+					font-size: 36upx;
+				}
+			}
+		}
+
+		.b-explain {
+			font-size: 28upx;
+			color: #808080;
+		}
+
+		.b-action {
+			margin-top: 50upx;
+			display: flex;
+			flex-direction: column;
+
+			.b-submit {
+				height: 100upx;
+				line-height: 100upx;
+				text-align: center;
+				color: #fff;
+				font-size: 32upx;
+				background: #DA5650;
+				border-radius: 50upx;
+				letter-spacing: 10upx;
+
+				&.disable {
+					background: rgba($color: #DA5650, $alpha: 0.7);
+				}
+			}
+
+			.b-next {
+				height: 100upx;
+				line-height: 100upx;
+				text-align: center;
+				color: #fff;
+				font-size: 32upx;
+				background: #FF8D1A;
+				border-radius: 50upx;
+				letter-spacing: 10upx;
+
+				&.disable {
+					background: rgba($color: #FF8D1A, $alpha: 0.2);
+				}
+			}
+
+			.b-home {
+				margin-top: 40upx;
+				text-align: center;
+				color: #808080;
+				font-size: 28upx;
+			}
+		}
+	}
+</style>

+ 183 - 0
src/pages/third/yunexamine/pages/examine/history.vue

@@ -0,0 +1,183 @@
+<template>
+	<view class="wrap">
+		<view class="main">
+			<view class="questions" v-if="state.items.length > 0">
+				<view class="b-item" v-for="(item, index) in state.items" :key="index" @click="onReview(item.id)">
+					<view class="b-main">
+						<view class="b-title">
+							<view class="s-num">{{index + 1}}.</view>
+							<text class="s-cont">【{{item.type_text}}】{{item.title}}</text>
+						</view>
+						<view class="b-info">
+							<view class="s-flag f-right" v-if="item.result == 2">{{item.result_text}}</view>
+							<view class="s-flag f-wrong" v-else-if="item.result == 3">{{item.result_text}}</view>
+							<view class="s-flag f-timeout" v-else-if="item.result == 4">{{item.result_text}}</view>
+							<view class="s-flag f-timeout-end" v-else-if="item.result == 5">{{item.result_text}}</view>
+							<view class="s-time">{{costTime(item.cost_time)}}</view>
+						</view>
+					</view>
+					<view class="b-more">
+						<view class="iconfont icon-more"></view>
+					</view>
+				</view>
+			</view>
+			<!-- 加载中 -->
+			<load-more :loadingType="loadingType" :loadingText="loadingText" color="#ffffff"></load-more>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getHistory
+	} from '../../service/api/examine.js';
+	import mixinsCommon from '../../mixins/common.js';
+	import mixinsAuth from '../../mixins/auth.js';
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+		data() {
+			return {
+				state: {
+					items: []
+				},
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+    components: {
+      LoadMore
+    },
+		onLoad(options) {
+			this.sessionId = options.session_id
+			this.loadHistory(true)
+		},
+		onReachBottom() {
+			if (this.loadingType !== 1 && this.loadingType !== 2) {
+				this.loadHistory();
+			}
+		},
+		methods: {
+			loadHistory(refresh) {
+				console.log('loadHistory', refresh);
+				if (refresh) {
+					this.page = 1;
+					this.state.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getHistory(this.sessionId, this.page, 10).then(([err, res]) => {
+					console.log('getHistory', err, res)
+					this.loadingType = -1;
+					if (!err) {
+						if (res.items.length > 0) {
+							this.state.items = [...this.state.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无数据'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			onReview(recordId) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/review?record_id=' + recordId
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #DA5650;
+	}
+
+	.wrap {}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.questions {
+		.b-item {
+			display: flex;
+			margin-bottom: 40upx;
+			background: #fff;
+			border-radius: 20upx;
+			padding: 20upx;
+
+			.b-main {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+
+				.b-title {
+					display: flex;
+
+					.s-num {
+						font-size: 28upx;
+					}
+
+					.s-cont {
+						margin-left: 0upx;
+						font-size: 28upx;
+					}
+				}
+
+				.b-info {
+					margin-top: 10upx;
+					display: flex;
+					align-items: center;
+					justify-content: space-between;
+
+					.s-flag {
+						font-size: 24upx;
+						padding: 0 10upx;
+						height: 50upx;
+						line-height: 50upx;
+						color: #fff;
+						border-radius: 10upx;
+
+						&.f-right {
+							background: #1EAB69;
+						}
+
+						&.f-wrong {
+							background: #D13B42;
+						}
+						
+						&.f-timeout {
+							background: #FF8D1A;
+						}
+						
+						&.f-timeout-end {
+							background: #338ADA;
+						}
+					}
+
+					.s-time {
+						color: #DA5650;
+						font-size: 24upx;
+					}
+				}
+			}
+
+			.b-more {
+				margin-left: 10upx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				color: #E5E5E5;
+			}
+		}
+	}
+</style>

+ 478 - 0
src/pages/third/yunexamine/pages/examine/review.vue

@@ -0,0 +1,478 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="process">
+				<view class="b-state">
+					<view class="b-title" v-if="onlyView">
+						<view class="s-tit">上次答题</view>
+					</view>
+					<view class="b-count" v-else>
+						<view class="s-tit1">答题进度</view>
+						<view class="s-num">{{state.session_seq}}</view>
+						<view class="s-tit2">/{{state.total_count}}</view>
+					</view>
+					<view class="b-time">
+						<view class="s-tit">花费时间</view>
+						<view class="s-time">{{costTime(state.cost_time)}}</view>
+					</view>
+				</view>
+				<view class="b-bar">
+					<view class="s-percent" :style="barPercent"></view>
+				</view>
+			</view>
+			<view class="answer">
+				<view class="b-type">{{state.type_text}}</view>
+				<view class="b-title">
+					<view class="s-num" v-if="!onlyView">{{state.session_seq}}.</view>
+					<text class="s-title">{{state.title}}<text class="s-score">({{state.score}}分)</text></text>
+				</view>
+				<view class="b-options">
+					<view class="b-item" v-for="(item, index) in state.optionList" :key="index"
+						:class="optionState(index)">
+						<view class="s-num">{{optionNum(index)}}.</view>
+						<view class="s-cont">{{item.label}}</view>
+						<view class="s-state iconfont" :class="optionIcon(index)"></view>
+					</view>
+				</view>
+				<view class="b-result" :class="resultIcon">
+					<view class="b-right" v-if="state.show_answer">
+						<view class="s-tit">正确答案:</view>
+						<view class="s-cont">{{trueAnswer}}</view>
+					</view>
+					<view class="b-mine" v-if="state.show_answer">
+						<view class="s-tit">已选答案:</view>
+						<view class="s-cont">{{userAnswer}}</view>
+					</view>
+				</view>
+				<view class="b-explain" v-if="state.show_answer && state.explain">
+					<text>解析:{{state.explain}}</text>
+				</view>
+				<view class="b-action" v-if="!onlyView">
+					<view class="b-last" :class="{ disabled: disabled.hasLast }" @click="onLast">上一题</view>
+					<view class="b-next" :class="{ disabled: disabled.hasNext }" @click="onNext">下一题</view>
+				</view>
+				<view class="b-home" @click="onHome" v-if="!onlyView">回到首页</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" color="#ffffff" :top="500"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getResult
+	} from '../../service/api/examine.js';
+  import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js';
+	import mixinsAuth from '../../mixins/auth.js';
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				hasLoad: false,
+				onlyView: false,
+				state: {},
+				disabled: {
+					hasLast: false,
+					hasNext: false
+				},
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			this.onlyView = options.view ? true : false
+			this.loadResult(options.record_id)
+		},
+		computed: {
+			barPercent() {
+				return 'width: ' + (this.state.session_seq * 100 / this.state.total_count) + '%'
+			},
+			optionNum() {
+				return (index) => {
+					return String.fromCharCode(64 + (index + 1))
+				}
+			},
+			optionState() {
+				return (index) => {
+					console.log('optionState', index)
+					let optionRight = false
+					let optionWrong = false
+					if (this.state.state == 2) {
+						if (this.state.trueAnswer.length > 0) {
+							if (this.state.type === 'radio' || this.state.type === 'yesorno') {
+								if (this.state.trueAnswer.includes(index)) {
+									optionRight = true
+								} else if (this.state.userAnswer.includes(index)) {
+									optionWrong = true
+								}
+							} else {
+								if (this.state.trueAnswer.includes(index)) {
+									optionRight = true
+								} else if ((this.state.userAnswer.includes(index) && !this.state.trueAnswer.includes(
+										index)) ||
+									(this.state.trueAnswer.includes(index) && !this.state.userAnswer.includes(index))
+								) {
+									optionWrong = true
+								}
+							}
+						}  else {
+							if (this.state.userAnswer.includes(index)) {
+								if (this.state.result == 2) {
+									optionRight = true
+								} else {
+									optionWrong = true
+								}
+							}
+						}
+					}
+
+					return {
+						'f-right': optionRight,
+						'f-wrong': optionWrong
+					}
+				}
+			},
+			optionIcon() {
+				return (index) => {
+					console.log('optionIcon', index)
+					return {
+						'icon-roundcheck': this.state.userAnswer.includes(index),
+						// 'icon-roundcheckfill': this.state.trueAnswer.includes(index),
+					}
+				}
+			},
+			resultIcon() {
+				return {
+					'f-right': this.state.result == 2,
+					'f-wrong': this.state.result == 3,
+					'f-timeout': this.state.result == 4,
+					'f-timeout-end': this.state.result == 5,
+				}
+			},
+			trueAnswer() {
+				let options = this.state.trueAnswer.map(item => {
+					return String.fromCharCode(64 + (parseInt(item) + 1))
+				})
+				return options.join('、')
+			},
+			userAnswer() {
+				let options = this.state.userAnswer.map(item => {
+					return String.fromCharCode(64 + (parseInt(item) + 1))
+				})
+				return options.join('、')
+			},
+		},
+		methods: {
+			loadResult(recordId) {
+				if (!recordId) {
+					showToast('没有更多记录')
+					return
+				}
+				getResult(recordId).then(([err, res]) => {
+					console.log('getResult', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+						if (!res.last_id) {
+							this.disabled.hasLast = true
+						} else {
+							this.disabled.hasLast = false
+						}
+						if (!res.next_id) {
+							this.disabled.hasNext = true
+						} else {
+							this.disabled.hasNext = false
+						}
+					} else {
+						if (!this.hasLoad) {
+							this.loadingType = 3
+							this.loadingText = err.data.msg || '加载失败'
+						}
+					}
+				})
+			},
+			onLast() {
+				this.loadResult(this.state.last_id)
+			},
+			onNext() {
+				this.loadResult(this.state.next_id)
+			},
+			onHome() {
+				uni.reLaunch({
+					url: '/pages/third/yunexamine/pages/home/index'
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #DA5650;
+	}
+
+	.wrap {}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.process {
+		display: flex;
+		flex-direction: column;
+
+		.b-state {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			color: #fff;
+
+			.b-title {
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 24upx;
+				}
+			}
+
+			.b-count {
+				display: flex;
+				align-items: center;
+
+				.s-tit1,
+				.s-tit2 {
+					font-size: 24upx;
+				}
+
+				.s-num {
+					margin: 0 10upx;
+					font-size: 32upx;
+				}
+			}
+
+			.b-time {
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 24upx;
+				}
+
+				.s-time {
+					margin-left: 10upx;
+					font-size: 28upx;
+				}
+			}
+		}
+
+		.b-bar {
+			margin-top: 10upx;
+			background: rgba(255, 255, 255, 0.5);
+			width: 100%;
+			height: 24upx;
+			position: relative;
+
+			.s-percent {
+				position: absolute;
+				left: 0;
+				bottom: 0;
+				width: 30%;
+				height: 24upx;
+				display: inline-block;
+				background: #fff;
+			}
+		}
+	}
+
+	.answer {
+		margin-top: 50upx;
+		padding: 40upx;
+		background: #fff;
+		border-radius: 20upx;
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+
+		.b-type {
+			width: fit-content;
+			background: #DA5650;
+			padding: 0 20upx;
+			font-size: 28upx;
+			color: #fff;
+			height: 50upx;
+			line-height: 50upx;
+		}
+
+		.b-title {
+			margin-top: 20upx;
+			font-size: 30upx;
+			display: flex;
+
+			.s-num {
+				font-weight: bold;
+			}
+
+			.s-title {
+				margin-left: 10upx;
+				flex: 1;
+				color: #333;
+			}
+
+			.s-score {
+				margin-left: 20upx;
+				color: #666;
+			}
+		}
+
+		.b-options {
+			margin-top: 40upx;
+
+			.b-item {
+				margin-bottom: 20upx;
+				padding: 20upx;
+				font-size: 28upx;
+				display: flex;
+				// align-items: center;
+				background: #F7F7F7;
+
+				&:last-child {
+					margin-bottom: 0;
+				}
+
+				&.f-select {
+					background: #ECF3FE;
+					color: #338ADA;
+				}
+
+				&.f-right {
+					background: #ECF6F0;
+					color: #1EAB69;
+				}
+
+				&.f-wrong {
+					background: #FFEBEB;
+					color: #D13B42;
+				}
+
+				.s-num {
+					font-weight: bold;
+				}
+
+				.s-cont {
+					margin-left: 10upx;
+					flex: 1;
+				}
+
+				.s-state {
+					margin-left: 10upx;
+					font-size: 36upx;
+				}
+			}
+		}
+
+		.b-result {
+			padding: 80upx 0;
+			min-height: 40upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			background-repeat: no-repeat;
+			background-size: 180upx auto;
+			background-position: center center;
+
+			&.f-right {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_right.png);
+			}
+
+			&.f-wrong {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_wrong.png);
+			}
+
+			&.f-timeout {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_timeout.png);
+			}
+
+			&.f-timeout-end {
+				background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/result_timeout_end.png);
+			}
+
+			.b-right,
+			.b-mine {
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 26upx;
+					color: #333;
+				}
+
+				.s-cont {
+					margin-left: 10upx;
+					font-weight: bold;
+					color: #DA5650;
+					font-size: 36upx;
+				}
+			}
+		}
+
+		.b-explain {
+			font-size: 28upx;
+			color: #808080;
+		}
+
+		.b-action {
+			margin-top: 50upx;
+			display: flex;
+
+			.b-last {
+				flex: 1;
+				height: 78upx;
+				line-height: 78upx;
+				text-align: center;
+				color: #DA5650;
+				font-size: 32upx;
+				border: 1upx solid #DA5650;
+				border-radius: 50upx;
+				letter-spacing: 5upx;
+
+				&.disabled {
+					color: #DA5650a3;
+					border: 1upx solid #DA5650a3;
+				}
+			}
+
+			.b-next {
+				margin-left: 50upx;
+				flex: 1;
+				height: 80upx;
+				line-height: 80upx;
+				text-align: center;
+				color: #fff;
+				font-size: 32upx;
+				background: #DA5650;
+				border-radius: 50upx;
+				letter-spacing: 5upx;
+
+				&.disabled {
+					background: #da5650a3;
+				}
+			}
+		}
+
+		.b-home {
+			margin-top: 40upx;
+			text-align: center;
+			color: #808080;
+			font-size: 28upx;
+		}
+	}
+</style>

+ 265 - 0
src/pages/third/yunexamine/pages/examine/summary.vue

@@ -0,0 +1,265 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="result" v-if="state.state && state.state != 1">
+				<view class="b-summary">
+					<view class="b-title" v-if="state.state == 2">获得成绩</view>
+					<view class="b-title" v-else-if="state.state == 3">超时结束</view>
+					<view class="b-score">
+						<view class="s-num">{{state.total_score || 0}}</view>
+						<view class="s-tit">分</view>
+					</view>
+					<text class="break-score" v-if="state.break_sid > 0">上次{{breakTips}},恭喜打破记录!</text>
+				</view>
+				<view class="b-detail">
+					<view class="b-item" v-if="state.scenario">
+						<view class="s-tit">答题模式</view>
+						<view class="s-cont">{{state.scenario_text}}</view>
+					</view>
+					<view class="b-item">
+						<view class="s-tit">总答题数</view>
+						<view class="s-cont">{{state.answer_count}}道</view>
+					</view>
+					<view class="b-item">
+						<view class="s-tit">答对数</view>
+						<view class="s-cont">{{state.right_count}}道</view>
+					</view>
+					<view class="b-item">
+						<view class="s-tit">答错数</view>
+						<view class="s-cont">{{state.wrong_count}}道</view>
+					</view>
+					<view class="b-item" v-if="state.state == 3">
+						<view class="s-tit">超时题数</view>
+						<view class="s-cont f-focus">{{state.timeout_count}}道</view>
+					</view>
+					<view class="b-item">
+						<view class="s-tit">答题耗时</view>
+						<view class="s-cont f-focus">{{costTime(state.cost_time)}}</view>
+					</view>
+					<view class="b-item">
+						<view class="s-tit">答题时间</view>
+						<view class="s-cont">{{startTime}}</view>
+					</view>
+				</view>
+				<view class="b-action">
+					<view class="b-history" @click="onHistory">查看历史</view>
+					<view class="b-home" @click="onHome">返回首页</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" color="#ffffff" :top="500"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getSummary
+	} from '../../service/api/examine.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import {
+		formatDate
+	} from '../../common/util.js'
+  import { showToast } from '../../common/util.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				hasLoad: false,
+				session_id: 0,
+				state: {},
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			switch (options.from) {
+				case 'last':
+					uni.setNavigationBarTitle({
+						title: '上次答题'
+					})
+					break
+				case 'max':
+					uni.setNavigationBarTitle({
+						title: '最好成绩'
+					})
+					break
+				case 'exercise':
+					uni.setNavigationBarTitle({
+						title: '练习结果'
+					})
+					break
+				case 'review':
+					uni.setNavigationBarTitle({
+						title: '复习结果'
+					})
+					break
+			}
+			this.session_id = options.session_id
+			getSummary(options.session_id).then(([err, res]) => {
+				console.log('getSummary', err, res)
+				if (!err) {
+					this.hasLoad = true
+					this.loadingType = -1
+					this.state = res
+				} else {
+					if (!this.hasLoad) {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+					showToast('答题未完成').then(([err, res]) => {
+						uni.reLaunch({
+							url: '/pages/third/yunexamine/pages/home/index'
+						})
+					})
+				}
+			})
+		},
+		computed: {
+			breakTips() {
+				if (this.state.total_score > this.state.break_score) {
+					return this.state.break_score + '分'
+				} else {
+					return this.costTime(this.state.break_time)
+				}
+			},
+			startTime() {
+				return formatDate(this.state.created_at, 'yyyy-MM-dd hh:mm:ss')
+			}
+		},
+		methods: {
+			onHistory() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/history?session_id=' + this.session_id
+				})
+			},
+			onHome() {
+				uni.reLaunch({
+					url: '/pages/third/yunexamine/pages/home/index'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #DA5650;
+	}
+
+	.wrap {}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.result {
+		margin-top: 50upx;
+		padding: 40upx;
+		background: #fff;
+		border-radius: 20upx;
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+
+		.b-summary {
+			margin-top: 20upx;
+			font-size: 30upx;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+
+			.b-title {
+				font-size: 36upx;
+			}
+
+			.b-score {
+				margin-top: 20upx;
+				display: flex;
+				align-items: baseline;
+
+				.s-num {
+					color: #FF8D1A;
+					font-weight: bold;
+					font-size: 80upx;
+				}
+
+				.s-tit {
+					margin-left: 10upx;
+					font-size: 28upx;
+				}
+			}
+
+			.break-score {
+				margin-top: 20upx;
+				color: #DA5650;
+				font-size: 32upx;
+				font-weight: bold;
+			}
+		}
+
+		.b-detail {
+			margin-top: 40upx;
+			padding: 20upx;
+			background: #F7F7F7;
+			border-radius: 10upx;
+
+			.b-item {
+				font-size: 28upx;
+				height: 60upx;
+				line-height: 60upx;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+
+				.s-tit {
+					color: #808080;
+				}
+
+				.s-cont {
+					color: #383838;
+				}
+
+				.f-focus {
+					color: #FF8D1A;
+				}
+			}
+		}
+
+		.b-action {
+			margin-top: 50upx;
+			display: flex;
+
+			.b-history {
+				flex: 1;
+				height: 78upx;
+				line-height: 78upx;
+				text-align: center;
+				color: #DA5650;
+				font-size: 32upx;
+				border: 1upx solid #DA5650;
+				border-radius: 50upx;
+				letter-spacing: 5upx;
+			}
+
+			.b-home {
+				margin-left: 50upx;
+				flex: 1;
+				height: 80upx;
+				line-height: 80upx;
+				text-align: center;
+				color: #fff;
+				font-size: 32upx;
+				background: #DA5650;
+				border-radius: 50upx;
+				letter-spacing: 5upx;
+			}
+		}
+	}
+</style>

+ 394 - 0
src/pages/third/yunexamine/pages/examine/top.vue

@@ -0,0 +1,394 @@
+<template>
+	<view class="wrap">
+		<view class="top-bg">
+			<image src="https://xy.wenlvti.net/app_static/yunexamine/imgs/top_bg.png" mode="widthFix"></image>
+		</view>
+		<view class="main">
+			<scroll-view class="top-scroll" scroll-y scroll-with-animation :lower-threshold="100"
+				@scrolltolower="onScrollToLower" :style="{ height: scrollHeight + 'px' }">
+				<!-- 数据列表 -->
+				<view class="user-list">
+					<view class="b-item" v-for="(item, index) in userData.items" :key="index">
+						<view class="b-sort">
+							<image :src="'https://xy.wenlvti.net/app_static/yunexamine/imgs/top_'+(index+1)+'.png'" mode="aspectFit" v-if="index < 3">
+							</image>
+							<text v-else>{{index+1}}</text>
+						</view>
+						<view class="b-avatar">
+							<image :src="item.avatar_url || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/user.png'" mode="aspectFill"></image>
+						</view>
+						<view class="b-user">
+							<view class="s-name">{{item.name}}</view>
+							<view class="s-org">{{item.group_name}}</view>
+						</view>
+						<view class="b-data">
+							<view class="s-score">{{item.max_score}}分</view>
+							<view class="s-time">{{costTime(item.cost_time)}}</view>
+						</view>
+					</view>
+				</view>
+				<!-- 加载中 -->
+				<load-more :loadingType="loadingType" :loadingText="loadingText"></load-more>
+			</scroll-view>
+		</view>
+		<view class="navbar">
+			<view class="b-mine" @click="onGoLast('max', mineData.max_sid)">
+				<view class="s-tit1">第</view>
+				<view class="s-num">{{mineData.group_top}}</view>
+				<view class="s-tit2">名</view>
+				<view class="s-mine">
+					<image :src="mineData.avatar_url || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/user.png'" mode="aspectFill"></image>
+				</view>
+				<view class="s-score">{{mineData.max_score}}分</view>
+				<view class="s-time">{{costTime(mineData.cost_time)}}</view>
+			</view>
+			<picker class="b-group" @change="onFilter" :value="groupIndex" :range="groupList" range-key="name">
+				<view class="b-main">
+					<view class="s-icon iconfont icon-unfold"></view>
+					<view class="s-name">{{ groupList[groupIndex].name }}排行</view>
+				</view>
+			</picker>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getTopList,
+		getMineTop
+	} from '../../service/api/examine.js'
+  import { rpx2px } from '../../common/util.js'
+	import Util from '../../common/util'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import noneData from './../components/none-data/none-data.vue'
+	import loadMore from './../components/load-more/load-more.vue'
+	import { useExamineStore } from '../../store/module/examine.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      loadMore
+    },
+		data() {
+			return {
+				hasLoad: false,
+				refreshTime: 0,
+				scrollHeight: 100,
+				userData: {
+					items: []
+				},
+				mineData: {},
+				groupList: [{
+					id: 0,
+					name: '全部'
+				}],
+				groupIndex: 0,
+				loadingType: 1,
+				loadingText: '',
+				examineStore: useExamineStore()
+			}
+		},
+		onLoad(options) {
+			console.log('onLoad', options)
+			this.initScrollHeight()
+			if (this.examineStore.topGroupId == -1) {
+				const groupId = options.groupId || 0
+				this.refreshTime = Util.getTimestamp()
+				this.loadTopList(groupId, true)
+				this.loadMineTop(groupId, true)
+			}
+		},
+		onShow() {
+			console.log('onShow', this.examineStore.topGroupId)
+			const groupId = this.examineStore.topGroupId
+			if (groupId != -1) {
+				this.examineStore.delTopGroupId()
+				this.refreshTime = Util.getTimestamp()
+				this.loadTopList(groupId, true)
+				this.loadMineTop(groupId, true)
+			} else {
+				if (this.hasLoad) {
+					const timestamp = Util.getTimestamp()
+					if (timestamp - this.refreshTime >= 5) {
+						this.refreshTime = timestamp
+						this.loadTopList(this.groupList[this.groupIndex].id, true)
+						this.loadMineTop(this.groupList[this.groupIndex].id, false)
+					}
+				}
+			}
+		},
+		computed: {},
+		methods: {
+			loadTopList(group, refresh) {
+				console.log('loadTopList', group, refresh)
+				if (refresh) {
+					this.page = 1
+					this.userData.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getTopList(group, this.page, 10).then(([err, res]) => {
+					console.log('getTopList', err, res)
+					this.loadingType = -1
+					this.hasLoad = true
+					if (!err) {
+						if (res.items.length > 0) {
+							this.userData.items = [...this.userData.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无数据'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			loadMineTop(group, init) {
+				console.log('loadMineTop', group, init)
+				getMineTop(group).then(([err, res]) => {
+					console.log('getMineTop', err, res)
+					if (!err) {
+						this.mineData = res.mineData
+						this.groupList = res.userGroups
+						if (init) {
+							this.groupList.map((item, index) => {
+								if (item.id == group) {
+									this.groupIndex = index
+								}
+							})
+						}
+					}
+				})
+			},
+			onFilter(e) {
+				console.log('onFilter', e.detail.value)
+				this.groupIndex = e.detail.value
+				this.loadTopList(this.groupList[this.groupIndex].id, true)
+				this.loadMineTop(this.groupList[this.groupIndex].id, false)
+			},
+			initScrollHeight() {
+				const systemInfo = uni.getSystemInfoSync()
+				const windowHeight = systemInfo.windowHeight
+				const searchBarHeight = rpx2px(360)
+				this.scrollHeight = windowHeight - searchBarHeight
+			},
+			onScrollToLower(e) {
+				console.log('onScrollToLower', e)
+				this.loadTopList(this.groupList[this.groupIndex].id, false)
+			},
+			onGoLast(from, sessionId) {
+				if (!sessionId) {
+					return
+				}
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/summary?from=' + from + '&session_id=' + sessionId
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.top-bg {
+		position: relative;
+
+		image {
+			width: 100%;
+			height: 460upx;
+		}
+	}
+
+	.main {
+		padding: 40upx 40upx 160upx 40upx;
+		display: flex;
+		flex-direction: column;
+	}
+
+	.top-scroll {
+		margin-top: -250upx;
+		padding-bottom: 20upx;
+		position: relative;
+		width: 670upx;
+		height: 100%;
+		background: #fff;
+		border-radius: 20upx;
+		z-index: 100;
+	}
+
+	.user-list {
+		padding: 40upx 40upx 20upx 10upx;
+
+		.b-item {
+			margin-bottom: 50upx;
+			height: 100upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+
+			.b-sort {
+				width: 100upx;
+				display: flex;
+				justify-content: center;
+				align-items: center;
+
+				text {
+					font-size: 36upx;
+					color: #808080;
+				}
+
+				image {
+					width: 60upx;
+					height: 60upx;
+				}
+			}
+
+			.b-avatar {
+				width: 100upx;
+				height: 100upx;
+
+				image {
+					width: 100upx;
+					height: 100upx;
+					border-radius: 50%;
+				}
+			}
+
+			.b-user {
+				margin-left: 20upx;
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+
+				.s-name {
+					font-size: 32upx;
+				}
+
+				.s-org {
+					margin-top: 10upx;
+					font-size: 26upx;
+					color: #A6A6A6;
+				}
+			}
+
+			.b-data {
+				display: flex;
+				flex-direction: column;
+				align-items: flex-end;
+
+				.s-score {
+					color: #FFC300;
+					font-size: 40upx;
+				}
+
+				.s-time {
+					margin-top: 10upx;
+					color: #A6A6A6;
+					font-size: 24upx;
+				}
+			}
+
+		}
+	}
+
+	.navbar {
+		position: fixed;
+		z-index: 999;
+		left: 0;
+		/* #ifdef  H5 */
+		bottom: calc(100upx + env(safe-area-inset-bottom));
+		/* #endif */
+		/* #ifdef  MP-WEIXIN */
+		bottom: 0upx;
+		/* #endif */
+		width: 100%;
+		height: 120upx;
+		display: flex;
+		align-items: center;
+		background: #fff;
+		border-top: 1upx solid #F7F7F7;
+		justify-content: space-between;
+
+		.b-mine {
+			margin-left: 20upx;
+			display: flex;
+			align-items: center;
+
+			.s-tit1,
+			.s-tit2 {
+				color: #808080;
+				font-size: 28upx;
+			}
+
+			.s-num {
+				margin: 0 10upx;
+				font-size: 50upx;
+				color: #DA5650;
+				font-weight: bold;
+			}
+
+			.s-mine {
+				margin-left: 20upx;
+				width: 70upx;
+				height: 70upx;
+
+				image {
+					width: 70upx;
+					height: 70upx;
+					border-radius: 50%;
+				}
+			}
+
+			.s-score {
+				margin-left: 20upx;
+				color: #FFC300;
+				font-size: 36upx;
+			}
+
+			.s-time {
+				margin-left: 20upx;
+				color: #808080;
+				font-size: 24upx;
+			}
+		}
+
+		.b-group {
+			margin-right: 20upx;
+
+			.b-main {
+				padding: 0 20upx;
+				display: flex;
+				align-items: center;
+				
+				border-radius: 30upx;
+				height: 60upx;
+
+				.s-icon {
+					color: #A6A6A6;
+					font-size: 30upx;
+				}
+
+				.s-name {
+					margin-left: 10upx;
+					font-size: 28upx;
+					color: #A6A6A6;
+				}
+			}
+		}
+	}
+</style>

+ 247 - 0
src/pages/third/yunexamine/pages/examine/wrong.vue

@@ -0,0 +1,247 @@
+<template>
+	<view class="wrap">
+		<view class="main">
+			<view class="questions" v-if="state.items.length > 0">
+				<view class="b-item" v-for="(item, index) in state.items" :key="index"
+					@click="onReview(item.last_record_id)">
+					<view class="b-main">
+						<view class="b-title">
+							<view class="s-num">{{index + 1}}.</view>
+							<text class="s-cont">【{{item.type_text}}】{{item.title}}</text>
+						</view>
+					</view>
+					<view class="b-more">
+						<view class="iconfont icon-more"></view>
+					</view>
+				</view>
+			</view>
+			<!-- 加载中 -->
+			<load-more :loadingType="loadingType" :loadingText="loadingText" color="#ffffff"></load-more>
+		</view>
+		<view class="navbar">
+			<view class="b-start" :class="{ disable: disable.submit }" @click="onContinue"
+				v-if="examineData.review_session && examineData.review_session.status == 1">继续复习</view>
+			<view class="b-start" :class="{ disable: disable.submit }" @click="onStartReview"
+				v-else-if="examineData.exercise_chance > 0">错题复习</view>
+			<view class="b-start disable" v-else>复习完成</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getHomeData
+	} from '../../service/api/page.js'
+	import {
+		startReview,
+		getWrongList
+	} from '../../service/api/examine.js';
+	import mixinsCommon from '../../mixins/common.js';
+	import mixinsAuth from '../../mixins/auth.js';
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				state: {
+					items: []
+				},
+				hasLoad: false,
+				examineData: {},
+				loadingType: 1,
+				loadingText: '',
+				disable: {
+					submit: false
+				},
+			}
+		},
+		onLoad(options) {
+			this.loadHomeData()
+			this.loadRecordList(true)
+		},
+		onReachBottom() {
+			if (this.loadingType !== 1 && this.loadingType !== 2) {
+				this.loadRecordList();
+			}
+		},
+		methods: {
+			loadHomeData() {
+				getHomeData().then(([err, res]) => {
+					console.log('getHomeData', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.examineData = res.examineData
+					}
+				})
+			},
+			loadRecordList(refresh) {
+				console.log('loadRecordList', refresh);
+				if (refresh) {
+					this.page = 1;
+					this.state.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getWrongList(this.page, 10).then(([err, res]) => {
+					console.log('getWrongList', err, res)
+					this.loadingType = -1;
+					if (!err) {
+						if (res.items.length > 0) {
+							this.state.items = [...this.state.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无数据'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			onReview(recordId) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/review?view=1&record_id=' + recordId
+				})
+			},
+			onStartReview() {
+				if (this.disable.submit) {
+					return
+				}
+				this.disable.submit = true
+				startReview().then(([err, res]) => {
+					console.log('startReview', err, res)
+					this.disable.submit = false
+					if (!err) {
+						if (res.state === 'new' || res.state === 'exist') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/answer?from=review&session_id=' + res.session_id
+							})
+						} else if (res.state === 'end') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/summary?from=review&session_id=' + res.session_id
+							})
+						}
+					}
+				})
+			},
+			onContinue() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/answer?from=review&session_id=' + this.examineData.review_session.session_id
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #DA5650;
+	}
+
+	.wrap {
+		padding-bottom: 100upx;
+	}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.questions {
+		.b-item {
+			display: flex;
+			margin-bottom: 40upx;
+			background: #fff;
+			border-radius: 20upx;
+			padding: 20upx;
+
+			.b-main {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+
+				.b-title {
+					display: flex;
+
+					.s-num {
+						font-size: 28upx;
+					}
+
+					.s-cont {
+						margin-left: 0upx;
+						font-size: 28upx;
+					}
+				}
+
+				.b-info {
+					margin-top: 10upx;
+					display: flex;
+					align-items: center;
+					justify-content: space-between;
+
+					.s-flag {
+						font-size: 24upx;
+						padding: 0 10upx;
+						height: 50upx;
+						line-height: 50upx;
+						color: #666;
+						border-radius: 10upx;
+					}
+
+					.s-time {
+						color: #DA5650;
+						font-size: 24upx;
+					}
+				}
+			}
+
+			.b-more {
+				margin-left: 10upx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				color: #E5E5E5;
+			}
+		}
+	}
+	
+	.navbar {
+		position: fixed;
+		bottom: 0;
+		left: 0;
+		width: 750upx;
+		height: 100upx;
+		background: #fff;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+	
+		.b-start {
+			width: 100%;
+			height: 100upx;
+			line-height: 100upx;
+			text-align: center;
+			color: #fff;
+			font-size: 32upx;
+			background: #FF8D1A;
+			letter-spacing: 10upx;
+	
+			&.disable {
+				background: rgba($color: #FF8D1A, $alpha: 0.8);
+			}
+	
+			&.f-finish {
+				font-size: 28upx;
+				background: #FFFFFF;
+				color: #DA5650;
+			}
+		}
+	}
+</style>

+ 303 - 0
src/pages/third/yunexamine/pages/gift/detail.vue

@@ -0,0 +1,303 @@
+<template>
+	<view class="wrap" v-if="hasLoad">
+		<view class="carousel">
+			<swiper indicator-dots circular="true" duration="400">
+				<swiper-item class="swiper-item" v-for="(item, index) in state.photo_list" :key="index">
+					<view class="image-wrapper">
+						<image :src="item" mode="aspectFill"></image>
+					</view>
+				</swiper-item>
+			</swiper>
+		</view>
+		<view class="info">
+			<view class="b-title">{{state.name}}</view>
+			<view class="b-data">
+				<view class="s-worth" v-if="state.order_worth > 0">价值 {{state.order_worth}}元</view>
+				<view class="s-count">
+					<view class="s-left">库存 {{state.count_left}}</view>
+					<view class="s-gain">已兑 {{state.count_gain}}</view>
+				</view>
+			</view>
+		</view>
+		<view class="specs" v-if="state.spec_list">
+			<view class="b-title">选择规格</view>
+			<view class="b-items">
+				<view class="b-item" v-for="(item, index) in state.spec_list" :key="index"
+					:class="{'f-active': index == specIndex}" @click="onSpecSelect(index)">{{item.name}}</view>
+			</view>
+			<view class="b-data">
+				<view class="s-left">库存 {{state.spec_list[specIndex].count_left}}</view>
+				<view class="s-gain">已兑 {{state.spec_list[specIndex].count_gain}}</view>
+			</view>
+		</view>
+		<view class="content">
+			<view class="b-title">
+				<view class="iconfont icon-line"></view>
+				<view class="s-tit">奖品详情</view>
+				<view class="iconfont icon-line"></view>
+			</view>
+			<view class="b-rich">
+				<rich-text :nodes="goodContent"></rich-text>
+			</view>
+		</view>
+		<view class="navbar">
+			<view class="b-action">
+				<view class="s-exchange f-disable" v-if="state.order_state == 1">兑换未开始</view>
+				<view class="s-exchange f-disable" v-else-if="state.order_state == 2">兑换已结束</view>
+				<view class="s-exchange f-disable" v-else-if="state.order_state == 3">兑换机会不足</view>
+				<view class="s-exchange f-disable" v-else-if="state.order_state == 4">已兑换完</view>
+				<view class="s-exchange" @click="onExchange()" v-else>立即兑换</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getDetail
+	} from '../../service/api/gift.js'
+	import {
+		formatRichText
+	} from './../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      loadMore
+    },
+		data() {
+			return {
+				state: {},
+				specIndex: 0,
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: '',
+
+			}
+		},
+		onLoad(options) {
+			this.loadDetail(options.gift_id)
+		},
+		computed: {
+			goodContent() {
+				return formatRichText(this.state.content)
+			},
+		},
+		methods: {
+			loadDetail(giftId) {
+				getDetail(giftId).then(([err, res]) => {
+					console.log('getDetail', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+			onExchange() {
+				let specId = this.state.spec_list ? this.state.spec_list[this.specIndex].id : 0
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/exchange?gift_id=' + this.state.id + '&spec_id=' + specId
+				})
+			},
+			onSpecSelect(index) {
+				this.specIndex = index
+				const specData = this.state.spec_list[this.specIndex]
+				this.state.order_amount = specData.amount
+				this.state.order_worth = specData.worth
+				if (this.state.order_state == 0) {
+					if (specData.count_left <= 0) {
+						this.state.order_state = 4
+					}
+				} else if (this.state.order_state == 4) {
+					if (specData.count_left > 0) {
+						this.state.order_state = 0
+					}
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+
+	.carousel {
+		height: 500upx;
+		position: relative;
+
+		swiper {
+			height: 100%;
+		}
+
+		.swiper-item {
+			display: flex;
+			justify-content: center;
+			align-content: center;
+			height: 320upx;
+			overflow: hidden;
+
+			image {
+				width: 100%;
+				height: 100%;
+			}
+		}
+
+		.image-wrapper {
+			width: 100%;
+			height: 100%;
+		}
+	}
+
+	.info {
+		padding: 30upx;
+		background: #fff;
+
+		.b-title {
+			font-size: 28upx;
+		}
+
+		.b-data {
+			margin-top: 20upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			.s-worth {
+				color: #999;
+				font-size: 24upx;
+			}
+
+			.s-count {
+				color: #999;
+				font-size: 24upx;
+				display: flex;
+				align-items: center;
+
+				.s-left {}
+
+				.s-gain {
+					margin-left: 40upx;
+				}
+			}
+		}
+	}
+
+	.specs {
+		margin-top: 30upx;
+		padding: 30upx;
+		background: #fff;
+
+		.b-title {
+			font-size: 30upx;
+		}
+
+		.b-items {
+			margin-top: 30upx;
+
+			.b-item {
+				margin-right: 20upx;
+				padding: 0 40upx;
+				height: 60upx;
+				line-height: 60upx;
+				display: inline-block;
+				background: #fff;
+				border: 1upx solid #E4EAF1;
+				color: #667085;
+				font-size: 26upx;
+
+				&.f-active {
+					background: #DA5650;
+					border: none;
+					color: #fff;
+				}
+			}
+		}
+
+		.b-data {
+			margin-top: 30upx;
+			color: #666;
+			font-size: 24upx;
+			display: flex;
+			align-items: center;
+
+			.s-left {}
+
+			.s-gain {
+				margin-left: 40upx;
+			}
+		}
+	}
+
+	.content {
+		margin-top: 30upx;
+
+		.b-title {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+
+			.iconfont {
+				color: #ccc;
+			}
+
+			.s-tit {
+				margin: 0 20upx;
+				font-size: 24upx;
+				color: #666;
+			}
+		}
+
+		.b-rich {
+			margin-top: 30upx;
+			padding: 30upx 0;
+			background: #fff;
+			font-size: 28upx;
+		}
+	}
+
+	.navbar {
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100upx;
+		background: #fff;
+		// border-top: 1upx solid #f6f6f6;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.b-action {
+			flex: 1;
+
+			.s-exchange {
+				background: #DA5650;
+				height: 100upx;
+				padding: 0 60upx;
+				line-height: 100upx;
+				font-size: 30upx;
+				color: #fff;
+				letter-spacing: 4upx;
+				text-align: center;
+
+				&.f-disable {
+					background: rgba(218, 86, 80, 0.8)
+				}
+			}
+		}
+	}
+</style>

+ 488 - 0
src/pages/third/yunexamine/pages/gift/exchange.vue

@@ -0,0 +1,488 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="gift-view">
+				<view class="b-left">
+					<image :src="giftData.cover_url" mode="aspectFill"></image>
+				</view>
+				<view class="b-main">
+					<view class="b-title">{{giftData.name}}</view>
+					<view class="b-spec" v-if="giftData.spec_data">
+						<view class="s-tit">规格:</view>
+						<view class="s-value">{{giftData.spec_data.name}}</view>
+					</view>
+					<view class="b-fund">
+						<view class="s-worth" v-if="giftData.order_worth > 0">价值 {{giftData.order_worth}}元</view>
+					</view>
+				</view>
+			</view>
+			<template v-if="giftData.type == 1">
+				<view class="box">
+					<view class="b-row">
+						<view class="b-tit">领取方式</view>
+						<view class="b-cont f-option">
+							<picker @change="onFilter" :value="gainMethodIndex" :range="gainMethodList"
+								range-key="name">
+								<view class="b-main">
+									<view class="s-cont">{{gainMethodList[gainMethodIndex].name}}</view>
+									<view class="iconfont icon-more"></view>
+								</view>
+							</picker>
+						</view>
+					</view>
+					<view class="b-row"
+						v-if="gainMethodList[gainMethodIndex].value == 1 && giftData.offline_place">
+						<view class="b-tit">领取地点</view>
+						<view class="b-cont f-text">{{giftData.offline_place}}</view>
+					</view>
+					<view class="b-address" v-if="gainMethodList[gainMethodIndex].value == 1">
+						<view class="b-row">
+							<view class="b-tit">收件人:</view>
+							<view class="b-cont">
+								<input type="text" value="" placeholder="选填" @input="onInputText"
+									data-field="name" />
+							</view>
+						</view>
+						<view class="b-row">
+							<view class="b-tit">手机号:</view>
+							<view class="b-cont">
+								<input type="text" value="" placeholder="选填" @input="onInputText"
+									data-field="phone" />
+							</view>
+						</view>
+					</view>
+					<view class="b-address" v-if="gainMethodList[gainMethodIndex].value == 2">
+						<view class="b-row">
+							<view class="b-tit">收件人:</view>
+							<view class="b-cont">
+								<input type="text" value="" placeholder="请填写收件人姓名" @input="onInputText"
+									data-field="name" />
+							</view>
+						</view>
+						<view class="b-row">
+							<view class="b-tit">手机号:</view>
+							<view class="b-cont">
+								<input type="text" value="" placeholder="请填写收件人手机号" @input="onInputText"
+									data-field="phone" />
+							</view>
+						</view>
+						<view class="b-row">
+							<view class="b-tit">所在地区:</view>
+							<view class="b-cont f-area">
+								<area-picker v-on:getAddressResultObj="getAddressResultObj"
+									:addressList.sync="addressList">
+									<div class="b-areas">
+										<view class="b-item">
+											<view class="s-cont">{{addressList[0]}}</view>
+										</view>
+										<view class="b-item">
+											<view class="s-cont">{{addressList[1]}}</view>
+										</view>
+										<view class="b-item">
+											<view class="s-cont">{{addressList[2]}}</view>
+										</view>
+									</div>
+								</area-picker>
+							</view>
+						</view>
+						<view class="b-row">
+							<view class="b-tit">详细地址:</view>
+							<view class="b-cont f-textarea">
+								<textarea placeholder="请填写详细地址,包括门牌号" auto-height @input="onInputText"
+									data-field="address"></textarea>
+							</view>
+						</view>
+					</view>
+					<view class="line"></view>
+					<view class="b-row f-fund">
+						<view class="b-tit">剩余机会:</view>
+						<view class="b-cont">
+							<view class="s-num">{{chanceData.chance_left}}</view>
+						</view>
+					</view>
+				</view>
+				<view class="box">
+					<view class="b-row">
+						<view class="b-tit">留言:</view>
+						<view class="b-cont f-textarea">
+							<textarea placeholder="这里填写内容" auto-height @input="onInputText"
+								data-field="note"></textarea>
+						</view>
+					</view>
+				</view>
+			</template>
+			<view class="box" v-else>
+				<view class="b-row f-fund">
+					<view class="b-tit">剩余机会:</view>
+					<view class="b-cont">
+						<view class="s-num">{{chanceData.chance_left}}</view>
+					</view>
+				</view>
+			</view>
+			<view class="navbar">
+				<view class="b-action">
+					<view class="s-exchange" @click="onExchange" v-if="amountEnough">确认兑换</view>
+					<view class="s-exchange f-disable" v-else>机会不足</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getPreview,
+		exchange
+	} from '../../service/api/gift.js';
+  import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js';
+	import mixinsAuth from '../../mixins/auth.js';
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				disable: {
+					submit: false
+				},
+				giftData: {},
+				chanceData: {},
+				orderData: {},
+				formData: {
+					gift_id: 0,
+					gift_spec_id: 0,
+					gain_method: 0,
+					name: '',
+					phone: '',
+					province: '',
+					city: '',
+					area: '',
+					address: '',
+					note: '',
+				},
+				gainMethodList: [{
+					value: 2,
+					name: '快递邮寄'
+				}],
+				gainMethodIndex: 0,
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: '',
+				addressList: ['', '', ''],
+			}
+		},
+		onLoad(options) {
+			this.loadPreview(options.gift_id, options.spec_id)
+			this.formData.gift_id = options.gift_id
+			this.formData.gift_spec_id = options.spec_id
+		},
+		computed: {
+			amountEnough() {
+				return this.chanceData.chance_left > 0
+			}
+		},
+		methods: {
+			loadPreview(giftId, specId) {
+				getPreview(giftId, specId).then(([err, res]) => {
+					console.log('getPreview', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.giftData = res.gift_data
+						this.chanceData = res.chance_data
+						this.orderData = res.order_data
+						// 领取方式
+						if (this.giftData.gain_methods) {
+							this.gainMethodList = []
+							if (this.giftData.gain_methods.indexOf('2') !== -1) {
+								this.gainMethodList.push({
+									value: 2,
+									name: '快递邮寄'
+								})
+							}
+							if (this.giftData.gain_methods.indexOf('1') !== -1) {
+								this.gainMethodList.push({
+									value: 1,
+									name: '线下领取'
+								})
+							}
+						}
+						this.formData.gain_method = this.gainMethodList[this.gainMethodIndex].value
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+			onExchange() {
+				if (this.disable.submit) {
+					return
+				}
+				if (this.giftData.type == 1) {
+					if (this.formData.gain_method == 2) {
+						if (!this.formData.name) {
+							return showToast('收件人不能为空')
+						}
+						if (!this.formData.phone) {
+							return showToast('手机号不能为空')
+						}
+						if (!this.formData.province || !this.formData.city || !this.formData.area) {
+							return showToast('所在地区不能为空')
+						}
+						if (!this.formData.address) {
+							return showToast('详细地址不能为空')
+						}
+					}
+				} else {
+					this.formData.gain_method = 3
+				}
+				this.disable.submit = true
+				exchange(this.formData).then(([err, res]) => {
+					console.log('exchange', err, res)
+					this.disable.submit = false
+					if (!err) {
+						uni.reLaunch({
+							url: '/pages/third/yunexamine/pages/gift/order_detail?submit=1&id=' + res.id
+						})
+					}
+				})
+			},
+			// 输入内容
+			onInputText(e) {
+				console.log('onInputText', e)
+				this.formData[e.target.dataset.field] = e.detail.value;
+			},
+			getAddressResultObj(e) {
+				console.log('getAddressResultObj', e)
+				this.addressList = [e.province.name, e.city.name, e.county.name]
+				this.formData.province = e.province.name
+				this.formData.city = e.city.name
+				this.formData.area = e.county.name
+			},
+			onFilter(e) {
+				console.log('onFilter', e.detail.value)
+				this.gainMethodIndex = e.detail.value
+				this.formData.gain_method = this.gainMethodList[this.gainMethodIndex].value
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.gift-view {
+		background: #fff;
+		padding: 30upx;
+		display: flex;
+
+		.b-left {
+			width: 160upx;
+			height: 160upx;
+
+			image {
+				width: 160upx;
+				height: 160upx;
+			}
+		}
+
+		.b-main {
+			margin-left: 30upx;
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+
+			.b-title {
+				font-size: 28upx;
+				color: #333;
+			}
+
+
+			.b-spec {
+				margin-top: 20upx;
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 24upx;
+					color: #999;
+				}
+
+				.s-value {
+					margin-left: 10upx;
+					font-size: 24upx;
+					color: #666;
+				}
+			}
+
+			.b-fund {
+				margin-top: 20upx;
+				display: flex;
+				align-items: baseline;
+
+				.s-worth {
+					font-size: 24upx;
+					color: #999;
+				}
+			}
+		}
+	}
+
+	.box {
+		margin-top: 30upx;
+		background: #fff;
+
+		.b-row {
+			padding: 30upx;
+			display: flex;
+			justify-content: space-between;
+
+			&.f-fund {
+				padding: 40upx 30upx;
+
+				.b-cont {
+					.s-num {
+						font-size: 32upx;
+						color: #DA5650;
+					}
+				}
+			}
+
+			.b-tit {
+				font-size: 28upx;
+				width: 160upx;
+			}
+
+			.b-cont {
+				// flex: 1;
+				display: flex;
+				align-items: center;
+
+				&.f-option {
+					.b-main {
+						display: flex;
+						align-items: center;
+
+						.s-cont {
+							font-size: 28upx;
+						}
+
+						.iconfont {
+							margin-left: 10upx;
+						}
+					}
+				}
+
+
+				&.f-textarea {
+					textarea {
+						width: 530upx;
+						font-size: 28upx;
+					}
+				}
+
+				&.f-text {
+					font-size: 28upx;
+					color: #666;
+				}
+			}
+		}
+
+		.b-address {
+			.b-row {
+				border-bottom: 1upx solid #eee;
+
+				&:last-child {
+					border-bottom: none;
+				}
+
+				.b-tit {
+					color: #666;
+					width: 160upx;
+				}
+
+				.b-cont {
+					flex: 1;
+
+					&.f-area {
+						.b-areas {
+							display: flex;
+							align-items: center;
+						}
+
+						.b-item {
+							margin-right: 40upx;
+
+							.s-cont {
+								font-size: 28upx;
+							}
+						}
+					}
+
+					textarea {
+						width: 530upx;
+						height: 200upx;
+					}
+				}
+			}
+		}
+
+		.line {
+			width: 100%;
+			height: 16upx;
+			background-image: url(https://xy.wenlvti.net/app_static/yunexamine/imgs/line-bg.png);
+			background-repeat: repeat-x;
+			background-size: auto 100%;
+
+			image {
+				width: 100%;
+				height: 20upx;
+			}
+		}
+
+		input {
+			font-size: 28upx;
+		}
+
+		.input-placeholder {
+			font-size: 28upx;
+		}
+	}
+
+	.navbar {
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100upx;
+		background: #fff;
+		border-top: 1upx solid #f6f6f6;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.b-action {
+			flex: 1;
+			.s-exchange {
+				background: #DA5650;
+				height: 100upx;
+				padding: 0 60upx;
+				line-height: 100upx;
+				font-size: 30upx;
+				color: #fff;
+				letter-spacing: 4upx;
+				text-align: center;
+			}
+		}
+	}
+</style>

+ 231 - 0
src/pages/third/yunexamine/pages/gift/express_feed.vue

@@ -0,0 +1,231 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="box">
+				<view class="b-row">
+					<view class="b-tit">快递公司</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.company}}</text>
+					</view>
+				</view>
+				<view class="b-row">
+					<view class="b-tit">快递单号</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.number}}</text>
+						<text class="s-copy" @click="onCopy(state.number)">点击复制</text>
+					</view>
+				</view>
+				<view class="b-row">
+					<view class="b-tit">发货时间</view>
+					<view class="b-cont">
+						<text class="s-text">{{timeFormat(state.deployed_at)}}</text>
+					</view>
+				</view>
+				<view class="b-row">
+					<view class="b-tit">当前状态</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.state_text}}</text>
+					</view>
+				</view>
+			</view>
+			<view class="feed-detail">
+				<view class="address-data">
+					<view class="iconfont icon-pos"></view>
+					<text class="b-address">
+						<text class="s-tit">收货地址:</text>
+						<text class="s-cont">{{state.address}}</text>
+					</text>
+				</view>
+				<view class="feed-data">
+					<view class="iconfont icon-feed"></view>
+					<view class="feed-list">
+						<view class="b-row" v-for="(item, index) in state.track_list" :key="index">
+							<view class="s-cont">{{item.context}}</view>
+							<view class="s-time">{{item.time}}</view>
+						</view>
+						<view class="b-text" v-if="state.track_list.length === 0">暂无数据</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getExpressFeed
+	} from '../../service/api/gift.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				state: {},
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			this.loadFeed(options.id)
+		},
+		methods: {
+			loadFeed(recordId) {
+				getExpressFeed(recordId).then(([err, res]) => {
+					console.log('getExpressFeed', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.box {
+		margin-top: 30upx;
+		background: #fff;
+		padding: 15upx 0;
+
+		.b-row {
+			padding: 15upx 30upx;
+			display: flex;
+			// justify-content: space-between;
+
+			.b-tit {
+				font-size: 28upx;
+				width: 180upx;
+				color: #666;
+			}
+
+			.b-cont {
+				display: flex;
+				align-items: center;
+				font-size: 28upx;
+				color: #333;
+			}
+
+			.s-copy {
+				margin-left: 20upx;
+				font-size: 24upx;
+				color: #FFC300;
+			}
+
+			.s-province,
+			.s-city,
+			.s-area {
+				margin-right: 10upx;
+			}
+
+			.s-phone {
+				margin-left: 20upx;
+			}
+
+			.s-btn {
+				padding: 0 30upx;
+				background: #FFC300;
+				color: #fff;
+				font-size: 24upx;
+				height: 60upx;
+				line-height: 60upx;
+				border-radius: 30upx;
+			}
+		}
+	}
+
+	.feed-detail {
+		padding: 30upx;
+		background: #fff;
+		margin-top: 30upx;
+
+		.address-data {
+			display: flex;
+
+			.iconfont {
+				margin-right: 20upx;
+				color: #999;
+				font-size: 36upx;
+			}
+
+			.b-address {
+				color: #999;
+				font-size: 28upx;
+				line-height: 40upx;
+
+				.s-tit {
+					margin-right: 10upx;
+				}
+
+				.s-cont {}
+			}
+		}
+
+		.feed-data {
+			margin-top: 60upx;
+			display: flex;
+
+			.iconfont {
+				margin-right: 20upx;
+				color: #aaa;
+				font-size: 36upx;
+			}
+
+			.feed-list {
+				.b-text {
+					color: #999;
+					font-size: 28upx;
+				}
+
+				.b-row {
+					margin-bottom: 30upx;
+
+					&:last-child {
+						margin-bottom: 0;
+					}
+
+					&:first-child {
+						.s-cont {
+							color: #333;
+						}
+
+						.s-time {
+							color: #666;
+						}
+					}
+
+					.s-cont {
+						color: #999;
+						font-size: 28upx;
+					}
+
+					.s-time {
+						margin-top: 10upx;
+						color: #999;
+						font-size: 26upx;
+					}
+				}
+			}
+		}
+	}
+</style>

+ 295 - 0
src/pages/third/yunexamine/pages/gift/index.vue

@@ -0,0 +1,295 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="user-data" :style="'background-image:url(' + topImage + ')'">
+				<view class="b-chance">
+					<view class="s-tit">兑奖机会</view>
+					<view class="s-num">{{userInfo.gift_chance}}</view>
+					<view class="s-unit">次</view>
+				</view>
+				<view class="b-action" @click="onJumpOrder">
+					<view class="s-btn">兑换订单</view>
+				</view>
+				<view class="b-state">
+					<view class="s-tit f-wait" v-if="giftData.gift_state == 0">兑奖未开始</view>
+					<view class="s-tit f-ing" v-else-if="giftData.gift_state == 1">兑奖进行中</view>
+					<view class="s-tit f-end" v-else-if="giftData.gift_state == 2">兑奖已结束</view>
+				</view>
+			</view>
+			<view class="all-gifts">
+				<view class="b-items" v-if="allList.length > 0">
+					<view class="b-item" v-for="(item, index) in allList" :key="index" @click="onJumpGift(item.id)">
+						<view class="b-left">
+							<image :src="item.cover_url" mode="aspectFill"></image>
+						</view>
+						<view class="b-main">
+							<view class="b-title too-long">{{item.name}}</view>
+							<view class="b-center">
+								<view class="b-fund">
+									<view class="s-worth" v-if="item.worth > 0">价值 {{item.worth}}元</view>
+								</view>
+								<view class="b-action">
+									<view class="s-action" v-if="item.count_left > 0">兑换</view>
+									<view class="s-action f-disable" v-else>已兑完</view>
+								</view>
+							</view>
+							<view class="b-amount">
+								<view class="s-left">库存 {{item.count_left}}</view>
+								<view class="s-gain">已兑 {{item.count_gain}}</view>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="b-empty" v-else>
+					暂无奖品
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getWrapData
+	} from '../../service/api/gift.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import loadMore from './../components/load-more/load-more.vue'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      loadMore
+    },
+		data() {
+			return {
+				giftData: {},
+				userInfo: {},
+				allList: [],
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: '',
+				display: {
+					avatarChange: false
+				},
+				appStore: useAppStore()
+			}
+		},
+		onShow() {
+			this.loadWrapData()
+		},
+		computed: {
+			topImage() {
+				return this.appStore.getGlobalConfig.gift_top_img || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/gift_top_bg.jpg'
+			}
+		},
+		methods: {
+			loadWrapData() {
+				getWrapData().then(([err, res]) => {
+					console.log('getWrapData', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.giftData = res.giftData
+						this.userInfo = res.userInfo
+						this.allList = res.giftList
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+			onJumpOrder() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/order'
+				})
+			},
+			onJumpGift(giftId) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/detail?gift_id=' + giftId
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.user-data {
+		padding: 160upx 40upx 30upx 40upx;
+		background: #fff;
+		background-image: url('../..https://xy.wenlvti.net/app_static/yunexamine/imgs/gift_top_bg.jpg');
+		background-repeat: no-repeat;
+		background-size: cover;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.b-chance {
+			margin-top: 50upx;
+			display: flex;
+			align-items: center;
+
+			.s-tit {
+				font-size: 28upx;
+				color: #eee;
+			}
+
+			.s-num {
+				margin: 0 20upx;
+				font-size: 60upx;
+				font-weight: bold;
+				color: #FBC315;
+			}
+
+			.s-unit {
+				font-size: 28upx;
+				color: #eee;
+			}
+
+		}
+
+		.b-action {
+			margin-top: 30upx;
+			border: 2upx solid #fff;
+			color: #fff;
+			font-size: 28upx;
+			padding: 0 50upx;
+			height: 60upx;
+			line-height: 60upx;
+			border-radius: 30upx;
+		}
+
+		.b-state {
+			margin-top: 50upx;
+			display: flex;
+			align-items: center;
+
+			.s-tit {
+				color: #fff;
+				font-size: 24upx;
+				padding: 0 20upx;
+				background: #FBC315;
+				height: 60upx;
+				line-height: 60upx;
+				border-radius: 10upx;
+
+				&.f-wait {
+					background: #444c69;
+				}
+
+				&.f-ing {
+					background: #FFC300;
+				}
+
+				&.f-end {
+					background: #c4c4c4;
+				}
+			}
+		}
+	}
+
+	.all-gifts {
+		background: #fff;
+
+		.b-empty {
+			height: 100upx;
+			line-height: 100upx;
+			text-align: center;
+			color: #999;
+			font-size: 28upx;
+		}
+
+		.b-items {
+
+			.b-item {
+				padding: 30upx;
+				border-bottom: 1upx solid #eee;
+				display: flex;
+
+				&:last-child {
+					border-bottom: none;
+				}
+
+				.b-left {
+					width: 160upx;
+					height: 160upx;
+
+					image {
+						width: 160upx;
+						height: 160upx;
+					}
+				}
+
+				.b-main {
+					margin-left: 30upx;
+					flex: 1;
+					display: flex;
+					flex-direction: column;
+
+					.b-title {
+						font-size: 28upx;
+						color: #333;
+						width: 500upx;
+					}
+
+					.b-center {
+						margin-top: 20upx;
+						display: flex;
+						justify-content: space-between;
+
+						.b-fund {
+							display: flex;
+							align-items: baseline;
+
+							.s-worth {
+								font-size: 24upx;
+								color: #999;
+							}
+						}
+
+						.b-action {
+							.s-action {
+								background: #DA5650;
+								color: #fff;
+								font-size: 24upx;
+								padding: 0 30upx;
+								border-radius: 30upx;
+								height: 60upx;
+								line-height: 60upx;
+
+								&.f-disable {
+									background: rgba(218, 86, 80, 0.8)
+								}
+							}
+						}
+					}
+
+					.b-amount {
+						margin-top: 10upx;
+						display: flex;
+						justify-content: space-between;
+
+						.s-left {
+							font-size: 24upx;
+							color: #999;
+						}
+
+						.s-gain {
+							font-size: 24upx;
+							color: #999;
+						}
+					}
+				}
+			}
+		}
+	}
+</style>

+ 363 - 0
src/pages/third/yunexamine/pages/gift/order.vue

@@ -0,0 +1,363 @@
+<template>
+	<view class="wrap">
+		<view class="filter-box">
+			<view class="b-item" v-for="(item, index) in groupList" :key="index"
+				:class="{'f-active': index == groupIndex}" @click="onGroupSelect(index)">
+				<view class="s-tit">{{item.name}}</view>
+			</view>
+		</view>
+		<view class="gift-list" v-if="state.items.length > 0">
+			<view class="b-item" v-for="(item, index) in state.items" :key="index">
+				<view class="b-head">
+					<view class="s-order">订单编号:{{item.number}}</view>
+					<view class="s-status" :class="stateStyle(item.status)">{{item.status_text}}</view>
+				</view>
+				<view class="b-gift" @click="onRecordDetail(item.id)">
+					<view class="b-left">
+						<image :src="item.gift_data.cover_url" mode="aspectFill"></image>
+					</view>
+					<view class="b-main">
+						<view class="b-title">{{item.gift_data.name}}</view>
+						<view class="b-spec" v-if="item.gift_data.spec_data">
+							<view class="s-tit">规格:</view>
+							<view class="s-value">{{item.gift_data.spec_data.name}}</view>
+						</view>
+						<view class="b-fund">
+							<view class="s-worth" v-if="item.gift_data.order_worth">价值 {{item.gift_data.order_worth}}元
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="b-foot">
+					<view class="s-time">兑换时间:{{timeFormat(item.created_at)}}</view>
+				</view>
+				<view class="b-action">
+					<view class="b-btn" @click="onRecordDetail(item.id)">订单详情</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" color="#888888"></load-more>
+		<view class="navbar">
+			<view class="b-action">
+				<view class="s-home" @click="onHome()">回到奖品首页</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getOrderList
+	} from '../../service/api/gift.js'
+	import Util from '../../common/util'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import loadMore from './../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      loadMore
+    },
+		data() {
+			return {
+				hasLoad: false,
+				refreshTime: 0,
+				groupIndex: 0,
+				groupList: [{
+						name: '全部',
+						value: 0
+					},
+					{
+						name: '待发货',
+						value: 1
+					},
+					{
+						name: '已发货',
+						value: 2
+					},
+					{
+						name: '已取消',
+						value: 3
+					},
+				],
+				state: {
+					items: []
+				},
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			this.loadOrderList(true)
+		},
+		onShow() {
+			if (this.hasLoad) {
+				const timestamp = Util.getTimestamp()
+				if (timestamp - this.refreshTime >= 10) {
+					this.refreshTime = timestamp
+					this.loadOrderList(true)
+				}
+			}
+		},
+		onReachBottom() {
+			if (this.loadingType !== 1 && this.loadingType !== 2) {
+				this.loadOrderList();
+			}
+		},
+		computed: {
+			stateStyle() {
+				return (status) => {
+					return {
+						'f-wait': status === 1,
+						'f-done': status === 2,
+						'f-cancel': status === 3,
+					}
+				}
+			},
+		},
+		methods: {
+			loadOrderList(refresh) {
+				console.log('loadOrderList', refresh);
+				if (refresh) {
+					this.page = 1;
+					this.state.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getOrderList(this.groupList[this.groupIndex].value, this.page, 10).then(([err, res]) => {
+					console.log('getList', err, res)
+					this.loadingType = -1
+					this.hasLoad = true
+					if (!err) {
+						if (res.items.length > 0) {
+							this.state.items = [...this.state.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无订单'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			onGroupSelect(index) {
+				this.groupIndex = index
+				this.loadOrderList(true)
+			},
+			onRecordDetail(recordId) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/order_detail?id=' + recordId
+				})
+			},
+			onHome() {
+				uni.switchTab({
+					url: '/pages/third/yunexamine/pages/gift/index'
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding-bottom: 50upx;
+	}
+
+	.filter-box {
+		padding: 30upx 50upx 0 50upx;
+		margin-bottom: 30upx;
+		background: #fff;
+		// border-bottom: 1upx solid #eee;
+		display: flex;
+		align-items: center;
+
+		.b-item {
+			margin: 0 40upx;
+			height: 80upx;
+			line-height: 80upx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 30upx;
+			padding-bottom: 6upx;
+
+			&.f-active {
+				padding-bottom: 0;
+				border-bottom: 6upx solid #DA5650;
+
+				.s-tit {
+					color: #DA5650;
+				}
+			}
+
+			.s-tit {
+				color: #333;
+			}
+		}
+	}
+
+	.gift-list {
+		.b-item {
+			background: #fff;
+			margin-bottom: 30upx;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+
+			.b-head {
+				padding: 20upx 30upx;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				border-bottom: 1upx solid #eee;
+
+				.s-order {
+					color: #666;
+					font-size: 26upx;
+				}
+
+				.s-status {
+					font-size: 28upx;
+
+					&.f-wait {
+						color: #FFC300;
+					}
+
+					&.f-done {
+						color: #DA5650;
+					}
+
+					&.f-cancel {
+						color: #666;
+					}
+				}
+			}
+
+			.b-gift {
+				padding: 30upx;
+				display: flex;
+				border-bottom: 1upx solid #eee;
+
+				.b-left {
+					width: 160upx;
+					height: 160upx;
+
+					image {
+						width: 160upx;
+						height: 160upx;
+					}
+				}
+
+				.b-main {
+					margin-left: 30upx;
+					flex: 1;
+					display: flex;
+					flex-direction: column;
+
+					.b-title {
+						font-size: 28upx;
+						color: #333;
+					}
+
+
+					.b-spec {
+						margin-top: 20upx;
+						display: flex;
+						align-items: center;
+
+						.s-tit {
+							font-size: 24upx;
+							color: #999;
+						}
+
+						.s-value {
+							margin-left: 10upx;
+							font-size: 24upx;
+							color: #666;
+						}
+					}
+
+					.b-fund {
+						margin-top: 20upx;
+						display: flex;
+						align-items: baseline;
+
+						.s-worth {
+							font-size: 24upx;
+							color: #999;
+						}
+					}
+				}
+			}
+
+			.b-foot {
+				padding: 20upx 30upx;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+
+				.s-time {
+					color: #999;
+					font-size: 26upx;
+				}
+			}
+
+
+			.b-action {
+				padding: 20upx 30upx 40upx 30upx;
+				display: flex;
+				align-items: center;
+				justify-content: flex-end;
+
+				.b-btn {
+					padding: 0 20upx;
+					border: 1upx solid #bbb;
+					color: #666;
+					font-size: 26upx;
+					line-height: 60upx;
+					height: 60upx;
+					border-radius: 10upx;
+				}
+			}
+		}
+	}
+	
+	.navbar {
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100upx;
+		background: #fff;
+		// border-top: 1upx solid #f6f6f6;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+	
+		.b-action {
+			flex: 1;
+			.s-home {
+				background: #DA5650;
+				height: 100upx;
+				padding: 0 60upx;
+				line-height: 100upx;
+				font-size: 30upx;
+				color: #fff;
+				letter-spacing: 4upx;
+				text-align: center;
+			}
+		}
+	}
+</style>

+ 480 - 0
src/pages/third/yunexamine/pages/gift/order_detail.vue

@@ -0,0 +1,480 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="state-box" :class="stateStyle">
+				<view class="iconfont" :class="stateIcon"></view>
+				<view class="s-tit">{{state.status_text}}</view>
+			</view>
+			<view class="gift-view" @click="onJumpGift(state.gift_data.id)">
+				<view class="b-left">
+					<image :src="state.gift_data.cover_url" mode="aspectFill"></image>
+				</view>
+				<view class="b-main">
+					<view class="b-title">{{state.gift_data.name}}</view>
+					<view class="b-spec" v-if="state.gift_data.spec_data">
+						<view class="s-tit">规格:</view>
+						<view class="s-value">{{state.gift_data.spec_data.name}}</view>
+					</view>
+					<view class="b-fund">
+						<view class="s-worth" v-if="state.gift_data.order_worth > 0">价值 {{state.gift_data.order_worth}}元</view>
+					</view>
+				</view>
+			</view>
+			<view class="box" v-if="state.gift_data.type == 1">
+				<view class="b-row">
+					<view class="b-tit">领取方式</view>
+					<view class="b-cont">
+						<view class="s-cont s-text">{{state.gain_method == 1 ? '线下领取' : '快递邮寄'}}</view>
+					</view>
+				</view>
+				<view class="b-row" v-if="state.gain_method == 1 && state.gift_data.offline_place">
+					<view class="b-tit">领取地点</view>
+					<view class="b-cont">
+						<view class="s-cont s-text">{{state.gift_data.offline_place}}</view>
+					</view>
+				</view>
+				<template v-if="state.address_data">
+					<view class="b-row">
+						<view class="b-tit">收货人</view>
+						<view class="b-cont">
+							<text class="s-name s-text">{{state.address_data.name}}</text>
+							<text class="s-phone s-text">{{state.address_data.phone}}</text>
+						</view>
+					</view>
+					<view class="b-row">
+						<view class="b-tit">收件地址</view>
+						<view class="b-cont">
+							<view class="s-text">
+								<text class="s-province">{{state.address_data.province}}</text>
+								<text class="s-city">{{state.address_data.city}}</text>
+								<text class="s-area">{{state.address_data.area}}</text>
+								<text class="s-address">{{state.address_data.address}}</text>
+							</view>
+						</view>
+					</view>
+				</template>
+				<template v-else>
+					<view class="b-row">
+						<view class="b-tit">收货人</view>
+						<view class="b-cont">
+							<text class="s-name s-text">{{state.name}}</text>
+							<text class="s-phone s-text">{{state.phone}}</text>
+						</view>
+					</view>
+				</template>
+				<view class="b-row">
+					<view class="b-tit">留言</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.note}}</text>
+					</view>
+				</view>
+				<template v-if="state.express_data">
+					<view class="b-row">
+						<view class="b-tit">快递公司</view>
+						<view class="b-cont">
+							<text class="s-text">{{state.express_data.company}}</text>
+						</view>
+					</view>
+					<view class="b-row">
+						<view class="b-tit">快递单号</view>
+						<view class="b-cont">
+							<text class="s-text">{{state.express_data.number}}</text>
+							<text class="s-copy" @click="onCopy(state.express_data.number)">点击复制</text>
+						</view>
+					</view>
+				</template>
+				<view class="b-row" v-if="state.status == 2">
+					<view class="b-tit">发货时间</view>
+					<view class="b-cont">
+						<text class="s-text">{{timeFormat(state.deployed_at)}}</text>
+					</view>
+				</view>
+			</view>
+			<view class="box" v-if="state.gift_data.type == 2">
+				<view class="b-row">
+					<view class="b-tit">券码信息</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.gift_data.code_data.data}}</text>
+						<text class="s-copy" @click="onCopy(state.gift_data.code_data.data)">点击复制</text>
+					</view>
+				</view>
+				<view class="b-row f-link" v-if="state.gift_data.external_link">
+					<view class="b-tit">兑换入口</view>
+					<view class="b-cont">
+						<view class="s-btn" @click="onExternalLink">点击进入</view>
+					</view>
+				</view>
+				<view class="b-row" v-if="state.status == 2">
+					<view class="b-tit">发货时间</view>
+					<view class="b-cont">
+						<text class="s-text">{{timeFormat(state.deployed_at)}}</text>
+					</view>
+				</view>
+			</view>
+			<view class="box">
+				<view class="b-row">
+					<view class="b-tit">订单编号</view>
+					<view class="b-cont">
+						<text class="s-text">{{state.number}}</text>
+						<text class="s-copy" @click="onCopy(state.id)">点击复制</text>
+					</view>
+				</view>
+				<view class="b-row">
+					<view class="b-tit">兑换时间</view>
+					<view class="b-cont">
+						<text class="s-text">{{timeFormat(state.created_at)}}</text>
+					</view>
+				</view>
+			</view>
+
+			<view class="action" v-if="submitIn">
+				<view class="b-home" @click="onHome">返回首页</view>
+				<view class="b-order" @click="onOrderList">我的订单</view>
+			</view>
+
+			<view class="navbar" v-if="!submitIn && state.gift_data.type == 1 
+			&& (state.status == 1 || state.express_data)">
+				<view class="b-action">
+					<view class="b-btn f-cancel" @click="onCancel" v-if="state.status == 1">取消订单</view>
+					<view class="b-btn f-feed" @click="onExpressFeed" v-if="state.express_data">物流动态</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getOrderDetail,
+		orderCancel
+	} from '../../service/api/gift.js';
+  import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js';
+	import mixinsAuth from '../../mixins/auth.js';
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				recordId: 0,
+				state: null,
+				hasLoad: false,
+				submitIn: false,
+				loadingType: 1,
+				loadingText: '',
+
+			}
+		},
+		onLoad(options) {
+			this.loadDetail(options.id)
+			this.recordId = options.id
+			if (options.submit) {
+				this.submitIn = true
+			}
+		},
+		computed: {
+			stateStyle() {
+				return {
+					'f-wait': this.state.status === 1,
+					'f-done': this.state.status === 2,
+					'f-cancel': this.state.status === 3,
+				}
+			},
+			stateIcon() {
+				return {
+					'icon-wait': this.state.status === 1,
+					'icon-done': this.state.status === 2,
+					'icon-cancel': this.state.status === 3,
+				}
+			}
+		},
+		methods: {
+			loadDetail(recordId) {
+				getOrderDetail(recordId).then(([err, res]) => {
+					console.log('getOrderDetail', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+			onExpressFeed() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/express_feed?id=' + this.recordId
+				})
+			},
+			onCancel() {
+				orderCancel(this.recordId).then(([err, res]) => {
+					console.log('orderCancel', err, res)
+					if (!err) {
+						showToast('取消成功')
+						this.loadDetail(this.recordId)
+					}
+				})
+			},
+			onExternalLink() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/home/webview?url=' + encodeURIComponent(this.state.gift_data.external_link)
+				})
+			},
+			onOrderList() {
+				uni.reLaunch({
+					url: '/pages/third/yunexamine/pages/gift/order'
+				})
+			},
+			onHome() {
+				uni.switchTab({
+					url: '/pages/third/yunexamine/pages/gift/index'
+				})
+			},
+			onJumpGift(giftId) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/gift/detail?gift_id=' + giftId
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding-bottom: 100upx;
+	}
+
+	.state-box {
+		height: 180upx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background: #fff;
+
+		&.f-wait {
+			color: #FFC300;
+		}
+
+		&.f-done {
+			color: #DA5650;
+		}
+
+		&.f-cancel {
+			color: #666;
+		}
+
+		.iconfont {
+			font-size: 64upx;
+		}
+
+		.s-tit {
+			margin-left: 20upx;
+			font-size: 50upx;
+			font-weight: bold;
+		}
+	}
+
+	.gift-view {
+		margin-top: 30upx;
+		background: #fff;
+		padding: 30upx;
+		display: flex;
+
+		.b-left {
+			width: 160upx;
+			height: 160upx;
+
+			image {
+				width: 160upx;
+				height: 160upx;
+			}
+		}
+
+		.b-main {
+			margin-left: 30upx;
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+
+			.b-title {
+				font-size: 28upx;
+				color: #333;
+			}
+
+
+			.b-spec {
+				margin-top: 20upx;
+				display: flex;
+				align-items: center;
+
+				.s-tit {
+					font-size: 24upx;
+					color: #999;
+				}
+
+				.s-value {
+					margin-left: 10upx;
+					font-size: 24upx;
+					color: #666;
+				}
+			}
+
+			.b-fund {
+				margin-top: 20upx;
+				display: flex;
+				align-items: baseline;
+
+				.s-worth {
+					font-size: 24upx;
+					color: #999;
+				}
+			}
+		}
+	}
+
+	.box {
+		margin-top: 30upx;
+		background: #fff;
+		padding: 15upx 0;
+
+		.b-row {
+			padding: 15upx 30upx;
+			display: flex;
+			// justify-content: space-between;
+			&.f-link {
+				align-items: center;
+			}
+
+			.b-tit {
+				font-size: 28upx;
+				width: 180upx;
+				color: #666;
+			}
+
+			.b-cont {
+				width: 510upx;
+				display: flex;
+				align-items: center;
+				font-size: 28upx;
+				color: #333;
+			}
+
+			.s-copy {
+				margin-left: 20upx;
+				font-size: 24upx;
+				color: #FFC300;
+			}
+
+			.s-province,
+			.s-city,
+			.s-area {
+				margin-right: 10upx;
+			}
+
+			.s-phone {
+				margin-left: 20upx;
+			}
+			
+			.s-text {
+				color: #333;
+				font-weight: bold;
+			}
+
+			.s-btn {
+				padding: 0 30upx;
+				background: #FFC300;
+				color: #fff;
+				font-size: 24upx;
+				height: 60upx;
+				line-height: 60upx;
+				border-radius: 30upx;
+			}
+		}
+	}
+
+	.action {
+		margin-top: 100upx;
+		display: flex;
+		padding: 0 20upx;
+
+		.b-home {
+			flex: 1;
+			height: 78upx;
+			line-height: 78upx;
+			text-align: center;
+			color: #DA5650;
+			font-size: 32upx;
+			border: 1upx solid #DA5650;
+			border-radius: 50upx;
+			letter-spacing: 5upx;
+		}
+
+		.b-order {
+			margin-left: 50upx;
+			flex: 1;
+			height: 80upx;
+			line-height: 80upx;
+			text-align: center;
+			color: #fff;
+			font-size: 32upx;
+			background: #DA5650;
+			border-radius: 50upx;
+			letter-spacing: 5upx;
+		}
+
+	}
+
+	.navbar {
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100upx;
+		background: #fff;
+		border-top: 1upx solid #f6f6f6;
+		display: flex;
+		align-items: center;
+		justify-content: flex-end;
+		border-top: 1upx solid #eee;
+
+		.b-action {
+			margin: 0 30upx;
+			display: flex;
+			align-items: center;
+
+			.b-btn {
+				margin-left: 20upx;
+				background: #DA5650;
+				height: 60upx;
+				border-radius: 30upx;
+				padding: 0 30upx;
+				line-height: 60upx;
+				font-size: 26upx;
+				color: #fff;
+				letter-spacing: 4upx;
+
+				&.f-feed {
+					color: #fff;
+					background: #FFC300;
+				}
+
+				&.f-cancel {
+					color: #666;
+					background: none;
+					border: 2upx solid #eee;
+				}
+			}
+		}
+	}
+</style>

+ 67 - 0
src/pages/third/yunexamine/pages/home/activity.vue

@@ -0,0 +1,67 @@
+<template>
+	<view class="wrap">
+		<view class="b-image" v-if="activityImage" @click="onPreview">
+			<image :src="activityImage || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/activity_loading.jpg'" mode="widthFix"></image>
+		</view>
+		<view class="b-rich" v-else>
+			<rich-text :nodes="activityRich"></rich-text>
+		</view>
+	</view>
+</template>
+
+<script>
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import {
+		formatRichText
+	} from './../common/util.js'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+		data() {
+			return {
+				appStore: useAppStore()
+			}
+		},
+		onLoad() {},
+		computed: {
+			activityRich() {
+				return formatRichText(this.appStore.getGlobalConfig.activity_rich)
+			},
+			activityImage() {
+				return this.appStore.getGlobalConfig.activity_img
+			}
+		},
+		methods: {
+			onPreview() {
+				uni.previewImage({
+					current: 0,
+					urls: [
+						this.activityImage
+					]
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.b-rich {
+		background: #fff;
+		font-size: 28upx;
+	}
+
+	.b-image {
+		image {
+			width: 100%;
+			height: 460upx;
+		}
+	}
+</style>

+ 267 - 0
src/pages/third/yunexamine/pages/home/dashboard.vue

@@ -0,0 +1,267 @@
+<template>
+	<view class="wrap">
+		<view class="filter-box">
+			<view class="b-item" v-for="(item, index) in groupList" :key="index"
+				:class="{'f-active': index == groupIndex}" @click="onGroupSelect(index)">
+				<view class="s-tit">{{item.name}}</view>
+			</view>
+		</view>
+		<view class="activity-list">
+			<view class="b-items" v-if="state.items.length > 0">
+				<view class="b-item" v-for="(item, index) in state.items" :key="index"
+					@click="onJumpActivitty(item.id)">
+					<view class="b-cover" v-if="item.cover_url">
+						<image :src="item.cover_url" mode="aspectFill"></image>
+					</view>
+					<view class="b-main">
+						<text class="b-title">{{item.name}}</text>
+						<view class="b-time">
+							<view class="s-start">开始时间 {{datetime(item.start_time)}}</view>
+							<view class="s-end">结束时间 {{datetime(item.end_time)}}</view>
+						</view>
+						<view class="b-foot">
+							<view class="b-state">
+								<view class="s-state f-wait" v-if="item.state == 0">未开始</view>
+								<view class="s-state f-ing" v-else-if="item.state == 1">进行中</view>
+								<view class="s-state f-end" v-else-if="item.state == 2">已结束</view>
+							</view>
+							<view class="b-action">
+								<view class="s-enter">进入活动</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getActivityList
+	} from '../../service/api/common.js'
+	import {
+		formatDate
+	} from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import loadMore from './../components/load-more/load-more.vue'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon],
+    components: {
+      loadMore
+    },
+		data() {
+			return {
+				groupIndex: 0,
+				groupList: [{
+						name: '全部',
+						value: 'all',
+					},
+					{
+						name: '进行中',
+						value: 'ing',
+					},
+					{
+						name: '已结束',
+						value: 'end',
+					},
+				],
+				state: {
+					items: []
+				},
+				loadingType: 1,
+				loadingText: '',
+				appStore: useAppStore()
+			}
+		},
+		onLoad(options) {
+			this.loadActivityList(true)
+			if (this.appStore.getActivityId) {
+				this.appStore.delActivityId()
+			}
+		},
+		onReachBottom() {
+			if (this.loadingType !== 1 && this.loadingType !== 2) {
+				this.loadActivityList();
+			}
+		},
+		computed: {
+			datetime() {
+				return (timestamp) => {
+					return formatDate(timestamp, 'yyyy-MM-dd hh:mm')
+				}
+			}
+		},
+		methods: {
+			loadActivityList(refresh) {
+				console.log('loadActivityList', refresh);
+				if (refresh) {
+					this.page = 1;
+					this.state.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getActivityList(this.groupList[this.groupIndex].value, this.page, 10).then(([
+					err, res
+				]) => {
+					console.log('getActivityList', err, res)
+					this.loadingType = -1;
+					if (!err) {
+						if (res.items.length > 0) {
+							this.state.items = [...this.state.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无数据'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			onGroupSelect(index) {
+				this.groupIndex = index
+				this.loadActivityList(true)
+			},
+			onJumpActivitty(id) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/home/welcome?from=jump&aid=' + id
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.filter-box {
+		padding: 30upx;
+		background: #fff;
+		display: flex;
+		align-items: center;
+
+		.b-item {
+			width: 240upx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 30upx;
+
+			&.f-active {
+				font-weight: bold;
+			}
+
+			.s-tit {
+				color: #333;
+			}
+		}
+	}
+
+	.activity-list {
+		padding: 40upx 40upx 0 40upx;
+
+		.b-items {
+			.b-item {
+				margin-bottom: 40upx;
+				border-bottom: 1upx solid #eee;
+				display: flex;
+				flex-direction: column;
+				background: #fff;
+				border-radius: 20upx;
+
+				&:last-child {
+					border-bottom: none;
+					margin-bottom: 0;
+				}
+
+				.b-cover {
+					width: 670upx;
+					height: 300upx;
+
+					image {
+						border-radius: 20upx 20upx 0 0;
+						width: 670upx;
+						height: 300upx;
+					}
+				}
+
+				.b-main {
+					padding: 20upx;
+				}
+
+				.b-title {
+					font-size: 32upx;
+					// font-weight: bold;
+				}
+
+				.b-time {
+					margin-top: 10upx;
+					display: flex;
+					flex-direction: column;
+					// align-items: center;
+					// justify-content: space-between;
+					font-size: 26upx;
+					color: #999;
+					line-height: 40upx;
+				}
+
+				.b-foot {
+					margin-top: 10upx;
+					display: flex;
+					align-items: center;
+					justify-content: space-between;
+
+					.b-state {
+						.s-state {
+							padding: 0 20upx;
+							background: #DA5650;
+							color: #fff;
+							font-size: 24upx;
+							height: 40upx;
+							line-height: 40upx;
+
+							&.f-wait {
+								background: #444c69;
+							}
+
+							&.f-ing {
+								background: #FFC300;
+							}
+
+							&.f-end {
+								background: #c4c4c4;
+							}
+						}
+
+					}
+
+					.b-action {
+						.s-enter {
+							padding: 0 30upx;
+							height: 60upx;
+							line-height: 60upx;
+							text-align: center;
+							color: #fff;
+							font-size: 28upx;
+							background: #DA5650;
+							border-radius: 30upx;
+						}
+					}
+				}
+			}
+		}
+	}
+</style>

+ 463 - 0
src/pages/third/yunexamine/pages/home/index.vue

@@ -0,0 +1,463 @@
+<template>
+	<view class="wrap">
+		<view class="logo">
+			<image :src="topImage || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/home_top_loading.jpg'" mode="widthFix"></image>
+		</view>
+		<view class="main" v-if="hasLoad">
+			<view class="user-info">
+				<view class="b-avatar">
+					<image :src="userInfo.avatar_url || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/user.png'" mode="aspectFill"></image>
+				</view>
+				<view class="b-info">
+					<view class="s-name">{{userInfo.name}}</view>
+					<view class="s-org">{{userInfo.group_name}}</view>
+				</view>
+				<view class="b-score" v-if="examineData.max_sid > 0" @click="onGoLast('max', examineData.max_sid)">
+					<view class="s-score">{{examineData.max_score}}分</view>
+					<view class="s-tit">{{costTime(examineData.cost_time)}}</view>
+				</view>
+			</view>
+			<view class="examine">
+				<view class="b-chance">
+					<view class="s-tit">答题机会</view>
+					<view class="s-flag">×</view>
+					<view class="s-num">{{examineData.challenge_chance}}</view>
+				</view>
+				<view class="b-start disable" v-if="examineData.activity_status == 0">答题未开始</view>
+				<template v-else-if="examineData.activity_status == 1">
+					<view class="b-start" :class="{ disable: disable.challenge }" @click="onContinue('challenge')"
+						v-if="examineData.challenge_session && examineData.challenge_session.status == 1">继续答题</view>
+					<view class="b-start" :class="{ disable: disable.challenge }" @click="onStartChallenge"
+						v-else-if="examineData.challenge_chance > 0">{{examineData.last_sid > 0 ? '重新答题' : '开始答题'}}
+					</view>
+					<view class="b-start disable" v-else>完成答题</view>
+				</template>
+				<view class="b-start disable" v-else-if="examineData.activity_status == 2">答题已结束</view>
+				<view class="b-start f-finish" v-if="examineData.last_sid > 0"
+					@click="onGoLast('last', examineData.last_sid)">上次成绩</view>
+				<view class="exercise" v-if="examineData.exercise_status">
+					<view class="b-try" :class="{ disable: disable.exercise }" @click="onContinue('exercise')"
+						v-if="examineData.exercise_session && examineData.exercise_session.status == 1">继续练习</view>
+					<view class="b-try" :class="{ disable: disable.exercise }" @click="onStartExercise"
+						v-else-if="examineData.exercise_chance > 0">练习模式 x {{examineData.exercise_chance}}次</view>
+					<view class="b-try disable" v-else>练习完成</view>
+					<view class="b-try f-wrong" @click="onWrongList">错题记录</view>
+				</view>
+			</view>
+			<view class="top-data">
+				<view class="b-tit">我的排名</view>
+				<view class="b-main">
+					<view class="b-item" v-for="(item, index) in topData" :key="index" @click="onGoTop(item.id)">
+						<view class="b-top">
+							<view class="s-tit1">{{item.group}}</view>
+							<view class="s-num">{{item.top}}</view>
+							<view class="s-tit2">名</view>
+						</view>
+						<view class="b-more">
+							<view class="iconfont icon-more"></view>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="rules">
+				<view class="b-tit">答题规则</view>
+				<text class="b-content">{{examineData.rule_content}}</text>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+		<!-- 上传头像 -->
+		<upload-avatar :show="display.avatarChange" :image="userInfo.avatar_url" @bindClose="onAvatarClose"
+			@bindChange="onAvatarChange"></upload-avatar>
+	</view>
+</template>
+
+<script>
+	import {
+		getHomeData
+	} from '../../service/api/page.js'
+	import {
+		startChallenge,
+		startExercise,
+		getNew
+	} from '../../service/api/examine.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import loadMore from '../../components/load-more/load-more.vue'
+	import uploadAvatar from '../../components/upload-avatar/upload-avatar.vue'
+	import { useAppStore } from '../../store/index.js'
+	import { useUserStore } from '../../store/module/user.js'
+	import { useExamineStore } from '../../store/module/examine.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      loadMore,
+      uploadAvatar
+    },
+		data() {
+			return {
+				userInfo: {},
+				topData: {},
+				examineData: {},
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: '',
+				disable: {
+					challenge: false,
+					exercise: false
+				},
+				display: {
+					avatarChange: false
+				},
+				appStore: useAppStore(),
+				userStore: useUserStore(),
+				examineStore: useExamineStore()
+			}
+		},
+		onShow() {
+			this.loadHomeData()
+		},
+		computed: {
+			topImage() {
+				return this.appStore.getGlobalConfig.home_top_img
+			}
+		},
+		methods: {
+			loadHomeData() {
+				getHomeData().then(([err, res]) => {
+					console.log('getHomeData', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.userInfo = res.userInfo
+						this.topData = res.topData
+						this.examineData = res.examineData
+						if (!this.userInfo.avatar_url) {
+							if (!this.userStore.getUploadAvatarTip) {
+								this.userStore.setUploadAvatarTip(true)
+								this.display.avatarChange = true
+							}
+						}
+					} else {
+						if (!this.hasLoad) {
+							this.loadingType = 3
+							this.loadingText = err.data.msg || '加载失败'
+						}
+					}
+				})
+			},
+			onStartChallenge() {
+				if (this.disable.challenge) {
+					return
+				}
+				this.disable.challenge = true
+				startChallenge().then(([err, res]) => {
+					console.log('startChallenge', err, res)
+					this.disable.challenge = false
+					if (!err) {
+						if (res.state === 'new' || res.state === 'exist') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/answer?from=challenge&session_id=' + res.session_id
+							})
+						} else if (res.state === 'end') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/summary?from=challenge&session_id=' + res.session_id
+							})
+						}
+					}
+				})
+			},
+			onStartExercise() {
+				if (this.disable.exercise) {
+					return
+				}
+				this.disable.exercise = true
+				startExercise().then(([err, res]) => {
+					console.log('startExercise', err, res)
+					this.disable.exercise = false
+					if (!err) {
+						if (res.state === 'new' || res.state === 'exist') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/answer?from=exercise&session_id=' + res.session_id
+							})
+						} else if (res.state === 'end') {
+							uni.navigateTo({
+								url: '/pages/third/yunexamine/pages/examine/summary?from=exercise&session_id=' + res.session_id
+							})
+						}
+					}
+				})
+			},
+			onContinue(type) {
+				let sessionId = 0
+				if (type === 'challenge') {
+					sessionId = this.examineData.challenge_session.session_id
+				} else if (type === 'exercise') {
+					sessionId = this.examineData.exercise_session.session_id
+				}
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/answer?from=' + type + '&session_id=' + sessionId
+				})
+			},
+			onGoTop(groupId) {
+				this.examineStore.setTopGroupId(groupId)
+				uni.switchTab({
+					url: '/pages/third/yunexamine/pages/examine/top'
+				})
+			},
+			onWrongList() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/wrong'
+				})
+			},
+			onGoLast(from, sessionId) {
+				if (!sessionId) {
+					return
+				}
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/examine/summary?from=' + from + '&session_id=' + sessionId
+				})
+			},
+			onAvatarClose() {
+				this.display.avatarChange = false
+			},
+			onAvatarChange(avatarUrl) {
+				this.userInfo.avatar_url = avatarUrl
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {}
+
+	.logo {
+		image {
+			width: 100%;
+			height: 460upx;
+		}
+	}
+
+	.main {
+		padding: 40upx;
+	}
+
+	.user-info {
+		padding: 30upx 20upx;
+		display: flex;
+		align-items: center;
+		background: #ffffff;
+		border-radius: 20upx;
+
+		justify-content: space-between;
+
+		.b-avatar {
+			width: 100upx;
+			height: 100upx;
+
+			image {
+				width: 100upx;
+				height: 100upx;
+				border-radius: 50%;
+			}
+		}
+
+		.b-info {
+			margin-left: 10upx;
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+
+			.s-name {
+				font-size: 28upx;
+			}
+
+			.s-org {
+				margin-top: 10upx;
+				padding: 0 20upx;
+				width: fit-content;
+				background: #DA5650;
+				border-radius: 10upx;
+				color: #fff;
+				line-height: 20px;
+				height: 20px;
+				font-size: 24upx;
+				text-align: center;
+			}
+		}
+
+		.b-score {
+			margin-left: 10upx;
+			display: flex;
+			flex-direction: column;
+			text-align: center;
+
+			.s-score {
+				font-size: 32upx;
+				font-weight: bold;
+			}
+
+			.s-tit {
+				margin-top: 10upx;
+				font-size: 24upx;
+				color: #999;
+			}
+		}
+	}
+
+	.examine {
+		margin-top: 40upx;
+
+		.b-chance {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+
+			.s-tit {
+				font-size: 26upx;
+				color: #808080;
+			}
+
+			.s-flag {
+				margin: 0 10upx;
+				font-size: 26upx;
+				color: #808080;
+			}
+
+			.s-num {
+				font-size: 36upx;
+				color: #DA5650;
+				font-weight: bold;
+			}
+		}
+
+		.b-start {
+			margin-top: 20upx;
+			height: 100upx;
+			line-height: 100upx;
+			text-align: center;
+			color: #fff;
+			font-size: 32upx;
+			background: #DA5650;
+			border-radius: 50upx;
+			letter-spacing: 10upx;
+
+			&.disable {
+				background: rgba($color: #DA5650, $alpha: 0.8);
+			}
+
+			&.f-finish {
+				height: 80upx;
+				line-height: 80upx;
+				font-size: 28upx;
+				font-weight: bold;
+				background: #FFFFFF;
+				color: #DA5650;
+				box-shadow: 1upx 1upx 1upx 4upx rgba($color: #000000, $alpha: 0.05),
+			}
+		}
+	}
+
+	.exercise {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.b-try {
+			margin-top: 20upx;
+			width: 320upx;
+			height: 80upx;
+			line-height: 80upx;
+			text-align: center;
+			color: #fff;
+			font-size: 28upx;
+			background: #5D9CEC;
+			border-radius: 40upx;
+
+			&.disable {
+				background: rgba($color: #5D9CEC, $alpha: 0.8);
+			}
+
+			&.f-wrong {
+				background: #FF8D1A;
+			}
+		}
+	}
+
+	.top-data {
+		margin-top: 40upx;
+
+		.b-tit {
+			font-size: 28upx;
+			font-weight: bold;
+		}
+
+		.b-main {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			flex-wrap: wrap;
+
+			.b-item {
+				margin-top: 20upx;
+				padding: 30upx;
+				width: 260upx;
+				background: #ffffff;
+				border-radius: 20upx;
+				display: flex;
+				align-items: center;
+
+				.b-top {
+					flex: 1;
+					display: flex;
+					align-items: center;
+
+					.s-tit1,
+					.s-tit2 {
+						font-size: 26upx;
+						color: #808080;
+					}
+
+					.s-num {
+						margin: 0 20upx;
+						font-size: 32upx;
+						color: #DA5650;
+					}
+				}
+
+				.b-more {
+					.iconfont {
+						font-size: 30upx;
+						color: #ccc;
+					}
+				}
+			}
+		}
+	}
+
+
+	.rules {
+		margin-top: 50upx;
+		padding: 30upx;
+		background: #ffffff;
+		border-radius: 20upx;
+
+		.b-tit {
+			color: #DA5650;
+			font-size: 30upx;
+		}
+
+		.b-content {
+			margin-top: 20upx;
+			display: block;
+			color: #383838;
+			font-size: 24upx;
+			line-height: 40upx;
+		}
+	}
+</style>

+ 33 - 0
src/pages/third/yunexamine/pages/home/webview.vue

@@ -0,0 +1,33 @@
+<template>
+	<view class="wrap">
+		<web-view :webview-styles="webviewStyles" :src="url"></web-view>
+	</view>
+</template>
+
+<script>
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+		data() {
+			return {
+				url: '',
+				webviewStyles: {
+					progress: {
+						color: '#FF3333'
+					}
+				}
+			}
+		},
+		onLoad(options) {
+			console.log('web onLoad', options)
+			this.options = options
+			this.url = decodeURIComponent(options.url)
+		}
+	};
+</script>
+
+<style lang="scss">
+	.wrap {}
+</style>

+ 139 - 0
src/pages/third/yunexamine/pages/home/welcome.vue

@@ -0,0 +1,139 @@
+<template>
+	<view class="wrap">
+		<view class="screen" @click="onJumpHome" v-if="screenImage" :style="{ height: screenHeight + 'px' }">
+			<image :src="screenImage" mode="aspectFill" :style="{ height: screenHeight + 'px' }"></image>
+			<view class="b-countdown">
+				<view class="s-text">{{screenCountdown}}</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getConfig
+	} from '../../service/api/common.js'
+	import {
+		silentReload,
+		smartLogin
+	} from '../../service/request/main.js'
+	import Util from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	import { useAppStore } from '../../store/index.js'
+
+	export default {
+		mixins: [mixinsCommon],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				from: '',
+				screenHeight: 0,
+				screenImage: '',
+				screenCountdown: 5,
+				loadingType: 1,
+				loadingText: '',
+				interval: 0,
+				appStore: useAppStore()
+			}
+		},
+		onLoad(options) {
+			console.log('web onLoad', options)
+			let params = options
+			this.from = params.from
+			if (params.scene) {
+				params = Util.urlDecode(decodeURIComponent(params.scene))
+				this.from = 'qrcode'
+			}
+			this.appStore.setActivityId(params.aid)
+			getConfig().then(([err, res]) => {
+				console.log('getConfig', err, res);
+				if (!err) {
+					this.loadingType = -1
+					this.appStore.setGlobalConfig(res)
+					this.screenImage = res.welcome_img
+					this.screenCountdown = res.welcome_countdown || 5
+					if (this.screenImage) {
+						// 验证码倒计时
+						this.interval = setInterval(() => {
+							this.screenCountdown--
+						}, 1000)
+						setTimeout(() => {
+							if (this.interval > 0) {
+								this.onJumpHome()
+							}
+						}, this.screenCountdown * 1000)
+					} else {
+						this.onJumpHome()
+					}
+				} else {
+					this.loadingType = 3
+					this.loadingText = err.data.msg || '加载失败'
+				}
+			})
+
+			// 获取窗口高度
+			const systemInfo = uni.getSystemInfoSync()
+			this.screenHeight = systemInfo.screenHeight
+		},
+		methods: {
+			onJumpHome() {
+				console.log('onJumpHome')
+				if (this.interval > 0) {
+					clearInterval(this.interval)
+					this.interval = 0
+				}
+				if (this.appStore.getAccessToken) {
+					console.log('hasLogin')
+					uni.switchTab({
+						url: '/pages/third/yunexamine/pages/home/index' // 跳到首页
+					})
+				} else {
+					if (this.from === 'qrcode') {
+						console.info('needLogin qrcode', 'silentReload')
+						silentReload(false, '') // 扫描尝试自动登录
+					} else {
+						console.log('needLogin jump')
+						smartLogin('')
+					}
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.wrap {}
+
+	.screen {
+		width: 750upx;
+
+		image {
+			width: 750upx;
+
+		}
+
+		.b-countdown {
+			position: absolute;
+			right: 80upx;
+			bottom: 80upx;
+			width: 80upx;
+			height: 80upx;
+			border-radius: 40upx;
+			background: rgba(0, 0, 0, 0.5);
+			display: flex;
+			align-items: center;
+			justify-content: center;
+
+			.s-text {
+				font-size: 32upx;
+				font-weight: bold;
+				color: #fff;
+			}
+		}
+	}
+</style>

+ 102 - 0
src/pages/third/yunexamine/pages/user/auto_login.vue

@@ -0,0 +1,102 @@
+<template>
+	<view class="wrap">
+		<view class="tips">
+			<image class="b-image" src="https://xy.wenlvti.net/app_static/yunexamine/imgs/loading_image.png" alt="" mode="aspectFit"></image>
+			<view class="b-tit">尝试自动登录,请稍后....</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		wechatAutoLogin
+	} from '../../service/api/user.js'
+	import {
+		smartLogin
+	} from '../../service/request/main.js'
+	import {
+		encodeRedirectUrl,
+		decodeRedirectUrl
+	} from '../../common/util.js'
+	import { useAppStore } from '../../store/index.js'
+
+	export default {
+		data() {
+			return {
+				appStore: useAppStore()
+			}
+		},
+		onLoad(options) {
+			console.log('onLoad autoLogin', options)
+			// 微信小程序自动登录
+			const returnPage = options.from ? decodeRedirectUrl(options.from) : 'pages/home/index';
+			uni.login().then(([err, res]) => {
+				console.log('uni.login', err, res);
+				if (!err) {
+					wechatAutoLogin(res.code).then(([err, res]) => {
+						console.log('wechatAutoLogin', err, res);
+						this.appStore.unlock('silentReload')
+						if (!err) {
+							console.warn('needLogin silentReload', 'success')
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							// 返回之前页面
+							uni.reLaunch({
+								url: '/' + returnPage
+							})
+						} else {
+							console.error('needLogin silentReload', 'fail');
+							// uni.reLaunch({
+							// 	url: '/pages/third/yunexamine/pages/user/login?from=' + encodeRedirectUrl(returnPage)
+							// })
+							smartLogin(returnPage)
+						}
+					})
+				} else {
+					console.error('needLogin silentReload', 'code fail')
+					this.appStore.unlock('silentReload')
+					// uni.reLaunch({
+					// 	url: '/pages/third/yunexamine/pages/user/login?from=' + encodeRedirectUrl(returnPage)
+					// })
+					smartLogin(returnPage)
+				}
+			})
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #ffffff;
+		height: 100%;
+	}
+
+	.wrap {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		height: 100%;
+	}
+
+	.tips {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.b-image {
+			width: 300upx;
+			height: 240upx;
+		}
+
+		.b-tit {
+			margin-top: 40upx;
+			color: #666;
+			font-size: 28upx;
+		}
+	}
+</style>

+ 145 - 0
src/pages/third/yunexamine/pages/user/change_code.vue

@@ -0,0 +1,145 @@
+<template>
+	<view class="wrap">
+		<view class="user-info">
+			<view class="b-item">
+				<view class="s-tit">旧登录码</view>
+				<input class="s-input" :value="form.oldCode" maxlength="12" @input="onInput" data-field="oldCode"
+					placeholder="必须输入旧的" />
+			</view>
+			<view class="b-item">
+				<view class="s-tit">新登录码</view>
+				<input class="s-input" :value="form.newCode" maxlength="12" @input="onInput" data-field="newCode"
+					placeholder="字符长度3-12" />
+			</view>
+			<view class="b-item">
+				<view class="s-tit">重复输入</view>
+				<input class="s-input" :value="form.repeatCode" maxlength="12" @input="onInput" @blur="onBlur"
+					data-field="repeatCode" placeholder="重复新的登录码" />
+			</view>
+		</view>
+		<view class="btn-save" :class="{ disable: disable.submit }" @click="onSave">保存修改</view>
+	</view>
+</template>
+
+<script>
+	import {
+		changeCode
+	} from '../../service/api/user.js'
+	import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+		data() {
+			return {
+				form: {
+					oldCode: '',
+					newCode: '',
+					repeatCode: '',
+				},
+				disable: {
+					submit: false
+				}
+			}
+		},
+		onLoad() {},
+		methods: {
+			onBlur(e) {
+				console.log('onBlur', e)
+				const field = e.target.dataset.field
+				if (field === 'repeatCode') {
+					if (this.newCode) {
+						if (this.repeatCode != this.newCode) {
+							return showToast('重复输入新的登录码不对')
+						}
+					}
+				}
+
+			},
+			onInput(e) {
+				console.log('onInput', e)
+				const field = e.target.dataset.field
+				this.form[field] = e.detail.value
+			},
+			onSave() {
+				if (this.disable.submit) {
+					return;
+				}
+				if (!this.form.oldCode) {
+					return showToast('旧登录码不能为空')
+				}
+				if (!this.form.newCode) {
+					return showToast('新登录码不能为空')
+				}
+				if (this.form.newCode.length < 3) {
+					return showToast('新登录码长度不能小于3位')
+				}
+				if (this.form.repeatCode != this.form.newCode) {
+					return showToast('重复输入新登录码不对')
+				}
+				this.disable.submit = true
+				changeCode(this.form.oldCode, this.form.newCode).then(([err, res]) => {
+					console.log('changeCode', err, res)
+					this.disable.submit = false
+					if (!err) {
+						showToast('修改成功').then(([err, res]) => {
+							uni.reLaunch({
+								url: '/pages/third/yunexamine/pages/user/mine'
+							})
+						})
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 0 40upx;
+	}
+
+	.user-info {
+		margin-top: 40upx;
+		background: #fff;
+		border-radius: 20upx;
+
+		.b-item {
+			padding: 0 40upx;
+			border-bottom: 1upx solid $pq-bg-color;
+			height: 100upx;
+			display: flex;
+			align-items: center;
+			position: relative;
+
+			.s-tit {
+				color: #000000;
+				font-size: 26upx;
+			}
+
+			.s-input {
+				flex: 1;
+				color: #808080;
+				text-align: right;
+				font-size: 26upx;
+			}
+		}
+	}
+
+	.btn-save {
+		margin-top: 50upx;
+		height: 100upx;
+		line-height: 100upx;
+		text-align: center;
+		color: #fff;
+		font-size: 32upx;
+		background: #DA5650;
+		border-radius: 50upx;
+		letter-spacing: 10upx;
+	}
+</style>

+ 256 - 0
src/pages/third/yunexamine/pages/user/change_phone.vue

@@ -0,0 +1,256 @@
+<template>
+	<view class="wrap">
+		<view class="main">
+			<view class="user-info">
+				<view class="b-item">
+					<view class="s-tit">旧手机号</view>
+					<text class="s-input">{{form.oldPhone}}</text>
+				</view>
+				<template v-if="loginMethods.indexOf('phone_captcha') !== -1">
+					<view class="b-item">
+						<view class="s-tit">新手机号</view>
+						<input class="s-input" :value="form.newPhone" minlength="11" maxlength="11" @input="onInput"
+							data-field="newPhone" placeholder="请输入新手机号" />
+					</view>
+					<view class="b-item">
+						<view class="s-tit">验证码</view>
+						<input class="s-input" :value="form.newCaptcha" maxlength="12" @input="onInput"
+							data-field="newCaptcha" placeholder="请输入验证码" />
+						<view class="s-captcha" :class="{ disable: captcha.second > 0 }" @click="onPhoneCaptcha">
+							<text v-if="!captcha.retry && !captcha.second">获取验证码</text>
+							<text v-else-if="captcha.second > 0">重新获取 {{ captcha.second }}秒</text>
+							<text v-else-if="captcha.retry">重新获取</text>
+						</view>
+					</view>
+				</template>
+			</view>
+			<template v-if="loginMethods.indexOf('phone_captcha') !== -1">
+				<view class="btn-save" :class="{ disable: disable.submit }" @click="onSave">保存修改</view>
+			</template>
+			<!-- #ifdef  MP-WEIXIN -->
+			<button class="btn-save f-wechat open-data-btn" open-type="getPhoneNumber"
+				@getphonenumber="onGetPhoneNumber" v-if="loginMethods.indexOf('wechat_phone') !== -1">授权手机号</button>
+			<!-- #endif -->
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getUserInfo,
+		sendPhoneCaptcha,
+		changePhone,
+		changeWechatPhone
+	} from '../../service/api/user.js'
+	import { showToast } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+		data() {
+			return {
+				form: {
+					oldPhone: '',
+					newPhone: '',
+					newCaptcha: ''
+				},
+				disable: {
+					submit: false
+				},
+				captcha: {
+					retry: false,
+					second: 0,
+				},
+				appStore: useAppStore()
+			}
+		},
+		onLoad() {
+			getUserInfo().then(([err, res]) => {
+				console.log('getUserInfo', err, res)
+				if (!err) {
+					this.form.oldPhone = res.phone
+				}
+			})
+		},
+		computed: {
+			loginMethods() {
+				return this.appStore.getGlobalConfig.login_methods || []
+			},
+		},
+		methods: {
+			onInput(e) {
+				console.log('onInput', e)
+				const field = e.target.dataset.field
+				this.form[field] = e.detail.value
+			},
+			onPhoneCaptcha() {
+				if (this.disable.captcha) {
+					return
+				}
+				if (!this.form.newPhone) {
+					return showToast('新手机号不能为空')
+				}
+				if (this.form.newPhone.length != 11) {
+					return showToast('新手机号长度必须为11位')
+				}
+				if (this.form.newPhone == this.form.oldPhone) {
+					return showToast('未修改手机号')
+				}
+				this.disable.captcha = true
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					sendPhoneCaptcha(this.form.newPhone, res ? res.code : '', false).then(([err, res]) => {
+						console.log('sendPhoneCaptcha', err, res)
+						if (!err) {
+							uni.showToast({
+								title: '发送成功'
+							})
+
+							// 验证码倒计时
+							this.captcha.second = 60
+							var interval = setInterval(() => {
+								this.captcha.second--
+							}, 1000)
+							setTimeout(() => {
+								clearInterval(interval)
+								this.captcha.retry = true
+								this.captcha.second = 0
+								this.disable.captcha = false
+							}, this.captcha.second * 1000)
+						} else {
+							this.disable.captcha = false
+						}
+					})
+				})
+			},
+			onSave() {
+				if (this.disable.submit) {
+					return;
+				}
+				if (!this.form.newPhone) {
+					return showToast('新手机号不能为空')
+				}
+				if (this.form.newPhone.length != 11) {
+					return showToast('新手机号长度必须为11位')
+				}
+				if (this.form.newPhone == this.form.oldPhone) {
+					return showToast('未修改手机号')
+				}
+				if (!this.form.newCaptcha) {
+					return showToast('验证码不能为空')
+				}
+				this.disable.submit = true
+				changePhone(this.form.newPhone, this.form.newCaptcha).then(([err, res]) => {
+					console.log('changePhone', err, res)
+					this.disable.submit = false
+					if (!err) {
+						showToast('修改成功').then(([err, res]) => {
+							uni.reLaunch({
+								url: '/pages/third/yunexamine/pages/user/mine'
+							})
+						})
+					}
+				})
+			},
+			onGetPhoneNumber(e) {
+				console.log('onGetPhoneNumber', e)
+				if (e.detail.errMsg.indexOf(':ok') === -1) {
+					// fail user deny
+					// showToast(e.detail.errMsg.replace('getPhoneNumber:', ''))
+					return
+				}
+				uni.showLoading({
+					title: '修改手机号中'
+				})
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					changeWechatPhone(e.detail.encryptedData, e.detail.iv, e.detail.code || '',
+						res ? res.code : '').then(([
+						err, res
+					]) => {
+						console.log('changeWechatPhone', err, res)
+						uni.hideLoading()
+						if (!err) {
+							showToast('修改成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/pages/third/yunexamine/pages/user/mine'
+								})
+							})
+						}
+					})
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 0 40upx;
+	}
+
+	.user-info {
+		margin-top: 40upx;
+		background: #fff;
+		border-radius: 20upx;
+
+		.b-item {
+			padding: 0 40upx;
+			border-bottom: 1upx solid $pq-bg-color;
+			height: 100upx;
+			display: flex;
+			align-items: center;
+			position: relative;
+
+			.s-tit {
+				color: #000000;
+				font-size: 26upx;
+			}
+
+			.s-input {
+				flex: 1;
+				color: #808080;
+				text-align: right;
+				font-size: 26upx;
+			}
+
+			.s-captcha {
+				margin-left: 20upx;
+				padding: 0 30upx;
+				background: #fff;
+				border: 2upx #ccc solid;
+				height: 64upx;
+				line-height: 64upx;
+				border-radius: 32upx;
+				color: #808080;
+				font-size: 24upx;
+
+				&.disable {
+					color: #999;
+				}
+			}
+		}
+	}
+
+	.btn-save {
+		margin-top: 50upx;
+		height: 100upx;
+		line-height: 100upx;
+		text-align: center;
+		color: #fff;
+		font-size: 32upx;
+		background: #DA5650;
+		border-radius: 50upx;
+		letter-spacing: 10upx;
+
+		&.f-wechat {
+			background: #35C773;
+		}
+	}
+</style>

+ 466 - 0
src/pages/third/yunexamine/pages/user/feedback.vue

@@ -0,0 +1,466 @@
+<template>
+	<view class="wrap">
+		<view class="box">
+			<view class="b-row">
+				<view class="b-tit">类型</view>
+				<view class="b-cont f-option">
+					<picker @change="onFilter" data-field="type" :value="typeIndex" :range="typeList" range-key="name">
+						<view class="b-main">
+							<view class="s-cont">{{this.typeList[this.typeIndex].name}}</view>
+							<view class="iconfont icon-more"></view>
+						</view>
+					</picker>
+				</view>
+			</view>
+			<view class="b-row">
+				<view class="b-tit">内容</view>
+				<view class="b-cont f-textarea">
+					<textarea placeholder="请填写详细内容" auto-height @input="onInputText" data-field="content"></textarea>
+				</view>
+			</view>
+			<view class="b-row">
+				<view class="b-tit">图片</view>
+				<view class="b-cont f-photos">
+					<view v-if="form.photos.length > 0" class="s-photos">
+						<view class="s-item" v-for="(item, index) in form.photos" :key="index">
+							<image :src="item.url" mode="widthFix" @click="onPreview(index)"></image>
+							<view class="s-del iconfont icon-wrong-b" @click="onDelete(index)"></view>
+						</view>
+					</view>
+					<view class="s-upload iconfont icon-upload" @click="onUploadPhotos"></view>
+				</view>
+			</view>
+		</view>
+		<view class="btn-save" :class="{ disable: disable.submit }" @click="onSave">提交反馈</view>
+		<view class="feedback" v-if="state.items.length > 0">
+			<view class="b-title" v-if="state.items.length > 0">
+				<view class="iconfont icon-line"></view>
+				<view class="s-tit">我的反馈</view>
+				<view class="iconfont icon-line"></view>
+			</view>
+			<view class="b-items">
+				<view class="b-item" v-for="(item, index) in state.items" :key="index" @click="onJumpDetail(item.id)">
+					<view class="b-main">
+						<text class="s-cont too-long">{{item.content}}</text>
+						<view class="s-more iconfont icon-more"></view>
+					</view>
+					<view class="b-foot">
+						<view class="s-info">
+							<view class="s-type">{{item.type_text}}</view>
+							<view class="s-state" :class="'f-state-' + item.status">{{item.status_text}}</view>
+						</view>
+						<view class="s-time">{{timeFormat(item.created_at)}}</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		uploadImage
+	} from '../../service/api/common.js'
+	import {
+		submit,
+		getRecordList
+	} from '../../service/api/feedback.js'
+	import {
+		compressImage,
+		showToast
+	} from './../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				form: {
+					photos: [],
+				},
+				disable: {
+					submit: false
+				},
+				state: {
+					items: []
+				},
+				typeList: [{
+					value: 1,
+					name: '数据问题'
+				}, {
+					value: 2,
+					name: '功能使用'
+				}],
+				typeIndex: 0,
+				loadingType: 1,
+				loadingText: '',
+			}
+		},
+		onLoad(options) {
+			this.onReset()
+			this.loadRecordList(true)
+		},
+		onReachBottom() {
+			if (this.loadingType !== 1 && this.loadingType !== 2) {
+				this.loadRecordList()
+			}
+		},
+		methods: {
+			// 输入内容
+			onInputText(e) {
+				console.log('onInputText', e)
+				this.form[e.target.dataset.field] = e.detail.value;
+			},
+			onFilter(e) {
+				console.log('onFilter', e.target.dataset.field, e.detail.value)
+				const field = e.target.dataset.field
+				if (field === 'type') {
+					this.typeIndex = e.detail.value
+					this.form.type = this.typeList[this.typeIndex].value
+				}
+			},
+			onReset() {
+				this.form = {
+					type: 1,
+					content: '',
+					photos: [],
+				}
+			},
+			onSave() {
+				if (this.disable.submit) {
+					return;
+				}
+				if (!this.form.content) {
+					return showToast('内容不能为空')
+				}
+				this.disable.submit = true
+				submit(this.form.type, this.form.content, this.form.photos).then(([err, res]) => {
+					console.log('submit', err, res)
+					this.disable.submit = false
+					if (!err) {
+						showToast('提交成功').then(([err, res]) => {
+							this.onReset()
+							this.loadRecordList(true)
+						})
+					}
+				})
+			},
+			onUploadPhotos(e) {
+				console.log('onUploadPhotos', e)
+				uni.chooseImage({
+					count: 9, // 1张
+					sizeType: ['compressed'], // 压缩图
+				}).then(([err, res]) => {
+					console.log('chooseImage', err, res)
+					if (!err) {
+						let index = this.form.photos.length
+						for (let i = 0; i < res.tempFilePaths.length; i++) {
+							compressImage(res.tempFiles[i], 1800, 2700).then(([err, res]) => {
+								console.log('compressImage', err, res)
+								if (!err) {
+									uploadImage(res).then(([err, res]) => {
+										console.log('uploadImage', err, res)
+										if (!err) {
+											this.form.photos[index++] = {
+												url: res.fullurl
+											}
+										}
+									})
+								}
+							})
+						}
+					}
+				})
+			},
+			onPreview(index) {
+				let photos = []
+				for (let i = 0; i < this.form.photos.length; i++) {
+					photos.push(this.form.photos[i].url)
+				}
+				uni.previewImage({
+					current: index,
+					urls: photos
+				})
+			},
+			onDelete(index) {
+				this.form.photos.splice(index, 1)
+			},
+			loadRecordList(refresh) {
+				console.log('loadRecordList', refresh);
+				if (refresh) {
+					this.page = 1;
+					this.state.items = []
+				} else {
+					this.page++
+				}
+				this.loadingType = 1
+				this.loadingText = ''
+				getRecordList(this.page, 10).then(([
+					err, res
+				]) => {
+					console.log('getRecordList', err, res)
+					this.loadingType = -1;
+					if (!err) {
+						if (res.items.length > 0) {
+							this.state.items = [...this.state.items, ...res.items]
+						} else {
+							this.loadingType = 2
+							if (this.page == 1) {
+								this.loadingText = '暂无数据'
+							}
+							this.page-- // 重置分页
+						}
+					} else {
+						this.loadingType = 3
+					}
+				})
+			},
+			onJumpDetail(id) {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/user/feedback_detail?id=' + id
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 0 40upx;
+	}
+
+	.box {
+		margin-top: 30upx;
+		background: #fff;
+		border-radius: 20upx;
+
+		.b-row {
+			padding: 30upx;
+			display: flex;
+			justify-content: space-between;
+
+			.b-tit {
+				font-size: 28upx;
+				width: 180upx;
+				color: #666;
+			}
+
+			.b-cont {
+				// flex: 1;
+				display: flex;
+				align-items: center;
+
+				input {
+					text-align: right;
+				}
+
+				textarea {
+					text-align: right;
+				}
+
+				&.f-option {
+
+					.b-main {
+						display: flex;
+						align-items: center;
+
+						.s-cont {
+							font-size: 28upx;
+						}
+
+						.iconfont {
+							margin-left: 10upx;
+						}
+					}
+				}
+
+				&.f-input {
+					input {
+						width: 480upx;
+					}
+				}
+
+				&.f-textarea {
+					textarea {
+						width: 530upx;
+						font-size: 28upx;
+					}
+				}
+
+				&.f-text {
+					font-size: 28upx;
+					color: #666;
+				}
+
+
+				&.f-photos {
+					display: flex;
+					align-items: inherit;
+
+					.s-photos {
+						width: 400upx;
+						display: flex;
+						flex-wrap: wrap;
+						justify-content: flex-end;
+
+						.s-item {
+							margin-bottom: 20upx;
+							margin-left: 20upx;
+							position: relative;
+							width: 180upx;
+
+							image {
+								width: 180upx;
+							}
+
+							.s-del {
+								position: absolute;
+								top: -15upx;
+								right: -15upx;
+								background: rgba(0, 0, 0, 0.5);
+								width: 30upx;
+								height: 30upx;
+								border-radius: 15upx;
+								color: #fff;
+								font-size: 26upx;
+								font-weight: bold;
+								display: flex;
+								align-items: center;
+								justify-content: center;
+							}
+						}
+					}
+
+					.s-upload {
+						margin-left: 20upx;
+						width: 100upx;
+						height: 100upx;
+						border: 2upx solid #ddd;
+						font-size: 50upx;
+						color: #ccc;
+						display: flex;
+						align-items: center;
+						justify-content: center;
+					}
+				}
+			}
+		}
+
+
+		input {
+			font-size: 28upx;
+		}
+
+		.input-placeholder {
+			font-size: 28upx;
+		}
+	}
+
+	.btn-save {
+		margin-top: 50upx;
+		height: 100upx;
+		line-height: 100upx;
+		text-align: center;
+		color: #fff;
+		font-size: 32upx;
+		background: #DA5650;
+		border-radius: 50upx;
+		letter-spacing: 10upx;
+	}
+
+	.feedback {
+		margin-top: 100upx;
+
+		.b-title {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+
+			.iconfont {
+				color: #ccc;
+			}
+
+			.s-tit {
+				margin: 0 20upx;
+				font-size: 30upx;
+				color: #666;
+			}
+		}
+
+		.b-items {
+			.b-item {
+				padding: 40upx 0;
+				border-bottom: 2upx solid #eee;
+
+				&:last-child {
+					border-bottom: none;
+				}
+			}
+		}
+
+		.b-main {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			.s-cont {
+				font-size: 28upx;
+				color: #333;
+			}
+
+			.s-more {
+				font-size: 40upx;
+				color: #ddd;
+			}
+		}
+
+		.b-foot {
+			margin-top: 30upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			.s-info {
+				display: flex;
+				align-items: center;
+
+				.s-type {
+					margin-right: 20upx;
+					font-size: 24upx;
+					padding: 5upx 15upx;
+					color: #fff;
+					background: #666;
+					border-radius: 10upx;
+				}
+
+				.s-state {
+					font-size: 26upx;
+					color: #666;
+
+					&.f-state-1 {}
+
+					&.f-state-2 {
+						color: #DA5650;
+						font-weight: bold;
+					}
+				}
+			}
+
+			.s-time {
+				font-size: 24upx;
+				color: #666;
+			}
+		}
+	}
+</style>

+ 210 - 0
src/pages/third/yunexamine/pages/user/feedback_detail.vue

@@ -0,0 +1,210 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="hasLoad">
+			<view class="b-main">
+				<text class="s-cont">{{state.content}}</text>
+			</view>
+			<view class="b-photos" v-if="state.photos.length > 0">
+				<view class="s-item" v-for="(item, index) in state.photos" :key="index">
+					<image :src="item.url" mode="widthFix" @click="onPreview(index)"></image>
+				</view>
+			</view>
+			<view class="b-foot">
+				<view class="s-info">
+					<view class="s-type">{{state.type_text}}</view>
+					<view class="s-state" :class="'f-state-' + state.status">{{state.status_text}}</view>
+				</view>
+				<view class="s-time">{{timeFormat(state.created_at)}}</view>
+			</view>
+			<view class="b-reply" v-if="state.status === 2">
+				<text class="s-cont">{{state.reply_content}}</text>
+				<view class="s-photos" v-if="state.reply_photos.length > 0">
+					<view class="s-item" v-for="(item, index) in state.reply_photos" :key="index">
+						<image :src="item.url" mode="widthFix" @click="onPreview(index, 'reply')"></image>
+					</view>
+				</view>
+				<view class="s-time">回复时间:{{timeFormat(state.reply_time)}}</view>
+			</view>
+		</view>
+		<!-- 加载中 -->
+		<load-more :loadingType="loadingType" :loadingText="loadingText" :top="300"></load-more>
+	</view>
+</template>
+
+<script>
+	import {
+		getRecordDetail
+	} from '../../service/api/feedback.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import mixinsAuth from '../../mixins/auth.js'
+  import LoadMore from '../../components/load-more/load-more.vue'
+	export default {
+		mixins: [mixinsCommon, mixinsAuth],
+    components: {
+      LoadMore
+    },
+		data() {
+			return {
+				state: null,
+				hasLoad: false,
+				loadingType: 1,
+				loadingText: ''
+			}
+		},
+		onLoad(options) {
+			this.recordId = options.id
+			this.loadDetail()
+		},
+		methods: {
+			loadDetail() {
+				getRecordDetail(this.recordId).then(([err, res]) => {
+					console.log('getRecordDetail', err, res)
+					if (!err) {
+						this.hasLoad = true
+						this.loadingType = -1
+						this.state = res
+					} else {
+						this.loadingType = 3
+						this.loadingText = err.data.msg || '加载失败'
+					}
+				})
+			},
+			onPreview(index, type) {
+				let photos = []
+				if (type === 'reply') {
+					for (let i = 0; i < this.state.reply_photos.length; i++) {
+						photos.push(this.state.reply_photos[i].url)
+					}
+				} else {
+					for (let i = 0; i < this.state.photos.length; i++) {
+						photos.push(this.state.photos[i].url)
+					}
+				}
+				uni.previewImage({
+					current: index,
+					urls: photos
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 40upx;
+	}
+
+	.b-main {
+		.s-cont {
+			font-size: 28upx;
+			color: #333;
+		}
+
+	}
+
+	.b-photos {
+		margin-top: 30upx;
+		display: flex;
+		flex-wrap: wrap;
+		justify-content: flex-start;
+
+		.s-item {
+			margin-bottom: 20upx;
+			margin-right: 20upx;
+			position: relative;
+			width: 210upx;
+
+			&:nth-child(3n) {
+				margin-right: 0;
+			}
+
+			image {
+				width: 210upx;
+			}
+		}
+	}
+
+	.b-foot {
+		margin-top: 30upx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.s-info {
+			display: flex;
+			align-items: center;
+
+			.s-type {
+				margin-right: 20upx;
+				font-size: 24upx;
+				padding: 5upx 15upx;
+				color: #fff;
+				background: #666;
+				border-radius: 10upx;
+			}
+
+			.s-state {
+				font-size: 26upx;
+				color: #666;
+
+				&.f-state-1 {}
+
+				&.f-state-2 {
+					color: #DA5650;
+					font-weight: bold;
+				}
+			}
+		}
+
+		.s-time {
+			font-size: 24upx;
+			color: #666;
+		}
+	}
+
+	.b-reply {
+		margin-top: 20upx;
+		background: #fff;
+		padding: 20upx;
+		display: flex;
+		flex-direction: column;
+
+		.s-cont {
+			font-size: 28upx;
+			color: #333;
+		}
+
+		.s-photos {
+			margin-top: 20upx;
+			display: flex;
+			flex-wrap: wrap;
+			justify-content: flex-start;
+
+			.s-item {
+				margin-bottom: 20upx;
+				margin-right: 20upx;
+				position: relative;
+				width: 210upx;
+
+				&:nth-child(3n) {
+					margin-right: 0;
+				}
+
+				image {
+					width: 210upx;
+				}
+			}
+		}
+
+		.s-time {
+			margin-top: 20upx;
+			font-size: 24upx;
+			color: #666;
+		}
+	}
+</style>

+ 561 - 0
src/pages/third/yunexamine/pages/user/login.vue

@@ -0,0 +1,561 @@
+<template>
+	<view class="wrap">
+		<view class="logo">
+			<image :src="topImage || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/login_top_loading.jpg'" mode="widthFix"></image>
+		</view>
+		<view class="main">
+			<template v-if="loginMethod === 'name_code'">
+				<view class="form">
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-account"></view>
+							<input type="text" :value="form.name" @input="onInputName" placeholder="请输入姓名" />
+						</view>
+					</view>
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-password"></view>
+							<input type="text" :value="form.code" @input="onInputCode" placeholder="请输入登录码" />
+						</view>
+					</view>
+				</view>
+				<view class="submit" :class="{ disable: disable.submit }" @click="onLoginByNameCode">立即登录</view>
+			</template>
+			<template v-else-if="loginMethod === 'number_code'">
+				<view class="form">
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-card"></view>
+							<input type="text" :value="form.number" @input="onInputNumber" placeholder="请输入编号" />
+						</view>
+					</view>
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-password"></view>
+							<input type="text" :value="form.code" @input="onInputCode" placeholder="请输入登录码" />
+						</view>
+					</view>
+				</view>
+				<view class="submit" :class="{ disable: disable.submit }" @click="onLoginByNumberCode">立即登录</view>
+			</template>
+			<template v-else-if="loginMethod === 'phone_captcha'">
+				<view class="form">
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-phone"></view>
+							<input type="text" :value="form.phone" @input="onInputPhone" placeholder="请输入手机号" />
+						</view>
+					</view>
+					<view class="b-row">
+						<view class="b-input">
+							<view class="s-icon iconfont icon-password"></view>
+							<input type="text" :value="form.captcha" @input="onInputCaptcha" placeholder="请输入验证码" />
+							<view class="s-captcha" :class="{ disable: captcha.second > 0 }" @click="onPhoneCaptcha">
+								<text v-if="!captcha.retry && !captcha.second">获取验证码</text>
+								<text v-else-if="captcha.second > 0">重新获取 {{ captcha.second }}秒</text>
+								<text v-else-if="captcha.retry">重新获取</text>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="submit" :class="{ disable: disable.submit }" @click="onLoginByPhoneCaptcha">立即登录</view>
+			</template>
+			<!-- #ifdef  MP-WEIXIN -->
+			<button class="submit f-wechat open-data-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber"
+				v-if="loginMethods.length === 1 &&  loginMethod === 'wechat_phone'">手机号快捷登录</button>
+			<!-- #endif -->
+			<view class="other" v-if="showOtherMethods">
+				<view class="split">
+					<view class="s-line"></view>
+					<view class="s-text">其他登录方式</view>
+					<view class="s-line"></view>
+				</view>
+				<view class="login-method">
+					<view class="b-method f-name"
+						v-if="loginMethods.indexOf('name_code') !== -1 && loginMethod !== 'name_code'"
+						@click="onChangeMethod('name_code')">
+						<view class="iconfont icon-key"></view>
+						<view class="s-text">姓名登录码</view>
+					</view>
+					<view class="b-method f-number"
+						v-if="loginMethods.indexOf('number_code') !== -1 && loginMethod !== 'number_code'"
+						@click="onChangeMethod('number_code')">
+						<view class="iconfont icon-key"></view>
+						<view class="s-text">编号登录码</view>
+					</view>
+					<view class="b-method f-phone"
+						v-if="loginMethods.indexOf('phone_captcha') !== -1 && loginMethod !== 'phone_captcha'"
+						@click="onChangeMethod('phone_captcha')">
+						<view class="iconfont icon-verify-fill"></view>
+						<view class="s-text">手机验证码</view>
+					</view>
+					<!-- #ifdef  MP-WEIXIN -->
+					<button class="b-method f-wechat open-data-btn" open-type="getPhoneNumber"
+						@getphonenumber="onGetPhoneNumber" v-if="loginMethods.indexOf('wechat_phone') !== -1">
+						<view class="iconfont icon-phone"></view>
+						<view class="s-text">手机号快捷登录</view>
+					</button>
+					<!-- #endif -->
+				</view>
+			</view>
+			<view class="auth-tips" v-if="openRegister">
+				<view class="s-text" @click="onJumpRegister">还没有账号?立即注册</view>
+			</view>
+			<view class="tips">{{loginNote}}</view>
+		</view>
+		<view class="select-activity" v-if="selectActivity" @click="onJumpActivity">
+			<view class="iconfont icon-dashboard"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		loginByNameCode,
+		loginByNumberCode,
+		loginByPhoneCaptcha,
+		loginByWechatPhone,
+		sendPhoneCaptcha
+	} from '../../service/api/user.js'
+	import {
+		smartLogin
+	} from '../../service/request/main.js'
+	import {
+		decodeRedirectUrl,
+		showToast
+	} from './../common/util'
+	import mixinsCommon from '../../mixins/common.js'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon],
+		data() {
+			return {
+				form: {
+					name: '',
+					number: '',
+					code: '',
+					phone: '',
+					captcha: '',
+				},
+				loginMethod: '',
+				disable: {
+					captcha: false,
+					submit: false
+				},
+				captcha: {
+					retry: false,
+					second: 0,
+				},
+				appStore: useAppStore()
+			}
+		},
+		onLoad(options) {
+			this.returnPage = options.from ? decodeRedirectUrl(options.from) : 'pages/home/index'
+			console.log('login returnPage', this.returnPage)
+			smartLogin('', true) // 判断是否扫码或进入活动列表页,因为当前页没有接口请求和权限校验
+		},
+		computed: {
+			selectActivity() {
+				return this.appStore.getGlobalConfig.select_activity
+			},
+			topImage() {
+				return this.appStore.getGlobalConfig.login_top_img
+			},
+			loginNote() {
+				return this.appStore.getGlobalConfig.login_note || '注:仅内部使用,请选择合适的方式登录参加答题竞赛活动。'
+			},
+			loginMethods() {
+				// ['name_code','number_code',phone_captcha','wechat_phone']
+				let loginMethods = this.appStore.getGlobalConfig.login_methods || []
+				if (loginMethods.length > 0) {
+					this.loginMethod = loginMethods[0]
+					console.log('this.loginMethod')
+				}
+
+				return loginMethods
+			},
+			openRegister() {
+				return this.appStore.getGlobalConfig.open_register
+			},
+			showOtherMethods() {
+				let show = this.loginMethods.length >= 2
+				// #ifndef MP-WEIXIN
+				show = this.loginMethods.length >= 3 ||
+					(this.loginMethods.length === 2 && this.loginMethods.indexOf('wechat_phone') === -1)
+				// #endif
+
+				return show
+			}
+		},
+		methods: {
+			onInputName(e) {
+				// console.log('onInputName', e)
+				this.form.name = e.detail.value
+			},
+			onInputNumber(e) {
+				// console.log('onInputName', e)
+				this.form.number = e.detail.value
+			},
+			onInputCode(e) {
+				// console.log('onInputCode', e)
+				this.form.code = e.detail.value
+			},
+			onInputPhone(e) {
+				// console.log('onInputPhone', e)
+				this.form.phone = e.detail.value;
+			},
+			onInputCaptcha(e) {
+				// console.log('onInputCaptcha', e)
+				this.form.captcha = e.detail.value
+			},
+			onChangeMethod(method) {
+				this.loginMethod = method
+			},
+			onPhoneCaptcha() {
+				if (this.disable.captcha) {
+					return
+				}
+				this.disable.captcha = true
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					sendPhoneCaptcha(this.form.phone, res ? res.code : '', true).then(([err, res]) => {
+						console.log('sendPhoneCaptcha', err, res)
+						if (!err) {
+							uni.showToast({
+								title: '发送成功'
+							})
+
+							// 验证码倒计时
+							this.captcha.second = 60
+							var interval = setInterval(() => {
+								this.captcha.second--
+							}, 1000)
+							setTimeout(() => {
+								clearInterval(interval)
+								this.captcha.retry = true
+								this.captcha.second = 0
+								this.disable.captcha = false
+							}, this.captcha.second * 1000)
+						} else {
+							this.disable.captcha = false
+						}
+					})
+				})
+			},
+			onLoginByNameCode() {
+				if (this.disable.submit) {
+					return
+				}
+				if (!this.form.name) {
+					return showToast('姓名不能为空')
+				}
+				if (!this.form.code) {
+					return showToast('登录码不能为空')
+				}
+				this.disable.submit = true;
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					loginByNameCode(this.form.name, this.form.code, res ? res.code : '').then(([err, res]) => {
+						console.log('loginByNameCode', err, res)
+						this.disable.submit = false
+						if (!err) {
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							showToast('登录成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/' + this.returnPage
+								})
+							})
+						}
+					})
+				})
+			},
+			onLoginByNumberCode() {
+				if (this.disable.submit) {
+					return
+				}
+				if (!this.form.number) {
+					return showToast('编号不能为空')
+				}
+				if (!this.form.code) {
+					return showToast('登录码不能为空')
+				}
+				this.disable.submit = true;
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					loginByNumberCode(this.form.number, this.form.code, res ? res.code : '').then(([err, res]) => {
+						console.log('loginByNumberCode', err, res)
+						this.disable.submit = false
+						if (!err) {
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							showToast('登录成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/' + this.returnPage
+								})
+							})
+						}
+					})
+				})
+			},
+			onLoginByPhoneCaptcha() {
+				if (this.disable.submit) {
+					return
+				}
+				if (!this.form.phone) {
+					return showToast('手机号不能为空')
+				}
+				if (!this.form.captcha) {
+					return showToast('验证码不能为空')
+				}
+				this.disable.submit = true
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					loginByPhoneCaptcha(this.form.phone, this.form.captcha, res ? res.code : '').then(([err,
+						res
+					]) => {
+						console.log('loginByPhoneCaptcha', err, res)
+						this.disable.submit = false
+						if (!err) {
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							showToast('登录成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/' + this.returnPage
+								})
+							})
+						}
+					})
+				})
+			},
+			onGetPhoneNumber(e) {
+				console.log('onGetPhoneNumber', e)
+				if (e.detail.errMsg.indexOf(':ok') === -1) {
+					// fail user deny
+					// showToast(e.detail.errMsg.replace('getPhoneNumber:', ''))
+					return
+				}
+				uni.showLoading({
+					title: '尝试登录中'
+				})
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					loginByWechatPhone(e.detail.encryptedData, e.detail.iv, e.detail.code || '', res.code).then(([
+						err, res
+					]) => {
+						console.log('loginByWechatPhone', err, res)
+						uni.hideLoading()
+						if (!err) {
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							showToast('登录成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/' + this.returnPage
+								})
+							})
+						}
+					})
+				})
+			},
+			onJumpActivity() {
+				uni.redirectTo({
+					url: '/pages/third/yunexamine/pages/home/dashboard'
+				})
+			},
+			onJumpRegister() {
+				uni.redirectTo({
+					url: '/pages/third/yunexamine/pages/user/register'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #ffffff;
+	}
+
+	.wrap {}
+
+	.logo {
+		image {
+			width: 100%;
+			height: 460upx;
+		}
+	}
+
+	.main {
+		padding: 50upx;
+	}
+
+	.form {
+		.b-row {
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+			margin-bottom: 30upx;
+			border-bottom: 1upx solid #E5E5E5;
+		}
+
+		.b-input {
+			flex: 1;
+			padding: 0 20upx;
+			width: 100%;
+			height: 90upx;
+			display: flex;
+			align-items: center;
+
+			.s-icon {
+				color: #ccc;
+				font-size: 40upx;
+			}
+
+			input {
+				margin-left: 30upx;
+				flex: 1;
+				color: #333;
+				font-size: 28upx;
+			}
+
+			.s-captcha {
+				padding: 0 30upx;
+				background: #fff;
+				border: 2upx #ccc solid;
+				height: 64upx;
+				line-height: 64upx;
+				border-radius: 32upx;
+				color: #808080;
+				font-size: 24upx;
+
+				&.disable {
+					color: #999;
+				}
+			}
+		}
+	}
+
+	.submit {
+		margin-top: 80upx;
+		height: 100upx;
+		line-height: 100upx;
+		text-align: center;
+		color: #fff;
+		font-size: 32upx;
+		background: #DA5650;
+		border-radius: 50upx;
+		letter-spacing: 10upx;
+
+		&.f-wechat {
+			background: #35C773;
+		}
+
+		&.disable {
+			background: rgba($color: #DA5650, $alpha: 0.2);
+		}
+	}
+
+	.split {
+		margin-top: 50upx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.s-line {
+			width: 200upx;
+			height: 1upx;
+			background: #eee;
+		}
+
+		.s-text {
+			font-size: 28upx;
+			color: #808080;
+		}
+	}
+
+	.login-method {
+		margin-top: 30upx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-wrap: wrap;
+
+		.b-method {
+			margin: 0 20upx 20upx 20upx;
+			width: 250upx;
+			// padding: 0 20upx;
+			height: 80upx;
+			border-radius: 10upx;
+			background: #808080;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			color: #fff;
+
+			&.f-name {
+				background: #64A2FA;
+			}
+
+			&.f-number {
+				background: #EE6C77;
+			}
+
+			&.f-phone {
+				background: #FF9C45;
+			}
+
+			&.f-wechat {
+				background: #35C773;
+			}
+
+			.iconfont {
+				margin-right: 10upx;
+				font-size: 40upx;
+			}
+
+			.s-text {
+				font-size: 26upx;
+			}
+		}
+	}
+
+	.auth-tips {
+		margin-top: 50upx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.s-text {
+			font-size: 28upx;
+			color: #808080;
+		}
+	}
+
+	.tips {
+		margin-top: 50upx;
+		width: 100%;
+		color: #383838;
+		font-size: 24upx;
+		text-align: center;
+	}
+
+	.select-activity {
+		position: absolute;
+		bottom: 50upx;
+		right: 50upx;
+		background: #fff;
+		border: 2upx solid #ddd;
+		width: 100upx;
+		height: 100upx;
+		border-radius: 50upx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.iconfont {
+			font-size: 50upx;
+			color: #DA5650;
+		}
+	}
+</style>

+ 283 - 0
src/pages/third/yunexamine/pages/user/mine.vue

@@ -0,0 +1,283 @@
+<template>
+	<view class="wrap">
+		<view class="main" v-if="userInfo">
+			<view class="user-view">
+				<view class="b-avatar">
+					<image :src="userInfo.avatar_url || 'https://xy.wenlvti.net/app_static/yunexamine/imgs/user.png'" mode="aspectFill">
+					</image>
+				</view>
+				<view class="b-info">
+					<!-- #ifdef  MP-WEIXIN -->
+					<button class="s-edit iconfont icon-edit open-data-btn" open-type="chooseAvatar"
+						@chooseavatar="onChooseAvatar"></button>
+					<!-- #endif -->
+					<!-- #ifdef  H5 -->
+					<view class="s-edit iconfont icon-edit" @click="onUploadAvatar"></view>
+					<!-- #endif -->
+					<view class="s-status" v-if="userInfo.status != 1">{{userInfo.status_text}}</view>
+				</view>
+			</view>
+			<view class="user-info">
+				<view class="b-item" v-if="disableOptions.indexOf('user_name') === -1">
+					<text class="s-tit">姓名</text>
+					<text class="s-tip">{{ userInfo.name }}</text>
+				</view>
+				<view class="b-item" v-if="disableOptions.indexOf('user_number') === -1">
+					<text class="s-tit">编号</text>
+					<text class="s-tip">{{ userInfo.number }}</text>
+				</view>
+				<view class="b-item" v-if="disableOptions.indexOf('user_phone') === -1">
+					<text class="s-tit">手机号</text>
+					<text class="s-tip">{{ userInfo.phone }}</text>
+				</view>
+				<view class="b-item" v-if="disableOptions.indexOf('user_group') === -1">
+					<text class="s-tit">所在分组</text>
+					<text class="s-tip">{{ userInfo.group_name }}</text>
+				</view>
+				<!-- #ifdef  MP-WEIXIN -->
+				<view class="b-item" @click="onChangePhone"
+					v-if="disableOptions.indexOf('change_phone') === -1 && (loginMethods.indexOf('phone_captcha') !== -1 || loginMethods.indexOf('wechat_phone') !== -1)">
+					<text class="s-tit">修改手机号</text>
+					<text class="s-tip"></text>
+					<text class="s-more iconfont icon-more"></text>
+				</view>
+				<!-- #endif -->
+				<!-- #ifdef  H5 -->
+				<view class="b-item" @click="onChangePhone"
+					v-if="disableOptions.indexOf('change_phone') === -1 && loginMethods.indexOf('phone_captcha') !== -1">
+					<text class="s-tit">修改手机号</text>
+					<text class="s-tip"></text>
+					<text class="s-more iconfont icon-more"></text>
+				</view>
+				<!-- #endif -->
+				<view class="b-item" @click="onChangeCode"
+					v-if="disableOptions.indexOf('change_code') === -1 && (loginMethods.indexOf('name_code') !== -1 || loginMethods.indexOf('number_code') !== -1)">
+					<text class="s-tit">修改登录码</text>
+					<text class="s-tip"></text>
+					<text class="s-more iconfont icon-more"></text>
+				</view>
+				<view class="b-item" @click="onFeedback" v-if="disableOptions.indexOf('feedback') === -1">
+					<text class="s-tit">意见反馈</text>
+					<text class="s-tip"></text>
+					<text class="s-more iconfont icon-more"></text>
+				</view>
+			</view>
+			<view class="btn-logout" @click="onLogout" v-if="disableOptions.indexOf('logout') === -1">退出登录</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getUserInfo,
+		updateUserInfo,
+		logout
+	} from '../../service/api/user.js'
+	import {
+		uploadImage
+	} from '../../service/api/common.js'
+	import {
+		compressImage, showToast 
+  } from '../../common/util.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon],
+		data() {
+			return {
+				userInfo: null,
+				appStore: useAppStore()
+			}
+		},
+		onLoad() {
+			getUserInfo().then(([err, res]) => {
+				console.log('getUserInfo', err, res)
+				if (!err) {
+					this.userInfo = res
+				}
+			})
+		},
+		computed: {
+			disableOptions() {
+				return this.appStore.getGlobalConfig.disable_options || []
+			},
+			loginMethods() {
+				return this.appStore.getGlobalConfig.login_methods || []
+			},
+		},
+		methods: {
+			onChangePhone() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/user/change_phone'
+				})
+			},
+			onChangeCode() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/user/change_code'
+				})
+			},
+			onFeedback() {
+				uni.navigateTo({
+					url: '/pages/third/yunexamine/pages/user/feedback'
+				})
+			},
+			onLogout() {
+				logout().then(([err, res]) => {
+					console.log('logout', err, res)
+					this.appStore.delAccessToken();
+					uni.reLaunch({
+						url: '/pages/third/yunexamine/pages/user/login'
+					})
+				})
+			},
+			onChooseAvatar(e) {
+				console.log('onChooseAvatar', e)
+				let imageUrl = e.detail.avatarUrl
+				uploadImage(imageUrl).then(([err, res]) => {
+					console.log('uploadImage', err, res)
+					if (!err) {
+						updateUserInfo(res.url).then(([err, res]) => {
+							console.log('updateUserInfo', err, res)
+							if (!err) {
+								this.userInfo.avatar_url = imageUrl
+								showToast('修改成功')
+							}
+						})
+					}
+				})
+			},
+			onUploadAvatar(e) {
+				console.log('onUploadAvatar', e)
+				uni.chooseImage({
+					count: 1, // 1张
+					sizeType: ['compressed'], // 压缩图
+				}).then(([err, res]) => {
+					console.log('chooseImage', err, res)
+					if (!err) {
+						compressImage(res.tempFiles[0], 500, 500).then(([err, res]) => {
+							console.log('compressImage', err, res)
+							if (!err) {
+								let imageUrl = res
+								uploadImage(imageUrl).then(([err, res]) => {
+									console.log('uploadImage', err, res)
+									if (!err) {
+										updateUserInfo(res.url).then(([err, res]) => {
+											console.log('updateUserInfo', err, res)
+											if (!err) {
+												this.userInfo.avatar_url = imageUrl
+												showToast('修改成功')
+											}
+										})
+									}
+								})
+							}
+						})
+					}
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 0 40upx 100upx 40upx;
+	}
+
+	.user-view {
+		margin-top: 50upx;
+		display: flex;
+		align-items: center;
+		flex-direction: column;
+		z-index: 1;
+
+		.b-avatar {
+			width: 180upx;
+			height: 180upx;
+
+			image {
+				width: 180upx;
+				height: 180upx;
+				border: 5upx solid #fff;
+				border-radius: 50%;
+			}
+		}
+
+		.b-info {
+			margin-top: 10upx;
+			display: flex;
+			align-items: center;
+			flex-direction: column;
+
+			.s-nickname {
+				font-weight: bold;
+			}
+
+			.s-edit {
+				margin-top: 20upx;
+				font-size: 32upx;
+				color: #bbb;
+			}
+
+			.s-status {
+				margin-top: 20upx;
+				padding: 0 30upx;
+				height: 50upx;
+				line-height: 50upx;
+				border-radius: 25upx;
+				background: #DA5650;
+				font-size: 26upx;
+				color: #fff;
+			}
+		}
+	}
+
+	.user-info {
+		margin-top: 50upx;
+		background: #fff;
+		border-radius: 20upx;
+
+		.b-item {
+			padding: 0 40upx;
+			border-bottom: 1upx solid $pq-bg-color;
+			height: 100upx;
+			display: flex;
+			align-items: center;
+			position: relative;
+			font-size: 26upx;
+
+			.s-tit {
+				color: #000;
+			}
+
+			.s-tip {
+				flex: 1;
+				color: #808080;
+				text-align: right;
+			}
+
+			.s-more {
+				margin-left: 10upx;
+				color: #808080;
+			}
+		}
+	}
+
+	.btn-logout {
+		margin-top: 60upx;
+		height: 80upx;
+		line-height: 80upx;
+		text-align: center;
+		color: #DA5650;
+		background: #ffffff;
+		font-size: 32upx;
+		border: 1px solid #DA5650;
+		border-radius: 40upx;
+		letter-spacing: 10upx;
+	}
+</style>

+ 455 - 0
src/pages/third/yunexamine/pages/user/register.vue

@@ -0,0 +1,455 @@
+<template>
+	<view class="wrap">
+		<view class="box">
+			<view class="b-row">
+				<view class="b-tit">所在分组</view>
+				<view class="b-cont f-option" v-if="groupList.length > 0">
+					<picker @change="onFilter" data-field="group_id" :value="groupIndex" :range="groupList"
+						range-key="name">
+						<view class="b-main">
+							<view class="s-cont">{{this.groupList[this.groupIndex].name}}</view>
+							<view class="iconfont icon-more"></view>
+						</view>
+					</picker>
+				</view>
+			</view>
+			<view class="b-row">
+				<view class="b-tit">姓名</view>
+				<view class="b-cont f-input">
+					<input :value="form.name" placeholder="请输入姓名" @input="onInputText" data-field="name">
+				</view>
+			</view>
+			<view class="b-row" v-if="registerFields.indexOf('number') !== -1">
+				<view class="b-tit">编号</view>
+				<view class="b-cont f-input">
+					<input :value="form.number" placeholder="请输入编号" @input="onInputText" data-field="number">
+				</view>
+			</view>
+			<view class="b-row" v-if="registerFields.indexOf('code') !== -1">
+				<view class="b-tit">登录码</view>
+				<view class="b-cont f-input">
+					<input :value="form.code" placeholder="请输入登录码" @input="onInputText" data-field="code">
+				</view>
+			</view>
+			<view class="b-row" v-if="registerFields.indexOf('gender') !== -1">
+				<view class="b-tit">性别</view>
+				<view class="b-cont f-option">
+					<picker @change="onFilter" data-field="gender" :value="genderIndex" :range="genderList"
+						range-key="name">
+						<view class="b-main">
+							<view class="s-cont">{{this.genderList[this.genderIndex].name}}</view>
+							<view class="iconfont icon-more"></view>
+						</view>
+					</picker>
+				</view>
+			</view>
+			<view class="b-row" v-for="(item, index) in customFields">
+				<view class="b-tit">{{item.name}}</view>
+				<view class="b-cont f-input" v-if="item.type === 'input'">
+					<input :value="form.custom[item.name]" :placeholder="item.tips" @input="onInputText"
+						data-group="custom" :data-field="item.name">
+				</view>
+				<view class="b-cont f-option" v-else-if="item.type === 'select'">
+					<picker @change="onFilter" data-group="custom" :data-field="item.name"
+						:value="selectIndex(item.value_list, form.custom[item.name])" :range="item.value_list">
+						<view class="b-main">
+							<view class="s-cont">
+								{{item.value_list[selectIndex(item.value_list, form.custom[item.name])]}}
+							</view>
+							<view class="iconfont icon-more"></view>
+						</view>
+					</picker>
+				</view>
+				<view class="b-cont f-radio" v-else-if="item.type === 'radio'">
+					<radio-group @change="onFilter" data-group="custom" :data-field="item.name">
+						<label v-for="(sItem, sIndex) in item.value_list">
+							<radio :value="sItem" :checked="sItem == form.custom[item.name]" color="#DA5650" />{{sItem}}
+						</label>
+					</radio-group>
+				</view>
+				<view class="b-cont f-checkbox" v-else-if="item.type === 'checkbox'">
+					<checkbox-group @change="onFilter" data-group="custom" :data-field="item.name">
+						<label v-for="(sItem, sIndex) in item.value_list">
+							<checkbox :value="sItem" :checked="sItem == form.custom[item.name]" color="#DA5650" />
+							{{sItem}}
+						</label>
+					</checkbox-group>
+				</view>
+			</view>
+			<view class="b-row" v-if="registerFields.indexOf('phone') !== -1">
+				<view class="b-tit">手机号</view>
+				<view class="b-cont f-input">
+					<input :value="form.phone" placeholder="请输入手机号" @input="onInputText" data-field="phone">
+				</view>
+			</view>
+			<view class="b-row" v-if="registerFields.indexOf('phone') !== -1">
+				<view class="b-tit">验证码</view>
+				<view class="b-cont f-captcha">
+					<view class="s-input">
+						<input :value="form.captcha" placeholder="请输入验证码" @input="onInputText" data-field="captcha">
+					</view>
+					<view class="s-captcha" :class="{ disable: captcha.second > 0 }" @click="onPhoneCaptcha">
+						<text v-if="!captcha.retry && !captcha.second">获取验证码</text>
+						<text v-else-if="captcha.second > 0">重新获取 {{ captcha.second }}秒</text>
+						<text v-else-if="captcha.retry">重新获取</text>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="btn-save" :class="{ disable: disable.submit }" @click="onRegister">立即注册</view>
+		<view class="auth-tips">
+			<view class="s-text" @click="onJumpLogin">已有账号?立即登录</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		register,
+		sendPhoneCaptcha
+	} from '../../service/api/user.js'
+  import { showToast } from '../../common/util.js'
+	import Util from './../common/util.js'
+	import Config from '../../service/config.js'
+	import mixinsCommon from '../../mixins/common.js'
+	import { useAppStore } from '../../store/index.js'
+	export default {
+		mixins: [mixinsCommon],
+		data() {
+			return {
+				form: {
+					group_id: 0,
+					name: '',
+					number: '',
+					code: '',
+					gender: 1,
+					phone: '',
+					captcha: '',
+					custom: {}
+				},
+				disable: {
+					submit: false
+				},
+				groupIndex: 0,
+				genderList: [{
+					value: 1,
+					name: '男'
+				}, {
+					value: 2,
+					name: '女'
+				}],
+				genderIndex: 0,
+				captcha: {
+					retry: false,
+					second: 0,
+				},
+				appStore: useAppStore()
+			}
+		},
+		onLoad(options) {},
+		computed: {
+			groupList() {
+				return this.appStore.getGlobalConfig.group_list || []
+			},
+			registerFields() {
+				return this.appStore.getGlobalConfig.register_fields || []
+			},
+			customFields() {
+				return this.appStore.getGlobalConfig.custom_fields || []
+			},
+			selectIndex() {
+				return (options, value) => {
+					if (value === undefined) {
+						return 0
+					}
+					for (let i = 0; i < options.length; i++) {
+						if (options[i] == value) {
+							return i
+						}
+					}
+					return 0
+				}
+			}
+		},
+		methods: {
+			// 输入内容
+			onInputText(e) {
+				console.log('onInputText', e)
+				const group = e.target.dataset.group
+				const field = e.target.dataset.field
+				const value = e.detail.value
+				if (group) {
+					this.form[group][field] = value
+				} else {
+					this.form[field] = value
+				}
+			},
+			onFilter(e) {
+				console.log('onFilter', e.target.dataset.field, e.detail.value)
+				const group = e.target.dataset.group
+				const field = e.target.dataset.field
+				const value = e.detail.value
+				if (group) {
+					this.form[group][field] = value
+				} else {
+					if (field === 'gender') {
+						this.genderIndex = value
+						this.form.gender = this.genderList[this.genderIndex].value
+					} else if (field === 'group_id') {
+						this.groupIndex = value
+					}
+				}
+			},
+			onPhoneCaptcha() {
+				if (this.disable.captcha) {
+					return
+				}
+				this.disable.captcha = true
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					sendPhoneCaptcha(this.form.phone, res ? res.code : '', true).then(([err, res]) => {
+						console.log('sendPhoneCaptcha', err, res)
+						if (!err) {
+							showToast('发送成功')
+
+							// 验证码倒计时
+							this.captcha.second = 60
+							var interval = setInterval(() => {
+								this.captcha.second--
+							}, 1000)
+							setTimeout(() => {
+								clearInterval(interval)
+								this.captcha.retry = true
+								this.captcha.second = 0
+								this.disable.captcha = false
+							}, this.captcha.second * 1000)
+						} else {
+							this.disable.captcha = false
+						}
+					})
+				})
+			},
+			onRegister() {
+				if (this.disable.submit) {
+					return;
+				}
+				if (!this.form.name) {
+					return showToast('姓名不能为空')
+				}
+				if (this.registerFields.indexOf('phone') !== -1) {
+					if (!this.form.phone) {
+						return showToast('手机号不能为空')
+					}
+					if (!this.form.captcha) {
+						return showToast('验证码不能为空')
+					}
+				} else {
+					if (this.registerFields.indexOf('code') !== -1) {
+						if (!this.form.code) {
+							return showToast('登录码不能为空')
+						}
+					}
+				}
+				for (const item of this.customFields) {
+					if (!this.form.custom.hasOwnProperty(item.name)) {
+						if (item.type === 'input' || item.type === 'radio') {
+							this.form.custom[item.name] = ''
+						} else if (item.type === 'select') {
+							this.form.custom[item.name] = item.value_list[0]
+						} else if (item.type === 'checkbox') {
+							this.form.custom[item.name] = []
+						}
+					}
+					if (item.required == 1) {
+						if (!this.form.custom[item.name] ||
+							(item.type === 'checkbox' && this.form.custom[item.name].length === 0)) {
+							return showToast(item.name + '不能为空')
+						}
+					}
+				}
+				console.log('form', this.form)
+				this.disable.submit = true
+				uni.login().then(([err, res]) => {
+					console.log('uni.login', err, res)
+					this.form.wx_code = res ? res.code : ''
+					this.form.group_id = this.groupList[this.groupIndex].value
+					register(this.form).then(([err, res]) => {
+						console.log('register', err, res)
+						this.disable.submit = false
+						if (!err) {
+							this.appStore.setAccessToken(res.token)
+							this.appStore.setActivityId(res.activity_id)
+							showToast('注册成功').then(([err, res]) => {
+								uni.reLaunch({
+									url: '/pages/third/yunexamine/pages/home/index'
+								})
+							})
+						}
+					})
+				})
+			},
+			onJumpLogin() {
+				uni.redirectTo({
+					url: '/pages/third/yunexamine/pages/user/login'
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		
+	}
+
+	.wrap {
+		padding: 0 40upx;
+	}
+
+	.box {
+		margin-top: 30upx;
+		background: #fff;
+		border-radius: 20upx;
+
+		.b-row {
+			padding: 30upx;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			.b-tit {
+				font-size: 28upx;
+				width: 180upx;
+				color: #666;
+			}
+
+			.b-cont {
+				// flex: 1;
+				display: flex;
+				align-items: center;
+
+				input {
+					text-align: right;
+				}
+
+				textarea {
+					text-align: right;
+				}
+
+				&.f-option {
+
+					.b-main {
+						display: flex;
+						align-items: center;
+
+						.s-cont {
+							font-size: 28upx;
+						}
+
+						.iconfont {
+							margin-left: 10upx;
+						}
+					}
+				}
+
+				&.f-input {
+					input {
+						width: 480upx;
+					}
+				}
+
+				&.f-textarea {
+					textarea {
+						width: 530upx;
+						font-size: 28upx;
+					}
+				}
+
+				&.f-checkbox {
+					font-size: 28upx;
+
+					label {
+						margin-left: 30upx;
+					}
+
+					checkbox {
+						transform: scale(0.8)
+					}
+				}
+
+				&.f-radio {
+					font-size: 28upx;
+
+					label {
+						margin-left: 30upx;
+					}
+
+					radio {
+						transform: scale(0.8)
+					}
+				}
+
+				&.f-captcha {
+					flex: 1;
+					display: flex;
+					align-items: center;
+					justify-content: flex-end;
+
+					.s-input {
+						width: 180upx;
+					}
+
+					.s-captcha {
+						margin-left: 20upx;
+						padding: 0 30upx;
+						background: #fff;
+						border: 2upx #ccc solid;
+						height: 64upx;
+						line-height: 64upx;
+						border-radius: 32upx;
+						color: #808080;
+						font-size: 24upx;
+
+						&.disable {
+							color: #999;
+						}
+					}
+				}
+			}
+		}
+
+
+		input {
+			font-size: 28upx;
+		}
+
+		.input-placeholder {
+			font-size: 28upx;
+		}
+	}
+
+	.btn-save {
+		margin-top: 50upx;
+		height: 100upx;
+		line-height: 100upx;
+		text-align: center;
+		color: #fff;
+		font-size: 32upx;
+		background: #DA5650;
+		border-radius: 50upx;
+		letter-spacing: 10upx;
+	}
+
+	.auth-tips {
+		margin-top: 50upx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.s-text {
+			font-size: 28upx;
+			color: #808080;
+		}
+	}
+</style>

+ 58 - 0
src/pages/third/yunexamine/pages/user/tips.vue

@@ -0,0 +1,58 @@
+<template>
+	<view class="wrap">
+		<view class="tips">
+			<image class="b-image" src="https://xy.wenlvti.net/app_static/yunexamine/imgs/loading_image.png" alt="" mode="aspectFit"></image>
+			<view class="b-tit">{{content}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				content: '系统异常,请联系客服人员处理'
+			}
+		},
+		onLoad(options) {
+			console.log('onLoad tips', options)
+			this.content = options.cont || '请扫活动二维码进入'
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss">
+	page {
+		padding-bottom: env(safe-area-inset-bottom);
+		background: #ffffff;
+		height: 100%;
+	}
+
+	.wrap {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		height: 100%;
+	}
+
+	.tips {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.b-image {
+			width: 300upx;
+			height: 240upx;
+		}
+
+		.b-tit {
+			margin-top: 40upx;
+			color: #666;
+			font-size: 28upx;
+		}
+	}
+</style>

+ 49 - 0
src/pages/third/yunexamine/service/api/common.js

@@ -0,0 +1,49 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js'
+
+/**
+ * 获取配置数据
+ * 
+ * @returns {Promise}
+ */
+export function getConfig() {
+	return http.post('/common/getConfig', {}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 获取活动列表
+ *
+ * @returns {Promise}
+ */
+export function getActivityList(group, page, page_size) {
+	return http.post('/common/getActivityList', {
+		group,
+		page,
+		page_size
+	}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 上传图片
+ * 
+ * @returns {Promise}
+ */
+export function uploadImage(imageUrl) {
+	return http.post('/api/common/upload', {
+		file: {
+			file: imageUrl,
+			name: 'file'
+		}
+	}, {
+		removeAddon: true,
+		uploadFile: true
+	})
+}

+ 127 - 0
src/pages/third/yunexamine/service/api/examine.js

@@ -0,0 +1,127 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js'
+
+/**
+ * 开始答题
+ * 
+ * @returns {Promise}
+ */
+export function startChallenge() {
+	return http.post('/examine/startChallenge')
+}
+
+/**
+ * 开始练习
+ *
+ * @returns {Promise}
+ */
+export function startExercise() {
+	return http.post('/examine/startExercise')
+}
+
+/**
+ * 开始复习
+ *
+ * @returns {Promise}
+ */
+export function startReview() {
+	return http.post('/examine/startReview')
+}
+
+/**
+ * 获取新题
+ * 
+ * @returns {Promise}
+ */
+export function getNew(session_id) {
+	return http.post('/examine/getNew', {
+		session_id
+	})
+}
+
+/**
+ * 提交答案
+ * 
+ * @returns {Promise}
+ */
+export function submitAnswer(id, answer) {
+	return http.post('/examine/submitAnswer', {
+		id,
+		answer
+	})
+}
+
+/**
+ * 获取考核结果
+ * 
+ * @returns {Promise}
+ */
+export function getSummary(session_id) {
+	return http.post('/examine/getSummary', {
+		session_id
+	})
+}
+
+/**
+ * 获取考核历史
+ * 
+ * @returns {Promise}
+ */
+export function getHistory(session_id, page, page_size) {
+	return http.post('/examine/getHistory', {
+		session_id,
+		page,
+		page_size
+	})
+}
+
+/**
+ * 获取错题列表
+ *
+ * @returns {Promise}
+ */
+export function getWrongList(page, page_size) {
+	return http.post('/examine/getWrongList', {
+		page,
+		page_size
+	})
+}
+
+/**
+ * 获取答题结果
+ * 
+ * @returns {Promise}
+ */
+export function getResult(id) {
+	return http.post('/examine/getResult', {
+		id
+	})
+}
+
+/**
+ * 获取排行数据
+ * 
+ * @returns {Promise}
+ */
+export function getTopList(group, page, page_size) {
+	return http.post('/examine/getTopList', {
+		group,
+		page,
+		page_size
+	})
+}
+
+/**
+ * 获取我的排行
+ * 
+ * @returns {Promise}
+ */
+export function getMineTop(group) {
+	return http.post('/examine/getMineTop', {
+		group
+	})
+}

+ 44 - 0
src/pages/third/yunexamine/service/api/feedback.js

@@ -0,0 +1,44 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js';
+
+
+/**
+ * 提交反馈
+ * 
+ * @returns {Promise}
+ */
+export function submit(type, content, photos) {
+	return http.post('/feedback/submit', {
+		type,
+		content,
+		photos
+	})
+}
+
+
+/**
+ * 反馈列表
+ * 
+ * @returns {Promise}
+ */
+export function getRecordList(page, page_size) {
+	return http.post('/feedback/getRecordList', {
+		page,
+		page_size
+	});
+}
+
+/**
+ * 反馈详情
+ * 
+ * @returns {Promise}
+ */
+export function getRecordDetail(id) {
+	return http.post('/feedback/getRecordDetail', {
+		id
+	});
+}

+ 98 - 0
src/pages/third/yunexamine/service/api/gift.js

@@ -0,0 +1,98 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js';
+
+
+/**
+ * 获取整体数据
+ * 
+ * @returns {Promise}
+ */
+export function getWrapData() {
+	return http.post('/gift/getWrapData');
+}
+
+/**
+ * 获取奖品详情
+ * 
+ * @returns {Promise}
+ */
+export function getDetail(id) {
+	return http.post('/gift/getDetail', {
+		id
+	});
+}
+
+
+/**
+ * 获取兑换预览
+ * 
+ * @returns {Promise}
+ */
+export function getPreview(gift_id, gift_spec_id) {
+	return http.post('/gift/getPreview', {
+		gift_id,
+		gift_spec_id
+	});
+}
+
+/**
+ * 进行兑换
+ * 
+ * @returns {Promise}
+ */
+export function exchange(formData) {
+	return http.post('/gift/exchange', formData);
+}
+
+
+/**
+ * 取消订单
+ * 
+ * @returns {Promise}
+ */
+export function orderCancel(order_id) {
+	return http.post('/gift/orderCancel', {
+		order_id
+	});
+}
+
+
+
+/**
+ * 获取兑换列表
+ * 
+ * @returns {Promise}
+ */
+export function getOrderList(group, page, page_size) {
+	return http.post('/gift/getOrderList', {
+		group,
+		page,
+		page_size
+	});
+}
+
+/**
+ * 获取兑换详情
+ * 
+ * @returns {Promise}
+ */
+export function getOrderDetail(id) {
+	return http.post('/gift/getOrderDetail', {
+		id
+	});
+}
+
+/**
+ * 获取物流动态
+ * 
+ * @returns {Promise}
+ */
+export function getExpressFeed(id) {
+	return http.post('/gift/getExpressFeed', {
+		id
+	});
+}

+ 57 - 0
src/pages/third/yunexamine/service/api/mock.js

@@ -0,0 +1,57 @@
+/**
+ * 模拟 API
+ */
+import {
+	http
+} from '../request/index.js' // 请求拦截
+import Cache from '../../common/cache.js'
+
+/**
+ * 数据列表
+ * 
+ * @returns {Promise}
+ */
+export function getList() {
+	return new Promise(resolve => {
+		const cacheKey = 'data:list'		const data = Cache.get(cacheKey)		if (data) {
+			resolve([0, data])		} else {
+			setTimeout(() => {
+				resolve([0, [{
+					id: 1,
+					type: 1
+				}, {
+					id: 2,
+					type: 2
+				}, {
+					id: 3,
+					type: 1
+				}, {
+					id: 4,
+					type: 2
+				}]])			}, 500)			// http.get('/v1/data/list').then(([err, res]) => {
+			// 	if (! err) {
+			// 		Cache.put(cacheKey, res, 60)			// 		resolve([0, res])			// 	} else {
+			// 		resolve([err])			// 	}	
+			// })		}
+	})}
+
+/**
+ * 数据详情
+ * 
+ * @returns {Promise}
+ */
+export function getDetail(id, refresh) {
+	return new Promise(resolve => {
+		const cacheKey = 'data:detail:' + id		const data = Cache.get(cacheKey)		if (data && !refresh) {
+			resolve([0, data])		} else {
+			setTimeout(() => {
+				resolve([0, {
+					id: 1,
+					type: 1,
+					name: '测试内容'
+				}])			}, 500)			// http.get('/v1/data/detail').then(([err, res]) => {
+			// 	if (! err) {
+			// 		Cache.put(cacheKey, res, 60)			// 		resolve([0, res])			// 	} else {
+			// 		resolve([err])			// 	}	
+			// })		}
+	})}

+ 13 - 0
src/pages/third/yunexamine/service/api/page.js

@@ -0,0 +1,13 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js'
+/**
+ * 获取首页数据
+ * 
+ * @returns {Promise}
+ */
+export function getHomeData() {
+	return http.post('/page/getHomeData')}

+ 174 - 0
src/pages/third/yunexamine/service/api/user.js

@@ -0,0 +1,174 @@
+/**
+ * 授权 API
+ */
+import {
+	http
+} from '../request/main.js'
+
+/**
+ * 用户注册
+ *
+ * @returns {Promise}
+ */
+export function register(data) {
+	return http.post('/user/register', data, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 用户登录 - 姓名登录码
+ * 
+ * @returns {Promise}
+ */
+export function loginByNameCode(name, code, wx_code) {
+	return http.post('/user/loginByNameCode', {
+		name,
+		code,
+		wx_code
+	}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 用户登录 - 编号登录码
+ *
+ * @returns {Promise}
+ */
+export function loginByNumberCode(number, code, wx_code) {
+	return http.post('/user/loginByNumberCode', {
+		number,
+		code,
+		wx_code
+	}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 用户登录 - 手机验证码
+ * 
+ * @returns {Promise}
+ */
+export function loginByPhoneCaptcha(phone, captcha, wx_code) {
+	return http.post('/user/loginByPhoneCaptcha', {
+		phone,
+		captcha,
+		wx_code
+	}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 用户登录 - 微信快捷登录
+ * 
+ * @returns {Promise}
+ */
+export function loginByWechatPhone(encrypted_data, iv, code, wx_code) {
+	return http.post('/user/loginByWechatPhone', {
+		encrypted_data,
+		iv,
+		code,
+		wx_code
+	}, {
+		muteLogin: true
+	})
+}
+
+/**
+ * 小程序自动登录
+ * 
+ * @returns {Promise}
+ */
+export function wechatAutoLogin(wx_code) {
+	return http.post('/user/wechatAutoLogin', {
+		wx_code
+	}, {
+		muteLogin: true
+	})
+}
+
+
+/**
+ * 发送手机验证吗
+ * 
+ * @returns {Promise}
+ */
+export function sendPhoneCaptcha(phone, wx_code, muteLogin) {
+	return http.post('/user/sendPhoneCaptcha', {
+		phone,
+		wx_code
+	}, {
+		muteLogin: muteLogin
+	})
+}
+
+
+/**
+ * 用户登出
+ * 
+ * @returns {Promise}
+ */
+export function logout() {
+	return http.post('/user/logout')
+}
+
+/**
+ * 更新用户信息
+ * 
+ * @returns {Promise}
+ */
+export function updateUserInfo(avatar_url) {
+	return http.post('/user/updateUserInfo', {
+		avatar_url
+	})
+}
+
+/**
+ * 获取用户信息
+ * 
+ * @returns {Promise}
+ */
+export function getUserInfo() {
+	return http.post('/user/getUserInfo')
+}
+
+/**
+ * 修改登录码
+ * 
+ * @returns {Promise}
+ */
+export function changeCode(oldCode, newCode) {
+	return http.post('/user/changeCode', {
+		oldCode,
+		newCode
+	})
+}
+
+/**
+ * 修改手机号
+ * 
+ * @returns {Promise}
+ */
+export function changePhone(phone, captcha) {
+	return http.post('/user/changePhone', {
+		phone,
+		captcha
+	})
+}
+
+/**
+ * 修改手机号 - 授权微信手机号
+ * 
+ * @returns {Promise}
+ */
+export function changeWechatPhone(encrypted_data, iv, code, wx_code) {
+	return http.post('/user/changeWechatPhone', {
+		encrypted_data,
+		iv,
+		code,
+		wx_code
+	})
+}

+ 6 - 0
src/pages/third/yunexamine/service/config.js

@@ -0,0 +1,6 @@
+let baseUrl = 'https://xy.wenlvti.net/index.php/addons/yunexamine'
+
+export default {
+	baseUrl
+}
+

+ 159 - 0
src/pages/third/yunexamine/service/request/request.js

@@ -0,0 +1,159 @@
+export default class Request {
+	// 判断url是否为绝对路径
+	static absolutelyUrl(url) {
+		return /(http|https):\/\/([\w.]+\/?)\S*/.test(url)
+	};
+
+	// 发起请求前
+	static beforeRequest(config) {
+		return config
+	};
+
+	// 发起请求后
+	static afterRequest(response) {
+		return response
+	};
+
+	// 基本配置
+	config = {
+		baseUrl: '',
+		header: {
+			'Content-Type': 'application/json;charset=UTF-8'
+		},
+		method: 'GET',
+		dataType: 'json',
+		responseType: 'text',
+		success() {},
+		fail() {},
+		complete() {}
+	};
+
+	// 过滤器
+	interceptor = {
+		beforeRequest(callback) {
+			if (callback) {
+				Request.beforeRequest = callback
+			}
+		},
+		afterRequest(callback) {
+			if (callback) {
+				Request.afterRequest = callback
+			}
+		}
+	};
+
+	// 设置配置
+	setConfig(callback) {
+		this.config = callback(this.config)
+	}
+
+	// 发起请求
+	request(options = {}) {
+		// 基本参数
+		options.baseUrl = options.baseUrl || this.config.baseUrl
+		options.dataType = options.dataType || this.config.dataType
+		options.url = Request.absolutelyUrl(options.url) ? options.url : (options.baseUrl + options.url)
+		options.data = options.data || {}
+		options.header = options.header || this.config.header
+		options.method = options.method || this.config.method
+
+		// promise 请求
+		return new Promise((resolve, reject) => {
+			let execute = true
+			let _config = null
+			// 请求完成
+			options.complete = (response) => {
+				response.config = _config
+				return resolve(Request.afterRequest(response))
+			}
+			// 默认配置 + 用户配置【复制作用】
+			let mConfig = {
+				...this.config,
+				...options
+			}
+			// 取消请求 - 回调
+			let cancel = (msg = 'request cancel') => {
+				execute = false
+				return resolve([{
+					config: mConfig,
+					data: {
+						code: 0,
+						msg: msg
+					}
+				}])
+			}
+			// 默认配置 + 用户配置 + 初始配置【复制作用】
+			_config = {
+				...mConfig,
+				...Request.beforeRequest(mConfig, cancel)
+			}
+			// 是否取消请求【如果execute == false, 那么已经resolve了,所以不需要return resolve】
+			if (!execute) return
+
+			// 移除addon前缀
+			if (_config.hasOwnProperty('removeAddon')) {
+				_config.url = _config.url.replace(/\/addons\/[a-z0-9]+/g, '')
+			}
+
+			// 发起请求
+			if (_config.hasOwnProperty('uploadFile')) {
+				const data = _config.data
+				let tConfig = {
+					url: _config.url,
+					filePath: data.file.file,
+					name: data.file.name,
+					formData: data.form,
+					// #ifdef  MP-WEIXIN
+					header: _config.header,
+					// #endif
+					success: _config.success,
+					fail: _config.fail,
+					complete: _config.complete,
+				}
+				console.log('uni.uploadFile', tConfig)
+				uni.uploadFile(tConfig)
+			} else {
+				uni.request(_config)
+			}
+		})
+	};
+
+	// 接口请求 - 支持重试
+	autoRetry(options, retryMax = 3) {
+		console.log('autoRetry', options, retryMax);
+		return new Promise((resolve, reject) => {
+			if (retryMax <= 0) {
+				return resolve(['网络错误']);
+			}
+			if (retryMax == 2) {
+				console.log('this.request exect');
+				this.request(options).then(([err, res]) => {
+					console.log('this.request', err, res);
+					if (!err) {
+						resolve([err, res]);
+					} else {
+						this.autoRetry(options, retryMax - 1);
+					}
+				});
+			} else {
+				return this.autoRetry(options, retryMax - 1);
+			}
+		});
+	}
+
+	// GET 请求
+	get(url, data, options = {}) {
+		options.url = url
+		options.data = data
+		options.method = 'GET'
+		return this.request(options)
+	}
+
+	// POST 请求
+	post(url, data, options = {}) {
+		options.url = url
+		options.data = data
+		options.method = 'POST'
+		return this.request(options)
+	}
+}

+ 119 - 0
src/pages/third/yunexamine/static/iconfont.css

@@ -0,0 +1,119 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 3719789 */
+  src: url('iconfont.woff2?t=1693452193811') format('woff2'),
+       url('iconfont.woff?t=1693452193811') format('woff'),
+       url('iconfont.ttf?t=1693452193811') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-captcha:before {
+  content: "\e83f";
+}
+
+.icon-verify-fill:before {
+  content: "\e63e";
+}
+
+.icon-upload:before {
+  content: "\e601";
+}
+
+.icon-card:before {
+  content: "\e936";
+}
+
+.icon-pos:before {
+  content: "\e615";
+}
+
+.icon-feed:before {
+  content: "\e621";
+}
+
+.icon-dashboard:before {
+  content: "\e67e";
+}
+
+.icon-gift-copy:before {
+  content: "\e8b6";
+}
+
+.icon-gift-copy-copy:before {
+  content: "\e8b7";
+}
+
+.icon-gift-full:before {
+  content: "\e8b5";
+}
+
+.icon-key:before {
+  content: "\e600";
+}
+
+.icon-wechat:before {
+  content: "\e609";
+}
+
+.icon-phone:before {
+  content: "\e678";
+}
+
+.icon-edit:before {
+  content: "\e607";
+}
+
+.icon-roundcheckfill:before {
+  content: "\e656";
+}
+
+.icon-roundclosefill:before {
+  content: "\e658";
+}
+
+.icon-roundclose:before {
+  content: "\e659";
+}
+
+.icon-back:before {
+  content: "\e67a";
+}
+
+.icon-more:before {
+  content: "\e6a3";
+}
+
+.icon-right-b:before {
+  content: "\e682";
+}
+
+.icon-wrong-b:before {
+  content: "\e697";
+}
+
+.icon-roundcheck:before {
+  content: "\e657";
+}
+
+.icon-unfold:before {
+  content: "\e661";
+}
+
+.icon-fold:before {
+  content: "\e6de";
+}
+
+.icon-password:before {
+  content: "\e61f";
+}
+
+.icon-account:before {
+  content: "\e634";
+}
+

BIN
src/pages/third/yunexamine/static/iconfont.ttf


BIN
src/pages/third/yunexamine/static/iconfont.woff


BIN
src/pages/third/yunexamine/static/iconfont.woff2


BIN
src/pages/third/yunexamine/static/imgs/activity_loading.jpg


BIN
src/pages/third/yunexamine/static/imgs/gift_top_bg.jpg


BIN
src/pages/third/yunexamine/static/imgs/home_top_loading.jpg


BIN
src/pages/third/yunexamine/static/imgs/line-bg.png


BIN
src/pages/third/yunexamine/static/imgs/loading_image.png


BIN
src/pages/third/yunexamine/static/imgs/login_top_loading.jpg


BIN
src/pages/third/yunexamine/static/imgs/result_right.png


BIN
src/pages/third/yunexamine/static/imgs/result_timeout.png


BIN
src/pages/third/yunexamine/static/imgs/result_timeout_end.png


BIN
src/pages/third/yunexamine/static/imgs/result_wrong.png


BIN
src/pages/third/yunexamine/static/imgs/top_1.png


BIN
src/pages/third/yunexamine/static/imgs/top_2.png


BIN
src/pages/third/yunexamine/static/imgs/top_3.png


BIN
src/pages/third/yunexamine/static/imgs/top_bg.png


BIN
src/pages/third/yunexamine/static/imgs/user.png


BIN
src/pages/third/yunexamine/static/tabs/gift.png


BIN
src/pages/third/yunexamine/static/tabs/gift_active.png


BIN
src/pages/third/yunexamine/static/tabs/home.png


BIN
src/pages/third/yunexamine/static/tabs/home_active.png


BIN
src/pages/third/yunexamine/static/tabs/info.png


BIN
src/pages/third/yunexamine/static/tabs/info_active.png


BIN
src/pages/third/yunexamine/static/tabs/mine.png


BIN
src/pages/third/yunexamine/static/tabs/mine_active.png


BIN
src/pages/third/yunexamine/static/tabs/top.png


BIN
src/pages/third/yunexamine/static/tabs/top_active.png


+ 101 - 0
src/pages/third/yunexamine/store/index.js

@@ -0,0 +1,101 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import Util from '../common/util.js'
+import Cache from '../common/cache.js'
+
+export const useAppStore = defineStore('app', () => {
+	const lockData = ref({})
+	const accessToken = ref(null)
+	const activityId = ref(null)
+	const globalConfig = ref(null)
+
+	function lock(key) {
+		lockData.value[key] = Util.getTimestamp(null) + 5
+	}
+
+	function unlock(key) {
+		lockData.value[key] = Util.getTimestamp(null) + 2
+	}
+
+	function setAccessToken(token) {
+		accessToken.value = token
+		Cache.put('accessToken', token)
+	}
+
+	function delAccessToken() {
+		accessToken.value = null
+		Cache.remove('accessToken')
+	}
+
+	function setActivityId(id) {
+		activityId.value = id
+		Cache.put('activityId', id)
+	}
+
+	function delActivityId() {
+		activityId.value = null
+		Cache.remove('activityId')
+	}
+
+	function setGlobalConfig(config) {
+		globalConfig.value = config
+		Cache.put('globalConfig', config)
+	}
+
+	const isLock = computed(() => (key) => {
+		const lockTime = lockData.value[key]
+		const timestamp = Util.getTimestamp(null)
+		if (lockTime && lockTime > timestamp) {
+			return true
+		}
+		return false
+	})
+
+	const getAccessToken = computed(() => {
+		if (accessToken.value === null) {
+			accessToken.value = Cache.get('accessToken')
+		}
+		return accessToken.value
+	})
+
+	const getActivityId = computed(() => {
+		if (activityId.value === null) {
+			activityId.value = Cache.get('activityId') || 0
+		}
+		return activityId.value
+	})
+
+	const isLogin = computed(() => {
+		if (accessToken.value === null) {
+			accessToken.value = Cache.get('accessToken')
+		}
+		return !!accessToken.value
+	})
+
+	const getGlobalConfig = computed(() => {
+		if (globalConfig.value === null) {
+			const config = Cache.get('globalConfig')
+			globalConfig.value = config ? config : {}
+		}
+		return globalConfig.value
+	})
+
+	return {
+		lockData,
+		accessToken,
+		activityId,
+		globalConfig,
+		lock,
+		unlock,
+		setAccessToken,
+		delAccessToken,
+		setActivityId,
+		delActivityId,
+		setGlobalConfig,
+		isLock,
+		getAccessToken,
+		getActivityId,
+		isLogin,
+		getGlobalConfig
+	}
+})

+ 20 - 0
src/pages/third/yunexamine/store/module/examine.js

@@ -0,0 +1,20 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useExamineStore = defineStore('examine', () => {
+	const topGroupId = ref(-1)
+
+	function setTopGroupId(groupId) {
+		topGroupId.value = groupId
+	}
+
+	function delTopGroupId() {
+		topGroupId.value = -1
+	}
+
+	return {
+		topGroupId,
+		setTopGroupId,
+		delTopGroupId
+	}
+})

+ 32 - 0
src/pages/third/yunexamine/store/module/user.js

@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import Cache from '../../common/cache.js'
+
+export const useUserStore = defineStore('user', () => {
+	const uploadAvatarTip = ref(null)
+	const userInfo = ref({})
+
+	function setUserInfo(info) {
+		userInfo.value = info
+	}
+
+	function setUploadAvatarTip(yesOrNo) {
+		uploadAvatarTip.value = yesOrNo
+		Cache.put('uploadAvatarTip', yesOrNo)
+	}
+
+	const getUploadAvatarTip = computed(() => {
+		if (uploadAvatarTip.value === null) {
+			uploadAvatarTip.value = Cache.get('uploadAvatarTip')
+		}
+		return uploadAvatarTip.value
+	})
+
+	return {
+		uploadAvatarTip,
+		userInfo,
+		setUserInfo,
+		setUploadAvatarTip,
+		getUploadAvatarTip
+	}
+})

+ 109 - 0
src/pages/third/yunexamine/uni.scss

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

+ 6 - 1
src/pages/user/index.vue

@@ -36,7 +36,7 @@
         <GridItem title="我的关注" icon="https://xy.wenlvti.net/app_static/images/mine/IconLevel.png" touchable @click="requireLogin(() => navTo('/pages/home/village/follows'), '登录后查看我的关注哦')" />
         <GridItem title="我的订单" icon="https://xy.wenlvti.net/app_static/images/mine/IconArchive.png" touchable @click="requireLogin(() => navTo('/pages/home/village/orders'), '登录后查看我的订单哦')" />
         <GridItem title="任务中心" icon="https://xy.wenlvti.net/app_static/images/mine/IconTask.png" touchable @click="navTo('/pages/home/village/task/index')" />
-        <GridItem title="兑换商城" icon="https://xy.wenlvti.net/app_static/images/mine/IconShop.png" touchable @click="toast('暂未开放,敬请期待')" />
+        <GridItem title="兑换商城" icon="https://xy.wenlvti.net/app_static/images/mine/IconShop.png" touchable @click="goStore" />
       </Grid>
     </ProvideVar>
     <ProvideVar :vars="{
@@ -106,4 +106,9 @@ function doLogout() {
     }
   });
 }
+function goStore() {
+  requireLogin(() => navTo('/pages/third/yunexamine/pages/home/index'), '登录后查看我的关注哦');
+}
+
+
 </script>