DESKTOP-SVI9JE1\muzen 2 years atrás
commit
3577174504
60 changed files with 13739 additions and 0 deletions
  1. 32 0
      .eslintrc.js
  2. 23 0
      .gitignore
  3. 23 0
      .hbuilderx/launch.json
  4. 107 0
      App.vue
  5. 21 0
      LICENSE
  6. 1 0
      README.md
  7. 18 0
      api/chat.js
  8. 22 0
      common/common.scss
  9. 35 0
      common/config.js
  10. 7 0
      common/mixin.js
  11. 68 0
      components/bottom-drawer/bottom-drawer.vue
  12. 142 0
      components/prompt-list/prompt-list.vue
  13. 38 0
      main.js
  14. 151 0
      manifest.json
  15. 4406 0
      package-lock.json
  16. 11 0
      package.json
  17. 57 0
      pages.json
  18. 228 0
      pages/main/bottom-func.vue
  19. 70 0
      pages/main/history.vue
  20. 450 0
      pages/main/index.vue
  21. 107 0
      pages/main/jump-default-browser.vue
  22. 273 0
      pages/main/login.vue
  23. 223 0
      pages/main/prompt/prompt-list.vue
  24. 72 0
      pages/main/prompt/prompt.vue
  25. BIN
      static/favicon.ico
  26. BIN
      static/icon/prompt/cunchuyouhua.png
  27. BIN
      static/icon/prompt/drawingpalette.png
  28. BIN
      static/icon/prompt/duihua.png
  29. BIN
      static/icon/prompt/gongzuozhoubao.png
  30. BIN
      static/icon/prompt/linggan.png
  31. BIN
      static/icon/prompt/loveletter.png
  32. BIN
      static/icon/prompt/lunwen.png
  33. BIN
      static/icon/prompt/lvyou.png
  34. BIN
      static/icon/prompt/robot.png
  35. BIN
      static/icon/prompt/tree.png
  36. BIN
      static/icon/prompt/voice.png
  37. BIN
      static/icon/prompt/wenzhang.png
  38. BIN
      static/icon/prompt/write-ms.png
  39. BIN
      static/icon/prompt/xiaohongshubiji.png
  40. BIN
      static/icon/prompt/xingming.png
  41. BIN
      static/icon/prompt/yuedu.png
  42. BIN
      static/icon/prompt/yuyanfanyi.png
  43. BIN
      static/logo-100.png
  44. BIN
      static/robot.png
  45. 17 0
      store/index.js
  46. 22 0
      store/module/app.js
  47. 127 0
      store/module/user.js
  48. 184 0
      uni_modules/colorui/animation.css
  49. 70 0
      uni_modules/colorui/components/cu-custom.vue
  50. 1226 0
      uni_modules/colorui/icon.css
  51. 3912 0
      uni_modules/colorui/main.css
  52. 784 0
      uni_modules/t-color-picker/t-color-picker.vue
  53. 125 0
      util/login.js
  54. 85 0
      util/request.js
  55. 85 0
      util/sqma.js
  56. 121 0
      util/squ.js
  57. 222 0
      util/squni.js
  58. 90 0
      util/websocket.js
  59. 74 0
      util/wx-share.js
  60. 10 0
      vue.config.js

+ 32 - 0
.eslintrc.js

