├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── config ├── dev.js ├── index.js └── prod.js ├── global.d.ts ├── package.json ├── project.config.json ├── src ├── app.less ├── app.tsx ├── assets │ └── images │ │ ├── bg.png │ │ ├── canvasBg.png │ │ ├── close.png │ │ ├── code.png │ │ ├── dragon.png │ │ ├── feedback.png │ │ ├── icon.close.png │ │ ├── icon_back_dark.png │ │ ├── icon_back_light.png │ │ ├── icon_menu_dark.png │ │ ├── icon_menu_light.png │ │ ├── icon_music_gif.png │ │ ├── icon_only_gif.png │ │ ├── icon_sound.png │ │ ├── icon_sound_off.png │ │ ├── pic_loading.png │ │ ├── powerbyversa.png │ │ ├── question.png │ │ ├── scale.png │ │ └── versa.png ├── components │ ├── AuthModal │ │ ├── index.less │ │ └── index.tsx │ ├── CategoryItem │ │ ├── index.less │ │ └── index.tsx │ ├── Icon │ │ ├── index.less │ │ └── index.tsx │ ├── ResultModal │ │ ├── index.less │ │ └── index.tsx │ ├── SceneList │ │ ├── SceneItem │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── Sticker │ │ ├── index.less │ │ └── index.tsx │ └── Title │ │ ├── index.less │ │ └── index.tsx ├── index.html ├── model │ ├── actions │ │ ├── counter.ts │ │ └── global.ts │ ├── constants │ │ ├── counter.ts │ │ └── global.ts │ ├── reducers │ │ ├── counter.ts │ │ ├── global.ts │ │ └── index.ts │ └── store │ │ └── index.ts ├── pages │ ├── dynamic │ │ ├── index.less │ │ ├── index.tsx │ │ ├── mock_theme_data.json │ │ └── mock_theme_dynamic_data.json │ ├── home │ │ ├── index.less │ │ ├── index.tsx │ │ └── mock.json │ └── index │ │ ├── index.less │ │ └── index.tsx ├── services │ ├── api.config.ts │ ├── cache.ts │ ├── config.ts │ ├── global_data.ts │ ├── http.ts │ ├── service.ts │ └── session.ts └── utils │ └── tool.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["taro"], 3 | "rules": { 4 | "no-unused-vars": ["error", { "varsIgnorePattern": "Taro" }], 5 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }] 6 | }, 7 | "parser": "babel-eslint", 8 | "plugins": ["typescript"] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .temp/ 3 | .rn_temp/ 4 | node_modules/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://r.cnpmjs.org 2 | disturl=https://r.cnpmjs.org/node 3 | sass_binary_site=https://r.cnpmjs.org/node-sass/ 4 | fse_binary_host_mirror=https://r.cnpmjs.org/fsevents 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 手把手教你用Taro框架写一个图像处理类微信小程序 3 | ### 前言 4 | 5 | 18年年中的时候,笔者所在的公司让我们开发一款微信小程序(马卡龙玩图)。主要的玩法是用户上传一张人像照片,图片经过后端的AI算法处理后识别出人物,将人物和周围环境进行分割(俗称抠图);前端将返回的抠像进行样式处理,包括设置大小位置旋转等;通过预设(或自定义上传)的一些主题场景以及点缀的贴纸或滤镜,用户对这些元素进行移动或缩放,可以衍生出很多好玩的修图玩法,比如更换动态背景,合成带有音频的动态视频等(文末有微信二维码)。 6 | 7 | ![演示](https://user-gold-cdn.xitu.io/2019/1/15/16850f0e2e212c2b?w=541&h=996&f=gif&s=3480524) 8 | 9 | 开发初期,当时可选的成熟的微信小程序框架只有[wepy](https://tencent.github.io/wepy/),经过开发实践发现,wepy在多层嵌套列表渲染,组件化支持等方面体验不是很友好。后面美团的技术团队开源了一款基于vue的小程序框架[mpvue](http://mpvue.com/),经过体验后感觉上,虽然在组件化上体验和vue别无差异,但是在性能上并不占优势。 10 | 11 | 直到某天有位朋友拉我进了一个Taro的开发群,原来京东的前端团队也在开发一款基于React规范的小程序框架,由于当时笔者担心Taro尚处早期,功能上也许不足抑或bug,迟迟没有入手。直到最近更新到1.2.4的版本,群里有道友不吝溢美之词进行了一波安利,所以笔者决定对项目的部分模块进行了重构,发现Taro确实在开发体验和性能上都得到了非常好的提高,在此向taro的贡献者致以崇高的敬意。本着开源的精神,笔者也将此次重构的demo源码以及心得体会和大家一起分享。 12 | 13 | ### 需求分析 14 | ![需求分析](https://user-gold-cdn.xitu.io/2019/1/15/1685057df9113e43) 15 | 用户上传的人像经过抠图处理后,将展示在作图区,同时展示的元素还有背景图片,可动或固定的贴纸。为了获取更好的用户视觉体验,每个场景下,通过预设人像和贴纸的大小和位置(参数为作图区域的百分比等)。人像和贴纸需支持单指和双指手势操作来改变大小和位置等样式,因此可以将人像和贴纸都封装为Sticker的组件,子组件Sticker向页面父组件传递手势操作变更后的样式参数,触发父组件setState来刷新,最终通过传递props到子组件来控制样式。 16 | 17 | 关于Sticker组件的一些细节还包括:贴纸组件具有激活状态(点击当前组件显示控制器,而其他组件则隐藏);切换场景后,要缓存之前用户的操作,当切回到原先的场景时,则恢复到该场景下用户最后的操作状态。 18 | 19 | 用户点击保存后,将作图区的所有元素按照层级大小进行排序,然后通过微信提供的canvas接口进行绘制,最终返回所见即所得的合成美图。 20 | 21 | ### 准备工作 22 | 根据Taro的[文档](https://nervjs.github.io/taro/docs/GETTING-STARTED.html),安装CLI工具以及创建项目模板,建议选择Typescript开发方式。 23 | 24 | 25 | ### 项目目录 26 | 简要分析下项目结构 27 | ``` 28 | Taro-makaron-demo 29 | ├── dist 编译结果目录 30 | ├── config 配置目录 31 | | ├── dev.js 开发时配置 32 | | ├── index.js 默认配置 33 | | └── prod.js 打包时配置 34 | ├── src 源码目录 35 | | ├── assets 静态资源 36 | | | ├── images 图片 37 | | ├── components 组件 38 | | | ├── Sticker 贴纸组件 39 | | | ├── ... 其他组件 40 | | ├── model Redux数据流 41 | | | ├── actions 42 | | | ├── constants 43 | | | ├── reducers 44 | | | ├── store 45 | | ├── pages 页面文件目录 46 | | | ├── home 首页 47 | | | | ├── index.js index 页面逻辑 48 | | | | └── index.css index 页面样式 49 | | | ├── dynamic 作图页 50 | | | | ├── index.js index 页面逻辑 51 | | | | └── index.css index 页面样式 52 | | ├── services 服务 53 | | | ├── config.ts 全局配置 54 | | | ├── api.config.ts api接口配置 55 | | | ├── http.ts 封装的http服务 56 | | | ├── global_data.ts 全局对象 57 | | | ├── cache.ts 缓存服务 58 | | | ├── session.ts 会话服务 59 | | | ├── service.ts 基础服务或业务服务 60 | | ├── utils 公共方法 61 | | | ├── tool.ts 工具函数 62 | | ├── app.css 项目总通用样式 63 | | └── app.js 项目入口文件 64 | └── package.json 65 | ``` 66 | ### 核心代码分析 67 | 68 | - sticker贴纸组件 69 | 70 | 贴纸组件相较其他展示型组件,涉及手势操作,大小位置计算等,所以稍显复杂。 71 | ``` 72 | // 使用 73 | class Page extends Component { 74 | state = { 75 | foreground: { // 人像state 76 | id: 'foreground', // id 77 | remoteUrl: '', // url 78 | zIndex:2, // 层级 79 | width:0, // 宽 80 | height:0, // 高 81 | x: 0, // x轴偏移量 82 | y:0, // y轴偏移量 83 | rotate: 0, // 旋转角度 84 | originWidth: 0, // 原始宽度 85 | originHeight: 0, // 原始高度 86 | autoWidth: 0, // 自适应后的宽度 87 | autoHeight: 0, // 自适应后的高度 88 | autoScale: 0, // 相对画框缩放比例 89 | fixed: false, // 是否固定 90 | isActive: true, // 是否激活 91 | visible: true, // 是否显示 92 | } 93 | } 94 | render () { 95 | reuturn 105 | } 106 | } 107 | 108 | // 组件定义 109 | class Sticker extends Component { 110 | ... 111 | render() { 112 | const { url, stylePrams } = this.props 113 | const { framePrams } = this.state 114 | const styleObj = this.formatStyle(this.props.stylePrams) 115 | return ( 116 | 0) ? '' : 'hidden' }`} 118 | style={styleObj} 119 | > 120 | 128 | 129 | 134 | 135 | 136 | 137 | ) 138 | } 139 | } 140 | 141 | ``` 142 | - 缓存服务 143 | 缓存服务对提高性能非常有帮助,比如canvas绘图需要图片是本地图片,可以通过数据字典的方式将图片的远程地址和下载到本地的地址进行一一对应,节省了大量的网络资源和时间 144 | ``` 145 | // services/cache.ts 缓存服务 146 | function Cache (name) { 147 | this.name = name 148 | } 149 | Cache.prototype = { 150 | set: function (key, value) { 151 | this[key] = value 152 | return this[key] 153 | }, 154 | get: function (key) { 155 | return this[key] 156 | }, 157 | clear: function () { 158 | // 清空 159 | Object.keys(this).forEach(v => { 160 | this[v] = null 161 | }) 162 | } 163 | } 164 | 165 | export const createCache = (name:string) => { 166 | return new Cache(name) 167 | } 168 | // 使用 169 | import {createCache} from '../../services/cache' 170 | class Page extends Component { 171 | cache = { 172 | source: createCache('source'), 173 | } 174 | // 下载照片并存储到本地 175 | downloadRemoteImage = async (remoteUrl = '') => { 176 | const cacheKey = `${remoteUrl}_localPath` 177 | const cache_source = this.cache['source'] 178 | let localImagePath = '' 179 | if (cache_source.get(cacheKey)) { 180 | // 有缓存 181 | return cache_source.get(cacheKey) 182 | } else { 183 | try { 184 | const result = await service.base.downloadFile(remoteUrl) 185 | localImagePath = result.tempFilePath 186 | } catch (err) { 187 | console.log('下载图片失败', err) 188 | } 189 | } 190 | return cache_source.set(cacheKey, localImagePath) 191 | } 192 | } 193 | 194 | ``` 195 | ### 性能优化 196 | 1. 避免频繁setState 197 | 198 | 由于微信小程序逻辑层和视图层各自独立,两边的数据传输是靠转换后的字符串。因此当setData频率过快,内容庞大时,会导致阻塞。由于本项目又涉及很多的手势操作,touchmove事件的频率很快,所以项目早期时候,在安卓系统下卡顿十分明显。 199 | 200 | 优化方式有:通过做函数节流,降低setData频次;将页面无关的数据不要绑定到data上,而是绑定到组件实例上(牺牲运算效率换取空间效率)。 201 | 202 | 使用微信的自定义组件,也是一个很大的提升因素,个人认猜测可能是自定义组件内部data的改变不会导致其他组件或页面的data更新。项目早期采用的是wepy框架,由于历史局限性(当时微信还未公布自定义组件方案),所以效率问题一直很是头疼。好在Taro框架通过编译的方式完美的支持了这个方案。 203 | 204 | 2. 归并setState 205 | 206 | 例如,当图片加载,获取到原始尺寸后,需要计算出该图片在当前场景下的预设尺寸和位置。必须先计算出自适应后的宽高,然后才能计算出预设的偏移量。因此可以将尺寸和位置参数都计算完毕后,再调用setState更新视图,这样不仅降低了频次,同时也解决了图片闪烁的bug. 207 | 208 | 3. 利用缓存 209 | 210 | 前面有提到过利用缓存模块来存储组件状态或资源信息,在此不再赘述。 211 | 212 | ### 心得 213 | Taro框架采取的是一种编译的方式,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动小程序、H5、React-Native 等),因此可以在性能上与各个平台保持一致。 214 | 215 | 而mpvue的方案则是修改vue的runtime,将vue 实例与小程序 Page 实例建立关联以及生命周期的绑定。私以为,这种通过映射的方式可能会导致通信效率上的降低,并且vue和微信又各自独立迭代,后期的协调也越来越费劲,所以个人感觉上,还是Taro的方案略胜一筹。个人薄见,还请海涵。 216 | 217 | ### 写在最后 218 | - Github 219 | 220 | 欢迎大家来这个demo项目下进行交流,[项目地址](https://github.com/HarryChen0506/taro-makaron-demo) (https://github.com/HarryChen0506/taro-makaron-demo), 你的点赞将是我莫大的动力😊 221 | - 线上项目 222 | 223 | 本demo项目的线上小程序可通过微信扫描下面的二维码前往体验👏 ![马卡龙玩图](https://user-gold-cdn.xitu.io/2019/1/15/16850cb1b039bba0?w=135&h=135&f=png&s=22173) 224 | -------------------------------------------------------------------------------- /config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"' 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'taro-makaron', 3 | date: '2018-12-28', 4 | designWidth: 750, 5 | deviceRatio: { 6 | '640': 2.34 / 2, 7 | '750': 1, 8 | '828': 1.81 / 2 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: 'dist', 12 | plugins: { 13 | babel: { 14 | sourceMap: true, 15 | presets: [ 16 | 'env' 17 | ], 18 | plugins: [ 19 | 'transform-decorators-legacy', 20 | 'transform-class-properties', 21 | 'transform-object-rest-spread' 22 | ] 23 | } 24 | }, 25 | defineConstants: { 26 | }, 27 | copy: { 28 | patterns: [ 29 | ], 30 | options: { 31 | } 32 | }, 33 | weapp: { 34 | module: { 35 | postcss: { 36 | autoprefixer: { 37 | enable: true, 38 | config: { 39 | browsers: [ 40 | 'last 3 versions', 41 | 'Android >= 4.1', 42 | 'ios >= 8' 43 | ] 44 | } 45 | }, 46 | pxtransform: { 47 | enable: true, 48 | config: { 49 | 50 | } 51 | }, 52 | url: { 53 | enable: true, 54 | config: { 55 | limit: 10240 // 设定转换尺寸上限 56 | } 57 | }, 58 | cssModules: { 59 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 60 | config: { 61 | namingPattern: 'module', // 转换模式,取值为 global/module 62 | generateScopedName: '[name]__[local]___[hash:base64:5]' 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | h5: { 69 | publicPath: '/', 70 | staticDirectory: 'static', 71 | module: { 72 | postcss: { 73 | autoprefixer: { 74 | enable: true, 75 | config: { 76 | browsers: [ 77 | 'last 3 versions', 78 | 'Android >= 4.1', 79 | 'ios >= 8' 80 | ] 81 | } 82 | }, 83 | cssModules: { 84 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 85 | config: { 86 | namingPattern: 'module', // 转换模式,取值为 global/module 87 | generateScopedName: '[name]__[local]___[hash:base64:5]' 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | module.exports = function (merge) { 96 | if (process.env.NODE_ENV === 'development') { 97 | return merge({}, config, require('./dev')) 98 | } 99 | return merge({}, config, require('./prod')) 100 | } 101 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.gif"; 3 | declare module "*.jpg"; 4 | declare module "*.jpeg"; 5 | declare module "*.svg"; 6 | declare module "*.css"; 7 | declare module "*.less"; 8 | declare module "*.scss"; 9 | declare module "*.sass"; 10 | declare module "*.styl"; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taro-makaron-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "马卡龙玩图-demo", 6 | "scripts": { 7 | "build:weapp": "taro build --type weapp", 8 | "build:swan": "taro build --type swan", 9 | "build:alipay": "taro build --type alipay", 10 | "build:tt": "taro build --type tt", 11 | "build:h5": "taro build --type h5", 12 | "build:rn": "taro build --type rn", 13 | "dev": "npm run build:weapp -- --watch", 14 | "dev:weapp": "npm run build:weapp -- --watch", 15 | "dev:swan": "npm run build:swan -- --watch", 16 | "dev:alipay": "npm run build:alipay -- --watch", 17 | "dev:tt": "npm run build:tt -- --watch", 18 | "dev:h5": "npm run build:h5 -- --watch", 19 | "dev:rn": "npm run build:rn -- --watch" 20 | }, 21 | "author": "", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@tarojs/async-await": "^1.2.2", 25 | "@tarojs/components": "^1.2.2", 26 | "@tarojs/redux": "^1.2.2", 27 | "@tarojs/redux-h5": "^1.2.2", 28 | "@tarojs/router": "^1.2.2", 29 | "@tarojs/taro": "^1.2.2", 30 | "@tarojs/taro-alipay": "^1.2.2", 31 | "@tarojs/taro-h5": "^1.2.2", 32 | "@tarojs/taro-swan": "^1.2.2", 33 | "@tarojs/taro-tt": "^1.2.2", 34 | "@tarojs/taro-weapp": "^1.2.2", 35 | "nerv-devtools": "^1.3.9", 36 | "nervjs": "^1.3.9", 37 | "path-to-regexp": "^2.4.0", 38 | "qs": "^6.6.0", 39 | "redux": "^4.0.0", 40 | "redux-logger": "^3.0.6", 41 | "redux-thunk": "^2.3.0" 42 | }, 43 | "devDependencies": { 44 | "@types/react": "^16.4.8", 45 | "@types/webpack-env": "^1.13.6", 46 | "@tarojs/plugin-babel": "^1.2.2", 47 | "@tarojs/plugin-csso": "^1.2.2", 48 | "@tarojs/plugin-less": "^1.2.2", 49 | "@tarojs/plugin-uglifyjs": "^1.2.2", 50 | "@tarojs/webpack-runner": "^1.2.2", 51 | "babel-plugin-transform-class-properties": "^6.24.1", 52 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 53 | "babel-plugin-transform-jsx-stylesheet": "^0.6.5", 54 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 55 | "babel-preset-env": "^1.6.1", 56 | "babel-eslint": "^8.2.3", 57 | "eslint": "^4.19.1", 58 | "eslint-config-taro": "^1.2.2", 59 | "eslint-plugin-react": "^7.8.2", 60 | "eslint-plugin-import": "^2.12.0", 61 | "eslint-plugin-taro": "^1.2.2", 62 | "eslint-plugin-typescript": "^0.12.0", 63 | "typescript": "^3.0.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "./dist", 3 | "projectname": "taro-makaron-demo", 4 | "description": "马卡龙玩图", 5 | "appid": "wxcfe56965f4d986f0", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false 11 | }, 12 | "compileType": "miniprogram" 13 | } 14 | -------------------------------------------------------------------------------- /src/app.less: -------------------------------------------------------------------------------- 1 | page { 2 | width:100%; 3 | height:100%; 4 | overflow-y:hidden; 5 | } 6 | button::after{ 7 | border: none; 8 | } 9 | 10 | .custom-button { 11 | border-radius:80rpx; 12 | width:100%; 13 | height:80rpx; 14 | line-height:80rpx; 15 | color:white; 16 | font-size:36rpx; 17 | text-align:center; 18 | transition:all 0.2s; 19 | &.btn-hover { 20 | transform: scale(0.85) 21 | } 22 | &.pink { 23 | background:#FF3366; 24 | } 25 | &.dark { 26 | background:#333; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import '@tarojs/async-await' 2 | import Taro, { Component, Config } from '@tarojs/taro' 3 | import { Provider } from '@tarojs/redux' 4 | import Index from './pages/index' 5 | 6 | import configStore from './model/store' 7 | 8 | import './app.less' 9 | 10 | // 如果需要在 h5 环境中开启 React Devtools 11 | // 取消以下注释: 12 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') { 13 | // require('nerv-devtools') 14 | // } 15 | 16 | const store = configStore() 17 | class App extends Component { 18 | 19 | /** 20 | * 指定config的类型声明为: Taro.Config 21 | * 22 | * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型 23 | * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string 24 | * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型 25 | */ 26 | config: Config = { 27 | pages: [ 28 | 'pages/home/index', 29 | 'pages/dynamic/index', 30 | 'pages/index/index' 31 | ], 32 | window: { 33 | backgroundTextStyle: 'light', 34 | navigationBarBackgroundColor: '#fff', 35 | navigationBarTitleText: 'WeChat', 36 | navigationBarTextStyle: 'black', 37 | navigationStyle: 'custom' 38 | } 39 | } 40 | 41 | componentDidMount () { 42 | } 43 | 44 | componentDidShow () {} 45 | 46 | componentDidHide () {} 47 | 48 | componentCatchError () {} 49 | 50 | componentDidCatchError () {} 51 | 52 | // 在 App 类中的 render() 函数没有实际作用 53 | // 请勿修改此函数 54 | render () { 55 | return ( 56 | 57 | 58 | 59 | ) 60 | } 61 | } 62 | 63 | Taro.render(, document.getElementById('app')) 64 | -------------------------------------------------------------------------------- /src/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/bg.png -------------------------------------------------------------------------------- /src/assets/images/canvasBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/canvasBg.png -------------------------------------------------------------------------------- /src/assets/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/close.png -------------------------------------------------------------------------------- /src/assets/images/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/code.png -------------------------------------------------------------------------------- /src/assets/images/dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/dragon.png -------------------------------------------------------------------------------- /src/assets/images/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/feedback.png -------------------------------------------------------------------------------- /src/assets/images/icon.close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon.close.png -------------------------------------------------------------------------------- /src/assets/images/icon_back_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_back_dark.png -------------------------------------------------------------------------------- /src/assets/images/icon_back_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_back_light.png -------------------------------------------------------------------------------- /src/assets/images/icon_menu_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_menu_dark.png -------------------------------------------------------------------------------- /src/assets/images/icon_menu_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_menu_light.png -------------------------------------------------------------------------------- /src/assets/images/icon_music_gif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_music_gif.png -------------------------------------------------------------------------------- /src/assets/images/icon_only_gif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_only_gif.png -------------------------------------------------------------------------------- /src/assets/images/icon_sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_sound.png -------------------------------------------------------------------------------- /src/assets/images/icon_sound_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/icon_sound_off.png -------------------------------------------------------------------------------- /src/assets/images/pic_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/pic_loading.png -------------------------------------------------------------------------------- /src/assets/images/powerbyversa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/powerbyversa.png -------------------------------------------------------------------------------- /src/assets/images/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/question.png -------------------------------------------------------------------------------- /src/assets/images/scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/scale.png -------------------------------------------------------------------------------- /src/assets/images/versa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/assets/images/versa.png -------------------------------------------------------------------------------- /src/components/AuthModal/index.less: -------------------------------------------------------------------------------- 1 | .auth-wrap { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | .modal { 11 | position: absolute; 12 | left: 0; 13 | top: 0; 14 | width: 100%; 15 | height: 100%; 16 | background: #000; 17 | opacity: 0.5; 18 | } 19 | button { 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/AuthModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Button } from '@tarojs/components' 4 | 5 | import './index.less' 6 | 7 | type ComponentStateProps = {} 8 | 9 | type ComponentOwnProps = { 10 | onClick: () => void 11 | } 12 | 13 | type ComponentState = {} 14 | 15 | type IProps = ComponentStateProps & ComponentOwnProps 16 | 17 | interface AuthModal { 18 | props: IProps; 19 | } 20 | 21 | class AuthModal extends Component { 22 | handleClick = () => { 23 | this.props.onClick && this.props.onClick() 24 | } 25 | render() { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | export default AuthModal as ComponentClass -------------------------------------------------------------------------------- /src/components/CategoryItem/index.less: -------------------------------------------------------------------------------- 1 | .category-box { 2 | position:relative; 3 | width:290rpx; 4 | height:296rpx; 5 | margin-top:20px; 6 | .category-box-button { 7 | width:100%; 8 | height:100%; 9 | padding:0; 10 | margin:0; 11 | background:transparent; 12 | transition:all 0.2s; 13 | .category-box-image { 14 | width:290rpx; 15 | height:296rpx; 16 | } 17 | &.btn-hover { 18 | transform: scale(0.85) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/CategoryItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Button, Image } from '@tarojs/components' 4 | 5 | import './index.less' 6 | import loading from '../../assets/images/pic_loading.png' 7 | 8 | type ComponentStateProps = {} 9 | 10 | type ComponentOwnProps = { 11 | onGetUserInfo: (data:any) => void, 12 | onClick?: () => void, 13 | url: string 14 | } 15 | 16 | type ComponentState = {} 17 | 18 | type IProps = ComponentStateProps & ComponentOwnProps 19 | 20 | interface CategotyItem { 21 | props: IProps; 22 | } 23 | 24 | class CategotyItem extends Component { 25 | static defaultProps = { 26 | url: loading 27 | } 28 | componentWillReceiveProps (nextProps) { 29 | // console.log(this.props, nextProps) 30 | } 31 | handleGgetUserInfo = (data) => { 32 | const { onGetUserInfo } = this.props 33 | onGetUserInfo(data) 34 | } 35 | render() { 36 | const { onClick, url } = this.props 37 | return ( 38 | 39 | 50 | 51 | ) 52 | } 53 | } 54 | 55 | export default CategotyItem as ComponentClass -------------------------------------------------------------------------------- /src/components/Icon/index.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | width:70rpx; 3 | height:50rpx; 4 | display:flex; 5 | align-items:center; 6 | justify-content:center; 7 | .icon-menu { 8 | width:40rpx; 9 | height:28rpx; 10 | } 11 | .icon-back { 12 | width:22rpx; 13 | height:38rpx; 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View } from '@tarojs/components' 4 | 5 | import menu_light from '../../assets/images/icon_menu_light.png' 6 | import menu_dark from '../../assets/images/icon_menu_dark.png' 7 | import back_light from '../../assets/images/icon_back_light.png' 8 | import back_dark from '../../assets/images/icon_back_dark.png' 9 | import './index.less' 10 | type ComponentStateProps = {} 11 | 12 | type ComponentOwnProps = { 13 | type: string, // icon类型 14 | theme?: string // 主题 15 | onClick?: () => void 16 | } 17 | type ComponentState = {} 18 | 19 | type IProps = ComponentStateProps & ComponentOwnProps 20 | 21 | interface Icon { 22 | props: IProps; 23 | } 24 | 25 | class Icon extends Component { 26 | static defaultProps = { 27 | theme: 'light', 28 | onClick: () => {} 29 | } 30 | handleClick = () => { 31 | const {onClick} = this.props 32 | typeof onClick === 'function' && onClick() 33 | } 34 | render() { 35 | const { type, theme } = this.props 36 | return ( 37 | 38 | {type === 'menu' ? : null} 39 | {type === 'back' ? : null} 40 | 41 | ) 42 | } 43 | } 44 | 45 | export default Icon as ComponentClass -------------------------------------------------------------------------------- /src/components/ResultModal/index.less: -------------------------------------------------------------------------------- 1 | .result-wrap { 2 | width:100%; 3 | height:100%; 4 | position:absolute; 5 | top:0; 6 | left:0; 7 | z-index:1000; 8 | .modal-mask { 9 | width: 100%; 10 | height:100%; 11 | position:absolute; 12 | top:0; 13 | left:0; 14 | background-color:#fff; 15 | } 16 | .modal-content { 17 | padding-top:140rpx; 18 | position:relative; 19 | z-index:1; 20 | width:100%; 21 | height:100%; 22 | box-sizing:border-box; 23 | display:flex; 24 | flex-direction:column; 25 | align-items:center; 26 | justify-content:center; 27 | .pic-wrap { 28 | width:690rpx; 29 | height:920rpx; 30 | .pic { 31 | width:100%; 32 | height:100%; 33 | } 34 | } 35 | .btn-wrap { 36 | width:690rpx; 37 | height:920rpx; 38 | margin-top: 80rpx; 39 | } 40 | } 41 | 42 | .custom-button { 43 | border-radius:80rpx; 44 | width:100%; 45 | height:80rpx; 46 | line-height:80rpx; 47 | color:white; 48 | font-size:36rpx; 49 | text-align:center; 50 | transition:all 0.2s; 51 | &.btn-hover { 52 | transform: scale(0.85) 53 | } 54 | &.pink { 55 | background:#FF3366; 56 | } 57 | &.dark { 58 | background:#333; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/components/ResultModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Button, Image } from '@tarojs/components' 4 | 5 | import './index.less' 6 | 7 | type ComponentStateProps = {} 8 | 9 | type ComponentOwnProps = { 10 | onClick: () => void, 11 | url: string, 12 | } 13 | 14 | type ComponentState = {} 15 | 16 | type IProps = ComponentStateProps & ComponentOwnProps 17 | 18 | interface ResultModal { 19 | props: IProps; 20 | } 21 | 22 | class ResultModal extends Component { 23 | handleClick = () => { 24 | this.props.onClick && this.props.onClick() 25 | } 26 | render() { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | } 42 | 43 | export default ResultModal as ComponentClass -------------------------------------------------------------------------------- /src/components/SceneList/SceneItem/index.less: -------------------------------------------------------------------------------- 1 | .scene-item { 2 | position: relative; 3 | display: inline-block; 4 | width: 120rpx; 5 | height: 150rpx; 6 | margin-right: 44rpx; 7 | box-shadow:1px 1px 5px rgba(0,0,0,0.3); 8 | .music-icon { 9 | position: absolute; 10 | right: 7rpx; 11 | top: 10rpx; 12 | width: 20rpx; 13 | height: 20rpx; 14 | } 15 | .bg { 16 | width: 120rpx; 17 | height: 120rpx; 18 | position: relative; 19 | } 20 | .tag { 21 | position: absolute; 22 | left: 0; 23 | bottom: 0; 24 | box-sizing: border-box; 25 | padding: 0 10rpx; 26 | height: 32rpx; 27 | width: 120rpx; 28 | line-height: 32rpx; 29 | font-size: 16rpx; 30 | color: #fff; 31 | background: #FFA9C2; 32 | display: flex; 33 | align-items:center; 34 | justify-content: center; 35 | .tag-title { 36 | font-size:16rpx; 37 | } 38 | .icon { 39 | margin-right: 10rpx; 40 | display: inline-block; 41 | width: 8rpx; 42 | height: 8rpx; 43 | background: #fff; 44 | border-radius: 8rpx; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/SceneList/SceneItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Text, Image } from '@tarojs/components' 4 | 5 | import './index.less' 6 | import loading from '../../../assets/images/pic_loading.png' 7 | import icon_music_gif from '../../../assets/images/icon_music_gif.png' 8 | import icon_only_gif from '../../../assets/images/icon_only_gif.png' 9 | 10 | type ComponentStateProps = {} 11 | 12 | type ComponentOwnProps = { 13 | onClick?: () => void, 14 | bgUrl: string, 15 | thumbnailUrl: string, 16 | sceneName: string, 17 | active: boolean 18 | } 19 | 20 | type ComponentState = {} 21 | 22 | type IProps = ComponentStateProps & ComponentOwnProps 23 | 24 | interface SceneItem { 25 | props: IProps; 26 | } 27 | 28 | class SceneItem extends Component { 29 | static defaultProps = { 30 | bgUrl: loading, 31 | thumbnailUrl: loading, 32 | sceneName: '', 33 | active: false 34 | } 35 | componentWillReceiveProps (nextProps) { 36 | // console.log(this.props, nextProps) 37 | } 38 | render() { 39 | const { onClick, active, thumbnailUrl, bgUrl, sceneName } = this.props 40 | return ( 41 | 42 | 47 | 48 | 52 | 56 | 57 | 58 | {active && } 59 | {sceneName} 60 | 61 | 62 | ) 63 | } 64 | } 65 | 66 | export default SceneItem as ComponentClass -------------------------------------------------------------------------------- /src/components/SceneList/index.less: -------------------------------------------------------------------------------- 1 | .scene-list { 2 | // margin-right:-68rpx; 3 | .scroll { 4 | white-space:nowrap; 5 | } 6 | } -------------------------------------------------------------------------------- /src/components/SceneList/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, ScrollView, Image } from '@tarojs/components' 4 | 5 | import './index.less' 6 | import SceneItem from './SceneItem' 7 | type ComponentStateProps = {} 8 | 9 | type ComponentOwnProps = { 10 | onClick?: (item:object) => void, 11 | list: Array, 12 | currentScene: object, 13 | styleObj: object 14 | } 15 | 16 | type ComponentState = {} 17 | 18 | type IProps = ComponentStateProps & ComponentOwnProps 19 | 20 | interface SceneList { 21 | props: IProps; 22 | } 23 | 24 | class SceneList extends Component { 25 | 26 | static defaultProps = { 27 | list: [], 28 | currentScene: {} 29 | } 30 | 31 | componentWillReceiveProps (nextProps) { 32 | // console.log(this.props, nextProps) 33 | } 34 | 35 | handleClick = (item) => { 36 | const { onClick } = this.props 37 | typeof onClick === 'function' && onClick(item) 38 | } 39 | 40 | render() { 41 | const { onClick, list, currentScene, styleObj } = this.props 42 | return ( 43 | 44 | 48 | {list.map((item, index) => { 49 | return 57 | })} 58 | 59 | 60 | ) 61 | } 62 | } 63 | 64 | export default SceneList as ComponentClass -------------------------------------------------------------------------------- /src/components/Sticker/index.less: -------------------------------------------------------------------------------- 1 | .sticker-wrap { 2 | position:absolute; 3 | top:0; 4 | left:0; 5 | z-index:1; 6 | // transition:opacity 0.1s; 7 | &.hidden { 8 | opacity: 0; 9 | } 10 | &.event-through { 11 | pointer-events: none;//使用当前属性点击div将触发链接 12 | } 13 | .border { 14 | position:absolute; 15 | left:0; 16 | top:0; 17 | z-index:-1; 18 | width:100%; 19 | height:100%; 20 | border:1px solid transparent; 21 | &.active { 22 | border:1px solid #fff; 23 | } 24 | } 25 | .control { 26 | width:50rpx; 27 | height:50rpx; 28 | position:absolute; 29 | right:-25rpx; 30 | bottom:-25rpx; 31 | background:#fff; 32 | border-radius:25rpx; 33 | display:flex; 34 | align-items:center; 35 | justify-content:center; 36 | opacity:0; 37 | &.active { 38 | opacity:1; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/components/Sticker/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Image } from '@tarojs/components' 4 | import tool from '../../utils/tool' 5 | import './index.less' 6 | import loading from '../../assets/images/pic_loading.png' 7 | import scale from '../../assets/images/scale.png' 8 | 9 | type ComponentStateProps = {} 10 | 11 | type ComponentOwnProps = { 12 | onChangeStyle: () => void, 13 | onTouchend: (data?:any) => void, 14 | onTouchstart: (data?:any) => void, 15 | onImageLoaded?: (detail:object, item?:any) => void, 16 | url: string, 17 | stylePrams: object 18 | } 19 | 20 | type ComponentState = { 21 | framePrams: { 22 | width: number, 23 | height: number, 24 | left: number, 25 | top: number, 26 | } 27 | } 28 | 29 | type IProps = ComponentStateProps & ComponentOwnProps 30 | 31 | interface Sticker { 32 | props: IProps; 33 | throttledStickerOntouchmove: () => void; 34 | throttledArrowOntouchmove: () => void; 35 | } 36 | 37 | class Sticker extends Component { 38 | static defaultProps = { 39 | url: loading, 40 | stylePrams: { 41 | id: '', 42 | zIndex: 0, 43 | width: 0, 44 | height: 0, 45 | x: 0, 46 | y: 0, 47 | rotate: 0, 48 | originWidth: 0, // 原始宽度 49 | originHeight: 0, // 原始高度 50 | autoWidth: 0, // 自适应后的宽度 51 | autoHeight: 0, // 自适应后的高度 52 | autoScale: 1, // 相对画框缩放比例 53 | fixed: false, // 是否固定 54 | isActive: false, // 是否激活 55 | visible: true, // 是否显示 56 | }, 57 | } 58 | state = { 59 | framePrams: { 60 | width: 0, 61 | height: 0, 62 | left: 0, 63 | top: 0, 64 | }, 65 | } 66 | gesture = { 67 | startX: 0, 68 | startY: 0, 69 | zoom: false, 70 | distance: 0, 71 | preV: {x:null, y:null}, 72 | center: {x:0, y:0}, // 中心点y坐标 73 | scale: 1 74 | } 75 | 76 | constructor (props) { 77 | super(props) 78 | this.throttledStickerOntouchmove = this.throttle(this.stickerOntouchmove, 1000/20).bind(this) 79 | this.throttledArrowOntouchmove = this.throttle(this.arrowOntouchmove, 1000/20).bind(this) 80 | } 81 | 82 | componentWillMount () { 83 | } 84 | 85 | componentWillReceiveProps (nextProps) { 86 | // console.log('sticker componentWillReceiveProps', this.props, nextProps) 87 | if (nextProps.framePrams && nextProps.framePrams.width > 0) { 88 | this.setState({ 89 | framePrams: nextProps.framePrams 90 | }) 91 | } 92 | } 93 | 94 | isFixed = () => { 95 | const {stylePrams} = this.props 96 | return stylePrams.fixed || false 97 | } 98 | 99 | emitTouchstart = () => { 100 | const {onTouchstart, stylePrams} = this.props 101 | typeof onTouchstart === 'function' && onTouchstart(stylePrams) 102 | } 103 | 104 | emitTouchend = () => { 105 | const {onTouchend, stylePrams} = this.props 106 | typeof onTouchend === 'function' && onTouchend(stylePrams) 107 | } 108 | 109 | stickerOntouchstart = (e) => { 110 | if (this.isFixed()) { 111 | // 若固定则不能移动 112 | return 113 | } 114 | // console.log('stickerOntouchstart', e) 115 | const {gesture} = this 116 | const {framePrams} = this.state 117 | const frameOffsetX = framePrams.left 118 | const frameOffsetY = framePrams.top 119 | if (e.touches.length === 1) { 120 | let { clientX, clientY } = e.touches[0] 121 | gesture.startX = clientX - frameOffsetX 122 | gesture.startY = clientY - frameOffsetY 123 | // console.log('gesture-one', gesture) 124 | } else { 125 | let xMove = e.touches[1].clientX - e.touches[0].clientX 126 | let yMove = e.touches[1].clientY - e.touches[0].clientY 127 | let distance = Math.sqrt(xMove * xMove + yMove * yMove) 128 | // 记录旋转 129 | let v = { x: xMove, y: yMove } 130 | gesture.preV = v 131 | gesture.distance = distance 132 | gesture.zoom = true 133 | // console.log('双指缩放', gesture) 134 | } 135 | this.emitTouchstart() 136 | } 137 | 138 | stickerOntouchmove = (e) => { 139 | if (this.isFixed()) { 140 | // 若固定则不能移动 141 | return 142 | } 143 | // console.log('stickerOntouchmove', e) 144 | const {gesture} = this 145 | const {stylePrams} = this.props 146 | const {framePrams} = this.state 147 | const frameOffsetX = framePrams.left 148 | const frameOffsetY = framePrams.top 149 | 150 | if (e.touches.length === 1) { 151 | //单指移动 152 | if (gesture.zoom) { 153 | //缩放状态,不处理单指 154 | // console.log('不能移动') 155 | return 156 | } 157 | let { clientX, clientY } = e.touches[0]; 158 | const pointX = clientX - frameOffsetX // 触摸点所在画框坐标系的x坐标 159 | const pointY = clientY - frameOffsetY // 触摸点所在画框坐标系的y坐标 160 | let offsetX = pointX - gesture.startX; 161 | let offsetY = pointY - gesture.startY; 162 | gesture.startX = pointX; 163 | gesture.startY = pointY; 164 | this.changeStyleParams({ 165 | offsetX, 166 | offsetY 167 | }, 'offset') 168 | } else { 169 | //双指缩放 170 | let xMove = e.touches[1].clientX - e.touches[0].clientX; 171 | let yMove = e.touches[1].clientY - e.touches[0].clientY; 172 | let distance = Math.sqrt(xMove * xMove + yMove * yMove); 173 | 174 | // 计算缩放 175 | let distanceDiff = distance - gesture.distance; 176 | let newScale = gesture.scale + 0.005 * distanceDiff; 177 | // console.log('newScale', newScale) 178 | if (newScale < 0.3) { 179 | newScale = 0.3; 180 | } 181 | if (newScale > 4) { 182 | newScale = 4; 183 | } 184 | let newWidth = newScale * stylePrams.autoWidth 185 | let newHeight = newScale * stylePrams.autoHeight 186 | let newX = stylePrams.x - (newWidth - gesture.scale * stylePrams.autoWidth) * 0.5 187 | let newY = stylePrams.y - (newHeight - gesture.scale * stylePrams.autoHeight) * 0.5 188 | 189 | // 计算旋转 190 | let newRotate = 0 191 | let preV = gesture.preV 192 | let v = { 193 | x: xMove, 194 | y: yMove 195 | } 196 | if (preV.x !== null) { 197 | let angle = tool.getRotateAngle(v, preV) 198 | newRotate = parseFloat(stylePrams.rotate) + angle 199 | } 200 | // 更新数据 201 | gesture.scale = newScale 202 | gesture.distance = distance 203 | gesture.preV = v 204 | this.changeStyleParams({ 205 | ...stylePrams, 206 | width: newWidth, 207 | height: newHeight, 208 | x : newX, 209 | y : newY, 210 | rotate: newRotate 211 | }) 212 | } 213 | } 214 | 215 | stickerOntouchend = (e) => { 216 | if (this.isFixed()) { 217 | // 若固定则不能移动 218 | return 219 | } 220 | // console.log('stickerOntouchend', e) 221 | if (e.touches.length === 0) { 222 | //重置缩放状态 223 | this.gesture.zoom = false 224 | } 225 | this.emitTouchend() 226 | } 227 | 228 | arrowOntouchstart = (e) => { 229 | if (this.isFixed()) { 230 | // 若固定则不能移动 231 | return 232 | } 233 | const {gesture} = this 234 | const {stylePrams} = this.props 235 | const {framePrams} = this.state 236 | const frameOffsetX = framePrams.left 237 | const frameOffsetY = framePrams.top 238 | const center = tool.calcCenterPosition(stylePrams.x, stylePrams.y, stylePrams.width, stylePrams.height) 239 | if (e.touches.length === 1) { 240 | let { clientX, clientY } = e.touches[0] 241 | gesture.startX = clientX - frameOffsetX 242 | gesture.startY = clientY - frameOffsetY 243 | // console.log('gesture-one', gesture) 244 | let xMove = clientX - frameOffsetX - center.x; 245 | let yMove = clientY - frameOffsetY -center.y; 246 | let distance = Math.sqrt(xMove * xMove + yMove * yMove); 247 | // 记录旋转 248 | let v = { x: xMove, y: yMove } 249 | gesture.distance = distance 250 | gesture.zoom = true 251 | gesture.preV = v 252 | gesture.center = center 253 | } 254 | this.emitTouchstart() 255 | } 256 | 257 | arrowOntouchmove = (e) => { 258 | if (this.isFixed()) { 259 | // 若固定则不能移动 260 | return 261 | } 262 | // console.log('arrowOntouchmove', e) 263 | const {gesture} = this 264 | const {stylePrams} = this.props 265 | const {center} = gesture 266 | const {framePrams} = this.state 267 | const frameOffsetX = framePrams.left 268 | const frameOffsetY = framePrams.top 269 | if (e.touches.length === 1) { 270 | let xMove = e.touches[0].clientX - frameOffsetX - center.x 271 | let yMove = e.touches[0].clientY - frameOffsetY - center.y 272 | let distance = Math.sqrt(xMove * xMove + yMove * yMove) 273 | // 计算缩放 274 | let distanceDiff = distance - gesture.distance; 275 | let newScale = gesture.scale + 0.005 * distanceDiff; 276 | if (newScale < 0.2) { 277 | newScale = 0.2; 278 | } 279 | if (newScale > 4) { 280 | newScale = 4; 281 | } 282 | let newWidth = newScale * stylePrams.autoWidth 283 | let newHeight = newScale * stylePrams.autoHeight 284 | let newX = stylePrams.x - (newWidth - gesture.scale * stylePrams.autoWidth) * 0.5 285 | let newY = stylePrams.y - (newHeight - gesture.scale * stylePrams.autoHeight) * 0.5 286 | 287 | // 计算旋转 288 | let newRotate 289 | let preV = gesture.preV 290 | let v = { 291 | x: xMove, 292 | y: yMove 293 | } 294 | if (preV.x !== null) { 295 | let angle = tool.getRotateAngle(v, preV) 296 | newRotate = parseFloat(stylePrams.rotate) + angle 297 | } 298 | // 更新数据 299 | gesture.scale = newScale 300 | gesture.distance = distance 301 | gesture.preV = v 302 | 303 | stylePrams.width = newWidth 304 | stylePrams.height = newHeight 305 | stylePrams.x = newX 306 | stylePrams.y = newY 307 | stylePrams.rotate = newRotate 308 | 309 | this.changeStyleParams({ 310 | ...stylePrams, 311 | width: newWidth, 312 | height: newHeight, 313 | x : newX, 314 | y : newY, 315 | rotate: newRotate 316 | }) 317 | } 318 | } 319 | 320 | arrowOntouchend = (e) => { 321 | if (this.isFixed()) { 322 | // 若固定则不能移动 323 | return 324 | } 325 | // console.log('arrowOntouchend', e) 326 | if (e.touches.length === 0) { 327 | //重置缩放状态 328 | this.gesture.zoom = false 329 | } 330 | this.emitTouchend() 331 | } 332 | 333 | handleImageLoaded = (e) => { 334 | const { onImageLoaded, stylePrams } = this.props 335 | onImageLoaded && onImageLoaded(e.detail, stylePrams) 336 | } 337 | 338 | changeStyleParams = (obj:any, type?:string) => { 339 | const {stylePrams} = this.props 340 | const {onChangeStyle} = this.props 341 | let newStylePrams:any = null 342 | if (type === 'offset') { 343 | const {offsetX, offsetY} = obj 344 | newStylePrams = { 345 | ...stylePrams, 346 | x: stylePrams.x + offsetX, 347 | y: stylePrams.y + offsetY, 348 | } 349 | } else { 350 | newStylePrams = { 351 | ...stylePrams, 352 | ...obj 353 | } 354 | } 355 | typeof onChangeStyle === 'function' && onChangeStyle(newStylePrams) 356 | } 357 | 358 | throttle = (func, deltaX) => { 359 | let lastCalledAt = new Date().getTime(); 360 | let that = this; 361 | return function() { 362 | if(new Date().getTime() - lastCalledAt >= deltaX) { 363 | func.apply(that, arguments); 364 | lastCalledAt = new Date().getTime(); 365 | } else { 366 | console.log('不执行') 367 | } 368 | } 369 | } 370 | 371 | formatStyle = (style) => { 372 | const {zIndex, width, height, x, y, rotate} = style 373 | return { 374 | zIndex: zIndex, 375 | width:`${width}px`, 376 | height:`${height}px`, 377 | transform: `translate(${x}px, ${y}px) rotate(${rotate}deg)` 378 | } 379 | } 380 | 381 | render() { 382 | const { url, stylePrams } = this.props 383 | const { framePrams } = this.state 384 | const styleObj = this.formatStyle(this.props.stylePrams) 385 | // console.log('sticker(this.props)', this.props) 386 | return ( 387 | 0) ? '' : 'hidden' }`} 389 | style={styleObj} 390 | > 391 | {/* {framePrams.width} */} 392 | {/* {stylePrams.autoWidth} */} 393 | {/* {stylePrams.width} */} 394 | 402 | 403 | 408 | 409 | 410 | 411 | ) 412 | } 413 | } 414 | 415 | export default Sticker as ComponentClass -------------------------------------------------------------------------------- /src/components/Title/index.less: -------------------------------------------------------------------------------- 1 | .title-wrap { 2 | position:absolute; 3 | width:100%; 4 | top:0; 5 | left: 0; 6 | z-index: 100; 7 | text-align:center; 8 | font-size:36px; 9 | color:#fff; 10 | .left { 11 | position:absolute; 12 | left: 20px; 13 | top: 2px; 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component } from '@tarojs/taro' 3 | import { View, Text } from '@tarojs/components' 4 | 5 | import './index.less' 6 | 7 | type ComponentStateProps = {} 8 | 9 | type ComponentOwnProps = { 10 | top: number, 11 | renderLeft: any, 12 | children: any, 13 | color?: string 14 | } 15 | 16 | type ComponentState = {} 17 | 18 | type IProps = ComponentStateProps & ComponentOwnProps 19 | 20 | interface Title { 21 | props: IProps; 22 | } 23 | 24 | class Title extends Component { 25 | static defaultProps = { 26 | color: '#fff' 27 | } 28 | componentWillReceiveProps (nextProps) { 29 | // console.log(this.props, nextProps) 30 | } 31 | render() { 32 | const { top, color } = this.props 33 | return ( 34 | 35 | 36 | {this.props.renderLeft} 37 | 38 | {this.props.children} 39 | 40 | ) 41 | } 42 | } 43 | 44 | export default Title as ComponentClass -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Taro 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/model/actions/counter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ADD, 3 | MINUS 4 | } from '../constants/counter' 5 | 6 | export const add = () => { 7 | return { 8 | type: ADD 9 | } 10 | } 11 | export const minus = () => { 12 | return { 13 | type: MINUS 14 | } 15 | } 16 | 17 | // 异步的action 18 | export function asyncAdd () { 19 | return dispatch => { 20 | setTimeout(() => { 21 | dispatch(add()) 22 | }, 2000) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/model/actions/global.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SYSTEM, 3 | } from '../constants/global' 4 | 5 | export const getSystemInfo = (data) => { 6 | return { 7 | type: SYSTEM, 8 | data 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/model/constants/counter.ts: -------------------------------------------------------------------------------- 1 | export const ADD = 'ADD' 2 | export const MINUS = 'MINUS' 3 | -------------------------------------------------------------------------------- /src/model/constants/global.ts: -------------------------------------------------------------------------------- 1 | export const SYSTEM = 'SYSTEM' -------------------------------------------------------------------------------- /src/model/reducers/counter.ts: -------------------------------------------------------------------------------- 1 | import { ADD, MINUS } from '../constants/counter' 2 | 3 | const INITIAL_STATE = { 4 | num: 10 5 | } 6 | 7 | export default function counter (state = INITIAL_STATE, action) { 8 | switch (action.type) { 9 | case ADD: 10 | return { 11 | ...state, 12 | num: state.num + 1 13 | } 14 | case MINUS: 15 | return { 16 | ...state, 17 | num: state.num - 1 18 | } 19 | default: 20 | return state 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/model/reducers/global.ts: -------------------------------------------------------------------------------- 1 | import { SYSTEM } from '../constants/global' 2 | 3 | const INITIAL_STATE = { 4 | system: { 5 | statusBarHeight: 20 6 | } 7 | } 8 | 9 | export default function counter (state = INITIAL_STATE, action) { 10 | switch (action.type) { 11 | case SYSTEM: 12 | return { 13 | ...state, 14 | system: {...state.system, ...action.data} 15 | } 16 | default: 17 | return state 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/model/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import counter from './counter' 3 | import global from './global' 4 | 5 | export default combineReducers({ 6 | counter, 7 | global 8 | }) 9 | -------------------------------------------------------------------------------- /src/model/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | 5 | const composeEnhancers = 6 | typeof window === 'object' && 7 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 8 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 9 | // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... 10 | }) : compose 11 | 12 | const middlewares = [ 13 | thunkMiddleware 14 | ] 15 | 16 | if (process.env.NODE_ENV === 'development') { 17 | middlewares.push(require('redux-logger').createLogger()) 18 | } 19 | 20 | const enhancer = composeEnhancers( 21 | applyMiddleware(...middlewares), 22 | // other store enhancers if any 23 | ) 24 | 25 | export default function configStore () { 26 | const store = createStore(rootReducer, enhancer) 27 | return store 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/dynamic/index.less: -------------------------------------------------------------------------------- 1 | .page-dynamic { 2 | position:relative; 3 | width:100%; 4 | height:100%; 5 | padding-top:130rpx; 6 | box-sizing:border-box; 7 | display:flex; 8 | flex-direction:column; 9 | align-items:center; 10 | justify-content:center; 11 | .pic-section { 12 | position:relative; 13 | width:690rpx; 14 | .raw { 15 | position: absolute; 16 | left: 0; 17 | top: 0; 18 | width:690rpx; 19 | height:920rpx; 20 | &.hidden { 21 | opacity: 0; 22 | } 23 | } 24 | .crop { 25 | position:relative; 26 | width:690rpx; 27 | height:920rpx; 28 | background: #ccc; 29 | overflow: hidden; 30 | &.hidden { 31 | opacity: 0; 32 | } 33 | .background-image { 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | width: 100%; 38 | height: 100%; 39 | } 40 | .raw-image { 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | height: 100%; 46 | } 47 | } 48 | } 49 | .button-section { 50 | margin-top: 20rpx 51 | } 52 | .canvas-wrap { 53 | position:fixed; 54 | left:0; 55 | bottom:-3050px; 56 | z-index:-2; 57 | opacity:0; 58 | visibility:none; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/dynamic/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component, Config } from '@tarojs/taro' 3 | import { View, Button, Image, Canvas } from '@tarojs/components' 4 | import { connect } from '@tarojs/redux' 5 | 6 | import globalData from '../../services/global_data' 7 | import { getSystemInfo } from '../../model/actions/global' 8 | import tool from '../../utils/tool' 9 | import {createCache} from '../../services/cache' 10 | import './index.less' 11 | import Title from '../../components/Title' 12 | import CustomIcon from '../../components/Icon' 13 | import Sticker from '../../components/Sticker' 14 | import SceneList from '../../components/SceneList' 15 | import ResultModal from '../../components/ResultModal' 16 | 17 | import service from '../../services/service' 18 | import mock_theme_data from './mock_theme_data.json' 19 | 20 | const mock_path = 'https://static01.versa-ai.com/upload/783272fc1375/999deac02e85f3ea.png' 21 | const mock_segment_url = 'https://static01.versa-ai.com/images/process/segment/2019/01/14/b4cf047a-17a5-11e9-817f-00163e001583.png' 22 | const getSceneList = function (sceneList:Array = []) { 23 | const result = [] 24 | sceneList.forEach(v => { 25 | const {bgUrl, sceneId, sceneName, shareContent, thumbnailUrl, sceneConfig, segmentType, segmentZIndex, bgZIndex} = v 26 | let supportMusic = false 27 | if (sceneConfig) { 28 | const {music = {}} = JSON.parse(sceneConfig) 29 | supportMusic = music.fileUrl ? true : false 30 | } 31 | result.push({bgUrl, sceneId, sceneName, shareContent, thumbnailUrl, sceneConfig, segmentType, segmentZIndex, bgZIndex, supportMusic}) 32 | }) 33 | return result 34 | } 35 | 36 | type PageStateProps = { 37 | global: { 38 | system: object 39 | } 40 | } 41 | 42 | type PageDispatchProps = { 43 | getSystemInfo: (data:object) => void 44 | } 45 | 46 | type PageOwnProps = {} 47 | 48 | type PageState = { 49 | foreground: { 50 | remoteUrl: string, 51 | zIndex: number, 52 | width: number, 53 | height: number, 54 | x: number, 55 | y: number, 56 | rotate: number, 57 | originWidth: number, 58 | originHeight: number, 59 | autoWidth: number, 60 | autoHeight: number, 61 | autoScale: number, 62 | fixed: boolean, 63 | visible: boolean 64 | } 65 | } 66 | 67 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps 68 | 69 | interface Dynamic { 70 | props: IProps; 71 | } 72 | 73 | @connect(({ counter, global }) => ({ 74 | counter, 75 | global 76 | }), (dispatch) => ({ 77 | getSystemInfo (data) { 78 | dispatch(getSystemInfo(data)) 79 | } 80 | })) 81 | class Dynamic extends Component { 82 | config: Config = { 83 | navigationBarTitleText: '马卡龙玩图-taro', 84 | disableScroll: true, 85 | } 86 | 87 | state = { 88 | rawImage: { 89 | localUrl: '', 90 | remoteUrl: '' 91 | }, 92 | frame: { 93 | width: 0, 94 | height: 0, 95 | left: 0, 96 | top: 0, 97 | }, 98 | foreground: { 99 | id: 'foreground', 100 | remoteUrl: '', 101 | zIndex:2, 102 | width:0, 103 | height:0, 104 | x: 0, 105 | y:0, 106 | rotate: 0, 107 | originWidth: 0, // 原始宽度 108 | originHeight: 0, // 原始高度 109 | autoWidth: 0, // 自适应后的宽度 110 | autoHeight: 0, // 自适应后的高度 111 | autoScale: 0, // 相对画框缩放比例 112 | fixed: false, // 是否固定 113 | isActive: true, // 是否激活 114 | visible: true, // 是否显示 115 | }, 116 | coverList: [ 117 | // { 118 | // id: 'cover-01', 119 | // remoteUrl: 'https://static01.versa-ai.com/images/process/segment/2019/01/07/a102310e-122a-11e9-b5ef-00163e023476.png', 120 | // originHeight: 2440, 121 | // originWidth: 750, 122 | // autoHeight: 244, 123 | // autoScale: 0.1, 124 | // autoWidth: 75, 125 | // width: 57.378244033967235, 126 | // height:186.6705539238401, 127 | // x: 185.1442062300867, 128 | // y: 155.66472303807996, 129 | // rotate: -25.912119928692746, 130 | // zIndex: 3, 131 | // fixed: false, // 是否固定 132 | // isActive: false, // 是否激活 133 | // visible: true, // 是否显示 134 | // } 135 | ], 136 | sceneList: [], 137 | currentScene: {}, 138 | canvas: { 139 | id: 'shareCanvas', 140 | ratio: 3 141 | }, 142 | result: { 143 | show: false, 144 | url: '', 145 | } 146 | } 147 | 148 | // 全局主题数据 149 | themeData = { 150 | sceneList: [], 151 | rawCoverList: [], // 原始贴纸数据 152 | } 153 | 154 | cache = { 155 | foreground: createCache('foreground'), 156 | cover: createCache('cover'), 157 | source: createCache('source'), 158 | } 159 | 160 | componentWillMount () { 161 | this.initSystemInfo() 162 | } 163 | componentDidMount () { 164 | this.calFrameRect() 165 | this.initRawImage() 166 | this.initSceneData(() => { 167 | this.initCoverData() 168 | }) 169 | this.initSegment() 170 | } 171 | componentWillReceiveProps (nextProps) { 172 | // console.log(this.props, nextProps) 173 | } 174 | 175 | componentWillUnmount () { } 176 | 177 | componentDidShow () { } 178 | 179 | componentDidHide () { } 180 | 181 | test = async () => { 182 | // try { 183 | // const result = await service.core.column() 184 | // console.log('result', result) 185 | // } catch(err) { 186 | // console.log('catch', err) 187 | // } 188 | // const uploadResult = await service.base.upload(mock_path, 'png') 189 | // console.log('uploadResult', uploadResult) 190 | } 191 | // 公共方法 192 | PageToHome = () => { 193 | Taro.redirectTo({ 194 | url: '/pages/home/index' 195 | }) 196 | } 197 | setStateTarget = (key, value = {}, callback?:() => void) => { 198 | const target = this.state[key] 199 | this.setState({ 200 | [key]: { 201 | ...target, 202 | ...value 203 | } 204 | }, () => { 205 | typeof callback === 'function' && callback() 206 | }) 207 | } 208 | getDomRect = (id:string, callback:(rect:object)=>void) => { 209 | Taro.createSelectorQuery().select('#' + id).boundingClientRect(function(rect){ 210 | // rect.id // 节点的ID 211 | // rect.dataset // 节点的dataset 212 | // rect.left // 节点的左边界坐标 213 | // rect.right // 节点的右边界坐标 214 | // rect.top // 节点的上边界坐标 215 | // rect.bottom // 节点的下边界坐标 216 | // rect.width // 节点的宽度 217 | // rect.height // 节点的高度 218 | typeof callback === 'function' && callback(rect) 219 | }).exec() 220 | } 221 | calFrameRect = () => { 222 | this.getDomRect('crop', rect => { 223 | this.setState({ 224 | frame: { 225 | width: rect.width, 226 | height: rect.height, 227 | left: rect.left, 228 | top: rect.top, 229 | } 230 | }) 231 | }) 232 | } 233 | getSceneInfoById = (id:string, list:Array = [], key:string) => { 234 | return list.filter(v => { 235 | return v[key] === id 236 | })[0] 237 | } 238 | getCoverInfoById = (id:string, list:Array = [], key:string) => { 239 | return list.filter(v => { 240 | return v[key] === id 241 | })[0] 242 | } 243 | formatRawCoverList = (list) => { 244 | return list.map(v => { 245 | const cover_model = { 246 | id: '', 247 | remoteUrl: '', 248 | originHeight: 0, 249 | originWidth: 0, 250 | autoHeight: 0, 251 | autoScale: 0, 252 | autoWidth: 0, 253 | width: 0, 254 | height: 0, 255 | x: 0, 256 | y: 0, 257 | rotate: 0, 258 | zIndex: 0, 259 | fixed: false, 260 | isActive: false, 261 | visible: false 262 | } 263 | cover_model.remoteUrl = v.imageUrl 264 | cover_model.id = v.id 265 | cover_model.zIndex = v.zIndex || 0 266 | cover_model.fixed = v.fixed || false 267 | cover_model.isActive = v.isActive || false 268 | cover_model.visible = true 269 | return cover_model 270 | }) 271 | } 272 | 273 | // 初始化系统信息 274 | initSystemInfo = () => { 275 | const {getSystemInfo, global} = this.props 276 | if (!global.system.model) { 277 | const systemInfo = Taro.getSystemInfoSync() 278 | getSystemInfo(systemInfo) 279 | } 280 | } 281 | initRawImage = () => { 282 | const {rawImage} = this.state 283 | this.setState({ 284 | rawImage: { 285 | ...rawImage, 286 | localUrl: globalData.choosedImage 287 | } 288 | }) 289 | } 290 | // 初始化分割 291 | initSegment = async () => { 292 | const {foreground} = this.state 293 | let segmentData 294 | try { 295 | Taro.showLoading({ 296 | title: '照片变身中...', 297 | mask: true, 298 | }) 299 | segmentData = await service.core.segmentDemo(globalData.choosedImage, mock_segment_url , 3000) 300 | Taro.hideLoading() 301 | } catch(err) { 302 | console.log('catch', err) 303 | } 304 | this.setState({ 305 | foreground: { 306 | ...foreground, 307 | remoteUrl: segmentData.result 308 | } 309 | }) 310 | } 311 | // 初始化场景信息 312 | initSceneData = (callback) => { 313 | // 全局主题数据 314 | const themeData = mock_theme_data.result 315 | this.themeData.sceneList = getSceneList(themeData.sceneList || []) 316 | 317 | // 去除sceneConfig属性 318 | const sceneList = this.themeData.sceneList.map((v:object) => { 319 | const {sceneConfig, ...rest} = v 320 | return { 321 | ...rest 322 | } 323 | }) 324 | const currentScene = sceneList[0] 325 | 326 | this.setState({ 327 | sceneList: sceneList, 328 | currentScene: currentScene 329 | }, () => { 330 | // console.log('state', this.state) 331 | typeof callback === 'function' && callback() 332 | }) 333 | } 334 | // 初始化贴纸 335 | initCoverData = () => { 336 | const {currentScene} = this.state 337 | const sceneInfo = this.getSceneInfoById(currentScene.sceneId, this.themeData.sceneList, 'sceneId') 338 | const sceneConfig = JSON.parse(sceneInfo.sceneConfig) 339 | const {cover = {}} = sceneConfig 340 | 341 | this.themeData.rawCoverList = cover.list || [] 342 | const coverList = this.formatRawCoverList(this.themeData.rawCoverList) 343 | 344 | this.setState({ 345 | coverList: coverList 346 | }) 347 | // console.log('initCoverData cover', cover, coverList) 348 | } 349 | 350 | // 背景 351 | handleBackgroundClick = () => { 352 | this.setForegroundActiveStatus(false) 353 | this.setCoverListActiveStatus({type: 'all'}, false) 354 | } 355 | // 人物 356 | handleForegroundLoaded = (detail:object, item?:any) => { 357 | // console.log('handleForegroundLoaded', detail, item) 358 | const {width, height} = detail 359 | this.setStateTarget('foreground', { 360 | originWidth: width, 361 | originHeight: height 362 | }, () => { 363 | this.foregroundAuto() 364 | }) 365 | } 366 | handleChangeStyle = (data) => { 367 | const {foreground} = this.state 368 | this.setState({ 369 | foreground: { 370 | ...foreground, 371 | ...data 372 | } 373 | }, () => { 374 | }) 375 | } 376 | handleForegroundTouchstart = (sticker) => { 377 | // console.log('handleForegroundTouchstart', sticker) 378 | this.setForegroundActiveStatus(true) 379 | this.setCoverListActiveStatus({type: 'all'}, false) 380 | } 381 | handleForegroundTouchend = () => { 382 | this.storeForegroundInfo() 383 | } 384 | // 贴纸 385 | handleCoverLoaded = (detail:object, item?:any) => { 386 | // console.log('handleCoverLoaded', detail, item) 387 | const {width, height} = detail 388 | const originInfo = { 389 | originWidth: width, 390 | originHeight: height 391 | } 392 | this.coverAuto(originInfo, item) 393 | } 394 | handleChangeCoverStyle = (data) => { 395 | const {id} = data 396 | const {coverList} = this.state 397 | coverList.forEach((v, i) => { 398 | if (v.id === id) { 399 | coverList[i] = data 400 | } 401 | }) 402 | this.setState({ 403 | coverList: coverList 404 | }) 405 | } 406 | handleCoverTouchstart = (sticker) => { 407 | // console.log('handleCoverTouchstart', sticker) 408 | this.setCoverListActiveStatus({type: 'some', ids:[sticker.id]}, true) 409 | this.setForegroundActiveStatus(false) 410 | } 411 | handleCoverTouchend = (sticker) => { 412 | // console.log('handleCoverTouchend', sticker) 413 | this.storeCoverInfo(sticker) 414 | } 415 | 416 | // 更换场景 417 | handleChooseScene = (scene) => { 418 | const {currentScene} = this.state 419 | if (!scene.sceneId || currentScene.sceneId === scene.sceneId ) { 420 | return 421 | } 422 | this.setState({ 423 | currentScene: scene 424 | }, () => { 425 | // console.log('handleChooseScene', this.state.currentScene) 426 | this.foregroundAuto() 427 | this.initCoverData() 428 | }) 429 | } 430 | // 保存 431 | handleOpenResult = async () => { 432 | Taro.showLoading({ 433 | title: '照片生成中...', 434 | mask: true, 435 | }) 436 | const canvasImageUrl = await this.createCanvas() 437 | console.log('canvasImageUrl', canvasImageUrl) 438 | Taro.hideLoading() 439 | this.setState({ 440 | result: { 441 | url: canvasImageUrl, 442 | show: true 443 | } 444 | }) 445 | } 446 | // 再玩一次 447 | handleResultClick = () => { 448 | this.setResultModalStatus(false) 449 | } 450 | 451 | setResultModalStatus = (flag = false) => { 452 | const {result} = this.state 453 | result.show = flag 454 | this.setState({ 455 | result: { 456 | ...result 457 | } 458 | }) 459 | } 460 | 461 | createCanvas = async () => { 462 | return new Promise(async (resolve, reject) => { 463 | const {currentScene, foreground, frame, canvas} = this.state 464 | const postfix = '?x-oss-process=image/resize,h_748,w_560' 465 | const context = Taro.createCanvasContext(canvas.id, this) 466 | const { ratio = 3 } = canvas 467 | // 下载远程背景图片 468 | let localBgImagePath = '' 469 | try { 470 | const bgUrl = currentScene.bgUrl + postfix 471 | localBgImagePath = await this.downloadRemoteImage(bgUrl) 472 | } catch (err) { 473 | console.log('下载背景图片失败', err) 474 | return 475 | } 476 | //防止锯齿,绘的图片是所需图片的3倍 477 | context.drawImage(localBgImagePath, 0, 0, frame.width * ratio, frame.height * ratio) 478 | // 绘制元素 479 | await this.canvasDrawElement(context, ratio) 480 | //绘制图片 481 | context.draw() 482 | //将生成好的图片保存到本地,需要延迟一会,绘制期间耗时 483 | setTimeout(function () { 484 | Taro.canvasToTempFilePath({ 485 | canvasId: canvas.id, 486 | fileType: 'jpg', 487 | success: function (res) { 488 | let tempFilePath = res.tempFilePath 489 | resolve(tempFilePath) 490 | }, 491 | fail: function (res) { 492 | reject(res) 493 | }, 494 | complete:function(){ 495 | } 496 | }); 497 | }, 400) 498 | }) 499 | } 500 | // 绘制贴纸,文字,覆盖层所有元素 501 | canvasDrawElement = async (context, ratio) => { 502 | const {currentScene, foreground, frame, canvas, coverList = []} = this.state 503 | // 收集所有元素进行排序 504 | let elements:Array = [] 505 | const element_foreground = { 506 | type: 'foreground', 507 | id: foreground.id, 508 | zIndex: foreground.zIndex, 509 | remoteUrl: foreground.remoteUrl, 510 | width: foreground.width * ratio, 511 | height: foreground.height * ratio, 512 | x: foreground.x * ratio, 513 | y: foreground.y * ratio, 514 | rotate: foreground.rotate, 515 | } 516 | // 收集人物 517 | elements.push(element_foreground) 518 | // 收集贴纸 519 | coverList.forEach(v => { 520 | const element_cover = { 521 | type: 'cover', 522 | zIndex: v.zIndex, 523 | id: v.id, 524 | remoteUrl: v.remoteUrl, 525 | width: v.width * ratio, 526 | height: v.height * ratio, 527 | x: v.x * ratio, 528 | y: v.y * ratio, 529 | rotate: v.rotate, 530 | } 531 | elements.push(element_cover) 532 | }) 533 | // 对元素进行排序 534 | elements.sort((a, b) => { 535 | return a.zIndex - b.zIndex 536 | }) 537 | // 下载成本地图片并绘制 538 | for (let i = 0; i < elements.length; i++ ) { 539 | const element = elements[i] 540 | try { 541 | const localImagePath = await this.downloadRemoteImage(element.remoteUrl) 542 | element.localUrl = localImagePath 543 | drawElement(element) 544 | } catch (err) { 545 | console.log('下载贴纸图片失败', err) 546 | continue 547 | } 548 | } 549 | // console.log('elements', elements) 550 | function drawElement ({localUrl, width, height, x, y, rotate}) { 551 | context.save() 552 | context.translate(x + 0.5 * width, y + 0.5 * height) 553 | context.rotate(rotate * Math.PI / 180) 554 | context.drawImage(localUrl, -0.5 * width, -0.5 * height, width, height) 555 | context.restore() 556 | context.stroke() 557 | } 558 | } 559 | 560 | // 下载照片并存储到本地 561 | downloadRemoteImage = async (remoteUrl = '') => { 562 | // 判断是否在缓存里 563 | const cacheKey = `${remoteUrl}_localPath` 564 | const cache_source = this.cache['source'] 565 | 566 | let localImagePath = '' 567 | if (cache_source.get(cacheKey)) { 568 | // console.log('get-cache', cacheKey, cache_source.get(cacheKey)) 569 | return cache_source.get(cacheKey) 570 | } else { 571 | try { 572 | const result = await service.base.downloadFile(remoteUrl) 573 | localImagePath = result.tempFilePath 574 | } catch (err) { 575 | console.log('下载图片失败', err) 576 | } 577 | } 578 | return this.cache['source'].set(cacheKey, localImagePath) 579 | } 580 | 581 | // 设置人物状态 582 | setForegroundActiveStatus = (value = false) => { 583 | this.setStateTarget('foreground', {isActive: value}) 584 | } 585 | // 设置贴纸状态 586 | setCoverListActiveStatus = (options = {}, value = false) => { 587 | const {type, ids = []} = options 588 | const {coverList} = this.state 589 | if (type === 'all') { 590 | coverList.forEach(v => { 591 | v['isActive'] = value 592 | }) 593 | } else { 594 | coverList.forEach(v => { 595 | if (ids.indexOf(v.id) > -1) { 596 | v['isActive'] = value 597 | } else { 598 | v['isActive'] = !value 599 | } 600 | }) 601 | } 602 | this.setState({ 603 | coverList 604 | }) 605 | } 606 | 607 | // 人物自适应 608 | foregroundAuto = (callback?:()=>void) => { 609 | // 先判断是否有缓存 610 | const {currentScene} = this.state 611 | const sceneId = currentScene.sceneId || 'demo_scene' 612 | const cache_foreground = this.cache['foreground'] 613 | const scene_foreground_params = cache_foreground.get(sceneId) 614 | 615 | if ( scene_foreground_params ) { 616 | this.setStateTarget('foreground', { 617 | ...scene_foreground_params 618 | }, () => { 619 | typeof callback === 'function' && callback() 620 | }) 621 | return 622 | } 623 | 624 | const size = this.calcForegroundSize() 625 | const position = this.calcForegroundPosition(size) 626 | this.setStateTarget('foreground', { 627 | ...size, 628 | ...position 629 | }, () => { 630 | typeof callback === 'function' && callback() 631 | }) 632 | } 633 | // 计算人物尺寸 634 | calcForegroundSize = () => { 635 | const {currentScene, sceneList, foreground, frame} = this.state 636 | const {originWidth, originHeight} = foreground 637 | const sceneInfo = this.getSceneInfoById(currentScene.sceneId, this.themeData.sceneList, 'sceneId') 638 | // console.log('calcForegroundSize', this.themeData.sceneList, currentScene.sceneId, sceneInfo) 639 | const imageRatio = originWidth / originHeight 640 | const params = JSON.parse(sceneInfo.sceneConfig) 641 | const autoScale = parseFloat(params.size.default) 642 | 643 | const result = { 644 | autoScale, 645 | autoWidth: 0, 646 | autoHeight: 0, 647 | width: 0, 648 | height: 0 649 | } 650 | if (originWidth > originHeight) { 651 | // 以最短边计算 652 | result.autoWidth = frame.width * autoScale 653 | result.autoHeight = result.autoWidth / imageRatio 654 | } else { 655 | result.autoHeight = frame.height * autoScale 656 | result.autoWidth = result.autoHeight * imageRatio 657 | } 658 | result.width = result.autoWidth 659 | result.height = result.autoHeight 660 | 661 | return result 662 | } 663 | // 计算人物位置 664 | calcForegroundPosition = ({width, height} = {}) => { 665 | const {currentScene, sceneList, foreground, frame} = this.state 666 | const {originWidth, originHeight} = foreground 667 | width = width || foreground.width 668 | height = height || foreground.height 669 | const sceneInfo = this.getSceneInfoById(currentScene.sceneId, this.themeData.sceneList, 'sceneId') 670 | 671 | const boxWidth = frame.width 672 | const boxHeight = frame.height 673 | const sceneConfig = JSON.parse(sceneInfo.sceneConfig) 674 | const {position} = sceneConfig 675 | const type = position.place || '0' 676 | const result = { 677 | x: 0, 678 | y: 0, 679 | rotate: 0 680 | } 681 | switch (type) { 682 | case '0': 683 | result.x = (boxWidth - width) * 0.5 684 | result.y = (boxHeight - height) * 0.5 685 | break 686 | case '1': 687 | result.x = 0 688 | result.y = 0 689 | break 690 | case '2': 691 | result.x = (boxWidth - width) * 0.5 692 | result.y = 0 693 | break 694 | case '3': 695 | result.x = boxWidth - width 696 | result.y = 0 697 | break 698 | case '4': 699 | result.x = boxWidth - width 700 | result.y = (boxHeight - height) * 0.5 701 | break 702 | case '5': 703 | result.x = boxWidth - width 704 | result.y = boxHeight - height 705 | break 706 | case '6': 707 | result.x = (boxWidth - width) * 0.5 708 | result.y = boxHeight - height 709 | break 710 | case '7': 711 | result.x = 0 712 | result.y = boxHeight - height 713 | break 714 | case '8': 715 | result.x = 0 716 | result.y = (boxHeight - height) * 0.5 717 | break 718 | case '9': 719 | const result_location = location(position, boxWidth, boxHeight, width, height) 720 | result.x = result_location.x 721 | result.y = result_location.y 722 | break 723 | case '10': 724 | const result_center = centerLocation(position, boxWidth, boxHeight, width, height) 725 | result.x = result_center.x 726 | result.y = result_center.y 727 | break 728 | default: 729 | result.x = (boxWidth - width) * 0.5 730 | result.y = (boxHeight - height) * 0.5 731 | } 732 | result.rotate = parseInt(sceneConfig.rotate) 733 | 734 | return result 735 | 736 | function location (position, boxWidth, boxHeight, width, height) { 737 | const result = { 738 | x: 0, 739 | y: 0 740 | } 741 | if (position.xAxis.derection === 'left') { 742 | result.x = position.xAxis.offset * boxWidth 743 | } 744 | if (position.xAxis.derection === 'right') { 745 | result.x = boxWidth * (1 - position.xAxis.offset) - width 746 | } 747 | if (position.yAxis.derection === 'top') { 748 | result.y = position.yAxis.offset * boxHeight 749 | } 750 | if (position.yAxis.derection === 'bottom') { 751 | result.y = boxHeight * (1 - position.yAxis.offset) - height 752 | } 753 | return result 754 | } 755 | // 中心点设置位置 756 | function centerLocation (position, boxWidth, boxHeight, width, height) { 757 | const result = { 758 | x: 0, 759 | y: 0 760 | } 761 | if (position.xAxis.derection === 'left') { 762 | result.x = position.xAxis.offset * boxWidth - width * 0.5 763 | } 764 | if (position.xAxis.derection === 'right') { 765 | result.x = boxWidth * (1 - position.xAxis.offset) - width * 0.5 766 | } 767 | if (position.yAxis.derection === 'top') { 768 | result.y = position.yAxis.offset * boxHeight - height * 0.5 769 | } 770 | if (position.yAxis.derection === 'bottom') { 771 | result.y = boxHeight * (1 - position.yAxis.offset) - height * 0.5 772 | } 773 | return result 774 | } 775 | } 776 | // 缓存人物尺寸位置 777 | storeForegroundInfo = () => { 778 | const {foreground, currentScene} = this.state 779 | const clone_foreground = tool.deepClone(foreground) 780 | clone_foreground.isActive = false 781 | const sceneId = currentScene.sceneId || 'demo_scene' 782 | this.cache['foreground'].set(sceneId, clone_foreground) 783 | // console.log('this.cache.foreground', this.cache['foreground'].get(sceneId)) 784 | } 785 | 786 | // 贴纸自适应 787 | coverAuto = (originInfo, cover, callback?:()=>void) => { 788 | const size = this.calcCoverSize(originInfo, cover) 789 | const position = this.calcCoverPosition(size, cover) 790 | const {coverList, currentScene} = this.state 791 | coverList.forEach((v, i) => { 792 | if (v.id === cover.id) { 793 | // 判断是否有缓存 794 | const cacheKey = `${currentScene.sceneId}_${v.id}` 795 | const cacheRes = this.cache['cover'].get(cacheKey) 796 | if (cacheRes) { 797 | coverList[i] = cacheRes 798 | } else { 799 | coverList[i] = {...v, ...size, ...position} 800 | } 801 | } 802 | }) 803 | 804 | this.setState({ 805 | coverList: coverList 806 | }, () => { 807 | typeof callback === 'function' && callback() 808 | }) 809 | } 810 | calcCoverSize = (originInfo, cover) => { 811 | const {originWidth, originHeight} = originInfo 812 | const {frame} = this.state 813 | const coverInfo = this.getCoverInfoById(cover.id, this.themeData.rawCoverList, 'id') 814 | 815 | const imageRatio = originWidth / originHeight 816 | const autoScale = parseFloat(coverInfo.size.default || 0.5) 817 | const result = { 818 | autoScale, 819 | autoWidth: 0, 820 | autoHeight: 0, 821 | width: 0, 822 | height: 0 823 | } 824 | if (originWidth > originHeight) { 825 | // 以最短边计算 826 | result.autoWidth = frame.width * autoScale 827 | result.autoHeight = result.autoWidth / imageRatio 828 | } else { 829 | result.autoHeight = frame.height * autoScale 830 | result.autoWidth = result.autoHeight * imageRatio 831 | } 832 | result.width = result.autoWidth 833 | result.height = result.autoHeight 834 | 835 | return result 836 | } 837 | calcCoverPosition = (size = {}, cover = {}) => { 838 | const {width = 0, height = 0} = size 839 | const {frame} = this.state 840 | const coverInfo = this.getCoverInfoById(cover.id, this.themeData.rawCoverList, 'id') 841 | const {position, rotate = 0} = coverInfo 842 | const boxWidth = frame.width 843 | const boxHeight = frame.height 844 | 845 | const type = position.place || '0' 846 | const result = { 847 | x: 0, 848 | y: 0, 849 | rotate: 0 850 | } 851 | switch (type) { 852 | case '0': 853 | result.x = (boxWidth - width) * 0.5 854 | result.y = (boxHeight - height) * 0.5 855 | break 856 | case '1': 857 | result.x = 0 858 | result.y = 0 859 | break 860 | case '2': 861 | result.x = (boxWidth - width) * 0.5 862 | result.y = 0 863 | break 864 | case '3': 865 | result.x = boxWidth - width 866 | result.y = 0 867 | break 868 | case '4': 869 | result.x = boxWidth - width 870 | result.y = (boxHeight - height) * 0.5 871 | break 872 | case '5': 873 | result.x = boxWidth - width 874 | result.y = boxHeight - height 875 | break 876 | case '6': 877 | result.x = (boxWidth - width) * 0.5 878 | result.y = boxHeight - height 879 | break 880 | case '7': 881 | result.x = 0 882 | result.y = boxHeight - height 883 | break 884 | case '8': 885 | result.x = 0 886 | result.y = (boxHeight - height) * 0.5 887 | break 888 | case '9': 889 | const result_location = location(position, boxWidth, boxHeight, width, height) 890 | result.x = result_location.x 891 | result.y = result_location.y 892 | break 893 | case '10': 894 | const result_center = centerLocation(position, boxWidth, boxHeight, width, height) 895 | result.x = result_center.x 896 | result.y = result_center.y 897 | break 898 | default: 899 | result.x = (boxWidth - width) * 0.5 900 | result.y = (boxHeight - height) * 0.5 901 | } 902 | result.rotate = parseInt(rotate) 903 | return result 904 | 905 | function location (position, boxWidth, boxHeight, width, height) { 906 | const result = { 907 | x: 0, 908 | y: 0 909 | } 910 | if (position.xAxis.derection === 'left') { 911 | result.x = position.xAxis.offset * boxWidth 912 | } 913 | if (position.xAxis.derection === 'right') { 914 | result.x = boxWidth * (1 - position.xAxis.offset) - width 915 | } 916 | if (position.yAxis.derection === 'top') { 917 | result.y = position.yAxis.offset * boxHeight 918 | } 919 | if (position.yAxis.derection === 'bottom') { 920 | result.y = boxHeight * (1 - position.yAxis.offset) - height 921 | } 922 | return result 923 | } 924 | // 中心点设置位置 925 | function centerLocation (position, boxWidth, boxHeight, width, height) { 926 | const result = { 927 | x: 0, 928 | y: 0 929 | } 930 | if (position.xAxis.derection === 'left') { 931 | result.x = position.xAxis.offset * boxWidth - width * 0.5 932 | } 933 | if (position.xAxis.derection === 'right') { 934 | result.x = boxWidth * (1 - position.xAxis.offset) - width * 0.5 935 | } 936 | if (position.yAxis.derection === 'top') { 937 | result.y = position.yAxis.offset * boxHeight - height * 0.5 938 | } 939 | if (position.yAxis.derection === 'bottom') { 940 | result.y = boxHeight * (1 - position.yAxis.offset) - height * 0.5 941 | } 942 | return result 943 | } 944 | } 945 | // 缓存贴纸信息 946 | storeCoverInfo = (sticker) => { 947 | const {currentScene} = this.state 948 | const clone_cover = tool.deepClone(sticker) 949 | // 贴纸存储不激活状态 950 | clone_cover.isActive = false 951 | const sceneId = currentScene.sceneId || 'demo_scene' 952 | const cacheKey = `${sceneId}_${sticker.id}` 953 | this.cache['cover'].set(cacheKey, clone_cover) 954 | } 955 | 956 | render () { 957 | const { global } = this.props 958 | const { rawImage, frame, foreground, coverList, sceneList, currentScene, result, canvas } = this.state 959 | // console.log('state sceneList', sceneList) 960 | return ( 961 | 962 | 967 | } 968 | >马卡龙玩图 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 982 | 983 | 993 | {coverList.map(item => { 994 | return 1004 | })} 1005 | 1006 | 1007 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1021 | 1022 | {result.show && 1023 | 1027 | } 1028 | 1029 | ) 1030 | } 1031 | } 1032 | 1033 | export default Dynamic as ComponentClass 1034 | -------------------------------------------------------------------------------- /src/pages/dynamic/mock_theme_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseCode": "0000", 3 | "responseMsg": "成功", 4 | "result": { 5 | "themeId": "190068240733376512", 6 | "styleId": "72", 7 | "shareTitle": "马卡龙定制杂志封面", 8 | "shareContent": "咻…咻咻!快来看我的时尚杂志大片", 9 | "shareUrl": "", 10 | "themeName": "杂志风", 11 | "bgUrl": "", 12 | "indexBgUrl": "", 13 | "videoUrl": "", 14 | "clueUrl": "", 15 | "buttonName": "杂志风格", 16 | "url": "https://static01.versa-ai.com/upload/prod/image/ca58a9bc-1357-42aa-87dd-96b8859cdcb7.jpg;https://static01.versa-ai.com/upload/prod/image/3fed1391-083d-43a6-8067-a03915beb111.jpg;https://static01.versa-ai.com/upload/prod/image/6ab7627d-26a2-4b17-a877-430e0ae90589.jpg;https://static01.versa-ai.com/upload/prod/image/47786454-bd05-481a-a1dc-08dec57c4c51.jpg;https://static01.versa-ai.com/upload/prod/image/21c1ff37-412e-47c6-a19b-a095e7107d54.png;https://static01.versa-ai.com/upload/prod/image/df723daf-9940-4e30-9157-e18d0132acd5.png", 17 | "sceneList": [{ 18 | "sceneId": "173404247591686144", 19 | "sceneName": "时尚ICON", 20 | "shareContent": "你这么好看,vermero杂志找你上封面~", 21 | "bgUrl": "https://static01.versa-ai.com/upload/prod/image/26ab6959-446c-4360-9b8d-4c5b30fe1ca4.jpg", 22 | "thumbnailUrl": "https://static01.versa-ai.com/upload/prod/image/98e60bf6-0c56-410d-8389-bd044b6782de.jpg", 23 | "filterUrl": "", 24 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{}},\"music\":{},\"fuse\":{},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.85\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":true,\"list\":[{\"id\":1532617477826,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/62b1eef5-7bac-4865-9ef5-087a3221ccac.png\",\"zIndex\":3,\"fixed\":true,\"size\":{\"default\":0.9,\"zoomInMax\":0.9,\"zoomOutMin\":0.9},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.5},\"yAxis\":{\"derection\":\"top\",\"offset\":0.1}}},{\"id\":1532618253152,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/55a21db8-db6b-47a6-8518-edb76ab962d9.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.3,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.55},\"yAxis\":{\"derection\":\"top\",\"offset\":0.21}}},{\"id\":1532618316806,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/d5816683-14f7-42c5-9aa5-8902f08fb4c5.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.11,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.7},\"yAxis\":{\"derection\":\"top\",\"offset\":0.39}}},{\"id\":1532625053253,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/d58f6fec-ab3f-4327-8e3f-27fcc4fc4ed2.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.24,\"zoomInMax\":0.24,\"zoomOutMin\":0.24},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.17},\"yAxis\":{\"derection\":\"top\",\"offset\":0.65}}},{\"id\":1532625112109,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/b118a901-f359-4515-b4cc-d8aad0242167.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.24,\"zoomInMax\":0.24,\"zoomOutMin\":0.24},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.18},\"yAxis\":{\"derection\":\"top\",\"offset\":0.4}}},{\"id\":1532625180019,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/d5f9dd3a-975d-4d33-bc91-26c44f508e43.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.4,\"zoomInMax\":0.4,\"zoomOutMin\":0.4},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.26},\"yAxis\":{\"derection\":\"top\",\"offset\":0.88}}}]}}", 25 | "sort": 3, 26 | "segmentType": 0, 27 | "bgZIndex": 0.0, 28 | "segmentZIndex": 1.0, 29 | "sceneType": 0 30 | }, { 31 | "sceneId": "190139721026834432", 32 | "sceneName": "单身青年", 33 | "shareContent": "单身原因是加班?加班单身青年也上封面了~", 34 | "bgUrl": "https://static01.versa-ai.com/upload/prod/image/2b2510a4-a137-4753-8f8c-004b266225d1.jpg", 35 | "thumbnailUrl": "https://static01.versa-ai.com/upload/prod/image/2956b669-3a06-49fe-b653-2454bf63569e.jpg", 36 | "filterUrl": "", 37 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{}},\"music\":{},\"fuse\":{\"support\":false},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.35},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.81\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":true,\"list\":[{\"id\":1536308557647,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/a5843482-9291-400a-8db2-150ae1882ae2.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.42,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.35},\"yAxis\":{\"derection\":\"top\",\"offset\":0.39}}},{\"id\":1536308603653,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/f4931d22-ea83-4ee0-82c7-b964fa716f89.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.29,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.35},\"yAxis\":{\"derection\":\"top\",\"offset\":0.24}}},{\"id\":1536308665978,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/f50492bf-6447-42f4-8e10-f76207c70d70.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.9,\"zoomInMax\":1,\"zoomOutMin\":1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.5},\"yAxis\":{\"derection\":\"top\",\"offset\":0.1}}},{\"id\":1536308726326,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/af366d6e-f8fe-4e15-8e7a-58d39a283729.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.43,\"zoomInMax\":0.43,\"zoomOutMin\":0.43},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.23},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0.15}}},{\"id\":1536308802069,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/3eb19645-36c3-4b1a-9472-f30abd9356cd.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.38,\"zoomInMax\":0.38,\"zoomOutMin\":0.38},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"right\",\"offset\":0.23},\"yAxis\":{\"derection\":\"top\",\"offset\":0.35}}}]}}", 38 | "sort": 1, 39 | "segmentType": 0, 40 | "bgZIndex": 0.0, 41 | "segmentZIndex": 1.0, 42 | "sceneType": 0 43 | }, { 44 | "sceneId": "173409565327429632", 45 | "sceneName": "萌娃闹钟", 46 | "shareContent": "带娃辛苦了,给你嘉奖上封面吧~", 47 | "bgUrl": "https://static01.versa-ai.com/upload/prod/image/1cd1e301-0a7c-4df8-94fb-e4f29b170008.jpg", 48 | "thumbnailUrl": "https://static01.versa-ai.com/upload/prod/image/002a5bf4-6cac-4d22-ac65-6f53d80af9a3.jpg", 49 | "filterUrl": "", 50 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{}},\"music\":{},\"fuse\":{},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.85\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":true,\"list\":[{\"id\":1532622831385,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/293a98a6-9704-4a36-b7fa-5f56fce404c8.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.3,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.58},\"yAxis\":{\"derection\":\"top\",\"offset\":0.2}}},{\"id\":1532622920681,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/6ab9acff-65dd-4b70-a7e6-d00140060a29.png\",\"zIndex\":3,\"fixed\":false,\"size\":{\"default\":0.37,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.5},\"yAxis\":{\"derection\":\"top\",\"offset\":0.6}}},{\"id\":1532623044014,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/54e16a31-6403-4a0c-a1f0-d9227efc4e04.png\",\"zIndex\":3,\"fixed\":true,\"size\":{\"default\":0.9,\"zoomInMax\":0.9,\"zoomOutMin\":0.9},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.5},\"yAxis\":{\"derection\":\"top\",\"offset\":0.1}}},{\"id\":1532625278384,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/5dc73a94-a47f-4db9-9a02-af8cb73efd85.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.3,\"zoomInMax\":0.3,\"zoomOutMin\":0.3},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"right\",\"offset\":0.19},\"yAxis\":{\"derection\":\"top\",\"offset\":0.41}}},{\"id\":1532625353886,\"imageUrl\":\"https://static01.versa-ai.com/upload/prod/image/e82becec-7736-4241-9ed7-503a91c77d59.png\",\"zIndex\":2,\"fixed\":true,\"size\":{\"default\":0.4,\"zoomInMax\":0.4,\"zoomOutMin\":0.4},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.23},\"yAxis\":{\"derection\":\"top\",\"offset\":0.88}}}]}}", 51 | "sort": 2, 52 | "segmentType": 0, 53 | "bgZIndex": 0.0, 54 | "segmentZIndex": 1.0, 55 | "sceneType": 0 56 | }], 57 | "stylePicUrl": "http://static01.versa-ai.com/upload/prod/image/style/b2094c24-181e-4eae-9a70-ccabcf34dc42.png", 58 | "columnId": "235791086641942528", 59 | "topShowUrl": "", 60 | "generalShowUrl": "https://static01.versa-ai.com/upload/69c696ba8cad/e6213578-0581-4ac9-8377-ac9c320b1acb.png", 61 | "themeSort": 4, 62 | "sceneType": 0 63 | }, 64 | "success": true 65 | } -------------------------------------------------------------------------------- /src/pages/dynamic/mock_theme_dynamic_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseCode": "0000", 3 | "responseMsg": "成功", 4 | "result": { 5 | "themeId": "230804512351129600", 6 | "styleId": "135", 7 | "shareTitle": "一切好玩,尽在马卡龙", 8 | "shareContent": "\uD83D\uDC37年吼吼,可爱会动的猪年壁纸全在这里啦!", 9 | "shareUrl": "", 10 | "themeName": "猪年限定", 11 | "bgUrl": "", 12 | "indexBgUrl": "", 13 | "videoUrl": "", 14 | "clueUrl": "", 15 | "buttonName": "猪年壁纸", 16 | "url": "https://static01.versa-ai.com/upload/c7aa5a4491e1/c2942184-9b53-4ebc-9f5a-642ae76490cf.jpg", 17 | "sceneList": [{ 18 | "sceneId": "230806273111560192", 19 | "sceneName": "鸿运新年", 20 | "shareContent": "新年快乐,好运通通来:D", 21 | "bgUrl": "https://static01.versa-ai.com/upload/4f8288d3f363/edf3dbb8-7d27-4328-8942-001b0d5b6829.gif", 22 | "thumbnailUrl": "https://static01.versa-ai.com/upload/ac0a30d7d98b/690d5fff-7da7-4398-ba93-9867ad7b55f1.jpg", 23 | "filterUrl": "", 24 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{\"axis\":\"x\",\"size\":1}},\"music\":{\"fileUrl\":\"https://static01.versa-ai.com/upload/94fbc207aa56/4905dae6-b6fe-4c1b-90c7-89449f851bbc.mp3\"},\"fuse\":{\"support\":false},\"position\":{\"place\":\"0\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.68\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":false,\"list\":[]}}", 25 | "sort": 1, 26 | "segmentType": 0, 27 | "bgZIndex": 0.0, 28 | "segmentZIndex": 1.0, 29 | "sceneType": 2 30 | }, { 31 | "sceneId": "231121322858450944", 32 | "sceneName": "男神祈愿", 33 | "shareContent": "你的男神,到底是哪一位?", 34 | "bgUrl": "https://static01.versa-ai.com/upload/96985fe52893/815c8b9e-ad3b-4b0c-8428-2ff4d77bb081.gif", 35 | "thumbnailUrl": "https://static01.versa-ai.com/upload/604627ef9899/1dfa0561-736b-4bba-b20e-4f6c5dda7aef.jpg", 36 | "filterUrl": "", 37 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{\"axis\":\"x\",\"size\":1}},\"music\":{\"fileUrl\":\"https://static01.versa-ai.com/upload/79f86c3a71dd/89acbaac-ae55-4b33-9626-9567f9a2a8b4.mp3\"},\"fuse\":{\"support\":false},\"position\":{\"place\":\"0\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.68\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":false,\"list\":[]}}", 38 | "sort": 2, 39 | "segmentType": 0, 40 | "bgZIndex": 0.0, 41 | "segmentZIndex": 2.0, 42 | "sceneType": 2 43 | }, { 44 | "sceneId": "231122266736234496", 45 | "sceneName": "猪年快落", 46 | "shareContent": "\uD83D\uDC37你一切顺遂~", 47 | "bgUrl": "https://static01.versa-ai.com/upload/90362c8416c9/80121eb2-7850-490d-baf3-3cf657efd44f.gif", 48 | "thumbnailUrl": "https://static01.versa-ai.com/upload/c94148842987/1bf92555-6eba-4f93-a157-05373b4a5a2b.jpg", 49 | "filterUrl": "", 50 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{\"axis\":\"x\",\"size\":1}},\"music\":{\"fileUrl\":\"https://static01.versa-ai.com/upload/222c2ff650c6/181fdcc2-7cb0-4350-a40d-585ee2c20004.mp3\"},\"fuse\":{\"support\":false},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.68\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":true,\"list\":[{\"id\":1547193898836,\"imageUrl\":\"https://static01.versa-ai.com/upload/4964bc161887/2ca7e76d-deef-4575-acc9-b6d7ae74b471.jpg\",\"zIndex\":3,\"fixed\":false,\"isActive\":false,\"size\":{\"default\":0.5,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"2\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"top\",\"offset\":0}}}]}}", 51 | "sort": 3, 52 | "segmentType": 0, 53 | "bgZIndex": 0.0, 54 | "segmentZIndex": 2.0, 55 | "sceneType": 2 56 | }, { 57 | "sceneId": "230807025443868672", 58 | "sceneName": "快乐腊肠猪", 59 | "shareContent": "一排排juju飞过,留下的全是对你的新祝愿!", 60 | "bgUrl": "https://static01.versa-ai.com/upload/6217d3903e41/e53cd91c-e09e-4b31-abc0-51029b5e1d1f.gif", 61 | "thumbnailUrl": "https://static01.versa-ai.com/upload/a78f7f53a286/40bfedc7-c22f-4e4a-8207-9905e30abd8c.jpg", 62 | "filterUrl": "", 63 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{\"axis\":\"x\",\"size\":1}},\"music\":{\"fileUrl\":\"https://static01.versa-ai.com/upload/37f7e52076eb/21627a04-6322-4c0f-bc5b-1d2e823827ba.mp3\"},\"fuse\":{\"support\":false},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.7\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":true,\"list\":[{\"id\":1547194102537,\"imageUrl\":\"https://static01.versa-ai.com/upload/eeb467903332/4b997ddf-cc55-48fc-b36f-c0d1c43f0ff4.jpg\",\"zIndex\":3,\"fixed\":true,\"isActive\":false,\"size\":{\"default\":0.28,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":\"180\",\"position\":{\"place\":\"9\",\"xAxis\":{\"derection\":\"right\",\"offset\":0.08},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}}},{\"id\":1547194149373,\"imageUrl\":\"https://static01.versa-ai.com/upload/05c99919516d/f7d04718-6c67-4607-81f4-76fe8c49ee27.jpg\",\"zIndex\":3,\"fixed\":false,\"isActive\":false,\"size\":{\"default\":0.34,\"zoomInMax\":1,\"zoomOutMin\":0.1},\"rotate\":0,\"position\":{\"place\":\"10\",\"xAxis\":{\"derection\":\"left\",\"offset\":0.11},\"yAxis\":{\"derection\":\"top\",\"offset\":0.2}}}]}}", 64 | "sort": 4, 65 | "segmentType": 0, 66 | "bgZIndex": 0.0, 67 | "segmentZIndex": 1.0, 68 | "sceneType": 2 69 | }, { 70 | "sceneId": "230807397633822720", 71 | "sceneName": "朋克飞天猪", 72 | "shareContent": "囧佳佳!跟我一起旋转飞天!", 73 | "bgUrl": "https://static01.versa-ai.com/upload/4d50c3a8296c/f9307d55-6816-4e4c-9a6a-6881f7b8cfee.gif", 74 | "thumbnailUrl": "https://static01.versa-ai.com/upload/2888ef4bae2e/18e4d869-b3c0-46f1-a13b-60a6b7ee5230.jpg", 75 | "filterUrl": "", 76 | "sceneConfig": "{\"filter\":{\"imageUrls\":[],\"position\":{\"axis\":\"x\",\"size\":1}},\"music\":{\"fileUrl\":\"https://static01.versa-ai.com/upload/9a12a660052d/b28addfc-00b1-43cf-b8fb-74bf7a0d9d47.mp3\"},\"fuse\":{\"support\":false},\"position\":{\"place\":\"6\",\"xAxis\":{\"derection\":\"left\",\"offset\":0},\"yAxis\":{\"derection\":\"bottom\",\"offset\":0}},\"size\":{\"default\":\"0.68\",\"zoomInMax\":1,\"zoomOutMin\":\"0.1\"},\"rotate\":0,\"text\":{\"support\":false,\"defaultText\":\"\",\"zIndex\":1,\"bgColor\":\"\",\"textColor\":\"\",\"fontSize\":15,\"bottom\":10},\"cover\":{\"support\":false,\"list\":[]}}", 77 | "sort": 5, 78 | "segmentType": 0, 79 | "bgZIndex": 0.0, 80 | "segmentZIndex": 2.0, 81 | "sceneType": 2 82 | }], 83 | "stylePicUrl": "http://static01.versa-ai.com/upload/prod/image/style/9300b813-37af-47e2-a5d1-58cbe95b9775.jpg", 84 | "columnId": "190064313619124224", 85 | "topShowUrl": "", 86 | "generalShowUrl": "https://static01.versa-ai.com/upload/17e7fe6ddcd8/5c016905-ab98-4d30-bc67-9ea12c222372.png", 87 | "themeSort": 1, 88 | "sceneType": 2 89 | }, 90 | "success": true 91 | } -------------------------------------------------------------------------------- /src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | .page-home { 2 | width:100%; 3 | height:100%; 4 | box-sizing:border-box; 5 | position:relative; 6 | background: #fff; 7 | .main { 8 | width:100%; 9 | height:100%; 10 | position:relative; 11 | overflow:auto; 12 | .main-bg { 13 | width:100%; 14 | height:98%; 15 | position:absolute; 16 | top:0; 17 | left:0; 18 | z-index:0; 19 | } 20 | .main-container { 21 | position:relative; 22 | width:100%; 23 | box-sizing:border-box; 24 | padding:78% 60rpx 40rpx; 25 | .category-wrap { 26 | width:100%; 27 | margin-top:10rpx; 28 | display:flex; 29 | flex-direction:row; 30 | flex-wrap:wrap; 31 | justify-content:space-between; 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component, Config } from '@tarojs/taro' 3 | import { View, Button, Text } from '@tarojs/components' 4 | import { connect } from '@tarojs/redux' 5 | 6 | import globalData from '../../services/global_data' 7 | import { getSystemInfo } from '../../model/actions/global' 8 | import './index.less' 9 | import bg from '../../assets/images/bg.png' 10 | import mock_data from './mock.json' 11 | import Title from '../../components/Title' 12 | import CustomIcon from '../../components/Icon' 13 | import CategoryItem from '../../components/CategoryItem' 14 | import AuthModal from '../../components/AuthModal' 15 | 16 | 17 | // console.log('mock_data', mock_data) 18 | type PageStateProps = { 19 | counter: { 20 | num: number 21 | }, 22 | global: { 23 | system: object 24 | } 25 | } 26 | 27 | type PageDispatchProps = { 28 | getSystemInfo: (data:object) => void 29 | } 30 | 31 | type PageOwnProps = {} 32 | 33 | type PageState = { 34 | categoryList: Array 35 | } 36 | 37 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps 38 | 39 | interface Home { 40 | props: IProps; 41 | } 42 | 43 | @connect(({ counter, global }) => ({ 44 | counter, 45 | global 46 | }), (dispatch) => ({ 47 | getSystemInfo (data) { 48 | dispatch(getSystemInfo(data)) 49 | } 50 | })) 51 | class Home extends Component { 52 | config: Config = { 53 | navigationBarTitleText: '马卡龙玩图-taro' 54 | } 55 | 56 | state = { 57 | categoryList: [], 58 | showAuth: false 59 | } 60 | 61 | componentWillMount () { 62 | const {getSystemInfo} = this.props 63 | const systemInfo = Taro.getSystemInfoSync() 64 | getSystemInfo(systemInfo) 65 | 66 | const categoryList = this.getCategotyList(mock_data.result) 67 | this.setState({ 68 | categoryList 69 | }) 70 | } 71 | componentDidMount () { } 72 | componentWillReceiveProps (nextProps) { 73 | // console.log(this.props, nextProps) 74 | } 75 | 76 | componentWillUnmount () { } 77 | 78 | componentDidShow () { } 79 | 80 | componentDidHide () { } 81 | 82 | getCategotyList (data: Array) { 83 | const list = [] 84 | data.forEach(item => { 85 | (item.themeList || [] ).forEach(theme => { 86 | list.push(theme) 87 | }) 88 | }) 89 | return list 90 | } 91 | 92 | handleChooseTheme = (item: object) => { 93 | // console.log('handleChooseTheme', item) 94 | } 95 | 96 | handleGetUserInfo = (data) => { 97 | // console.log('handleGetUserInfo', data) 98 | const {detail: {userInfo}} = data 99 | if (userInfo) { 100 | globalData.userInfo = userInfo 101 | this.todo() 102 | } else { 103 | Taro.showToast({ 104 | title: '请授权', 105 | icon: 'success', 106 | duration: 2000 107 | }) 108 | } 109 | } 110 | 111 | todo = () => { 112 | this.showActionSheet((path)=>{ 113 | console.log('choosedImage', path) 114 | globalData.choosedImage = path 115 | Taro.redirectTo({ 116 | url: '/pages/dynamic/index' 117 | }) 118 | }) 119 | } 120 | 121 | showActionSheet = async (callback) => { 122 | const _this = this 123 | Taro.showActionSheet({ 124 | itemList: [ 125 | '拍摄人像照', 126 | '从相册选择带有人像的照片', 127 | ], 128 | success: function ({tapIndex}) { 129 | if (tapIndex === 0) { 130 | Taro.authorize({ 131 | scope: "scope.camera", 132 | }).then(res => { 133 | Taro.chooseImage({ 134 | count: 1, 135 | sourceType: ['camera'], 136 | sizeType: ['compressed '], 137 | }).then(({tempFilePaths: [path]}) => { 138 | typeof callback === 'function' && callback(path) 139 | }) 140 | }, err => { 141 | console.log('authorize err', err) 142 | Taro.getSetting().then(authSetting => { 143 | if (authSetting['scope.camera']) { 144 | } else { 145 | Taro.showModal({ 146 | title: '拍摄图片需要授权', 147 | content: '拍摄图片需要授权\n可以授权吗?', 148 | confirmText: "允许", 149 | cancelText: "拒绝", 150 | }).then(res => { 151 | if (res.confirm) { 152 | _this.showAuthModal(true) 153 | } 154 | }) 155 | } 156 | }) 157 | }) 158 | } else if (tapIndex === 1) { 159 | Taro.chooseImage({ 160 | count: 1, 161 | sourceType: ['album'], 162 | }).then(({tempFilePaths: [path]}) => { 163 | typeof callback === 'function' && callback(path) 164 | }) 165 | } 166 | } 167 | }) 168 | } 169 | 170 | showAuthModal = (flag = false) => { 171 | this.setState({ 172 | showAuth: flag 173 | }) 174 | } 175 | 176 | closeAuthModal = () => { 177 | this.setState({ 178 | showAuth: false 179 | }) 180 | } 181 | 182 | render () { 183 | const { global } = this.props 184 | const { categoryList, showAuth } = this.state 185 | return ( 186 | 187 | 191 | } 192 | >马卡龙玩图 193 | 194 | 195 | 196 | 197 | 198 | 199 | { 200 | categoryList.map(item => { 201 | return 207 | }) 208 | } 209 | 210 | 211 | 212 | {showAuth && } 213 | 214 | ) 215 | } 216 | } 217 | 218 | export default Home as ComponentClass 219 | -------------------------------------------------------------------------------- /src/pages/home/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseCode": "0000", 3 | "responseMsg": "成功", 4 | "result": [{ 5 | "columnId": "225592575938842624", 6 | "columanName": "圣诞套装", 7 | "themeList": [{ 8 | "themeId": "225595811529805824", 9 | "styleId": "478", 10 | "shareTitle": "呼唤你来玩圣诞~", 11 | "shareContent": "唯美圣诞,等你开启!", 12 | "shareUrl": "", 13 | "themeName": "圣诞套装", 14 | "bgUrl": "", 15 | "indexBgUrl": "", 16 | "videoUrl": "", 17 | "videoGifUrl": "", 18 | "clueUrl": "", 19 | "buttonName": "圣诞套装", 20 | "status": 1, 21 | "deleted": 0, 22 | "columnId": "225592575938842624", 23 | "topShowUrl": "", 24 | "generalShowUrl": "https://static01.versa-ai.com/upload/25625ed77208/43362035-d32b-4e87-ace4-d1e0a3c63fa2.png", 25 | "themeSort": 1, 26 | "sceneType": 1 27 | }, { 28 | "themeId": "153516911857815552", 29 | "activityId": "131078232380067840", 30 | "styleId": "466", 31 | "shareTitle": "一切有趣尽在马卡龙玩图", 32 | "shareContent": "一切有趣尽在马卡龙玩图", 33 | "shareUrl": "", 34 | "themeName": "平行世界", 35 | "bgUrl": "", 36 | "indexBgUrl": "", 37 | "videoUrl": "", 38 | "videoGifUrl": "", 39 | "clueUrl": "", 40 | "buttonName": "平行世界", 41 | "status": 1, 42 | "deleted": 0, 43 | "columnId": "225592575938842624", 44 | "topShowUrl": "", 45 | "generalShowUrl": "https://static01.versa-ai.com/upload/d04272d3fd29/0f58992b-7bf1-4540-a14f-16aed1fa460b.png", 46 | "themeSort": 2, 47 | "sceneType": 1 48 | }] 49 | }, { 50 | "columnId": "225627888010711040", 51 | "columanName": "杂志风格", 52 | "themeList": [] 53 | }, { 54 | "columnId": "189034360682106880", 55 | "columanName": "魔法穿越", 56 | "themeList": [{ 57 | "themeId": "189443144248250368", 58 | "activityId": "177436189463990272", 59 | "styleId": "478", 60 | "shareTitle": "马卡龙定制杂志封面", 61 | "shareContent": "咻…咻咻!快来看我的时尚杂志大片", 62 | "shareUrl": "", 63 | "themeName": "杂志风", 64 | "bgUrl": "", 65 | "indexBgUrl": "", 66 | "videoUrl": "", 67 | "videoGifUrl": "", 68 | "clueUrl": "", 69 | "buttonName": "杂志风格", 70 | "status": 1, 71 | "deleted": 0, 72 | "columnId": "189034360682106880", 73 | "topShowUrl": "", 74 | "generalShowUrl": "https://static01.versa-ai.com/upload/87dd5c9b0e2e/4f478679-d1cf-4fab-99e3-026b1cc4d496.png", 75 | "themeSort": 1, 76 | "sceneType": 0 77 | }, { 78 | "themeId": "189061533203746816", 79 | "activityId": "177436189463990272", 80 | "styleId": "473", 81 | "shareTitle": "甜蜜大头贴", 82 | "shareContent": "甜蜜大头魔幻秀", 83 | "shareUrl": "", 84 | "themeName": "动态背景测试", 85 | "bgUrl": "", 86 | "indexBgUrl": "", 87 | "videoUrl": "", 88 | "videoGifUrl": "", 89 | "clueUrl": "", 90 | "buttonName": "大头贴", 91 | "status": 1, 92 | "deleted": 0, 93 | "columnId": "189034360682106880", 94 | "topShowUrl": "", 95 | "generalShowUrl": "https://static01.versa-ai.com/upload/a6720e3a3dce/8f91b63b-6e2b-4c93-b7b0-e23b57e72101.png", 96 | "themeSort": 2, 97 | "sceneType": 2 98 | }] 99 | }], 100 | "success": true 101 | } -------------------------------------------------------------------------------- /src/pages/index/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryChen0506/taro-makaron-demo/7314667c120dd2c6ff19a7b1464dc2f6be4490c9/src/pages/index/index.less -------------------------------------------------------------------------------- /src/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from 'react' 2 | import Taro, { Component, Config } from '@tarojs/taro' 3 | import { View, Button, Text } from '@tarojs/components' 4 | import { connect } from '@tarojs/redux' 5 | 6 | import { add, minus, asyncAdd } from '../../model/actions/counter' 7 | 8 | import './index.less' 9 | 10 | // #region 书写注意 11 | // 12 | // 目前 typescript 版本还无法在装饰器模式下将 Props 注入到 Taro.Component 中的 props 属性 13 | // 需要显示声明 connect 的参数类型并通过 interface 的方式指定 Taro.Component 子类的 props 14 | // 这样才能完成类型检查和 IDE 的自动提示 15 | // 使用函数模式则无此限制 16 | // ref: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20796 17 | // 18 | // #endregion 19 | 20 | type PageStateProps = { 21 | counter: { 22 | num: number 23 | } 24 | } 25 | 26 | type PageDispatchProps = { 27 | add: () => void 28 | dec: () => void 29 | asyncAdd: () => any 30 | } 31 | 32 | type PageOwnProps = {} 33 | 34 | type PageState = {} 35 | 36 | type IProps = PageStateProps & PageDispatchProps & PageOwnProps 37 | 38 | interface Index { 39 | props: IProps; 40 | } 41 | 42 | @connect(({ counter }) => ({ 43 | counter 44 | }), (dispatch) => ({ 45 | add () { 46 | dispatch(add()) 47 | }, 48 | dec () { 49 | dispatch(minus()) 50 | }, 51 | asyncAdd () { 52 | dispatch(asyncAdd()) 53 | } 54 | })) 55 | class Index extends Component { 56 | 57 | /** 58 | * 指定config的类型声明为: Taro.Config 59 | * 60 | * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型 61 | * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string 62 | * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型 63 | */ 64 | config: Config = { 65 | navigationBarTitleText: '首页' 66 | } 67 | 68 | componentWillReceiveProps (nextProps) { 69 | // console.log(this.props, nextProps) 70 | } 71 | 72 | componentWillUnmount () { } 73 | 74 | componentDidShow () { } 75 | 76 | componentDidHide () { } 77 | 78 | render () { 79 | return ( 80 | 81 | 82 | 83 | 84 | {this.props.counter.num} 85 | Hello, World 86 | 87 | ) 88 | } 89 | } 90 | 91 | // #region 导出注意 92 | // 93 | // 经过上面的声明后需要将导出的 Taro.Component 子类修改为子类本身的 props 属性 94 | // 这样在使用这个子类时 Ts 才不会提示缺少 JSX 类型参数错误 95 | // 96 | // #endregion 97 | 98 | export default Index as ComponentClass 99 | -------------------------------------------------------------------------------- /src/services/api.config.ts: -------------------------------------------------------------------------------- 1 | // host域名管理 2 | import {ENV} from './config' 3 | const host = { 4 | miniapi: { 5 | dev: 'https://abc.xxx.com', 6 | prod: 'https://abc.xxx.com' 7 | }, 8 | upload: { 9 | dev: 'https://abc.xxx.com', 10 | prod: 'https://abc.xxx.com' 11 | }, 12 | download: { 13 | dev: 'https://abc.xxx.com', 14 | prod: 'https://abc.xxx.com' 15 | } 16 | } 17 | 18 | // 获取域名 19 | function getHost (type = 'miniapi', ENV = 'dev') { 20 | return host[type][ENV] 21 | } 22 | export const api = { 23 | base: { 24 | uploadToken: `${getHost('miniapi', ENV)}/xxx`, 25 | upload: `${getHost('miniapi', ENV)}/xxx`, 26 | auth: `${getHost('miniapi', ENV)}/xxx`, 27 | }, 28 | core: { 29 | segment: `${getHost('miniapi', ENV)}/xxx`, 30 | column: `${getHost('miniapi', ENV)}/xxx` 31 | } 32 | } 33 | export default { 34 | name: 'api-config', 35 | api, 36 | } -------------------------------------------------------------------------------- /src/services/cache.ts: -------------------------------------------------------------------------------- 1 | // 缓存服务 2 | function Cache (name) { 3 | this.name = name 4 | } 5 | Cache.prototype = { 6 | set: function (key, value) { 7 | this[key] = value 8 | return this[key] 9 | }, 10 | get: function (key) { 11 | return this[key] 12 | }, 13 | clear: function () { 14 | // 清空 15 | Object.keys(this).forEach(v => { 16 | this[v] = undefined 17 | }) 18 | } 19 | } 20 | 21 | export const createCache = (name:string) => { 22 | return new Cache(name) 23 | } 24 | 25 | export const cacheCover = new Cache('cover') 26 | export default { 27 | createCache, 28 | cacheCover 29 | } -------------------------------------------------------------------------------- /src/services/config.ts: -------------------------------------------------------------------------------- 1 | // 配置文件 2 | export const ENV:string = 'dev' // 'dev' 测试 3 | // export const ENV = 'prod' // 'prod' 生产 4 | export const appId:string = 'wxcfe56965f4d986f0' 5 | export const appConfig:object = { 6 | image_oss_postfix: '?x-oss-process=image/resize,h_748,w_560', 7 | } 8 | export default { 9 | ENV, 10 | appId: appId, 11 | appConfig: appConfig 12 | } -------------------------------------------------------------------------------- /src/services/global_data.ts: -------------------------------------------------------------------------------- 1 | // 全局对象 单例 2 | 3 | interface GlobalProps { 4 | name: string 5 | } 6 | 7 | class GlobalObj implements GlobalProps { 8 | name: string; 9 | constructor (name) { 10 | this.name = name 11 | } 12 | } 13 | 14 | export const createGlobalObj = (name:string) => { 15 | return new GlobalObj(name) 16 | } 17 | 18 | const globalData = createGlobalObj('global object') 19 | export default globalData -------------------------------------------------------------------------------- /src/services/http.ts: -------------------------------------------------------------------------------- 1 | // import qs from 'qs' 2 | import tool from '../utils/tool' 3 | import pathToRegexp from 'path-to-regexp' 4 | import Taro from '@tarojs/taro' 5 | import Session from '../services/session' 6 | 7 | const fetch = (options) => { 8 | let { 9 | method = 'get', 10 | header = {}, 11 | data, 12 | url, 13 | params, // params用来填充url, 如pathToRegexp.compile('/user/:id')({id: 123}) 14 | } = options 15 | const cloneData = tool.deepClone(data) 16 | try { 17 | let domain = '' 18 | if (url.match(/[a-zA-z]+:\/\/[^/]*/)) { 19 | [domain] = url.match(/[a-zA-z]+:\/\/[^/]*/) 20 | url = url.slice(domain.length) 21 | } 22 | url = pathToRegexp.compile(url)(params) 23 | url = domain + url 24 | } catch (e) { 25 | console.log('pathToRegexp error', e) 26 | } 27 | 28 | return Taro.request({ 29 | url: url, 30 | data:cloneData, 31 | header: { 32 | 'content-type': 'application/json', 33 | ...header 34 | }, 35 | method: method.toUpperCase(), 36 | }) 37 | } 38 | 39 | // 普通请求 40 | export const commonRequest = (options) => { 41 | return fetch(options) 42 | .then((response:any) => { 43 | // console.log('request fetch', response) 44 | const { statusCode, data, errMsg } = response 45 | if (statusCode !== 200) { 46 | return Promise.reject({ 47 | status: 'error', 48 | statusCode: statusCode, 49 | result: data, 50 | message: data.responseMsg || errMsg, 51 | }) 52 | } else { 53 | if ( data.responseCode === '40001') { 54 | return Promise.reject({ 55 | status: 'error', 56 | statusCode: statusCode, 57 | result: data, 58 | message: data.responseMsg || errMsg, 59 | }) 60 | } else { 61 | return Promise.resolve({ 62 | status: 'success', 63 | statusCode: statusCode, 64 | result: data, 65 | message: data.responseMsg || '成功', 66 | }) 67 | } 68 | } 69 | }).catch((error) => { 70 | // console.log('request catch', error) 71 | const {message, result} = error 72 | let msg 73 | let statusCode 74 | if (result && result instanceof Object) { 75 | statusCode = result.status || error.statusCode 76 | msg = result.message || message 77 | } else { 78 | statusCode = 600 79 | msg = message || 'Network Error' 80 | } 81 | /* eslint-disable */ 82 | return Promise.reject({ status: 'error', statusCode, message: msg }) 83 | }) 84 | } 85 | 86 | // 定制请求 87 | export const request = (options) => { 88 | options = Object.assign({}, options) 89 | if (!options.data) { 90 | options.data = {} 91 | } 92 | const session = Session.get() 93 | options.data[Session.headerKey] = session 94 | options.data['deviceId'] = tool.getDeviceId() 95 | 96 | return fetch(options) 97 | .then(async (response:any) => { 98 | const { statusCode, data, errMsg } = response 99 | if (statusCode !== 200) { 100 | return Promise.reject({ 101 | status: 'error', 102 | statusCode: statusCode, 103 | result: data, 104 | message: data.responseMsg || errMsg, 105 | }) 106 | } else { 107 | if ( data.responseCode === '40001') { 108 | return continueSessionRequest(options) 109 | } else { 110 | return Promise.resolve({ 111 | status: 'success', 112 | statusCode: statusCode, 113 | result: data, 114 | message: data.responseMsg || '成功', 115 | }) 116 | } 117 | } 118 | }).catch((error) => { 119 | const {message, result} = error 120 | let msg 121 | let statusCode 122 | if (result && result instanceof Object) { 123 | statusCode = result.status || error.statusCode 124 | msg = result.message || message 125 | } else { 126 | statusCode = 600 127 | msg = message || 'Network Error' 128 | } 129 | return Promise.reject({ status: 'error', statusCode, message: msg }) 130 | }) 131 | } 132 | 133 | // 续session请求 134 | async function continueSessionRequest (options:object = {}) { 135 | // 最多执行2次 136 | let rejectData = {} 137 | for (let i = 0; i < 2; i++) { 138 | Session.remove() 139 | const session = await Session.set() 140 | console.log('续session次数:' + i, session) 141 | options.data[Session.headerKey] = session 142 | let result = await fetch(options) 143 | const {data, statusCode} = result 144 | if (statusCode === 200 && data.responseCode !== '40001') { 145 | return Promise.resolve({ 146 | status: 'success', 147 | statusCode: statusCode, 148 | result: data, 149 | message: data.responseMsg || '成功', 150 | }) 151 | } 152 | rejectData = { 153 | status: 'error', 154 | statusCode: statusCode, 155 | result: data, 156 | message: data.responseMsg, 157 | } 158 | } 159 | console.log('续session仍然失败!!!') 160 | return Promise.reject(rejectData) 161 | } 162 | 163 | export default { 164 | commonRequest, 165 | request 166 | } -------------------------------------------------------------------------------- /src/services/service.ts: -------------------------------------------------------------------------------- 1 | // http服务 2 | import Taro from '@tarojs/taro' 3 | import {commonRequest, request} from './http' 4 | import { api } from './api.config' 5 | import tool from '../utils/tool' 6 | 7 | interface segmentData { 8 | clientType: string; 9 | timestamp: string; 10 | imageUrl: string; 11 | segmentType?: string; 12 | } 13 | 14 | export const base = { 15 | uploadToken: function () { 16 | return request({ 17 | url: api.base.uploadToken, 18 | method: 'GET', 19 | dataType: 'json', 20 | data: { 21 | clientType: 'mini-program', 22 | fileType: 'image', 23 | filename: 'image.jpeg' 24 | } 25 | }) 26 | }, 27 | async getUploadToken () { 28 | let token = Taro.getStorageSync('token') 29 | if (token && token.expire > Date.now()) { 30 | return token 31 | } 32 | try { 33 | const data = await base.uploadToken() 34 | token = data && data.result && data.result.result 35 | Taro.setStorageSync('token', token) 36 | return token 37 | } catch (err) { 38 | console.log('get uploadToken fail', err) 39 | } 40 | }, 41 | async upload (localFilePath, type?:string) { 42 | // 上传图片 43 | let imageType = type || 'png' 44 | const token = await base.getUploadToken() 45 | const imgName = tool.createImgName(16) 46 | const prefix = token.prefix // 'upload/prod/image/' 47 | token.params.key = `${prefix}${imgName}.${imageType}` 48 | let {data} = await Taro.uploadFile({ 49 | filePath: localFilePath, 50 | name: 'file', 51 | url: token.host, 52 | formData: token.params 53 | }) 54 | if (typeof data === 'string') { 55 | try { 56 | let result = JSON.parse(data) 57 | result.host = 'https://static01.versa-ai.com/' 58 | result.url = result.host + result.picurl 59 | return result 60 | } catch (err) { 61 | console.log('upload image string parse to json fail !!!') 62 | } 63 | } 64 | return { 65 | host: '', 66 | picurl: '', 67 | url: '' 68 | } 69 | }, 70 | auth (data) { 71 | return commonRequest({ 72 | url: api.base.auth, 73 | method: 'POST', 74 | data: data 75 | }) 76 | }, 77 | downloadFile (url) { 78 | return Taro.downloadFile({url: url}) 79 | } 80 | } 81 | export const core = { 82 | segment: function (remoteImgUrl, segmentType) { 83 | let postData:segmentData = { 84 | clientType: 'mini-program', 85 | timestamp: Date.now().toString(), 86 | imageUrl: remoteImgUrl, 87 | } 88 | if (segmentType !== undefined) { 89 | postData.segmentType = segmentType 90 | } 91 | return request({ 92 | url: api.core.segment, 93 | method: 'POST', 94 | header: {'content-type': 'application/x-www-form-urlencoded'}, 95 | data: postData, 96 | }) 97 | }, 98 | column (data?:object) { 99 | return request({ 100 | url: api.core.column, 101 | method: 'GET', 102 | data: data, 103 | }) 104 | }, 105 | segmentDemo: function (rawImgUrl, resImgUrl, time = 100) { 106 | console.log('分割图片:', rawImgUrl) 107 | return new Promise((resolve) => { 108 | setTimeout(() => { 109 | resolve({ 110 | result: resImgUrl 111 | }) 112 | }, time) 113 | }) 114 | } 115 | } 116 | 117 | export default { 118 | base, 119 | core 120 | } 121 | -------------------------------------------------------------------------------- /src/services/session.ts: -------------------------------------------------------------------------------- 1 | // session管理 2 | import Taro from '@tarojs/taro' 3 | import service from './service' 4 | import config from './config' 5 | 6 | const Session = { 7 | storageKey: 'session', 8 | headerKey: 'sessionId', 9 | get () { 10 | return Taro.getStorageSync(this.storageKey) 11 | }, 12 | async set () { 13 | const loginInfo = await Taro.login() 14 | const {appId} = config 15 | const {code} = loginInfo 16 | try { 17 | const data = await service.base.auth({ 18 | code, appId 19 | }) 20 | const {statusCode, result} = data 21 | if (statusCode === 200 && result.responseCode === '0000') { 22 | Taro.setStorageSync(this.storageKey, result.result) 23 | return result.result 24 | } else { 25 | console.log('auth fail!(get sessionId)', data) 26 | } 27 | } catch (err) { 28 | console.log('auth fail!(get sessionId)', err) 29 | } 30 | }, 31 | remove () { 32 | Taro.removeStorageSync(this.storageKey) 33 | } 34 | } 35 | 36 | export default Session -------------------------------------------------------------------------------- /src/utils/tool.ts: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | 3 | /** 4 | * @description 深拷贝 5 | * @param {*} obj 目标对象 6 | * @return {*} 返回的深拷贝对象 7 | */ 8 | function deepClone (obj) { 9 | if (!obj || typeof obj !== 'object') { 10 | return obj 11 | } 12 | let objArray = Array.isArray(obj) ? [] : {} 13 | if (obj && typeof obj === 'object') { 14 | for (let key in obj) { 15 | if (obj.hasOwnProperty(key)) { 16 | // 如果obj的属性是对象,递归操作 17 | if (obj[key] && typeof obj[key] === 'object') { 18 | objArray[key] = deepClone(obj[key]) 19 | } else { 20 | objArray[key] = obj[key] 21 | } 22 | } 23 | } 24 | } 25 | return objArray 26 | } 27 | // 角度计算 28 | const getLen = function(v) { 29 | return Math.sqrt(v.x * v.x + v.y * v.y); 30 | } 31 | const dot = function (v1, v2) { 32 | return v1.x * v2.x + v1.y * v2.y; 33 | } 34 | const getAngle = function (v1, v2) { 35 | let mr = getLen(v1) * getLen(v2); 36 | if (mr === 0) return 0; 37 | let r = dot(v1, v2) / mr; 38 | if (r > 1) r = 1; 39 | return Math.acos(r); 40 | } 41 | const cross = function (v1, v2) { 42 | return v1.x * v2.y - v2.x * v1.y; 43 | } 44 | const getRotateAngle = function (v1, v2) { 45 | let angle = getAngle(v1, v2); 46 | if (cross(v1, v2) > 0) { 47 | angle *= -1; 48 | } 49 | return angle * 180 / Math.PI; 50 | } 51 | // 计算中心点坐标 52 | const calcCenterPosition = (offsetX, offsetY, width, height) => { 53 | return { 54 | x: offsetX + 0.5 * width, 55 | y: offsetY + 0.5 * height 56 | } 57 | } 58 | 59 | const tool = { 60 | uuid: function () { // 生产uuid 61 | const s:Array = [] 62 | const hexDigits = '0123456789abcdef' 63 | for (var i = 0; i < 36; i++) { 64 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 65 | } 66 | s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 67 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 68 | s[8] = s[13] = s[18] = s[23] = '-' 69 | const uuid = s.join('') 70 | return uuid 71 | }, 72 | getDeviceId: function () { 73 | if (!Taro.getStorageSync('deviceId')) { 74 | Taro.setStorageSync('deviceId', this.uuid()) 75 | } 76 | return Taro.getStorageSync('deviceId') 77 | }, 78 | createImgName: function (length = 32) { 79 | var s:Array = [] 80 | var hexDigits = '0123456789abcdef' 81 | for (var i = 0; i < length; i++) { 82 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 83 | } 84 | return s.join('') 85 | }, 86 | isEmpty: function (obj) { 87 | // 判断字符是否为空的方法 88 | if (typeof obj === 'undefined' || obj === null || obj === '') { 89 | return true 90 | } 91 | return false 92 | }, 93 | isRepeat: function (arr) { 94 | var hash = {} 95 | for (var i in arr) { 96 | if (hash[arr[i]]) { 97 | return true 98 | } 99 | hash[arr[i]] = true 100 | } 101 | return false 102 | }, 103 | deepClone: deepClone, 104 | getRotateAngle, 105 | calcCenterPosition, 106 | } 107 | 108 | export default tool 109 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "preserve", 19 | "jsxFactory": "Taro.createElement", 20 | "allowJs": true, 21 | "typeRoots": [ 22 | "node_modules/@types" 23 | ] 24 | }, 25 | "compileOnSave": false 26 | } 27 | --------------------------------------------------------------------------------