├── .gitignore ├── README.md ├── index.html ├── package.json ├── src ├── App.vue ├── api │ ├── README.md │ └── common.ts ├── components │ ├── BookItem.vue │ ├── BookTip.vue │ ├── Form │ │ ├── README.md │ │ ├── TheForm.vue │ │ ├── TheFormItem.vue │ │ └── hooks.ts │ ├── Icons.vue │ ├── LoadMoreTip │ │ ├── README.md │ │ └── index.vue │ ├── Picker │ │ ├── Date.vue │ │ ├── README.md │ │ ├── index.vue │ │ └── picker.scss │ ├── README.md │ ├── TheButton.vue │ ├── TheFooter.vue │ └── Upload │ │ ├── Image.vue │ │ └── README.md ├── env.d.ts ├── hooks │ ├── README.md │ ├── book.ts │ ├── index.ts │ └── loadMore.ts ├── main.ts ├── manifest.json ├── pages.json ├── pages │ ├── README.md │ ├── book.vue │ ├── button.vue │ ├── load-more-list.vue │ └── tabBar │ │ ├── home.vue │ │ └── personal.vue ├── static │ ├── README.md │ ├── arrow-right.png │ ├── default_head.png │ ├── home_off.png │ ├── home_on.png │ ├── logo.png │ ├── logo_wx.png │ ├── logo_zfb.png │ ├── none_data.png │ ├── personal_off.png │ └── personal_on.png ├── store │ ├── AppOption.ts │ ├── README.md │ ├── User.ts │ └── index.ts ├── styles │ ├── README.md │ ├── index.scss │ └── loading.scss ├── type.d.ts ├── types │ ├── README.md │ ├── index.ts │ └── user.ts ├── uni.scss └── utils │ ├── Config.ts │ ├── Control.ts │ ├── README.md │ ├── icon.ts │ ├── index.ts │ └── request.ts ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | unpackage/ 4 | dist/ 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .project 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw* 24 | 25 | package-lock.json 26 | yarn.lock 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于 vue 实现的小说 App 阅读器 2 | 3 | > 由于之前 vue2 中 uni-app 某个版本依赖不再维护,所以当前项目更新为 vue3 版本 4 | 5 | [预览地址](https://huangjingsheng.gitee.io/hjs/reader-vue) 6 | 7 | [掘金介绍](https://juejin.cn/post/6844904127898583048) 8 | 9 | [模板引用](https://github.com/Hansen-hjs/uni-app-template) 10 | 11 | ## 初始化项目 12 | ``` 13 | npm install 14 | ``` 15 | 16 | ### 运行项目,APP 中连接手机调试需要借助 HbuildX 17 | ``` 18 | npm run dev 19 | ``` 20 | 21 | ### 打包项目 22 | ``` 23 | npm run build 24 | ``` 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uni-app-vue3", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev:app": "uni -p app", 6 | "dev:custom": "uni -p", 7 | "dev": "uni", 8 | "dev:h5:ssr": "uni --ssr", 9 | "dev:mp-alipay": "uni -p mp-alipay", 10 | "dev:mp-baidu": "uni -p mp-baidu", 11 | "dev:mp-kuaishou": "uni -p mp-kuaishou", 12 | "dev:mp-lark": "uni -p mp-lark", 13 | "dev:mp-qq": "uni -p mp-qq", 14 | "dev:mp-toutiao": "uni -p mp-toutiao", 15 | "dev:mp-weixin": "uni -p mp-weixin", 16 | "dev:quickapp-webview": "uni -p quickapp-webview", 17 | "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", 18 | "dev:quickapp-webview-union": "uni -p quickapp-webview-union", 19 | "build:app": "uni build -p app", 20 | "build:custom": "uni build -p", 21 | "build": "uni build", 22 | "build:h5:ssr": "uni build --ssr", 23 | "build:mp-alipay": "uni build -p mp-alipay", 24 | "build:mp-baidu": "uni build -p mp-baidu", 25 | "build:mp-kuaishou": "uni build -p mp-kuaishou", 26 | "build:mp-lark": "uni build -p mp-lark", 27 | "build:mp-qq": "uni build -p mp-qq", 28 | "build:mp-toutiao": "uni build -p mp-toutiao", 29 | "build:mp-weixin": "uni build -p mp-weixin", 30 | "build:quickapp-webview": "uni build -p quickapp-webview", 31 | "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", 32 | "build:quickapp-webview-union": "uni build -p quickapp-webview-union" 33 | }, 34 | "dependencies": { 35 | "@dcloudio/uni-app": "3.0.0-alpha-3061020221121002", 36 | "@dcloudio/uni-app-plus": "3.0.0-alpha-3061020221121002", 37 | "@dcloudio/uni-components": "3.0.0-alpha-3061020221121002", 38 | "@dcloudio/uni-h5": "3.0.0-alpha-3061020221121002", 39 | "@dcloudio/uni-mp-alipay": "3.0.0-alpha-3061020221121002", 40 | "@dcloudio/uni-mp-baidu": "3.0.0-alpha-3061020221121002", 41 | "@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-3061020221121002", 42 | "@dcloudio/uni-mp-lark": "3.0.0-alpha-3061020221121002", 43 | "@dcloudio/uni-mp-qq": "3.0.0-alpha-3061020221121002", 44 | "@dcloudio/uni-mp-toutiao": "3.0.0-alpha-3061020221121002", 45 | "@dcloudio/uni-mp-weixin": "3.0.0-alpha-3061020221121002", 46 | "@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3061020221121002", 47 | "vue": "3.2.41" 48 | }, 49 | "devDependencies": { 50 | "@dcloudio/types": "3.0.16", 51 | "@dcloudio/uni-automator": "3.0.0-alpha-3061020221121002", 52 | "@dcloudio/uni-cli-shared": "3.0.0-alpha-3061020221121002", 53 | "@dcloudio/vite-plugin-uni": "3.0.0-alpha-3061020221121002", 54 | "@types/node": "17.0.40", 55 | "sass": "1.52.2", 56 | "typescript": "4.8.3", 57 | "vite": "3.1.8" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 32 | -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | # 接口模块 2 | 3 | ## 正常 GET 和 POST 请求 4 | 5 | ```ts 6 | /** 7 | * 通过`id`获取用户信息 8 | * @param id 9 | */ 10 | export function getUserInfo(id: number | string) { 11 | return request('GET', '/getUserInfoById', { id }) 12 | } 13 | 14 | /** 15 | * 普通`post`json请求 16 | * @param params 17 | */ 18 | export function saveBannerInfo(params: { img: string, date: string, sort: number }) { 19 | return request('POST', '/saveBannerInfo', params) 20 | } 21 | ``` 22 | 23 | ## 表单请求 24 | 25 | ```ts 26 | import { jsonToFormData } from '@/utils'; 27 | 28 | /** 29 | * `post`表单请求 30 | * @param params 31 | */ 32 | export function saveUserInfo(params: { account: string, password: string }) { 33 | return request('POST', '/saveUserInfo', jsonToFormData(params), { 34 | headers: { 35 | 'codeMode': 'form' 36 | } 37 | }) 38 | } 39 | ``` 40 | 41 | ## 请求获取文件流 42 | 43 | ```ts 44 | /** 45 | * 响应结果类型为`blob` 46 | * @param params 47 | */ 48 | export function getBlob(params: { name: string }) { 49 | return request('GET', '/getXlsxBlob', params, { 50 | responseType: 'blob' 51 | }) 52 | } 53 | ``` -------------------------------------------------------------------------------- /src/api/common.ts: -------------------------------------------------------------------------------- 1 | import { createBookListData } from '@/hooks/book'; 2 | import { randomText, ranInt } from '@/utils'; 3 | import request from '@/utils/request'; 4 | 5 | // ============================= 常用接口模块 ============================= 6 | 7 | /** 8 | * 用户登录 9 | * @param form 10 | */ 11 | export function login(form: { account: string | number, password: string | number }) { 12 | return request("POST", "/login", form); 13 | } 14 | 15 | /** 16 | * 查询用户类型 17 | * @param value 用户标识 18 | */ 19 | export function searchUserType(value: "admin" | "vip" | "normal") { 20 | return request("POST", "/user/searchType", { type: value }); 21 | } 22 | 23 | const images = [ 24 | "https://muse-ui.org/img/img1.35d144b4.png", 25 | "https://muse-ui.org/img/img2.9bd96df4.png", 26 | "https://muse-ui.org/img/img3.6e264e66.png", 27 | "https://muse-ui.org/img/sun.a646a52d.jpg", 28 | "https://muse-ui.org/img/breakfast.f1098290.jpg" 29 | ] 30 | 31 | const testList = new Array(52).fill(0).map((_, index) => { 32 | return { 33 | id: index + 1, 34 | name: randomText(6, 30), 35 | img: images[ranInt(0, images.length - 1)] 36 | } 37 | }) 38 | 39 | /** 40 | * 模拟请求数据 41 | * @param params 42 | */ 43 | export function getTestList(params: PageInfo & { id?: number, keyword?: string }) { 44 | const delay = ranInt(200, 2000); 45 | 46 | const result: ApiResult = { 47 | code: -1, 48 | data: { 49 | currentPage: params.currentPage, 50 | pageSize: params.pageSize, 51 | list: [], 52 | total: testList.length 53 | }, 54 | msg: "" 55 | } 56 | 57 | return new Promise>(function (resolve, reject) { 58 | setTimeout(function () { 59 | if (delay > 1500) { 60 | result.msg = "接口查询超时" 61 | resolve(result); 62 | } else { 63 | result.msg = "success"; 64 | result.code = 1; 65 | const index = (params.currentPage - 1) * params.pageSize; 66 | let list = [...testList].splice(index, params.pageSize); 67 | if (params.keyword) { 68 | list = list.map(item => { 69 | return { 70 | id: item.id, 71 | name: `${params.keyword}:${item.name}`, 72 | img: item.img 73 | } 74 | }); 75 | } 76 | result.data.list = list; 77 | resolve(result); 78 | } 79 | }, delay) 80 | }) 81 | } 82 | 83 | /** 84 | * 获取小说列表 85 | * @param params 86 | */ 87 | export function getBookList(params: PageInfo) { 88 | const delay = ranInt(200, 1000); 89 | const result: ApiResult = { 90 | code: 1, 91 | data: { 92 | currentPage: params.currentPage, 93 | pageSize: params.pageSize, 94 | list: [] 95 | }, 96 | msg: "" 97 | } 98 | return new Promise>(function (resolve, reject) { 99 | setTimeout(function () { 100 | if (delay > 900 && params.currentPage !== 0) { 101 | result.msg = "接口查询超时" 102 | result.code = 0; 103 | resolve(result); 104 | } else { 105 | const total = params.currentPage < 6 ? params.pageSize : ranInt(2, params.pageSize - 2); 106 | result.msg = "success" 107 | result.code = 1; 108 | result.data.list = createBookListData(params.currentPage * params.pageSize, total); 109 | resolve(result); 110 | } 111 | }, delay) 112 | }) 113 | } -------------------------------------------------------------------------------- /src/components/BookItem.vue: -------------------------------------------------------------------------------- 1 | 46 | 74 | -------------------------------------------------------------------------------- /src/components/BookTip.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/Form/README.md: -------------------------------------------------------------------------------- 1 | # from 表单组件 2 | 3 | **demo:[预览地址](http://huangjingsheng.gitee.io/hjs/uni-app/#/pages/form)** 4 | 5 | 这里只实现两个核心组件``和``;因为移动端样式变化比较灵活,如果把所有表单组件都封装成`element-ui`或者`vant-ui`这类型一体库的话,导致很多无用代码和性能开销,所以这里只提供必需的功能组件,其他表单组件根据实际情况定义,保证高度灵活性。 6 | 7 | ## `` 8 | 9 | 参数说明: 10 | 11 | | props | 类型 | 是否必选 | 说明 | 12 | | --- | --- | --- | --- | 13 | | model | object | 是 | 表单绑定的值(表单数据) | 14 | | rules | object | 否 | 表单验证数据(和`element-ui`一致) | 15 | | labelWidth | string | 否 | 表单字段宽度,`px`、`rpx`、`%` | 16 | | labelPosition | string: `left`,`right`,`top` | 否 | 表单字段排版,默认`left` | 17 | | border | boolean | 否 | 是否需要显示底部边框,默认`false` | 18 | | validateScroll | boolean | 否 | 是否需要在验证时,滚动到不通过的位置,默认`true`,短表单时建议关闭,长表单开启 | 19 | 20 | 和`element-ui`差异: 21 | - `props.rules`移除了`validator`,增加了`reg`正则匹配:注意:因为微信小程序的一些特殊机制,导致传参类型会把非 `number|string|object` 的类型过滤掉,所以这里在写正则的时候,在末尾加上`.toString()`即可; 22 | - `props.rules`移除了`change`触发条件,组件内部做了智能触发机制; 23 | 24 | 25 | 事件/方法说明: 26 | 27 | | 方法 | 参数 | 参数说明 | 说明 | 28 | | --- | --- | --- | --- | 29 | | validate(callback(...)) | callback(isValid, rules) | 有两个回调参数,和`element-ui`一致 | 表单验证 | 30 | | validateField(prop, callback(...)) | `prop`是指定验证的键值,`callback`和上面一致 | 有两个回调参数,和`element-ui`一致 | 指定验证某个字段 | 31 | | resetFields() | - | - | 移除所有校验 | 32 | | resetField(prop) | `prop`是指定移除验证的键值 | - | 移除指定校验 | 33 | 34 | ## `` 35 | 36 | 参数说明: 37 | 38 | | props | 类型 | 是否必选 | 说明 | 39 | | --- | --- | --- | --- | 40 | | prop | string | 否 | 表单对象`key`字段 | 41 | | rules | array | 否 | 这里是上面`rules`对象里面的数组,类型一致;优先级高于父组件 | 42 | | label | string | 否 | 表单展示字段 | 43 | | labelWidth | string | 否 | 表单字段宽度,`px`、`rpx`、`%`;优先级高于父组件 | 44 | | labelPosition | string: `left`,`right`,`top` | 否 | 表单字段排版,默认`left`;优先级高于父组件 | 45 | | border | boolean | 否 | 是否需要显示底部边框,默认`false`;优先级高于父组件 | 46 | 47 | ## 使用示例 48 | 49 | ```html 50 | 69 | 70 | 155 | ``` 156 | -------------------------------------------------------------------------------- /src/components/Form/TheForm.vue: -------------------------------------------------------------------------------- 1 | 6 | 14 | 237 | -------------------------------------------------------------------------------- /src/components/Form/TheFormItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 23 | 211 | -------------------------------------------------------------------------------- /src/components/Form/hooks.ts: -------------------------------------------------------------------------------- 1 | import { PropType } from "vue"; 2 | import { LabelPosition } from "@/types"; 3 | 4 | /** 5 | * 表单相同`props` 6 | * @param isItem 是否`item`使用 7 | */ 8 | export function useFormProps(isItem?: boolean) { 9 | return { 10 | /** 表单字段宽度,这里使用字符串,因为可能是`px`或者`rpx` */ 11 | labelWidth: { 12 | type: String, 13 | default: "" 14 | }, 15 | /** 表单字段排版 */ 16 | labelPosition: { 17 | type: String as PropType, 18 | default: isItem ? "" : "left" 19 | }, 20 | /** 是否需要显示底部边框 */ 21 | border: { 22 | type: [Boolean, String], 23 | default: isItem ? "-" : false, // 微信小程序抽风会把空字符串转成 boolean 所以这里随便给个字符串 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Icons.vue: -------------------------------------------------------------------------------- 1 | 4 | 18 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/LoadMoreTip/README.md: -------------------------------------------------------------------------------- 1 | # 触底加载更多组件 2 | 3 | 使用示例 4 | 5 | ```html 6 | 12 | 53 | ``` 54 | -------------------------------------------------------------------------------- /src/components/LoadMoreTip/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 37 | 66 | -------------------------------------------------------------------------------- /src/components/Picker/Date.vue: -------------------------------------------------------------------------------- 1 | 26 | 59 | 217 | 220 | -------------------------------------------------------------------------------- /src/components/Picker/README.md: -------------------------------------------------------------------------------- 1 | # 弹出层选择器 2 | 3 | 参数说明: 4 | 5 | | props | 类型 | 是否必选 | 说明 | 6 | | --- | --- | --- | --- | 7 | | show | boolean | 是 | 是否显示弹出选择器 | 8 | | title | string,number | 否 | 选择器标题 | 9 | | list | `Array` | 是 | 选择器数据,最多显示三层 | 10 | | column | 列数:0-3 | 否 | 指定选择器列数(优先级最高),默认是`0`,即自动根据`list`的层级动态生成 | 11 | | pickerId | string,number | 否 | `change`事件携带的`id`,一个页面有多个组件的时候用来区分用 | 12 | 13 | 事件说明: 14 | 15 | | 方法件名 | 说明 | 16 | | --- | --- | 17 | | `setIndexs(indexs: Array)` | 设置当前选择器选中的位置,传入的参数则为每一列的索引位置 | 18 | 19 | `list`注意事项说明:小程序端无法多列动态自适应,需要使用`column`指定列数;原因是组件用了`v-show`在小程序端有`bug`,改用`v-if`可以实现动态层级,不过会有些展示上的行为小瑕疵,可以自行修改测试看具体效果就知道了,这里推荐使用`v-show`+指定列数`column`在小程序端的使用。 20 | 21 | `PickerSelectItem`说明: 22 | 23 | ```ts 24 | /** 选择器`item`数据 */ 25 | interface PickerSelectItem { 26 | /** 展示字段 */ 27 | label: string 28 | /** 对应的值 */ 29 | value: T 30 | /** 31 | * 下级数据 32 | * @description 最多三层,选择器栏目数根据当前下级动态显示 33 | */ 34 | children?: Array> 35 | /** 其他携带的值 */ 36 | [key: string]: any 37 | } 38 | ``` 39 | 40 | **使用示例** 41 | 42 | ```html 43 | 51 | 87 | ``` 88 | 89 | ## 日期选择组件 90 | 91 | | props | 类型 | 是否必选 | 说明 | 92 | | --- | --- | --- | --- | 93 | | show | boolean | 是 | 是否显示弹出选择器 | 94 | | title | string,number | 否 | 选择器标题 | 95 | | type | string: `Y-M-D`,`Y-M`,`Y` | 否,默认`Y-M-D` | 分别对应年月日 | 96 | | value | string | 否 | 初始化选中值,格式和`type`对应 | 97 | | startDate | string | 否 | 开始日期,格式和`type`对应 | 98 | | endDate | string | 否 | 结束日期,格式和`type`对应 | 99 | 100 | 细心看过`index.vue`的可以发现,其实可以二次封装一下日期数据就可以复用上面的组件,这里我单独抽出来的理由是:因为上面的组件需要依赖固定的上下级数据,如果日期选择两个年份差较大的话,会导致庞大的日期数据;另外日期的范围逻辑也跟其他的选项数据不太一样,所以这里单独抽出来性能会好很多。 101 | 102 | **使用示例** 103 | 104 | ```html 105 | 116 | 138 | ``` 139 | -------------------------------------------------------------------------------- /src/components/Picker/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 35 | 152 | 155 | -------------------------------------------------------------------------------- /src/components/Picker/picker.scss: -------------------------------------------------------------------------------- 1 | @import "../../uni.scss"; 2 | 3 | $duration: 0.3s all; 4 | 5 | .the-picker { 6 | width: 100%; 7 | height: 100%; 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | background-color: rgba(0, 0, 0, 0.45); 12 | z-index: 999; 13 | overflow: hidden; 14 | flex-direction: column; 15 | -webkit-flex-direction: column; 16 | transition: $duration; 17 | opacity: 0; 18 | visibility: hidden; 19 | 20 | .picker-option { 21 | width: 100%; 22 | padding: 12rpx 0; 23 | 24 | .picker-title { 25 | font-size: 32rpx; 26 | color: #555; 27 | font-weight: 500; 28 | text-align: center; 29 | line-height: 1; 30 | } 31 | 32 | .btn { 33 | font-size: 30rpx; 34 | width: 160rpx; 35 | line-height: 80rpx; 36 | color: #999; 37 | text-align: center; 38 | } 39 | 40 | .confirm { 41 | color: $blue; 42 | } 43 | } 44 | 45 | .picker-content { 46 | background-color: #fff; 47 | transition: $duration; 48 | transform: translate3d(0, 100%, 0); 49 | 50 | .picker-view { 51 | width: 100%; 52 | height: 236px; 53 | 54 | .picker-item { 55 | text-align: center; 56 | line-height: 36px; 57 | font-size: 16px; 58 | } 59 | } 60 | } 61 | } 62 | 63 | .the-picker-show { 64 | opacity: 1; 65 | visibility: visible; 66 | 67 | .picker-content { 68 | transform: translate3d(0, 0%, 0); 69 | } 70 | } -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # 通用组件目录 2 | 3 | 局部组件不要写在这里 4 | -------------------------------------------------------------------------------- /src/components/TheButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 47 | -------------------------------------------------------------------------------- /src/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 33 | -------------------------------------------------------------------------------- /src/components/Upload/Image.vue: -------------------------------------------------------------------------------- 1 | 29 | 34 | 117 | -------------------------------------------------------------------------------- /src/components/Upload/README.md: -------------------------------------------------------------------------------- 1 | # 上传组件 2 | 3 | ## 上传图片组件 4 | 5 | 使用示例 6 | 7 | ```html 8 | 14 | 27 | ``` 28 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue" 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/README.md: -------------------------------------------------------------------------------- 1 | # 通用`hooks`目录 2 | -------------------------------------------------------------------------------- /src/hooks/book.ts: -------------------------------------------------------------------------------- 1 | import { randomText, ranInt } from "@/utils"; 2 | 3 | /** 书本id */ 4 | let bookId = 0; 5 | 6 | /** 图片列表 */ 7 | const images = [ 8 | "https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eefa59a729d34f4fa470d9c53be410c8~tplv-k3u1fbpfcp-watermark.image", 9 | "https://img.yzcdn.cn/vant/ipad.jpeg" 10 | ]; 11 | 12 | /** 13 | * 小说数据类型 14 | */ 15 | export interface BookInfo { 16 | id: number 17 | index: number 18 | /** 封面图 */ 19 | thumb: string 20 | /** 小说名 */ 21 | name: string 22 | /** 描述 */ 23 | info: string 24 | /** 评分 */ 25 | score: string 26 | /** 标签 */ 27 | label: string 28 | /** 小说热度 */ 29 | value: string 30 | } 31 | 32 | /** 33 | * 创建单个小说数据 34 | */ 35 | export function createBookInfo() { 36 | return { 37 | id: 0, 38 | index: 0, 39 | thumb: "", 40 | name: "", 41 | info: "", 42 | score: "", 43 | label: "", 44 | value: "", 45 | } as BookInfo 46 | } 47 | 48 | /** 49 | * 创建小说列表数据 50 | * @param index 索引开始 51 | * @param total 列表总数 52 | */ 53 | export function createBookListData(index: number = 0, total: number = 8) { 54 | const list: Array = []; 55 | const titleStr = "小说名小说名小说名"; 56 | const decStr = "小说描述小说描述小说描述小说描述"; 57 | for (let i = index; i < index + total; i++) { 58 | bookId++; 59 | list.push({ 60 | id: bookId, 61 | index: i, 62 | thumb: images[Math.floor(Math.random() * images.length)], 63 | name: `小说${bookId} ${titleStr.slice(Math.floor(Math.random() * titleStr.length))}`, 64 | info: `小说描述${decStr.slice(Math.floor(Math.random() * decStr.length))}`, 65 | score: `${(Math.floor(Math.random() * 100) / 10).toFixed(1)}`, 66 | label: "都市·穿越·完结·29万人在读", 67 | value: "785万热度", 68 | }); 69 | } 70 | return list; 71 | } 72 | 73 | /** 段落分隔符 */ 74 | export const paragraphSeparate = "

"; 75 | 76 | /** 77 | * 创建阅读器内容信息(一个章节) 78 | * @param index 79 | */ 80 | export function createBookContent(index: number) { 81 | function outputText() { 82 | const total = ranInt(1, 5); 83 | let text = ""; 84 | for (let i = 0; i < total; i++) { 85 | text += randomText(2, 10); 86 | if (i == total - 1) { 87 | text += "。" + paragraphSeparate; 88 | } else { 89 | text += ","; 90 | } 91 | } 92 | return text; 93 | } 94 | function outputParagraph() { 95 | let paragraph = ""; 96 | const total = ranInt(50, 100); 97 | for (let i = 0; i < total; i++) { 98 | paragraph += outputText(); 99 | } 100 | return paragraph; 101 | } 102 | 103 | return { 104 | title: `第${index + 1}章节`, 105 | content: outputParagraph() 106 | }; 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { PickerSelectItem } from "@/types"; 2 | 3 | /** 城市数据 */ 4 | export function useCityData(): Array { 5 | return [ 6 | { 7 | label: "广东省", 8 | value: 0, 9 | children: [ 10 | { 11 | label: "广州市", 12 | value: 0, 13 | children: [ 14 | { 15 | label: "天河区", 16 | value: 0 17 | }, 18 | { 19 | label: "番禺区", 20 | value: 1 21 | }, 22 | { 23 | label: "白云区", 24 | value: 2 25 | } 26 | ] 27 | }, 28 | { 29 | label: "深圳市", 30 | value: 1, 31 | children: [ 32 | { 33 | label: "保安区", 34 | value: 0 35 | }, 36 | { 37 | label: "龙华区", 38 | value: 1 39 | }, 40 | { 41 | label: "福田区", 42 | value: 2 43 | } 44 | ] 45 | }, 46 | { 47 | label: "其他", 48 | value: 2 49 | } 50 | ] 51 | }, 52 | { 53 | label: "湖南省", 54 | value: 1, 55 | children: [ 56 | { 57 | label: "长沙市", 58 | value: 0, 59 | children: [ 60 | { 61 | label: "芙蓉区", 62 | value: 0 63 | }, 64 | { 65 | label: "天心区", 66 | value: 1 67 | }, 68 | { 69 | label: "岳麓区", 70 | value: 2 71 | } 72 | ] 73 | }, 74 | { 75 | label: "衡阳市", 76 | value: 1, 77 | children: [ 78 | { 79 | label: "衡阳县", 80 | value: 0 81 | }, 82 | { 83 | label: "衡南县", 84 | value: 1 85 | }, 86 | { 87 | label: "衡山县", 88 | value: 2 89 | } 90 | ] 91 | } 92 | ] 93 | }, 94 | { 95 | label: "黑龙江省", 96 | value: 1, 97 | children: [ 98 | { 99 | label: "哈尔滨市", 100 | value: 0, 101 | children: [ 102 | { 103 | label: "道外区", 104 | value: 0 105 | }, 106 | { 107 | label: "松北区", 108 | value: 1 109 | }, 110 | { 111 | label: "呼兰区", 112 | value: 2 113 | } 114 | ] 115 | }, 116 | { 117 | label: "沈阳市", 118 | value: 1, 119 | } 120 | ] 121 | }, 122 | { 123 | label: '浙江省', 124 | value: 12, 125 | } 126 | ] 127 | } -------------------------------------------------------------------------------- /src/hooks/loadMore.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { onReachBottom } from "@dcloudio/uni-app"; 3 | import { showToast } from "@/utils/control"; 4 | import { modifyData } from "@/utils"; 5 | 6 | export interface LoadMoreInfo extends PageInfo { 7 | /** 加载状态 */ 8 | state: "wait" | "loading" | "nomore" 9 | /** 加载的列表数据 */ 10 | list: Array 11 | /** 12 | * 请求成功次数 13 | * - 成功时才会累加,`requestCount === 1` 时为首次获取数据 14 | */ 15 | requestCount: number 16 | /** 17 | * 列表数据`key`值,因为每个后台返回的数组字段不一样,所以这里加一个动态设置的字段 18 | * @example 19 | * ```js 20 | * // 假设后台返回的数据是这样的: 21 | * const res = { 22 | * currentPage: 1, 23 | * records: [...], 24 | * pageSize: 10 25 | * ...more 26 | * } 27 | * // 那么 listDataKey = "records"; 28 | * // 默认 listDataKey = "list"; 29 | * ``` 30 | */ 31 | listDataKey: string 32 | } 33 | 34 | /** 加载更多数据对象 */ 35 | export function useLoadMoreData(): LoadMoreInfo { 36 | return { 37 | state: "wait", 38 | list: [], 39 | currentPage: 1, 40 | pageSize: 10, 41 | requestCount: 0, 42 | listDataKey: "list" 43 | } 44 | } 45 | 46 | /** 47 | * 加载更多-功能函数 48 | */ 49 | export default function useLoadMore() { 50 | 51 | /** 加载更多数据 */ 52 | const loadMoreData = reactive(useLoadMoreData()); 53 | 54 | /** 请求函数 */ 55 | let requestListFn: () => Promise>; 56 | 57 | /** 58 | * 设置请求函数 59 | * @param fn 60 | */ 61 | function setRequestListFn(fn: () => Promise>) { 62 | requestListFn = fn; 63 | } 64 | 65 | /** 66 | * 开始请求获取列表数据 67 | * @param callback 加载结束回调 68 | */ 69 | function getData(callback?: () => void) { 70 | if (!requestListFn) return console.log("%c 请调用“setRequestListFn”方法设置请求函数 >>", "color: #4fc08d"); 71 | const { state, list } = loadMoreData; 72 | if (state === "nomore" || state === "loading") return; 73 | loadMoreData.state = "loading"; 74 | requestListFn().then(result => { 75 | loadMoreData.state = "wait"; 76 | if (result.code === 1) { 77 | const listData = result.data[loadMoreData.listDataKey as "list"]; 78 | loadMoreData.requestCount++; 79 | loadMoreData.list = list.concat(listData); 80 | // 判断是否没有数据了 81 | // result.data.totalCount >= loadMoreData.list.length 82 | if (listData.length < loadMoreData.pageSize) { 83 | loadMoreData.state = "nomore"; 84 | } else { 85 | loadMoreData.currentPage++; 86 | } 87 | } else { 88 | showToast(result.msg || "加载列表出错"); 89 | } 90 | callback && callback(); 91 | }).catch(error => { 92 | console.log("%c getListData error >>", "color: #f04e7d", error); 93 | loadMoreData.state = "wait"; 94 | callback && callback(); 95 | }) 96 | } 97 | 98 | /** 99 | * 刷新数据 100 | * @param callback 101 | */ 102 | function refreshData(callback?: () => void) { 103 | loadMoreData.currentPage = 1; 104 | loadMoreData.list = []; 105 | loadMoreData.state = "wait"; 106 | getData(callback); 107 | } 108 | 109 | /** 重置列表数据 */ 110 | function resetListData() { 111 | modifyData(loadMoreData, useLoadMoreData()); 112 | } 113 | 114 | onReachBottom(function () { 115 | getData(); 116 | }) 117 | 118 | return { 119 | loadMoreData, 120 | refreshData, 121 | resetListData, 122 | setRequestListFn 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | export function createApp() { 5 | const app = createSSRApp(App); 6 | return { 7 | app, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-uni-app-template", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | "app-plus": { 9 | "usingComponents": true, 10 | "nvueStyleCompiler": "uni-app", 11 | "compilerVersion": 3, 12 | "splashscreen": { 13 | "alwaysShowBeforeRender": true, 14 | "waiting": true, 15 | "autoclose": true, 16 | "delay": 0 17 | }, 18 | "modules": {}, 19 | "distribute": { 20 | "android": { 21 | "permissions": [ 22 | "", 23 | "", 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "" 37 | ] 38 | }, 39 | "ios": {}, 40 | "sdkConfigs": {} 41 | } 42 | }, 43 | "quickapp": {}, 44 | "mp-weixin": { 45 | "appid": "", 46 | "setting": { 47 | "urlCheck": false 48 | }, 49 | "usingComponents": true 50 | }, 51 | "mp-alipay": { 52 | "usingComponents": true 53 | }, 54 | "mp-baidu": { 55 | "usingComponents": true 56 | }, 57 | "mp-toutiao": { 58 | "usingComponents": true 59 | }, 60 | "uniStatistics": { 61 | "enable": false 62 | }, 63 | "vueVersion": "3" 64 | } -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "path": "pages/tabBar/home", 5 | "style": { 6 | "navigationBarTitleText": "首页", 7 | "enablePullDownRefresh": true 8 | } 9 | }, 10 | { 11 | "path": "pages/tabBar/personal", 12 | "style": { 13 | "navigationBarTitleText": "个人页" 14 | } 15 | }, 16 | { 17 | "path": "pages/load-more-list", 18 | "style": { 19 | "navigationBarTitleText": "加载更多列表", 20 | "navigationBarBackgroundColor": "#222222", 21 | "navigationBarTextStyle": "white", 22 | "enablePullDownRefresh": true 23 | } 24 | }, 25 | { 26 | "path": "pages/book", 27 | "style": { 28 | "navigationBarTitleText": "小说阅读器", 29 | "navigationStyle": "custom" 30 | } 31 | } 32 | ], 33 | "globalStyle": { 34 | "navigationBarTextStyle": "black", 35 | "navigationBarTitleText": "uni-app", 36 | "navigationBarBackgroundColor": "#F8F8F8", 37 | "backgroundColor": "#F8F8F8" 38 | }, 39 | "tabBar": { 40 | "color": "#7A7E83", 41 | "selectedColor": "#000000", 42 | "borderStyle": "white", 43 | "backgroundColor": "#f4f4f4", 44 | "list": [ 45 | { 46 | "pagePath": "pages/tabBar/home", 47 | "iconPath": "static/home_off.png", 48 | "selectedIconPath": "static/home_on.png", 49 | "text": "首页" 50 | }, 51 | { 52 | "pagePath": "pages/tabBar/personal", 53 | "iconPath": "static/personal_off.png", 54 | "selectedIconPath": "static/personal_on.png", 55 | "text": "我的" 56 | } 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /src/pages/README.md: -------------------------------------------------------------------------------- 1 | # 页面目录 2 | -------------------------------------------------------------------------------- /src/pages/book.vue: -------------------------------------------------------------------------------- 1 | 126 | 692 | -------------------------------------------------------------------------------- /src/pages/button.vue: -------------------------------------------------------------------------------- 1 | 17 | 30 | -------------------------------------------------------------------------------- /src/pages/load-more-list.vue: -------------------------------------------------------------------------------- 1 | 12 | 53 | -------------------------------------------------------------------------------- /src/pages/tabBar/home.vue: -------------------------------------------------------------------------------- 1 | 10 | 37 | -------------------------------------------------------------------------------- /src/pages/tabBar/personal.vue: -------------------------------------------------------------------------------- 1 | 8 | 18 | -------------------------------------------------------------------------------- /src/static/README.md: -------------------------------------------------------------------------------- 1 | # 静态文件存放目录 2 | -------------------------------------------------------------------------------- /src/static/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/arrow-right.png -------------------------------------------------------------------------------- /src/static/default_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/default_head.png -------------------------------------------------------------------------------- /src/static/home_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/home_off.png -------------------------------------------------------------------------------- /src/static/home_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/home_on.png -------------------------------------------------------------------------------- /src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/logo.png -------------------------------------------------------------------------------- /src/static/logo_wx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/logo_wx.png -------------------------------------------------------------------------------- /src/static/logo_zfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/logo_zfb.png -------------------------------------------------------------------------------- /src/static/none_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/none_data.png -------------------------------------------------------------------------------- /src/static/personal_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/personal_off.png -------------------------------------------------------------------------------- /src/static/personal_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Travis-hjs/Reader-Vue/60a6e969061a993150531a04575eefc43acccb8c/src/static/personal_on.png -------------------------------------------------------------------------------- /src/store/AppOption.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | 3 | export default class ModuleAppOption { 4 | 5 | /** `APP`操作信息 */ 6 | readonly appOption = reactive({ 7 | /** `小程序`导航栏高度 */ 8 | navBarHeight: 0, 9 | /** `小程序`胶囊距右方间距(方保持左、右间距一致) */ 10 | menuRight: 0, 11 | /** `小程序`胶囊距底部间距(保持底部间距一致) */ 12 | menuBottom: 0, 13 | /** `小程序`胶囊高度(自定义内容可与胶囊高度保证一致) */ 14 | menuHeight: 0, 15 | /** `小程序`胶囊宽度 */ 16 | menuWidth: 0, 17 | /** 状态栏高度 */ 18 | statusBarHeight: 0, 19 | /** 原生底部`tabbar`高度 */ 20 | tabBarHeight: 0, 21 | /** 可使用窗口高度 */ 22 | windowHeight: 0, 23 | /** 可使用窗口宽度 */ 24 | windowWidth: 0, 25 | /** 屏幕宽度 */ 26 | screenWidth: 0, 27 | /** 屏幕高度 */ 28 | screenHeight: 0, 29 | /** 是否为`iPhoneX`系列(做底部`UI`判断) */ 30 | isIPhoneX: false 31 | }) 32 | 33 | /** 34 | * 初始化`APP`操作信息 35 | * @description 最好放在`App.onLaunch`执行,因为这时才是页页面初始化完成,各个尺寸值会比较准确 36 | * @learn 条件编译 https://uniapp.dcloud.io/platform 37 | */ 38 | initAppOption() { 39 | const systemInfo = uni.getSystemInfoSync(); 40 | 41 | this.appOption.statusBarHeight = systemInfo.statusBarHeight!; 42 | this.appOption.tabBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - systemInfo.statusBarHeight!; 43 | this.appOption.windowHeight = systemInfo.windowHeight; 44 | this.appOption.windowWidth = systemInfo.windowWidth; 45 | this.appOption.screenWidth = systemInfo.screenWidth 46 | this.appOption.screenHeight = systemInfo.screenHeight 47 | 48 | const isIos = systemInfo.system.toLocaleLowerCase().includes("ios"); 49 | const vaule = (systemInfo.screenWidth / systemInfo.screenHeight) < 0.5; 50 | this.appOption.isIPhoneX = (isIos && vaule); 51 | 52 | // #ifdef H5 53 | this.appOption.tabBarHeight = 50; 54 | this.appOption.isIPhoneX = false; // 网页端不需要判断底部UI判断 55 | // #endif 56 | 57 | // #ifdef MP 58 | const menuButtonInfo = uni.getMenuButtonBoundingClientRect(); 59 | // 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度 60 | this.appOption.navBarHeight = (menuButtonInfo.top - systemInfo.statusBarHeight!) * 2 + menuButtonInfo.height + systemInfo.statusBarHeight!; 61 | this.appOption.menuRight = systemInfo.screenWidth - menuButtonInfo.right; 62 | this.appOption.menuBottom = menuButtonInfo.top - systemInfo.statusBarHeight!; 63 | this.appOption.menuHeight = menuButtonInfo.height; 64 | this.appOption.menuWidth = menuButtonInfo.width; 65 | // #endif 66 | } 67 | } -------------------------------------------------------------------------------- /src/store/README.md: -------------------------------------------------------------------------------- 1 | # 状态模块 2 | -------------------------------------------------------------------------------- /src/store/User.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { modifyData } from "@/utils"; 3 | import { UserInfo } from "@/types/user"; 4 | 5 | const cacheName = "user-info"; 6 | 7 | function useUserInfo(): UserInfo { 8 | return { 9 | id: "", 10 | token: "", 11 | phone: "" 12 | } 13 | } 14 | 15 | /** 16 | * 用户状态管理模块 17 | */ 18 | export default class ModuleUser { 19 | constructor() { 20 | this.init(); 21 | } 22 | 23 | /** 初始化用户数据(从本地获取) */ 24 | private init() { 25 | const data = uni.getStorageSync(cacheName); 26 | if (data) { 27 | this.update(JSON.parse(data)); 28 | } 29 | } 30 | 31 | /** 用户信息 */ 32 | readonly info = reactive>(useUserInfo()); 33 | 34 | /** 35 | * 更新用户信息字段 36 | * @param value 修改的值 37 | */ 38 | update(value: Partial) { 39 | modifyData(this.info, value); 40 | uni.setStorageSync(cacheName, JSON.stringify(this.info)); 41 | } 42 | 43 | /** 重置用户信息 */ 44 | reset() { 45 | modifyData(this.info, useUserInfo()); 46 | uni.setStorageSync(cacheName, JSON.stringify(this.info)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { modifyData } from "@/utils"; 2 | import { reactive } from "vue"; 3 | import ModuleAppOption from "./AppOption"; 4 | import ModuleUser from "./User"; 5 | 6 | // import { getIamgeByName } from "@/utils"; 7 | export class ModuleStore extends ModuleAppOption { 8 | constructor() { 9 | super(); 10 | this.initBookOption(); 11 | } 12 | 13 | /** 图片对象集 */ 14 | get imageInfo() { 15 | // 需要用作背景图的可以用`import`引入 16 | return { 17 | iconWx: "/static/logo_wx.png", 18 | iconZfb: "/static/logo_zfb.png", 19 | logo: "/static/logo.png", 20 | defaultHead: "/static/default_head.png", 21 | noneData: "/static/none_data.png", 22 | iconArrowRight: "/static/arrow-right.png" 23 | // iconWx: getIamgeByName("logo_wx.png"), 24 | // iconZfb: getIamgeByName("logo_zfb.png"), 25 | // logo: getIamgeByName("logo.png"), 26 | // defaultHead: getIamgeByName("default_head.png"), 27 | // noneData: getIamgeByName("none_data.png"), 28 | // iconArrowRight: getIamgeByName("arrow-right.png") 29 | } 30 | } 31 | 32 | /** 用户状态 */ 33 | readonly user = new ModuleUser(); 34 | 35 | /** 小说操作信息 */ 36 | readonly bookOption = reactive({ 37 | /** 是否首次打开 */ 38 | first: true, 39 | /** 主题 */ 40 | theme: 1, 41 | /** 阅读器字体信息 */ 42 | sizeInfo: { 43 | /** 标题字体大小 */ 44 | title: 24, 45 | /** 段落字体大小 */ 46 | p: 18, 47 | /** 标题行高 */ 48 | tLineHeight: 24 * 1.5, 49 | /** 段落行高 */ 50 | pLineHeight: 18 * 1.5, 51 | /** 下边距 */ 52 | margin: 18 53 | } 54 | }) 55 | 56 | /** 保存小说操作信息 */ 57 | saveBookOption() { 58 | uni.setStorageSync("book-app-option", JSON.stringify(this.bookOption)); 59 | } 60 | 61 | /** 获取小说操作信息 */ 62 | private initBookOption() { 63 | const data = uni.getStorageSync("book-app-option"); 64 | if (data) { 65 | modifyData(this.bookOption, JSON.parse(data)) 66 | } 67 | } 68 | 69 | /** 清除小说操作信息 */ 70 | removeBookOption() { 71 | uni.removeStorageSync("book-app-option"); 72 | } 73 | 74 | } 75 | 76 | /** 77 | * 状态管理模块 78 | * - `OOP`单例设计模式 79 | * - 参考 [你不需要`Vuex`](https://juejin.cn/post/6844903904023429128) 80 | */ 81 | const store = new ModuleStore(); 82 | 83 | export default store; -------------------------------------------------------------------------------- /src/styles/README.md: -------------------------------------------------------------------------------- 1 | # 通用样式文件目录 2 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "../uni.scss"; 2 | 3 | @mixin reset { margin: 0; padding: 0; box-sizing: border-box; } 4 | 5 | page { background-color: #fff; width: 100%; min-height: 100vh; @include reset(); } 6 | view, button, input, textarea { font-family: "Arial", "Helvetica Neue", "Helvetica", "sans-serif"; @include reset(); } 7 | image, input, textarea, picker { display: block; width: 100%; @include reset();} 8 | input::after, input::before, button::after, button::before { border: none; @include reset(); } 9 | 10 | // h5默认是100vh,即使页面没有内容也会滚动,这里取消这个设定 11 | // 注意:scss 条件编译必须以 css 注释为标准才能条件编译 12 | /* #ifdef H5 */ 13 | uni-page-body { min-height: auto !important; } 14 | /* #endif */ 15 | 16 | /* flex布局 */ 17 | .flex { display: flex; flex-wrap: nowrap; } 18 | .fwrap { display: flex; flex-wrap: wrap; } 19 | .f1 { flex: 1; } 20 | .f2 { flex: 2; } 21 | .f3 { flex: 3; } 22 | /* 垂直居中 */ 23 | .fvertical { display: flex; align-items: center; } 24 | /* 水平居中 */ 25 | .fcenter { display: flex; justify-content: center; } 26 | /* 水平+垂直居中 */ 27 | .fvc { display: flex; align-items: center; justify-content: center; } 28 | /* 右对齐 */ 29 | .fright { display: flex; justify-content: flex-end; } 30 | /* 两端对齐 */ 31 | .fbetween { display: flex; justify-content: space-between; } 32 | /* 靠底部对齐 */ 33 | .fbottom { display: flex; align-items: flex-end; } 34 | 35 | /* 溢出...显示 当前节点生效 */ 36 | .ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 37 | 38 | /* 溢出...显示 子节点生效 */ 39 | .ellipsis_1 { @include ellipsis(1); } 40 | .ellipsis_2 { @include ellipsis(2); } 41 | .ellipsis_3 { @include ellipsis(3); } 42 | 43 | /* 卡片 */ 44 | .card { 45 | border-radius: 2px; 46 | box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12); 47 | background-color: #fff; 48 | overflow: hidden; 49 | } 50 | 51 | .button { @include button($dark, #eee); } 52 | .button-dark { @include button(#e3b480, $dark); } 53 | .button-pink { @include button($white, $pink); } 54 | .button-red { @include button($white, $red); } 55 | 56 | .line { width: 100%; padding-top: 40rpx; margin-bottom: 40rpx; border-bottom: solid 1px #eee; } 57 | 58 | /* 上下左右边距 */ 59 | @for $index from 1 through 5 { 60 | .mgl_#{$index}0 { 61 | margin-left: 10rpx * $index; 62 | } 63 | .mgr_#{$index}0 { 64 | margin-right: 10rpx * $index; 65 | } 66 | .mgt_#{$index}0 { 67 | margin-top: 10rpx * $index; 68 | } 69 | .mgb_#{$index}0 { 70 | margin-bottom: 10rpx * $index; 71 | } 72 | .pdl_#{$index}0 { 73 | padding-left: 10rpx * $index; 74 | } 75 | .pdr_#{$index}0 { 76 | padding-right: 10rpx * $index; 77 | } 78 | .pdt_#{$index}0 { 79 | padding-top: 10rpx * $index; 80 | } 81 | .pdb_#{$index}0 { 82 | padding-bottom: 10rpx * $index; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/loading.scss: -------------------------------------------------------------------------------- 1 | .preloader-inner { 2 | position: relative; 3 | display: block; 4 | width: 100%; 5 | height: 100%; 6 | animation: md-preloader-inner-rotate 5.25s cubic-bezier(.35, 0, .25, 1) infinite; 7 | 8 | .preloader-inner-gap { 9 | position: absolute; 10 | width: 2px; 11 | left: 50%; 12 | margin-left: -1px; 13 | top: 0; 14 | bottom: 0; 15 | box-sizing: border-box; 16 | border-top: 3px solid #010101; 17 | } 18 | 19 | .preloader-inner-left, 20 | .preloader-inner-right { 21 | position: absolute; 22 | top: 0; 23 | height: 100%; 24 | width: 50%; 25 | overflow: hidden 26 | } 27 | 28 | .preloader-inner-half-circle { 29 | position: absolute; 30 | top: 0; 31 | height: 100%; 32 | width: 200%; 33 | box-sizing: border-box; 34 | border: 3px solid #010101; 35 | border-bottom-color: transparent !important; 36 | border-radius: 50%; 37 | animation-iteration-count: infinite; 38 | animation-duration: 1.3125s; 39 | animation-timing-function: cubic-bezier(.35, 0, .25, 1) 40 | } 41 | 42 | .preloader-inner-left { 43 | left: 0; 44 | 45 | .preloader-inner-half-circle { 46 | left: 0; 47 | border-right-color: transparent !important; 48 | animation-name: md-preloader-left-rotate; 49 | } 50 | } 51 | 52 | .preloader-inner-right { 53 | right: 0; 54 | 55 | .preloader-inner-half-circle { 56 | right: 0; 57 | border-left-color: transparent !important; 58 | animation-name: md-preloader-right-rotate; 59 | } 60 | } 61 | } 62 | 63 | @keyframes md-preloader-left-rotate { 64 | 65 | 0%, 66 | 100% { 67 | transform: rotate(130deg) 68 | } 69 | 70 | 50% { 71 | transform: rotate(-5deg) 72 | } 73 | } 74 | 75 | @keyframes md-preloader-right-rotate { 76 | 77 | 0%, 78 | 100% { 79 | transform: rotate(-130deg) 80 | } 81 | 82 | 50% { 83 | transform: rotate(5deg) 84 | } 85 | } 86 | 87 | @keyframes md-preloader-inner-rotate { 88 | 12.5% { 89 | transform: rotate(135deg) 90 | } 91 | 92 | 25% { 93 | transform: rotate(270deg) 94 | } 95 | 96 | 37.5% { 97 | transform: rotate(405deg) 98 | } 99 | 100 | 50% { 101 | transform: rotate(540deg) 102 | } 103 | 104 | 62.5% { 105 | transform: rotate(675deg) 106 | } 107 | 108 | 75% { 109 | transform: rotate(810deg) 110 | } 111 | 112 | 87.5% { 113 | transform: rotate(945deg) 114 | } 115 | 116 | 100% { 117 | transform: rotate(1080deg) 118 | } 119 | } 120 | 121 | @keyframes md-preloader-outer { 122 | 0% { 123 | transform: rotate(0) 124 | } 125 | 126 | 100% { 127 | transform: rotate(360deg) 128 | } 129 | } 130 | 131 | .preloader { 132 | display: block; 133 | width: 40px; 134 | height: 40px; 135 | animation: md-preloader-outer 3.3s linear infinite; 136 | } -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | // 全局声明文件,局部声明文件不要写在这里 2 | 3 | /** 深层递归所有属性为可选 */ 4 | type DeepPartial = { 5 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 6 | } 7 | 8 | /** 深层递归所有属性为只读 */ 9 | type DeepReadonly = { 10 | readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; 11 | } 12 | 13 | /** 深层递归所有属性为必选选(貌似不生效) */ 14 | type DeepRequired = { 15 | [P in keyof T]-?: T[P] extends object ? Required : T[P]; 16 | } 17 | 18 | /** 运算符号 */ 19 | type NumberSymbols = "+" | "-" | "*" | "/"; 20 | 21 | /** 22 | * `JavaScript`类型 23 | * - 这里只枚举一些常见类型,后续根据使用场景自行添加即可 24 | */ 25 | type JavaScriptTypes = "string" | "number" | "array" | "object" | "boolean" | "function" | "null" | "undefined" | "regexp" | "promise" | "formdata"; 26 | 27 | /** 基础对象 */ 28 | interface BaseObj { 29 | [key: string]: T 30 | } 31 | 32 | /** 接口请求基础响应数据 */ 33 | interface ApiResult { 34 | /** 接口状态`code === 1`为成功 */ 35 | code: number 36 | /** 接口响应数据 */ 37 | data: T 38 | /** 接口响应信息 */ 39 | msg: string 40 | } 41 | 42 | /** 接口请求列表响应数据 */ 43 | interface ApiResultList extends PageInfo { 44 | /** 列表数据 */ 45 | list: Array 46 | } 47 | 48 | /** 请求配置 */ 49 | interface RequestOptions { 50 | /** 51 | * 请求头配置(header 中不能设置 Referer。),会覆盖默认设置 52 | * - 需要表单形式就`headers: { "codeMode": "form" }`; 53 | * - 其他头部设置自行定义 54 | */ 55 | headers: BaseObj & { 56 | codeMode?: "json" | "form" 57 | }, 58 | /** 请求数据类型 */ 59 | dataType: "json" | "text" | "" 60 | /** 接口数据响应类型 */ 61 | responseType: "json" | "arraybuffer" | "blob" | "stream" | "document" | "text" 62 | /** 是否在`res.code !== 1`的时候显示提示,默认`false`,传入`true`则用`res.msg`作为提示,也可以传入字符串作为提示内容 */ 63 | showTip: string | boolean 64 | } 65 | 66 | /** 页码信息 */ 67 | interface PageInfo { 68 | /** 一页多少条 */ 69 | pageSize: number 70 | /** 当前页,从`1`开始 */ 71 | currentPage: number 72 | /** 总数 */ 73 | total?: number 74 | } 75 | 76 | /** `uni-app`change事件参数 */ 77 | interface UniAppChangeEvent { 78 | detail: { 79 | /** `@change`事件触发的值 */ 80 | value: T 81 | } 82 | } 83 | 84 | interface Window { 85 | /** 86 | * 当前版本,方便在控制台查看调试用 87 | * @description 引用的是`package.json`中的`version` 88 | */ 89 | version: string 90 | } 91 | 92 | /** .nvue文件专用模块 */ 93 | declare const weex: any; 94 | -------------------------------------------------------------------------------- /src/types/README.md: -------------------------------------------------------------------------------- 1 | # 类型文件目录,统一小写命名 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // import { ComponentInternalInstance } from "vue"; 2 | 3 | // /** 4 | // * `vue3`类型扩充 5 | // * - 弥补官方的类型不足问题 6 | // */ 7 | // export declare namespace Vue3 { 8 | // /** 9 | // * `vue`实例上下文 10 | // * - `vue3`类型文件中貌似没有暴露可以使用的类型,所以自定义补充一个接口代替,和`vue2`中一样的类型一致 11 | // */ 12 | // interface Ctx { 13 | // /** 当前组件节点 */ 14 | // readonly $el: HTMLElement 15 | // /** 父节点 */ 16 | // readonly $parent?: Ctx 17 | // /** 当前组件实例 */ 18 | // readonly $root: Ctx 19 | // /** 选项配置 */ 20 | // readonly $options: { 21 | // name: string 22 | // } 23 | // } 24 | // /** 25 | // * `hooks: getCurrentInstance()`钩子函数返回的类型扩充 26 | // */ 27 | // interface Instance extends ComponentInternalInstance { 28 | // /** 实例上下文对象 */ 29 | // ctx: Ctx 30 | // } 31 | // } 32 | 33 | /** 上传图片返回结果 */ 34 | export interface UploadChange { 35 | /** 和当前上传组件绑定的`id` */ 36 | id: string | number 37 | /** 图片路径 */ 38 | src: string 39 | } 40 | 41 | /** 表单规则类型 */ 42 | export interface TheFormRulesItem { 43 | /** 是否必填项 */ 44 | required?: boolean 45 | /** 提示字段 */ 46 | message?: string 47 | /** 指定类型 */ 48 | type?: "number" | "array" 49 | /** 50 | * 自定义的校验规则(正则) 51 | * - 考虑到微信一些特殊的抽风机制,在微信小程序中,除`number|string|object|undefined|null`这几个基础类型外,其他类型是会被过滤掉,所以这里在写正则的时候,在末尾加上`.toString()`即可 52 | */ 53 | reg?: string // | RegExp 54 | } 55 | 56 | /** 表单规则类型 */ 57 | export type TheFormRules = { [key: string]: Array }; 58 | 59 | /** `label`布局位置 */ 60 | export type LabelPosition = "left" | "right" | "top"; 61 | 62 | /** 表单验证回调类型 */ 63 | export interface TheFormValidateCallback { 64 | ( 65 | /** 是否验证通过 */ 66 | isValid: boolean, 67 | /** 验证不通过的规则列表 */ 68 | rules: { [key: string]: Array } 69 | ): void 70 | } 71 | 72 | /** 选择器`item`数据 */ 73 | export interface PickerSelectItem { 74 | /** 展示字段 */ 75 | label: string 76 | /** 对应的值 */ 77 | value: T 78 | /** 79 | * 下级数据 80 | * @description 最多三层,选择器栏目数根据当前下级动态显示 81 | */ 82 | children?: Array> 83 | /** 其他携带的值 */ 84 | [key: string]: any 85 | } 86 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | /** 用户信息类型 */ 2 | export interface UserInfo { 3 | /** 登录凭据 */ 4 | token: string 5 | /** 用户手机号 */ 6 | phone: number | "", 7 | /** 用户`id` */ 8 | id: number | "" 9 | } 10 | -------------------------------------------------------------------------------- /src/uni.scss: -------------------------------------------------------------------------------- 1 | // 每个页面需要引入的文件,注意:vue.config.js 中配置的 css.loaderOptions 是无法在当前项目生效的, 2 | // 可能是uni-app项目设定和标准vue-cli项目设定不一样导致的,需要在当前文件全局引入即可 3 | // 全局样式写在`App.vue`里面,一些预处理工具样式可以写这里,因为每个页面都会注入一遍 4 | // 全局样式不写在这里的原因是因为每个页面都注入一遍的话会导致`html-style`有多个相同的代码标签 5 | 6 | $white: #fff; 7 | $black: #010101; 8 | $dark: #222; 9 | $red: #fa2d2d; 10 | $pink: #f04e7d; 11 | $blue: #2C72F3; 12 | 13 | // 溢出...显示 14 | @mixin ellipsis($number) { 15 | display: -webkit-box; 16 | -webkit-box-orient: vertical; 17 | -webkit-line-clamp: $number; 18 | overflow: hidden; 19 | } 20 | 21 | @mixin button($color, $bg) { 22 | font-size: 30rpx; 23 | min-width: 160rpx; 24 | padding: 0 20rpx; 25 | border-radius: 2px; 26 | height: 80rpx; 27 | background-color: $bg; 28 | border-color: $bg; 29 | color: $color; 30 | line-height: 1; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | 35 | &::after, 36 | &::before { 37 | border: none; 38 | background-color: transparent; 39 | } 40 | 41 | &:hover { 42 | background-color: $bg; 43 | border-color: $bg; 44 | color: $color; 45 | } 46 | 47 | &:active { 48 | opacity: 0.85; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/Config.ts: -------------------------------------------------------------------------------- 1 | function moduleConfig() { 2 | 3 | const env = process.env.NODE_ENV === "development" ? "dev" : "prod"; 4 | 5 | const url = { 6 | dev: `/api`, 7 | prod: "https://huangjingsheng.com/api" 8 | } 9 | 10 | return { 11 | /** 请求超时毫秒 */ 12 | get requestOvertime() { 13 | return 8000; 14 | }, 15 | /** `api`请求域名 */ 16 | get apiUrl() { 17 | return url[env]; 18 | }, 19 | /** 当前环境模式 */ 20 | get env() { 21 | return env; 22 | }, 23 | /** 上传图片地址 */ 24 | get uploadUrl() { 25 | return "http://xxx.com/upload" 26 | } 27 | } 28 | } 29 | 30 | /** 配置模块 */ 31 | const config = moduleConfig(); 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /src/utils/Control.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from "vue"; 2 | 3 | // ========================= 控件模块 ========================= 4 | 5 | /** 6 | * 打开其他应用 7 | * @param name 应用名 8 | * @param callback 错误回调 9 | */ 10 | export function openApp(name: "qq" | "wx" | "zfb" | "sina" | "taobao", callback?: (result: any) => void) { 11 | // #ifdef APP-PLUS 12 | // learn: https://ask.dcloud.net.cn/article/35621 13 | const data = { 14 | qq: "mqq://", 15 | wx: "weixin://", 16 | zfb: "alipay://", 17 | sina: "sinaweibo://", 18 | taobao: "taobao://", 19 | } 20 | plus.runtime.openURL(data[name], callback); 21 | // #endif 22 | } 23 | 24 | /** 25 | * 显示加载提示 26 | * @param text 提示文字 27 | */ 28 | export function showLoading(text: string = "加载中..") { 29 | uni.showLoading({ 30 | title: text, 31 | mask: true 32 | }); 33 | } 34 | 35 | /** 36 | * 显示提示条 37 | * @param tip 提示文字 38 | * @param duration 持续时间 39 | */ 40 | export function showToast(tip: string, duration = 2000) { 41 | uni.showToast({ 42 | title: tip, 43 | // position: "bottom", 44 | icon: "none", 45 | duration: duration, 46 | // image: src 47 | }); 48 | } 49 | 50 | /** 51 | * 显示提示框 52 | * @param content 提示的内容 53 | * @param success 确认回调 54 | * @param title 提示标题 55 | */ 56 | export function showAlert(content: string, success?: (res: UniApp.ShowModalRes) => void, title: string = "操作提示") { 57 | uni.showModal({ 58 | title: title, 59 | content: content, 60 | showCancel: false, 61 | success: success 62 | }) 63 | } 64 | 65 | interface ShowConfirmOptions { 66 | /** 内容 */ 67 | content: string 68 | /** 标题 */ 69 | title?: string 70 | /** 确认回调 */ 71 | confirm?(): void 72 | /** 取消回调 */ 73 | cancel?(): void 74 | /** 确认按钮文字 */ 75 | confirmText?: string 76 | /** 取消按钮文字,超过4个中文会报错 */ 77 | cancelText: string 78 | } 79 | 80 | /** 81 | * 确认弹窗 82 | * @param options 传参对象 83 | */ 84 | export function showConfirm(options: ShowConfirmOptions) { 85 | uni.showModal({ 86 | title: options.title || "操作提示", 87 | content: options.content, 88 | showCancel: true, 89 | confirmText: options.confirmText || "确认", 90 | cancelText: options.cancelText || "取消", 91 | success(res) { 92 | if (res.confirm) { 93 | options.confirm && options.confirm(); 94 | } else if (res.cancel) { 95 | options.cancel && options.cancel(); 96 | } 97 | }, 98 | }); 99 | } 100 | 101 | /** 102 | * 复制文本 103 | * @param value 复制的内容 104 | * @param success 成功回调 105 | */ 106 | export function copyText(value: string, success?: () => void) { 107 | value = value.replace(/(^\s*)|(\s*$)/g, ""); 108 | if (!value) { 109 | return showToast("复制的内容不能为空!"); 110 | } 111 | 112 | // #ifndef H5 113 | uni.setClipboardData({ 114 | data: value, 115 | success() { 116 | showToast("复制成功"); 117 | success && success(); 118 | } 119 | }); 120 | // #endif 121 | 122 | // #ifdef H5 123 | const id = "the-clipboard"; 124 | let clipboard = document.getElementById(id) as HTMLTextAreaElement; 125 | if (!clipboard) { 126 | clipboard = document.createElement("textarea"); 127 | clipboard.id = id; 128 | clipboard.readOnly = true; 129 | clipboard.style.cssText = "font-size: 15px; position: fixed; top: -1000%; left: -1000%;"; 130 | document.body.appendChild(clipboard); 131 | } 132 | clipboard.value = value; 133 | clipboard.select(); 134 | clipboard.setSelectionRange(0, clipboard.value.length); 135 | const state = document.execCommand("copy"); 136 | if (state) { 137 | showToast("复制成功"); 138 | success && success(); 139 | } else { 140 | showToast("复制失败"); 141 | } 142 | // #endif 143 | } 144 | 145 | interface ScrollviewCenterOptions { 146 | /** 147 | * 当前实例 148 | * ```js 149 | * import { getCurrentInstance } from "vue"; 150 | * getCurrentInstance(); 151 | * ``` 152 | */ 153 | ctx: T, 154 | /** 要滚动的目标节点`id` */ 155 | id: string 156 | /** ``的宽度,默认是屏幕宽度 */ 157 | wrapWidth?: number 158 | /** 点击事件 */ 159 | event?: Event 160 | /** 是否主动设置偏移到中心位置,设置值时,`event`不需要传入 */ 161 | scrollValue?: number 162 | /** 回调 */ 163 | callback?: (left: number, info: UniApp.NodeInfo) => void 164 | } 165 | 166 | /** 167 | * 监听``组件指定元素滚动到视图中心的偏移值 168 | * @param option 配置参数 169 | */ 170 | export function onScrollviewCenter(option: ScrollviewCenterOptions) { 171 | const width = option.wrapWidth || uni.getSystemInfoSync().windowWidth; 172 | nextTick(function () { 173 | const node = uni.createSelectorQuery().in(option.ctx).select(`#${option.id}`); 174 | const left = option.event ? (option.event.currentTarget as any).offsetLeft : 0; 175 | node.boundingClientRect(function (nodeInfo) { 176 | let result = 0; 177 | if (nodeInfo && !Array.isArray(nodeInfo)) { 178 | if (typeof option.scrollValue === "number") { 179 | result = option.scrollValue + nodeInfo.left! + nodeInfo.width! / 2 - width / 2; 180 | } else { 181 | result = left + nodeInfo.width! / 2 - width / 2; 182 | } 183 | } 184 | typeof option.callback === "function" && option.callback(result, nodeInfo as any); 185 | }).exec(); 186 | }); 187 | } 188 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # 工具类目录 2 | -------------------------------------------------------------------------------- /src/utils/icon.ts: -------------------------------------------------------------------------------- 1 | const iconData = { 2 | /** icon 数量 */ 3 | icons: { 4 | "pulldown": "\ue588", 5 | "refreshempty": "\ue461", 6 | "back": "\ue471", 7 | "forward": "\ue470", 8 | "more": "\ue507", 9 | "more-filled": "\ue537", 10 | "scan": "\ue612", 11 | "qq": "\ue264", 12 | "weibo": "\ue260", 13 | "weixin": "\ue261", 14 | "pengyouquan": "\ue262", 15 | "loop": "\ue565", 16 | "refresh": "\ue407", 17 | "refresh-filled": "\ue437", 18 | "arrowthindown": "\ue585", 19 | "arrowthinleft": "\ue586", 20 | "arrowthinright": "\ue587", 21 | "arrowthinup": "\ue584", 22 | "undo-filled": "\ue7d6", 23 | "undo": "\ue406", 24 | "redo": "\ue405", 25 | "redo-filled": "\ue7d9", 26 | "bars": "\ue563", 27 | "chatboxes": "\ue203", 28 | "camera": "\ue301", 29 | "chatboxes-filled": "\ue233", 30 | "camera-filled": "\ue7ef", 31 | "cart-filled": "\ue7f4", 32 | "cart": "\ue7f5", 33 | "checkbox-filled": "\ue442", 34 | "checkbox": "\ue7fa", 35 | "arrowleft": "\ue582", 36 | "arrowdown": "\ue581", 37 | "arrowright": "\ue583", 38 | "smallcircle-filled": "\ue801", 39 | "arrowup": "\ue580", 40 | "circle": "\ue411", 41 | "eye-filled": "\ue568", 42 | "eye-slash-filled": "\ue822", 43 | "eye-slash": "\ue823", 44 | "eye": "\ue824", 45 | "flag-filled": "\ue825", 46 | "flag": "\ue508", 47 | "gear-filled": "\ue532", 48 | "reload": "\ue462", 49 | "gear": "\ue502", 50 | "hand-thumbsdown-filled": "\ue83b", 51 | "hand-thumbsdown": "\ue83c", 52 | "hand-thumbsup-filled": "\ue83d", 53 | "heart-filled": "\ue83e", 54 | "hand-thumbsup": "\ue83f", 55 | "heart": "\ue840", 56 | "home": "\ue500", 57 | "info": "\ue504", 58 | "home-filled": "\ue530", 59 | "info-filled": "\ue534", 60 | "circle-filled": "\ue441", 61 | "chat-filled": "\ue847", 62 | "chat": "\ue263", 63 | "mail-open-filled": "\ue84d", 64 | "email-filled": "\ue231", 65 | "mail-open": "\ue84e", 66 | "email": "\ue201", 67 | "checkmarkempty": "\ue472", 68 | "list": "\ue562", 69 | "locked-filled": "\ue856", 70 | "locked": "\ue506", 71 | "map-filled": "\ue85c", 72 | "map-pin": "\ue85e", 73 | "map-pin-ellipse": "\ue864", 74 | "map": "\ue364", 75 | "minus-filled": "\ue440", 76 | "mic-filled": "\ue332", 77 | "minus": "\ue410", 78 | "micoff": "\ue360", 79 | "mic": "\ue302", 80 | "clear": "\ue434", 81 | "smallcircle": "\ue868", 82 | "close": "\ue404", 83 | "closeempty": "\ue460", 84 | "paperclip": "\ue567", 85 | "paperplane": "\ue503", 86 | "paperplane-filled": "\ue86e", 87 | "person-filled": "\ue131", 88 | "contact-filled": "\ue130", 89 | "person": "\ue101", 90 | "contact": "\ue100", 91 | "images-filled": "\ue87a", 92 | "phone": "\ue200", 93 | "images": "\ue87b", 94 | "image": "\ue363", 95 | "image-filled": "\ue877", 96 | "location-filled": "\ue333", 97 | "location": "\ue303", 98 | "plus-filled": "\ue439", 99 | "plus": "\ue409", 100 | "plusempty": "\ue468", 101 | "help-filled": "\ue535", 102 | "help": "\ue505", 103 | "navigate-filled": "\ue884", 104 | "navigate": "\ue501", 105 | "mic-slash-filled": "\ue892", 106 | "search": "\ue466", 107 | "settings": "\ue560", 108 | "sound": "\ue590", 109 | "sound-filled": "\ue8a1", 110 | "spinner-cycle": "\ue465", 111 | "download-filled": "\ue8a4", 112 | "personadd-filled": "\ue132", 113 | "videocam-filled": "\ue8af", 114 | "personadd": "\ue102", 115 | "upload": "\ue402", 116 | "upload-filled": "\ue8b1", 117 | "starhalf": "\ue463", 118 | "star-filled": "\ue438", 119 | "star": "\ue408", 120 | "trash": "\ue401", 121 | "phone-filled": "\ue230", 122 | "compose": "\ue400", 123 | "videocam": "\ue300", 124 | "trash-filled": "\ue8dc", 125 | "download": "\ue403", 126 | "chatbubble-filled": "\ue232", 127 | "chatbubble": "\ue202", 128 | "cloud-download": "\ue8e4", 129 | "cloud-upload-filled": "\ue8e5", 130 | "cloud-upload": "\ue8e6", 131 | "cloud-download-filled": "\ue8e9", 132 | "headphones":"\ue8bf", 133 | "shop":"\ue609" 134 | }, 135 | /** 字体文件 */ 136 | font: 'data:font/truetype;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTYoJ48wAAGf4AAAAHEdERUYAJwCMAABn2AAAAB5PUy8yWXpc3QAAAVgAAABgY21hcB9SCa8AAAPQAAADImdhc3D//wADAABn0AAAAAhnbHlmWWfecQAACAQAAFYcaGVhZBehAMAAAADcAAAANmhoZWEH+gSHAAABFAAAACRobXR4D3IujAAAAbgAAAIYbG9jYa77miAAAAb0AAABDm1heHABnACoAAABOAAAACBuYW1lj4vbUwAAXiAAAAM5cG9zdH/g11YAAGFcAAAGcwABAAAAAQAAGbvTeF8PPPUACwQAAAAAANoxE3MAAAAA2jSpUAAA/5UEHANrAAAACAACAAAAAAAAAAEAAAOA/4AAXASAAAAAAAQcAAEAAAAAAAAAAAAAAAAAAACGAAEAAACGAJwADAAAAAAAAgAAAAoACgAAAP8AAAAAAAAAAwQBAZAABQAAAokCzAAAAI8CiQLMAAAB6wAyAQgAAAIABQMAAAAAAAAAAAAAEAAAAAAAAAAAAAAAUGZFZABAAB3o6QOA/4AAXAOAAIAAAAABAAAAAAIAAs0AAAAgAAEEAAAAAAAAAAFVAAAEAABLBAAAiQQAACEEAABLBAAAlwQAACkEAABdBAAAJwQAACgEAAAABAAAcwQAACcEAAAoBAAAAAQAACAEgABVBAAAegQAACgEAACcBAAAkgQAAAgEAADNBAAAyQQAAN0EAADJBAAAeAQAAAYEAABCBAAAVgQAAGoEAACEBAAAhAQAAEsEAAAxBAAAMQQAAEsEAAAcBAAASwQAAEsEAABLBAAASwQAAEsEAAAcBAAASwQAAEsEAABLBAAASQQAAOMEAAEABAAASwQAABwEAAAdBAAAbQQAAJ8EAAFABAABQAQAALgEAAALBAAASwQAAFYEAAA/BAAASwQAAEsEAADRBAAAZAQAAIMEAAALBAAAVgQAAEsEAABLBAAAZAQAAFAEAABRBAAAkgQAAAQEAABqBAAAAAQAAIwEAACMBAABLwQAAS4EAAC7BAAAuwQAAHIEAAByBAABHgQAAA0EAAA5BAAAQAQAADEEAAAxBAAACAQAABEEAAASBAAASQQAAEsEAAAABAAAAAQAAAAEAACDBAAAVQQAADwEAABVBAAAVgQAADwEAABWBAAAKAQAACYEAAAmBAAA1gQAAEEEAAFfBAAAZwQAAEsEAAA/BAAABgQAAAAEAAAABAAASwQAAHgEAAAABAAAhAQAAJIEAACEBAAARQQAAIQEEgAcBBIAHAQSABwEEgAcAVUAAAAAAAMAAAADAAAAHAABAAAAAAIcAAMAAQAAABwABAIAAAAAfABAAAUAPAAAAB3hAuEy4gPiM+Jk4wPjM+Ng42TkCeQR5DTkOeRC5GPkZuRo5HLlCOUw5TLlNeU35WDlY+Vl5WjliOWQ5gnmEufW59nn7+f15/roAegl6EDoR+hO6FboXOhe6GToaOhu6Hfoe+iE6JLooeik6K/osei/6Nzo5ujp//8AAAAAAB3hAOEw4gDiMOJg4wDjMuNg42PkAOQQ5DTkN+RA5GDkZeRo5HDlAOUw5TLlNOU35WDlYuVl5WflgOWQ5gnmEufW59nn7+f05/roAegi6DvoR+hN6FboXOhe6GToaOhu6HfoeuiE6JLooeik6K/osei/6Nzo5Ojp//8AAf/kHwMe1h4JHd0dsR0WHOgcvBy6HB8cGRv3G/Ub7xvSG9Eb0BvJGzwbFRsUGxMbEhrqGuka6BrnGtAayRpRGkkYhhiEGG8YaxhnGGEYQRgsGCYYIRgaGBUYFBgPGAwYBxf/F/0X9RfoF9oX2BfOF80XwBekF50XmwABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQYAAAEAAAAAAAAAAQIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEoAmgEgAWIBkAH4AnACwgMUA5YD3AQkBE4EoAU0Ba4GPgauBvQHVgfsCFAIigjgCRIJmgnkCkAKigsUC2oLvgwUDHQM1A1ADaYN+A42DmQOqA8CDzIPcA+aD9oQEhBAEGoQsBEAEfoSNhJmEnoSjhK6ExwTaBQuFIAU2hVIFYwV6BY+FpwXChdSF6wX4Bh4GN4ZHhmAGd4aGho8GmIahBqqGtwbDhtAG3IbhBwMHLgdOh1wHaYeEB5oHsgfFB8uH5QgAiBSIIog7iGgIgQiMCLiIzQjhCPUJDwkbCSmJNolNCViJZwl5iY+Jpgm0CdCJ64n+CgqKHIowik6KcQqJCquKw4rDgAAAAMAS//LA7UDNQALAB0AKQAABT4BNy4BJw4BBx4BEw4BBy4BJz4BNx4BFxQGBy4BJz4BNy4BJw4BBx4BAgC4+AUF+Li59wUF+LhijCIrMAEEzJybzQQxKyKMYj9TAQJSPz9TAQJSNQX4uLj4BQX4uLj4AR4BOScwfEebzQQEzZtHfDEoOUoBWkZDWgICWkNGWQAAAAAEAIn/8gN3Aw0ACwAXACIALQAAAT4BNy4BJw4BBx4BNy4BJz4BNx4BFw4BASEWJy4BJw4BBwY3Bjc0NjceARUWJwIAT2gCAmhPTmkCAmlOMEMBAUIxMkEBAUL+wgIaagEBxbCwxQEBVhEBnZSUnQEQAYACclVUbgICb1RVcT4CTDo5SgEBSTk6Tf4xAUZbsQYGsVtGQgENO4kGBok7DQEABQAh/6wD4ANUAAsAFwAsADgAVQAAAT4BNy4BJw4BBx4BNy4BJz4BNx4BFw4BByIGBxYXNjceARcWJyEGByEWJy4BAT4BNy4BJw4BBx4BNyImPQEjLgE0NjsBNTQ2MhYdATMyFhQGByMVFAYCaU5pAgJoT05pAgJpTjFCAQFCMTFCAQFCMTplKRsVP1mUnAEBEf6FAQoBcmoBAsT94lt8AgJ7XFx7AwN7XAsRUgsPDwtSERcQUgsPDwtSEAHHAnJVVW0CAm9TVXI/AUw7OUkBAUk5Ok13GRYWHB8BBok7DQEhIAFGW7H+IQJ8XFx7AgJ7XF17Sg4NWAEPFg9ZDA4ODFkPFg8BWA0OAAAAAAMAS//LA7UDNQALABcAJAAABT4BNy4BJw4BBx4BEx4BFw4BBy4BJz4BARcOASImJzc+ATceAQIAuPgFBfi4ufcFBfi4P1ICAVM/P1MBAVMBRAEziJaJMgEchmNjhTUF+Li4+AUF+Li4+AKnAlpDRloBAllGQ1r+DQU1Ojo1BSlBAgJBAAIAl///A2kDAQALABgAAAE+ATcuAScOAQceAQMhMjY1LgEnDgEHFBYCAEdjAgJjR0diAgJixgIaMioCv6iovwIqAYwBalJRZgEBZ1FRav5yHB1ZqAYGqFkdHAAABAAp/7ID2ANOAAsAGgAmAEMAAAE+ATcuAScOAQceARciBgceARUUByEyNicuAQE+ATcuAScOAQceATciJic1IyImNDY7ATU+ATIWFxUzMhYUBisBFQ4BAm9HYgICYkdHYgICYkc3XycvNggBbTIqAQG//etcewMCfFxcewICe10MEAFRDA8PDFEBEBcQAVEMDg4MUQEQAdkCaVJRZgEBZ1FRaU8YFCZuQSAfHRxZqP4sAnxbXHwCAnxcXHtKDgxZDxcPWQwODgxZDxcPWQwOAAIAXf/cA6QDJAAnAE4AAAUWNj8BNic2LwEmIg8BBicuAycmPwE+AS8BJiMmDwEOARUUHgI3Ii4CJzY3Njc+AR8BFhQPAQYUFx4DFxYyPwE2Mh8BFgYPAQYCzDdQIQknAQE5fR0/GyEODxI6MiwNCg4hGgEVVycuKSsMJCBu0NRdU8CrbwEBMgQFEycMUwcKJhYQEzc0QhoWMhYmChUKfRMBEgYuIwEhJQosKC8oVhQaIQ4KDDIyMxUODiEbPx19OAEnCSBQN13V0G5Ca6vEVEgtAwQQAhN9ChUKJhcxFho+NDoUEBYmCgZUDCcUCDEAAAUAJwAPA9kC8QANABcAHQAhAC4AADchMjY1ETQjISIGFREUCQE2MyEyFwEGIgURNRcHJgERJzcBIiclFxY3Fj8BBQYjrQKyOz+G/U46QAGm/rwOFAKqFA/+vRsy/oD39gEDMPX1/RMSDQEAHCwtLC0cAQAOEw9CQwHZhEJC/ieFAVsBQAYH/sEbuwHZBPLzBAHc/iLx8f3gBv0bKwEBKxv9BgAAAgAo/74D2AM5ABkAMAAAJTYXFjM+ATcuAScOAQcUFh8BMiMXHgEXNzYBNiQ3FgQXBgQHIicxJgYHBj4BLwEuAQFlKSsjJK/kBATkr6/kBEQ/EgECBxkZARoM/ssFAQrJyQEKBQX+9skrKCtZaC1EIx0XSlVkDgkFBLyJibwEBLyJRnwvDQQTLxsNBgFMqOAEBOCoqd8FBghFHwxHVhkQN5kAAAADAAD/tQQAAuUAJwBAAFkAABcyNj8BFhczFx4BMz4BPQEzPgE3NS4BJyM1LgEnIQ4BBxEeARczFRQ3LgErASImJxE+ATMhMhYXFSMOAQcVFBcHBScuASsBIiYnNT4BMyEyFhcVDgErASIGB/ENGRB4J0t7dxAWDRIUD0RQAQFQRDgBUEn+AkdTAQFTRy82AQ8MRjA0AQE0MAH3MDQB6UdNAQeDAiZyChIOdi0xAQExLQFULTEBATEtJgwPASMMD2srAWYNDwEXFVUBTEfSR0wBG0lPAQFPSf63SU8BYSqjEA4zMgFFMjMzMhkBTEfSHRh5J2cJBzAv0C8wMC/QLzAODwAAAAEAc//xA40DDwAsAAAlHgEXFjc+ATU0Ji8BJiMGDwEGIicuAycmND8BNjc0LwEmByIGBw4BFR4BAUxf0V5TOxITDQ+EHRccHB8HFAcUPUEzCwUGHh4BFVwYJBUqEx8dAnjNXnsCAT8TKxYQHgtdFQEeHgYEDDNBPRQIEgcgHBwXHoEfARMSHkkpXs8ABAAnAA8D2QLxAAoAEQAYACQAAAEWNwEmIyEiBwEWBQkBBhURFAU2NRE0JwkBITI3AQcGIi8BARYCARobAXQYP/1ONxUBdxv+SwEv/tAKA6gKCf7R/gwCsjYV/swdKlwqHf7MGAFPARwBcRYV/o4c+gErASwSLP4nLhITLQHZKxL+1f6QFAEyHCoqHP7PFQAAAQAo/74D2AM5ABYAABM2JDcWBBcGBAciJzEmBgcGPgEvAS4BKAUBCsnJAQoFBf72ySsoK1loLUQjHRdKVQGtqOAEBOCoqd8FBghFHwxHVhkQN5kAAgAA/7IEAALtABwANQAAFzI2PwEuASc1PgE7AScuASchDgEHER4BFzMVFBYFPgE9ATM+ATc1LgEnIQ4BHQEUFhczFx4B5gsSDWoQFQEBXlT+AQRHPv4PP0oBAUo/PBECWQ8RJj9KAQFKP/6aQkhIQnODDRIbCwxiCy4s8FRdDjhCAQFGQf6cQksBaREUMwEUEGoBS0HdQUYBAUZB3UFLAXcMDAAHACAAGgP6AzYACwAgACwAOABEAE0AVgAAATYmBwYmNzYWBwYmAS4BJzQ2Nz4BBwY2NzYWBwYWFxYCAy4BBw4BFx4BNz4BAwYWNzYWBwYWNzYmAQ4BJy4BNz4BFx4BIyYOAR4BPgEmNyYOAR4BPgEmAxsJLCMgCx5JWRMNNf6enPEGS0SU0yEEGANzhCIECQu3zhoLrXp6mAULrXp6mBkjDiVqgxwGPQ8nuP7XGnU9OioYG2w7PDG5EygXCSUqGAsqBw8IAw8QCAQCEyMxBgM2CwxlRh0R/iYBh3g/ikSNBIYRBQEvMF8NCQNN/ssBAVBaCg56UVBbCg96AkIMPwMRkGkkFCGTzP2hODESFV80MzAOEV0IDiUkEQ4mJCEDBg4NBwYPDQAAAAYAVf/2BBwDCgAWAB8AKAA5AEMATQAAATIXLgEnDgEHFBYXBzceATMyNyY1PgEnMhYUBiImNDYHIiY0NjIWFAYBLgEnDgEHHgEXMjY3Fyc+ASUiJjQ2Nx4BFAYzIiY0NjceARQGAuMREBm/gZHBBEhBIncgOR8QEAoCozsUFxcoHh7bFB8fJxcXAs0EqHd9owMDo30ZNBpeGjRD/oAOFBQOExcXqQ4UFQ0TFxcCGwJqhQIDo31GdC1nPAcKASImc5hgFycXFycXVRcnFxcnF/7uaYwDA4xpaowDCwc0VidkZRUaFAEBFBsUFRoUAQEUGxQAAAAJAHr/+gOGAwYABwAQABgAIAAoAEAASABQAFgAACUOAR8BPgE3JRUWFzI3JyYGEyIHFxY3NSYFBgcUFzc2JzcOAQchMjYnBxUUHwEWOwEyPwE2PQE0LwEmKwEiDwEGJQcGFzM2NzQDERQWPwEuAQEeARcRLgEHAfUCAgKQPGMj/hRNWigl7QIFpycl7gUBTf5FJQEI7gMGETxjIwFRAgICtgJeAgOFAwJeAgJeAgOFAwJeAgIm7QQGzCUBrQUCkBZN/YQVTjUBBAKoAQQCkBVNNUXNJQEI7QICAhcH7gMFzSXdTVsoJe4FAsgWTTUFAp2EAwJeAgJeAgOEBAJdAwNdAwftBQJNWycBHf6wAgICkDxj/lw8YyMBUQICAgAAAAAFACj/xQPYAzsAGAAxADoAQwBMAAAFMjY/ASE+ATURNCYjISIGFREUFhczFRQWNzU0JisBIiY1ETQ2MyEyFhURFAYjISIGBwMuASIGFBYyNjcuASIGFBYyNjc0JiIGFBYyNgEvEBsTlAETYGRkYP3YYGRkYBUYKA8RNUE+PkECKEE+PkH+6hEXDFEBIDAgIDAgxAEgMCAgMCDEITAgIDAhOxERgwFlXwFIX2VlX/64X2UBbxkdTnwSD0A/AUg/QEA//rg/QAgNAScYICAwISEYGCAgMCEhGBggIDAhIQAAAAEAnP/ZA2QDJgApAAAlLgEnFAYHHgEHBiYnDgEnJjY3LgE1DgEHIiY3Nj8BJjY3HgEHFxYXFgYDWhE2AykrGDsIE8A0NMATCDsYKykDNhEIAhoMECYFgI2MgAQmEAwaAnEETQYoWiYHHhQOAgYGAg4UHgcmWigGTQROVigoX5TKBATIll8oKFZOAAAABACSAKUDbgJbAA8AHwAtAD8AABMVHgEzITI2PQE0JiMhIgYnITIWFREUBiMhIiYnET4BBRUUHwEWNjc1LgEPAQYnNz4BHgEVERQOASYvASY9ATTbARQQASUPFRUP/tsQFAEBbh4rKx7+kh4qAQEqAjEHJAkUAQEUCSQHKW0JFBQLCxQUCW0OAe7cDxUVD9wPFRVeKx7+3B4rKx4BJB4rtUwJBR4GCguGCwoGHgUlWAYDCRIL/uILEgkCB1gLEXARAAAAAAUACP/nA/gDGQAbADsARwBVAGQAABchNjcRJisBIiYvAS4BKwEiBg8BDgErASIHERY3IiY1ETQ2OwEyNj8BPgE7ATIWHwEeATsBMhYVERQGIyU+ATcuAScOAQceAQEyNjc0LgEiDgEVFBYXAS4BJz4BNzIeAhQOAo8C4oYBAYZkGBoNIw8nIasgKA8jDRoYYYYBAYcgIyMgcR0kECIRHhx/HB4RIhAkHXQgIyMg/pBkgwMDg2RkgwMDgwGYFh4BDhkcGQ4eF/7MSF8CAl9IIj0wGhowPRkBhAHBhA0QJhITExImEA2E/j+ERCIiAbkiIQ4SJRQPDxQlEg4hIv5HIiJEA4RkZIQCAoRkZIQBUR4WDxgODhgPFh4B/u8BYEhIXwIZMD5EPjAZAAAAAAMAzf+1AzMDSwANABkAQgAAAREuAScOAQcRHgEXPgEnFAYiJjURNDYyFhcBIgYUFjMhMjY0JisBNT4BNzU0JiIGHQEOAQcuASc1NCYiBgcVHgEXFQKcAVVGRlUBAVVGRlVAMVMyMlMxAf7lDhISDgF/DRMTDaB9lAITGxMBgXBvggETGhMBApR9AZUBDktbAgJbS/7yS1wBAVxLMDg4MAEOMDc3MP1TExsTExsTZAyggFcNExMNVW+CAgKCb1UNExMNV4CgDGQAAgDJ/8QDNwM3ABAAHwAAAS4BJw4BBx4BHwEWMj8BPgElPgE3HgEXBgIHBiInJgIC7gKCamqCAgJsWQoLJAsKWWz93QOwhISwAwm4SRQzE0m4AduBkQEBkYFL0nUODQ0OddNKprUBAbWmg/7YVhYWVQEpAAACAN3/xAMjAzwADQA2AAABES4BJw4BBxEeARc+AQEOARQWMyEyNjQmJyM1PgE3NTQmIgYHFQ4BBy4BJzUuASIGHQEeARcVAoIBRzo6RwEBRzo6R/63DRMTDQGQDRMTDah3iwESGhIBAX1mZn0BARIaEgGLdgGAATI9TAEBTD3+zjxNAQFN/sEBExoTExoTAV4MmndlDRISDWVkfAICfGRlDRISDWV3mgxeAAAAAgDJ/8QDNwM3AA4AGgAAEz4BNx4BFwYCBwYiJyYCJT4BNy4BJw4BBx4ByQOwhISwAwm4SRQzE0m4AS4vPgEBPi8vPgEBPgHbprUBAbWmg/7YVhYWVQEpOgE+Ly8+AQE+Ly8+AAUAeP/AA4cDQAARAB0APgBKAFkAAAEeAR0BFxEuAScOAQ8BFzU+AQEWMjY0JwEmIgYUFxMiBhQWMyEyNjQmKwE1NjcnBgcuASc1NCYiBh0BHgEXFQE0JiIGBxUUBxc2NQUyNjcnBiMiJic1JxUeAQHoJSxCAk9COkwJAT8BLAGLChsTCv00ChsUCp4NExMNAZANExMNp1I7LjVLZnwCEhsSAop3AUMSGhIBBTUP/tUZJA41Bg8iJgFCAU0DAgEzJ85CAQ5EVgEBQzYNPiwnM/0dChMbCgLNChQbCv0WExoTExoTXggqLiQBAn1kZQ0SEg1ld5sMXgHhDRISDWUZGTMuN5MJCTQHKSIaQ1RKSQAAAwAG//UD+gMLAAwAHwArAAAXITI3ESYnISIHERYzAS4BDwEnJiciDwERNjMhMhYVESU+ATcuAScOAQceAY0C5oYBAYb9GoYBAYYCQR1HHcFQGx4dGoABQQLkICL9kio5AQE5Kis5AQE5CoQCDIQBhf30hQGMGgEbrUgYARhzAdhDISL+J9MBOiorOQICOSsqOQAAAAQAQv/RA74DLwAbACUALAA4AAAFMj8BNjURJiciDwEnJiIPAQYVERQWMzI/ARcWJSI1ETQ/AREHBgUmLwERHwETETc2NxYXERQPAQYCjRgT4SUBMA8U5OkTMBTeJhoXDxXZ7Rj+GAYOwMIDAdYJCboNv0XCBAIFAQ6sCi8LfxUrAlIwAQt+jgwMfxUq/a4YGgx1hQxpBwITDwlv/cxrAQ4FBWkCMgh0/c8CNWkCAQEG/e0QCGQGAAADAFb/zQOmAzAACQARACkAAAE3NjQvASYGDwEBNwEnAQcGFgMhMjY3EQcRDgEjISInETYzITchIgcRFgN8HwsLCgobCx/+TVMBezv+hicCCasB9zo/AUUBHhf+C0ECAkEBc0X+R4YBAQLKHwwbCwsKAgof/gckAXo6/oZQBgr+w0NCAd1F/mshIkMB50NFhP4ShQAABgBq/6EDlgNfAB8AKQAzAEAATQBZAAAlEzMyNjQmJyM1NCYnIw4BBxUjDgEUFjsBEx4BFyE+AQE0NjsBMhYdASMDLgEnAyEDDgEHJzI2NxM0JiIGBwMUFiMyNjUDNCYiBhUTHgE3ETQmIgYHER4BMjYDLh4rDRISDbw5MqEyOAG6DRMTDSsdAzgvAYkuOP5eGBSWFBjuRxMYAR4CDxwBGBQ/Cw4BDA4VDgENDvMLDg0PFQ4NAQ2kDxUPAQEPFQ8GAnMSGxMBQC42AQE2LkABEhwS/Y0vNQEBNQMfEhcXEjz9JwEYEwJs/ZQTGAFMDw0BxA0PDwz+OwwQEAwBxQwPDw3+PA0PHAHFDA8PDP47DBAQAAAAAgCE/5wDfQNkABoAOAAAJTI2NREnFxYyNjQvASYiDwEOARYyPwEHERQWAyE2JxE2JyMVMzIWFREUBiMhJicRNjczNSMiFREUAgAOFAJdChsSCpEMGgyRCQERHApeAxToAeyHAQGHd3YgIiIg/hdCAQFCdniG7BMOAbhAYwoQGwmMDAyMCRoRCmRB/kgOE/6wAYQBp4QBRSIi/mEiIgFDAZ9DAUWF/lmFAAAAAAIAhP+xA30DTgAaADgAACUyPwE2NCYiDwE3ETQmIgYVERcnJiIGFh8BFgMhMicRNicjFTMyFhURFAYjISInETY3MzUjBhURFAIADQyRChIbCl0CFBwUA14KHBEBCpAM6QHshwEBh3x7ICIiIP4XQgEBQnp8hssMiwobEApkQAHEDhMTDv48QGQKEBsKiw3+6IQBu4QBRSMh/k0iIUMBs0MBRQGE/kWFAAMAS//LA7UDNQALABcANAAABT4BNy4BJw4BBx4BNy4BJz4BNx4BFw4BJTI/ARcWMjY0LwE3NjQmIg8BJyYiBhQfAQcGFBYCALj4BQX4uLn3BQX4uJvNBATMnJvNBAPO/tIPCnp5Ch4TCnp7ChQcCnt7ChwUCnp6ChQ1Bfi4uPgFBfi4uPhEBMycm80EBM2bnMyyC3p6ChMeCXp7ChwUCnt6ChMdCnp6CR4TAAACADH/9gPPAwkAIAA+AAAFMjY3ATY0JwEuASMiBh0BIwYCFx4BMxY2Nz4BFzMVFBY3Ij0BNAcjDgEHBiI1PgE3MxY9ATQ2MhcBFhQHAQYCKw8cEAFSFxf+rhIZDxccDebVAQEZEg4bCzinew0cLwYOOpnCJQIFAqzZOg4DBwMBMQUF/s8ECQ4OAT8YLBgBPBAPHheiAv7w8BwdAQ0TaFABpBYcXAamDwEBX1IEBZ7xBwEPqgMDA/7bBAgE/t8EAAACADH/9gPPAwkAIAA+AAAFMjY9ATM2FhceATcyNjc2AicjNTQmIyIGBwEGFBcBHgEnIicBJjQ3ATYyFh0BFDczHgEXFCInLgEnIyYdARQB1RYcDXunNwwbDhIZAQHV5g0cFw8aEf6uFxcBUhAbCQME/s8FBQExAwcDDjrZrAIGASXCmToOCRwWpAFQZxQNAR0c8AEQA6IWHg8Q/sQYLBj+wQ4OXAQBIQQIBAElAwMDqg8BB/GfBARSXwEBD6YGAAADAEv/ywO1AzUACwAXAEMAAAU+ATcuAScOAQceATcuASc+ATceARcOAQEeARc+ATc0JiIGFQ4BBy4BJz4BNzIXBwYeATI/ATY0LwEmIgYUHwEmIw4BAgC4+AUF+Li59wUF+LibzQQEzJybzQQDzv6lAmxSUWsCERgRAko4OUoCAko5CAcqCAEOFwhTCAhSCBgOBx4GBkpqNQX4uLj4BQX4uLj4RATMnJvNBATNm5zMAVRSbQICbVEMEBAMOUoCAko5OEoCASkIGA8IUwgXCVQIEBcIHwECaQACABz/sQPkA0kAGQA9AAAXFj8BFxY2JwM3NiYjBQMmIgcDJSIGHwEDBjciPwE2LwEmNjMFFj8BNjIfARY3JTIWDwEGHwEWBi8BJg8BBtsaKOPjKDUQWeUoFDL+51UPQRBV/ucxFSnlWhBZAQFVCRbVAwEEAQMaCEoCAwFKCBoBBAMBA9UWClUBAgPOFhXPAjwTHqamHicuAQukHD8CAQwvL/70Aj8cpP71LkEE9RkPkwIDBQEa+AQE+BoBBQMCkw8Z9QQCA50QEJ4CAAADAEv/ywO1AzUACwAXADQAAAU+ATcuAScOAQceATcuASc+ATceARcOASc+ATc1MzI2NCYnIzUuASIGHQEjDgEUFjsBFRQWAgC4+AUF+Li59wUF+LibzQQEzJybzQQDzpwREwGGEhYVE4YBEyIShhMWFxKGEjUF+Li4+AUF+Li4+EQEzJybzQQEzZuczJwBFRN/EiMSAYYTFhYThgESIxJ/EhYAAAMAS//LA7UDNQALABcAIwAABT4BNy4BJw4BBx4BNy4BJz4BNx4BFw4BASEyNjQmIyEiBhQWAgC4+AUF+Li59wUF+LibzQQEzJybzQQDzv64AVkSFhUT/qcTFhc1Bfi4uPgFBfi4uPhEBMycm80EBM2bnMwBRBIiExMiEgACAEv/ywO1AzUACwAXAAAFPgE3LgEnDgEHHgE3LgEnPgE3HgEXDgECALj4BQX4uLn3BQX4uJvNBATMnJvNBAPONQX4uLj4BQX4uLj4RATMnJvNBATNm5zMAAAAAAIAS//LA7UDNQALACgAAAU+ATcuAScOAQceATciJjQ/AScmNDYyHwE3Nh4CDwEXFhQGIi8BBwYCALj4BQX4uLn3BQX4Hg8VC4CACxUeCoGBCx0UAQuAgAoVHgqAgAs1Bfi4uPgFBfi4uPjyFR4KgYAKHhQKgIAMARQeCoGACh8VCoGBCgAAAAACAEv/ywO1AzUACwA3AAAFPgE3LgEnDgEHHgEDPgE3MhcnJjQ2Mh8BHgEPAQYiJjQ/ASYHDgEHHgEXPgE3NDYyFhUOAQcuAQIAuPgFBfi4ufcFBfgLA2tMBgYfBw8YCFQHAQhUCRcPCCoHCDpMAQFMOjlLAhEZEQJtUlNuNQX4uLj4BQX4uLj4AZ5SawIBHwgYEAhWCBgIVAgPGAgqAQEBSzk6SwICSzoMEREMU24CAm8AAAABABz/sQPkA0kAGQAAFxY/ARcWNicDNzYmIwUDJiIHAyUiBh8BAwbbGijj4yg1EFnlKBQy/udVD0EQVf7nMRUp5VoQPBMepqYeJy4BC6QcPwIBDC8v/vQCPxyk/vUuAAACAEv/ywO1AzUACwAoAAAFPgE3LgEnDgEHHgE3IiY9ASMiJjQ2OwE1NDYyFh0BMzIWDgErARUUBgIAuPgFBfi4ufcFBfi3EhONExcWFI0TJBSNFBcBFhSNFDUF+Li4+AUF+Li4+NsXE4UTJROOExcWFI4TJROFFBYAAAAAAgBL/8sDtQM1AAsAFwAABT4BNy4BJw4BBx4BEyImNDYzITIWFAYjAgC4+AUF+Li59wUF+AMUFxcUAWoUFhcTNQX4uLj4BQX4uLj4AYoTJRMTJRMAAwBL/8sDtQM1AAsAFwAjAAAFPgE3LgEnDgEHHgE3LgEnPgE3HgEXDgEnPgE3LgEnDgEHHgECALj4BQX4uLn3BQX4uJvNBATMnJvNBAPOm3SeAwOedHadAwOeNQX4uLj4BQX4uLj4RATMnJvNBATNm5zMUgOedXWeAwOedXWeAAACAEn/yQO3AzcACwAgAAAFLgEnPgE3HgEXDgETJiIPAQYiLwEmIgYUHwEWMj8BNjQCALr4BQX4urr4BQX4CwkaCcgKGQk7ChkTCWgJGQr0CTcF+Lq6+AUF+Lq6+AJGCQnICgo6ChMZCmcJCfQKGQAAAQDjAGMDHQKdABsAADcGFBYyPwEXFjI2NC8BNzY0JiIPAScmIgYUHwHuCxYfDNzcCx8XC9zcCxcfC9zcDB8WC9ukCx8XC9zcCxcfC9zcCx8XC9zcCxcfC9wAAAABAQAAgAMAAtgAFgAAJS4BJz4BNzUXBzUOAQceARc+ATczDgECAG2QAwOQbcDAXHoCAnpcXHoCKAOQgAOQbW2QA1iAb28CelxcegICelxtkQAAAAABAEv/nQO1A14AKQAABT4BNy4BJyYOARYXHgEXDgEHLgEnPgE3FR4BPwE2NC8BJgYHFQ4BBx4BAgC4+AUBYlQPHREHDUVRAQPOm5vNBAOafAEZEooODokSGgGZxgMF+GMF+LhtuD0LBRwbCjKYXZvNBATNm4XAHj4WDA1gChsLYAwLFz0g66K4+AAAAAIAHP+xA+QDSQAZAC0AABcWPwEXFjYnAzc2JiMFAyYiBwMlIgYfAQMGJRE2HwEWNyUyFg8BBh8BFgYvASbbGijj4yg1EFnlKBQy/udVD0EQVf7nMRUp5VoQAUACAUoIGgEEAwED1RYKVQECA84KPBMepqYeJy4BC6QcPwIBDC8v/vQCPxyk/vUu5wIiAQT4GgEFAwKTDxn1BAIDnQgAAAAMAB3/nQPjA2MADAAZACYAMwBAAE0AWgBnAHQAgQCOAJsAAAEiBgcVHgEyNjc1LgEHDgEfAR4BPgEvAS4BBSYGDwEGHgE2PwE2JgUGFh8BFj4BJi8BJgYFLgEPAQ4BHgE/AT4BFzQmJyMOARQWFzM+ASUUFhczPgE0JicjDgEFNiYvASYOARYfARY2JR4BPwE+AS4BDwEOAQU+AS8BLgEOAR8BHgElFjY/ATYuAQYPAQYWFzI2NzUuASIGBxUeAQIADREBAREaEQEBEf4MBgZMBxgWBwdMBxcB2AwXCEwGBxYYB0wGBv1gBgcLhAsYDQYMhAsYAz8HGAuFCwYNFwyECwc6EQ6YDRERDZgOEfw6EQ2ZDRERDZkNEQOGBgcLhAwXDQYLhAwY/MEHGAuFCwYNGAuECwcCmgwGBkwHGBcGBkwIF/4pCxcISwcGGBcHTAYG/Q0RAQERGhEBAREDYxEOmA0REQ2YDhFABxgLhQsGDRgLhAsHBgYHC4QLGQwGC4ULGKoMFwhMBgYXGAdMBgYMDAYGTAcYFwYGTAgX5g0RAQERGhEBARENDREBAREaEQEBEf4LFwhMBgYXGAdMBgYMDAYGTAcYFwYGTAgXvQcYC4ULBg0XDIQLBwYGBwuEDBcNBgyECxhHEQ6YDRERDZgOEQAAAAIAbf/pA5QDFwAVACEAACUyNjcXFjI+AS8BPgE3LgEnDgEHHgE3LgEnPgE3HgEXDgEBtjRhK8sOKhoBDsogIwEEuoyMugMDuoxtkwICk21tkwMDk4QgHssOGykPyiplOYu7AwO7i4y6QwOTbW2SAwOSbW2TAAAAAAEAnwAXA2EC6AAcAAAlPgE1ESE+ATQmIyERNCYiBhURISIGFBYXIREUFgIAEBYBFRAWFhD+6xYgFv7rEBYWEAEVFhcBFQ8BHQEWIBYBHg8VFQ/+4hYgFgH+4w8VAAAAAAEBQABAAsACwAAFAAABNwkBJwEBQEEBP/7BQQD/An9B/sD+wEEA/wABAUAAQALAAsAABQAAAScJATcDAsBB/sEBP0H/An9B/sD+wEEA/wAAAQC4AIUDWgJ/ABcAAAEXFhQHAQYiLwEmND8BNjIfARYyNwE2MgNDDQoK/lwLHQy1CwsNCx0LdQwdCwFjCx0CdA0LHQv+XAsLtgsdDAwLC3UKCgFjCwAAAAIAC/+9A/UDQwAnAD0AABchPgE1ERcWFzI2NyYvATU0JicjDgEdAScmIgcBBgceATM2PwERFBYBNCYrASIGFREjJicRAT4BFwERBgcj5wIzLjI3DRIQFAEBDJURDjgOEaoXOBf+SwwBARQQEg03MwHCEQ+2DxKPKQEBJgcQBwEmASmQQwExLQGHMg4BEg8TCof9DhABARAOkZoVFf5yCRMPEgEOMv55LjABYQ8REQ/+4wEqAbUBDAYBB/70/ksqAQAAAAADAEv/ywO1AzUACwAXACwAAAU+ATcuAScOAQceATcuASc+ATceARcOASUyPwE2Mh8BFjI2JicDJiIHAwYUFgIAuPgFBfi4ufcFBfi4m80EBMycm80EA87+tQsGlAYKBpMIFQ0BA6QLKgqlAww1Bfi4uPgFBfi4uPhEBMycm80EBM2bnMyIB5QFBZQHDRMJAaMaGv5dCBQNAAQAVv/TA6wDKgAtAGYAcgB+AAAlNjc+ATc2NyY2NyYnBiY3NSYnBwYiLwEHFRYGJyMHFxYUDwEWFzM2FgcWFz4BByYnNzYmDwEmJzc2NC8BNjcXFjYvATY3FxYyPwEWFwcGFj8BFhcHBhQfAQYHJyYGHwEGBycmIg8BEz4BNy4BJw4BBx4BFy4BJz4BNx4BFw4BAoYPDwJlTQcGNwI4BAZSaQILDAI6lzoGEwJrUwoHBj09AwUFBFJrAQ4OOJGsQzsCATovMSMRJyEhKQ8gOC86AQM4PiUgUyAiQDcCATsuLCMOHiIiHBElJS86AQI8RBkgUyAcXy8+AQE+Ly8+AQE+L0ZdAgJdRkZdAgJdGwYHTWUCDw85kTgODgFrUgQFBQM9PQcIClNrAhMGOpc5AwwLAmlSBgQ4An4OIywvOgECN0AiIFMgJT44AwE6LzggDykhIScRIzEvOgECO0McIFMgGUQ8AgE7LiUlERwiIh8BPwE+Ly8+AQE+Ly8+OAJdRkZdAgJdRkZdAAAAAAMAP/+/A8EDQQAUACAALQAABTI2NwE2NCYiBwEOARUUFhcFEx4BAyUmNDclNj8BBwYHAyInAwE+ATcHBgcDBgJXFyIMARkMGCse/R8cJCgfATVaCRxr/tgKCQJEGRkxLhcSmAQDWgEmEigRFwwK2wRBJR8C3R4rGAz+5QohFx0cCVr+ziEpAb1aAwgE2woMFyUTEv15CgEoAScSMBYxGRr9vAkABABL/8sDtQM1AAsAFwAgADkAAAU+ATcuAScOAQceATcuASc+ATceARcOAQMyNjQmIgYUFgMzPgE0JisBNTQmKwEiBhQWOwEVIyIGFBYCALj4BQX4uLn3BQX4uJvNBATMnJvNBAPOnxgfHzAfIDCuDhERDjUREFENEhINLjUOERE1Bfi4uPgFBfi4uPhEBMycm80EBM2bnMwCASAvICAvIP57ARAaEdoSFREaEcURGhAAAAAABABL/8sDtQM1AAsAFwA8AEUAAAU+ATcuAScOAQceATcuASc+ATceARcOAQM+AT0BNDY3PgE3LgEOAQcGFRQWMzI2NzY3HgEVFAYHDgEdARQXPgE0JiIGFBYCALj4BQX4uLn3BQX4uJvNBATMnJvNBAPOpBATFRYgJwECTnBFCQQSCxIPCRUrHSMbHBgeIRMbGicbGzUF+Li4+AUF+Li4+EQEzJybzQQEzZuczAEPARENBREbDxMvJTY4ASseCwsODxEMJQEBHRkVHhIQJx8GIoABGSYZGSYZAAAAAAMA0f/LAy8DNQAUABwAKwAAASIGBxUGFREUFjMhMjY1ETQnNS4BBz4BMhYXFSEFMhYVERQGIyEGNRE0NjMCAF2FA0owMAGeMDBKA4X9AlqIWgL+wAFtDw4OD/5mHQ4PAzWBg2IJW/7FNDExNAE7Wwlig4H7Wl9fWmlBDhL+vBIPASIBRBIOAAAGAGQBLgOdAdMACAASABsAJQAuADgAAAEeARQGIiY0NjcOARQWMjY0JicFHgEUBiImNDY3DgEUFjI2NCYnBR4BFAYiJjQ2Nw4BFBYyNjQmJwIAFBoaKBoaFCMvL0YvLyP+thMbGycaGhQkLi5HLy8jApUUGhonGxsTIy8vRy4uJAGvARooGhooGiUBL0YvL0YvASQBGigaGigaJQEvRi8vRi8BJAEaKBoaKBolAS9GLy9GLwEAAAAAAgCD/9sDfQMlACEANAAAFz4BNzU+ATceARcyPgI3ES4BIw4BBy4BJyIOAgcRHgEBLgEnIgYHET4BMx4BFzY3EQ4BpA4SAQg6MHO4bTE1LRoBARkTD0A3brd0MTUtGgEBEgI5Z7l4JDwSBDYybrhzRiwFNSUBEg7uBA8BBUQFCxUkHQG0ERMBEAEFRAULFSQd/TgOEgEVBUQFCAgBkwsWBEQFAQ3+bwsWAAAAAAIAC/+9A/UDQwAhADkAABMeATM2NwE2MhcBFhcyNjcmLwE1NCYnIw4BHQEnJiIHAQYTFBYXMxE0NjczHgEVETM+ATURASYiBwELARQQEg0BogcQBwGiDRIQFAEBDJUQDjkOEaoXOBf+Swx6My2uEg+XDxKtLjL+lAcPB/6VAYMPEgEOAX0HB/6DDgESDxMKh/0OEAEBEA6SmxUV/nMK/oYtMQEBMQ8RAQERD/7PATEtATkBSAcH/rYAAAAAAgBW/9MDrAMqADgARAAABSYnNzYmDwEmJzc2NC8BNjcXFjYvATY3FxYyPwEWFwcGFj8BFhcHBhQfAQYHJyYGHwEGBycmIg8BNz4BNy4BJw4BBx4BAaFDOwIBOi8xIxEnISEpDyA4LzoBAzg+JSBTICJANwIBOy4sIw4eIiIcESUlLzoBAjxEGSBTIBxfTmcCAmdOTmcCAmcsDiMsLzoBAjdAIiBTICU+OAMBOi84IA8pISEnESMxLzoBAjtDHCBTIBlEPAIBOy4lJREcIiIf9gJnTk5nAgJnTk5nAAMAS//LA7UDNQALABQALQAABT4BNy4BJw4BBx4BEyImNDYyHgEGAy4BNDY7ATUjIiY0NjsBMhYdATMyFhQGBwIAuPgFBfi4ufcFBfi0FyAfMB8BIV4OEREONS4NEhINURARNQ4REQ41Bfi4uPgFBfi4uPgCSSAvICAvIP57ARAaEcURGhEVEtoRGhABAAAAAAMAS//LA7UDNQALADAAOQAABT4BNy4BJw4BBx4BEyI9ATQ2Nz4BNTQmJwYHDgEjIiYnNDc+AhYXDgEHDgEdARQGByImNDYyFg4BAgC4+AUF+Li59wUF+LAkHxkeHCQfLRUKERILEwEECUl2UgIBKSEXGBIQExwcJxwBHDUF+Li4+AUF+Li4+AFTIwYhKRETIBYaHgECJg0REA8LCyAtATs4JzEVDxwTBQ4SgRopGRkpGgAAAAMAZAEuA50B0wAJABMAHQAAAQ4BFBYyNjQmJyEOARQWMjY0JichDgEUFjI2NCYnAgAjLy9GLy8j/rYkLi5HLy8jApUjLy9HLi4kAdMBL0YvL0YvAQEvRi8vRi8BAS9GLy9GLwEAAAAABgBQABMDsALsABgAIQA5AEIAWwBkAAABMjY3MzI2NCYrAS4BIgYHISIOARYzIR4BNy4BNDYyFhQGBSIGFBYXMx4BMjY3IT4CJichLgEiBgcXIiY0Nh4BFAYBPgE3MzI2NCYnIy4BIgYHIQ4BHgEzIR4BNyImNDYyHgEGApohNAyUDRMTDZQMM0Q0C/46DxMBFQ4Bxgs0IhYcHSocHP3CDRMTDZkLNEQ0CwHBDxMBFQ7+Pws0RDMMYRUdHSscHAEZIjMLlQ0TEw2VCzRDNAv+Og4VARMPAcYLNCIVHRwrHAEeAh8lHxQdFB4mJh4UHRQfJTQBHCsdHCwcshMeEwEeJiUfARMdFAEeJSUeVR0qHQEcKxz+xQElHxMeEwEfJCQfARQdEx8lNB0rHBwrHQAAAAYAUQBHA7ACuQAIABQAHQApADIAPgAAEz4BNCYOARQWNyEyNjQmJyEOARQWAzI2NCYiBhQWNyE+AS4BJyEOARQWAz4BNCYOARQWNyEyNjQmJyEOARQWhhYgIC0eHtUCSQ8TEw/9tw8TE68WICAtHh7VAkkOFQETD/23DxMTrxcfIC0eHtUCSQ8TEw/9tw8TEwJNAR8sIAEeLh4TEx4TAQETHhP+6SAsIB8uHhIBFB0TAQETHhP+6QEeLSABHi4eExMeEwEBEx4TAAAAAAMAkgClA20CWwAMABkAJgAAEz4BMyEyFhQGByEiJhU+ATchHgEUBgchLgEVPgE3IR4BFAYjISImkgEUEAKSEBQUEP1uDxYBFBACkhAUFBD9bg8WARQQApIQFBQQ/W4PFgI3DxUVHxQBFqgQFAEBFCAUAQEVqBAUAQEUHxUVAAAAAgAE/88D/AMYAB0AOwAAASMuAScOAQcGHgE2Nz4BNx4BFyMiBh8BFjI/ATYmBTMeARc+ATc2LgEGBw4BBy4BJzM+AS8BLgEPAQYWA9o4FeilX6M7CwIZGgsyiU2HwRM9FgsMXAoaCl0MC/w1OBXopV+jOwsCGBsKMIlQiMATPRYLDFwKGgpdDAsBn6HUBAFORA0dEQQMOT4BA6qGGRGEDg6DEhlYodMEAU5DDh0RBAw4PwECqoYBGBKDDgEPgxEZAAAAAAEAav+3A50DUAAzAAAJAQYuAjcBPgEXFgYHAQYuAjcBPgEmBgcBDgEXFjY3ATY0Jy4BBwEGFhceATcBNi4BBgMm/sU/kG0DPAGuJl4lIgYl/lwQIhcDDwElCgETGAr+2SABHiBTIgGmPDU1jD/+UE4ESEvDUwE9CgETGgFw/sU9BG2PQAGtJgcjJV4m/lwQBBchEQElChgTAQr+2iJVHiACIQGmPos2NAE8/lBTw0tIBE4BPQocEwEAAAAAAwAAAC8EAAKyAAsAFwAgAAAlNiQ3JiQnBgQHFgQ3LgEnPgE3HgEXDgEnMjY0JiIGFBYCAOcBFQQE/urm5f7pBAQBGORadwICd1padwICd1ogLCtBLCwvDe5HRu4NDe5GR+5iA3dZWnYCAnZaWXeELEArK0AsAAAAAQCMAK8DdAJRABAAADcGFBYyNwkBFjI2NCcBJiIHlwsWIgsBMQExCyIWC/60DCIM8QojFQsBOP7ICxUjCgFUDAwAAAABAIwArgN0AlIAEQAAJTY3ATY0JgYHCQEuAQYUFwEWAgARDAFMCxcgDP7P/s8MIBcLAUwMrgEMAVQLIBgBC/7IATgLARggDP6tDAAAAQEvAAwC0QL0ABAAACUWMjY0JwkBNjQmIgcBBhQXAo8LIhUL/sgBOAsVIgv+rAwMFwsWIQwBMQExDCEWC/60DCIMAAABAS4ADALRAvQAEQAAJTI3ATY0JwEmIgYWFwkBBhQWAVYQDAFTDAz+rQwgGAELATj+yAsWDAsBTA0hDAFLDBcgDP7P/s8LIhYAAAAAAQC7/+sDRQMVABwAAAUyNjURJx8BFjI2NCcBJiIHAQYUFjI/AgcRFBYCABEVA4BiCyAVDP7kDSAM/uMMFSALYoADFRUVEQI0XI1gChUfDQEdDQ3+4w0fFQpgjVz9zBEVAAAAAAEAu//rA0UDFQAcAAABIgYVERcvASYiBhQXARYyNwE2NCYiDwI3ETQmAgARFQOAYgsgFQwBHA0gDAEdDBUgC2KAAxUDFRUR/cxcjWAKFR8N/uMNDQEdDR8VCmCNXAI0ERUAAAABAHIAOwOOAsYAHAAAExQXARYyNjQvAhchMjY0JiMhBz8BNi4CBwEGcg0BHQ0fFQpgkWgCHhEVFRH94meQYAsBFR8O/uQNAYAQDf7kDBUgC2KDBhUiFQaDYgsgFQEO/uUNAAAAAQByADsDjgLGABwAAAE0JwEmDgEUHwInISIGFBYzITcPAQYUFjI3ATYDjg3+5A4fFQpgkWj94hEVFRECHmiRYAoVHw0BHQ0BgBANARsOARUgC2KDBhUiFQaDYgsgFQwBHA0AAAEBHgAHAtoC3wAGAAAlEyMRIxEjAfzekZuQBwEoAbD+UAAAAAQADf/3A/MDCQAZAC4ARQBbAAAFMjY1ETQmIyIGDwEGKwEmHQEUNzMyHwEeASUWNjc+ATQmJy4BDgEXHgEUBgcGFgUiLwEuASsBBj0BNDsBMjY/ATYyFREUNxY2Nz4BNCYnLgEHDgEXHgEUBgcGFgH2FhwcFw8aEckEB39bW38HBMkQGwGCDRsKKi8uKwobGQMJJCgoJAkD/oEDBL4IDgiPGRmPCA4IvgMK2gwaChocHRkKGgwOAwoTFRYSCgMJHBYCqxceDxCyBAFgq2ABBLQODlcIBg07l6aXPA0FERsPNIGQgjMOHAYEqwcFARq1GQQIrAMG/bAGcAgFDSJdZl0jDAUHCh0OGkdORxoOHAAABgA5/98D0gMiACQATABQAGIAZgByAAABNDEmLwEuAQchJgYPAgYVHgEXMzI2Nx4BNzY3HgEzMRY3PgEHBisBIiYvAQcGBwYHIiYvAQcOASsBLgE9ATQ/AjY3ITIWHwIWBgcmJwcXIwYHFSE1JicRFBYzITI2NRElJicHASEiJjQ2NyEeARQGA74BAkwLNCH95CAyC1MBCQFiSwcoRxozjjsMChpHKC4pOi+MFxkEGCoPODgGCB0mFyoPOTgQKhcGLDoFAlIFDgInBwwDTAIMHNoCAgPxAh8j/ZYnIh8XApAXH/3/AgEDAWv+PBAWFhABxBAWFgIUAQUEwR8kAQEiH8gFHB5NZwMiIDsMMAsMICEBFiF5WgwUE0RECAYYARQTREUSFQI9LgESEQXHDgEKB8MGKElrAQECCg8G4OEHEv70FxsbFwEKBwEBAgEAFR8UAQEUHxUAAAAFAED/4APAAyAACwAfADMASABdAAABISImNDYzITIWFAYDIyImNDY7ATI2PQE0NjIWHQEOAQUjLgEnNTQ2MhYdARQWOwEyFhQGAyImPQE+ATczMhYUBisBIgYdARQGISImPQE0JisBIiY0NjsBHgEXFRQGA6D8wA4SEg4DQA4SEm7ADhISDsAOEhIcEgE2/fegKTYBEhwSEg6gDhIS7g4SATYpoA4SEg6gDhISAvIOEhIOwA4SEg7AKTYBEgFgEhwSEhwS/oASHBISDqAOEhIOoCk2AQE2KaAOEhIOoA4SEhwSAiASDqApNgESHBISDqAOEhIOoA4SEhwSATYpoA4SAAAAAAEAMf/2A88DCQAgAAAFMjY9ATM2FhceATcyNjc2AicjNTQmIyIGBwEGFBcBHgEB1RYcDXunNwwbDhIZAQHV5g0cFw8aEf6uFxcBUhAbCRwWpAFQZxQNAR0c8AEQA6IWHg8Q/sQYLBj+wQ4OAAEAMf/2A88DCQAgAAAFMjY3ATY0JwEuASMiBh0BIwYCFx4BMxY2Nz4BFzMVFBYCKw8cEAFSFxf+rhIZDxccDebVAQEZEg4bCzinew0cCQ4OAT8YLBgBPBAPHheiAv7w8BwdAQ0TaFABpBYcAAQACP/nA/gDGQAbACcANQBEAAAXITY3ESYrASImLwEuASsBIgYPAQ4BKwEiBxEWJS4BJz4BNx4BFw4BEyImNTQ+ATIeARUOAQcBMj4CNC4CIw4BBx4BjwLihgEBhmQYGg0jDychqyAoDyMNGhhhhgEBAfdkgwMDg2RkgwMDg9AXHg4ZHBkOAR4W/swiPTAaGjA9IkhfAgJfGQGEAcGEDRAmEhMTEiYQDYT+P4SIA4RkZIQCAoRkZIQBUR4WDxgODhgPFh4B/u8ZMD5EPjAZAl9ISGAAAwAR/9sD7wMlACUALgA3AAATHgE7ARMeATMhMjY0JiMhLgEvASEyNj8BNjcuASMhJy4BKwEiBgEeATI2NCYiBgUUFjI2NCYiBhEBEg2RRQYyLwH0DRISDf4TEhYDBwIgLzIHIgEBARUR/UQIAxkglw0SATgBJzopKTonAZAoOygoOygDBQ0T/ikuNRIcEgEXFC01LuMKBhATNxgZE/0OHicoOignHh4nJzwnJwAAAAAEABL/2wPvAyUAJAArADQAPQAAJSEyNjQmIyEuAS8BITI2PwE2Ny4BIyEnLgErASIGFBY7ARMeAQEHDgEjIScTMjY0JiIGFBYhMjY0JiIGFBYBbgH0DRISDf4TEhYDBwIgLzIHIgEBARUR/UQIAxkglw0SEg2RRQYyAmcfAhYT/d4lexwpKTonJwGuHigoOygoqxIcEgEXFC01LuMKBhATNxgZExoT/ikuNQHRzRQX+P1fKDooJzwnJzwnJzwnAAADAEn/yQO3AzcAFAAgACwAAAEWFA8BBiIvASY0NjIfARYyPwE2MgM+ATcuAScOAQceARcuASc+ATceARcOAQLFCQn0ChkJaAkTGQo7CRkKyAoZvJvPBATPm5vPBATPm7r4BQX4urr4BQX4AhQKGQr0CQlnChkTCjoKCsgJ/fUEz5ubzwQEz5ubz00F+Lq6+AUF+Lq6+AAAAAEAS//LA7UDNQALAAAFPgE3LgEnDgEHHgECALj4BQX4uLn3BQX4NQX4uLj4BQX4uLj4AAAFAAAAFgQAAr4ACwAcAC0ANgA8AAAlFjI+AScBJg4CFwE+ATcmJCcGBxc2Mx4BFxQPATY3JwYjLgEnNDcnDgEHFgQBLgMjIgcXJx4BFzMnAyQJFxABCf2RCBgQAQkCklhgAQP+6+hfUmImKVl1AhG/aFZiKzFZdQIWgl1mAQQBFgFdAREiKxgHB4TtAkMzD4YfCREXCQJvCAEQGAj9+zt7I0bqDQEcYRECdForJO0BH2MWAnZXMyqDPH8lReoBNxgrIhEBgw8yQwGGAAAFAAAAGAQAArsACwAdAC8ANwA/AAAlFj4BNCcBJg4CFyUGBxc2Mx4BFxQGBxc+ATcmJAM2NycGBy4BJz4BNycOAQcWBCU2NS4BJwYHEzY3AQYVHgEDHwoWEQj9lgkXEAEIAUtgUDA9Q8D7BFlOLlhhAQP+7OlnVjBCS8D7BAFeUi5dZgEEARYBpBECdVgsJVExKv7rFQJ0IQkBEBcJAmoIARAXCRQBHTATDMktGWMxLjx8I0bq/ZQBHjEVAQvFMhZoMy48fyRF698mK1l0AgEQ/nMBFQEVKjJXdQAABAAAAC8EAAKxAAsAFwAjACwAACU2JDcmJCcGBAcWBDcuASc+ATceARcOASc+ATcuAScOAQceATcuATQ2MhYUBgIA5wEVBAT+6+fk/ugEBAEY5L78BAT8vr39BAT9vVp2AgJ2Wlp3AQJ2Wh4oKDwnJy8M7kdG7g0N7kZH7i8LyDMtzA0NzC0zyCkCeFhadQICdVpYeIoBJzsoKDsnAAAAAAEAg//bA30DJQAhAAAXPgE3NT4BNx4BFzI+AjcRLgEjDgEHLgEnIg4CBxEeAaQOEgEIOjBzuG0xNS0aAQEZEw9AN263dDE1LRoBARIlARIO7gQPAQVEBQsVJB0BtBETARABBUQFCxUkHf04DhIAAAACAFX/wwOrAzwAMgBAAAATDgEHFhcWBw4BFRQXFgcOARUUHgEOARUUFjsBHgEVDgEHFBYzMjY3PgE3PgE3NCYnIyIBLgEnIx4BBw4BBzM+AdwaKAEBCgQHFB0PBwsPEgkTFgsqIZkdIwRABCIaFh0MMXQzKikBrpk8VQKrAWhSTTo3AQMxHj9KYAMzBiIfGQ0JAwkkGh4TCgcIIhYPHRARHRIgLAEbGC+HPB8hHRlem0I2bkt6mwT+62OGAyt8SlF1IwKFAAAAAAMAPP+dA8QDYwAwAGkAdwAAASMiBgcOAQcWFw4BFBcOARUUFwYVHgEXMzYXDgEHHgEXMjY3PgE3Mz4BNy4BJyMuAQczHgEXFgYHDgEHDgEnIic+ATcuASsBLgE1JjY3NjQnLgE1NDc2NTQnLgE1Jjc2NTQnLgE1NDc+AQUeARcOAQcjPgE1NCYnAZ47KUIYLDMBAQQWGAoPEQ4TAT8xohEBBUAEATUsHy8UJmZMUVJrAgJ1WI0rZXY8iaICASUrMnYyDBIKIQEFQAQBMyeZFRsBCw0GBAsJHQ0DCQgBLgoCCAQvEzoBuDtSAQFJNCcYFionA2MFBgs5KBAPEC81Fw8qFyEZGygxQAIBDiWJRC04ASMoSpRbA5dwb5YDFxlBA4dvRGk5QJ5fGRABJTeGNSUpARsWDxcMBg0FDxcNHhYJCwUEERQLIhcFCgMFEhAHIQwFBTADcVRVcgItYTg8ZygAAAAAAgBV/8QDqwM9ADIAQAAABT4BNyYnJjc+ATc0JyY3PgE1NC4BPgE1NCYnIyImJz4BNzQmIyIGBw4BBw4BFRQWFzMyAR4BFzMuATc+ATcjDgEDIxsoAQEKBQgUHAEPCAwPEgoSFgsqIZkeIgEFQAQiGhYdDDF0Myoqrpk8VP1WAWhSTTo3AQMxHj9KYDMGIh8YDgkDCSQaHhMKBwgiFg8dEBEdEiArARwYL4c8HiIdGV6cQTZuTHmbBAEVY4YDK3tLUXUjAoUAAAABAFb/9wOqAwkAFwAABTI2NzYSNy4BJyIGBy4BIw4BBxYSFx4BAgAHEQe40QIDhmo9XR0dXjxqhgMC0LkHEQkHBHQBB4pzjQJANzdAAo1ziv73cgQHAAAAAwA8/50DxANjADEAagB4AAAFMzI2Nz4BNyYnPgE0Jz4BNTQmJzY1LgEnIyInPgE3LgEnIgYHDgEHIw4BBx4BFzMeATcnLgEnJjY3PgE3PgEXMhcOAQceATsBHgEVFgYHBhQXHgEVFAcGFQYXHgEVFgcGFRQXHgEVFAcOASUuASc+ATczDgEVFBYXAmI7KUIYLDMBAQQWGAoPEQcHEwE/MaIRAQVABAE1LB8vFCZmTFFSawICdViNK2V2PImiAgElKzJ2MgwSCiEBBUAEATMnmRUbAQsNBgQLCR0MAQMJCAEuCgIIBC8TOv5IO1IBAUk0JxgWKidjBQYLOSgQDxAvNhYPKhgPHgwbKDFAAg0liUQtOAEjKEqUWwOWcW+WAxcZQQECh29EaTlAnl8ZEAElNoc1JSkBGxYPFwwGDQUPFwwfFgkLBAURFAsiFwUKAwUSEAchDAUFMANxVFVyAi1hODxnKAACAFb/9wOqAwkAFgAwAAATFhIXHgEyNjc2EjcuASciBgcuASMOARc+ATceARceATI2Nz4BNx4BFw4BBwYiJy4BVgLStwcRDhEHt9ICA4ZqPV0dHV48aoZCAl9NO0oVCA4ODQkVSjtNXwIFzIsHBQaLzAIHiv76dQQHBwR1AQaKc40CMywsMwKNc1ZmAQE6IgwKCgwiOgEBZlZ67VwFBVztAAAAAAQAKP/DA9gDPQAYACEAKgAzAAAFMjY/ASEyNjURNCYnIQ4BFREUFjsBFRQWEw4BIiY0NjIWFw4BIiY0NjIWFw4BIiY0NjIWASYNFg+bASFgZGRg/dhgZGRgFBRZASU1JCM2JdcBJTQlJDUl1wEkNSQkNSQ9Dg6NZV8BSF9lAQFlX/64X2V8FRcB/xskJDUlJRobJCQ1JSUaGyQkNSUlAAACACb/mwPaA2UAIgAsAAAXAScmNwE+ATIWFwEWFA8BATY1ETYmJwEuASIGBwEOARcRFBchMjcBJiIHARYxARToDQ4BbhAYGRYRAW8HB+YBEgoBERf+pxgqLioX/qYXEQF6ArI/Gf6LGzQb/okVHwER4RALARwNDQ0N/uQFDwfh/vASLAGyIioTAQsTFhYT/vUTKiL+Ti1YFwFxGxv+jRUABQAm/5UD2gNrABMAIwApADAAOgAAFyEyNRE2JicBLgEiBgcBDgEXERQBLgEiBg8BLQE+ATIWFw0CETcXByYBERQHJzcWASIjATYyFwEiI60CpoYBGB7+tRcrLioX/rUeGQECMxYtLSwWHP7/AUIPFxsWDwFD/wD98gH28QYDMAXw9AH9EwQFASsbMhsBKgUFa4UBqi02FwEEExYWE/78FzYt/laFAZsWFBQWG/37DA0NDPz8+wG2DPTsDAHI/koRDe3xBP4BASYcHP7aAAAAAAIA1v/OAyoDMgAUABwAAAEiBgcVBhURFBYzITI2NRE0JzUuAQc+ATIWFxUhAgBdhQNFKyoBqiorRQOF/QJaiFoC/sADMoGDZwdS/rkuKysuAUhSB2aDgftaX19abwAAAAMAQf/UA74DLAAHABQAIAAABREnJicRFxYlMj8BEQYPAQYXERQWBTY/ATY1ESYnIg8BAnHUCw3YCv4MDxW0DAzLJwEaAlAGB+AlATAPFL4sAs2BBwP9KXkFCgthAtcFB3UVKv2uGBkMAQV/FSoCUzABC2kAAgFf/7oCoQNGABMAHAAABTI2NxE+ATcuAScOAQceARcRHgEDLgE0NjIWFAYCAA4ZATZCAQJaRUVaAgFDNQEZIBcfHy4fH0ZkYAGKD1U4RVsCAltFOVUO/nZgYwLgASAuHx8uIAAAAAADAGf/ywOZAzUAFQAeADgAACUyNjc1PgE3NC4CIw4BBx4BFxUeAQMiJjQ2MhYUBhM+ATcuAScVHgEXDgEHLgEnPgE3NQ4BBx4BAgAOGQE2QgEYLjsgRVoCAUM1ARkgFx8fLh8fF8TUAQjFYUSJBgKnlZWnAgaJRGHFCAHUamRfyg9VOSA7LhgCW0Q6VA/KX2QCISAuICAuIP1AA2xLV2EBQwE7MDRGAgFHNDA7AUMBYVdLbAACAEv/ywO1AzUACwAXAAAFPgE3LgEnDgEHHgETLgEnPgE3HgEXDgECALj4BQX4uLn3BQX4uDdMAQFMNzhLAQFLNQX4uLj4BQX4uLj4ASwBSzg3SwEBSzc4SwAAAAEAP/+/A8EDQQAfAAAFMjY3ATY0JiIHAQ4BFRQWHwEWNjcBNhYHAQ4BHwEeAQJXFyIMARkMGCse/R8cJCgf6BQbDgHWCQ4H/koMBAdDCRxBJR8C3R4rGAz+5QohFx0cCUYGAw0BtwcOCf4oDBwV4iEpAAAAAwAG//UD+gMLAAwAGAAsAAAXITI3ESYnISIHERYzEy4BJz4BNx4BFw4BAy4BPQE3NjMyHwE3NjIfARUUBgeNAuaGAQGG/RqGAQGGuy08AQE8LSw8AQE85h8jgx0eIB1SzSBJIMUjHwqEAgyEAYX99IUBgQE7LS08AQE8LS07/sMBIx8bchobSbYbHLZDHyIBAAAAAAQAAP/ABAADAgAOABoAIwA6AAATNDYzITU0IyEiFREUOwEXITI1ETQjISIXEQYBLgE0NjIWFAYDIiY9ATc+ATIWHwE3PgEyFh8BFQ4BI41VUwIJef21enoTrgJLenr9tXoBAQEPJzIyTDMzvhscQxokKCcaJX8fMDIxH2MBHBoB2VNUCnh4/mh3u3gBnHd3/mR4AUoBM0wzM0wz/vMdGiA+FxwcGCByHSMiHl5RGh0ABAAA/8AEAAMCABMAIQAzADwAADczFQYzITI1ETQrATU0IyEiFREUNyInETY3IRYXFSEiFxETNjMhMhcRJy4BDwEnJiMiDwE3PgE0JiIGFBZ6SAF6Akt6ekh5/bV6ezsCAjsCSTsB/jt6AT4BOwJJOwKOGkAbrkUZHBkZZdImNDRNMzN7Q3h4AZx3P3h4/mh3Pj0BkTwBATw8d/7lARc9Pf69hhcBGJo/FhZYqQE0TTQ0TTQAAgBL/8sDtQM1AAsAIAAABT4BNy4BJw4BBx4BNyImNDcTNjIXExYUBiIvASYiDwEGAgC4+AUF+Li59wUF+A4KDAOfCikJnwQMFQePBQoFjwc1Bfi4uPgFBfi4uPjVDBMIAZUZGf5rCBMMBo8GBo8GAAUAeP/EA4cDPAAIABQANQBBAEgAAAERLgEnDgEHFQEWMjY0JwEmIgYUFxMOARQWMyEyNjQmJyM1NjcnBgcuASc1NCYiBh0BHgEXFQE0JiIGBxUUBxc2NQcnFR4BFzICagFGOzlHAQHoChsTCv00ChsUCp4NExMNAZANExMNp1I7LjVLZnwCER0RAop3AUMRHBEBBTUP570BRjojAawBBj1MAQFKOgf9cgoUGgsCzAoUGwr9FwETGhMTGhMBXgcqLiMBAnxkZA8REQ9kd5oMXgHfDxERD2QZGDQvNnq9QzxNAQAAAAMAAP/4BAADBAAZAC4ARAAABTI2NRE0JiciBg8BBisBIgcVFhczMh8BHgElFjY3PgE0JicuAQ4BFx4BFAYHBhYnFjY3PgE0JicuAQcOARceARQGBwYWAdsXGxwWEBkRuwUGf1sBAVt/BgW7DxwBqwwbCiouLioKGxkDCiMnJyQJBJ4LGwoZHB0YChoMDgQLEhUVEwkDCBwWAqYWHQEPELEEYKVgAQSzDg1XCQYNO5allTwMBhEcDjOBjoEzDhxqBwUMIlxmXCINBQgKHA8ZRk1HGg4cAAAAAAIAhP+xA30DTgALAC4AAAE+ATIWFzU0JiIGFRcRBz8BPgEWFA8BBiIvASY0NjIfAScRIyYVERQ3ITInETYjAd4BExwTARQcFEUCG0EJHRILkA0aDZALEhwKXQPThoYB7IcBAYcCWA0TEw3VDhMTDrr+8T4dRQoBEhsKjAwMjAobEgtiPgEPAYX+R4UBhAG5hAAAAAADAJIApQNuAlsAAAAMAB4AABMzITIVERQjISI1ETQFNz4BHgEVERQOASYvASY9ATSSSQFuSUn+kkkCM20JFBQLCxQUCW0OAltJ/txJSQEkSYdYBgMJEgv+4gsSCQIHWAsRcBEAAgCE/5wDfQNkABoALgAAAT4BPQEnFxYyNjQvASYiDwEOARYyPwEHFRQWAyE2JxE2KwERDgEiJjURIyIVERQCAA4UAl0KGxIKkQwaDJEJAREcCl4DFOgB7IcBAYfKARkkGcuGAh4BEg6HP2MKERoJjAwMjAkaEQpkQIcOEv19AYQBmoX+zRIYGBIBM4X+ZoUAAwBF/+QDuwMcABwAKgA4AAA3MzI9AT4BNx4BFxUUOwEyNj0BLgEnIw4BBxUUFhczMjY9ATQmJyMGBxUWITMyPQE0JyMOAR0BFBZlFwsCzaqrzAILFw4SA+u+Hr7rAxKJKScrKycpKwEBAkMoLS0oKCoqjgvxm7MBAbOb8QsQDe+u0QMD0a7vDRCqKCW5JicBASv8LCz8KwEBJya5JSgAAAUAhP+vA3wDUQAfACkANgBDAE8AABsBHgEzITI2NxMzMjY0JicjNS4BKwEiBgcVIw4BFBYzNzQ2OwEyFh0BIxMuATUTPgEyFhUDDgEFLgE1AzQ2MhYVExQGNw4BIiYnET4BMhYV1RsCLSgBcigtAhwxDRISDbABMyudKjMBrw4SEg7pGBSOFBjm/gwQEgEQGBATAQ/+3gwPFBEYEBIPnAERGBABARAYEgJ0/Y4pKiopAnISHBIBPSwzMyw9ARIcEn4SFxcSPf1RARIOAfENEhIN/g4OEQEBEQ4B8g0SEg3+Dw4SIA4SEg4B8Q0SEg0AAAIAHP+5BAkDOwBBAFwAACU1MzI+AjcuAS8BNzY1LgEnIgYPAScmDgIfAQcOAQcUHgI7ARUjLgEnPgE3Jj4CFz4BNx4BFxQHHgEXDgEHBRY/ATY0JiIPATcRNCYiBhURFycmDgEUHwEWAonOFigfEAEBLiY6BgEDeFs2Xh4cMRYqIxMBATcpMwESIysZwsJRagIBUUABID5LJSd6S3ifAwE8TAECZEz+ug0MkQoSGwpdAhQcFANeChwQCpANp0UQICgWJzoJDjsKClt4AjMuKg4GCBsnFjkMC0ArGCwiEkUCa1BFZBAnRDAOCzpFAQOfeA0ND15ATGUC7QEMjAkbEApjQAFKDhMTDv62QGMLARAbCYwMAAIAHAADBAoDOwAgADwAACUVDgEmJzUhLgEnPgE3Jj4CFz4BNx4BFxQHHgEXDgEHASYPAQYUFjI/AgcVHgEyNjc1JxcWMjY0LwEmAjYBJCMB/uxQawIBUUABIT5KJiZ7S3ifAwI9SwICZUz+ugwNkAoRHAlBHQMBEx0TAQNeChsRCpEMp3sUFRUUewJrUEVkECdEMA4LOkUBA594DQ0PXkBMZQIBagEMjAkbEQtEH0C2DhMTDrZAYwsRGwmMDAAAAAIAHAAIBAoDOwA/AFoAACU1Mz4BNy4BLwE3NjUuASciBg8BJyYOAh8BBw4BFRQeAjsBFSMuASc+ATcmPgIXPgE3HgEXFAceARcOAQcBNh8BFhQGIi8BFxEOASImJxE3BwYiJjQ/ATYCis0vPgEBLiU6BQECeVo3Xh4cMRUrIxMBAjcqMxIiLBjDw1BrAgFRQAEhPkomJntLeJ8DAj1LAgJlTP66DQyRChEbCl4DARMdEwEDXgkcEQqQDadFAT4vJzoJDjsKClt4AjMuKg4GCBsnFjkMC0ArGCwiEkUCa1BFZBAnRDAOCzpFAQOfeA0ND15ATGUCAWoBDIwJGxELY0D+tg4TEw4BSkBjCxEbCYwMAAAAAAIAHP+5BAkDOwAjAD4AACU1NC4BIg4BHQEhLgEnPgE3Jj4CFz4BNx4BFxQHHgEXDgEHBRY/ATY0JiIPATc1NCYiBh0BFycmDgEUHwEWAj0MFBgUDP70UWoCAVFAASA+SyUnekt4nwMBPEwBAmRM/roNDJEKEhsKXQIUHBQDXgocEAqQDafxDBQMDBQM8QJrUEVkECdEMA4LOkUBA594DQ0PXkBMZQLtAQyMCRsQCmNATw4TEw5PQGMLARAbCYwMAAAAEgDeAAEAAAAAAAAAEwAoAAEAAAAAAAEACABOAAEAAAAAAAIABwBnAAEAAAAAAAMAFQCbAAEAAAAAAAQACADDAAEAAAAAAAUAOwFEAAEAAAAAAAYACAGSAAEAAAAAAAoAKwHzAAEAAAAAAAsAEwJHAAMAAQQJAAAAJgAAAAMAAQQJAAEAEAA8AAMAAQQJAAIADgBXAAMAAQQJAAMAKgBvAAMAAQQJAAQAEACxAAMAAQQJAAUAdgDMAAMAAQQJAAYAEAGAAAMAAQQJAAoAVgGbAAMAAQQJAAsAJgIfAEMAcgBlAGEAdABlAGQAIABiAHkAIABpAGMAbwBuAGYAbwBuAHQAAENyZWF0ZWQgYnkgaWNvbmZvbnQAAHUAbgBpAGkAYwBvAG4AcwAAdW5paWNvbnMAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAHUAbgBpAGkAYwBvAG4AcwA6AFYAZQByAHMAaQBvAG4AIAAxAC4AMAAwAAB1bmlpY29uczpWZXJzaW9uIDEuMDAAAHUAbgBpAGkAYwBvAG4AcwAAdW5paWNvbnMAAFYAZQByAHMAaQBvAG4AIAAxAC4AMAAwADsASgBhAG4AdQBhAHIAeQAgADMALAAgADIAMAAyADAAOwBGAG8AbgB0AEMAcgBlAGEAdABvAHIAIAAxADIALgAwAC4AMAAuADIANQAzADUAIAA2ADQALQBiAGkAdAAAVmVyc2lvbiAxLjAwO0phbnVhcnkgMywgMjAyMDtGb250Q3JlYXRvciAxMi4wLjAuMjUzNSA2NC1iaXQAAHUAbgBpAGkAYwBvAG4AcwAAdW5paWNvbnMAAEcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAAcwB2AGcAMgB0AHQAZgAgAGYAcgBvAG0AIABGAG8AbgB0AGUAbABsAG8AIABwAHIAbwBqAGUAYwB0AC4AAEdlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC4AAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAGh0dHA6Ly9mb250ZWxsby5jb20AAAAAAAIAAAAAAAAACQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAhgAAAQIAAgEDAQQBBQEGAQcBCAEJAQoBCwEMAQ0BDgEPARABEQESARMBFAEVARYBFwEYARkBGgEbARwBHQEeAR8BIAEhASIBIwEkASUBJgEnAA4A7wEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4BbwFwAXEBcgFzAXQBdQF2AXcBeAF5AXoBewF8AX0BfgF/AYABgQGCAYMHdW5pMDAwMAdjb250YWN0BnBlcnNvbglwZXJzb25hZGQNY29udGFjdGZpbGxlZAxwZXJzb25maWxsZWQPcGVyc29uYWRkZmlsbGVkBXBob25lBWVtYWlsCmNoYXRidWJibGUJY2hhdGJveGVzC3Bob25lZmlsbGVkC2VtYWlsZmlsbGVkEGNoYXRidWJibGVmaWxsZWQPY2hhdGJveGVzZmlsbGVkBXdlaWJvBndlaXhpbgtwZW5neW91cXVhbgRjaGF0AnFxCHZpZGVvY2FtBmNhbWVyYQNtaWMIbG9jYXRpb24JbWljZmlsbGVkDmxvY2F0aW9uZmlsbGVkBm1pY29mZgVpbWFnZQNtYXAHY29tcG9zZQV0cmFzaAZ1cGxvYWQIZG93bmxvYWQFY2xvc2UEcmVkbwR1bmRvB3JlZnJlc2gEc3Rhcgt3aGl0ZWNpcmNsZQVjbGVhcg1yZWZyZXNoZmlsbGVkCnN0YXJmaWxsZWQKcGx1c2ZpbGxlZAttaW51c2ZpbGxlZAxjaXJjbGVmaWxsZWQOY2hlY2tib3hmaWxsZWQKY2xvc2VlbXB0eQxyZWZyZXNoZW1wdHkGcmVsb2FkCHN0YXJoYWxmDHNwaW5uZXJjeWNsZQZzZWFyY2gJcGx1c2VtcHR5B2ZvcndhcmQEYmFjaw5jaGVja21hcmtlbXB0eQRob21lCG5hdmlnYXRlBGdlYXIKcGFwZXJwbGFuZQRpbmZvBGhlbHAGbG9ja2VkBG1vcmUEZmxhZwpob21lZmlsbGVkCmdlYXJmaWxsZWQKaW5mb2ZpbGxlZApoZWxwZmlsbGVkCm1vcmVmaWxsZWQIc2V0dGluZ3MEbGlzdARiYXJzBGxvb3AJcGFwZXJjbGlwCWV5ZWZpbGxlZAx1cHdhcmRzYXJyb3cOZG93bndhcmRzYXJyb3cObGVmdHdhcmRzYXJyb3cPcmlnaHR3YXJkc2Fycm93C2Fycm93dGhpbnVwDWFycm93dGhpbmRvd24NYXJyb3d0aGlubGVmdA5hcnJvd3RoaW5yaWdodAhwdWxsZG93bgVzb3VuZARzaG9wBHNjYW4KdW5kb2ZpbGxlZApyZWRvZmlsbGVkDGNhbWVyYWZpbGxlZApjYXJ0ZmlsbGVkBGNhcnQIY2hlY2tib3gRc21hbGxjaXJjbGVmaWxsZWQOZXllc2xhc2hmaWxsZWQIZXllc2xhc2gDZXllCmZsYWdmaWxsZWQVaGFuZHRodW1ic2Rvd25fZmlsbGVkDmhhbmR0aHVtYnNkb3duEmhhbmR0aHVtYnN1cGZpbGxlZAtoZWFydGZpbGxlZAxoYW5kdGh1bWJzdXAOYmxhY2toZWFydHN1aXQKY2hhdGZpbGxlZA5tYWlsb3BlbmZpbGxlZAhtYWlsb3Blbgxsb2NrZWRmaWxsZWQJbWFwZmlsbGVkBm1hcHBpbg1tYXBwaW5lbGxpcHNlC3NtYWxsY2lyY2xlEHBhcGVycGxhbmVmaWxsZWQLaW1hZ2VmaWxsZWQMaW1hZ2VzZmlsbGVkBmltYWdlcw5uYXZpZ2F0ZWZpbGxlZA5taWNzbGFzaGZpbGxlZAtzb3VuZGZpbGxlZA5kb3dubG9hZGZpbGxlZA52aWRlb2NhbWZpbGxlZAx1cGxvYWRmaWxsZWQKaGVhZHBob25lcwt0cmFzaGZpbGxlZA1jbG91ZGRvd25sb2FkEWNsb3VkdXBsb2FkZmlsbGVkC2Nsb3VkdXBsb2FkE2Nsb3VkZG93bmxvYWRmaWxsZWQHdW5pMDAwOQAAAAAB//8AAgABAAAADAAAABYAAAACAAEAAQCFAAEABAAAAAIAAAAAAAAAAQAAAADVpCcIAAAAANoxE3MAAAAA2jSpUA==' 137 | } 138 | 139 | export default iconData; -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测类型 3 | * @param target 检测的目标 4 | */ 5 | export function checkType(target: any) { 6 | const value: string = Object.prototype.toString.call(target); 7 | const result = (value.match(/\[object (\S*)\]/) as RegExpMatchArray)[1]; 8 | return result.toLocaleLowerCase() as JavaScriptTypes; 9 | } 10 | 11 | /** 12 | * 修改属性值-只修改之前存在的值 13 | * @param target 修改的目标 14 | * @param value 修改的内容 15 | */ 16 | export function modifyData(target: T, value: T) { 17 | for (const key in value) { 18 | if (Object.prototype.hasOwnProperty.call(target, key)) { 19 | // target[key] = value[key]; 20 | // 需要的话,深层逐个赋值 21 | if (checkType(target[key]) === "object") { 22 | modifyData(target[key], value[key]); 23 | } else { 24 | target[key] = value[key]; 25 | } 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * 设置属性值-之前不存在的值也根据传入的`value`值去设置 32 | * @param target 设置的目标 33 | * @param value 设置的内容 34 | */ 35 | export function setData(target: T, value: T) { 36 | for (const key in value) { 37 | target[key] = value[key]; 38 | } 39 | } 40 | 41 | /** 42 | * 格式化日期 43 | * @param value 指定日期 44 | * @param format 格式化的规则 45 | * @example 46 | * ```js 47 | * formatDate(); 48 | * formatDate(1603264465956); 49 | * formatDate(1603264465956, "h:m:s"); 50 | * formatDate(1603264465956, "Y年M月D日"); 51 | * ``` 52 | */ 53 | export function formatDate(value: string | number | Date = Date.now(), format = "Y-M-D h:m:s") { 54 | if (["null", null, "undefined", undefined, ""].includes(value as any)) return ""; 55 | // ios 和 mac 系统中,带横杆的字符串日期是格式不了的,这里做一下判断处理 56 | if (typeof value === "string" && new Date(value).toString() === "Invalid Date") { 57 | value = value.replace(/-/g, "/"); 58 | } 59 | const formatNumber = (n: number) => `0${n}`.slice(-2); 60 | const date = new Date(value); 61 | const formatList = ["Y", "M", "D", "h", "m", "s"]; 62 | const resultList = []; 63 | resultList.push(date.getFullYear().toString()); 64 | resultList.push(formatNumber(date.getMonth() + 1)); 65 | resultList.push(formatNumber(date.getDate())); 66 | resultList.push(formatNumber(date.getHours())); 67 | resultList.push(formatNumber(date.getMinutes())); 68 | resultList.push(formatNumber(date.getSeconds())); 69 | for (let i = 0; i < resultList.length; i++) { 70 | format = format.replace(formatList[i], resultList[i]); 71 | } 72 | return format; 73 | } 74 | 75 | /** 76 | * 数字运算(主要用于小数点精度问题) 77 | * [see](https://juejin.im/post/6844904066418491406#heading-12) 78 | * @param a 前面的值 79 | * @param type 计算方式 80 | * @param b 后面的值 81 | * @example 82 | * ```js 83 | * // 可链式调用 84 | * const res = computeNumber(1.3, "-", 1.2).next("+", 1.5).next("*", 2.3).next("/", 0.2).result; 85 | * console.log(res); 86 | * ``` 87 | */ 88 | export function computeNumber(a: number, type: NumberSymbols, b: number) { 89 | /** 90 | * 获取数字小数点的长度 91 | * @param n 数字 92 | */ 93 | function getDecimalLength(n: number) { 94 | const decimal = n.toString().split(".")[1]; 95 | return decimal ? decimal.length : 0; 96 | } 97 | /** 98 | * 修正小数点 99 | * @description 防止出现 `33.33333*100000 = 3333332.9999999995` && `33.33*10 = 333.29999999999995` 这类情况做的处理 100 | * @param n 数字 101 | */ 102 | const amend = (n: number, precision = 15) => parseFloat(Number(n).toPrecision(precision)); 103 | const power = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b))); 104 | let result = 0; 105 | 106 | a = amend(a * power); 107 | b = amend(b * power); 108 | 109 | switch (type) { 110 | case "+": 111 | result = (a + b) / power; 112 | break; 113 | case "-": 114 | result = (a - b) / power; 115 | break; 116 | case "*": 117 | result = (a * b) / (power * power); 118 | break; 119 | case "/": 120 | result = a / b; 121 | break; 122 | } 123 | 124 | result = amend(result); 125 | 126 | return { 127 | /** 计算结果 */ 128 | result, 129 | /** 130 | * 继续计算 131 | * @param nextType 继续计算方式 132 | * @param nextValue 继续计算的值 133 | */ 134 | next(nextType: NumberSymbols, nextValue: number) { 135 | return computeNumber(result, nextType, nextValue); 136 | }, 137 | /** 138 | * 小数点进位 139 | * @param n 小数点后的位数 140 | */ 141 | toHex(n: number) { 142 | const strings = result.toString().split("."); 143 | if (n > 0 && strings[1] && strings[1].length > n) { 144 | const decimal = strings[1].slice(0, n); 145 | const value = Number(`${strings[0]}.${decimal}`); 146 | const difference = 1 / Math.pow(10, decimal.length); 147 | result = computeNumber(value, "+", difference).result; 148 | } 149 | return result; 150 | } 151 | }; 152 | } 153 | 154 | /** 155 | * 获取`url?`后面参数(JSON对象) 156 | * @param name 获取指定参数名 157 | * @param target 目标字段,默认`location.search` 158 | * @example 159 | * ```js 160 | * // 当前网址为 www.https://hjs.com?id=99&age=123&key=sdasfdfr 161 | * const targetAge = getQueryParam("age", "id=12&age=14&name=hjs"); 162 | * const params = getQueryParam(); 163 | * const age = getQueryParam("age"); 164 | * // 非IE浏览器下简便方法 165 | * new URLSearchParams(location.search).get("age"); 166 | * ``` 167 | */ 168 | export function getQueryParam(name?: string, target?: string) { 169 | const code = target || location.href.split("?")[1] || ""; 170 | const list = code.split("&"); 171 | const params: any = {}; 172 | for (let i = 0; i < list.length; i++) { 173 | const item = list[i]; 174 | const items = item.split("="); 175 | if (items.length > 1) { 176 | params[items[0]] = item.replace(`${items[0]}=`, ""); 177 | } 178 | } 179 | if (name) { 180 | return params[name] || ""; 181 | } else { 182 | return params; 183 | } 184 | } 185 | 186 | /** 187 | * 获取一些深层`key`的对象值 188 | * @param target 目标对象 189 | * @param key `key`字段 190 | * @example 191 | * ```js 192 | * const info = { 193 | * list: [ 194 | * { value: "hjs" } 195 | * ] 196 | * } 197 | * getDeepLevelValue(info, "list.0.value"); // => "hjs" 198 | * ``` 199 | */ 200 | export function getDeepLevelValue(target: any, key: string) { 201 | const keys = key.split("."); 202 | let result; 203 | for (let i = 0; i < keys.length; i++) { 204 | const prop = keys[i]; 205 | result = target[prop]; 206 | const type = checkType(result); 207 | if (type !== "object" && type !== "array") { 208 | break; 209 | } else { 210 | target = target[prop]; 211 | } 212 | } 213 | return result; 214 | } 215 | 216 | /** 217 | * ES5 兼容 ES6 `Array.findIndex` 218 | * @param array 219 | * @param compare 对比函数 220 | */ 221 | export function findIndex(array: Array, compare: (value: T, index: number) => boolean) { 222 | var result = -1; 223 | for (var i = 0; i < array.length; i++) { 224 | if (compare(array[i], i)) { 225 | result = i; 226 | break; 227 | } 228 | } 229 | return result; 230 | } 231 | 232 | /** 233 | * 范围随机整数 234 | * @param min 最小数 235 | * @param max 最大数 236 | */ 237 | export function ranInt(min: number, max: number) { 238 | return Math.round(Math.random() * (max - min) + min); 239 | } 240 | 241 | /** 242 | * 随机生成中文 243 | * @param min 最小数 244 | * @param max 最大数 245 | */ 246 | export function randomText(min: number, max: number) { 247 | const len = Math.floor(Math.random() * max) + min; 248 | const base = 20000; 249 | const range = 1000; 250 | let str = ""; 251 | let i = 0; 252 | while (i < len) { 253 | i++; 254 | const lower = Math.floor(Math.random() * range); 255 | str += String.fromCharCode(base + lower); 256 | } 257 | return str; 258 | } 259 | 260 | // /** 261 | // * 获取`/static/`目录下的图片路径 262 | // * @param name 图片或文件路径名,需要带后缀 263 | // * @returns 264 | // */ 265 | // export function getIamgeByName(name: string) { 266 | // console.log(name, import.meta.url); 267 | // return new URL(`../static/${name}`, import.meta.url).href; 268 | // } 269 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import config from "./config"; 2 | import store from "@/store"; 3 | 4 | function getResultInfo(result: { statusCode: number, data: any }) { 5 | const info: ApiResult = { code: -1, msg: "", data: null } 6 | switch (result.statusCode) { 7 | case 999: 8 | info.msg = "网络出错了"; 9 | break; 10 | case 200: 11 | info.code = 1; 12 | info.msg = "ok"; 13 | info.data = result.data; 14 | break; 15 | case 400: 16 | info.msg = "接口传参不正确"; 17 | break; 18 | case 403: 19 | info.msg = "登录已过期"; 20 | store.user.reset(); 21 | break; 22 | case 404: 23 | info.msg = "接口不存在"; 24 | break; 25 | default: 26 | break; 27 | } 28 | if (result.statusCode >= 500) { 29 | info.msg = "服务器闹脾气了"; 30 | } 31 | return info; 32 | } 33 | 34 | /** 35 | * 基础请求 36 | * @param method 请求方法 37 | * @param path 请求路径 38 | * @param data 请求参数 39 | * @param options 其他配置参数 40 | */ 41 | export default function request(method: "GET" | "POST" | "DELETE" | "PUT", path: string, data?: any, options: Partial = {}) { 42 | const headers = options.headers || {}; 43 | return new Promise>(function (resolve, reject) { 44 | uni.request({ 45 | method: method, 46 | header: { 47 | "Authorization": store.user.info.token, 48 | ...headers 49 | }, 50 | url: config.apiUrl + path, 51 | data: data, 52 | timeout: config.requestOvertime, 53 | success(res) { 54 | // console.log("request.success", res); 55 | const info = getResultInfo(res); 56 | if (info.code !== 1 && options.showTip) { 57 | uni.showToast({ 58 | title: typeof options.showTip === 'boolean' ? (info.msg || "操作失败") : options.showTip as string, 59 | position: "bottom", 60 | icon: "none", 61 | duration: 2400 62 | }); 63 | } 64 | resolve(info); 65 | }, 66 | fail(err) { 67 | const info = getResultInfo({ statusCode: 999, data: null }); 68 | info.msg = err.errMsg; 69 | resolve(info); 70 | } 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "baseUrl": "./", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "types": [ 18 | "@dcloudio/types", 19 | "@types/node" 20 | ] 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.d.ts", 25 | "src/**/*.tsx", 26 | "src/**/*.vue" 27 | ] 28 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vite"; 3 | import uni from "@dcloudio/vite-plugin-uni"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [uni()], 8 | base: "./", 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "src") 12 | } 13 | }, 14 | server: { 15 | port: 2023, 16 | host: "0.0.0.0" 17 | } 18 | }); 19 | --------------------------------------------------------------------------------