@@ -0,0 +1,32 @@
+module.exports = {
+    "env": {
+        "browser": true,
+        "es2021": true
+    },
+    "extends": [
+        "eslint:recommended",
+        "plugin:vue/vue3-essential"
+    ],
+    "overrides": [
+        {
+            "env": {
+                "node": true
+            },
+            "files": [
+                ".eslintrc.{js,cjs}"
+            ],
+            "parserOptions": {
+                "sourceType": "script"
+            }
+        }
+    ],
+    "parserOptions": {
+        "ecmaVersion": "latest",
+        "sourceType": "module"
+    },
+    "plugins": [
+        "vue"
+    ],
+    "rules": {
+    }
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+unpackage/
+dist/
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.project
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*

+ 23 - 0
.hbuilderx/launch.json

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

+ 107 - 0
App.vue

@@ -0,0 +1,107 @@
+<script>
+	import Vue from 'vue'
+	import {
+		updateMini
+	} from '@/util/sqma.js'
+	import {
+		isWechat
+	} from '@/util/squ.js'
+	import { login, LoginAlready, LoginSuccess, LoginUnBind, LoginRedirect } from '@/util/login.js'
+	import squni from '@/util/squni.js'
+	import config from '@/common/config.js'
+	export default {
+		async onLaunch(e) {
+			console.log('App onLaunch...')
+			this.vueStylePrototype()
+
+			// #ifdef MP-WEIXIN
+			this.$store.commit('setPlatform', 'WxMa')
+			// 微信小程序打开场景
+			this.$store.commit('setScene', e.scene)
+			// 微信小程序更新检查
+			updateMini();
+			// #endif
+			
+			// #ifdef H5
+			this.$store.commit('setPlatform', isWechat() ? 'WxMp' : 'H5')
+			// #endif
+		},
+		async onShow() {
+			console.log('App onShow...')
+			
+			// 如果写在onLaunch里面,用户重新进入小程序Token可能过期,需要做Token刷新逻辑
+			await this.checkReady()
+			
+			// 保证onLaunch执行完后,再执行页面级别的onShow/onLoad(两个需要分别进行await this.$ready)
+			this.$emitReady()
+		},
+		onHide() {
+			console.log('App onHide...')
+		},
+		methods: {
+			async checkReady() {
+				if (config.jumpDefaultBrowser && isWechat()) {
+					squni.routeWithParams('/pages/main/jump-default-browser')
+					return
+				}
+				
+				// 登录
+				await login().then(async res => {
+					if (res === LoginAlready || res === LoginSuccess) {
+						await this.$store.dispatch('GetUserInfo')
+					} else if (res === LoginUnBind) {
+						await this.$store.dispatch('GetUserInfo')
+					} else if (res === LoginRedirect) {
+						// do nothing...
+					} else {
+						squni.toast((res && res.data && res.data.message) || '登录失败')
+					}
+				})
+			},
+			vueStylePrototype() {
+				uni.getSystemInfo({
+					success: function(e) {
+						// ====== 顶部导航高度 ======//
+						// #ifndef MP
+						Vue.prototype.StatusBar = e.statusBarHeight;
+						if (e.platform == 'android') {
+							Vue.prototype.CustomBar = e.statusBarHeight + 50;
+						} else {
+							Vue.prototype.CustomBar = e.statusBarHeight + 45;
+						};
+						// #endif
+
+						// #ifdef MP-WEIXIN
+						Vue.prototype.StatusBar = e.statusBarHeight;
+						let custom = wx.getMenuButtonBoundingClientRect();
+						Vue.prototype.Custom = custom;
+						Vue.prototype.CustomBar = custom.bottom + custom.top - e.statusBarHeight;
+						// #endif		
+
+						// #ifdef MP-ALIPAY
+						Vue.prototype.StatusBar = e.statusBarHeight;
+						Vue.prototype.CustomBar = e.statusBarHeight + e.titleBarHeight;
+						// #endif
+
+						// ====== 可用窗口高度 =====//
+						// #ifdef MP-WEIXIN
+						Vue.prototype.AvailableHeight = e.windowHeight + e.statusBarHeight;
+						// #endif
+						// #ifdef H5
+						Vue.prototype.AvailableHeight = e.windowHeight + e.statusBarHeight - Vue.prototype
+							.CustomBar;
+						// #endif
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*每个页面公共css */
+	@import "@/uni_modules/colorui/main.css";
+	@import "@/uni_modules/colorui/icon.css";
+	// @import "@/uni_modules/uview-ui/index.scss";
+	@import "common/common.scss";
+</style>

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 www.aezo.cn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 0
README.md

@@ -0,0 +1 @@
+## aezo-chat-gpt-m

+ 18 - 0
api/chat.js

@@ -0,0 +1,18 @@
+import { get, post } from '@/util/request.js'
+
+// AI聊天(弃用)
+export const sendMsgApi = (params, config = {}) => post('/tools/chat/sendMsg', params, config)
+
+// 查询用户余额
+export const getUserChatAssetApi = (params, config = {}) => post('/tools/chat/getUserChatAsset', params, config)
+
+// 开启新会话
+export const startNewChatApi = (params, config = {}) => post('/tools/chat/startNewChat', params, config)
+
+// 查询提示语
+export const findPromptListApi = (params, config = {}) => post('/tools/chat/findPromptList', params, config)
+
+// 激励视频奖励
+export const rewardedVideoAdAssetApi = (params, config = {}) => post('/tools/chat/rewardedVideoAdAsset', params, config)
+
+

+ 22 - 0
common/common.scss

@@ -0,0 +1,22 @@
+/* ==================
+        扩展ColorUI
+ ==================== */
+.cu-load.info::before {
+	content: "\e6e5";
+}
+.cu-load.info::after {
+	content: "上滑加载更多";
+}
+
+/* ==================
+        覆盖UniApp
+ ==================== */
+/* 防止 uni.showToast 被遮盖 */
+uni-toast{
+	z-index: 999999;
+}
+
+/* ==================
+        覆盖ColorUI
+ ==================== */
+

+ 35 - 0
common/config.js

@@ -0,0 +1,35 @@
+const env = process.env
+const dev = process.env.NODE_ENV === 'development'
+// 改成自己的IP  hhhhhh
+const baseUrl = dev ? 'https://chatapi.radio1964.com/api' : 'https://chatapi.radio1964.com/api'
+const wssUrl = dev ? 'ws://chatapi.radio1964.com/api' : 'wss://chatapi.radio1964.com/api'
+
+console.log(`\n %c API URL %c ${baseUrl} \n\n`, 'color: #ffffff; background: #f37b1d; padding:5px 0;', 'color: #f37b1d;background: #ffffff; padding:5px 0;');
+
+const config = {
+	env,
+	baseUrl,
+	wssUrl,
+	// 当前微信小程序ID wxf1e0c425e7cc7c81 抽屉:wx5f9e2e22f03c0d5d
+	appIdWxMa: 'wx5f9e2e22f03c0d5d',
+	// 微信小程序激励视频广告位代码
+	wxRewardedVideoAd: 'adunit-c058410d707e27ee',
+	// 通过微信浏览器访问H5时对应的公众号AppId(通过微信打开连接,关联公众号获取用户信息,留空则不走微信公众号登录)
+	appIdWxMp: '', // wxf24768d263e9fe9b
+	// 唯一项目代码,如用于本地Storage前缀
+	key: 'aezo-chat-gpt',
+	// Token的名字
+	Authorization: 'SQ-ACCESS-TOKEN',
+	// 如果是微信浏览器打开,是否强制显示引导页(引导用默认浏览器打开, 防封域名)
+	jumpDefaultBrowser: true,
+	// 主页类型: Tab(通过switchTab跳转)/Page(通过redirectTo跳转)
+	indexType: 'Page',
+	// 主页路径. 必须以/开头
+	indexPath: '/pages/main/index',
+	// H5登录页面
+	loginPath: '/pages/main/login',
+	// 验证码发送方式: Email|SMS(短信)
+	identifyingSendMethod: 'SMS',
+}
+
+export default config

+ 7 - 0
common/mixin.js

@@ -0,0 +1,7 @@
+export default {
+	data() {
+		return {
+
+		}
+	}
+}

+ 68 - 0
components/bottom-drawer/bottom-drawer.vue

@@ -0,0 +1,68 @@
+<template>
+	<view class="cu-modal bottom-modal" :class="show ? 'show' : ''" @click="close">
+		<view class="cu-dialog" @click.stop="() => {}">
+			<view class="cu-bar bg-white justify-end">
+				<view class="content">{{ title }}</view>
+				<view class="action" @click="close">
+					<text class="cuIcon-close"></text>
+				</view>
+			</view>
+			<view class="padding-sm content">
+				<view class="padding-lr-xs">
+					<slot></slot>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			value: {
+				type: Boolean,
+				default: false
+			},
+			title: {
+				type: String,
+				default: '菜单'
+			}
+		},
+		data() {
+			return {
+				show: this.value
+			};
+		},
+		watch: {
+			value (n, o) {
+				this.show = this.value
+			}
+		},
+		methods: {
+			open() {
+				this.show = true;
+				this.$emit('input', this.show)
+			},
+			close() {
+				this.show = false;
+				this.$emit('input', this.show)
+			}
+		}
+	};
+</script>
+
+<style>
+	.cu-dialog {
+		border-radius: 20upx 20upx 0 0;
+		padding-bottom: 40upx;
+		background: #fff;
+	}
+	.content {
+		background-color: #fff;
+		padding-top: 16upx;
+	}
+	.cu-bar .content {
+		color: #333333;
+		font-weight: 700;
+	}
+</style>

+ 142 - 0
components/prompt-list/prompt-list.vue

@@ -0,0 +1,142 @@
+<template>
+	<view>
+		<view class="cu-list menu-avatar margin-bottom">
+			<view v-for="item in promptList" :key="item.title" class="cu-item">
+				<text v-show="$store.getters.favorPromptList.indexOf(item.id) >= 0" class="cuIcon-favorfill text-orange"></text>
+				<image class="cu-avatar radius lg" :src="`/static/icon/prompt/${item.icon}`">
+				<view class="content" @tap="runPrompt(item)">
+					<view class="text-shadow text-black">{{ item.title }}</view>
+					<view class="text-gray text-sm flex">
+						<text class="desc">{{ item.remark }}</text>
+					</view>
+				</view>
+				<view class="action">
+					<view class="text-grey">{{ renderWeight(item) }}</view>
+					<button class="cu-btn round sm line-cyan flex" @tap="showDetail(item)">
+						详情
+					</button>
+				</view>
+			</view>
+		</view>
+		
+		<view class="cu-modal bottom-modal" :class="showDetailModal ? 'show' : ''" @tap="showDetailModal = false">
+			<view class="cu-dialog bg-white" @tap.stop="" style="height: 60%;overflow: auto;">
+				<view class="cu-bar bg-white justify-end">
+					<view class="action" @tap="toggleFavor(curItem)">
+						<text :class="[$store.getters.favorPromptList.indexOf(curItem.id) >= 0 ? 'cuIcon-favorfill' : 'cuIcon-favor', 'text-orange']"></text>
+					</view>
+					<view class="content">提示词详细</view>
+					<view class="action text-cyan" @tap="runPrompt(curItem)">进入对话</view>
+					<view class="action" @tap="showDetailModal = false">
+						<text class="cuIcon-close"></text>
+					</view>
+				</view>
+				<view class="padding-bottom-lg">
+					<view class="cu-bar bg-white solid-bottom">
+						<view class="action">
+							<text class="cuIcon-titles text-cyan"></text> 提示词
+						</view>
+					</view>
+					<view class="text-left padding-lr-sm padding-top-sm bg-white flex justify-between">
+						<view>{{ curItem.title }}</view>
+						<view>{{ renderWeight(curItem) }}</view>
+					</view>
+					<view class="cu-bar bg-white solid-bottom">
+						<view class="action">
+							<text class="cuIcon-titles text-cyan"></text> 说明
+						</view>
+					</view>
+					<view class="text-left padding-lr-sm padding-top-sm bg-white">
+						{{ curItem.remark }}
+					</view>
+					<view class="cu-bar bg-white solid-bottom">
+						<view class="action">
+							<text class="cuIcon-titles text-cyan"></text> 中文提示
+						</view>
+					</view>
+					<view class="text-left padding-lr-sm padding-top-sm bg-white">
+						{{ curItem.desc_cn }}
+					</view>
+					<view class="cu-bar bg-white solid-bottom">
+						<view class="action">
+							<text class="cuIcon-titles text-cyan"></text> 英文提示
+						</view>
+					</view>
+					<view class="text-left padding-lr-sm padding-top-sm bg-white">
+						{{ curItem.desc_en }}
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'PromptList',
+		props: {
+			promptList: {
+				type: Array,
+				default: () => []
+			}
+		},
+		data() {
+			return {
+				curItem: {},
+				showDetailModal: false
+			}
+		},
+		methods: {
+			showDetail(item) {
+				this.curItem = item
+				this.showDetailModal = true
+			},
+			runPrompt(item) {
+				this.$squni.navigateTo('/pages/main/index?promptId=' + item.id)
+			},
+			toggleFavor(item) {
+				let favorPromptList = this.$store.getters.favorPromptList
+				const index = favorPromptList.indexOf(item.id)
+				if(index >= 0) {
+					favorPromptList.splice(index, 1)
+				} else {
+					favorPromptList.push(item.id)
+				}
+				this.$store.commit('setFavorPromptList', favorPromptList)
+				this.$emit('toggle-favor')
+			},
+			renderWeight(item) {
+				return item.weight > 10000 ? ('🚀 ' + (item.weight / 10000).toFixed(1) + 'w') : (item.weight > 1000 ? ('🔥 ' + (item.weight / 1000).toFixed(1) + 'k') : '🧊 ' + item.weight)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.cu-list.menu-avatar>.cu-item {
+		.content {
+			width: calc(100% - 96upx - 60upx - 110upx - 20upx);
+		}
+		.action {
+			width: 110upx;
+		}
+	}
+	
+	.cu-list .cu-item {
+		.cuIcon-favorfill {
+			position: absolute;
+			bottom: 2upx;
+			left: 12upx;
+		}
+		.cu-avatar {
+			background-color: transparent;
+		}
+		.content .desc {
+			overflow : hidden;
+			text-overflow: ellipsis;
+			display: -webkit-box;
+			-webkit-line-clamp: 2;
+			-webkit-box-orient: vertical;
+		}
+	}
+</style>

+ 38 - 0
main.js

@@ -0,0 +1,38 @@
+import Vue from 'vue'
+import App from './App'
+import store from './store'
+import config from './common/config.js'
+import squni from './util/squni.js'
+import mixin from './common/mixin'
+// #ifdef MP-WEIXIN
+import wxShare from './util/wx-share.js'
+// #endif
+
+// ColorUI返回组件
+import cuCustom from '@/uni_modules/colorui/components/cu-custom.vue'
+
+Vue.config.productionTip = false
+Vue.prototype.$store = store
+Vue.prototype.$config = config
+Vue.prototype.$squni = squni
+
+// 为了保证onLaunch执行完后,再执行页面级别的onShow/onLoad
+Vue.prototype.$ready = new Promise(resolve => {
+  Vue.prototype.$emitReady = resolve
+})
+
+Vue.component('cu-custom', cuCustom)
+Vue.mixin(mixin)
+// #ifdef MP-WEIXIN
+Vue.mixin(wxShare)
+// #endif
+
+App.mpType = 'app'
+
+const app = new Vue({
+    store,
+    ...App
+})
+squni.setApp(app)
+
+app.$mount()

+ 151 - 0
manifest.json

@@ -0,0 +1,151 @@
+{
+    "name" : "AirSmartChat聊天",
+    "appid" : "__UNI__CB0880C",
+    "description" : "多平台快速开发的UI框架",
+    "versionName" : "1.1.0",
+    "versionCode" : 1,
+    "transformPx" : false,
+    "app-plus" : {
+        "optimization" : {
+            "subPackages" : true
+        },
+        "safearea" : {
+            "bottom" : {
+                "offset" : "none"
+            }
+        },
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "modules" : {
+            "Webview-x5" : {}
+        },
+        "distribute" : {
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ],
+                "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
+            },
+            "ios" : {
+                "idfa" : false
+            },
+            "sdkConfigs" : {
+                "ad" : {}
+            },
+            "icons" : {
+                "android" : {
+                    "hdpi" : "unpackage/res/icons/72x72.png",
+                    "xhdpi" : "unpackage/res/icons/96x96.png",
+                    "xxhdpi" : "unpackage/res/icons/144x144.png",
+                    "xxxhdpi" : "unpackage/res/icons/192x192.png"
+                },
+                "ios" : {
+                    "appstore" : "unpackage/res/icons/1024x1024.png",
+                    "ipad" : {
+                        "app" : "unpackage/res/icons/76x76.png",
+                        "app@2x" : "unpackage/res/icons/152x152.png",
+                        "notification" : "unpackage/res/icons/20x20.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "proapp@2x" : "unpackage/res/icons/167x167.png",
+                        "settings" : "unpackage/res/icons/29x29.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "spotlight" : "unpackage/res/icons/40x40.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png"
+                    },
+                    "iphone" : {
+                        "app@2x" : "unpackage/res/icons/120x120.png",
+                        "app@3x" : "unpackage/res/icons/180x180.png",
+                        "notification@2x" : "unpackage/res/icons/40x40.png",
+                        "notification@3x" : "unpackage/res/icons/60x60.png",
+                        "settings@2x" : "unpackage/res/icons/58x58.png",
+                        "settings@3x" : "unpackage/res/icons/87x87.png",
+                        "spotlight@2x" : "unpackage/res/icons/80x80.png",
+                        "spotlight@3x" : "unpackage/res/icons/120x120.png"
+                    }
+                }
+            }
+        }
+    },
+    "quickapp" : {},
+    "mp-weixin" : {
+        "appid" : "wxf1e0c425e7cc7c81",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : false,
+            "minified" : false,
+            "postcss" : false
+        },
+        "optimization" : {
+            "subPackages" : true
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true,
+        "component2" : true
+    },
+    "mp-qq" : {
+        "optimization" : {
+            "subPackages" : true
+        },
+        "appid" : "15646153"
+    },
+    "mp-baidu" : {
+        "usingComponents" : true,
+        "appid" : ""
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true,
+        "appid" : ""
+    },
+    "h5" : {
+        "template" : "",
+        "router" : {
+            "mode" : "history",
+            "base" : "./"
+        },
+        "optimization" : {
+            "treeShaking" : {
+                "enable" : false
+            }
+        },
+        "title" : "AirSmartChat聊天",
+        "sdkConfigs" : {
+            "maps" : {
+                "qqmap" : {
+                    "key" : ""
+                }
+            }
+        },
+        "domain" : ""
+    },
+    "vueVersion" : "2"
+}

File diff suppressed because it is too large
+ 4406 - 0
package-lock.json


+ 11 - 0
package.json

@@ -0,0 +1,11 @@
+{
+	"id": "aezo-chat-gpt-m",
+	"scripts": {
+		"test": "eslint . --fix"
+	},
+	"devDependencies": {
+		"eslint": "^8.2.0",
+		"eslint-config-airbnb": "^19.0.0",
+		"eslint-plugin-vue": "^9.15.1"
+	}
+}

+ 57 - 0
pages.json

@@ -0,0 +1,57 @@
+{
+	// "condition": { //模式配置,仅开发期间生效
+	// 	"current": 0, //当前激活的模式(list 的索引项)
+	// 	"list": [{
+	// 		"name": "test", //模式名称
+	// 		"path": "pages/packA/test/test", //启动页面,必选
+	// 		"query": "" //启动参数,在页面的onLoad函数里面得到
+	// 	}]
+	// },
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/main/index",
+			"style": {
+				"navigationBarTitleText": "AirSmartChat聊天"
+			}
+		}
+		,{
+			"path": "pages/main/history",
+			"style": {
+				"navigationBarTitleText": "聊天历史"
+			}
+		}
+        ,{
+            "path" : "pages/main/login",
+            "style": {
+            	"navigationBarTitleText": "AirSmartChat聊天 | 登录注册"
+            }
+        }
+        ,{
+            "path" : "pages/main/jump-default-browser",
+            "style": {
+            	"navigationBarTitleText": "请在默认浏览器中打开"
+            }
+        }
+        ,{
+            "path" : "pages/main/prompt/prompt",
+            "style" :
+            {
+                "navigationBarTitleText": "提示词"
+            }
+        }
+		,{
+            "path" : "pages/main/prompt/prompt-list",
+            "style" :
+            {
+                "navigationBarTitleText": "提示词分类",
+				"enablePullDownRefresh": true
+            }
+        }
+    ],
+	"globalStyle": {
+		"navigationBarBackgroundColor": "#0081ff",
+		"navigationBarTitleText": "AirSmartChat聊天",
+		"navigationBarTextStyle": "white",
+		"navigationStyle": "custom"
+	}
+}

+ 228 - 0
pages/main/bottom-func.vue

@@ -0,0 +1,228 @@
+<template>
+	<view>
+		<view class="cu-modal bottom-modal" :class="show ? 'show' : ''" @click="close">
+			<view class="cu-dialog" @click.stop="() => {}">
+				<view class="cu-bar bg-white justify-end">
+					<view class="content">我的</view>
+					<view class="action" @click="close">
+						<text class="cuIcon-close"></text>
+					</view>
+				</view>
+				<view class="padding-sm content">
+					<view class="padding-lr-xs">
+						<view class="flex align-start">
+							<view class="cu-avatar lg">VIP</view>
+							<view class="flex flex-direction align-start margin-xs margin-left">
+								<view>普通用户</view>
+								<view @click="usernameSimple && $squni.copy(usernameSimple)">ID:
+									<text class="text-bold">{{ usernameSimple || '尚未登录' }}</text>
+									<text class="cuIcon-copy text-cyan margin-left-xs"></text>
+								</view>
+							</view>
+						</view>
+						<view class="flex align-start margin-top">
+<!--							<view class="cu-capsule">-->
+<!--								<view class='cu-tag radius bg-cyan'>拥有额度</view>-->
+<!--								<view class="cu-tag line-cyan">{{ chatAsset.n || 0 }}次</view>-->
+<!--							</view>-->
+							<view class="cu-capsule">
+								<view class='cu-tag radius bg-cyan'>每日免费剩余额度</view>
+								<view class="cu-tag line-cyan">{{ chatAsset.dfn || 0 }}次</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import BottomDrawer from '@/components/bottom-drawer/bottom-drawer.vue'
+	import { mapGetters } from 'vuex'
+	import {
+		rewardedVideoAdAssetApi
+	} from '@/api/chat.js'
+	let rewardedVideoAd = null
+	export default {
+		components: { BottomDrawer },
+		props: {
+			chatAsset: {
+				type: Object,
+				default: () => {}
+			}
+		},
+		data() {
+			return {
+				show: true,
+				showVip: false,
+				showNum: false,
+				showCustomer: false,
+				cuIconList: [{
+					cuIcon: 'vip',
+					color: 'orange',
+					name: '开通会员',
+					callback: () => {
+						this.showVip = true
+					}
+				}, {
+					cuIcon: 'baby',
+					color: 'blue',
+					name: '次数包',
+					callback: () => {
+						this.showNum = true
+					}
+				}, {
+					cuIcon: 'service',
+					color: 'cyan',
+					name: '客服领次数',
+					type: 'button',
+					callback: () => {
+						// #ifndef MP-WEIXIN
+						this.showCustomer = true
+						// #endif
+					}
+				}, {
+					cuIcon: 'flashlightclose',
+					color: 'red',
+					name: '清除记忆',
+					callback: () => {
+						this.$squni.setStorageSync('chatHistory', [])
+						this.$squni.toast('清除记忆成功', 'success')
+					}
+				}],
+				rewardedCount: 0
+			};
+		},
+		computed: {
+			...mapGetters([
+				'usernameSimple'
+			]),
+			showFuncList () {
+				return this.showVip || this.showNum || this.showCustomer
+			}
+		},
+		watch: {
+			showFuncList (n, o) {
+				if (n !== o) {
+					this.show = !n
+				}
+			}
+		},
+		created() {
+			// #ifdef MP-WEIXIN
+			if (wx.createRewardedVideoAd) {
+				rewardedVideoAd = wx.createRewardedVideoAd({
+					adUnitId: this.$config.wxRewardedVideoAd
+				})
+				rewardedVideoAd.onLoad(() => {
+					// 激励视频广告加载成功。用户观看完会重新加载新的广告并重新进入此方法
+					console.log('onLoad event emit')
+				})
+				rewardedVideoAd.onClose(res => {
+				    // 用户点击了【关闭广告】按钮
+				    if (res && res.isEnded) {
+				      // 正常播放结束,可以下发游戏奖励
+					  rewardedVideoAdAssetApi().then(res => {
+						  this.rewardedCount = res.data.rewardedCount
+						  if(res.status === 'success') {
+							  this.$squni.toast(`已增加次数${this.rewardedCount >= 5 ? '(今日次数已用完)' : ''}`, 'success')
+							  this.$emit('rewarded-video-ad')
+						  }
+					  })
+				    } else {
+				      // 播放中途退出,不下发游戏奖励;退出前ad组件会自动进行confirm
+				    }
+				})
+				rewardedVideoAd.onError((err) => {
+					console.log('onError event emit', err)
+				})
+			}
+			// #endif
+		},
+		methods: {
+			open() {
+				this.show = true;
+			},
+			close() {
+				this.show = false;
+			},
+			navigateTo(item) {
+				if (item.callback) {
+					item.callback(item)
+				} else {
+					uni.navigateTo({
+						url: item.page
+					})
+				}
+			},
+			oepnRewardedVideoAd () {
+				// #ifdef MP-WEIXIN
+				if (rewardedVideoAd) {
+					if(this.rewardedCount >= 5) {
+						this.$squni.toast('今日次数已用完,明天再来吧~')
+						return
+					}
+					rewardedVideoAd.show().catch(() => {
+						// 失败重试
+						rewardedVideoAd.load()
+							.then(() => rewardedVideoAd.show())
+							.catch(err => {
+								// https://developers.weixin.qq.com/miniprogram/dev/api/ad/RewardedVideoAd.onError.html
+								console.log('激励视频广告显示失败', err)
+								this.$squni.toast('广告加载失败了,请稍后再试~')
+							})
+					})
+				}
+				// #endif
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	.cu-dialog {
+		border-radius: 20upx 20upx 0 0;
+	}
+	.content {
+		background-color: #fff;
+		padding-top: 16upx;
+	}
+	.cu-bar .content {
+		color: #333333;
+		font-weight: 700;
+	}
+	.cu-list {
+		margin-bottom: 20upx;
+	}
+	.cu-list .func-list {
+		margin: 0upx 20upx -20upx 0;
+	}
+	.cu-list .func-list .func-icon {
+		border: 1px solid #eee;
+		border-radius: 8upx;
+		padding: 30upx;
+		font-size: 80upx;
+	}
+	.cu-list.grid>.cu-item.item-button {
+		padding: 0;
+		.cu-btn {
+			display: inline-block;
+			background: transparent;
+			height: 164upx;
+			margin-top: 0;
+			padding-right: 20upx;
+			border-radius: 0;
+			.button-icon {
+				border: 1px solid #eee;
+				padding: 30upx 0upx 36upx 0;
+				margin-top: 28upx;
+			}
+			.cuIcon-service:before {
+				/* #ifdef H5 */
+				font-size: 84upx;
+				/* #endif */
+			}
+		}
+	}
+</style>

+ 70 - 0
pages/main/history.vue

@@ -0,0 +1,70 @@
+<template>
+	<view>
+		<cu-custom bgColor="bg-cyan" :isBack="true">
+			<block slot="backText">返回</block>
+			<block slot="content">聊天历史</block>
+		</cu-custom>
+		<view class="cu-chat">
+			<block v-for="(x,i) in msgList" :key="i">
+				<!-- 用户消息 -->
+				<view v-if="x.my && x.type === 'msg'" class="cu-item self" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
+					<view class="main">
+						<view class="content bg-cyan shadow" @click="x.msg && $squni.copy(x.msg)">
+							<text>{{ x.msg }}</text>
+						</view>
+					</view>
+					<image class="cu-avatar round" src="/static/logo-100.png">
+					<view v-if="i === 0" class="date">{{ x.date }}</view>
+				</view>
+				<!-- AI消息 -->
+				<view v-if="!x.my && x.type === 'msg'" class="cu-item" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
+					<image class="cu-avatar round chat-avatar" src="/static/robot.png">
+					<view class="main">
+						<view class="content shadow" @click="x.msg && $squni.copy(x.msg)">
+							<text>{{ x.msg }}</text>
+						</view>
+					</view>
+					<view v-if="i === 0" class="date">{{ x.date }}</view>
+				</view>
+				<view v-if="x.type === 'error'" class="cu-info">
+					<text class="cuIcon-roundclosefill text-red "></text> {{ x.msg }}
+				</view>
+			</block>
+			<view v-if="msgList.length === 0" class="text-center gray margin-top">无历史聊天记录</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				msgList: []
+			}
+		},
+		onShow() {
+			this.msgList = this.$squni.getStorageSync('chatHistory') || []
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style>
+	page {
+		padding-bottom: 200upx;
+	}
+	.cu-item:not(.first) {
+		padding-bottom: 0upx;
+	}
+	.cu-item.sec {
+		padding-top: 0upx;
+	}
+	.main .content {
+		word-wrap: break-word;
+	}
+	.cu-chat .cu-item>.main {
+		max-width: calc(100% - 160upx);
+	}
+</style>

+ 450 - 0
pages/main/index.vue

@@ -0,0 +1,450 @@
+<template>
+	<view>
+		<cu-custom bgColor="bg-cyan" :isBack="$squni.getPrePage() != null">
+			<block slot="backText">返回</block>
+			<block slot="content">AirSmartChat</block>
+		</cu-custom>
+		<view class="cu-chat">
+			<block v-for="(x,i) in msgList" :key="i">
+				<!-- 用户消息 -->
+				<view v-if="x.my && x.type === 'msg'" class="cu-item self" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
+					<view class="main">
+						<view class="content bg-cyan shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
+							<text>{{ x.msg }}</text>
+						</view>
+					</view>
+					<image class="cu-avatar round" src="/static/logo-100.png">
+					<view v-if="i === 0" class="date">{{ x.date }}</view>
+				</view>
+				<!-- AI消息 -->
+				<view v-if="!x.my && x.type === 'msg'" class="cu-item" :class="[i === 0 ? 'first' : '', i === 1 ? 'sec' : '']">
+					<view class="flex flex-direction align-center">
+						<image class="cu-avatar round chat-avatar" src="/static/robot.png">
+						<text v-if="i === 0" class="cuIcon-title" :class="[statusColor]"></text>
+					</view>
+					<view class="main">
+						<view class="content shadow"><!-- @click="x.msg && $squni.copy(x.msg)" -->
+							<text>{{ x.msg }}</text>
+						</view>
+					</view>
+					<view v-if="i === 0" class="date">{{ x.date }}</view>
+				</view>
+				<view v-if="x.type === 'error'" class="cu-info">
+					<text class="cuIcon-roundclosefill text-red "></text> {{ x.msg }}
+				</view>
+			</block>
+			
+			<view v-if="msgLoading" class="cu-item">
+				<view class="flex flex-direction align-center">
+					<image class="cu-avatar round chat-avatar" src="/static/robot.png">
+				</view>
+				<view class="main">
+					<text class="cuIcon-loading2 cuIconfont-spin text-cyan"></text>
+				</view>
+			</view>
+		</view>
+
+		<view class="cu-bar foot input" :style="[{bottom:inputBottom+'px'}]">
+			<view class="tip-box">
+				<button class="cu-btn round shadow sm cuIcon line-cyan" @click="startNewChat">
+					<text class="cuIcon-paint"></text>
+				</button>
+				<button class="cu-btn round shadow sm line-cyan" @click="$squni.navigateTo('/pages/main/history')">历史消息</button>
+				<button class="cu-btn round shadow sm bg-orange" @click="$squni.navigateTo('/pages/main/prompt/prompt')">提示词</button>
+				<button class="cu-btn round shadow sm line-cyan" @click="openBottomFunc">我的信息</button>
+			</view>
+			<!-- <view class="action func" @click="openBottomFunc">
+				<text class="cuIcon-list text-cyan" style="font-size: 60upx;"></text>
+			</view> -->
+			<input v-model="msg" class="solid padding-lr" :adjust-position="false" :focus="false" maxlength="5000"
+				cursor-spacing="10" :placeholder="loading ? '小AirSmart正在思考中,请稍后~' : '你可以问我任何问题'"
+				@focus="inputFocus" @blur="inputBlur" @confirm="sendMsg"></input>
+			<!-- <view class="action">
+				<text class="cuIcon-emojifill text-grey"></text>
+			</view> -->
+			<button class="cu-btn bg-cyan shadow" :disabled="loading" @click="sendMsg">
+				<text class="cuIcon-loading2 cuIconfont-spin" v-if="loading || !ready"></text>{{ !ready ? '连接中' : '发送' }}
+			</button>
+		</view>
+		
+		<bottom-func v-if="bottomFuncShow" ref="bottomFunc" :chatAsset="chatAsset" @rewarded-video-ad="() => chatAsset.dfn += 2"></bottom-func>
+	</view>
+</template>
+
+<script>
+	import {
+		dateFormat, interval
+	} from '@/util/squ.js'
+	import {
+		scrollToBottom
+	} from '@/util/squni.js'
+	import websocket from '@/util/websocket'
+	import {
+		sendMsgApi, getUserChatAssetApi, startNewChatApi, findPromptListApi
+	} from '@/api/chat.js'
+	import BottomFunc from './bottom-func'
+	const HELLO_MSG = {
+		type: 'msg',
+		my: false,
+		msg: '连接中,请稍后~',
+		date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
+	}
+	export default {
+		components: { BottomFunc },
+		data() {
+			return {
+				loading: false,
+				userId: null,
+				msgList: [HELLO_MSG],
+				msgContent: "",
+				msg: "",
+				inputBottom: 0,
+				bottomFuncShow: false,
+				chatAsset: {},
+				assetType: 'n',
+				statusColor: 'text-red',
+				statusTimer: null,
+				msgLoading: false,
+				promptId: null,
+				curPrompt: {}
+			}
+		},
+		computed: {
+			ready () {
+				return this.statusColor === 'text-green'
+			}
+		},
+		watch: {
+			loading(n, o) {
+				if (n !== o && !n) {
+					let last = this.msgList[this.msgList.length - 1]
+					if (!last.my) {
+						this.addHistory(last)
+					}
+				}
+			},
+			statusColor(n, o) {
+				if (n === 'text-green') {
+					let prompt = ''
+					if (this.curPrompt && this.curPrompt.title) {
+						prompt = `(提示词:${this.curPrompt.title})`
+					}
+					HELLO_MSG.msg = '你好,我是AirSmartChat机器人,可以帮你解答疑惑或提供灵感' + prompt
+				} else {
+					HELLO_MSG.msg = '连接中,请稍后~'
+				}
+			}
+		},
+		async onShow() {
+			await this.$ready
+			
+			this.userId = this.$store.getters.userId
+			if(!this.userId) {
+				this.$squni.toast('请先进行登录哦')
+				return
+			}
+			
+			this.promptId = this.$squni.getCurQuery('promptId')
+			if(this.promptId) {
+				findPromptListApi({ id: this.promptId }).then(res => {
+					if (res.status === 'success') {
+						this.curPrompt = res.data
+						
+						if(this.ready && HELLO_MSG.msg.indexOf('提示词') < 0) {
+							let prompt = ''
+							if (this.curPrompt && this.curPrompt.title) {
+								prompt = `(提示词:${this.curPrompt.title})`
+							}
+							HELLO_MSG.msg += prompt
+						}
+					}
+				})
+			}
+			
+			getUserChatAssetApi().then(res => {
+				if (res.status === 'success') {
+					this.chatAsset = res.data
+				}
+			})
+			
+			this.heartStatus()
+			try {
+				//建立socket连接
+				websocket.connectSocket(this.$config.wssUrl + '/tools/chat/user/' + this.userId, msg => {
+					this.recvMsg(msg)
+				}, () => {
+					//如果连接成功则发送心跳检测
+					this.heartBeatTest()
+				})
+			} catch (error) {
+				console.log('websocket connectSocket error:' + error)
+			}
+		},
+		onHide() {
+			this.closeSocket()
+		},
+		methods: {
+			logout() {
+				this.loginLoading = true
+				doLoginApi('H5', {
+					username: this.email || this.phone|| this.username,
+					password: this.password
+				}).then(res => {
+					this.loginLoading = false
+					if (res === LoginSuccess) {
+						uni.showToast({ title: '登录成功' })
+						// 跳转到主页发现已经登录,会自动重新获取用户信息,此处无需获取
+						window.location.href = getUrlQuery('_originHref')
+					} else {
+						uni.showToast({ title: (res && res.data && res.data.message) || '登录失败', icon: 'none' })
+						this.createCode(4)
+					}
+				})
+			},
+			sendMsg() {
+				if (this.msg == "") {
+					this.$squni.toast('请先输入您的问题哦')
+					return
+				}
+				let msg = this.msg
+				this.putMsg(this.msg, true)
+				this.msgLoading = true
+				this.loading = true
+				
+				// ======== 开发环境模拟回复 ========
+				//return this.mockReply()
+				// ======== 开发环境模拟回复 ========
+				
+				if(this.calcAsset() === false) {
+					this.loading = false
+					return
+				}
+				
+				// 发送消息: M1/M2
+				websocket.sendMessage(JSON.stringify({
+					model: 'M1',
+					msg: msg,
+					platform: this.$squni.getStorageSync('platform'),
+					openid: this.$squni.getStorageSync('openid'),
+					promptId: this.promptId
+				}), null, () => {
+					this.putMsgError('机器人被拔网线了,请稍后再试~')
+				})
+			},
+			recvMsg(msg) {
+				this.msgLoading = false
+				if(!msg) {
+					this.putMsgError('机器人开小差了,请稍后再试~')
+					return
+				}
+				// 发送消息
+				// 1+1
+				// 收到消息
+				// {"role":"assistant","content":null}
+				// {"role":null,"content":"2"}
+				// {"role":null,"content":null}
+				// [DONE]
+				if (msg === '[DONE]') {
+					this.loading = false
+				} else {
+					try {
+						let msgJson = JSON.parse(msg)
+						if (msgJson.role === 'sqchat') {
+							let content = msgJson.content
+							if (msgJson.codeKey) {
+								content += `[${msgJson.codeKey}]`
+								if(msgJson.codeKey === 'chat.asset_short') {
+									this.openBottomFunc()
+								} else if(msgJson.codeKey.indexOf('chat.asset_') >= 0) {
+									this.chatAsset[this.assetType]++
+								}
+							}
+							this.putMsgError(content)
+						} if (msgJson.role === 'assistant') {
+							this.putMsg('', false)
+						} else if (msgJson.role == null && msgJson.content) {
+							this.msgList[this.msgList.length - 1].msg += msgJson.content
+							scrollToBottom()
+						}
+					} catch(error) {
+						this.putMsgError(msg)
+					}
+				}
+			},
+			startNewChat() {
+				HELLO_MSG.date = dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
+				this.msgList = [HELLO_MSG]
+				startNewChatApi()
+			},
+			sendMsgBak() {
+				let that = this
+				if (this.msg == "") {
+					return
+				}
+				this.msgContent += (this.userId + ":" + this.msg + "\n")
+				this.putMsg(this.msg, true)
+				this.loading = true
+
+				// ======== 开发环境模拟回复 ========
+				return this.mockReply()
+				// ======== 开发环境模拟回复 ========
+
+				sendMsgApi({
+					userId: this.userId + '',
+					question: this.msgContent
+				}).then(({
+					status, data
+				}) => {
+					if (status === 'success') {
+						this.putMsg(data.ack, false)
+						this.msgContent += ("openai:" + this.msg + "\n")
+					} else {
+						this.putMsg(res.message || '机器人开小差了,请稍后再试~', false, 'error')
+					}
+					that.loading = false
+				})
+			},
+			calcAsset() {
+				if (this.chatAsset.dfn > 0) {
+					this.chatAsset.dfn--
+					this.assetType = 'dfn'
+				} else if (this.chatAsset.n > 0) {
+					this.chatAsset.n--
+					this.assetType = 'n'
+				} else {
+					this.$squni.toast('剩余次数不足')
+					this.openBottomFunc()
+					return false
+				}
+			},
+			putMsg (msg, my = false, type = 'msg') {
+				let item = {
+					type: type,
+					msg: msg,
+					my: my,
+					date: dateFormat(new Date(), 'yyyy年MM月dd日 hh:mm')
+				}
+				this.msgList.push(item)
+				scrollToBottom()
+				if (my) {
+					this.addHistory(item)
+					// 清除消息
+					this.msg = ''
+					this.msgReply = ''
+				}
+			},
+			putMsgError(msg) {
+				this.putMsg(msg, false, 'error')
+				this.msgLoading = false
+				this.loading = false
+			},
+			addHistory(item) {
+				if (item.type === 'msg') {
+					let chatHistory = this.$squni.getStorageSync('chatHistory')
+					if(!chatHistory) {
+						chatHistory = []
+					}
+					if(chatHistory.length >= 50) {
+						chatHistory.splice(0, 1)
+					}
+					chatHistory.push(item)
+					this.$squni.setStorageSync('chatHistory', chatHistory)
+				}
+			},
+			//心跳检测
+			heartBeatTest() {
+				let globalTimer = null
+				//清除定时器
+				clearInterval(globalTimer)
+				//开启定时器定时检测心跳
+				globalTimer = setInterval(() => {
+					//发送消息给服务端
+					websocket.sendMessage('PING', null, () => {
+						//如果失败则清除定时器
+						clearInterval(globalTimer)
+					})
+				}, 10000)
+			},
+			heartStatus() {
+				this.statusTimer = interval(() => {
+					if (websocket.isOpen) {
+						this.statusColor = 'text-green'
+					} else if(this.statusColor === 'text-red') {
+						this.statusColor = 'text-yellow'
+					} else {
+						this.statusColor = 'text-red'
+					}
+				}, this.statusTimer, 200)
+			},
+			closeSocket() {
+				websocket.closeSocket()
+				clearInterval(this.statusTimer)
+				this.statusColor = 'text-red'
+			},
+			inputFocus(e) {
+				this.inputBottom = e.detail.height
+			},
+			inputBlur(e) {
+				this.inputBottom = 0
+			},
+			openBottomFunc() {
+				this.bottomFuncShow = true
+				this.$nextTick(() => {
+					this.$refs.bottomFunc.open()
+				})
+			},
+			mockReply() {
+				// 开发环境模拟回复
+				if (process.env.NODE_ENV === 'development') {
+					setTimeout(() => {
+						this.putMsg('这是模拟返回消息: ' + new Date(), false)
+						this.loading = false
+					}, 1000)
+					return
+				}
+			}
+		}
+	}
+</script>
+
+<style>
+	page {
+		padding-bottom: 220upx;
+	}
+	.cu-chat .chat-avatar.cu-avatar {
+	    width: 82upx;
+	    height: 82upx;
+	}
+	.cu-item:not(.first) {
+		padding-bottom: 0upx;
+	}
+	.cu-item.sec {
+		padding-top: 0upx;
+	}
+	.cu-chat .cu-item>.main {
+		max-width: calc(100% - 160upx);
+	}
+	.main .content {
+		word-wrap: break-word;
+		cursor: text;
+		user-select: text;
+	}
+	.cu-bar.foot {
+		box-shadow: 0 -0.5px 1px rgba(0, 0, 0, 0.1);
+	}
+	.foot {
+		padding-top: 20upx;
+		padding-bottom: 60upx;
+	}
+	.foot .cu-btn {
+		margin-right: 20upx;
+	}
+	.foot .action.func {
+		margin-left: 30upx;
+	}
+	.foot .tip-box {
+		position: absolute;
+		top: -60upx;
+		margin: 0 30upx;
+	}
+</style>

+ 107 - 0
pages/main/jump-default-browser.vue

@@ -0,0 +1,107 @@
+<template>
+	<view style="background: rgb(255, 255, 255);height: 100vh;">
+		<view class="top-bg">
+			<view class="top-bg-icon display-row align-center">
+				<span>点击右上角</span>
+				<view class="icon-bg"></view>
+				<span>可以继续浏览本站哦</span>
+			</view>
+		</view>
+		<view class="center-bg display-column align-center justify-center">
+			<view class="center-bg-icon"></view>
+		</view>
+		<view class="tip display-column align-center">
+			<span>您也可以复制本站网址,到默认浏览器中打开</span>
+			<button class="tip-btn" @tap="copy">点此复制</button>
+		</view>
+	</view>
+</template>
+
+<script>
+import {
+	isWechat
+} from '@/util/squ.js'
+export default {
+	data() {
+		return {
+			
+		}
+	},
+	created () {
+		// #ifdef H5
+		if(!isWechat()) {
+			window.location.href = this.$squni.getCurQuery('_originHref')
+		}
+		// #endif
+	},
+	methods: {
+		copy() {
+			this.$squni.copy(this.$squni.getCurQuery('_originHref'))
+		}
+	}
+}
+</script>
+
+<style>
+.top-bg {
+	width: 100%;
+	background-image: url(https://cdn7.aezo.cn/common/others/guide-browser-1.png);
+	background-size: 100% 100%;
+	height: 102px;
+	position: relative;
+}
+.top-bg .top-bg-icon {
+    position: absolute;
+    top: 22px;
+    left: 22px;
+    font-size: 13px;
+    font-weight: 600;
+    color: #fff;
+}
+.top-bg .top-bg-icon .icon-bg {
+    width: 22px;
+    height: 22px;
+    margin: 0 11px;
+	background-image: url(https://cdn7.aezo.cn/common/others/guide-browser-3.png);
+	background-position: 0% 0%;
+	background-size: 100% 100%;
+	background-repeat: no-repeat;
+}
+.center-bg {
+    width: 100%;
+}
+.center-bg .center-bg-icon {
+	width: 342px;
+	height: 50vh;
+	background-image: url(https://cdn7.aezo.cn/common/others/guide-browser-2.png);
+	background-position: 0% 0%;
+	background-size: 100% 100%;
+	background-repeat: no-repeat;
+}
+.tip {
+	margin-top: 80rpx;
+}
+.tip .tip-btn {
+    width: 171px;
+    height: 39px;
+    border-radius: 57px;
+    background-color: #3082ff;
+    font-size: 14px;
+    font-weight: 600;
+    color: #fff;
+    text-align: center;
+    line-height: 39px;
+	margin-top: 30rpx;
+}
+.align-center {
+    align-items: center;
+}
+.display-row {
+    display: flex;
+    flex-direction: row;
+}
+.display-column {
+    display: flex;
+    flex-direction: column;
+}
+</style>

+ 273 - 0
pages/main/login.vue

@@ -0,0 +1,273 @@
+<template>
+	<view style="height:100vh;background: #fff;">
+		<view class="img-a bg-gradual-cyan">
+			<view class="t-b">
+				您好,
+				<br />
+				欢迎使用,AirSmartChat
+			</view>
+		</view>
+		<view class="login-view" style="">
+			<view class="t-login">
+				<form v-show="showType === 'login'" class="cl">
+					<view  class="t-a">
+						<text class="txt">账号</text>
+						<input type="text" name="username" placeholder="请输入您的账号" maxlength="20" v-model="username" />
+					</view>
+					<view class="t-a">
+						<text class="txt">密码</text>
+						<input type="password" name="password" maxlength="18" placeholder="请输入您的密码" v-model="password" />
+					</view>
+					<button v-show="showType === 'login'" class="bg-cyan shadow margin-top-lg" @tap="login()" :loading="loginLoading">登 录</button>
+				</form>
+			</view>
+		</view>
+	</view>
+</template>
+<script>
+import { getUrlQuery } from '@/util/squ.js'
+import { doLogin as doLoginApi, LoginSuccess } from '@/util/login.js'
+import {
+	post
+} from '@/util/request.js'
+export default {
+	data() {
+		return {
+			showType: 'login',
+			email: '',
+			phone: '',
+			password: '',
+			username:'',
+			passwordConfirm: '',
+			verificationCode: '',
+			identifyingCode: '',
+			loginLoading: false,
+			registerLoading: false,
+			doRegisterLoading: false
+		}
+	},
+	mounted() {
+		this.createCode(4)
+	},
+	methods: {
+		login() {
+			if (!this.check()) {
+				this.createCode(4)
+				return
+			}
+			this.doLogin()
+		},
+		doLogin() {
+			this.loginLoading = true
+			doLoginApi('H5', {
+				username: this.email || this.phone|| this.username,
+				password: this.password
+			}).then(res => {
+				this.loginLoading = false
+				if (res === LoginSuccess) {
+					uni.showToast({ title: '登录成功' })
+					// 跳转到主页发现已经登录,会自动重新获取用户信息,此处无需获取
+					window.location.href = getUrlQuery('_originHref')
+				} else {
+					uni.showToast({ title: (res && res.data && res.data.message) || '登录失败', icon: 'none' })
+					this.createCode(4)
+				}
+			})
+		},
+		check () {
+			if (!this.username) {
+				uni.showToast({ title: '请输入您的账号', icon: 'none' })
+				return false
+			}
+			if (!this.password) {
+				uni.showToast({ title: '请输入您的密码', icon: 'none' })
+				return false
+			}
+			return true
+		},
+		createCode(length) {
+		    var code = "";
+		    var codeLength = parseInt(length); //验证码的长度
+		    var checkCode = document.getElementById("checkCode");
+		    // 所有候选组成验证码的字符,当然也可以用中文的
+		    var codeChars = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+		    'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
+		    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'); 
+		    //循环组成验证码的字符串
+		    for (var i = 0; i < codeLength; i++) {
+		        //获取随机验证码下标
+		        var charNum = Math.floor(Math.random() * 62);
+		        //组合成指定字符验证码
+		        code += codeChars[charNum]
+		    }
+		    if (checkCode) {
+		        //为验证码区域添加样式名
+		        checkCode.className = "code"
+		        //将生成验证码赋值到显示区
+		        checkCode.innerHTML = code
+		    }
+		},
+		validateCode(inputCode) {
+			// 获取显示区生成的验证码
+			var checkCode = document.getElementById("checkCode").innerHTML
+			//console.log(checkCode, inputCode);
+			if (inputCode.length <= 0) {
+				uni.showToast({ title: '请输入验证码', icon: 'none' })
+				return false
+			} else if (inputCode.toUpperCase() != checkCode.toUpperCase()) {
+				uni.showToast({ title: '验证码输入有误', icon: 'none' })
+				return false
+			} else {
+				return true
+			}       
+		}
+	}
+};
+</script>
+<style>
+.txt {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333333;
+}
+.img-a {
+	width: 100%;
+	height: 450rpx;
+	background-size: 100%;
+}
+.reg {
+	font-size: 28rpx;
+	color: #fff;
+	height: 90rpx;
+	line-height: 90rpx;
+	border-radius: 50rpx;
+	font-weight: bold;
+	background: #f5f6fa;
+	color: #000000;
+	text-align: center;
+	margin-top: 30rpx;
+}
+
+.login-view {
+	width: 100%;
+	position: relative;
+	margin-top: -120rpx;
+	background-color: #ffffff;
+	border-radius: 8% 8% 0% 0;
+}
+
+.t-login {
+	width: 600rpx;
+	margin: 0 auto;
+	font-size: 28rpx;
+	padding-top: 80rpx;
+}
+
+.t-login button {
+	font-size: 28rpx;
+	color: #fff;
+	height: 90rpx;
+	line-height: 90rpx;
+	border-radius: 50rpx;
+	font-weight: bold;
+}
+
+.t-login input {
+	height: 90rpx;
+	line-height: 90rpx;
+	margin-bottom: 50rpx;
+	border-bottom: 1px solid #e9e9e9;
+	font-size: 28rpx;
+}
+
+.t-login .t-a {
+	position: relative;
+}
+
+.t-b {
+	text-align: left;
+	font-size: 42rpx;
+	color: #ffffff;
+	padding: 130rpx 0 0 70rpx;
+	font-weight: bold;
+	line-height: 70rpx;
+}
+
+.t-login .t-c {
+	position: absolute;
+	right: 22rpx;
+	top: 22rpx;
+	background: #5677fc;
+	color: #fff;
+	font-size: 24rpx;
+	border-radius: 50rpx;
+	height: 50rpx;
+	line-height: 50rpx;
+	padding: 0 25rpx;
+}
+
+.t-login .t-d {
+	text-align: center;
+	color: #999;
+	margin: 80rpx 0;
+}
+
+.t-login .t-g {
+	float: left;
+	width: 50%;
+}
+
+.t-login .t-e image {
+	width: 50rpx;
+	height: 50rpx;
+}
+
+.t-login .uni-input-placeholder {
+	color: #aeaeae;
+}
+
+.cl {
+	zoom: 1;
+}
+
+.cl:after {
+	clear: both;
+	display: block;
+	visibility: hidden;
+	height: 0;
+	content: '\20';
+}
+
+.bg-gradual-cyan {
+	background-image: linear-gradient(45deg, #1da480, #1cbbb4);
+	color: #ffffff;
+}
+.code-box .code {
+	 font-family:Arial;
+	 font-style:italic;
+	 color:blue;
+	 font-size:24px;
+	 border:0;
+	 padding:2px 3px;
+	 letter-spacing:3px;
+	 font-weight:bolder;            
+	 float:left;           
+	 cursor:pointer;
+	 width:90px;
+	 height:30px;
+	 line-height:30px;
+	 text-align:center;
+	 vertical-align:middle;
+	 background-color:#D8B7E3;
+ }
+.code-box span {
+	text-decoration:none;
+	font-size:14px;
+	color:#288bc4;
+	padding-left:10px;
+}
+.code-box span:hover {
+	text-decoration:underline;
+	cursor:pointer;
+}
+</style>

+ 223 - 0
pages/main/prompt/prompt-list.vue

@@ -0,0 +1,223 @@
+<template>
+	<view>
+		<cu-custom bgColor="bg-cyan" :isBack="true">
+			<block slot="backText">返回</block>
+			<block slot="content">提示词分类</block>
+		</cu-custom>
+		
+		<scroll-view scroll-x class="bg-white nav margin-bottom-xs" scroll-with-animation :scroll-left="scrollLeft">
+			<view class="cu-item" :class="item.value == tabCur ? 'text-cyan cur' : ''"
+				v-for="(item, index) in navList" :key="index" @tap="e => tabSelect(e, item, index)" :data-id="item.value">
+				{{ item.label }}
+			</view>
+		</scroll-view>
+		<PromptList :promptList="promptList"></PromptList>
+		<view v-if="loadStatus !== 'none'" class="cu-load text-grey" :class="loadStatus === 'loading' ? 'loading' : 'over'"></view>
+		<view v-else class="cu-load text-grey info"></view>
+	</view>
+</template>
+
+<script>
+	import PromptList from '@/components/prompt-list/prompt-list.vue'
+	import { findPromptListApi } from '@/api/chat.js'
+	export default {
+		components: { PromptList },
+		data() {
+			return {
+				tabCur: 'personal',
+				scrollLeft: 0,
+				contentScrollWidth: 0,
+				// Array.prototype.slice.call($(".checkboxList_autc").getElementsByTagName("li")).map(x => ({value: x.getElementsByTagName("label")[0].getAttribute("for").replaceAll("showcase_checkbox_id_", ""), label: x.getElementsByTagName("label")[0].childNodes[0].data}));
+				navList: [
+					// {
+					// 	"value": "favorite",
+					// 	"label": "常用"
+					// },
+					{
+						"value": "latest",
+						"label": "最新"
+					},
+					{
+						"value": "personal",
+						"label": "个人"
+					},
+					{
+						"value": "contribute",
+						"label": "投稿"
+					},
+					{
+						"value": "write",
+						"label": "写作辅助"
+					},
+					{
+						"value": "language",
+						"label": "语言/翻译"
+					},
+					{
+						"value": "article",
+						"label": "文章/报告"
+					},
+					{
+						"value": "code",
+						"label": "IT/编程"
+					},
+					{
+						"value": "social",
+						"label": "心理/社交"
+					},
+					{
+						"value": "tool",
+						"label": "工具"
+					},
+					{
+						"value": "mind",
+						"label": "发散思维"
+					},
+					{
+						"value": "ai",
+						"label": "AI"
+					},
+					{
+						"value": "interesting",
+						"label": "趣味知识"
+					},
+					{
+						"value": "life",
+						"label": "自助百科"
+					},
+					{
+						"value": "living",
+						"label": "生活质量"
+					},
+					{
+						"value": "speech",
+						"label": "辩论/演讲"
+					},
+					{
+						"value": "music",
+						"label": "音乐"
+					},
+					{
+						"value": "philosophy",
+						"label": "哲学/宗教"
+					},
+					{
+						"value": "comments",
+						"label": "点评/评鉴"
+					},
+					{
+						"value": "company",
+						"label": "企业职位"
+					},
+					{
+						"value": "pedagogy",
+						"label": "教育/学生"
+					},
+					{
+						"value": "academic",
+						"label": "学术/教师"
+					},
+					{
+						"value": "professional",
+						"label": "行业顾问"
+					},
+					{
+						"value": "doctor",
+						"label": "医生"
+					},
+					{
+						"value": "finance",
+						"label": "金融顾问"
+					},
+					{
+						"value": "games",
+						"label": "游戏"
+					},
+					{
+						"value": "interpreter",
+						"label": "终端/解释器"
+					},
+					{
+						"value": "seo",
+						"label": "SEO"
+					},
+					{
+						"value": "text",
+						"label": "文本/词语"
+					}
+				],
+				promptList: [],
+				current: 1,
+				loadStatus: 'none' // none/loading/over
+			}
+		},
+		created() {
+			this.findList()
+		},
+		mounted() {
+			this.getScrollWidth()
+		},
+		onReachBottom() {
+			if (this.loadStatus === 'over') {
+				return
+			}
+			this.current ++
+			this.fetchData()
+		},
+		methods: {
+			findList() {
+				this.loadStatus = 'none'
+				this.promptList = []
+				this.current = 1
+				this.fetchData()
+			},
+			fetchData() {
+				this.loadStatus = 'loading'
+				findPromptListApi({ 
+					current: this.current,
+					tags: this.tabCur
+				}).then(({ status, data }) => {
+					if (status === 'success') {
+						if(data.records && data.records.length > 0) {
+							this.promptList.push(...data.records)
+							if (data.records.length < 20) {
+								this.loadStatus = 'over'
+							} else {
+								this.loadStatus = 'none'
+							}
+						} else {
+							this.loadStatus = 'over'
+						}
+					}
+				})
+			},
+			tabSelect(e, item, index) {
+				this.tabCur = e.currentTarget.dataset.id
+				// 当前点击子元素距离左边栏的距离 - scroll-view 宽度的一半  + 当前点击子元素一半的宽度 实现居中展示
+				this.scrollLeft = this.navList[index].left - this.contentScrollWidth / 2 + this.navList[index].width / 2
+				this.findList()
+			},
+			getScrollWidth() {
+				const query = uni.createSelectorQuery().in(this)
+				query.select('.nav').boundingClientRect(data => {
+					// 拿到 scroll-view 组件宽度                    
+					this.contentScrollWidth = data.width
+				}).exec()
+				
+				query.selectAll('.cu-item').boundingClientRect(data => {
+				let dataLen = data.length;
+					for (let i = 0; i < dataLen; i++) {
+						//  scroll-view 子元素组件距离左边栏的距离
+						this.navList[i].left = data[i].left
+						//  scroll-view 子元素组件宽度
+						this.navList[i].width = data[i].width
+					}
+				}).exec()
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	
+</style>

+ 72 - 0
pages/main/prompt/prompt.vue

@@ -0,0 +1,72 @@
+<template>
+	<view>
+		<cu-custom bgColor="bg-cyan" :isBack="true">
+			<block slot="backText">返回</block>
+			<block slot="content">提示词</block>
+		</cu-custom>
+		
+		<view class="cu-bar bg-white margin-top-xs">
+			<view class="action sub-title">
+				<text class="text-xl text-bold text-cyan text-shadow">收藏</text>
+				<text class="text-ABC text-cyan">Favorite</text>
+			</view>
+		</view>
+		<PromptList :promptList="promptFavor" @toggle-favor="getPromptFavor"></PromptList>
+		
+		<view class="cu-bar bg-white margin-top-xs">
+			<view class="action sub-title">
+				<text class="text-xl text-bold text-cyan text-shadow">常用</text>
+				<text class="text-ABC text-cyan">Common</text>
+			</view>
+			<view class="action">
+				<text class="text-cyan" @tap="$squni.navigateTo('/pages/main/prompt/prompt-list')">查看更多></text>
+			</view>
+		</view>
+		<PromptList :promptList="promptCommon" @toggle-favor="getPromptFavor"></PromptList>
+	</view>
+</template>
+
+<script>
+	import PromptList from '@/components/prompt-list/prompt-list.vue'
+	import { findPromptListApi } from '@/api/chat.js'
+	export default {
+		components: { PromptList },
+		data() {
+			return {
+				promptFavor: [],
+				promptCommon: []
+			}
+		},
+		created() {
+			findPromptListApi({ commonOpt: 1 }).then(({ status, data }) => {
+				if (status === 'success') {
+					this.promptCommon = data
+				}
+			})
+		},
+		onShow() {
+			this.getPromptFavor()
+		},
+		methods: {
+			getPromptFavor () {
+				if (this.$store.getters.favorPromptList.length <= 0) {
+					return
+				}
+				findPromptListApi({ ids: this.$store.getters.favorPromptList }).then(({ status, data }) => {
+					if (status === 'success') {
+						let map = {}
+						data.forEach(x => map[x.id] = x)
+						let list = []
+						for(let id of this.$store.getters.favorPromptList) {
+							list.push(map[id])
+						}
+						this.promptFavor = list
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+</style>

BIN
static/favicon.ico


BIN
static/icon/prompt/cunchuyouhua.png


BIN
static/icon/prompt/drawingpalette.png


BIN
static/icon/prompt/duihua.png


BIN
static/icon/prompt/gongzuozhoubao.png


BIN
static/icon/prompt/linggan.png


BIN
static/icon/prompt/loveletter.png


BIN
static/icon/prompt/lunwen.png


BIN
static/icon/prompt/lvyou.png


BIN
static/icon/prompt/robot.png


BIN
static/icon/prompt/tree.png


BIN
static/icon/prompt/voice.png


BIN
static/icon/prompt/wenzhang.png


BIN
static/icon/prompt/write-ms.png


BIN
static/icon/prompt/xiaohongshubiji.png


BIN
static/icon/prompt/xingming.png


BIN
static/icon/prompt/yuedu.png


BIN
static/icon/prompt/yuyanfanyi.png


BIN
static/logo-100.png


BIN
static/robot.png


+ 17 - 0
store/index.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import user from './module/user.js'
+import app from './module/app.js'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {
+    user,
+    app
+  }
+})

+ 22 - 0
store/module/app.js

@@ -0,0 +1,22 @@
+import squni from '@/util/squni.js'
+
+export default {
+	state: {
+		favorPromptList: squni.getStorageSync('favorPromptList') || []
+	},
+
+	getters: {
+		favorPromptList: state => state.favorPromptList
+	},
+
+	// this.$store.commit('setFavorPromptMap', favorPromptList)
+	mutations: {
+		setFavorPromptList(state, favorPromptList) {
+			state.favorPromptList = favorPromptList
+			squni.setStorageSync('favorPromptList', favorPromptList)
+		}
+	},
+
+	// this.$store.dispatch('LoginByUsername', userInfo)
+	actions: {}
+}

+ 127 - 0
store/module/user.js

@@ -0,0 +1,127 @@
+import squni from '@/util/squni.js'
+import {
+	post
+} from '@/util/request.js'
+
+export default {
+	state: {
+		appInfo: squni.getStorageSync('appInfo') || {},
+		token: squni.getStorageSync('token') || '',
+		refreshToken: squni.getStorageSync('refreshToken') || '',
+		userId: squni.getStorageSync('userId') || '',
+		openid: squni.getStorageSync('openid') || '',
+		userInfo: squni.getStorageSync('userInfo') || {},
+		userInfoOth: squni.getStorageSync('userInfoOth') || {},
+		permission: squni.getStorageSync('permission') || {},
+		roles: squni.getStorageSync('roles') || [],
+		equipment: squni.getStorageSync('equipment') || '',
+		platform: squni.getStorageSync('platform') || '',
+		// 进入(微信小程序)场景值
+		scene: squni.getStorageSync('scene') || ''
+	},
+
+	getters: {
+		appInfo: state => state.appInfo,
+		token: state => state.token,
+		refreshToken: state => state.refreshToken,
+		userId: state => state.userId,
+		openid: state => state.openid,
+		userInfo: state => state.userInfo,
+		userInfoOth: state => state.userInfoOth,
+		permission: state => state.permission,
+		roles: state => state.roles,
+		equipment: state => state.equipment,
+		platform: state => state.platform,
+		scene: state => state.scene,
+		usernameSimple: state => {
+			// 移除sq_
+			let username = state.userInfo.username || ''
+			if (username.indexOf('_') > 0) {
+				username = username.substring(username.indexOf('_') + 1)
+			}
+			return username.toUpperCase()
+		}
+	},
+
+	// this.$store.commit('setAppInfo', appInfo)
+	mutations: {
+		setAppInfo(state, appInfo) {
+			state.appInfo = appInfo
+			squni.setStorageSync('appInfo', appInfo)
+		},
+		setToken(state, token) {
+			state.token = token
+			squni.setStorageSync('token', token)
+		},
+		setRefreshToken(state, refreshToken) {
+			state.refreshToken = refreshToken
+			squni.setStorageSync('refreshToken', refreshToken)
+		},
+		setUserId(state, userId) {
+			state.userId = userId
+			squni.setStorageSync('userId', userId)
+		},
+		setOpenid(state, openid) {
+			state.openid = openid
+			squni.setStorageSync('openid', openid)
+		},
+		setUserInfo(state, userInfo) {
+			state.userInfo = userInfo
+			squni.setStorageSync('userInfo', userInfo)
+		},
+		setUserInfoOth(state, userInfoOth) {
+			state.userInfoOth = userInfoOth
+			squni.setStorageSync('userInfoOth', userInfoOth)
+		},
+		setPermission(state, permission) {
+			state.permission = permission
+			squni.setStorageSync('permission', permission)
+		},
+		setRoles(state, roles) {
+			state.roles = roles
+			squni.setStorageSync('roles', roles)
+		},
+		setEquipment(state, equipment) {
+			state.equipment = equipment
+			squni.setStorageSync('equipment', equipment)
+		},
+		setPlatform(state, platform) {
+			state.platform = platform
+			squni.setStorageSync('platform', platform)
+		},
+		setScene(state, scene) {
+			state.scene = scene
+			squni.setStorageSync('scene', scene)
+		}
+	},
+
+	// this.$store.dispatch('LoginByUsername', userInfo)
+	actions: {
+		GetUserInfo({
+			commit
+		}) {
+			return new Promise((resolve, reject) => {
+				post('/core/user/info')
+					.then(res => {
+						if (res.code === 20000) {
+							const data = res.data
+							commit('setUserId', (data.userInfo || {}).id)
+							commit('setUserInfo', data.userInfo || {})
+							commit('setRoles', data.roles || [])
+							commit('setPermission', data.permissions || [])
+							delete data.userInfo
+							delete data.roles
+							delete data.permissions
+							commit('setUserInfoOth', data || {})
+							resolve(data)
+						} else {
+							reject(res.message)
+						}
+					})
+					.catch(err => {
+						reject(err)
+					})
+			})
+		}
+	}
+}

+ 184 - 0
uni_modules/colorui/animation.css

@@ -0,0 +1,184 @@
+/* 
+  Animation 微动画  
+  基于ColorUI组建库的动画模块 by 文晓港 2019年3月26日19:52:28
+ */
+
+/* css 滤镜 控制黑白底色gif的 */
+.gif-black{  
+  mix-blend-mode: screen;  
+}
+.gif-white{  
+  mix-blend-mode: multiply; 
+}
+
+
+/* Animation css */
+[class*=animation-] {
+    animation-duration: .5s;
+    animation-timing-function: ease-out;
+    animation-fill-mode: both
+}
+
+.animation-fade {
+    animation-name: fade;
+    animation-duration: .8s;
+    animation-timing-function: linear
+}
+
+.animation-scale-up {
+    animation-name: scale-up
+}
+
+.animation-scale-down {
+    animation-name: scale-down
+}
+
+.animation-slide-top {
+    animation-name: slide-top
+}
+
+.animation-slide-bottom {
+    animation-name: slide-bottom
+}
+
+.animation-slide-left {
+    animation-name: slide-left
+}
+
+.animation-slide-right {
+    animation-name: slide-right
+}
+
+.animation-shake {
+    animation-name: shake
+}
+
+.animation-reverse {
+    animation-direction: reverse
+}
+
+@keyframes fade {
+    0% {
+        opacity: 0
+    }
+
+    100% {
+        opacity: 1
+    }
+}
+
+@keyframes scale-up {
+    0% {
+        opacity: 0;
+        transform: scale(.2)
+    }
+
+    100% {
+        opacity: 1;
+        transform: scale(1)
+    }
+}
+
+@keyframes scale-down {
+    0% {
+        opacity: 0;
+        transform: scale(1.8)
+    }
+
+    100% {
+        opacity: 1;
+        transform: scale(1)
+    }
+}
+
+@keyframes slide-top {
+    0% {
+        opacity: 0;
+        transform: translateY(-100%)
+    }
+
+    100% {
+        opacity: 1;
+        transform: translateY(0)
+    }
+}
+
+@keyframes slide-bottom {
+    0% {
+        opacity: 0;
+        transform: translateY(100%)
+    }
+
+    100% {
+        opacity: 1;
+        transform: translateY(0)
+    }
+}
+
+@keyframes shake {
+
+    0%,
+    100% {
+        transform: translateX(0)
+    }
+
+    10% {
+        transform: translateX(-9px)
+    }
+
+    20% {
+        transform: translateX(8px)
+    }
+
+    30% {
+        transform: translateX(-7px)
+    }
+
+    40% {
+        transform: translateX(6px)
+    }
+
+    50% {
+        transform: translateX(-5px)
+    }
+
+    60% {
+        transform: translateX(4px)
+    }
+
+    70% {
+        transform: translateX(-3px)
+    }
+
+    80% {
+        transform: translateX(2px)
+    }
+
+    90% {
+        transform: translateX(-1px)
+    }
+}
+
+@keyframes slide-left {
+    0% {
+        opacity: 0;
+        transform: translateX(-100%)
+    }
+
+    100% {
+        opacity: 1;
+        transform: translateX(0)
+    }
+}
+
+@keyframes slide-right {
+    0% {
+        opacity: 0;
+        transform: translateX(100%)
+    }
+
+    100% {
+        opacity: 1;
+        transform: translateX(0)
+    }
+}

+ 70 - 0
uni_modules/colorui/components/cu-custom.vue

@@ -0,0 +1,70 @@
+<template>
+	<view>
+		<view class="cu-custom" :style="[{height:CustomBar + 'px'}]">
+			<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]">
+				<view class="action" @click="BackPage" v-if="isBack">
+					<text class="cuIcon-back"></text>
+					<slot name="backText"></slot>
+				</view>
+				<view class="content" :style="[{top:StatusBar + 'px'}]">
+					<slot name="content"></slot>
+				</view>
+				<slot name="right"></slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				StatusBar: this.StatusBar,
+				CustomBar: this.CustomBar
+			};
+		},
+		name: 'cu-custom',
+		computed: {
+			style() {
+				var StatusBar= this.StatusBar;
+				var CustomBar= this.CustomBar;
+				var bgImage = this.bgImage;
+				var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`;
+				if (this.bgImage) {
+					style = `${style}background-image:url(${bgImage});`;
+				}
+				return style
+			}
+		},
+		props: {
+			bgColor: {
+				type: String,
+				default: ''
+			},
+			isBack: {
+				type: [Boolean, String],
+				default: false
+			},
+			// 不要使用本地路径,需要使用base64或网络路径,否则微信小程序不显示
+			bgImage: {
+				type: String,
+				default: ''
+			},
+		},
+		methods: {
+			BackPage() {
+				// 修改Colorui原代码
+				// uni.navigateBack({
+				// 	delta: 1
+				// });
+				
+				// 修改后,考虑了分享过来的退回按钮
+				this.$squni.navigateBack(this.$parent)
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

File diff suppressed because it is too large
+ 1226 - 0
uni_modules/colorui/icon.css


File diff suppressed because it is too large
+ 3912 - 0
uni_modules/colorui/main.css


+ 784 - 0
uni_modules/t-color-picker/t-color-picker.vue

@@ -0,0 +1,784 @@
+<template>
+	<view v-show="show" class="t-wrapper" @touchmove.stop.prevent="moveHandle">
+		<view class="t-mask" :class="{active:active}" @click.stop="close"></view>
+		<view class="t-box" :class="{active:active}">
+			<view class="t-header">
+				<view class="t-header-button" @click="close">取消</view>
+				<view class="t-header-button" @click="confirm">确认</view>
+			</view>
+			<view class="t-color__box" :style="{ background: 'rgb(' + bgcolor.r + ',' + bgcolor.g + ',' + bgcolor.b + ')'}">
+				<view class="t-background boxs" @touchstart="touchstart($event, 0)" @touchmove="touchmove($event, 0)" @touchend="touchend($event, 0)">
+					<view class="t-color-mask"></view>
+					<view class="t-pointer" :style="{ top: site[0].top - 8 + 'px', left: site[0].left - 8 + 'px' }"></view>
+				</view>
+			</view>
+			<view class="t-control__box">
+				<view class="t-control__color">
+					<view class="t-control__color-content" :style="{ background: 'rgba(' + rgba.r + ',' + rgba.g + ',' + rgba.b + ',' + rgba.a + ')' }"></view>
+				</view>
+				<view class="t-control-box__item">
+					<view class="t-controller boxs" @touchstart="touchstart($event, 1)" @touchmove="touchmove($event, 1)" @touchend="touchend($event, 1)">
+						<view class="t-hue">
+							<view class="t-circle" :style="{ left: site[1].left - 12 + 'px' }"></view>
+						</view>
+					</view>
+					<view class="t-controller boxs" @touchstart="touchstart($event, 2)" @touchmove="touchmove($event, 2)" @touchend="touchend($event, 2)">
+						<view class="t-transparency">
+							<view class="t-circle" :style="{ left: site[2].left - 12 + 'px' }"></view>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="t-result__box">
+				<view v-if="mode" class="t-result__item">
+					<view class="t-result__box-input">{{hex}}</view>
+					<view class="t-result__box-text">HEX</view>
+				</view>
+				<template v-else>
+					<view class="t-result__item">
+						<view class="t-result__box-input">{{rgba.r}}</view>
+						<view class="t-result__box-text">R</view>
+					</view>
+					<view class="t-result__item">
+						<view class="t-result__box-input">{{rgba.g}}</view>
+						<view class="t-result__box-text">G</view>
+					</view>
+					<view class="t-result__item">
+						<view class="t-result__box-input">{{rgba.b}}</view>
+						<view class="t-result__box-text">B</view>
+					</view>
+					<view class="t-result__item">
+						<view class="t-result__box-input">{{rgba.a}}</view>
+						<view class="t-result__box-text">A</view>
+					</view>
+				</template>
+
+				<view class="t-result__item t-select" @click="select">
+					<view class="t-result__box-input">
+						<view>切换</view>
+						<view>模式</view>
+					</view>
+				</view>
+			</view>
+			<view class="t-alternative">
+				<view class="t-alternative__item" v-for="(item,index) in colorList" :key="index">
+					<view class="t-alternative__item-content" :style="{ background: 'rgba(' + item.r + ',' + item.g + ',' + item.b + ',' + item.a + ')' }"
+					 @click="selectColor(item)">
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			color: {
+				type: Object,
+				default () {
+					return {
+						r: 0,
+						g: 0,
+						b: 0,
+						a: 0
+					}
+				}
+			},
+			spareColor: {
+				type: Array,
+				default () {
+					return []
+				}
+			}
+		},
+		data() {
+			return {
+				show: false,
+				active: false,
+				// rgba 颜色
+				rgba: {
+					r: 0,
+					g: 0,
+					b: 0,
+					a: 1
+				},
+				// hsb 颜色
+				hsb: {
+					h: 0,
+					s: 0,
+					b: 0
+				},
+				site: [{
+					top: 0,
+					left: 0
+				}, {
+					left: 0
+				}, {
+					left: 0
+				}],
+				index: 0,
+				bgcolor: {
+					r: 255,
+					g: 0,
+					b: 0,
+					a: 1
+				},
+				hex: '#000000',
+				mode: true,
+				colorList: [{
+					r: 244,
+					g: 67,
+					b: 54,
+					a: 1
+				}, {
+					r: 233,
+					g: 30,
+					b: 99,
+					a: 1
+				}, {
+					r: 156,
+					g: 39,
+					b: 176,
+					a: 1
+				}, {
+					r: 103,
+					g: 58,
+					b: 183,
+					a: 1
+				}, {
+					r: 63,
+					g: 81,
+					b: 181,
+					a: 1
+				}, {
+					r: 33,
+					g: 150,
+					b: 243,
+					a: 1
+				}, {
+					r: 3,
+					g: 169,
+					b: 244,
+					a: 1
+				}, {
+					r: 0,
+					g: 188,
+					b: 212,
+					a: 1
+				}, {
+					r: 0,
+					g: 150,
+					b: 136,
+					a: 1
+				}, {
+					r: 76,
+					g: 175,
+					b: 80,
+					a: 1
+				}, {
+					r: 139,
+					g: 195,
+					b: 74,
+					a: 1
+				}, {
+					r: 205,
+					g: 220,
+					b: 57,
+					a: 1
+				}, {
+					r: 255,
+					g: 235,
+					b: 59,
+					a: 1
+				}, {
+					r: 255,
+					g: 193,
+					b: 7,
+					a: 1
+				}, {
+					r: 255,
+					g: 152,
+					b: 0,
+					a: 1
+				}, {
+					r: 255,
+					g: 87,
+					b: 34,
+					a: 1
+				}, {
+					r: 121,
+					g: 85,
+					b: 72,
+					a: 1
+				}, {
+					r: 158,
+					g: 158,
+					b: 158,
+					a: 1
+				}, {
+					r: 0,
+					g: 0,
+					b: 0,
+					a: 0.5
+				}, {
+					r: 0,
+					g: 0,
+					b: 0,
+					a: 0
+				}, ]
+			};
+		},
+		created() {
+			this.rgba = this.color;
+			if (this.spareColor.length !== 0) {
+				this.colorList = this.spareColor;
+			}
+		},
+		methods: {
+			/**
+			 * 初始化
+			 */
+			init() {
+				// hsb 颜色
+				this.hsb = this.rgbToHex(this.rgba);
+				// this.setColor();
+				this.setValue(this.rgba);
+			},
+			moveHandle() {},
+			open() {
+				this.show = true;
+				this.$nextTick(() => {
+					this.init();
+					setTimeout(() => {
+						this.active = true;
+						setTimeout(() => {
+							this.getSelectorQuery();
+						}, 350)
+					}, 50)
+				})
+
+			},
+			close() {
+				this.active = false;
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this.show = false;
+					}, 500)
+				})
+			},
+			confirm() {
+				this.close();
+				this.$emit('confirm', {
+					rgba: this.rgba,
+					hex: this.hex
+				})
+			},
+			// 选择模式
+			select() {
+				this.mode = !this.mode
+			},
+			// 常用颜色选择
+			selectColor(item) {
+				this.setColorBySelect(item)
+			},
+			touchstart(e, index) {
+				const {
+					pageX,
+					pageY
+				} = e.touches[0];
+				this.pageX = pageX;
+				this.pageY = pageY;
+				this.setPosition(pageX, pageY, index);
+			},
+			touchmove(e, index) {
+				const {
+					pageX,
+					pageY
+				} = e.touches[0];
+				this.moveX = pageX;
+				this.moveY = pageY;
+				this.setPosition(pageX, pageY, index);
+			},
+			touchend(e, index) {},
+			/**
+			 * 设置位置
+			 */
+			setPosition(x, y, index) {
+				this.index = index;
+				const {
+					top,
+					left,
+					width,
+					height
+				} = this.position[index];
+				// 设置最大最小值
+
+				this.site[index].left = Math.max(0, Math.min(parseInt(x - left), width));
+				if (index === 0) {
+					this.site[index].top = Math.max(0, Math.min(parseInt(y - top), height));
+					// 设置颜色
+					this.hsb.s = parseInt((100 * this.site[index].left) / width);
+					this.hsb.b = parseInt(100 - (100 * this.site[index].top) / height);
+					this.setColor();
+					this.setValue(this.rgba);
+				} else {
+					this.setControl(index, this.site[index].left);
+				}
+			},
+			/**
+			 * 设置 rgb 颜色
+			 */
+			setColor() {
+				const rgb = this.HSBToRGB(this.hsb);
+				this.rgba.r = rgb.r;
+				this.rgba.g = rgb.g;
+				this.rgba.b = rgb.b;
+			},
+			/**
+			 * 设置二进制颜色
+			 * @param {Object} rgb
+			 */
+			setValue(rgb) {
+				this.hex = '#' + this.rgbToHex(rgb);
+			},
+			setControl(index, x) {
+				const {
+					top,
+					left,
+					width,
+					height
+				} = this.position[index];
+
+				if (index === 1) {
+					this.hsb.h = parseInt((360 * x) / width);
+					this.bgcolor = this.HSBToRGB({
+						h: this.hsb.h,
+						s: 100,
+						b: 100
+					});
+					this.setColor()
+				} else {
+					this.rgba.a = (x / width).toFixed(1);
+				}
+				this.setValue(this.rgba);
+			},
+			/**
+			 * rgb 转 二进制 hex
+			 * @param {Object} rgb
+			 */
+			rgbToHex(rgb) {
+				let hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)];
+				hex.map(function(str, i) {
+					if (str.length == 1) {
+						hex[i] = '0' + str;
+					}
+				});
+				return hex.join('');
+			},
+			setColorBySelect(getrgb) {
+				const {
+					r,
+					g,
+					b,
+					a
+				} = getrgb;
+				let rgb = {}
+				rgb = {
+					r: r ? parseInt(r) : 0,
+					g: g ? parseInt(g) : 0,
+					b: b ? parseInt(b) : 0,
+					a: a ? a : 0,
+				};
+				this.rgba = rgb;
+				this.hsb = this.rgbToHsb(rgb);
+				this.changeViewByHsb();
+			},
+			changeViewByHsb() {
+				const [a, b, c] = this.position;
+				this.site[0].left = parseInt(this.hsb.s * a.width / 100);
+				this.site[0].top = parseInt((100 - this.hsb.b) * a.height / 100);
+				this.setColor(this.hsb.h);
+				this.setValue(this.rgba);
+				this.bgcolor = this.HSBToRGB({
+					h: this.hsb.h,
+					s: 100,
+					b: 100
+				});
+
+				this.site[1].left = this.hsb.h / 360 * b.width;
+				this.site[2].left = this.rgba.a * c.width;
+
+			},
+			/**
+			 * hsb 转 rgb
+			 * @param {Object} 颜色模式  H(hues)表示色相,S(saturation)表示饱和度,B(brightness)表示亮度
+			 */
+			HSBToRGB(hsb) {
+				let rgb = {};
+				let h = Math.round(hsb.h);
+				let s = Math.round((hsb.s * 255) / 100);
+				let v = Math.round((hsb.b * 255) / 100);
+				if (s == 0) {
+					rgb.r = rgb.g = rgb.b = v;
+				} else {
+					let t1 = v;
+					let t2 = ((255 - s) * v) / 255;
+					let t3 = ((t1 - t2) * (h % 60)) / 60;
+					if (h == 360) h = 0;
+					if (h < 60) {
+						rgb.r = t1;
+						rgb.b = t2;
+						rgb.g = t2 + t3;
+					} else if (h < 120) {
+						rgb.g = t1;
+						rgb.b = t2;
+						rgb.r = t1 - t3;
+					} else if (h < 180) {
+						rgb.g = t1;
+						rgb.r = t2;
+						rgb.b = t2 + t3;
+					} else if (h < 240) {
+						rgb.b = t1;
+						rgb.r = t2;
+						rgb.g = t1 - t3;
+					} else if (h < 300) {
+						rgb.b = t1;
+						rgb.g = t2;
+						rgb.r = t2 + t3;
+					} else if (h < 360) {
+						rgb.r = t1;
+						rgb.g = t2;
+						rgb.b = t1 - t3;
+					} else {
+						rgb.r = 0;
+						rgb.g = 0;
+						rgb.b = 0;
+					}
+				}
+				return {
+					r: Math.round(rgb.r),
+					g: Math.round(rgb.g),
+					b: Math.round(rgb.b)
+				};
+			},
+			rgbToHsb(rgb) {
+				let hsb = {
+					h: 0,
+					s: 0,
+					b: 0
+				};
+				let min = Math.min(rgb.r, rgb.g, rgb.b);
+				let max = Math.max(rgb.r, rgb.g, rgb.b);
+				let delta = max - min;
+				hsb.b = max;
+				hsb.s = max != 0 ? 255 * delta / max : 0;
+				if (hsb.s != 0) {
+					if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta;
+					else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta;
+					else hsb.h = 4 + (rgb.r - rgb.g) / delta;
+				} else hsb.h = -1;
+				hsb.h *= 60;
+				if (hsb.h < 0) hsb.h = 0;
+				hsb.s *= 100 / 255;
+				hsb.b *= 100 / 255;
+				return hsb;
+			},
+			getSelectorQuery() {
+				const views = uni.createSelectorQuery().in(this);
+				views
+					.selectAll('.boxs')
+					.boundingClientRect(data => {
+						if (!data || data.length === 0) {
+							setTimeout(() => this.getSelectorQuery(), 20)
+							return
+						}
+						this.position = data;
+						// this.site[0].top = data[0].height;
+						// this.site[0].left = 0;
+						// this.site[1].left = data[1].width;
+						// this.site[2].left = data[2].width;
+						this.setColorBySelect(this.rgba);
+					})
+					.exec();
+			}
+		},
+		watch: {
+			spareColor(newVal) {
+				this.colorList = newVal;
+			}
+		}
+	};
+</script>
+
+<style>
+	.t-wrapper {
+		position: fixed;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		width: 100%;
+		box-sizing: border-box;
+		z-index: 9999;
+	}
+
+	.t-box {
+		width: 100%;
+		position: absolute;
+		bottom: 0;
+		padding: 30upx 0;
+		padding-top: 0;
+		background: #fff;
+		transition: all 0.3s;
+		transform: translateY(100%);
+	}
+
+	.t-box.active {
+		transform: translateY(0%);
+	}
+
+	.t-header {
+		display: flex;
+		justify-content: space-between;
+		width: 100%;
+		height: 100upx;
+		border-bottom: 1px #eee solid;
+		box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1);
+		background: #fff;
+	}
+
+	.t-header-button {
+		display: flex;
+		align-items: center;
+		width: 150upx;
+		height: 100upx;
+		font-size: 30upx;
+		color: #666;
+		padding-left: 20upx;
+	}
+
+	.t-header-button:last-child {
+		justify-content: flex-end;
+		padding-right: 20upx;
+	}
+
+	.t-mask {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.6);
+		z-index: -1;
+		transition: all 0.3s;
+		opacity: 0;
+	}
+
+	.t-mask.active {
+		opacity: 1;
+	}
+
+	.t-color__box {
+		position: relative;
+		height: 400upx;
+		background: rgb(255, 0, 0);
+		overflow: hidden;
+		box-sizing: border-box;
+		margin: 0 20upx;
+		margin-top: 20upx;
+		box-sizing: border-box;
+	}
+
+	.t-background {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
+	}
+
+	.t-color-mask {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		width: 100%;
+		height: 400upx;
+		background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
+	}
+
+	.t-pointer {
+		position: absolute;
+		bottom: -8px;
+		left: -8px;
+		z-index: 2;
+		width: 15px;
+		height: 15px;
+		border: 1px #fff solid;
+		border-radius: 50%;
+	}
+
+	.t-show-color {
+		width: 100upx;
+		height: 50upx;
+	}
+
+	.t-control__box {
+		margin-top: 50upx;
+		width: 100%;
+		display: flex;
+		padding-left: 20upx;
+		box-sizing: border-box;
+	}
+
+	.t-control__color {
+		flex-shrink: 0;
+		width: 100upx;
+		height: 100upx;
+		border-radius: 50%;
+		background-color: #fff;
+		background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
+			linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
+		background-size: 36upx 36upx;
+		background-position: 0 0, 18upx 18upx;
+		border: 1px #eee solid;
+		overflow: hidden;
+	}
+
+	.t-control__color-content {
+		width: 100%;
+		height: 100%;
+	}
+
+	.t-control-box__item {
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		width: 100%;
+		padding: 0 30upx;
+	}
+
+	.t-controller {
+		position: relative;
+		width: 100%;
+		height: 16px;
+		background-color: #fff;
+		background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
+			linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
+		background-size: 32upx 32upx;
+		background-position: 0 0, 16upx 16upx;
+	}
+
+	.t-hue {
+		width: 100%;
+		height: 100%;
+		background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
+	}
+
+	.t-transparency {
+		width: 100%;
+		height: 100%;
+		background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0));
+	}
+
+	.t-circle {
+		position: absolute;
+		/* right: -10px; */
+		top: -2px;
+		width: 20px;
+		height: 20px;
+		box-sizing: border-box;
+		border-radius: 50%;
+		background: #fff;
+		box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1);
+	}
+
+	.t-result__box {
+		margin-top: 20upx;
+		padding: 10upx;
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+	}
+
+	.t-result__item {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		padding: 10upx;
+		width: 100%;
+		box-sizing: border-box;
+	}
+
+	.t-result__box-input {
+		padding: 10upx 0;
+		width: 100%;
+		font-size: 28upx;
+		box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);
+		color: #999;
+		text-align: center;
+		background: #fff;
+	}
+
+	.t-result__box-text {
+		margin-top: 10upx;
+		font-size: 28upx;
+		line-height: 2;
+	}
+
+	.t-select {
+		flex-shrink: 0;
+		width: 150upx;
+		padding: 0 30upx;
+	}
+
+	.t-select .t-result__box-input {
+		border-radius: 10upx;
+		border: none;
+		color: #999;
+		box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.1);
+		background: #fff;
+	}
+
+	.t-select .t-result__box-input:active {
+		box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.1);
+	}
+
+	.t-alternative {
+		display: flex;
+		flex-wrap: wrap;
+		/* justify-content: space-between; */
+		width: 100%;
+		padding-right: 10upx;
+		box-sizing: border-box;
+	}
+
+	.t-alternative__item {
+		margin-left: 12upx;
+		margin-top: 10upx;
+		width: 50upx;
+		height: 50upx;
+		border-radius: 10upx;
+		background-color: #fff;
+		background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
+			linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
+		background-size: 36upx 36upx;
+		background-position: 0 0, 18upx 18upx;
+		border: 1px #eee solid;
+		overflow: hidden;
+	}
+
+	.t-alternative__item-content {
+		width: 50upx;
+		height: 50upx;
+		background: rgba(255, 0, 0, 0.5);
+	}
+
+	.t-alternative__item:active {
+		transition: all 0.3s;
+		transform: scale(1.1);
+	}
+</style>

+ 125 - 0
util/login.js

@@ -0,0 +1,125 @@
+import config from '@/common/config.js'
+import {
+	isWechat, objToUrlQuery
+} from '@/util/squ.js'
+import {
+	getCurQuery, getCurQueryAll, routeWithParams, getStorageSync, toast
+} from '@/util/squni.js'
+import {
+	post
+} from '@/util/request.js'
+import store from '@/store'
+
+export const LoginAlready = 'LoginAlready'
+export const LoginSuccess = 'LoginSuccess'
+export const LoginUnBind = 'LoginUnBind'
+export const LoginRedirect = 'LoginRedirect'
+export const LoginError = 'LoginError'
+
+// 开始登录
+export const login = (force) => {
+	return new Promise((resolve) => {
+		if (!force && getStorageSync('token')) {
+			console.debug('本地已存在token, 则认为已经登录')
+			resolve(LoginAlready)
+			return
+		}
+		
+		// #ifdef H5
+		const accessToken = getCurQuery('accessToken')
+		if (!force && accessToken) {
+			console.debug('请求参数中包含accessToken, 则认为登录成功')
+			store.commit('setToken', accessToken)
+			resolve(LoginSuccess)
+		} else if (config.appIdWxMp && isWechat()) {
+			h5WxLogin().then((res) => {
+				resolve(res)
+			})
+		} else {
+			h5Login().then((res) => {
+				resolve(res)
+			})
+		}
+		// #endif
+
+		// #ifdef MP-WEIXIN
+		// 获取进入不同平台的登录方法
+		uni.getProvider({
+			service: 'oauth',
+			success: function(res) {
+				// 当前只做微信小程序
+				if (~res.provider.indexOf('weixin')) {
+					uni.login({
+						provider: 'weixin',
+						success: function(loginRes) {
+							// 前端获取到微信登录临时码,传至后台换openId和sessionId
+							let code = loginRes.code
+							doLogin('WxMa', { code }).then((wxRes) => {
+								resolve(wxRes)
+							})
+						},
+					})
+				}
+			},
+		})
+		// #endif
+	})
+}
+
+// 请求后台登录
+export const doLogin = (loginType, params) => {
+	return new Promise((resolve) => {
+		let inviterUserId = getCurQuery('inviterUserId')
+		console.debug(`请求登录. inviterUserId=${inviterUserId || ''}`)
+		post(`/core/user/login?loginType=${loginType}`, Object.assign({}, {
+			appid: config[`appId${loginType}`],
+			inviterUserId: inviterUserId,
+			autoRegister: 'true'
+		}, params)).then((res) => {
+			console.log(res);
+			if (res.status === 'success') {
+				// 调用成功,根据是否有token判断跳转的页面
+				let data = res.data
+				store.commit('setOpenid', data.openid)
+				if (data.tokenInfo) {
+					// tokenInfo不为空,可直接进入首页
+					console.debug('登录成功. loginType=' + loginType)
+					store.commit('setToken', data.tokenInfo.tokenValue)
+					store.commit('setRefreshToken', data.refreshToken)
+					resolve(LoginSuccess)
+				} else {
+					console.debug('登录失败. loginType=' + loginType + ", openid=" + data.openid)
+					resolve(LoginUnBind)
+				}
+			} else {
+				resolve({ status: LoginError, data: res})
+			}
+		})
+	})
+}
+
+// H5微信登录(公众号)
+export const h5WxLogin = () => {
+	const code = getCurQuery('code')
+	if (!code) {
+		console.debug('微信公众号登录. 不包含code,需进行用户授权')
+		// 授权获取openid
+		const redirectUri = encodeURIComponent(window.location.href)
+		const scope = 'snsapi_base' // snsapi_base snsapi_userinfo // 静默授权 用户无感知
+		const state = 'sqbiz' // 自定义参数,回调时会带上
+		const url =
+			`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${config.appIdWxMp}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
+		window.location.href = url
+	} else {
+		console.debug('微信公众号登录. 包含code,执行登录')
+		return doLogin('WxMp', { code })
+	}
+}
+
+// H5登录
+export const h5Login = () => {
+	routeWithParams(config.loginPath)
+	return new Promise((resolve) => {
+		resolve(LoginRedirect)
+	})
+}

+ 85 - 0
util/request.js

@@ -0,0 +1,85 @@
+import {
+	login, h5Login, LoginSuccess, LoginAlready
+} from '@/util/login.js'
+import squni from '@/util/squni.js'
+import gloablConfig from '@/common/config.js'
+import store from '@/store/index.js'
+import squ from './squ.js'
+
+export const get = (url, params, config) =>
+	ajax(url, params, Object.assign({
+		method: 'GET'
+	}, config))
+
+export const post = (url, params, config) =>
+	ajax(url, params, Object.assign({
+		method: 'POST'
+	}, config))
+
+const ajax = (url, params, config) => {
+	const accessToken = squni.getStorageSync('token')
+	return new Promise((resolve, reject) => {
+		uni.request({
+			method: config.method,
+			url: (config.baseUrl || gloablConfig.baseUrl) + url,
+			header: Object.assign({
+				'Content-Type': 'application/json',
+				[gloablConfig.Authorization]: accessToken || '',
+				platform: squni.getStorageSync('platform')
+			}, config.header),
+			data: params,
+			success(resp) {
+				resolve(resp.data)
+			},
+			fail(resp) {
+				// resp.status !== 200
+				if (config.toast !== false) {
+					squni.toast('请求出错')
+				}
+				reject(resp)
+			},
+			complete(resp) {
+				// console.log(resp)
+				if (config.check !== false) {
+					check(resp, config)
+				}
+			}
+		})
+	})
+}
+
+const check = (resp, config) => {
+	let code = resp.data && Number(resp.data.code)
+	if (!code) {
+		console.error(resp)
+		code = 50000
+	}
+	const message = (resp.data && resp.data.message) || '未知错误'
+	// 如果是401则跳转到登录页面
+	if (code >= 40100 && code <= 40199) {
+		// #ifdef MP-WEIXIN
+		login(true).then(async res => {
+			if (res === LoginSuccess || res === LoginAlready) {
+				await store.dispatch('GetUserInfo')
+				let curPage = squni.getCurPage()
+				curPage.$vm.$emitReady()
+				uni.redirectTo({
+					url: '/' + curPage.route + '?' + squ.objToUrlQuery(squni.getCurQueryAll())
+				})
+			}
+		})
+		// 微信小程序静默登录
+		return
+		// #endif
+		// #ifdef H5
+		// refresh token 逻辑(先直接跳转到登录页)
+		h5Login()
+		// #endif
+	}
+	// 如果请求为非200否者默认统一处理
+	if (code !== 20000) {
+		if (config.toast !== false) {
+			squni.toast(message)
+		}
+	}
+}

+ 85 - 0
util/sqma.js

@@ -0,0 +1,85 @@
+// ============= 微信小程序工具类 =============
+/**
+ * 检查小程序是否有更新,并进行更新
+ */
+export const updateMini = () => {
+	const updateManager = uni.getUpdateManager();
+	updateManager.onCheckForUpdate(function(res) {
+		// 请求完新版本信息的回调
+		if (res.hasUpdate) {
+			// 有新版本,静默下载
+			updateManager.onUpdateReady(function(res) {
+				uni.showModal({
+					title: '更新提示',
+					content: '新版本已经准备好,是否重启应用?',
+					success(res) {
+						if (res.confirm) {
+							// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
+							updateManager.applyUpdate();
+						} else if (res.cancel) {
+							// 强制用户更新
+							uni.showModal({
+								title: '温馨提示',
+								content: '本次版本更新涉及到新的功能添加,旧版本部分功能可能无法正常使用~',
+								success(result) {
+									updateMini();
+								}
+							});
+						}
+					}
+				});
+
+			});
+		}
+	});
+
+	updateManager.onUpdateFailed(function(res) {
+		// 新的版本下载失败
+		uni.showModal({
+			title: '已经有新版本啦',
+			content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
+		})
+	});
+}
+
+/**
+ * 申请授权
+ */
+export const guideAuth = (guideMsg) => {
+	//引导用户开启权限
+	uni.showModal({
+		content: guideMsg || '我们需要您的授权,才能继续工作',
+		success: (res) => {
+			if (res.confirm) {
+				uni.openSetting({
+					success: (result) => {
+						console.log(result.authSetting);
+					}
+				});
+			}
+		}
+	});
+}
+
+/**
+ * 申请保存相册授权
+ */
+export const writePhotosAlbumAuth = (guideMsg, authCallback) => {
+	uni.authorize({
+		scope: 'scope.writePhotosAlbum',
+		success: () => {
+			// 已授权
+			authCallback && authCallback();
+		},
+		fail: () => {
+			// 拒绝授权,获取当前设置
+			uni.getSetting({
+				success: (result) => {
+					if (!result.authSetting['scope.writePhotosAlbum']) {
+						guideAuth(guideMsg)
+					}
+				}
+			});
+		}
+	})
+}

+ 121 - 0
util/squ.js

@@ -0,0 +1,121 @@
+/**
+ * 是否为微信浏览器
+ * @returns true | false
+ */
+export const isWechat = () => {
+	if (!window || !window.navigator || !window.navigator.userAgent) {
+		// 小程序没有Windows对象
+		return false
+	}
+	const ua = window.navigator.userAgent.toLowerCase()
+	return ua.match(/micromessenger/i) == 'micromessenger'
+}
+
+/**
+ * 是否为IOS浏览器
+ * @returns true | false
+ */
+export const isIOS = () => {
+	let isIphone = navigator.userAgent.includes('iPhone')
+	let isIpad = navigator.userAgent.includes('iPad')
+	return isIphone || isIpad
+}
+
+/**
+ * 日期格式化
+ * 
+ * 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
+ * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
+ * 例子:
+ * (new Date()).Format("yyyy-MM-dd h:m:s.S") ==> 2006-07-02 8:9:4.423
+ * (new Date()).Format("yyyy-M-d hh:mm:ss") ==> 2006-7-2 08:09:04.18
+ * (new Date()).Format("yyyy年MM月dd日 hh:mm") ==> 2006年7月2日 08:09
+ * (new Date()).Format("hh:mm") ==> 08:09
+ */
+export const dateFormat = (date, fmt) => {
+	let o = {
+		"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+)/.test(fmt))
+		fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
+	for (let k in o)
+		if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : ((
+			"00" + o[k]).substr(("" + o[k]).length)));
+	return fmt;
+}
+
+/**
+ * 获取url参数. uni-app可参考squni.js中的 getCurQuery
+ */
+export const getUrlQuery = (name, href) => {
+	// return (
+	// 	decodeURIComponent(
+	// 		(new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(href) || [, ''])[1].replace(
+	// 			/\+/g, '%20')
+	// 	) || null
+	// )
+	href = href || window.location.href
+	let urlStr = href.split('?')[1]
+	const urlSearchParams = new URLSearchParams(urlStr)
+	const params = Object.fromEntries(urlSearchParams.entries())
+	Object.keys(params).forEach(key => params[key] = decodeURIComponent(params[key]))
+	return name ? params[name] : params
+}
+
+/**
+ * Map转成url参数: k1=v1&k2=v2
+ */
+export const objToUrlQuery = (obj, ignoreFields) => {
+	if (!ignoreFields) {
+		ignoreFields = []
+	}
+    return Object.keys(obj)
+        .filter(key => obj[key] != null && ignoreFields.indexOf(key) === -1)
+        .map(key => key + '=' + encodeURIComponent(obj[key])).join('&')
+}
+
+/**
+ * 定时执行,需要再回调中清除定时
+ */
+export const interval = (callback, timer = null, interval = 1000) => {
+	//清除原定时器
+	clearInterval(timer)
+	//开启定时器定时
+	timer = setInterval(() => {
+		callback && callback()
+	}, interval)
+	return timer
+}
+
+/**
+ * 生成uuid. eg: bb8263ae-ce73-4e3b-82de-67c105bc1a4b
+ */
+export const uuid = (removeMidline) => {
+	let s = [];
+	let hexDigits = "0123456789abcdef";
+	for (let i = 0; i < 36; i++) {
+		s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
+	}
+	s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
+	s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
+	s[8] = s[13] = s[18] = s[23] = "-";
+
+	let uuid = s.join("");
+	return removeMidline ? uuid.replace(/\-/g, '') : uuid;
+}
+
+export default {
+	isWechat,
+	isIOS,
+	dateFormat,
+	getUrlQuery,
+	objToUrlQuery,
+	interval,
+	uuid
+}

+ 222 - 0
util/squni.js

@@ -0,0 +1,222 @@
+import gloablConfig from '@/common/config.js'
+import { getUrlQuery, objToUrlQuery } from '@/util/squ.js'
+const globalKeyPrefix = gloablConfig.key ? gloablConfig.key + '-' : 'squni-'
+
+/**
+ * 缓存全局APP实例对象(vm)
+ */
+let APP = null
+
+// ====== 存储
+// uni.getStorageInfo uni.getStorageInfoSync uni.clearStorageSync 略
+export const setStorage = config =>
+	uni.setStorage({
+		key: globalKeyPrefix + config.key,
+		data: config.data,
+		success: config.success,
+		fail: config.fail,
+		complete: config.complete
+	})
+
+export const setStorageSync = (key, value) => uni.setStorageSync(globalKeyPrefix + key, value)
+
+export const getStorage = config =>
+	uni.getStorage({
+		key: globalKeyPrefix + config.key,
+		success: config.success,
+		fail: config.fail,
+		complete: config.complete
+	})
+
+export const getStorageSync = key => uni.getStorageSync(globalKeyPrefix + key)
+
+export const removeStorage = config =>
+	uni.removeStorage({
+		key: globalKeyPrefix + config.key,
+		success: config.success,
+		fail: config.fail,
+		complete: config.complete
+	})
+
+export const removeStorageSync = key => uni.removeStorageSync(globalKeyPrefix + key)
+
+// ====== 路由
+export const setApp = (vm) => {
+	APP = vm
+}
+
+export const getApp = () => {
+	return APP
+}
+
+export const switchTab = (path, config) => {
+	uni.switchTab({
+		url: path,
+		success: config && config.success,
+		fail: config && config.fail,
+		complete: config && config.complete
+	})
+}
+
+export const navigateTo = (path, config) => {
+	uni.navigateTo({
+		url: path,
+		animationType: config && config.animationType,
+		animationDuration: config && config.animationDuration,
+		events: config && config.events,
+		success: config && config.success,
+		fail: config && config.fail,
+		complete: config && config.complete
+	})
+}
+
+export const navigateBack = (vm) => {
+	// 从分享打开的,返回首页. 可引入wx-share.js
+	if (vm.share == true) {
+		redirectHome()
+	} else {
+		// 不是从分享打开的,返回上一页
+		uni.navigateBack({
+			delta: 1,
+		})
+	}
+}
+
+export const redirectTo = (path, config) => {
+	uni.redirectTo({
+		url: path,
+		success: config && config.success,
+		fail: config && config.fail,
+		complete: config && config.complete
+	})
+}
+
+export const redirectHome = () => {
+	if (gloablConfig.indexType === 'Tab') {
+		uni.switchTab({
+			url: gloablConfig.indexPath
+		})
+	} else {
+		uni.redirectTo({
+			url: gloablConfig.indexPath
+		})
+	}
+}
+
+export const routeWithParams = (path, method) => {
+	method = method || 'redirectTo'
+	const params = getCurQueryAll()
+	const _originHref = params['_originHref'] || window.location.href
+	params['_originHref'] = _originHref
+	uni[method]({
+		url: path + '?' + objToUrlQuery(params)
+	})
+}
+
+/**
+ * 获取当前页面请求路径
+ * 返回: { $vm, route, options, ... }
+ */
+export const getCurPage = () => {
+	// uni-app内置函数: https://uniapp.dcloud.net.cn/api/window/window.html#getcurrentpages
+	const pages = getCurrentPages()
+	return (pages && pages.length > 0) ? pages[pages.length - 1] : {}
+}
+/**
+ * 获取上个页面
+ * 返回: { $vm, route, options, ... } | null
+ */
+export const getPrePage = () => {
+	const pages = getCurrentPages()
+	return pages && pages.length > 1 ? pages[pages.length - 2] : null
+}
+/**
+ * 获取当前页面请求路径所有参数
+ */
+export const getCurQueryAll = () => {
+	const curPage = getCurPage()
+	// 在微信小程序或是app中,通过curPage.options;如果是H5,则需要curPage.$route.query
+	let params = curPage.options || (curPage.$route && curPage.$route.query)
+	// 有时候H5会出现 curPage.options = {}
+	if (params == null
+		// #ifdef H5
+		|| Object.getOwnPropertyNames(params).length === 0
+		// #endif
+	) {
+		params = getUrlQuery()
+	}
+	return params
+}
+/**
+ * 获取当前页面请求路径参数. 可参考 squ.js 中的 getUrlQuery
+ */
+export const getCurQuery = (name) => {
+	const query = getCurQueryAll()
+	return query ? query[name] : null
+}
+
+// ====== 其他
+export const copy = (value, msg) => {
+	uni.setClipboardData({
+		data: value,
+		success: () => {
+			uni.showToast({
+				title: msg || '复制成功',
+				duration: 800
+			})
+		}
+	})
+}
+
+/**
+ * 信息提示更简洁
+ */
+export const toast = (message, icon, config) => {
+	config = config || {}
+	config.title = message
+	config.icon = icon || 'none'
+	uni.showToast(config)
+}
+
+/**
+ * 滚动页面到底部。如聊天时
+ */
+export const scrollToBottom = () => {
+	// 要加点延迟, 不然有可能不生效
+	setTimeout(() => {
+		uni.pageScrollTo({
+			scrollTop: 999999,
+			duration: 0
+		})
+	}, 50)
+}
+
+export const shareWeb = () => {
+	let url = window.location.protocol + '//' + window.location.host + window.location.pathname
+	const query = getCurQueryAll()
+	query.inviterUserId = getStorageSync('userId')
+	url = url + '?' + objToUrlQuery(query)
+	copy(url, '已复制分享链接,快去分享吧~')
+}
+
+export default {
+	globalKeyPrefix,
+	setStorageSync,
+	getStorageSync,
+	removeStorageSync,
+	setApp,
+	getApp,
+	switchTab,
+	navigateTo,
+	navigateBack,
+	redirectTo,
+	redirectHome,
+	routeWithParams,
+	getCurPage,
+	getPrePage,
+	getCurQuery,
+	getCurQueryAll,
+	copy,
+	toast,
+	shareWeb
+}

+ 90 - 0
util/websocket.js

@@ -0,0 +1,90 @@
+const _WEBSOCKET = {
+	//是否打开连接
+	isOpen: false,
+	//连接socket. https://ask.dcloud.net.cn/question/74505
+	connectSocket(url, recvMsgFunc = null, successFunc = null, errorFunc = null) {
+		try {
+			let that = this
+			//监听socket连接
+			uni.onSocketOpen((res) => {
+				console.log('WebSocket连接已打开!', url, res)
+				that.isOpen = true
+				successFunc && successFunc(res)
+			})
+			//监听socket连接失败
+			uni.onSocketError((res) => {
+				that.isOpen = false
+				console.log('WebSocket连接打开失败,请检查!', url, res)
+				errorFunc && errorFunc(res)
+			})
+			//监听收到消息
+			uni.onSocketMessage((res) => {
+				console.log('收到服务器内容:' + res.data, url)
+				//可结合服务端@OnOpen返回消息OK来进行判断是否已经打开连接
+				if(res.data === 'OK') {
+					console.log('WebSocket连接已打开!', url, res)
+					that.isOpen = true
+					successFunc && successFunc(res)
+					return
+				}
+				recvMsgFunc && recvMsgFunc(res.data)
+			})
+			//监听socket关闭
+			uni.onSocketClose((res) => {
+				console.log('WebSocket 已关闭!', url)
+				that.isOpen = false
+			})
+			
+			//连接socket
+			setTimeout(() => {
+				// 防止还没监听到onSocketOpen就已经完成连接
+				uni.connectSocket({
+					url,
+					success() {
+						if (that.isOpen) {
+							console.log('Websocket连接已打开,刷新状态完成!', url)
+						} else {
+							console.log('Websocket初始化成功!正在监听连接打开状态...', url)
+						}
+					}
+				})
+			}, 100)
+		} catch (error) {
+			console.log('err:' + error)
+		}
+	},
+	//发送消息
+	sendMessage(msg = '', successFunc = null, errorFunc = null) {
+		if (!msg) {
+			console.log('未传消息!')
+			errorFunc && errorFunc('未传消息!')
+			return
+		}
+		if (!this.isOpen) {
+			console.log('连接未打开!')
+			errorFunc && errorFunc('连接未打开!')
+			return
+		}
+		uni.sendSocketMessage({
+			data: msg,
+			success(res) {
+				console.log('消息发送成功!', msg)
+				successFunc && successFunc(res)
+			},
+			fail(err) {
+				console.log('消息发送失败!', msg)
+				errorFunc && errorFunc(err)
+			}
+		})
+	},
+	//关闭连接
+	closeSocket() {
+		if (!this.isOpen) {
+			return
+		}
+		//关闭socket连接
+		uni.closeSocket()
+	}
+}
+
+export default _WEBSOCKET

+ 74 - 0
util/wx-share.js

@@ -0,0 +1,74 @@
+import {
+	objToUrlQuery
+} from '@/util/squ.js'
+export default {
+	data() {
+		return {
+			shareTitle: 'AirSmartChat聊天',
+			// 分享的连接
+			sharePath: '',
+			// 分享连接扩展参数,如果定义了sharePath则此参数无效
+			shareQuery: {},
+			shareUrl: '/static/logo-100.png',
+			// 进入当前页面是否是通过点击分享连接进入
+			share: false
+		}
+	},
+	onLoad(options) {
+		// 页面初次加载判断有没有携带分享参数,从而解决分享页面无法返回问题
+		if (options && (options.share || options.share == 'true')) {
+			this.share = true
+		}
+		try {
+			wx.showShareMenu({
+				withShareTicket: true,
+				menus: ["shareAppMessage", "shareTimeline"]
+			})
+		} catch(error) {
+			console.error(error)
+		}
+	},
+	// 分享朋友圈和微信好友函数和 onLoad 等生命周期函数同级
+	onShareAppMessage(res) {
+		let path = this.getSharePath()
+		if (res.from === 'button') {
+			// 为自定义按钮分享. 这块需要传参,不然链接地址进去获取不到数据
+			return {
+				title: this.shareTitle,
+				path: path,
+				imageUrl: this.shareUrl
+			}
+		}
+		if (res.from === 'menu') {
+			return {
+				title: this.shareTitle,
+				path: path,
+				imageUrl: this.shareUrl
+			}
+		}
+	},
+	// 分享到朋友圈
+	onShareTimeline(res) {
+		let path = this.getSharePath()
+		return {
+			title: this.shareTitle,
+			path: path,
+			imageUrl: this.shareUrl
+		}
+	},
+	methods: {
+		getSharePath() {
+			if (this.sharePath) {
+				return this.sharePath
+			}
+			let sharePath = `/${this.$scope.route}?share=true&inviterUserId=${this.$store.getters.userId}&`
+			if (this.shareQuery) {
+				sharePath += objToUrlQuery(this.shareQuery)
+			}
+			return sharePath
+		},
+		navigateBack () {
+			this.$squni.navigateBack(this)
+		},
+	}
+}

+ 10 - 0
vue.config.js

@@ -0,0 +1,10 @@
+module.exports = {
+    // 配置路径别名
+    configureWebpack: {
+        devServer: {
+            // 调试时允许内网穿透,让外网的人访问到本地调试的H5页面
+            disableHostCheck: true
+        }
+    }
+    // productionSourceMap: false,
+}