├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── index.html ├── src ├── App.vue ├── cloudfunctions │ ├── callback │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── echo │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── login │ │ ├── config.json │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── openapi │ │ ├── config.json │ │ ├── index.js │ │ └── package.json ├── common │ ├── menu.js │ ├── mixins │ │ └── openWeb.js │ ├── palette │ │ └── share.js │ ├── uni.css │ ├── util.js │ └── vmeitime-http │ │ ├── article.js │ │ ├── auth.js │ │ ├── guide.js │ │ ├── home.js │ │ ├── hospital.js │ │ ├── index.js │ │ ├── interface.js │ │ ├── me.js │ │ └── shop.js ├── components │ ├── HM-filterDropdown │ │ └── HM-filterDropdown.vue │ ├── index │ │ └── list.vue │ ├── jyf-parser │ │ ├── jyf-parser.vue │ │ └── libs │ │ │ ├── CssHandler.js │ │ │ ├── MpHtmlParser.js │ │ │ ├── config.js │ │ │ ├── handler.sjs │ │ │ ├── handler.wxs │ │ │ └── trees.vue │ ├── me │ │ └── pd-list2.vue │ ├── mescroll-uni │ │ ├── components │ │ │ ├── mescroll-down.css │ │ │ ├── mescroll-down.vue │ │ │ ├── mescroll-empty.vue │ │ │ ├── mescroll-top.vue │ │ │ ├── mescroll-up.css │ │ │ └── mescroll-up.vue │ │ ├── mescroll-body.css │ │ ├── mescroll-body.vue │ │ ├── mescroll-mixins.js │ │ ├── mescroll-uni-option.js │ │ ├── mescroll-uni.css │ │ ├── mescroll-uni.js │ │ └── mescroll-uni.vue │ ├── painter │ │ ├── index.vue │ │ ├── lib │ │ │ ├── downloader.js │ │ │ ├── gradient.js │ │ │ ├── pen.js │ │ │ ├── qrcode.js │ │ │ └── util.js │ │ ├── painter.js │ │ ├── painter.json │ │ └── painter.wxml │ ├── popup │ │ └── marker.vue │ ├── uni-popup │ │ └── uni-popup.vue │ ├── uni-transition │ │ └── uni-transition.vue │ ├── wuc-tab │ │ └── wuc-tab.vue │ └── xfl-select │ │ └── xfl-select.vue ├── main.js ├── manifest.json ├── pages.json ├── pages │ ├── article │ │ ├── detail.vue │ │ ├── html.js │ │ └── index.vue │ ├── guide │ │ └── index.vue │ ├── index │ │ ├── index.vue │ │ └── map.vue │ ├── me │ │ ├── about.vue │ │ ├── feedSuccess.vue │ │ ├── feedback.vue │ │ ├── me.vue │ │ └── support.vue │ ├── search │ │ ├── html.js │ │ ├── search.vue │ │ └── test.vue │ ├── trends │ │ └── index.vue │ └── webview.vue ├── static │ ├── about.png │ ├── about2.png │ ├── article.png │ ├── articleActive.png │ ├── btn_hospital.png │ ├── btn_hospital_nor.png │ ├── btn_quezhen.png │ ├── btn_quezhen_nor.png │ ├── btn_share.png │ ├── buy.png │ ├── code.jpg │ ├── dangerInfo.png │ ├── defaultAuthor.png │ ├── del.png │ ├── evaluateSuccess.png │ ├── follow.png │ ├── green_mark.png │ ├── hospital_icon.png │ ├── hospital_type1.png │ ├── hospital_type2.png │ ├── icon_close_black.png │ ├── icon_close_white.png │ ├── icon_hospital.png │ ├── icon_lessen.png │ ├── icon_location.png │ ├── icon_location2.png │ ├── icon_search.png │ ├── icon_search2.png │ ├── icon_zoom.png │ ├── icon_zoom2.png │ ├── idea.png │ ├── index.png │ ├── indexActive.png │ ├── loca.png │ ├── location_me_small.png │ ├── logout.png │ ├── my.png │ ├── myActive.png │ ├── myBg.jpg │ ├── nahan.png │ ├── near.png │ ├── nearActive.png │ ├── range.png │ ├── red_mark.png │ ├── refresh_active.png │ ├── share.png │ ├── sub.png │ ├── support.png │ ├── trends.png │ ├── trendsActive.png │ ├── yellow_mark.png │ └── zm.png ├── store │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── index.js │ │ ├── shop.js │ │ └── user.js └── uni.scss ├── tsconfig.json └── vue.config.js /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 【疫况】小程序 2 | 3 | 《疫况》,超3.6千万用户。疫情期间起到了全国防范作用,现将此开源,以供大家公益学习参考。欢迎star支持 4 | 5 | 6 | ### 作者 7 | [@俊瑶先森](https://weibo.com/232246784) 8 | ### 技术栈 9 | 10 | > 框架:uniapp 11 | 12 | ### 线上预览(微信、支付宝、QQ) 13 |
14 | 15 | 16 | 17 |
18 | 19 | ### 截图预览 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | ### 媒体链接: 29 | 30 | 少数派:[我做了一个「疫情地图」小程序,但我希望大家「明天」就不需要它](https://mp.weixin.qq.com/s/FpTfD1uHYcaKGuggUGP0aA) 31 | 微信派:[月子中心里诞生出的疫情小程序,7天从0到1000万](https://mp.weixin.qq.com/s/t08bCstJzMOVgy2Gse3P6w) 32 | 路透社:[Chinese citizens turn to virus tracker apps to avoid infected neighborhoods](https://uk.reuters.com/article/us-china-health-apps/chinese-citizens-turn-to-virus-tracker-apps-to-avoid-infected-neighborhoods-idUKKBN1ZX2IH) 33 | 医况:[「疫况助出行」日本导演拍南京抗疫纪录片,登上雅虎首页](https://mp.weixin.qq.com/s/Eqj2xaZOvGnpTCwA5eZlqw) 34 | 阿拉丁指数:[2月疫情政务、教育、品牌、小游戏类小程序TOP榜单](https://mp.weixin.qq.com/s/P_gjavKveVp8j0IZ4NAcXw) 35 | 雷锋网:[这个小程序可以查你家周围的疫情爆发点](https://mp.weixin.qq.com/s/nCP8VcsTToN7Dn-Mms8ETw) 36 | 南方都市报:[它告诉你疫情有多远!这个4小时写出来的小程序为三千多万人预警](https://m.mp.oeeee.com/a/BAAFRD000020200225270122.html?layer=4&share=chat&isndappinstalled=0) 37 | 38 | ### 志愿者 (如有遗漏,告知我更新) 39 | #### 重点技术支持: 40 | [@少数派](https://sspai.com/) 41 | #### 地区志愿者(不分先后): 42 | 倾尽时光初遇见_香港、阿Ki、我不是糨糊🇨🇳——重庆万州.成都、纪步靑、钟桦服、阿黎、🚀-佛山、Ben、靖思、张韦楠-济南泰安、Mr.球、酒姑苏-淮安、索拉否、se7encifer、山吅厂敢、彭宇轩-湖北黄石、黎闹闹、讲者-韶关、阿振、jijiali、思考问题的熊、淘子-重庆、CC-济宁、Ivan、yztdgs、t_VIPer、kylinzi、大力他爹、Pray、南星、陈逸添、linxinqi_、硅基生命体、wu987054063、余航、风行一刻、Max吳、四儿、九宫格、neesar、cd028dm、九分、csdelphi、Chisakaow、Neesar、彭宇轩-湖北黄石、lzh728510、Ne_YO_cat、bestbacky、LQQ1881366313382、余航、Lukejone、18617356501、xuwenlin、lukejone、兔九(CX1625544326)、hugh-广东惠州、摄影师灬君狄、gonganchenyan、豆浆小姐、Xsu、yoyo3303、787089788(李十九)、fakegeek、VizardMask、xukai1539、fanao2013、孙其昌 Henry、lairenlaiwang 等等 43 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const plugins = [] 2 | 3 | if (process.env.UNI_OPT_TREESHAKINGNG) { 4 | plugins.push(require('@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js')) 5 | } 6 | 7 | if (process.env.UNI_PLATFORM === 'app-plus' && process.env.UNI_USING_V8) { 8 | const path = require('path') 9 | 10 | const isWin = /^win/.test(process.platform) 11 | 12 | const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path) 13 | 14 | const input = normalizePath(process.env.UNI_INPUT_DIR) 15 | try { 16 | plugins.push([ 17 | require('@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console'), 18 | { 19 | file (file) { 20 | file = normalizePath(file) 21 | if (file.indexOf(input) === 0) { 22 | return path.relative(input, file) 23 | } 24 | return false 25 | } 26 | } 27 | ]) 28 | } catch (e) {} 29 | } 30 | 31 | process.UNI_LIBRARIES = process.UNI_LIBRARIES || ['@dcloudio/uni-ui'] 32 | process.UNI_LIBRARIES.forEach(libraryName => { 33 | plugins.push([ 34 | 'import', 35 | { 36 | 'libraryName': libraryName, 37 | 'customName': (name) => { 38 | return `${libraryName}/lib/${name}/${name}` 39 | } 40 | } 41 | ]) 42 | }) 43 | module.exports = { 44 | presets: [ 45 | [ 46 | '@vue/app', 47 | { 48 | modules: 'commonjs', 49 | useBuiltIns: process.env.UNI_PLATFORM === 'h5' ? 'usage' : 'entry' 50 | } 51 | ] 52 | ], 53 | plugins 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uni-assistant", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "npm run dev:h5", 7 | "build": "npm run build:h5", 8 | "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build", 9 | "build:custom": "cross-env NODE_ENV=production uniapp-cli custom", 10 | "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build", 11 | "build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build", 12 | "build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build", 13 | "build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build", 14 | "build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build", 15 | "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build", 16 | "dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch", 17 | "dev:custom": "cross-env NODE_ENV=development uniapp-cli custom", 18 | "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve", 19 | "dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch", 20 | "dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch", 21 | "dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch", 22 | "dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch", 23 | "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch", 24 | "info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js" 25 | }, 26 | "dependencies": { 27 | "@dcloudio/uni-app-plus": "^2.0.0-28820200820001", 28 | "@dcloudio/uni-h5": "^2.0.0-28820200820001", 29 | "@dcloudio/uni-helper-json": "^1.0.5", 30 | "@dcloudio/uni-mp-alipay": "^2.0.0-28820200820001", 31 | "@dcloudio/uni-mp-baidu": "^2.0.0-28820200820001", 32 | "@dcloudio/uni-mp-qq": "^2.0.0-28820200820001", 33 | "@dcloudio/uni-mp-toutiao": "^2.0.0-28820200820001", 34 | "@dcloudio/uni-mp-weixin": "^2.0.0-28820200820001", 35 | "@dcloudio/uni-stat": "^2.0.0-28820200820001", 36 | "copy-webpack-plugin": "^5.1.2", 37 | "dayjs": "^1.8.34", 38 | "flyio": "^0.6.2", 39 | "regenerator-runtime": "^0.12.1", 40 | "vue": "2.6.10", 41 | "vuex": "^3.5.1" 42 | }, 43 | "devDependencies": { 44 | "@dcloudio/uni-cli-shared": "^2.0.0-28820200820001", 45 | "@dcloudio/uni-template-compiler": "^2.0.0-28820200820001", 46 | "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-28820200820001", 47 | "@dcloudio/vue-cli-plugin-uni": "^2.0.0-28820200820001", 48 | "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-28820200820001", 49 | "@dcloudio/webpack-uni-mp-loader": "^2.0.0-28820200820001", 50 | "@dcloudio/webpack-uni-pages-loader": "^2.0.0-28820200820001", 51 | "@types/html5plus": "^1.0.1", 52 | "@types/uni-app": "^1.4.3", 53 | "@vue/cli-plugin-babel": "3.5.1", 54 | "@vue/cli-service": "^4.5.4", 55 | "babel-plugin-import": "^1.11.0", 56 | "less": "^3.12.2", 57 | "less-loader": "^5.0.0", 58 | "mini-types": "^0.1.4", 59 | "miniprogram-api-typings": "^2.12.0", 60 | "node-sass": "^4.14.1", 61 | "postcss-comment": "^2.0.0", 62 | "sass-loader": "^8.0.2", 63 | "vue-template-compiler": "2.6.10" 64 | }, 65 | "browserslist": [ 66 | "Android >= 4", 67 | "ios >= 8" 68 | ], 69 | "uni-app": { 70 | "scripts": {} 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: require('postcss-comment'), 3 | plugins: [ 4 | require('postcss-import'), 5 | require('autoprefixer')({ 6 | remove: process.env.UNI_PLATFORM !== 'h5' 7 | }), 8 | require('@dcloudio/vue-cli-plugin-uni/packages/postcss') 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 17 | 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 78 | -------------------------------------------------------------------------------- /src/cloudfunctions/callback/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | "customerServiceMessage.send" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /src/cloudfunctions/callback/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | 3 | cloud.init({ 4 | // API 调用都保持和云函数当前所在环境一致 5 | env: cloud.DYNAMIC_CURRENT_ENV 6 | }) 7 | 8 | // 云函数入口函数 9 | exports.main = async (event, context) => { 10 | const { 11 | OPENID 12 | } = cloud.getWXContext() 13 | var params = {} 14 | if (event.MsgType === 'text') { 15 | switch (event.Content) { 16 | case "1": 17 | let res = await cloud.downloadFile({ 18 | fileID: 'cloud://yikuang-g6lyc.7969-yikuang-g6lyc-1301476030/qrcode.jpg', 19 | }) 20 | const media = await cloud.openapi.customerServiceMessage.uploadTempMedia({ 21 | type: 'image', 22 | media: { 23 | contentType: 'image/jpeg', 24 | value: res.fileContent, 25 | }, 26 | }) 27 | params = { 28 | touser: OPENID, 29 | msgtype: 'image', 30 | image: { 31 | media_id: media.mediaId 32 | } 33 | } 34 | break; 35 | default: 36 | param = { 37 | touser: OPENID, 38 | msgtype: 'text', 39 | text: { 40 | content: event.Content 41 | } 42 | } 43 | break; 44 | } 45 | } 46 | const result = await cloud.openapi.customerServiceMessage.send(params) 47 | return result 48 | } -------------------------------------------------------------------------------- /src/cloudfunctions/callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callback", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /src/cloudfunctions/echo/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/cloudfunctions/echo/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | 3 | exports.main = async (event, context) => { 4 | // event.userInfo 是已废弃的保留字段,在此不做展示 5 | // 获取 OPENID 等微信上下文请使用 cloud.getWXContext() 6 | delete event.userInfo 7 | return event 8 | } 9 | -------------------------------------------------------------------------------- /src/cloudfunctions/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /src/cloudfunctions/login/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/cloudfunctions/login/index.js: -------------------------------------------------------------------------------- 1 | // 云函数模板 2 | // 部署:在 cloud-functions/login 文件夹右击选择 “上传并部署” 3 | 4 | const cloud = require('wx-server-sdk') 5 | 6 | // 初始化 cloud 7 | cloud.init({ 8 | // API 调用都保持和云函数当前所在环境一致 9 | env: cloud.DYNAMIC_CURRENT_ENV 10 | }) 11 | 12 | /** 13 | * 这个示例将经自动鉴权过的小程序用户 openid 返回给小程序端 14 | * 15 | * event 参数包含小程序端调用传入的 data 16 | * 17 | */ 18 | exports.main = (event, context) => { 19 | console.log(event) 20 | console.log(context) 21 | 22 | // 可执行其他自定义逻辑 23 | // console.log 的内容可以在云开发云函数调用日志查看 24 | 25 | // 获取 WX Context (微信调用上下文),包括 OPENID、APPID、及 UNIONID(需满足 UNIONID 获取条件)等信息 26 | const wxContext = cloud.getWXContext() 27 | 28 | return { 29 | event, 30 | openid: wxContext.OPENID, 31 | appid: wxContext.APPID, 32 | unionid: wxContext.UNIONID, 33 | env: wxContext.ENV, 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/cloudfunctions/login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "login", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/cloudfunctions/openapi/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | "wxacode.get", 5 | "templateMessage.send", 6 | "templateMessage.addTemplate", 7 | "templateMessage.deleteTemplate", 8 | "templateMessage.getTemplateList", 9 | "templateMessage.getTemplateLibraryById", 10 | "templateMessage.getTemplateLibraryList" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /src/cloudfunctions/openapi/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | 4 | cloud.init({ 5 | // API 调用都保持和云函数当前所在环境一致 6 | env: cloud.DYNAMIC_CURRENT_ENV 7 | }) 8 | 9 | // 云函数入口函数 10 | exports.main = async (event, context) => { 11 | console.log(event) 12 | switch (event.action) { 13 | case 'sendTemplateMessage': { 14 | return sendTemplateMessage(event) 15 | } 16 | case 'getWXACode': { 17 | return getWXACode(event) 18 | } 19 | case 'getOpenData': { 20 | return getOpenData(event) 21 | } 22 | default: { 23 | return 24 | } 25 | } 26 | } 27 | 28 | async function uploadTempMedia(event) { 29 | const { 30 | OPENID 31 | } = cloud.getWXContext() 32 | let res = await cloud.downloadFile({ 33 | fileID: 'cloud://yikuang-g6lyc.7969-yikuang-g6lyc-1301476030/qrcode.jpg ', 34 | }) 35 | const file = fs.readFileSync(path.join(__dirname, 'kefu_wx.JPG')) 36 | res = cloud.openapi.customerServiceMessage.uploadTempMedia({ 37 | type: 'image', 38 | media: { 39 | contentType: 'image/png', 40 | value: res.fileContent 41 | } 42 | }) 43 | console.log(res); 44 | return res; 45 | 46 | return sendResult 47 | } 48 | 49 | async function sendTemplateMessage(event) { 50 | const { 51 | OPENID 52 | } = cloud.getWXContext() 53 | 54 | // 接下来将新增模板、发送模板消息、然后删除模板 55 | // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用 56 | const addResult = await cloud.openapi.templateMessage.addTemplate({ 57 | id: 'AT0002', 58 | keywordIdList: [3, 4, 5] 59 | }) 60 | 61 | const templateId = addResult.templateId 62 | 63 | const sendResult = await cloud.openapi.templateMessage.send({ 64 | touser: OPENID, 65 | templateId, 66 | formId: event.formId, 67 | page: 'pages/openapi/openapi', 68 | data: { 69 | keyword1: { 70 | value: '未名咖啡屋', 71 | }, 72 | keyword2: { 73 | value: '2019 年 1 月 1 日', 74 | }, 75 | keyword3: { 76 | value: '拿铁', 77 | }, 78 | } 79 | }) 80 | 81 | await cloud.openapi.templateMessage.deleteTemplate({ 82 | templateId, 83 | }) 84 | 85 | return sendResult 86 | } 87 | 88 | async function getWXACode(event) { 89 | 90 | // 此处将获取永久有效的小程序码,并将其保存在云文件存储中,最后返回云文件 ID 给前端使用 91 | 92 | const wxacodeResult = await cloud.openapi.wxacode.get({ 93 | path: 'pages/openapi/openapi', 94 | }) 95 | 96 | const fileExtensionMatches = wxacodeResult.contentType.match(/\/([^\/]+)/) 97 | const fileExtension = (fileExtensionMatches && fileExtensionMatches[1]) || 'jpg' 98 | 99 | const uploadResult = await cloud.uploadFile({ 100 | // 云文件路径,此处为演示采用一个固定名称 101 | cloudPath: `wxacode_default_openapi_page.${fileExtension}`, 102 | // 要上传的文件内容可直接传入图片 Buffer 103 | fileContent: wxacodeResult.buffer, 104 | }) 105 | 106 | if (!uploadResult.fileID) { 107 | throw new Error(`upload failed with empty fileID and storage server status code ${uploadResult.statusCode}`) 108 | } 109 | 110 | return uploadResult.fileID 111 | } 112 | 113 | async function getOpenData(event) { 114 | // 需 wx-server-sdk >= 0.5.0 115 | return cloud.getOpenData({ 116 | list: event.openData.list, 117 | }) 118 | } -------------------------------------------------------------------------------- /src/cloudfunctions/openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/mixins/openWeb.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | openWeb(item) { 4 | console.log("clickBanner=>", item); 5 | if (item.jump_type == "mini") { 6 | return uni.navigateToMiniProgram({ 7 | ...(item.mini_url || {}), 8 | fail(err) { 9 | console.log(err, "??er"); 10 | } 11 | }); 12 | } else if (item.link) { 13 | if (item.jump_type == "url") { 14 | uni.navigateTo({ 15 | url: `/pages/webview?url=${encodeURIComponent(item.link)}` 16 | }); 17 | } else { 18 | uni.navigateTo({ 19 | url: item.link 20 | }); 21 | } 22 | } 23 | }, 24 | } 25 | } -------------------------------------------------------------------------------- /src/common/palette/share.js: -------------------------------------------------------------------------------- 1 | class Share { 2 | userInfo = {} 3 | qrcode = '' 4 | do(userInfo, url, mapUrl) { 5 | this.userInfo = JSON.parse(JSON.stringify(userInfo)) 6 | this.qrcode = url 7 | this.mapUrl = mapUrl 8 | return this._template() 9 | } 10 | _template() { 11 | let userInfo = this.userInfo || {}; 12 | console.log(userInfo, "???userInfo") 13 | let obj = { 14 | background: '#ffffff', 15 | width: '640rpx', 16 | height: '800rpx', 17 | borderRadius: '4rpx', 18 | views: [] 19 | } 20 | let textGenerator = ({ top, left, fontSize, color, text, ...data }) => { 21 | return { 22 | type: 'text', 23 | text, 24 | css: { 25 | top, 26 | left, 27 | fontSize, 28 | color, 29 | ...data 30 | } 31 | } 32 | } 33 | 34 | let rectGenerator = ({ 35 | top, 36 | left, 37 | width, 38 | height, 39 | color, 40 | borderRadius = 0 41 | }) => { 42 | return { 43 | type: 'rect', 44 | css: { 45 | top, 46 | left, 47 | width, 48 | height, 49 | color, 50 | borderRadius 51 | } 52 | } 53 | } 54 | 55 | let imgGenerator = ({ top, left, url, width, height, ...data }) => { 56 | return { 57 | type: 'image', 58 | url, 59 | css: { 60 | top, 61 | left, 62 | width, 63 | height, 64 | ...data 65 | } 66 | } 67 | } 68 | 69 | let textList = [ 70 | { 71 | top: '52rpx', 72 | left: '160rpx', 73 | fontSize: '32rpx', 74 | fontWeight: 'bold', 75 | lineHeight: '48rpx', 76 | width: '380rpx', 77 | maxLines: 1, 78 | color: '#2A2A2A', 79 | text: userInfo.nickName 80 | }, 81 | { 82 | top: '100rpx', 83 | left: '160rpx', 84 | fontSize: '32rpx', 85 | maxLines: 2, 86 | lineHeight: '48rpx', 87 | width: '380rpx', 88 | color: '#2A2A2A', 89 | text: `附近的确诊病例逗留情况` 90 | }, 91 | { 92 | top: '624rpx', 93 | left: '220rpx', 94 | fontSize: '32rpx', 95 | maxLines: 2, 96 | lineHeight: '48rpx', 97 | width: '380rpx', 98 | color: '#2A2A2A', 99 | text: `长按前往「疫况」小程序 查看你身边的实时疫情` 100 | }, 101 | 102 | ] 103 | 104 | let rectList = [ 105 | 106 | ] 107 | let imgList = [ 108 | { 109 | top: '50rpx', 110 | left: '40rpx', 111 | width: '100rpx', 112 | height: '100rpx', 113 | url: userInfo.avatar 114 | }, 115 | { 116 | top: '202rpx', 117 | left: '0rpx', 118 | width: '640rpx', 119 | height: '332rpx', 120 | url: this.mapUrl 121 | }, 122 | { 123 | top: '586rpx', 124 | left: '32rpx', 125 | width: 'auto', 126 | height: '170rpx', 127 | url: this.qrcode, 128 | } 129 | ] 130 | textList.forEach(i => { 131 | obj.views.push(textGenerator(i)) 132 | }) 133 | rectList.forEach(i => { 134 | obj.views.push(rectGenerator(i)) 135 | }) 136 | imgList.forEach(i => { 137 | obj.views.push(imgGenerator(i)) 138 | }) 139 | return obj 140 | } 141 | } 142 | export default Share; 143 | -------------------------------------------------------------------------------- /src/common/util.js: -------------------------------------------------------------------------------- 1 | function formatTime(time) { 2 | if (typeof time !== 'number' || time < 0) { 3 | return time 4 | } 5 | 6 | var hour = parseInt(time / 3600) 7 | time = time % 3600 8 | var minute = parseInt(time / 60) 9 | time = time % 60 10 | var second = time 11 | 12 | return ([hour, minute, second]).map(function (n) { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | }).join(':') 16 | } 17 | 18 | function formatLocation(longitude, latitude) { 19 | if (typeof longitude === 'string' && typeof latitude === 'string') { 20 | longitude = parseFloat(longitude) 21 | latitude = parseFloat(latitude) 22 | } 23 | 24 | longitude = longitude.toFixed(2) 25 | latitude = latitude.toFixed(2) 26 | 27 | return { 28 | longitude: longitude.toString().split('.'), 29 | latitude: latitude.toString().split('.') 30 | } 31 | } 32 | 33 | var dateUtils = { 34 | UNITS: { 35 | '年': 31557600000, 36 | '月': 2629800000, 37 | '天': 86400000, 38 | '小时': 3600000, 39 | '分钟': 60000, 40 | '秒': 1000 41 | }, 42 | humanize: function (milliseconds) { 43 | var humanize = ''; 44 | for (var key in this.UNITS) { 45 | if (milliseconds >= this.UNITS[key]) { 46 | humanize = Math.floor(milliseconds / this.UNITS[key]) + key + '前'; 47 | break; 48 | } 49 | } 50 | return humanize || '刚刚'; 51 | }, 52 | format: function (dateStr) { 53 | var date = this.parse(dateStr) 54 | var diff = Date.now() - date.getTime(); 55 | if (diff < this.UNITS['天']) { 56 | return this.humanize(diff); 57 | } 58 | var _format = function (number) { 59 | return (number < 10 ? ('0' + number) : number); 60 | }; 61 | return date.getFullYear() + '/' + _format(date.getMonth() + 1) + '/' + _format(date.getDay()) + '-' + 62 | _format(date.getHours()) + ':' + _format(date.getMinutes()); 63 | }, 64 | parse: function (str) { //将"yyyy-mm-dd HH:MM:ss"格式的字符串,转化为一个Date对象 65 | var a = str.split(/[^0-9]/); 66 | return new Date(a[0], a[1] - 1, a[2], a[3], a[4], a[5]); 67 | } 68 | }; 69 | // 函数防抖 70 | function debounce(fn, wait = 0, initStart = false) { 71 | let timer; 72 | return function () { 73 | let context = this; // 74 | let args = arguments; // 75 | if (initStart) { 76 | timer = null; 77 | clearTimeout(timer); 78 | return fn.apply(context, args); 79 | } 80 | if (timer) clearTimeout(timer); 81 | timer = setTimeout(() => { 82 | timer = null; 83 | fn.apply(context, args); 84 | }, wait) 85 | } 86 | } 87 | // 函数节流 88 | function throttle(fn, wait) { 89 | var timer = null; 90 | return function () { 91 | var context = this; 92 | var args = arguments; 93 | if (!timer) { 94 | timer = setTimeout(function () { 95 | fn.apply(context, args); 96 | timer = null; 97 | }, wait) 98 | } 99 | } 100 | } 101 | module.exports = { 102 | formatTime: formatTime, 103 | formatLocation: formatLocation, 104 | dateUtils: dateUtils, 105 | throttle, 106 | debounce 107 | } -------------------------------------------------------------------------------- /src/common/vmeitime-http/article.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | exports.articles = (data) => { 5 | return http.request({ 6 | url: 'api/v1/articles', 7 | method: 'GET', 8 | data 9 | }) 10 | } 11 | 12 | exports.articlesDetail = (id,data) => { 13 | return http.request({ 14 | url: `api/v1/articles/${id}`, 15 | method: 'GET', 16 | data 17 | }) 18 | } -------------------------------------------------------------------------------- /src/common/vmeitime-http/auth.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | exports.loginByOauth = (data) => { 5 | return http.request({ 6 | url: 'api/v1/auth/loginByOauth', 7 | method: 'POST', 8 | data, 9 | }) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/common/vmeitime-http/guide.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | exports.guides = (data) => { 5 | return http.request({ 6 | url: 'api/v1/pages/news', 7 | method: 'GET', 8 | data, 9 | // handle:true 10 | }) 11 | } -------------------------------------------------------------------------------- /src/common/vmeitime-http/home.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | import store from '@/store' 3 | // const type = store.state.app.type 4 | 5 | //search页面菜单 6 | exports.searchPage = (data) => { 7 | return http.request({ 8 | url: 'api/v1/pages/search', 9 | method: 'GET', 10 | data 11 | }) 12 | } 13 | //首页数据 14 | exports.homePage = (data,type=0) => { 15 | return http.request({ 16 | url: type === 0 ? 'api/v1/pages/home' : 'api/v1/pages/homeHospital', 17 | method: 'GET', 18 | data, 19 | 20 | }) 21 | } 22 | // banner 23 | exports.banners = (data) => { 24 | return http.request({ 25 | url: 'api/v1/banners', 26 | method: 'GET', 27 | data 28 | }) 29 | } 30 | 31 | /* #ifdef MP-QQ */ 32 | exports.markers = (data, type = 0) => { 33 | data.type = 'qq' 34 | return http.request({ 35 | url: type === 0 ? 'api/v2/pages/home/mapData' : 'api/v2/pages/home/mapDataHospital', 36 | method: 'POST', 37 | data 38 | }) 39 | } 40 | /* #endif */ 41 | 42 | /* #ifndef MP-QQ */ 43 | exports.markers = (data, type = 0) => { 44 | return http.request({ 45 | url: type === 0 ? 'api/v2/pages/home/mapData' : 'api/v2/pages/home/mapDataHospital', 46 | method: 'POST', 47 | data 48 | }) 49 | } 50 | /* #endif */ -------------------------------------------------------------------------------- /src/common/vmeitime-http/hospital.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | exports.hospitals = (data) => { 5 | return http.request({ 6 | url: 'api/v1/hospitals', 7 | method: 'GET', 8 | data, 9 | // handle:true 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/common/vmeitime-http/index.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | // 默认全部导出 import api from '@/common/vmeitime-http/' 5 | export default { 6 | ...require("./home"), 7 | ...require("./me"), 8 | ...require("./shop"), 9 | ...require("./hospital"), 10 | ...require("./guide"), 11 | ...require("./auth"), 12 | ...require("./article") 13 | } -------------------------------------------------------------------------------- /src/common/vmeitime-http/interface.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | config: { 4 | // baseUrl: 'https://appointment.junyao.tech/', 5 | // baseUrl: 'http://119.23.73.222:7001/', 6 | baseUrl: 'https://api.webhunt.cn/', 7 | // baseUrl: 'http://119.23.230.227:7001/', 8 | // baseUrl: 'http://127.0.0.1:7001/', 9 | header: { 10 | 'Content-Type': 'application/json;charset=UTF-8', 11 | 'Content-Type': 'application/x-www-form-urlencoded', 12 | }, 13 | data: {}, 14 | method: "GET", 15 | dataType: "json", 16 | /* 如设为json,会对返回的数据做一次 JSON.parse */ 17 | responseType: "text", 18 | success() {}, 19 | fail() {}, 20 | complete() {} 21 | }, 22 | interceptor: { 23 | request: null, 24 | response: null 25 | }, 26 | request(options) { 27 | if (!options) { 28 | options = {} 29 | } 30 | options.dataType = options.dataType || this.config.dataType 31 | options.url = (options.baseUrl || this.config.baseUrl) + options.url 32 | options.data = options.data || {} 33 | options.method = options.method || this.config.method 34 | 35 | const _token = { 36 | 'Authorization': uni.getStorageSync('user') ? 'Bearer ' + uni.getStorageSync('user').token : 'undefined' 37 | } 38 | options.header = Object.assign({}, options.header, _token) 39 | console.log('options', options) 40 | return new Promise((resolve, reject) => { 41 | let _config = null 42 | 43 | options.complete = (response) => { 44 | let statusCode = response.statusCode 45 | response.config = _config 46 | if (process.env.NODE_ENV === 'development') { 47 | if (statusCode === 200) { 48 | // console.log("【" + _config.requestId + "】 结果:" + JSON.stringify(response.data)) 49 | } 50 | } 51 | if (this.interceptor.response) { 52 | let newResponse = this.interceptor.response(response) 53 | if (newResponse) { 54 | response = newResponse 55 | } 56 | } 57 | // 统一的响应日志记录 58 | _reslog(response) 59 | 60 | if (statusCode === 200) { //成功 61 | // console.log(response.data.code) 62 | if (response.data.code === 401 || response.data.code === 402 || response.data.code === 403) { 63 | uni.removeStorageSync('user'); 64 | uni.switchTab({ 65 | url: '/pages/me/me' 66 | }); 67 | uni.showToast({ 68 | title: '请先登陆', 69 | icon: 'none' 70 | }); 71 | } 72 | resolve(response); 73 | } else { 74 | reject(response) 75 | } 76 | } 77 | 78 | _config = Object.assign({}, this.config, options) 79 | _config.requestId = new Date().getTime() 80 | 81 | if (this.interceptor.request) { 82 | this.interceptor.request(_config) 83 | } 84 | 85 | // 统一的请求日志记录 86 | _reqlog(_config) 87 | 88 | if (process.env.NODE_ENV === 'development') { 89 | // console.log("【" + _config.requestId + "】 地址:" + _config.url) 90 | if (_config.data) { 91 | // console.log("【" + _config.requestId + "】 参数:" + JSON.stringify(_config.data)) 92 | } 93 | } 94 | 95 | uni.request(_config); 96 | }); 97 | }, 98 | get(url, data, options) { 99 | if (!options) { 100 | options = {} 101 | } 102 | options.url = url 103 | options.data = data 104 | options.method = 'GET' 105 | return this.request(options) 106 | }, 107 | post(url, data, options) { 108 | if (!options) { 109 | options = {} 110 | } 111 | options.url = url 112 | options.data = data 113 | options.method = 'POST' 114 | return this.request(options) 115 | }, 116 | put(url, data, options) { 117 | if (!options) { 118 | options = {} 119 | } 120 | options.url = url 121 | options.data = data 122 | options.method = 'PUT' 123 | return this.request(options) 124 | }, 125 | delete(url, data, options) { 126 | if (!options) { 127 | options = {} 128 | } 129 | options.url = url 130 | options.data = data 131 | options.method = 'DELETE' 132 | return this.request(options) 133 | } 134 | } 135 | 136 | 137 | /** 138 | * 请求接口日志记录 139 | */ 140 | function _reqlog(req) { 141 | if (process.env.NODE_ENV === 'development') { 142 | // console.log("【" + req.requestId + "】 地址:" + req.url) 143 | if (req.data) { 144 | // console.log("【" + req.requestId + "】 请求参数:" + JSON.stringify(req.data)) 145 | } 146 | } 147 | //TODO 调接口异步写入日志数据库 148 | } 149 | 150 | /** 151 | * 响应接口日志记录 152 | */ 153 | function _reslog(res) { 154 | let _statusCode = res.statusCode; 155 | if (process.env.NODE_ENV === 'development') { 156 | // console.log("【" + res.config.requestId + "】 地址:" + res.config.url) 157 | if (res.config.data) { 158 | // console.log("【" + res.config.requestId + "】 请求参数:" + JSON.stringify(res.config.data)) 159 | } 160 | // console.log("【" + res.config.requestId + "】 响应结果:" + JSON.stringify(res)) 161 | } 162 | //TODO 除了接口服务错误外,其他日志调接口异步写入日志数据库 163 | switch (_statusCode) { 164 | case 200: 165 | break; 166 | case 401: 167 | break; 168 | case 404: 169 | break; 170 | default: 171 | break; 172 | } 173 | } -------------------------------------------------------------------------------- /src/common/vmeitime-http/me.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | // 反馈 4 | exports.feedback = (data) => { 5 | return http.request({ 6 | url: 'api/v1/feedbacks', 7 | method: 'POST', 8 | data, 9 | // handle:true 10 | }) 11 | } 12 | 13 | //赞助支持 14 | exports.supports = (data) => { 15 | return http.request({ 16 | url: 'api/v1/supports', 17 | method: 'GET', 18 | data, 19 | // handle:true 20 | }) 21 | } 22 | 23 | //赞助支持 24 | exports.subscribe = (data) => { 25 | return http.request({ 26 | url: 'api/v1/subscribes', 27 | method: 'POST', 28 | data, 29 | // handle:true 30 | }) 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/common/vmeitime-http/shop.js: -------------------------------------------------------------------------------- 1 | import http from './interface' 2 | 3 | 4 | exports.shops = (data,type=0) => { 5 | return http.request({ 6 | url: type === 0 ? 'api/v2/shops' : 'api/v1/hospitals', 7 | method: 'GET', 8 | data, 9 | // handle:true 10 | }) 11 | } 12 | 13 | exports.getShopDetail = (id, data = {},type=0) => { 14 | return http.request({ 15 | url: type === 0 ? ('api/v1/shops/' + id) : ('api/v1/hospitals/' + id), 16 | method: 'GET', 17 | data 18 | }) 19 | } -------------------------------------------------------------------------------- /src/components/index/list.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 83 | 84 | 85 | 232 | -------------------------------------------------------------------------------- /src/components/jyf-parser/libs/CssHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | 解析和匹配 Css 的选择器 3 | github:https://github.com/jin-yufeng/Parser 4 | docs:https://jin-yufeng.github.io/Parser 5 | author:JinYufeng 6 | */ 7 | const config = require("./config.js"); 8 | class CssHandler { 9 | constructor(tagStyle = {}) { 10 | this.styles = Object.assign({}, tagStyle); 11 | }; 12 | getStyle(data) { 13 | var style = ''; 14 | data = data.replace(/<[sS][tT][yY][lL][eE][\s\S]*?>([\s\S]*?)<\/[sS][tT][yY][lL][eE][\s\S]*?>/g, function($, $1) { 15 | style += $1; 16 | return ''; 17 | }) 18 | this.styles = new CssParser(style, this.styles).parse(); 19 | return data; 20 | }; 21 | match(name, attrs) { 22 | var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : ''; 23 | if (attrs.class) { 24 | var classes = attrs.class.split(' '); 25 | for (var i = 0; i < classes.length; i++) 26 | if (tmp = this.styles['.' + classes[i]]) 27 | matched += tmp + ';'; 28 | } 29 | if (tmp = this.styles['#' + attrs.id]) 30 | matched += tmp + ';'; 31 | return matched; 32 | }; 33 | } 34 | module.exports = CssHandler; 35 | class CssParser { 36 | constructor(data, tagStyle) { 37 | this.data = data; 38 | this.res = tagStyle; 39 | for (var item in config.userAgentStyles) { 40 | if (tagStyle[item]) tagStyle[item] = config.userAgentStyles[item] + ';' + tagStyle[item]; 41 | else tagStyle[item] = config.userAgentStyles[item]; 42 | } 43 | this._comma = false; 44 | this._floor = 0; 45 | this._i = 0; 46 | this._list = []; 47 | this._start = 0; 48 | this._state = this.Space; 49 | }; 50 | parse() { 51 | for (; this._i < this.data.length; this._i++) 52 | this._state(this.data[this._i]); 53 | return this.res; 54 | }; 55 | // 状态机 56 | Space(c) { 57 | if (c == '.' || c == '#' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { 58 | this._start = this._i; 59 | this._state = this.StyleName; 60 | } else if (c == '/' && this.data[this._i + 1] == '*') 61 | this.Comment(); 62 | else if (!config.blankChar[c] && c != ';') 63 | this._state = this.Ignore; 64 | }; 65 | Comment() { 66 | this._i = this.data.indexOf("*/", this._i) + 1; 67 | if (!this._i) this._i = this.data.length; 68 | this._state = this.Space; 69 | }; 70 | Ignore(c) { 71 | if (c == '{') this._floor++; 72 | else if (c == '}' && !--this._floor) { 73 | this._list = []; 74 | this._state = this.Space; 75 | } 76 | }; 77 | StyleName(c) { 78 | if (config.blankChar[c]) { 79 | if (this._start != this._i) 80 | this._list.push(this.data.substring(this._start, this._i)); 81 | this._state = this.NameSpace; 82 | } else if (c == '{') { 83 | this._list.push(this.data.substring(this._start, this._i)); 84 | this._start = this._i + 1; 85 | this.Content(); 86 | } else if (c == ',') { 87 | this._list.push(this.data.substring(this._start, this._i)); 88 | this._start = this._i + 1; 89 | this._comma = true; 90 | } else if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '#' && c != '-' && 91 | c != '_') 92 | this._state = this.Ignore; 93 | }; 94 | NameSpace(c) { 95 | if (c == '{') { 96 | this._start = this._i + 1; 97 | this.Content(); 98 | } else if (c == ',') { 99 | this._comma = true; 100 | this._start = this._i + 1; 101 | this._state = this.StyleName; 102 | } else if (!config.blankChar[c]) { 103 | if (this._comma) { 104 | this._state = this.StyleName; 105 | this._start = this._i--; 106 | this._comma = false; 107 | } else this._state = this.Ignore; 108 | } 109 | }; 110 | Content() { 111 | this._i = this.data.indexOf('}', this._i); 112 | if (this._i == -1) this._i = this.data.length; 113 | var content = this.data.substring(this._start, this._i); 114 | for (var i = this._list.length; i--;) 115 | this.res[this._list[i]] = (this.res[this._list[i]] || '') + content; 116 | this._list = []; 117 | this._state = this.Space; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/jyf-parser/libs/config.js: -------------------------------------------------------------------------------- 1 | /* 配置文件 */ 2 | function makeMap(str, obj = {}) { 3 | var map = obj, 4 | list = str.split(','); 5 | for (var i = list.length; i--;) 6 | map[list[i]] = true; 7 | return map; 8 | } 9 | // 信任的属性列表,不在列表中的属性将被移除 10 | const trustAttrs = makeMap( 11 | "align,allowfullscreen,alt,app-id,appid,apid,author,autoplay,border,cellpadding,cellspacing,class,color,colspan,controls,data-src,dir,face,frameborder,height,href,id,ignore,loop,media,muted,name,path,poster,rowspan,size,span,src,start,style,type,unit-id,unitId,width,xmlns" 12 | ); 13 | // 信任的标签,将保持标签名不变 14 | const trustTags = makeMap( 15 | "a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,u,ul,video" 16 | // #ifdef APP-PLUS 17 | + 18 | ",embed,iframe" 19 | // #endif 20 | ); 21 | // 块级标签,将被转为 div 22 | const blockTags = makeMap("address,article,aside,body,center,cite,footer,header,html,nav,pre,section"); 23 | // 被移除的标签(其中 svg 系列标签会被转为图片) 24 | const ignoreTags = makeMap( 25 | "area,base,basefont,canvas,circle,command,ellipse,frame,head,input,isindex,keygen,line,link,map,meta,param,path,polygon,rect,script,source,svg,textarea,track,use,wbr" 26 | // #ifndef APP-PLUS 27 | + 28 | ",embed,iframe" 29 | // #endif 30 | ); 31 | // 只能用 rich-text 显示的标签(其中图片不能预览、不能显示视频、音频等) 32 | const richOnlyTags = makeMap("a,colgroup,fieldset,legend,picture,table,tbody,td,tfoot,th,thead,tr"); 33 | // 自闭合标签 34 | const selfClosingTags = makeMap( 35 | "area,base,basefont,br,col,circle,ellipse,embed,frame,hr,img,input,isindex,keygen,line,link,meta,param,path,polygon,rect,source,track,use,wbr" 36 | ); 37 | // 空白字符 38 | const blankChar = makeMap(" ,\u00A0,\t,\r,\n,\f"); 39 | // 默认的标签样式 40 | var userAgentStyles = { 41 | a: "color:#366092;word-break:break-all;padding:1.5px 0 1.5px 0", 42 | address: "font-style:italic", 43 | big: "display:inline;font-size:1.2em", 44 | blockquote: "background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px", 45 | center: "text-align:center", 46 | cite: "font-style:italic", 47 | dd: "margin-left:40px", 48 | img: "max-width:100%", 49 | mark: "background-color:yellow", 50 | picture: "max-width:100%", 51 | pre: "font-family:monospace;white-space:pre;overflow:scroll", 52 | s: "text-decoration:line-through", 53 | small: "display:inline;font-size:0.8em", 54 | u: "text-decoration:underline" 55 | }; 56 | const screenWidth = wx.getSystemInfoSync().screenWidth; 57 | // #ifdef MP-WEIXIN 58 | // 版本兼容 59 | if (wx.canIUse("editor")) { 60 | makeMap("bdi,bdo,caption,rt,ruby,pre", trustTags); 61 | makeMap("bdi,bdo,caption,rt,ruby,pre", richOnlyTags); 62 | ignoreTags.rp = true; 63 | blockTags.pre = undefined; 64 | } else 65 | // #endif 66 | blockTags.caption = true; 67 | 68 | function bubbling(Parser) { 69 | for (var i = Parser._STACK.length; i--;) { 70 | if (!richOnlyTags[Parser._STACK[i].name]) 71 | Parser._STACK[i].c = 1; 72 | else return false; 73 | } 74 | return true; 75 | } 76 | module.exports = { 77 | // 高亮处理函数 78 | highlight: null, 79 | // 处理标签的属性,需要通过组件递归方式显示的标签需要调用 bubbling(Parser) 80 | LabelHandler(node, Parser) { 81 | var attrs = node.attrs; 82 | attrs.style = Parser.CssHandler.match(node.name, attrs, node) + (attrs.style || ''); 83 | switch (node.name) { 84 | case "div": 85 | case 'p': 86 | if (attrs.align) { 87 | attrs.style = `text-align:${attrs.align};${attrs.style}`; 88 | attrs.align = void 0; 89 | } 90 | break; 91 | case "img": 92 | if (attrs["data-src"]) { 93 | attrs.src = attrs.src || attrs["data-src"]; 94 | attrs["data-src"] = void 0; 95 | } 96 | if (attrs.width && parseInt(attrs.width) > screenWidth) 97 | attrs.style += ";height:auto !important"; 98 | if (attrs.src && !attrs.ignore) { 99 | if (bubbling(Parser)) attrs.i = (Parser._imgNum++).toString(); 100 | else attrs.ignore = 'T'; 101 | } 102 | break; 103 | case 'a': 104 | case "ad": 105 | // #ifdef APP-PLUS 106 | case "iframe": 107 | case "embed": 108 | // #endif 109 | bubbling(Parser); 110 | break; 111 | case "font": 112 | if (attrs.color) { 113 | attrs.style = `color:${attrs.color};${attrs.style}`; 114 | attrs.color = void 0; 115 | } 116 | if (attrs.face) { 117 | attrs.style = `font-family:${attrs.face};${attrs.style}`; 118 | attrs.face = void 0; 119 | } 120 | if (attrs.size) { 121 | var size = parseInt(attrs.size); 122 | if (size < 1) size = 1; 123 | else if (size > 7) size = 7; 124 | var map = ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"]; 125 | attrs.style = `font-size:${map[size - 1]};${attrs.style}`; 126 | attrs.size = void 0; 127 | } 128 | break; 129 | case "video": 130 | case "audio": 131 | if (attrs.id) Parser[`_${node.name}Num`]++; 132 | else attrs.id = (node.name + (++Parser[`_${node.name}Num`])); 133 | if (node.name == "video") { 134 | attrs.style = attrs.style || ''; 135 | if (attrs.width) { 136 | attrs.style = `width:${parseFloat(attrs.width) + attrs.width.includes('%') ? '%' : "px"};${attrs.style}`; 137 | attrs.width = void 0; 138 | } 139 | if (attrs.height) { 140 | attrs.style = `height:${parseFloat(attrs.height) + attrs.height.includes('%') ? '%' : "px"};${attrs.style}`; 141 | attrs.height = void 0; 142 | } 143 | if (Parser._videoNum > 3) node.lazyLoad = true; 144 | } 145 | attrs.source = []; 146 | if (attrs.src) attrs.source.push(attrs.src); 147 | if (!attrs.controls && !attrs.autoplay) 148 | console.warn(`存在没有 controls 属性的 ${node.name} 标签,可能导致无法播放`, node); 149 | bubbling(Parser); 150 | break; 151 | case "source": 152 | var i, parent = Parser._STACK[Parser._STACK.length - 1]; 153 | if (!parent || !attrs.src) break; 154 | if (parent.name == "video" || parent.name == "audio") 155 | parent.attrs.source.push(attrs.src); 156 | else { 157 | var i, media = attrs.media; 158 | if (parent.name == "picture" && !parent.attrs.src && (!media || (media.includes("px") && 159 | (((i = media.indexOf("min-width")) != -1 && (i = media.indexOf(':', i + 8)) != -1 && 160 | screenWidth > parseInt(media.substring(i + 1))) || 161 | ((i = media.indexOf("max-width")) != -1 && (i = media.indexOf(':', i + 8)) != -1 && 162 | screenWidth < parseInt(media.substring(i + 1))))))) 163 | parent.attrs.src = attrs.src; 164 | } 165 | } 166 | // 压缩 style 167 | var styles = attrs.style.split(';'), 168 | compressed = {}; 169 | attrs.style = ""; 170 | for (var i = 0, len = styles.length; i < len; i++) { 171 | var info = styles[i].split(':'); 172 | if (info.length < 2) continue; 173 | var key = info[0].trim().toLowerCase(), 174 | value = info.slice(1).join(':').trim(); 175 | // 填充链接 176 | if (value.includes("url")) { 177 | var j = value.indexOf('('); 178 | if (j++ != -1) { 179 | while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++; 180 | if (value[j] == '/') { 181 | if (value[j + 1] == '/') value = value.substring(0, j) + Parser._protocol + ':' + value.substring(j); 182 | else if (Parser._domain) value = value.substring(0, j) + Parser._domain + value.substring(j); 183 | } 184 | } 185 | } 186 | // 转换 rpx 187 | else if (value.includes("rpx")) 188 | value = value.replace(/[0-9.]*rpx/g, function($) { 189 | return parseFloat($) * screenWidth / 750 + "px"; 190 | }) 191 | if (value.includes("-webkit") || value.includes("-moz") || value.includes("-ms") || value.includes("-o") || value.includes( 192 | "safe")) 193 | attrs.style += `;${key}:${value}`; 194 | else if (!compressed[key] || value.includes("import") || !compressed[key].includes("import")) 195 | compressed[key] = value; 196 | } 197 | if (node.name == "img" && compressed.width && compressed.width.includes("%") && parseInt(compressed.width) > 198 | screenWidth) 199 | compressed.height = "auto !important"; 200 | for (var key in compressed) 201 | attrs.style += `;${key}:${compressed[key]}`; 202 | attrs.style = attrs.style.substr(1); 203 | if (!attrs.style) attrs.style = void 0; 204 | if (Parser._useAnchor && attrs.id) bubbling(Parser); 205 | }, 206 | trustAttrs, 207 | trustTags, 208 | blockTags, 209 | ignoreTags, 210 | selfClosingTags, 211 | blankChar, 212 | userAgentStyles, 213 | screenWidth 214 | } 215 | -------------------------------------------------------------------------------- /src/components/jyf-parser/libs/handler.sjs: -------------------------------------------------------------------------------- 1 | var inlineTags = { 2 | abbr: 1, 3 | b: 1, 4 | big: 1, 5 | code: 1, 6 | del: 1, 7 | em: 1, 8 | i: 1, 9 | ins: 1, 10 | label: 1, 11 | q: 1, 12 | small: 1, 13 | span: 1, 14 | strong: 1 15 | } 16 | export default { 17 | // 从 rich-text 顶层标签的样式中取出一些给 rich-text 18 | getStyle: function(style, display) { 19 | if (style) { 20 | var i, j, res = ""; 21 | if ((i = style.indexOf("display")) != -1) 22 | res = style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j); 23 | else res = "display:" + display; 24 | if (style.indexOf("flex") != -1) res += ';' + style.match(getRegExp("flex[:-][^;]+/g")).join(';'); 25 | return res; 26 | } else return "display:" + display; 27 | }, 28 | getNode: function(item) { 29 | return [item]; 30 | }, 31 | // 是否通过 rich-text 显示 32 | useRichText: function(item) { 33 | // rich-text 不支持 inline 34 | if (item.c || inlineTags[item.name] || (item.attrs.style && item.attrs.style.indexOf("display:inline") != -1)) 35 | return false; 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/jyf-parser/libs/handler.wxs: -------------------------------------------------------------------------------- 1 | var inlineTags = { 2 | abbr: 1, 3 | b: 1, 4 | big: 1, 5 | code: 1, 6 | del: 1, 7 | em: 1, 8 | i: 1, 9 | ins: 1, 10 | label: 1, 11 | q: 1, 12 | small: 1, 13 | span: 1, 14 | strong: 1 15 | } 16 | module.exports = { 17 | // 从 rich-text 顶层标签的样式中取出一些给 rich-text 18 | getStyle: function(style, display) { 19 | if (style) { 20 | var i, j, res = ""; 21 | if ((i = style.indexOf("display")) != -1) 22 | res = style.substring(i, (j = style.indexOf(';', i)) == -1 ? style.length : j); 23 | else res = "display:" + display; 24 | if (style.indexOf("flex") != -1) res += ';' + style.match(getRegExp("flex[:-][^;]+/g")).join(';'); 25 | return res; 26 | } else return "display:" + display; 27 | }, 28 | // 处理懒加载 29 | getNode: function(item, imgLoad) { 30 | if (!imgLoad) { 31 | var img = { 32 | name: "img", 33 | attrs: JSON.parse(JSON.stringify(item.attrs)) 34 | } 35 | delete img.attrs.src; 36 | img.attrs.style += ";width:20px !important;height:20px !important"; 37 | return [img]; 38 | } else return [item]; 39 | }, 40 | // 是否通过 rich-text 显示 41 | useRichText: function(item) { 42 | // rich-text 不支持 inline 43 | if (item.c || inlineTags[item.name] || (item.attrs.style && item.attrs.style.indexOf("display:inline") != -1)) 44 | return false; 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/me/pd-list2.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 77 | 78 | 190 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-down.css: -------------------------------------------------------------------------------- 1 | /* 下拉刷新区域 */ 2 | .mescroll-downwarp { 3 | position: absolute; 4 | top: -100%; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | text-align: center; 9 | } 10 | 11 | /* 下拉刷新--内容区,定位于区域底部 */ 12 | .mescroll-downwarp .downwarp-content { 13 | position: absolute; 14 | left: 0; 15 | bottom: 0; 16 | width: 100%; 17 | min-height: 60rpx; 18 | padding: 20rpx 0; 19 | text-align: center; 20 | } 21 | 22 | /* 下拉刷新--提示文本 */ 23 | .mescroll-downwarp .downwarp-tip { 24 | display: inline-block; 25 | font-size: 28rpx; 26 | color: gray; 27 | vertical-align: middle; 28 | margin-left: 16rpx; 29 | } 30 | 31 | /* 下拉刷新--旋转进度条 */ 32 | .mescroll-downwarp .downwarp-progress { 33 | display: inline-block; 34 | width: 32rpx; 35 | height: 32rpx; 36 | border-radius: 50%; 37 | border: 2rpx solid gray; 38 | border-bottom-color: transparent; 39 | vertical-align: middle; 40 | } 41 | 42 | /* 旋转动画 */ 43 | .mescroll-downwarp .mescroll-rotate { 44 | animation: mescrollDownRotate 0.6s linear infinite; 45 | } 46 | 47 | @keyframes mescrollDownRotate { 48 | 0% { 49 | transform: rotate(0deg); 50 | } 51 | 52 | 100% { 53 | transform: rotate(360deg); 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-down.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-empty.vue: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | 48 | 49 | 91 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-top.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 49 | 50 | 82 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-up.css: -------------------------------------------------------------------------------- 1 | /* 上拉加载区域 */ 2 | .mescroll-upwarp { 3 | min-height: 60rpx; 4 | padding: 30rpx 0; 5 | text-align: center; 6 | clear: both; 7 | } 8 | 9 | /*提示文本 */ 10 | .mescroll-upwarp .upwarp-tip, 11 | .mescroll-upwarp .upwarp-nodata { 12 | display: inline-block; 13 | font-size: 28rpx; 14 | color: gray; 15 | vertical-align: middle; 16 | } 17 | 18 | .mescroll-upwarp .upwarp-tip { 19 | margin-left: 16rpx; 20 | } 21 | 22 | /*旋转进度条 */ 23 | .mescroll-upwarp .upwarp-progress { 24 | display: inline-block; 25 | width: 32rpx; 26 | height: 32rpx; 27 | border-radius: 50%; 28 | border: 2rpx solid gray; 29 | border-bottom-color: transparent; 30 | vertical-align: middle; 31 | } 32 | 33 | /* 旋转动画 */ 34 | .mescroll-upwarp .mescroll-rotate { 35 | animation: mescrollUpRotate 0.6s linear infinite; 36 | } 37 | 38 | @keyframes mescrollUpRotate { 39 | 0% { 40 | transform: rotate(0deg); 41 | } 42 | 43 | 100% { 44 | transform: rotate(360deg); 45 | } 46 | } -------------------------------------------------------------------------------- /src/components/mescroll-uni/components/mescroll-up.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/mescroll-body.css: -------------------------------------------------------------------------------- 1 | page { 2 | -webkit-overflow-scrolling: touch; /* 使iOS滚动流畅 */ 3 | } 4 | 5 | .mescroll-body { 6 | position: relative; /* 下拉刷新区域相对自身定位 */ 7 | height: auto; /* 不可固定高度,否则overflow: hidden, 可通过设置最小高度使列表不满屏仍可下拉*/ 8 | overflow: hidden; /* 遮住顶部下拉刷新区域 */ 9 | box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ 10 | } -------------------------------------------------------------------------------- /src/components/mescroll-uni/mescroll-body.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 277 | 278 | 283 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/mescroll-mixins.js: -------------------------------------------------------------------------------- 1 | // mescroll-body 和 mescroll-uni 通用 2 | 3 | // import MescrollUni from "./mescroll-uni.vue"; 4 | // import MescrollBody from "./mescroll-body.vue"; 5 | 6 | const MescrollMixin = { 7 | // components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册 8 | // MescrollUni, 9 | // MescrollBody 10 | // }, 11 | data() { 12 | return { 13 | mescroll: null //mescroll实例对象 14 | } 15 | }, 16 | // 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例) 17 | onPullDownRefresh(){ 18 | this.mescroll && this.mescroll.onPullDownRefresh(); 19 | }, 20 | // 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效) 21 | onPageScroll(e) { 22 | this.mescroll && this.mescroll.onPageScroll(e); 23 | }, 24 | // 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效) 25 | onReachBottom() { 26 | this.mescroll && this.mescroll.onReachBottom(); 27 | }, 28 | methods: { 29 | // mescroll组件初始化的回调,可获取到mescroll对象 30 | mescrollInit(mescroll) { 31 | this.mescroll = mescroll; 32 | this.mescrollInitByRef(); // 兼容字节跳动小程序 33 | }, 34 | // 以ref的方式初始化mescroll对象 (兼容字节跳动小程序: http://www.mescroll.com/qa.html?v=20200107#q26) 35 | mescrollInitByRef() { 36 | if(!this.mescroll || !this.mescroll.resetUpScroll){ 37 | let mescrollRef = this.$refs.mescrollRef; 38 | if(mescrollRef) this.mescroll = mescrollRef.mescroll 39 | } 40 | }, 41 | // 下拉刷新的回调 42 | downCallback() { 43 | // mixin默认resetUpScroll 44 | this.mescroll.resetUpScroll() 45 | }, 46 | // 上拉加载的回调 47 | upCallback() { 48 | // mixin默认延时500自动结束加载 49 | setTimeout(()=>{ 50 | this.mescroll.endErr(); 51 | }, 500) 52 | } 53 | }, 54 | mounted() { 55 | this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况 56 | } 57 | 58 | } 59 | 60 | export default MescrollMixin; 61 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/mescroll-uni-option.js: -------------------------------------------------------------------------------- 1 | // 全局配置 2 | // mescroll-body 和 mescroll-uni 通用 3 | const GlobalOption = { 4 | down: { 5 | // 其他down的配置参数也可以写,这里只展示了常用的配置: 6 | textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本 7 | textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本 8 | textLoading: '加载中 ...', // 加载中的提示文本 9 | offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调 10 | native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例) 11 | }, 12 | up: { 13 | // 其他up的配置参数也可以写,这里只展示了常用的配置: 14 | textLoading: '加载中 ...', // 加载中的提示文本 15 | textNoMore: '-- END --', // 没有更多数据的提示文本 16 | offset: 80, // 距底部多远时,触发upCallback 17 | isBounce: false, // 默认禁止橡皮筋的回弹效果, 必读事项: http://www.mescroll.com/qa.html?v=190725#q25 18 | toTop: { 19 | // 回到顶部按钮,需配置src才显示 20 | src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png ) 21 | offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px 22 | right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx) 23 | bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx) 24 | width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx) 25 | }, 26 | empty: { 27 | use: true, // 是否显示空布局 28 | icon: "http://www.mescroll.com/img/mescroll-empty.png?v=1", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png ) 29 | tip: '~ 暂无相关数据 ~' // 提示 30 | } 31 | } 32 | } 33 | 34 | export default GlobalOption 35 | -------------------------------------------------------------------------------- /src/components/mescroll-uni/mescroll-uni.css: -------------------------------------------------------------------------------- 1 | page { 2 | height: 100%; 3 | box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ 4 | } 5 | 6 | .mescroll-uni-warp{ 7 | height: 100%; 8 | } 9 | 10 | .mescroll-uni { 11 | position: relative; 12 | width: 100%; 13 | height: 100%; 14 | min-height: 200rpx; 15 | overflow-y: auto; 16 | box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ 17 | } 18 | 19 | /* 定位的方式固定高度 */ 20 | .mescroll-uni-fixed{ 21 | z-index: 1; 22 | position: fixed; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | bottom: 0; 27 | width: auto; /* 使right生效 */ 28 | height: auto; /* 使bottom生效 */ 29 | } 30 | -------------------------------------------------------------------------------- /src/components/painter/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 317 | 318 | -------------------------------------------------------------------------------- /src/components/painter/lib/downloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用 3 | * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3 4 | */ 5 | const util = require('./util'); 6 | 7 | const SAVED_FILES_KEY = 'savedFiles'; 8 | const KEY_TOTAL_SIZE = 'totalSize'; 9 | const KEY_PATH = 'path'; 10 | const KEY_TIME = 'time'; 11 | const KEY_SIZE = 'size'; 12 | 13 | // 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M 14 | let MAX_SPACE_IN_B = 6 * 1024 * 1024; 15 | let savedFiles = {}; 16 | 17 | export default class Dowloader { 18 | constructor() { 19 | // app 如果设置了最大存储空间,则使用 app 中的 20 | if (getApp().PAINTER_MAX_LRU_SPACE) { 21 | MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE; 22 | } 23 | uni.getStorage({ 24 | key: SAVED_FILES_KEY, 25 | success: function (res) { 26 | if (res.data) { 27 | savedFiles = res.data; 28 | } 29 | }, 30 | }); 31 | } 32 | 33 | /** 34 | * 下载文件,会用 lru 方式来缓存文件到本地 35 | * @param {String} url 文件的 url 36 | */ 37 | download(url) { 38 | return new Promise((resolve, reject) => { 39 | if (!(url && util.isValidUrl(url))) { 40 | resolve(url); 41 | return; 42 | } 43 | const file = getFile(url); 44 | 45 | if (file) { 46 | // 检查文件是否正常,不正常需要重新下载 47 | uni.getSavedFileInfo({ 48 | filePath: file[KEY_PATH], 49 | success: (res) => { 50 | resolve(file[KEY_PATH]); 51 | }, 52 | fail: (error) => { 53 | console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`); 54 | downloadFile(url).then((path) => { 55 | resolve(path); 56 | }, () => { 57 | reject(); 58 | }); 59 | }, 60 | }); 61 | } else { 62 | downloadFile(url).then((path) => { 63 | resolve(path); 64 | }, () => { 65 | reject(); 66 | }); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | function downloadFile(url) { 73 | return new Promise((resolve, reject) => { 74 | uni.downloadFile({ 75 | url: url, 76 | success: function (res) { 77 | if (res.statusCode !== 200 && !res.tempFilePath) { 78 | console.error(`downloadFile ${url} failed res.statusCode is not 200`); 79 | reject(); 80 | return; 81 | } 82 | const { tempFilePath } = res; 83 | uni.getFileInfo({ 84 | filePath: tempFilePath, 85 | apFilePath: tempFilePath, 86 | success: (tmpRes) => { 87 | const newFileSize = tmpRes.size; 88 | doLru(newFileSize).then(() => { 89 | saveFile(url, newFileSize, tempFilePath).then((filePath) => { 90 | resolve(filePath); 91 | }); 92 | }, () => { 93 | resolve(tempFilePath); 94 | }); 95 | }, 96 | fail: (error) => { 97 | // 文件大小信息获取失败,则此文件也不要进行存储 98 | console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`); 99 | resolve(res.tempFilePath); 100 | }, 101 | }); 102 | }, 103 | fail: function (error) { 104 | console.error(`downloadFile failed, ${JSON.stringify(error)} `); 105 | reject(); 106 | }, 107 | }); 108 | }); 109 | } 110 | 111 | function saveFile(key, newFileSize, tempFilePath) { 112 | return new Promise((resolve, reject) => { 113 | uni.saveFile({ 114 | tempFilePath: tempFilePath, 115 | success: (fileRes) => { 116 | const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0; 117 | savedFiles[key] = {}; 118 | savedFiles[key][KEY_PATH] = fileRes.savedFilePath; 119 | savedFiles[key][KEY_TIME] = new Date().getTime(); 120 | savedFiles[key][KEY_SIZE] = newFileSize; 121 | savedFiles['totalSize'] = newFileSize + totalSize; 122 | uni.setStorage({ 123 | key: SAVED_FILES_KEY, 124 | data: savedFiles, 125 | }); 126 | resolve(fileRes.savedFilePath); 127 | }, 128 | fail: (error) => { 129 | console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`); 130 | // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件 131 | resolve(tempFilePath); 132 | // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功 133 | reset(); 134 | }, 135 | }); 136 | }); 137 | } 138 | 139 | /** 140 | * 清空所有下载相关内容 141 | */ 142 | function reset() { 143 | uni.removeStorage({ 144 | key: SAVED_FILES_KEY, 145 | success: () => { 146 | uni.getSavedFileList({ 147 | success: (listRes) => { 148 | removeFiles(listRes.fileList); 149 | }, 150 | fail: (getError) => { 151 | console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`); 152 | }, 153 | }); 154 | }, 155 | }); 156 | } 157 | 158 | function doLru(size) { 159 | return new Promise((resolve, reject) => { 160 | let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0; 161 | 162 | if (size + totalSize <= MAX_SPACE_IN_B) { 163 | resolve(); 164 | return; 165 | } 166 | // 如果加上新文件后大小超过最大限制,则进行 lru 167 | const pathsShouldDelete = []; 168 | // 按照最后一次的访问时间,从小到大排序 169 | const allFiles = JSON.parse(JSON.stringify(savedFiles)); 170 | delete allFiles[KEY_TOTAL_SIZE]; 171 | const sortedKeys = Object.keys(allFiles).sort((a, b) => { 172 | return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME]; 173 | }); 174 | 175 | for (const sortedKey of sortedKeys) { 176 | totalSize -= savedFiles[sortedKey].size; 177 | pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]); 178 | delete savedFiles[sortedKey]; 179 | if (totalSize + size < MAX_SPACE_IN_B) { 180 | break; 181 | } 182 | } 183 | 184 | savedFiles['totalSize'] = totalSize; 185 | 186 | uni.setStorage({ 187 | key: SAVED_FILES_KEY, 188 | data: savedFiles, 189 | success: () => { 190 | // 保证 storage 中不会存在不存在的文件数据 191 | if (pathsShouldDelete.length > 0) { 192 | removeFiles(pathsShouldDelete); 193 | } 194 | resolve(); 195 | }, 196 | fail: (error) => { 197 | console.error(`doLru setStorage failed, ${JSON.stringify(error)}`); 198 | reject(); 199 | }, 200 | }); 201 | }); 202 | } 203 | 204 | function removeFiles(pathsShouldDelete) { 205 | for (const pathDel of pathsShouldDelete) { 206 | let delPath = pathDel; 207 | if (typeof pathDel === 'object') { 208 | delPath = pathDel.filePath; 209 | } 210 | uni.removeSavedFile({ 211 | filePath: delPath, 212 | fail: (error) => { 213 | console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`); 214 | }, 215 | }); 216 | } 217 | } 218 | 219 | function getFile(key) { 220 | if (!savedFiles[key]) { 221 | return; 222 | } 223 | savedFiles[key]['time'] = new Date().getTime(); 224 | uni.setStorage({ 225 | key: SAVED_FILES_KEY, 226 | data: savedFiles, 227 | }); 228 | return savedFiles[key]; 229 | } 230 | -------------------------------------------------------------------------------- /src/components/painter/lib/gradient.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 当ctx传入当前文件,const grd = ctx.createCircularGradient() 和 3 | // const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理 4 | // 先分析,在外部创建grd,再传入使用就可以 5 | 6 | !(function () { 7 | 8 | var api = { 9 | isGradient: function(bg) { 10 | if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) { 11 | return true; 12 | } 13 | return false; 14 | }, 15 | 16 | doGradient: function(bg, width, height, ctx) { 17 | if (bg.startsWith('linear')) { 18 | linearEffect(width, height, bg, ctx); 19 | } else if (bg.startsWith('radial')) { 20 | radialEffect(width, height, bg, ctx); 21 | } 22 | }, 23 | } 24 | 25 | function analizeGrad(string) { 26 | const colorPercents = string.substring(0, string.length - 1).split("%,"); 27 | const colors = []; 28 | const percents = []; 29 | for (let colorPercent of colorPercents) { 30 | colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(" ")).trim()); 31 | percents.push(colorPercent.substring(colorPercent.lastIndexOf(" "), colorPercent.length) / 100); 32 | } 33 | return {colors: colors, percents: percents}; 34 | } 35 | 36 | function radialEffect(width, height, bg, ctx) { 37 | const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]); 38 | const grd = ctx.createCircularGradient(0, 0, width < height ? height / 2 : width / 2); 39 | for (let i = 0; i < colorPer.colors.length; i++) { 40 | grd.addColorStop(colorPer.percents[i], colorPer.colors[i]); 41 | } 42 | ctx.setFillStyle(grd); 43 | ctx.fillRect(-(width / 2), -(height / 2), width, height); 44 | } 45 | 46 | function analizeLinear(bg, width, height) { 47 | const direction = bg.match(/([-]?\d{1,3})deg/); 48 | const dir = direction && direction[1] ? parseFloat(direction[1]) : 0; 49 | let coordinate; 50 | switch (dir) { 51 | case 0: coordinate = [0, -height / 2, 0, height / 2]; break; 52 | case 90: coordinate = [width / 2, 0, -width / 2, 0]; break; 53 | case -90: coordinate = [-width / 2, 0, width / 2, 0]; break; 54 | case 180: coordinate = [0, height / 2, 0, -height / 2]; break; 55 | case -180: coordinate = [0, -height / 2, 0, height / 2]; break; 56 | default: 57 | let x1 = 0; 58 | let y1 = 0; 59 | let x2 = 0; 60 | let y2 = 0; 61 | if (direction[1] > 0 && direction[1] < 90) { 62 | x1 = (width / 2) - ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; 63 | y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; 64 | x2 = -x1; 65 | y1 = -y2; 66 | } else if (direction[1] > -180 && direction[1] < -90) { 67 | x1 = -(width / 2) + ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; 68 | y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; 69 | x2 = -x1; 70 | y1 = -y2; 71 | } else if (direction[1] > 90 && direction[1] < 180) { 72 | x1 = (width / 2) + (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; 73 | y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; 74 | x2 = -x1; 75 | y1 = -y2; 76 | } else { 77 | x1 = -(width / 2) - (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; 78 | y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; 79 | x2 = -x1; 80 | y1 = -y2; 81 | } 82 | coordinate = [x1, y1, x2, y2]; 83 | break; 84 | } 85 | return coordinate; 86 | } 87 | 88 | function linearEffect(width, height, bg, ctx) { 89 | const param = analizeLinear(bg, width, height); 90 | const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]); 91 | const content = bg.match(/linear-gradient\((.+)\)/)[1]; 92 | const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1)); 93 | for (let i = 0; i < colorPer.colors.length; i++) { 94 | grd.addColorStop(colorPer.percents[i], colorPer.colors[i]); 95 | } 96 | ctx.setFillStyle(grd); 97 | ctx.fillRect(-(width / 2), -(height / 2), width, height); 98 | } 99 | 100 | module.exports = { api } 101 | 102 | })(); 103 | -------------------------------------------------------------------------------- /src/components/painter/lib/util.js: -------------------------------------------------------------------------------- 1 | 2 | function isValidUrl(url) { 3 | return /(ht|f)tp(s?):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url); 4 | } 5 | 6 | /** 7 | * 深度对比两个对象是否一致 8 | * from: https://github.com/epoberezkin/fast-deep-equal 9 | * @param {Object} a 对象a 10 | * @param {Object} b 对象b 11 | * @return {Boolean} 是否相同 12 | */ 13 | /* eslint-disable */ 14 | function equal(a, b) { 15 | if (a === b) return true; 16 | 17 | if (a && b && typeof a == 'object' && typeof b == 'object') { 18 | var arrA = Array.isArray(a) 19 | , arrB = Array.isArray(b) 20 | , i 21 | , length 22 | , key; 23 | 24 | if (arrA && arrB) { 25 | length = a.length; 26 | if (length != b.length) return false; 27 | for (i = length; i-- !== 0;) 28 | if (!equal(a[i], b[i])) return false; 29 | return true; 30 | } 31 | 32 | if (arrA != arrB) return false; 33 | 34 | var dateA = a instanceof Date 35 | , dateB = b instanceof Date; 36 | if (dateA != dateB) return false; 37 | if (dateA && dateB) return a.getTime() == b.getTime(); 38 | 39 | var regexpA = a instanceof RegExp 40 | , regexpB = b instanceof RegExp; 41 | if (regexpA != regexpB) return false; 42 | if (regexpA && regexpB) return a.toString() == b.toString(); 43 | 44 | var keys = Object.keys(a); 45 | length = keys.length; 46 | 47 | if (length !== Object.keys(b).length) 48 | return false; 49 | 50 | for (i = length; i-- !== 0;) 51 | if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; 52 | 53 | for (i = length; i-- !== 0;) { 54 | key = keys[i]; 55 | if (!equal(a[key], b[key])) return false; 56 | } 57 | 58 | return true; 59 | } 60 | 61 | return a!==a && b!==b; 62 | } 63 | 64 | module.exports = { 65 | isValidUrl, 66 | equal 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /src/components/painter/painter.js: -------------------------------------------------------------------------------- 1 | import Pen from './lib/pen'; 2 | import Downloader from './lib/downloader'; 3 | 4 | const util = require('./lib/util'); 5 | 6 | const downloader = new Downloader(); 7 | 8 | // 最大尝试的绘制次数 9 | const MAX_PAINT_COUNT = 5; 10 | Component({ 11 | canvasWidthInPx: 0, 12 | canvasHeightInPx: 0, 13 | paintCount: 0, 14 | /** 15 | * 组件的属性列表 16 | */ 17 | properties: { 18 | customStyle: { 19 | type: String, 20 | }, 21 | palette: { 22 | type: Object, 23 | observer: function (newVal, oldVal) { 24 | if (this.isNeedRefresh(newVal, oldVal)) { 25 | this.paintCount = 0; 26 | this.startPaint(); 27 | } 28 | }, 29 | }, 30 | // 启用脏检查,默认 false 31 | dirty: { 32 | type: Boolean, 33 | value: false, 34 | }, 35 | }, 36 | 37 | data: { 38 | picURL: '', 39 | showCanvas: true, 40 | painterStyle: '', 41 | }, 42 | 43 | attached() { 44 | setStringPrototype(); 45 | }, 46 | 47 | methods: { 48 | /** 49 | * 判断一个 object 是否为 空 50 | * @param {object} object 51 | */ 52 | isEmpty(object) { 53 | for (const i in object) { 54 | return false; 55 | } 56 | return true; 57 | }, 58 | 59 | isNeedRefresh(newVal, oldVal) { 60 | if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) { 61 | return false; 62 | } 63 | return true; 64 | }, 65 | 66 | startPaint() { 67 | if (this.isEmpty(this.properties.palette)) { 68 | return; 69 | } 70 | 71 | if (!(getApp().systemInfo && getApp().systemInfo.screenWidth)) { 72 | try { 73 | getApp().systemInfo = uni.getSystemInfoSync(); 74 | } catch (e) { 75 | const error = `Painter get system info failed, ${JSON.stringify(e)}`; 76 | that.triggerEvent('imgErr', { error: error }); 77 | console.error(error); 78 | return; 79 | } 80 | } 81 | screenK = getApp().systemInfo.screenWidth / 750; 82 | 83 | this.downloadImages().then((palette) => { 84 | const { width, height } = palette; 85 | this.canvasWidthInPx = width.toPx(); 86 | this.canvasHeightInPx = height.toPx(); 87 | if (!width || !height) { 88 | console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`); 89 | return; 90 | } 91 | this.setData({ 92 | painterStyle: `width:${width};height:${height};`, 93 | }); 94 | const ctx = uni.createCanvasContext('k-canvas', this); 95 | const pen = new Pen(ctx, palette); 96 | pen.paint(() => { 97 | this.saveImgToLocal(); 98 | }); 99 | }); 100 | }, 101 | 102 | downloadImages() { 103 | return new Promise((resolve, reject) => { 104 | let preCount = 0; 105 | let completeCount = 0; 106 | const paletteCopy = JSON.parse(JSON.stringify(this.properties.palette)); 107 | if (paletteCopy.background) { 108 | preCount++; 109 | downloader.download(paletteCopy.background).then((path) => { 110 | paletteCopy.background = path; 111 | completeCount++; 112 | if (preCount === completeCount) { 113 | resolve(paletteCopy); 114 | } 115 | }, () => { 116 | completeCount++; 117 | if (preCount === completeCount) { 118 | resolve(paletteCopy); 119 | } 120 | }); 121 | } 122 | if (paletteCopy.views) { 123 | for (const view of paletteCopy.views) { 124 | if (view && view.type === 'image' && view.url) { 125 | preCount++; 126 | /* eslint-disable no-loop-func */ 127 | downloader.download(view.url).then((path) => { 128 | view.url = path; 129 | uni.getImageInfo({ 130 | src: view.url, 131 | success: (res) => { 132 | // 获得一下图片信息,供后续裁减使用 133 | view.sWidth = res.width; 134 | view.sHeight = res.height; 135 | }, 136 | fail: (error) => { 137 | // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了 138 | view.url = ""; 139 | console.error(`getImageInfo ${view.url} failed, ${JSON.stringify(error)}`); 140 | }, 141 | complete: () => { 142 | completeCount++; 143 | if (preCount === completeCount) { 144 | resolve(paletteCopy); 145 | } 146 | }, 147 | }); 148 | }, () => { 149 | completeCount++; 150 | if (preCount === completeCount) { 151 | resolve(paletteCopy); 152 | } 153 | }); 154 | } 155 | } 156 | } 157 | if (preCount === 0) { 158 | resolve(paletteCopy); 159 | } 160 | }); 161 | }, 162 | 163 | saveImgToLocal() { 164 | const that = this; 165 | setTimeout(() => { 166 | uni.canvasToTempFilePath({ 167 | canvasId: 'k-canvas', 168 | success: function (res) { 169 | that.getImageInfo(res.tempFilePath); 170 | }, 171 | fail: function (error) { 172 | console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); 173 | that.triggerEvent('imgErr', { error: error }); 174 | }, 175 | }, this); 176 | }, 300); 177 | }, 178 | 179 | getImageInfo(filePath) { 180 | const that = this; 181 | uni.getImageInfo({ 182 | src: filePath, 183 | success: (infoRes) => { 184 | if (that.paintCount > MAX_PAINT_COUNT) { 185 | const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`; 186 | console.error(error); 187 | that.triggerEvent('imgErr', { error: error }); 188 | return; 189 | } 190 | // 比例相符时才证明绘制成功,否则进行强制重绘制 191 | if (Math.abs((infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) / (infoRes.height * that.canvasHeightInPx)) < 0.01) { 192 | that.triggerEvent('imgOK', { path: filePath }); 193 | } else { 194 | that.startPaint(); 195 | } 196 | that.paintCount++; 197 | }, 198 | fail: (error) => { 199 | console.error(`getImageInfo failed, ${JSON.stringify(error)}`); 200 | that.triggerEvent('imgErr', { error: error }); 201 | }, 202 | }); 203 | }, 204 | }, 205 | }); 206 | 207 | let screenK = 0.5; 208 | 209 | function setStringPrototype() { 210 | /* eslint-disable no-extend-native */ 211 | /** 212 | * 是否支持负数 213 | * @param {Boolean} minus 是否支持负数 214 | */ 215 | String.prototype.toPx = function toPx(minus) { 216 | let reg; 217 | if (minus) { 218 | reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g; 219 | } else { 220 | reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g; 221 | } 222 | const results = reg.exec(this); 223 | if (!this || !results) { 224 | console.error(`The size: ${this} is illegal`); 225 | return 0; 226 | } 227 | const unit = results[2]; 228 | const value = parseFloat(this); 229 | 230 | let res = 0; 231 | if (unit === 'rpx') { 232 | res = Math.round(value * screenK); 233 | } else if (unit === 'px') { 234 | res = value; 235 | } 236 | return res; 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /src/components/painter/painter.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /src/components/painter/painter.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/popup/marker.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 139 | 140 | -------------------------------------------------------------------------------- /src/components/uni-popup/uni-popup.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 85 | -------------------------------------------------------------------------------- /src/components/uni-transition/uni-transition.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 217 | 218 | 280 | -------------------------------------------------------------------------------- /src/components/wuc-tab/wuc-tab.vue: -------------------------------------------------------------------------------- 1 | 18 | 76 | 134 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import api from '@/common/vmeitime-http/' 4 | import store from '@/store' 5 | Vue.config.productionTip = false 6 | Vue.prototype.$api = api 7 | Vue.prototype.$store = store; 8 | 9 | 10 | App.mpType = 'app' 11 | 12 | const app = new Vue({ 13 | ...App, 14 | store 15 | }) 16 | app.$mount() -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | "app-plus": { 9 | "usingComponents": true, 10 | "splashscreen": { 11 | "alwaysShowBeforeRender": true, 12 | "waiting": true, 13 | "autoclose": true, 14 | "delay": 0 15 | }, 16 | "modules": {}, 17 | "distribute": { 18 | "android": { 19 | "permissions": [ 20 | "", 21 | "", 22 | "", 23 | "", 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "" 42 | ] 43 | }, 44 | "ios": {}, 45 | "sdkConfigs": {} 46 | } 47 | }, 48 | "quickapp": { /* 快应用特有相关 */}, 49 | "mp-weixin": { 50 | "appid": "wxc888b51893c07e7c", 51 | "setting": { 52 | "urlCheck": false 53 | }, 54 | "permission": { 55 | "scope.userLocation": { 56 | "desc": "你的位置信息将用于小程序位置接口的效果展示" 57 | } 58 | }, 59 | "cloudfunctionRoot": "./cloudfunctions/", 60 | "usingComponents": true 61 | }, 62 | "mp-alipay": { 63 | "usingComponents": true 64 | }, 65 | "mp-baidu": { 66 | "usingComponents": true 67 | }, 68 | "mp-toutiao": { 69 | "usingComponents": true 70 | }, 71 | "mp-qq": { 72 | "permission": { 73 | "scope.userLocation": { 74 | "desc": "你的位置信息将用于小程序位置接口的效果展示" 75 | } 76 | }, 77 | "usingComponents": true 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "path": "pages/index/index", 5 | "style": { 6 | "navigationBarTitleText": "我周边的疫情" 7 | } 8 | }, 9 | { 10 | "path": "pages/me/me", 11 | "style": { 12 | "navigationBarTitleText": "我的" 13 | } 14 | }, 15 | { 16 | "path": "pages/me/feedback", 17 | "style": { 18 | "navigationBarTitleText": "反馈" 19 | } 20 | }, 21 | { 22 | "path": "pages/me/feedSuccess", 23 | "style": { 24 | "navigationBarTitleText": "反馈" 25 | } 26 | }, 27 | { 28 | "path": "pages/search/search", 29 | "style": { 30 | "navigationBarTitleText": "搜索全国疫情" 31 | } 32 | }, 33 | { 34 | "path": "pages/me/about", 35 | "style": { 36 | "navigationBarTitleText": "查询" 37 | } 38 | }, 39 | { 40 | "path": "pages/webview", 41 | "style": { 42 | "navigationBarTitleText": "" 43 | } 44 | }, 45 | { 46 | "path": "pages/index/map", 47 | "style": { 48 | "navigationBarTitleText": "疫情地图" 49 | } 50 | }, 51 | { 52 | "path": "pages/search/test", 53 | "style": { 54 | "navigationBarTitleText": "test" 55 | } 56 | }, 57 | { 58 | "path": "pages/me/support", 59 | "style": { 60 | "navigationBarTitleText": "赞助支持" 61 | } 62 | }, 63 | { 64 | "path": "pages/trends/index", 65 | "style": { 66 | "navigationBarTitleText": "全国疫情趋势" 67 | } 68 | }, 69 | { 70 | "path": "pages/guide/index", 71 | "style": { 72 | "navigationBarTitleText": "疫情应对指南" 73 | } 74 | }, 75 | { 76 | "path": "pages/article/index", 77 | "style": { 78 | "navigationBarTitleText": "新闻" 79 | } 80 | }, 81 | { 82 | "path": "pages/article/detail", 83 | "style": { 84 | "navigationBarTitleText": "新闻详情" 85 | } 86 | } 87 | ], 88 | "tabBar": { 89 | "color": "#888888", 90 | "selectedColor": "#4E8CEE", 91 | "borderStyle": "white", 92 | "backgroundColor": "#ffffff", 93 | "list": [ 94 | { 95 | "pagePath": "pages/index/index", 96 | "iconPath": "static/near.png", 97 | "selectedIconPath": "static/nearActive.png", 98 | "text": "附近" 99 | }, 100 | { 101 | "pagePath": "pages/search/search", 102 | "iconPath": "static/index.png", 103 | "selectedIconPath": "static/indexActive.png", 104 | "text": "查询" 105 | }, 106 | { 107 | "pagePath": "pages/guide/index", 108 | "iconPath": "static/article.png", 109 | "selectedIconPath": "static/articleActive.png", 110 | "text": "指南" 111 | }, 112 | { 113 | "pagePath": "pages/trends/index", 114 | "iconPath": "static/trends.png", 115 | "selectedIconPath": "static/trendsActive.png", 116 | "text": "趋势" 117 | }, 118 | { 119 | "pagePath": "pages/me/me", 120 | "iconPath": "static/my.png", 121 | "selectedIconPath": "static/myActive.png", 122 | "text": "帮助" 123 | } 124 | // { 125 | // "pagePath": "pages/article/index", 126 | // "iconPath": "static/my.png", 127 | // "selectedIconPath": "static/myActive.png", 128 | // "text": "新闻" 129 | // } 130 | ] 131 | } 132 | /* eslint-disable no-alert, no-console */ 133 | // #ifdef MP-ALIPAY 134 | ,"globalStyle": { 135 | "navigationBarTextStyle": "white", 136 | "navigationBarTitleText": "疫况", 137 | "navigationBarBackgroundColor": "#4E8CEE", 138 | "backgroundColor": "#ffffff", 139 | "navigationStyle": "default" 140 | } 141 | // #endif 142 | // #ifndef MP-ALIPAY 143 | ,"globalStyle": { 144 | "navigationBarTextStyle": "black", 145 | "navigationBarTitleText": "疫况", 146 | "navigationBarBackgroundColor": "#F8F8F8", 147 | "backgroundColor": "#F8F8F8", 148 | "navigationStyle": "default" 149 | } 150 | // #endif 151 | /* eslint-disable no-alert, no-console */ 152 | } 153 | /* eslint-disable */ -------------------------------------------------------------------------------- /src/pages/article/detail.vue: -------------------------------------------------------------------------------- 1 | 7 | 53 | 54 | 56 | -------------------------------------------------------------------------------- /src/pages/article/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 73 | 74 | 146 | 147 | -------------------------------------------------------------------------------- /src/pages/guide/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 140 | 141 | -------------------------------------------------------------------------------- /src/pages/index/map.vue: -------------------------------------------------------------------------------- 1 | 2 | 41 | 311 | 320 | 387 | -------------------------------------------------------------------------------- /src/pages/me/about.vue: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 73 | 74 | 126 | -------------------------------------------------------------------------------- /src/pages/me/feedSuccess.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 52 | 53 | 99 | -------------------------------------------------------------------------------- /src/pages/me/feedback.vue: -------------------------------------------------------------------------------- 1 | 2 |