Repository: wechat-miniprogram/awesome-skyline Branch: main Commit: 4789c045ccb9 Files: 218 Total size: 335.4 KB Directory structure: gitextract_s27a10mg/ ├── .gitignore ├── LICENSE ├── README.md └── examples/ ├── address-book/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ ├── address-book/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── navigation-bar/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── pages/ │ │ └── index/ │ │ ├── data.js │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ └── sitemap.json ├── album/ │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ ├── album/ │ │ │ ├── album-image/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.wxss │ │ │ └── route.js │ │ ├── navigation-bar/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── previewer/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ ├── preview-home/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ ├── preview-image/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── preview-list/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── pages/ │ │ ├── album/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── preview/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ ├── sitemap.json │ └── utils/ │ ├── event-bus.js │ ├── store.js │ └── worklet.js ├── app-bar/ │ ├── .eslintrc.js │ ├── app-bar/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ └── navigation-bar/ │ │ ├── navigation-bar.js │ │ ├── navigation-bar.json │ │ ├── navigation-bar.wxml │ │ └── navigation-bar.wxss │ ├── pages/ │ │ ├── detail/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── index/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ └── sitemap.json ├── associated-scroll-view/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ └── category-list/ │ │ ├── category-list.js │ │ ├── category-list.json │ │ ├── category-list.wxml │ │ └── category-list.wxss │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ ├── sitemap.json │ └── util.js ├── card_transition/ │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ ├── card/ │ │ │ ├── card.js │ │ │ ├── card.json │ │ │ ├── card.wxml │ │ │ └── card.wxss │ │ └── navigation-bar/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── pages/ │ │ ├── detail/ │ │ │ ├── detail.js │ │ │ ├── detail.json │ │ │ ├── detail.wxml │ │ │ └── detail.wxss │ │ └── list/ │ │ ├── list.js │ │ ├── list.json │ │ ├── list.wxml │ │ ├── list.wxss │ │ ├── route.js │ │ └── utils.js │ ├── project.config.json │ ├── project.private.config.json │ └── sitemap.json ├── expanded-scroll-view/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ ├── sitemap.json │ └── util.js ├── half-screen/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── pages/ │ │ └── index/ │ │ ├── comment-data.js │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ └── sitemap.json ├── product-list/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ └── navigation-bar/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ ├── sitemap.json │ └── util.js ├── refresher-two-level/ │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ └── navigation-bar/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── goods/ │ │ ├── index.json │ │ ├── index.less │ │ ├── index.ts │ │ └── index.wxml │ ├── goods.less │ ├── goods.wxml │ ├── index/ │ │ ├── index.json │ │ ├── index.less │ │ ├── index.ts │ │ └── index.wxml │ ├── project.config.json │ └── util.js ├── segmented-half-screen/ │ ├── .eslintrc.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── pages/ │ │ └── index/ │ │ ├── comment-data.js │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── project.config.json │ ├── project.private.config.json │ └── sitemap.json └── tab-indicator/ ├── .eslintrc.js ├── app.js ├── app.json ├── app.wxss ├── components/ │ └── navigation-bar/ │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── pages/ │ └── index/ │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── project.private.config.json └── sitemap.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 wechat-miniprogram Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # awesome-skyline Skyline 是微信小程序推出的新渲染引擎,用于替代 WebView 渲染,其能够带来更好的渲染性能,并且添加了诸多增强特性,让小程序拥有更接近原生的交互体验。更多细节可查看[文档](https://developers.weixin.qq.com/miniprogram/dev/framework/runtime/skyline/introduction.html)。 ## 示例集 - [通讯录](./examples/address-book) - [相册](./examples/album) - [卡片转场](./examples/card_transition) - [半屏弹窗](./examples/half-screen) - [分段式半屏](./examples/segmented-half-screen) - [Tab 指示条](./examples/tab-indicator) - [搜索栏吸附](./examples/product-list) - [沉浸式商品浏览](./examples/expanded-scroll-view) - [分类列表联动](./examples/associated-scroll-view) ================================================ FILE: examples/address-book/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/address-book/app.js ================================================ // app.js App({}) ================================================ FILE: examples/address-book/app.json ================================================ { "pages": [ "pages/index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "lazyCodeLoading": "requiredComponents", "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/address-book/app.wxss ================================================ ================================================ FILE: examples/address-book/components/address-book/index.js ================================================ const throttle = function throttle(func, wait, options) { let context let args let result = void 0 let timeout let previous = 0 if (!options) options = {} const later = function later() { previous = options.leading === false ? 0 : Date.now() timeout = null result = func.apply(context, args) if (!timeout) context = args = null } return function () { const now = Date.now() if (!previous && options.leading === false) previous = now const remaining = wait - (now - previous) context = this args = arguments if (remaining <= 0 || remaining > wait) { clearTimeout(timeout) timeout = null previous = now result = func.apply(context, args) if (!timeout) context = args = null } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining) } return result } } Component({ behaviors: [], options: { addGlobalClass: false, virtualHost: true, pureDataPattern: /^_/, }, properties: { list: { type: Array, value: [], observer(newVal) { if (newVal.length === 0) return const alphabet = this.data.list.map((item, index) => { this.data._tops[index] = 2e10 return item.alpha }) this._sharedTops.value = this.data._tops this.setData({ alphabet, current: alphabet[0] }, () => { this.computedSize() }) }, }, }, data: { current: 'A', intoView: '', touching: false, alphabet: [], _vibrated: true, _tops: [], _anchorItemH: 0, _anchorItemW: 0, _anchorTop: 0, }, observers: { 'current': function (current) { this._sharedCurrentIdx.value = this.data.alphabet.indexOf(current) }, }, lifetimes: { created() { this._handlePan = throttle(this._handlePan, 100, {}) this._sharedTops = wx.worklet.shared([]) this._sharedScrollTop = wx.worklet.shared(0) this._sharedHeight = wx.worklet.shared(0) this._sharedCurrentIdx = wx.worklet.shared(0) }, attached() { // scroll-view 高度 this.createSelectorQuery().select('.scroll-view').boundingClientRect(res => { this._sharedHeight.value = res.height }).exec() // 右侧目录计算 const query = this.createSelectorQuery() query.select('.anchor-list').boundingClientRect(rect => { this.data._anchorItemH = rect.height / this.data.alphabet.length this.data._anchorItemW = rect.width this.data._anchorTop = rect.top }).exec() }, }, methods: { handlePan(e) { this._handlePan(e) }, _handlePan(e) { const data = this.data const clientY = e.changedTouches[0].clientY const index = Math.floor((clientY - data._anchorTop) / data._anchorItemH) const current = data.alphabet[index] if (current !== this.data.current) { wx.vibrateShort({ type: 'light' }) this.setData({ current, intoView: current, touching: true }) } }, cancelPan() { setTimeout(() => { this.setData({ touching: false }) }, 150) }, computedSize() { this.data.alphabet.forEach((element, index) => { // NOTE: 在 Skyline 下如果用了 list-view / grid-view 会有按需渲染特性,取其子节点的 clientRect // 时若不在屏会取不到,而这里是取 sticky-header 的,会立即渲染也就能立即返回,但 top 值是预估的。 // 因为 list-view 的高度是预估的(第一个子节点的高度 * 数量),由于其子节点是等高,故预估是基本准确的 this.createSelectorQuery().select(`#${element}`).boundingClientRect(res => { this.data._tops[index] = res.top this._sharedTops.value = this.data._tops }).exec() }) }, handleScroll(e) { 'worklet' const scrollTop = e.detail.scrollTop // 用于计算每个 header 的 offsetTop this._sharedScrollTop.value = scrollTop // 下面判断当前选中态,按需更新 const tops = this._sharedTops.value for (let i = tops.length - 1; i >= 0; i--) { // header 超过屏幕一半就改为选中态 if (scrollTop + this._sharedHeight.value / 2 > tops[i]) { if (i !== this._sharedCurrentIdx.value) { // worklet 函数运行在 UI 线程,setData 调用要抛回 JS 线程执行 wx.worklet.runOnJS(this.updateCurrent.bind(this))(i) } break } } }, updateCurrent(idx) { if (this.data.touching) return this.setData({ current: this.data.alphabet[idx] }) }, } }) ================================================ FILE: examples/address-book/components/address-book/index.json ================================================ { "component": true, "usingComponents": {}, "addGlobalClass": false, "componentFramework": "glass-easel" } ================================================ FILE: examples/address-book/components/address-book/index.wxml ================================================ {{item.alpha}} {{subItem.name}} {{alpha}} {{alpha}} ================================================ FILE: examples/address-book/components/address-book/index.wxss ================================================ .wx-flex { display: flex; align-items: center } .scroll-view { height: 100%; } .thin-border-bottom { position: relative } .thin-border-top { position: relative } .square-tag { color: #9a9a9a; text-align: center; height: 30px; line-height: 30px; box-sizing: border-box; border-radius: 2px; background-color: #f7f7f7; font-size: 12px; position: relative } .square-tag.selected { background: rgba(26, 173, 25, 0.1); color: #1AAD19 } .select-city__hd { padding: 0 15px; position: fixed; height: 50px; background-color: #fff; left: 0; right: 0; z-index: 990 } .current-city__name { display: inline-block; margin-right: 10px; margin-left: 11px } .city-group_part .city-group__title { padding-bottom: 12px } .city-group__title { padding: 12px 12px 11px } .square-tag { width: 100px; display: inline-block; margin-right: 12px; margin-bottom: 12px; height: 40px; line-height: 40px; font-size: 15px; color: #000; background-color: rgba(0, 0, 0, 0.02); overflow: hidden; white-space: nowrap; text-overflow: ellipsis } .city-group__item { padding: 15px 12px; font-size: 15px } .city-group_all { padding-bottom: 50px } .fixed__top { position: fixed; top: 0 } .anchor-bar__wrp { position: fixed; top: 0; bottom: 0; right: 0; width: 30px; z-index: 999 } .anchor-item { font-size: 0; text-align: center; position: relative } .anchor-item__inner { line-height: 10px; height: 14px; width: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; margin: 1px 0; font-weight: 500 } .tapped .anchor-item__pop { display: flex; } .anchor-item__pop { position: absolute; font-size: 50px; width: 55px; height: 55px; line-height: 45px; color: #fff; background-color: #C9C9C9; border-radius: 50%; border: 5px solid transparent; right: 40px; top: 50%; transform: translateY(-50%); display: none; box-sizing: border-box; align-items: center; justify-content: center; } .anchor-item__pop_after { position: absolute; width: 0; height: 0; left: 42px; border: 20px solid; border-color: transparent transparent transparent #C9C9C9; top: 50%; transform: translateY(-50%) } .anchor-item.selected .anchor-item__inner { color: #fff; background-color: #1aad19 } .right-directory { position: absolute; top: 0; right: 0; z-index: 1; width: 30px; height: 100vh; justify-content: center; flex-direction: column; } .anchor-bar { display: flex; flex-direction: column; align-items: center; width: 30px; } .tips-color { color: #5d5d5d; background-color: #EAEAEA; font-weight: bold; } sticky-header { position: sticky; top: 0; z-index: 1; display: block; } ================================================ FILE: examples/address-book/components/navigation-bar/index.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const rect = wx.getMenuButtonBoundingClientRect() wx.getSystemInfo({ success: (res) => { this.setData({ statusBarHeight: res.statusBarHeight, innerPaddingRight: `padding-right:${res.windowWidth - rect.left}px`, leftWidth: `width:${res.windowWidth - rect.left}px`, navBarHeight: rect.bottom + rect.top - res.statusBarHeight, }) } }) }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${show ? '1' : '0'};transition: opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data if (data.delta) { wx.navigateBack({ delta: data.delta }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/address-book/components/navigation-bar/index.json ================================================ { "component": true, "usingComponents": {}, "addGlobalClass": true, "componentFramework": "glass-easel" } ================================================ FILE: examples/address-book/components/navigation-bar/index.wxml ================================================ {{title}} ================================================ FILE: examples/address-book/components/navigation-bar/index.wxss ================================================ .weui-navigation-bar { overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; box-sizing: border-box; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; width: 95px; padding-left: 16px; box-sizing: border-box; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: bold; } @media(prefers-color-scheme: dark) { .weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } .weui-navigation-bar__inner { background-color: #1f1f1f; } } ================================================ FILE: examples/address-book/pages/index/data.js ================================================ export const cityData = [{ "fullname": "东城区", "id": "110101", "location": { "lat": 39.92855, "lng": 116.41637 }, "name": "东城", "pinyin": ["dong", "cheng"] }, { "fullname": "西城区", "id": "110102", "location": { "lat": 39.91231, "lng": 116.36611 }, "name": "西城", "pinyin": ["xi", "cheng"] }, { "fullname": "朝阳区", "id": "110105", "location": { "lat": 39.9219, "lng": 116.44355 }, "name": "朝阳", "pinyin": ["chao", "yang"] }, { "fullname": "丰台区", "id": "110106", "location": { "lat": 39.85856, "lng": 116.28616 }, "name": "丰台", "pinyin": ["feng", "tai"] }, { "fullname": "石景山区", "id": "110107", "location": { "lat": 39.90569, "lng": 116.22299 }, "name": "石景山", "pinyin": ["shi", "jing", "shan"] }, { "fullname": "海淀区", "id": "110108", "location": { "lat": 39.95933, "lng": 116.29845 }, "name": "海淀", "pinyin": ["hai", "dian"] }, { "fullname": "门头沟区", "id": "110109", "location": { "lat": 39.94048, "lng": 116.10146 }, "name": "门头沟", "pinyin": ["men", "tou", "gou"] }, { "fullname": "房山区", "id": "110111", "location": { "lat": 39.74788, "lng": 116.14294 }, "name": "房山", "pinyin": ["fang", "shan"] }, { "fullname": "通州区", "id": "110112", "location": { "lat": 39.90998, "lng": 116.65714 }, "name": "通州", "pinyin": ["tong", "zhou"] }, { "fullname": "顺义区", "id": "110113", "location": { "lat": 40.13012, "lng": 116.65477 }, "name": "顺义", "pinyin": ["shun", "yi"] }, { "fullname": "昌平区", "id": "110114", "location": { "lat": 40.22077, "lng": 116.23128 }, "name": "昌平", "pinyin": ["chang", "ping"] }, { "fullname": "大兴区", "id": "110115", "location": { "lat": 39.72684, "lng": 116.34159 }, "name": "大兴", "pinyin": ["da", "xing"] }, { "fullname": "怀柔区", "id": "110116", "location": { "lat": 40.316, "lng": 116.63177 }, "name": "怀柔", "pinyin": ["huai", "rou"] }, { "fullname": "平谷区", "id": "110117", "location": { "lat": 40.14062, "lng": 117.12141 }, "name": "平谷", "pinyin": ["ping", "gu"] }, { "fullname": "密云区", "id": "110118", "location": { "lat": 40.37625, "lng": 116.84317 }, "name": "密云", "pinyin": ["mi", "yun"] }, { "fullname": "延庆区", "id": "110119", "location": { "lat": 40.45678, "lng": 115.97503 }, "name": "延庆", "pinyin": ["yan", "qing"] }, { "fullname": "和平区", "id": "120101", "location": { "lat": 39.11712, "lng": 117.2147 }, "name": "和平", "pinyin": ["he", "ping"] }, { "fullname": "河东区", "id": "120102", "location": { "lat": 39.12827, "lng": 117.25228 }, "name": "河东", "pinyin": ["he", "dong"] }, { "fullname": "河西区", "id": "120103", "location": { "lat": 39.10954, "lng": 117.22336 }, "name": "河西", "pinyin": ["he", "xi"] }, { "fullname": "南开区", "id": "120104", "location": { "lat": 39.13815, "lng": 117.15011 }, "name": "南开", "pinyin": ["nan", "kai"] }, { "fullname": "河北区", "id": "120105", "location": { "lat": 39.14784, "lng": 117.19674 }, "name": "河北", "pinyin": ["he", "bei"] }, { "fullname": "红桥区", "id": "120106", "location": { "lat": 39.16734, "lng": 117.15161 }, "name": "红桥", "pinyin": ["hong", "qiao"] }, { "fullname": "东丽区", "id": "120110", "location": { "lat": 39.08652, "lng": 117.31428 }, "name": "东丽", "pinyin": ["dong", "li"] }, { "fullname": "西青区", "id": "120111", "location": { "lat": 39.14111, "lng": 117.00739 }, "name": "西青", "pinyin": ["xi", "qing"] }, { "fullname": "津南区", "id": "120112", "location": { "lat": 38.9375, "lng": 117.3571 }, "name": "津南", "pinyin": ["jin", "nan"] }, { "fullname": "北辰区", "id": "120113", "location": { "lat": 39.22393, "lng": 117.13544 }, "name": "北辰", "pinyin": ["bei", "chen"] }, { "fullname": "武清区", "id": "120114", "location": { "lat": 39.38408, "lng": 117.0443 }, "name": "武清", "pinyin": ["wu", "qing"] }, { "fullname": "宝坻区", "id": "120115", "location": { "lat": 39.71755, "lng": 117.30983 }, "name": "宝坻", "pinyin": ["bao", "di"] }, { "fullname": "滨海新区", "id": "120116", "location": { "lat": 39.0032, "lng": 117.71071 }, "name": "滨海", "pinyin": ["bin", "hai"] }, { "fullname": "宁河区", "id": "120117", "location": { "lat": 39.33091, "lng": 117.82478 }, "name": "宁河", "pinyin": ["ning", "he"] }, { "fullname": "静海区", "id": "120118", "location": { "lat": 38.94737, "lng": 116.97428 }, "name": "静海", "pinyin": ["jing", "hai"] }, { "fullname": "蓟州区", "id": "120119", "location": { "lat": 40.04577, "lng": 117.40829 }, "name": "蓟州", "pinyin": ["ji", "zhou"] }, { "cidx": [0, 21], "fullname": "石家庄市", "id": "130100", "location": { "lat": 38.04276, "lng": 114.5143 }, "name": "石家庄", "pinyin": ["shi", "jia", "zhuang"] }, { "cidx": [22, 36], "fullname": "唐山市", "id": "130200", "location": { "lat": 39.63048, "lng": 118.18058 }, "name": "唐山", "pinyin": ["tang", "shan"] }, { "cidx": [37, 43], "fullname": "秦皇岛市", "id": "130300", "location": { "lat": 39.93545, "lng": 119.59964 }, "name": "秦皇岛", "pinyin": ["qin", "huang", "dao"] }, { "cidx": [44, 61], "fullname": "邯郸市", "id": "130400", "location": { "lat": 36.62556, "lng": 114.53918 }, "name": "邯郸", "pinyin": ["han", "dan"] }, { "cidx": [62, 80], "fullname": "邢台市", "id": "130500", "location": { "lat": 37.07055, "lng": 114.50443 }, "name": "邢台", "pinyin": ["xing", "tai"] }, { "cidx": [81, 104], "fullname": "保定市", "id": "130600", "location": { "lat": 38.87396, "lng": 115.46459 }, "name": "保定", "pinyin": ["bao", "ding"] }, { "cidx": [105, 120], "fullname": "张家口市", "id": "130700", "location": { "lat": 40.82444, "lng": 114.88755 }, "name": "张家口", "pinyin": ["zhang", "jia", "kou"] }, { "cidx": [121, 131], "fullname": "承德市", "id": "130800", "location": { "lat": 40.9515, "lng": 117.9634 }, "name": "承德", "pinyin": ["cheng", "de"] }, { "cidx": [132, 147], "fullname": "沧州市", "id": "130900", "location": { "lat": 38.30441, "lng": 116.83869 }, "name": "沧州", "pinyin": ["cang", "zhou"] }, { "cidx": [148, 157], "fullname": "廊坊市", "id": "131000", "location": { "lat": 39.53775, "lng": 116.68376 }, "name": "廊坊", "pinyin": ["lang", "fang"] }, { "cidx": [158, 168], "fullname": "衡水市", "id": "131100", "location": { "lat": 37.73886, "lng": 115.67054 }, "name": "衡水", "pinyin": ["heng", "shui"] }, { "cidx": [169, 178], "fullname": "太原市", "id": "140100", "location": { "lat": 37.87059, "lng": 112.55067 }, "name": "太原", "pinyin": ["tai", "yuan"] }, { "cidx": [179, 188], "fullname": "大同市", "id": "140200", "location": { "lat": 40.07637, "lng": 113.30001 }, "name": "大同", "pinyin": ["da", "tong"] }, { "cidx": [189, 193], "fullname": "阳泉市", "id": "140300", "location": { "lat": 37.85668, "lng": 113.58047 }, "name": "阳泉", "pinyin": ["yang", "quan"] }, { "cidx": [194, 205], "fullname": "长治市", "id": "140400", "location": { "lat": 36.19581, "lng": 113.11649 }, "name": "长治", "pinyin": ["chang", "zhi"] }, { "cidx": [206, 211], "fullname": "晋城市", "id": "140500", "location": { "lat": 35.49039, "lng": 112.85113 }, "name": "晋城", "pinyin": ["jin", "cheng"] }, { "cidx": [212, 217], "fullname": "朔州市", "id": "140600", "location": { "lat": 39.33155, "lng": 112.43286 }, "name": "朔州", "pinyin": ["shuo", "zhou"] }, { "cidx": [218, 228], "fullname": "晋中市", "id": "140700", "location": { "lat": 37.68702, "lng": 112.75278 }, "name": "晋中", "pinyin": ["jin", "zhong"] }, { "cidx": [229, 241], "fullname": "运城市", "id": "140800", "location": { "lat": 35.02628, "lng": 111.00699 }, "name": "运城", "pinyin": ["yun", "cheng"] }, { "cidx": [242, 255], "fullname": "忻州市", "id": "140900", "location": { "lat": 38.4167, "lng": 112.73418 }, "name": "忻州", "pinyin": ["xin", "zhou"] }, { "cidx": [256, 272], "fullname": "临汾市", "id": "141000", "location": { "lat": 36.08822, "lng": 111.51962 }, "name": "临汾", "pinyin": ["lin", "fen"] }, { "cidx": [273, 285], "fullname": "吕梁市", "id": "141100", "location": { "lat": 37.51934, "lng": 111.14165 }, "name": "吕梁", "pinyin": ["lv", "liang"] }, { "cidx": [286, 294], "fullname": "呼和浩特市", "id": "150100", "location": { "lat": 40.84149, "lng": 111.75199 }, "name": "呼和浩特", "pinyin": ["hu", "he", "hao", "te"] }, { "cidx": [295, 303], "fullname": "包头市", "id": "150200", "location": { "lat": 40.65781, "lng": 109.84021 }, "name": "包头", "pinyin": ["bao", "tou"] }, { "cidx": [304, 306], "fullname": "乌海市", "id": "150300", "location": { "lat": 39.65384, "lng": 106.79546 }, "name": "乌海", "pinyin": ["wu", "hai"] }, { "cidx": [307, 318], "fullname": "赤峰市", "id": "150400", "location": { "lat": 42.2586, "lng": 118.88894 }, "name": "赤峰", "pinyin": ["chi", "feng"] }, { "cidx": [319, 326], "fullname": "通辽市", "id": "150500", "location": { "lat": 43.65247, "lng": 122.24469 }, "name": "通辽", "pinyin": ["tong", "liao"] }, { "cidx": [327, 335], "fullname": "鄂尔多斯市", "id": "150600", "location": { "lat": 39.60845, "lng": 109.78087 }, "name": "鄂尔多斯", "pinyin": ["e", "er", "duo", "si"] }, { "cidx": [336, 349], "fullname": "呼伦贝尔市", "id": "150700", "location": { "lat": 49.21163, "lng": 119.76584 }, "name": "呼伦贝尔", "pinyin": ["hu", "lun", "bei", "er"] }, { "cidx": [350, 356], "fullname": "巴彦淖尔市", "id": "150800", "location": { "lat": 40.74317, "lng": 107.38773 }, "name": "巴彦淖尔", "pinyin": ["ba", "yan", "nao", "er"] }, { "cidx": [357, 367], "fullname": "乌兰察布市", "id": "150900", "location": { "lat": 40.99391, "lng": 113.13376 }, "name": "乌兰察布", "pinyin": ["wu", "lan", "cha", "bu"] }, { "cidx": [368, 373], "fullname": "兴安盟", "id": "152200", "location": { "lat": 46.08208, "lng": 122.03818 }, "name": "兴安", "pinyin": ["xing", "an"] }, { "cidx": [374, 385], "fullname": "锡林郭勒盟", "id": "152500", "location": { "lat": 43.9332, "lng": 116.04775 }, "name": "锡林郭勒", "pinyin": ["xi", "lin", "guo", "le"] }, { "cidx": [386, 388], "fullname": "阿拉善盟", "id": "152900", "location": { "lat": 38.85153, "lng": 105.72898 }, "name": "阿拉善", "pinyin": ["a", "la", "shan"] }, { "cidx": [389, 401], "fullname": "沈阳市", "id": "210100", "location": { "lat": 41.67718, "lng": 123.4631 }, "name": "沈阳", "pinyin": ["shen", "yang"] }, { "cidx": [402, 411], "fullname": "大连市", "id": "210200", "location": { "lat": 38.91369, "lng": 121.61476 }, "name": "大连", "pinyin": ["da", "lian"] }, { "cidx": [412, 418], "fullname": "鞍山市", "id": "210300", "location": { "lat": 41.10777, "lng": 122.9946 }, "name": "鞍山", "pinyin": ["an", "shan"] }, { "cidx": [419, 425], "fullname": "抚顺市", "id": "210400", "location": { "lat": 41.87971, "lng": 123.95722 }, "name": "抚顺", "pinyin": ["fu", "shun"] }, { "cidx": [426, 431], "fullname": "本溪市", "id": "210500", "location": { "lat": 41.29413, "lng": 123.76686 }, "name": "本溪", "pinyin": ["ben", "xi"] }, { "cidx": [432, 437], "fullname": "丹东市", "id": "210600", "location": { "lat": 39.9998, "lng": 124.35601 }, "name": "丹东", "pinyin": ["dan", "dong"] }, { "cidx": [438, 444], "fullname": "锦州市", "id": "210700", "location": { "lat": 41.09515, "lng": 121.12703 }, "name": "锦州", "pinyin": ["jin", "zhou"] }, { "cidx": [445, 450], "fullname": "营口市", "id": "210800", "location": { "lat": 40.66683, "lng": 122.2349 }, "name": "营口", "pinyin": ["ying", "kou"] }, { "cidx": [451, 457], "fullname": "阜新市", "id": "210900", "location": { "lat": 42.02166, "lng": 121.67011 }, "name": "阜新", "pinyin": ["fu", "xin"] }, { "cidx": [458, 464], "fullname": "辽阳市", "id": "211000", "location": { "lat": 41.26809, "lng": 123.23736 }, "name": "辽阳", "pinyin": ["liao", "yang"] }, { "cidx": [465, 468], "fullname": "盘锦市", "id": "211100", "location": { "lat": 41.11996, "lng": 122.07078 }, "name": "盘锦", "pinyin": ["pan", "jin"] }, { "cidx": [469, 475], "fullname": "铁岭市", "id": "211200", "location": { "lat": 42.2862, "lng": 123.84241 }, "name": "铁岭", "pinyin": ["tie", "ling"] }, { "cidx": [476, 482], "fullname": "朝阳市", "id": "211300", "location": { "lat": 41.57347, "lng": 120.4508 }, "name": "朝阳", "pinyin": ["chao", "yang"] }, { "cidx": [483, 488], "fullname": "葫芦岛市", "id": "211400", "location": { "lat": 40.711, "lng": 120.83699 }, "name": "葫芦岛", "pinyin": ["hu", "lu", "dao"] }, { "cidx": [489, 498], "fullname": "长春市", "id": "220100", "location": { "lat": 43.81602, "lng": 125.32357 }, "name": "长春", "pinyin": ["chang", "chun"] }, { "cidx": [499, 507], "fullname": "吉林市", "id": "220200", "location": { "lat": 43.83784, "lng": 126.54944 }, "name": "吉林", "pinyin": ["ji", "lin"] }, { "cidx": [508, 513], "fullname": "四平市", "id": "220300", "location": { "lat": 43.16646, "lng": 124.35036 }, "name": "四平", "pinyin": ["si", "ping"] }, { "cidx": [514, 517], "fullname": "辽源市", "id": "220400", "location": { "lat": 42.88805, "lng": 125.14368 }, "name": "辽源", "pinyin": ["liao", "yuan"] }, { "cidx": [518, 524], "fullname": "通化市", "id": "220500", "location": { "lat": 41.72829, "lng": 125.9399 }, "name": "通化", "pinyin": ["tong", "hua"] }, { "cidx": [525, 530], "fullname": "白山市", "id": "220600", "location": { "lat": 41.9408, "lng": 126.42443 }, "name": "白山", "pinyin": ["bai", "shan"] }, { "cidx": [531, 535], "fullname": "松原市", "id": "220700", "location": { "lat": 45.1411, "lng": 124.82515 }, "name": "松原", "pinyin": ["song", "yuan"] }, { "cidx": [536, 540], "fullname": "白城市", "id": "220800", "location": { "lat": 45.6196, "lng": 122.83871 }, "name": "白城", "pinyin": ["bai", "cheng"] }, { "cidx": [541, 548], "fullname": "延边朝鲜族自治州", "id": "222400", "location": { "lat": 42.89119, "lng": 129.5091 }, "name": "延边", "pinyin": ["yan", "bian"] }, { "cidx": [549, 566], "fullname": "哈尔滨市", "id": "230100", "location": { "lat": 45.80216, "lng": 126.5358 }, "name": "哈尔滨", "pinyin": ["ha", "er", "bin"] }, { "cidx": [567, 582], "fullname": "齐齐哈尔市", "id": "230200", "location": { "lat": 47.35431, "lng": 123.91796 }, "name": "齐齐哈尔", "pinyin": ["qi", "qi", "ha", "er"] }, { "cidx": [583, 591], "fullname": "鸡西市", "id": "230300", "location": { "lat": 45.29524, "lng": 130.96954 }, "name": "鸡西", "pinyin": ["ji", "xi"] }, { "cidx": [592, 599], "fullname": "鹤岗市", "id": "230400", "location": { "lat": 47.34989, "lng": 130.29785 }, "name": "鹤岗", "pinyin": ["he", "gang"] }, { "cidx": [600, 607], "fullname": "双鸭山市", "id": "230500", "location": { "lat": 46.64658, "lng": 131.1591 }, "name": "双鸭山", "pinyin": ["shuang", "ya", "shan"] }, { "cidx": [608, 616], "fullname": "大庆市", "id": "230600", "location": { "lat": 46.58758, "lng": 125.10307 }, "name": "大庆", "pinyin": ["da", "qing"] }, { "cidx": [617, 633], "fullname": "伊春市", "id": "230700", "location": { "lat": 47.72752, "lng": 128.84049 }, "name": "伊春", "pinyin": ["yi", "chun"] }, { "cidx": [634, 643], "fullname": "佳木斯市", "id": "230800", "location": { "lat": 46.79977, "lng": 130.31882 }, "name": "佳木斯", "pinyin": ["jia", "mu", "si"] }, { "cidx": [644, 647], "fullname": "七台河市", "id": "230900", "location": { "lat": 45.77065, "lng": 131.00306 }, "name": "七台河", "pinyin": ["qi", "tai", "he"] }, { "cidx": [648, 657], "fullname": "牡丹江市", "id": "231000", "location": { "lat": 44.55269, "lng": 129.63244 }, "name": "牡丹江", "pinyin": ["mu", "dan", "jiang"] }, { "cidx": [658, 663], "fullname": "黑河市", "id": "231100", "location": { "lat": 50.24523, "lng": 127.52852 }, "name": "黑河", "pinyin": ["hei", "he"] }, { "cidx": [664, 673], "fullname": "绥化市", "id": "231200", "location": { "lat": 46.65246, "lng": 126.96932 }, "name": "绥化", "pinyin": ["sui", "hua"] }, { "cidx": [674, 677], "fullname": "大兴安岭地区", "id": "232700", "location": { "lat": 51.92398, "lng": 124.59216 }, "name": "大兴安岭", "pinyin": ["da", "xing", "an", "ling"] }, { "fullname": "黄浦区", "id": "310101", "location": { "lat": 31.23162, "lng": 121.48461 }, "name": "黄浦", "pinyin": ["huang", "pu"] }, { "fullname": "徐汇区", "id": "310104", "location": { "lat": 31.18826, "lng": 121.43687 }, "name": "徐汇", "pinyin": ["xu", "hui"] }, { "fullname": "长宁区", "id": "310105", "location": { "lat": 31.22024, "lng": 121.42394 }, "name": "长宁", "pinyin": ["chang", "ning"] }, { "fullname": "静安区", "id": "310106", "location": { "lat": 31.22352, "lng": 121.45591 }, "name": "静安", "pinyin": ["jing", "an"] }, { "fullname": "普陀区", "id": "310107", "location": { "lat": 31.2494, "lng": 121.397 }, "name": "普陀", "pinyin": ["pu", "tuo"] }, { "fullname": "虹口区", "id": "310109", "location": { "lat": 31.26451, "lng": 121.50515 }, "name": "虹口", "pinyin": ["hong", "kou"] }, { "fullname": "杨浦区", "id": "310110", "location": { "lat": 31.25956, "lng": 121.52609 }, "name": "杨浦", "pinyin": ["yang", "pu"] }, { "fullname": "闵行区", "id": "310112", "location": { "lat": 31.11325, "lng": 121.38206 }, "name": "闵行", "pinyin": ["min", "hang"] }, { "fullname": "宝山区", "id": "310113", "location": { "lat": 31.40527, "lng": 121.48941 }, "name": "宝山", "pinyin": ["bao", "shan"] }, { "fullname": "嘉定区", "id": "310114", "location": { "lat": 31.37482, "lng": 121.26621 }, "name": "嘉定", "pinyin": ["jia", "ding"] }, { "fullname": "浦东新区", "id": "310115", "location": { "lat": 31.22114, "lng": 121.54409 }, "name": "浦东", "pinyin": ["pu", "dong"] }, { "fullname": "金山区", "id": "310116", "location": { "lat": 30.74185, "lng": 121.34242 }, "name": "金山", "pinyin": ["jin", "shan"] }, { "fullname": "松江区", "id": "310117", "location": { "lat": 31.03241, "lng": 121.22654 }, "name": "松江", "pinyin": ["song", "jiang"] }, { "fullname": "青浦区", "id": "310118", "location": { "lat": 31.14979, "lng": 121.12426 }, "name": "青浦", "pinyin": ["qing", "pu"] }, { "fullname": "奉贤区", "id": "310120", "location": { "lat": 30.91803, "lng": 121.4741 }, "name": "奉贤", "pinyin": ["feng", "xian"] }, { "fullname": "崇明区", "id": "310151", "location": { "lat": 31.6229, "lng": 121.3973 }, "name": "崇明", "pinyin": ["chong", "ming"] }, { "cidx": [678, 688], "fullname": "南京市", "id": "320100", "location": { "lat": 32.05838, "lng": 118.79647 }, "name": "南京", "pinyin": ["nan", "jing"] }, { "cidx": [689, 695], "fullname": "无锡市", "id": "320200", "location": { "lat": 31.49099, "lng": 120.31237 }, "name": "无锡", "pinyin": ["wu", "xi"] }, { "cidx": [696, 705], "fullname": "徐州市", "id": "320300", "location": { "lat": 34.2044, "lng": 117.28577 }, "name": "徐州", "pinyin": ["xu", "zhou"] }, { "cidx": [706, 711], "fullname": "常州市", "id": "320400", "location": { "lat": 31.81072, "lng": 119.97365 }, "name": "常州", "pinyin": ["chang", "zhou"] }, { "cidx": [712, 720], "fullname": "苏州市", "id": "320500", "location": { "lat": 31.29834, "lng": 120.58319 }, "name": "苏州", "pinyin": ["su", "zhou"] }, { "cidx": [721, 728], "fullname": "南通市", "id": "320600", "location": { "lat": 31.97958, "lng": 120.89371 }, "name": "南通", "pinyin": ["nan", "tong"] }, { "cidx": [729, 734], "fullname": "连云港市", "id": "320700", "location": { "lat": 34.59669, "lng": 119.22295 }, "name": "连云港", "pinyin": ["lian", "yun", "gang"] }, { "cidx": [735, 741], "fullname": "淮安市", "id": "320800", "location": { "lat": 33.61016, "lng": 119.01595 }, "name": "淮安", "pinyin": ["huai", "an"] }, { "cidx": [742, 750], "fullname": "盐城市", "id": "320900", "location": { "lat": 33.34951, "lng": 120.16164 }, "name": "盐城", "pinyin": ["yan", "cheng"] }, { "cidx": [751, 756], "fullname": "扬州市", "id": "321000", "location": { "lat": 32.39358, "lng": 119.41269 }, "name": "扬州", "pinyin": ["yang", "zhou"] }, { "cidx": [757, 762], "fullname": "镇江市", "id": "321100", "location": { "lat": 32.18959, "lng": 119.425 }, "name": "镇江", "pinyin": ["zhen", "jiang"] }, { "cidx": [763, 768], "fullname": "泰州市", "id": "321200", "location": { "lat": 32.45546, "lng": 119.92554 }, "name": "泰州", "pinyin": ["tai", "zhou"] }, { "cidx": [769, 773], "fullname": "宿迁市", "id": "321300", "location": { "lat": 33.96193, "lng": 118.27549 }, "name": "宿迁", "pinyin": ["su", "qian"] }, { "cidx": [774, 786], "fullname": "杭州市", "id": "330100", "location": { "lat": 30.27415, "lng": 120.15515 }, "name": "杭州", "pinyin": ["hang", "zhou"] }, { "cidx": [787, 796], "fullname": "宁波市", "id": "330200", "location": { "lat": 29.87386, "lng": 121.55027 }, "name": "宁波", "pinyin": ["ning", "bo"] }, { "cidx": [797, 808], "fullname": "温州市", "id": "330300", "location": { "lat": 27.99492, "lng": 120.69939 }, "name": "温州", "pinyin": ["wen", "zhou"] }, { "cidx": [809, 815], "fullname": "嘉兴市", "id": "330400", "location": { "lat": 30.74501, "lng": 120.7555 }, "name": "嘉兴", "pinyin": ["jia", "xing"] }, { "cidx": [816, 820], "fullname": "湖州市", "id": "330500", "location": { "lat": 30.89305, "lng": 120.08805 }, "name": "湖州", "pinyin": ["hu", "zhou"] }, { "cidx": [821, 826], "fullname": "绍兴市", "id": "330600", "location": { "lat": 30.03033, "lng": 120.5802 }, "name": "绍兴", "pinyin": ["shao", "xing"] }, { "cidx": [827, 835], "fullname": "金华市", "id": "330700", "location": { "lat": 29.07812, "lng": 119.64759 }, "name": "金华", "pinyin": ["jin", "hua"] }, { "cidx": [836, 841], "fullname": "衢州市", "id": "330800", "location": { "lat": 28.93592, "lng": 118.87419 }, "name": "衢州", "pinyin": ["qu", "zhou"] }, { "cidx": [842, 845], "fullname": "舟山市", "id": "330900", "location": { "lat": 29.98539, "lng": 122.20778 }, "name": "舟山", "pinyin": ["zhou", "shan"] }, { "cidx": [846, 854], "fullname": "台州市", "id": "331000", "location": { "lat": 28.65611, "lng": 121.42056 }, "name": "台州", "pinyin": ["tai", "zhou"] }, { "cidx": [855, 863], "fullname": "丽水市", "id": "331100", "location": { "lat": 28.4672, "lng": 119.92293 }, "name": "丽水", "pinyin": ["li", "shui"] }, { "cidx": [864, 872], "fullname": "合肥市", "id": "340100", "location": { "lat": 31.82057, "lng": 117.22901 }, "name": "合肥", "pinyin": ["he", "fei"] }, { "cidx": [873, 880], "fullname": "芜湖市", "id": "340200", "location": { "lat": 31.35246, "lng": 118.43313 }, "name": "芜湖", "pinyin": ["wu", "hu"] }, { "cidx": [881, 887], "fullname": "蚌埠市", "id": "340300", "location": { "lat": 32.91548, "lng": 117.38932 }, "name": "蚌埠", "pinyin": ["beng", "bu"] }, { "cidx": [888, 894], "fullname": "淮南市", "id": "340400", "location": { "lat": 32.62549, "lng": 116.9998 }, "name": "淮南", "pinyin": ["huai", "nan"] }, { "cidx": [895, 900], "fullname": "马鞍山市", "id": "340500", "location": { "lat": 31.67067, "lng": 118.50611 }, "name": "马鞍山", "pinyin": ["ma", "an", "shan"] }, { "cidx": [901, 904], "fullname": "淮北市", "id": "340600", "location": { "lat": 33.95479, "lng": 116.79834 }, "name": "淮北", "pinyin": ["huai", "bei"] }, { "cidx": [905, 908], "fullname": "铜陵市", "id": "340700", "location": { "lat": 30.94486, "lng": 117.81232 }, "name": "铜陵", "pinyin": ["tong", "ling"] }, { "cidx": [909, 918], "fullname": "安庆市", "id": "340800", "location": { "lat": 30.54294, "lng": 117.06354 }, "name": "安庆", "pinyin": ["an", "qing"] }, { "cidx": [919, 925], "fullname": "黄山市", "id": "341000", "location": { "lat": 29.71517, "lng": 118.33866 }, "name": "黄山", "pinyin": ["huang", "shan"] }, { "cidx": [926, 933], "fullname": "滁州市", "id": "341100", "location": { "lat": 32.30181, "lng": 118.31683 }, "name": "滁州", "pinyin": ["chu", "zhou"] }, { "cidx": [934, 941], "fullname": "阜阳市", "id": "341200", "location": { "lat": 32.88963, "lng": 115.81495 }, "name": "阜阳", "pinyin": ["fu", "yang"] }, { "cidx": [942, 946], "fullname": "宿州市", "id": "341300", "location": { "lat": 33.64614, "lng": 116.96391 }, "name": "宿州", "pinyin": ["su", "zhou"] }, { "cidx": [947, 953], "fullname": "六安市", "id": "341500", "location": { "lat": 31.73488, "lng": 116.52324 }, "name": "六安", "pinyin": ["liu", "an"] }, { "cidx": [954, 957], "fullname": "亳州市", "id": "341600", "location": { "lat": 33.84461, "lng": 115.77931 }, "name": "亳州", "pinyin": ["bo", "zhou"] }, { "cidx": [958, 961], "fullname": "池州市", "id": "341700", "location": { "lat": 30.66469, "lng": 117.49142 }, "name": "池州", "pinyin": ["chi", "zhou"] }, { "cidx": [962, 968], "fullname": "宣城市", "id": "341800", "location": { "lat": 30.94078, "lng": 118.75866 }, "name": "宣城", "pinyin": ["xuan", "cheng"] }, { "cidx": [969, 981], "fullname": "福州市", "id": "350100", "location": { "lat": 26.07421, "lng": 119.29647 }, "name": "福州", "pinyin": ["fu", "zhou"] }, { "cidx": [982, 987], "fullname": "厦门市", "id": "350200", "location": { "lat": 24.47951, "lng": 118.08948 }, "name": "厦门", "pinyin": ["xia", "men"] }, { "cidx": [988, 992], "fullname": "莆田市", "id": "350300", "location": { "lat": 25.454, "lng": 119.00771 }, "name": "莆田", "pinyin": ["pu", "tian"] }, { "cidx": [993, 1004], "fullname": "三明市", "id": "350400", "location": { "lat": 26.26385, "lng": 117.63922 }, "name": "三明", "pinyin": ["san", "ming"] }, { "cidx": [1005, 1016], "fullname": "泉州市", "id": "350500", "location": { "lat": 24.87389, "lng": 118.67587 }, "name": "泉州", "pinyin": ["quan", "zhou"] }, { "cidx": [1017, 1027], "fullname": "漳州市", "id": "350600", "location": { "lat": 24.51347, "lng": 117.64725 }, "name": "漳州", "pinyin": ["zhang", "zhou"] }, { "cidx": [1028, 1037], "fullname": "南平市", "id": "350700", "location": { "lat": 27.33175, "lng": 118.12043 }, "name": "南平", "pinyin": ["nan", "ping"] }, { "cidx": [1038, 1044], "fullname": "龙岩市", "id": "350800", "location": { "lat": 25.07504, "lng": 117.01722 }, "name": "龙岩", "pinyin": ["long", "yan"] }, { "cidx": [1045, 1053], "fullname": "宁德市", "id": "350900", "location": { "lat": 26.66571, "lng": 119.54819 }, "name": "宁德", "pinyin": ["ning", "de"] }, { "cidx": [1054, 1062], "fullname": "南昌市", "id": "360100", "location": { "lat": 28.68202, "lng": 115.85794 }, "name": "南昌", "pinyin": ["nan", "chang"] }, { "cidx": [1063, 1066], "fullname": "景德镇市", "id": "360200", "location": { "lat": 29.26869, "lng": 117.17839 }, "name": "景德镇", "pinyin": ["jing", "de", "zhen"] }, { "cidx": [1067, 1071], "fullname": "萍乡市", "id": "360300", "location": { "lat": 27.62289, "lng": 113.85427 }, "name": "萍乡", "pinyin": ["ping", "xiang"] }, { "cidx": [1072, 1084], "fullname": "九江市", "id": "360400", "location": { "lat": 29.70548, "lng": 116.00146 }, "name": "九江", "pinyin": ["jiu", "jiang"] }, { "cidx": [1085, 1086], "fullname": "新余市", "id": "360500", "location": { "lat": 27.81776, "lng": 114.91713 }, "name": "新余", "pinyin": ["xin", "yu"] }, { "cidx": [1087, 1089], "fullname": "鹰潭市", "id": "360600", "location": { "lat": 28.26019, "lng": 117.06919 }, "name": "鹰潭", "pinyin": ["ying", "tan"] }, { "cidx": [1090, 1107], "fullname": "赣州市", "id": "360700", "location": { "lat": 25.83109, "lng": 114.93476 }, "name": "赣州", "pinyin": ["gan", "zhou"] }, { "cidx": [1108, 1120], "fullname": "吉安市", "id": "360800", "location": { "lat": 27.11382, "lng": 114.99376 }, "name": "吉安", "pinyin": ["ji", "an"] }, { "cidx": [1121, 1130], "fullname": "宜春市", "id": "360900", "location": { "lat": 27.81443, "lng": 114.41612 }, "name": "宜春", "pinyin": ["yi", "chun"] }, { "cidx": [1131, 1141], "fullname": "抚州市", "id": "361000", "location": { "lat": 27.94781, "lng": 116.35809 }, "name": "抚州", "pinyin": ["fu", "zhou"] }, { "cidx": [1142, 1153], "fullname": "上饶市", "id": "361100", "location": { "lat": 28.45463, "lng": 117.94357 }, "name": "上饶", "pinyin": ["shang", "rao"] }, { "cidx": [1154, 1165], "fullname": "济南市", "id": "370100", "location": { "lat": 36.65184, "lng": 117.12009 }, "name": "济南", "pinyin": ["ji", "nan"] }, { "cidx": [1166, 1175], "fullname": "青岛市", "id": "370200", "location": { "lat": 36.06623, "lng": 120.38299 }, "name": "青岛", "pinyin": ["qing", "dao"] }, { "cidx": [1176, 1183], "fullname": "淄博市", "id": "370300", "location": { "lat": 36.8131, "lng": 118.0548 }, "name": "淄博", "pinyin": ["zi", "bo"] }, { "cidx": [1184, 1189], "fullname": "枣庄市", "id": "370400", "location": { "lat": 34.81071, "lng": 117.32196 }, "name": "枣庄", "pinyin": ["zao", "zhuang"] }, { "cidx": [1190, 1194], "fullname": "东营市", "id": "370500", "location": { "lat": 37.43365, "lng": 118.67466 }, "name": "东营", "pinyin": ["dong", "ying"] }, { "cidx": [1195, 1206], "fullname": "烟台市", "id": "370600", "location": { "lat": 37.46353, "lng": 121.44801 }, "name": "烟台", "pinyin": ["yan", "tai"] }, { "cidx": [1207, 1218], "fullname": "潍坊市", "id": "370700", "location": { "lat": 36.70686, "lng": 119.16176 }, "name": "潍坊", "pinyin": ["wei", "fang"] }, { "cidx": [1219, 1229], "fullname": "济宁市", "id": "370800", "location": { "lat": 35.41459, "lng": 116.58724 }, "name": "济宁", "pinyin": ["ji", "ning"] }, { "cidx": [1230, 1235], "fullname": "泰安市", "id": "370900", "location": { "lat": 36.19994, "lng": 117.0884 }, "name": "泰安", "pinyin": ["tai", "an"] }, { "cidx": [1236, 1239], "fullname": "威海市", "id": "371000", "location": { "lat": 37.51348, "lng": 122.12171 }, "name": "威海", "pinyin": ["wei", "hai"] }, { "cidx": [1240, 1243], "fullname": "日照市", "id": "371100", "location": { "lat": 35.41646, "lng": 119.52719 }, "name": "日照", "pinyin": ["ri", "zhao"] }, { "cidx": [1244, 1255], "fullname": "临沂市", "id": "371300", "location": { "lat": 35.10465, "lng": 118.35646 }, "name": "临沂", "pinyin": ["lin", "yi"] }, { "cidx": [1256, 1266], "fullname": "德州市", "id": "371400", "location": { "lat": 37.4355, "lng": 116.35927 }, "name": "德州", "pinyin": ["de", "zhou"] }, { "cidx": [1267, 1274], "fullname": "聊城市", "id": "371500", "location": { "lat": 36.45702, "lng": 115.98549 }, "name": "聊城", "pinyin": ["liao", "cheng"] }, { "cidx": [1275, 1281], "fullname": "滨州市", "id": "371600", "location": { "lat": 37.38211, "lng": 117.97279 }, "name": "滨州", "pinyin": ["bin", "zhou"] }, { "cidx": [1282, 1290], "fullname": "菏泽市", "id": "371700", "location": { "lat": 35.23363, "lng": 115.48115 }, "name": "菏泽", "pinyin": ["he", "ze"] }, { "cidx": [1291, 1302], "fullname": "郑州市", "id": "410100", "location": { "lat": 34.74725, "lng": 113.62493 }, "name": "郑州", "pinyin": ["zheng", "zhou"] }, { "cidx": [1303, 1311], "fullname": "开封市", "id": "410200", "location": { "lat": 34.79726, "lng": 114.30731 }, "name": "开封", "pinyin": ["kai", "feng"] }, { "cidx": [1312, 1326], "fullname": "洛阳市", "id": "410300", "location": { "lat": 34.61812, "lng": 112.45361 }, "name": "洛阳", "pinyin": ["luo", "yang"] }, { "cidx": [1327, 1336], "fullname": "平顶山市", "id": "410400", "location": { "lat": 33.76609, "lng": 113.19241 }, "name": "平顶山", "pinyin": ["ping", "ding", "shan"] }, { "cidx": [1337, 1345], "fullname": "安阳市", "id": "410500", "location": { "lat": 36.09771, "lng": 114.3931 }, "name": "安阳", "pinyin": ["an", "yang"] }, { "cidx": [1346, 1350], "fullname": "鹤壁市", "id": "410600", "location": { "lat": 35.747, "lng": 114.29745 }, "name": "鹤壁", "pinyin": ["he", "bi"] }, { "cidx": [1351, 1362], "fullname": "新乡市", "id": "410700", "location": { "lat": 35.30323, "lng": 113.92675 }, "name": "新乡", "pinyin": ["xin", "xiang"] }, { "cidx": [1363, 1372], "fullname": "焦作市", "id": "410800", "location": { "lat": 35.21563, "lng": 113.24201 }, "name": "焦作", "pinyin": ["jiao", "zuo"] }, { "cidx": [1373, 1378], "fullname": "濮阳市", "id": "410900", "location": { "lat": 35.76189, "lng": 115.02932 }, "name": "濮阳", "pinyin": ["pu", "yang"] }, { "cidx": [1379, 1384], "fullname": "许昌市", "id": "411000", "location": { "lat": 34.0357, "lng": 113.85233 }, "name": "许昌", "pinyin": ["xu", "chang"] }, { "cidx": [1385, 1389], "fullname": "漯河市", "id": "411100", "location": { "lat": 33.58149, "lng": 114.01681 }, "name": "漯河", "pinyin": ["luo", "he"] }, { "cidx": [1390, 1395], "fullname": "三门峡市", "id": "411200", "location": { "lat": 34.77261, "lng": 111.2003 }, "name": "三门峡", "pinyin": ["san", "men", "xia"] }, { "cidx": [1396, 1408], "fullname": "南阳市", "id": "411300", "location": { "lat": 32.99073, "lng": 112.52851 }, "name": "南阳", "pinyin": ["nan", "yang"] }, { "cidx": [1409, 1417], "fullname": "商丘市", "id": "411400", "location": { "lat": 34.41427, "lng": 115.65635 }, "name": "商丘", "pinyin": ["shang", "qiu"] }, { "cidx": [1418, 1427], "fullname": "信阳市", "id": "411500", "location": { "lat": 32.14714, "lng": 114.09279 }, "name": "信阳", "pinyin": ["xin", "yang"] }, { "cidx": [1428, 1437], "fullname": "周口市", "id": "411600", "location": { "lat": 33.62583, "lng": 114.69695 }, "name": "周口", "pinyin": ["zhou", "kou"] }, { "cidx": [1438, 1447], "fullname": "驻马店市", "id": "411700", "location": { "lat": 33.01142, "lng": 114.02299 }, "name": "驻马店", "pinyin": ["zhu", "ma", "dian"] }, { "fullname": "济源市", "id": "419001", "location": { "lat": 35.06707, "lng": 112.60273 }, "name": "济源", "pinyin": ["ji", "yuan"] }, { "cidx": [1448, 1460], "fullname": "武汉市", "id": "420100", "location": { "lat": 30.59276, "lng": 114.30525 }, "name": "武汉", "pinyin": ["wu", "han"] }, { "cidx": [1461, 1466], "fullname": "黄石市", "id": "420200", "location": { "lat": 30.19953, "lng": 115.0389 }, "name": "黄石", "pinyin": ["huang", "shi"] }, { "cidx": [1467, 1474], "fullname": "十堰市", "id": "420300", "location": { "lat": 32.62918, "lng": 110.79801 }, "name": "十堰", "pinyin": ["shi", "yan"] }, { "cidx": [1475, 1487], "fullname": "宜昌市", "id": "420500", "location": { "lat": 30.69186, "lng": 111.28642 }, "name": "宜昌", "pinyin": ["yi", "chang"] }, { "cidx": [1488, 1496], "fullname": "襄阳市", "id": "420600", "location": { "lat": 32.009, "lng": 112.12255 }, "name": "襄阳", "pinyin": ["xiang", "yang"] }, { "cidx": [1497, 1499], "fullname": "鄂州市", "id": "420700", "location": { "lat": 30.39085, "lng": 114.89495 }, "name": "鄂州", "pinyin": ["e", "zhou"] }, { "cidx": [1500, 1504], "fullname": "荆门市", "id": "420800", "location": { "lat": 31.03546, "lng": 112.19945 }, "name": "荆门", "pinyin": ["jing", "men"] }, { "cidx": [1505, 1511], "fullname": "孝感市", "id": "420900", "location": { "lat": 30.92483, "lng": 113.91645 }, "name": "孝感", "pinyin": ["xiao", "gan"] }, { "cidx": [1512, 1519], "fullname": "荆州市", "id": "421000", "location": { "lat": 30.33479, "lng": 112.24069 }, "name": "荆州", "pinyin": ["jing", "zhou"] }, { "cidx": [1520, 1529], "fullname": "黄冈市", "id": "421100", "location": { "lat": 30.45347, "lng": 114.87238 }, "name": "黄冈", "pinyin": ["huang", "gang"] }, { "cidx": [1530, 1535], "fullname": "咸宁市", "id": "421200", "location": { "lat": 29.84126, "lng": 114.32245 }, "name": "咸宁", "pinyin": ["xian", "ning"] }, { "cidx": [1536, 1538], "fullname": "随州市", "id": "421300", "location": { "lat": 31.69013, "lng": 113.38262 }, "name": "随州", "pinyin": ["sui", "zhou"] }, { "cidx": [1539, 1546], "fullname": "恩施土家族苗族自治州", "id": "422800", "location": { "lat": 30.27217, "lng": 109.48817 }, "name": "恩施", "pinyin": ["en", "shi"] }, { "fullname": "仙桃市", "id": "429004", "location": { "lat": 30.36251, "lng": 113.4545 }, "name": "仙桃", "pinyin": ["xian", "tao"] }, { "fullname": "潜江市", "id": "429005", "location": { "lat": 30.40147, "lng": 112.8993 }, "name": "潜江", "pinyin": ["qian", "jiang"] }, { "fullname": "天门市", "id": "429006", "location": { "lat": 30.66339, "lng": 113.16614 }, "name": "天门", "pinyin": ["tian", "men"] }, { "fullname": "神农架林区", "id": "429021", "location": { "lat": 31.74452, "lng": 110.67598 }, "name": "神农架", "pinyin": ["shen", "nong", "jia"] }, { "cidx": [1547, 1555], "fullname": "长沙市", "id": "430100", "location": { "lat": 28.22778, "lng": 112.93886 }, "name": "长沙", "pinyin": ["chang", "sha"] }, { "cidx": [1556, 1564], "fullname": "株洲市", "id": "430200", "location": { "lat": 27.82767, "lng": 113.13396 }, "name": "株洲", "pinyin": ["zhu", "zhou"] }, { "cidx": [1565, 1569], "fullname": "湘潭市", "id": "430300", "location": { "lat": 27.82975, "lng": 112.94411 }, "name": "湘潭", "pinyin": ["xiang", "tan"] }, { "cidx": [1570, 1581], "fullname": "衡阳市", "id": "430400", "location": { "lat": 26.89324, "lng": 112.57195 }, "name": "衡阳", "pinyin": ["heng", "yang"] }, { "cidx": [1582, 1593], "fullname": "邵阳市", "id": "430500", "location": { "lat": 27.2389, "lng": 111.4677 }, "name": "邵阳", "pinyin": ["shao", "yang"] }, { "cidx": [1594, 1602], "fullname": "岳阳市", "id": "430600", "location": { "lat": 29.35728, "lng": 113.12919 }, "name": "岳阳", "pinyin": ["yue", "yang"] }, { "cidx": [1603, 1611], "fullname": "常德市", "id": "430700", "location": { "lat": 29.03158, "lng": 111.69854 }, "name": "常德", "pinyin": ["chang", "de"] }, { "cidx": [1612, 1615], "fullname": "张家界市", "id": "430800", "location": { "lat": 29.11667, "lng": 110.47839 }, "name": "张家界", "pinyin": ["zhang", "jia", "jie"] }, { "cidx": [1616, 1621], "fullname": "益阳市", "id": "430900", "location": { "lat": 28.55391, "lng": 112.35516 }, "name": "益阳", "pinyin": ["yi", "yang"] }, { "cidx": [1622, 1632], "fullname": "郴州市", "id": "431000", "location": { "lat": 25.77063, "lng": 113.01485 }, "name": "郴州", "pinyin": ["chen", "zhou"] }, { "cidx": [1633, 1643], "fullname": "永州市", "id": "431100", "location": { "lat": 26.42034, "lng": 111.61225 }, "name": "永州", "pinyin": ["yong", "zhou"] }, { "cidx": [1644, 1655], "fullname": "怀化市", "id": "431200", "location": { "lat": 27.56974, "lng": 110.0016 }, "name": "怀化", "pinyin": ["huai", "hua"] }, { "cidx": [1656, 1660], "fullname": "娄底市", "id": "431300", "location": { "lat": 27.69728, "lng": 111.99458 }, "name": "娄底", "pinyin": ["lou", "di"] }, { "cidx": [1661, 1668], "fullname": "湘西土家族苗族自治州", "id": "433100", "location": { "lat": 28.31173, "lng": 109.73893 }, "name": "湘西", "pinyin": ["xiang", "xi"] }, { "cidx": [1669, 1679], "fullname": "广州市", "id": "440100", "location": { "lat": 23.12908, "lng": 113.26436 }, "name": "广州", "pinyin": ["guang", "zhou"] }, { "cidx": [1680, 1689], "fullname": "韶关市", "id": "440200", "location": { "lat": 24.81039, "lng": 113.59723 }, "name": "韶关", "pinyin": ["shao", "guan"] }, { "cidx": [1690, 1698], "fullname": "深圳市", "id": "440300", "location": { "lat": 22.54286, "lng": 114.05956 }, "name": "深圳", "pinyin": ["shen", "zhen"] }, { "cidx": [1699, 1702], "fullname": "珠海市", "id": "440400", "location": { "lat": 22.27073, "lng": 113.57668 }, "name": "珠海", "pinyin": ["zhu", "hai"] }, { "cidx": [1703, 1709], "fullname": "汕头市", "id": "440500", "location": { "lat": 23.3535, "lng": 116.68221 }, "name": "汕头", "pinyin": ["shan", "tou"] }, { "cidx": [1710, 1714], "fullname": "佛山市", "id": "440600", "location": { "lat": 23.02185, "lng": 113.12192 }, "name": "佛山", "pinyin": ["fo", "shan"] }, { "cidx": [1715, 1721], "fullname": "江门市", "id": "440700", "location": { "lat": 22.57865, "lng": 113.08161 }, "name": "江门", "pinyin": ["jiang", "men"] }, { "cidx": [1722, 1730], "fullname": "湛江市", "id": "440800", "location": { "lat": 21.27134, "lng": 110.35894 }, "name": "湛江", "pinyin": ["zhan", "jiang"] }, { "cidx": [1731, 1735], "fullname": "茂名市", "id": "440900", "location": { "lat": 21.66329, "lng": 110.92523 }, "name": "茂名", "pinyin": ["mao", "ming"] }, { "cidx": [1736, 1743], "fullname": "肇庆市", "id": "441200", "location": { "lat": 23.0469, "lng": 112.46528 }, "name": "肇庆", "pinyin": ["zhao", "qing"] }, { "cidx": [1744, 1748], "fullname": "惠州市", "id": "441300", "location": { "lat": 23.11075, "lng": 114.41679 }, "name": "惠州", "pinyin": ["hui", "zhou"] }, { "cidx": [1749, 1756], "fullname": "梅州市", "id": "441400", "location": { "lat": 24.28844, "lng": 116.12264 }, "name": "梅州", "pinyin": ["mei", "zhou"] }, { "cidx": [1757, 1760], "fullname": "汕尾市", "id": "441500", "location": { "lat": 22.78566, "lng": 115.37514 }, "name": "汕尾", "pinyin": ["shan", "wei"] }, { "cidx": [1761, 1766], "fullname": "河源市", "id": "441600", "location": { "lat": 23.74365, "lng": 114.70065 }, "name": "河源", "pinyin": ["he", "yuan"] }, { "cidx": [1767, 1770], "fullname": "阳江市", "id": "441700", "location": { "lat": 21.85829, "lng": 111.98256 }, "name": "阳江", "pinyin": ["yang", "jiang"] }, { "cidx": [1771, 1778], "fullname": "清远市", "id": "441800", "location": { "lat": 23.68201, "lng": 113.05615 }, "name": "清远", "pinyin": ["qing", "yuan"] }, { "cidx": [1779, 1779], "fullname": "东莞市", "id": "441900", "location": { "lat": 23.02067, "lng": 113.75179 }, "name": "东莞", "pinyin": ["dong", "guan"] }, { "cidx": [1780, 1780], "fullname": "中山市", "id": "442000", "location": { "lat": 22.51595, "lng": 113.3926 }, "name": "中山", "pinyin": ["zhong", "shan"] }, { "cidx": [1781, 1783], "fullname": "潮州市", "id": "445100", "location": { "lat": 23.6567, "lng": 116.62296 }, "name": "潮州", "pinyin": ["chao", "zhou"] }, { "cidx": [1784, 1788], "fullname": "揭阳市", "id": "445200", "location": { "lat": 23.54972, "lng": 116.37271 }, "name": "揭阳", "pinyin": ["jie", "yang"] }, { "cidx": [1789, 1793], "fullname": "云浮市", "id": "445300", "location": { "lat": 22.91525, "lng": 112.04453 }, "name": "云浮", "pinyin": ["yun", "fu"] }, { "cidx": [1794, 1805], "fullname": "南宁市", "id": "450100", "location": { "lat": 22.81673, "lng": 108.3669 }, "name": "南宁", "pinyin": ["nan", "ning"] }, { "cidx": [1806, 1815], "fullname": "柳州市", "id": "450200", "location": { "lat": 24.32543, "lng": 109.41552 }, "name": "柳州", "pinyin": ["liu", "zhou"] }, { "cidx": [1816, 1832], "fullname": "桂林市", "id": "450300", "location": { "lat": 25.27361, "lng": 110.29002 }, "name": "桂林", "pinyin": ["gui", "lin"] }, { "cidx": [1833, 1839], "fullname": "梧州市", "id": "450400", "location": { "lat": 23.47691, "lng": 111.27917 }, "name": "梧州", "pinyin": ["wu", "zhou"] }, { "cidx": [1840, 1843], "fullname": "北海市", "id": "450500", "location": { "lat": 21.48112, "lng": 109.12008 }, "name": "北海", "pinyin": ["bei", "hai"] }, { "cidx": [1844, 1847], "fullname": "防城港市", "id": "450600", "location": { "lat": 21.68713, "lng": 108.35472 }, "name": "防城港", "pinyin": ["fang", "cheng", "gang"] }, { "cidx": [1848, 1851], "fullname": "钦州市", "id": "450700", "location": { "lat": 21.9797, "lng": 108.65431 }, "name": "钦州", "pinyin": ["qin", "zhou"] }, { "cidx": [1852, 1856], "fullname": "贵港市", "id": "450800", "location": { "lat": 23.11306, "lng": 109.59764 }, "name": "贵港", "pinyin": ["gui", "gang"] }, { "cidx": [1857, 1863], "fullname": "玉林市", "id": "450900", "location": { "lat": 22.65451, "lng": 110.18098 }, "name": "玉林", "pinyin": ["yu", "lin"] }, { "cidx": [1864, 1875], "fullname": "百色市", "id": "451000", "location": { "lat": 23.90216, "lng": 106.61838 }, "name": "百色", "pinyin": ["bai", "se"] }, { "cidx": [1876, 1880], "fullname": "贺州市", "id": "451100", "location": { "lat": 24.40346, "lng": 111.56655 }, "name": "贺州", "pinyin": ["he", "zhou"] }, { "cidx": [1881, 1891], "fullname": "河池市", "id": "451200", "location": { "lat": 24.69291, "lng": 108.0854 }, "name": "河池", "pinyin": ["he", "chi"] }, { "cidx": [1892, 1897], "fullname": "来宾市", "id": "451300", "location": { "lat": 23.7521, "lng": 109.22238 }, "name": "来宾", "pinyin": ["lai", "bin"] }, { "cidx": [1898, 1904], "fullname": "崇左市", "id": "451400", "location": { "lat": 22.37895, "lng": 107.36485 }, "name": "崇左", "pinyin": ["chong", "zuo"] }, { "cidx": [1905, 1908], "fullname": "海口市", "id": "460100", "location": { "lat": 20.04422, "lng": 110.19989 }, "name": "海口", "pinyin": ["hai", "kou"] }, { "cidx": [1909, 1912], "fullname": "三亚市", "id": "460200", "location": { "lat": 18.25248, "lng": 109.51209 }, "name": "三亚", "pinyin": ["san", "ya"] }, { "cidx": [1913, 1915], "fullname": "三沙市", "id": "460300", "location": { "lat": 16.83272, "lng": 112.33356 }, "name": "三沙", "pinyin": ["san", "sha"] }, { "cidx": [1916, 1916], "fullname": "儋州市", "id": "460400", "location": { "lat": 19.52093, "lng": 109.58069 }, "name": "儋州", "pinyin": ["dan", "zhou"] }, { "fullname": "五指山市", "id": "469001", "location": { "lat": 18.77515, "lng": 109.51696 }, "name": "五指山", "pinyin": ["wu", "zhi", "shan"] }, { "fullname": "琼海市", "id": "469002", "location": { "lat": 19.25838, "lng": 110.47464 }, "name": "琼海", "pinyin": ["qiong", "hai"] }, { "fullname": "文昌市", "id": "469005", "location": { "lat": 19.54329, "lng": 110.79774 }, "name": "文昌", "pinyin": ["wen", "chang"] }, { "fullname": "万宁市", "id": "469006", "location": { "lat": 18.79532, "lng": 110.38975 }, "name": "万宁", "pinyin": ["wan", "ning"] }, { "fullname": "东方市", "id": "469007", "location": { "lat": 19.09614, "lng": 108.65367 }, "name": "东方", "pinyin": ["dong", "fang"] }, { "fullname": "定安县", "id": "469021", "location": { "lat": 19.68121, "lng": 110.3593 }, "name": "定安", "pinyin": ["ding", "an"] }, { "fullname": "屯昌县", "id": "469022", "location": { "lat": 19.35182, "lng": 110.10347 }, "name": "屯昌", "pinyin": ["tun", "chang"] }, { "fullname": "澄迈县", "id": "469023", "location": { "lat": 19.73849, "lng": 110.00487 }, "name": "澄迈", "pinyin": ["cheng", "mai"] }, { "fullname": "临高县", "id": "469024", "location": { "lat": 19.91243, "lng": 109.69077 }, "name": "临高", "pinyin": ["lin", "gao"] }, { "fullname": "白沙黎族自治县", "id": "469025", "location": { "lat": 19.22543, "lng": 109.45167 }, "name": "白沙", "pinyin": ["bai", "sha"] }, { "fullname": "昌江黎族自治县", "id": "469026", "location": { "lat": 19.29828, "lng": 109.05559 }, "name": "昌江", "pinyin": ["chang", "jiang"] }, { "fullname": "乐东黎族自治县", "id": "469027", "location": { "lat": 18.74986, "lng": 109.17361 }, "name": "乐东", "pinyin": ["le", "dong"] }, { "fullname": "陵水黎族自治县", "id": "469028", "location": { "lat": 18.50596, "lng": 110.0372 }, "name": "陵水", "pinyin": ["ling", "shui"] }, { "fullname": "保亭黎族苗族自治县", "id": "469029", "location": { "lat": 18.63905, "lng": 109.70259 }, "name": "保亭", "pinyin": ["bao", "ting"] }, { "fullname": "琼中黎族苗族自治县", "id": "469030", "location": { "lat": 19.03334, "lng": 109.83839 }, "name": "琼中", "pinyin": ["qiong", "zhong"] }, { "fullname": "万州区", "id": "500101", "location": { "lat": 30.8079, "lng": 108.40873 }, "name": "万州", "pinyin": ["wan", "zhou"] }, { "fullname": "涪陵区", "id": "500102", "location": { "lat": 29.70239, "lng": 107.38779 }, "name": "涪陵", "pinyin": ["fu", "ling"] }, { "fullname": "渝中区", "id": "500103", "location": { "lat": 29.55314, "lng": 106.5686 }, "name": "渝中", "pinyin": ["yu", "zhong"] }, { "fullname": "大渡口区", "id": "500104", "location": { "lat": 29.48408, "lng": 106.48225 }, "name": "大渡口", "pinyin": ["da", "du", "kou"] }, { "fullname": "江北区", "id": "500105", "location": { "lat": 29.60661, "lng": 106.57439 }, "name": "江北", "pinyin": ["jiang", "bei"] }, { "fullname": "沙坪坝区", "id": "500106", "location": { "lat": 29.54098, "lng": 106.45773 }, "name": "沙坪坝", "pinyin": ["sha", "ping", "ba"] }, { "fullname": "九龙坡区", "id": "500107", "location": { "lat": 29.50207, "lng": 106.5114 }, "name": "九龙坡", "pinyin": ["jiu", "long", "po"] }, { "fullname": "南岸区", "id": "500108", "location": { "lat": 29.52168, "lng": 106.56256 }, "name": "南岸", "pinyin": ["nan", "an"] }, { "fullname": "北碚区", "id": "500109", "location": { "lat": 29.80583, "lng": 106.39628 }, "name": "北碚", "pinyin": ["bei", "bei"] }, { "fullname": "綦江区", "id": "500110", "location": { "lat": 28.96463, "lng": 106.92852 }, "name": "綦江", "pinyin": ["qi", "jiang"] }, { "fullname": "大足区", "id": "500111", "location": { "lat": 29.48604, "lng": 105.78017 }, "name": "大足", "pinyin": ["da", "zu"] }, { "fullname": "渝北区", "id": "500112", "location": { "lat": 29.71798, "lng": 106.63043 }, "name": "渝北", "pinyin": ["yu", "bei"] }, { "fullname": "巴南区", "id": "500113", "location": { "lat": 29.40268, "lng": 106.54041 }, "name": "巴南", "pinyin": ["ba", "nan"] }, { "fullname": "黔江区", "id": "500114", "location": { "lat": 29.53322, "lng": 108.771086 }, "name": "黔江", "pinyin": ["qian", "jiang"] }, { "fullname": "长寿区", "id": "500115", "location": { "lat": 29.85781, "lng": 107.08105 }, "name": "长寿", "pinyin": ["chang", "shou"] }, { "fullname": "江津区", "id": "500116", "location": { "lat": 29.29014, "lng": 106.25936 }, "name": "江津", "pinyin": ["jiang", "jin"] }, { "fullname": "合川区", "id": "500117", "location": { "lat": 29.97288, "lng": 106.27679 }, "name": "合川", "pinyin": ["he", "chuan"] }, { "fullname": "永川区", "id": "500118", "location": { "lat": 29.356, "lng": 105.92709 }, "name": "永川", "pinyin": ["yong", "chuan"] }, { "fullname": "南川区", "id": "500119", "location": { "lat": 29.15788, "lng": 107.09896 }, "name": "南川", "pinyin": ["nan", "chuan"] }, { "fullname": "璧山区", "id": "500120", "location": { "lat": 29.59202, "lng": 106.22742 }, "name": "璧山", "pinyin": ["bi", "shan"] }, { "fullname": "铜梁区", "id": "500151", "location": { "lat": 29.84475, "lng": 106.05638 }, "name": "铜梁", "pinyin": ["tong", "liang"] }, { "fullname": "潼南区", "id": "500152", "location": { "lat": 30.19054, "lng": 105.83952 }, "name": "潼南", "pinyin": ["tong", "nan"] }, { "fullname": "荣昌区", "id": "500153", "location": { "lat": 29.41671, "lng": 105.61188 }, "name": "荣昌", "pinyin": ["rong", "chang"] }, { "fullname": "开州区", "id": "500154", "location": { "lat": 31.16098, "lng": 108.39311 }, "name": "开州", "pinyin": ["kai", "zhou"] }, { "fullname": "梁平区", "id": "500155", "location": { "lat": 30.67373, "lng": 107.80235 }, "name": "梁平", "pinyin": ["liang", "ping"] }, { "fullname": "武隆区", "id": "500156", "location": { "lat": 29.32543, "lng": 107.75993 }, "name": "武隆", "pinyin": ["wu", "long"] }, { "fullname": "城口县", "id": "500229", "location": { "lat": 31.94767, "lng": 108.66433 }, "name": "城口", "pinyin": ["cheng", "kou"] }, { "fullname": "丰都县", "id": "500230", "location": { "lat": 29.86352, "lng": 107.73085 }, "name": "丰都", "pinyin": ["feng", "du"] }, { "fullname": "垫江县", "id": "500231", "location": { "lat": 30.3268, "lng": 107.33515 }, "name": "垫江", "pinyin": ["dian", "jiang"] }, { "fullname": "忠县", "id": "500233", "location": { "lat": 30.30026, "lng": 108.03767 }, "name": "忠县", "pinyin": ["zhong", "xian"] }, { "fullname": "云阳县", "id": "500235", "location": { "lat": 30.93063, "lng": 108.69698 }, "name": "云阳", "pinyin": ["yun", "yang"] }, { "fullname": "奉节县", "id": "500236", "location": { "lat": 31.018551, "lng": 109.40093 }, "name": "奉节", "pinyin": ["feng", "jie"] }, { "fullname": "巫山县", "id": "500237", "location": { "lat": 31.07462, "lng": 109.8788 }, "name": "巫山", "pinyin": ["wu", "shan"] }, { "fullname": "巫溪县", "id": "500238", "location": { "lat": 31.3986, "lng": 109.57016 }, "name": "巫溪", "pinyin": ["wu", "xi"] }, { "fullname": "石柱土家族自治县", "id": "500240", "location": { "lat": 29.99968, "lng": 108.11415 }, "name": "石柱", "pinyin": ["shi", "zhu"] }, { "fullname": "秀山土家族苗族自治县", "id": "500241", "location": { "lat": 28.44832, "lng": 109.00714 }, "name": "秀山", "pinyin": ["xiu", "shan"] }, { "fullname": "酉阳土家族苗族自治县", "id": "500242", "location": { "lat": 28.84126, "lng": 108.76778 }, "name": "酉阳", "pinyin": ["you", "yang"] }, { "fullname": "彭水苗族土家族自治县", "id": "500243", "location": { "lat": 29.29376, "lng": 108.16555 }, "name": "彭水", "pinyin": ["peng", "shui"] }, { "cidx": [1917, 1936], "fullname": "成都市", "id": "510100", "location": { "lat": 30.5702, "lng": 104.06476 }, "name": "成都", "pinyin": ["cheng", "du"] }, { "cidx": [1937, 1942], "fullname": "自贡市", "id": "510300", "location": { "lat": 29.3392, "lng": 104.77844 }, "name": "自贡", "pinyin": ["zi", "gong"] }, { "cidx": [1943, 1947], "fullname": "攀枝花市", "id": "510400", "location": { "lat": 26.58228, "lng": 101.71872 }, "name": "攀枝花", "pinyin": ["pan", "zhi", "hua"] }, { "cidx": [1948, 1954], "fullname": "泸州市", "id": "510500", "location": { "lat": 28.8717, "lng": 105.44257 }, "name": "泸州", "pinyin": ["lu", "zhou"] }, { "cidx": [1955, 1960], "fullname": "德阳市", "id": "510600", "location": { "lat": 31.12679, "lng": 104.3979 }, "name": "德阳", "pinyin": ["de", "yang"] }, { "cidx": [1961, 1969], "fullname": "绵阳市", "id": "510700", "location": { "lat": 31.46751, "lng": 104.6796 }, "name": "绵阳", "pinyin": ["mian", "yang"] }, { "cidx": [1970, 1976], "fullname": "广元市", "id": "510800", "location": { "lat": 32.43549, "lng": 105.84357 }, "name": "广元", "pinyin": ["guang", "yuan"] }, { "cidx": [1977, 1981], "fullname": "遂宁市", "id": "510900", "location": { "lat": 30.53286, "lng": 105.59273 }, "name": "遂宁", "pinyin": ["sui", "ning"] }, { "cidx": [1982, 1986], "fullname": "内江市", "id": "511000", "location": { "lat": 29.58015, "lng": 105.05844 }, "name": "内江", "pinyin": ["nei", "jiang"] }, { "cidx": [1987, 1997], "fullname": "乐山市", "id": "511100", "location": { "lat": 29.55221, "lng": 103.76539 }, "name": "乐山", "pinyin": ["le", "shan"] }, { "cidx": [1998, 2006], "fullname": "南充市", "id": "511300", "location": { "lat": 30.83731, "lng": 106.11073 }, "name": "南充", "pinyin": ["nan", "chong"] }, { "cidx": [2007, 2012], "fullname": "眉山市", "id": "511400", "location": { "lat": 30.07563, "lng": 103.84851 }, "name": "眉山", "pinyin": ["mei", "shan"] }, { "cidx": [2013, 2022], "fullname": "宜宾市", "id": "511500", "location": { "lat": 28.7513, "lng": 104.6417 }, "name": "宜宾", "pinyin": ["yi", "bin"] }, { "cidx": [2023, 2028], "fullname": "广安市", "id": "511600", "location": { "lat": 30.45596, "lng": 106.63322 }, "name": "广安", "pinyin": ["guang", "an"] }, { "cidx": [2029, 2035], "fullname": "达州市", "id": "511700", "location": { "lat": 31.20864, "lng": 107.46791 }, "name": "达州", "pinyin": ["da", "zhou"] }, { "cidx": [2036, 2043], "fullname": "雅安市", "id": "511800", "location": { "lat": 30.01053, "lng": 103.0424 }, "name": "雅安", "pinyin": ["ya", "an"] }, { "cidx": [2044, 2048], "fullname": "巴中市", "id": "511900", "location": { "lat": 31.86715, "lng": 106.74733 }, "name": "巴中", "pinyin": ["ba", "zhong"] }, { "cidx": [2049, 2051], "fullname": "资阳市", "id": "512000", "location": { "lat": 30.12859, "lng": 104.62798 }, "name": "资阳", "pinyin": ["zi", "yang"] }, { "cidx": [2052, 2064], "fullname": "阿坝藏族羌族自治州", "id": "513200", "location": { "lat": 31.8994, "lng": 102.22477 }, "name": "阿坝", "pinyin": ["a", "ba"] }, { "cidx": [2065, 2082], "fullname": "甘孜藏族自治州", "id": "513300", "location": { "lat": 30.04932, "lng": 101.96254 }, "name": "甘孜", "pinyin": ["gan", "zi"] }, { "cidx": [2083, 2099], "fullname": "凉山彝族自治州", "id": "513400", "location": { "lat": 27.88164, "lng": 102.26746 }, "name": "凉山", "pinyin": ["liang", "shan"] }, { "cidx": [2100, 2109], "fullname": "贵阳市", "id": "520100", "location": { "lat": 26.64702, "lng": 106.63024 }, "name": "贵阳", "pinyin": ["gui", "yang"] }, { "cidx": [2110, 2113], "fullname": "六盘水市", "id": "520200", "location": { "lat": 26.59336, "lng": 104.83023 }, "name": "六盘水", "pinyin": ["liu", "pan", "shui"] }, { "cidx": [2114, 2127], "fullname": "遵义市", "id": "520300", "location": { "lat": 27.72545, "lng": 106.92723 }, "name": "遵义", "pinyin": ["zun", "yi"] }, { "cidx": [2128, 2133], "fullname": "安顺市", "id": "520400", "location": { "lat": 26.25367, "lng": 105.9462 }, "name": "安顺", "pinyin": ["an", "shun"] }, { "cidx": [2134, 2141], "fullname": "毕节市", "id": "520500", "location": { "lat": 27.29847, "lng": 105.30504 }, "name": "毕节", "pinyin": ["bi", "jie"] }, { "cidx": [2142, 2151], "fullname": "铜仁市", "id": "520600", "location": { "lat": 27.69066, "lng": 109.18099 }, "name": "铜仁", "pinyin": ["tong", "ren"] }, { "cidx": [2152, 2159], "fullname": "黔西南布依族苗族自治州", "id": "522300", "location": { "lat": 25.08988, "lng": 104.90437 }, "name": "黔西南", "pinyin": ["qian", "xi", "nan"] }, { "cidx": [2160, 2175], "fullname": "黔东南苗族侗族自治州", "id": "522600", "location": { "lat": 26.58364, "lng": 107.98416 }, "name": "黔东南", "pinyin": ["qian", "dong", "nan"] }, { "cidx": [2176, 2187], "fullname": "黔南布依族苗族自治州", "id": "522700", "location": { "lat": 26.25427, "lng": 107.52226 }, "name": "黔南", "pinyin": ["qian", "nan"] }, { "cidx": [2188, 2201], "fullname": "昆明市", "id": "530100", "location": { "lat": 24.87966, "lng": 102.83322 }, "name": "昆明", "pinyin": ["kun", "ming"] }, { "cidx": [2202, 2210], "fullname": "曲靖市", "id": "530300", "location": { "lat": 25.49002, "lng": 103.79625 }, "name": "曲靖", "pinyin": ["qu", "jing"] }, { "cidx": [2211, 2219], "fullname": "玉溪市", "id": "530400", "location": { "lat": 24.3518, "lng": 102.54714 }, "name": "玉溪", "pinyin": ["yu", "xi"] }, { "cidx": [2220, 2224], "fullname": "保山市", "id": "530500", "location": { "lat": 25.11205, "lng": 99.16181 }, "name": "保山", "pinyin": ["bao", "shan"] }, { "cidx": [2225, 2235], "fullname": "昭通市", "id": "530600", "location": { "lat": 27.33817, "lng": 103.7168 }, "name": "昭通", "pinyin": ["zhao", "tong"] }, { "cidx": [2236, 2240], "fullname": "丽江市", "id": "530700", "location": { "lat": 26.85648, "lng": 100.2271 }, "name": "丽江", "pinyin": ["li", "jiang"] }, { "cidx": [2241, 2250], "fullname": "普洱市", "id": "530800", "location": { "lat": 22.82521, "lng": 100.96624 }, "name": "普洱", "pinyin": ["pu", "er"] }, { "cidx": [2251, 2258], "fullname": "临沧市", "id": "530900", "location": { "lat": 23.88426, "lng": 100.08884 }, "name": "临沧", "pinyin": ["lin", "cang"] }, { "cidx": [2259, 2268], "fullname": "楚雄彝族自治州", "id": "532300", "location": { "lat": 25.04495, "lng": 101.52767 }, "name": "楚雄", "pinyin": ["chu", "xiong"] }, { "cidx": [2269, 2281], "fullname": "红河哈尼族彝族自治州", "id": "532500", "location": { "lat": 23.36422, "lng": 103.3756 }, "name": "红河", "pinyin": ["hong", "he"] }, { "cidx": [2282, 2289], "fullname": "文山壮族苗族自治州", "id": "532600", "location": { "lat": 23.39849, "lng": 104.21504 }, "name": "文山", "pinyin": ["wen", "shan"] }, { "cidx": [2290, 2292], "fullname": "西双版纳傣族自治州", "id": "532800", "location": { "lat": 22.00749, "lng": 100.79739 }, "name": "西双版纳", "pinyin": ["xi", "shuang", "ban", "na"] }, { "cidx": [2293, 2304], "fullname": "大理白族自治州", "id": "532900", "location": { "lat": 25.60648, "lng": 100.26764 }, "name": "大理", "pinyin": ["da", "li"] }, { "cidx": [2305, 2309], "fullname": "德宏傣族景颇族自治州", "id": "533100", "location": { "lat": 24.43232, "lng": 98.58486 }, "name": "德宏", "pinyin": ["de", "hong"] }, { "cidx": [2310, 2313], "fullname": "怒江傈僳族自治州", "id": "533300", "location": { "lat": 25.81763, "lng": 98.8567 }, "name": "怒江", "pinyin": ["nu", "jiang"] }, { "cidx": [2314, 2316], "fullname": "迪庆藏族自治州", "id": "533400", "location": { "lat": 27.81908, "lng": 99.70305 }, "name": "迪庆", "pinyin": ["di", "qing"] }, { "cidx": [2317, 2324], "fullname": "拉萨市", "id": "540100", "location": { "lat": 29.64415, "lng": 91.1145 }, "name": "拉萨", "pinyin": ["la", "sa"] }, { "cidx": [2325, 2342], "fullname": "日喀则市", "id": "540200", "location": { "lat": 29.26705, "lng": 88.88116 }, "name": "日喀则", "pinyin": ["ri", "ka", "ze"] }, { "cidx": [2343, 2353], "fullname": "昌都市", "id": "540300", "location": { "lat": 31.14073, "lng": 97.17225 }, "name": "昌都", "pinyin": ["chang", "du"] }, { "cidx": [2354, 2360], "fullname": "林芝市", "id": "540400", "location": { "lat": 29.64895, "lng": 94.36155 }, "name": "林芝", "pinyin": ["lin", "zhi"] }, { "cidx": [2361, 2372], "fullname": "山南市", "id": "540500", "location": { "lat": 29.23705, "lng": 91.77313 }, "name": "山南", "pinyin": ["shan", "nan"] }, { "cidx": [2373, 2383], "fullname": "那曲市", "id": "540600", "location": { "lat": 31.47614, "lng": 92.05136 }, "name": "那曲", "pinyin": ["na", "qu"] }, { "cidx": [2384, 2390], "fullname": "阿里地区", "id": "542500", "location": { "lat": 30.40051, "lng": 81.1454 }, "name": "阿里", "pinyin": ["a", "li"] }, { "cidx": [2391, 2403], "fullname": "西安市", "id": "610100", "location": { "lat": 34.34127, "lng": 108.93984 }, "name": "西安", "pinyin": ["xi", "an"] }, { "cidx": [2404, 2407], "fullname": "铜川市", "id": "610200", "location": { "lat": 34.89673, "lng": 108.94515 }, "name": "铜川", "pinyin": ["tong", "chuan"] }, { "cidx": [2408, 2419], "fullname": "宝鸡市", "id": "610300", "location": { "lat": 34.36194, "lng": 107.23732 }, "name": "宝鸡", "pinyin": ["bao", "ji"] }, { "cidx": [2420, 2433], "fullname": "咸阳市", "id": "610400", "location": { "lat": 34.32932, "lng": 108.70929 }, "name": "咸阳", "pinyin": ["xian", "yang"] }, { "cidx": [2434, 2444], "fullname": "渭南市", "id": "610500", "location": { "lat": 34.49997, "lng": 109.51015 }, "name": "渭南", "pinyin": ["wei", "nan"] }, { "cidx": [2445, 2457], "fullname": "延安市", "id": "610600", "location": { "lat": 36.58529, "lng": 109.48978 }, "name": "延安", "pinyin": ["yan", "an"] }, { "cidx": [2458, 2468], "fullname": "汉中市", "id": "610700", "location": { "lat": 33.06761, "lng": 107.02377 }, "name": "汉中", "pinyin": ["han", "zhong"] }, { "cidx": [2469, 2480], "fullname": "榆林市", "id": "610800", "location": { "lat": 38.2852, "lng": 109.73458 }, "name": "榆林", "pinyin": ["yu", "lin"] }, { "cidx": [2481, 2490], "fullname": "安康市", "id": "610900", "location": { "lat": 32.68486, "lng": 109.02932 }, "name": "安康", "pinyin": ["an", "kang"] }, { "cidx": [2491, 2497], "fullname": "商洛市", "id": "611000", "location": { "lat": 33.87036, "lng": 109.94041 }, "name": "商洛", "pinyin": ["shang", "luo"] }, { "cidx": [2498, 2505], "fullname": "兰州市", "id": "620100", "location": { "lat": 36.06138, "lng": 103.83417 }, "name": "兰州", "pinyin": ["lan", "zhou"] }, { "cidx": [2506, 2506], "fullname": "嘉峪关市", "id": "620200", "location": { "lat": 39.77194, "lng": 98.28971 }, "name": "嘉峪关", "pinyin": ["jia", "yu", "guan"] }, { "cidx": [2507, 2508], "fullname": "金昌市", "id": "620300", "location": { "lat": 38.52006, "lng": 102.18759 }, "name": "金昌", "pinyin": ["jin", "chang"] }, { "cidx": [2509, 2513], "fullname": "白银市", "id": "620400", "location": { "lat": 36.5447, "lng": 104.13773 }, "name": "白银", "pinyin": ["bai", "yin"] }, { "cidx": [2514, 2520], "fullname": "天水市", "id": "620500", "location": { "lat": 34.58085, "lng": 105.72486 }, "name": "天水", "pinyin": ["tian", "shui"] }, { "cidx": [2521, 2524], "fullname": "武威市", "id": "620600", "location": { "lat": 37.9282, "lng": 102.63797 }, "name": "武威", "pinyin": ["wu", "wei"] }, { "cidx": [2525, 2530], "fullname": "张掖市", "id": "620700", "location": { "lat": 38.92592, "lng": 100.44981 }, "name": "张掖", "pinyin": ["zhang", "ye"] }, { "cidx": [2531, 2537], "fullname": "平凉市", "id": "620800", "location": { "lat": 35.54303, "lng": 106.6653 }, "name": "平凉", "pinyin": ["ping", "liang"] }, { "cidx": [2538, 2544], "fullname": "酒泉市", "id": "620900", "location": { "lat": 39.73255, "lng": 98.49394 }, "name": "酒泉", "pinyin": ["jiu", "quan"] }, { "cidx": [2545, 2552], "fullname": "庆阳市", "id": "621000", "location": { "lat": 35.70978, "lng": 107.64292 }, "name": "庆阳", "pinyin": ["qing", "yang"] }, { "cidx": [2553, 2559], "fullname": "定西市", "id": "621100", "location": { "lat": 35.58113, "lng": 104.62524 }, "name": "定西", "pinyin": ["ding", "xi"] }, { "cidx": [2560, 2568], "fullname": "陇南市", "id": "621200", "location": { "lat": 33.401, "lng": 104.92166 }, "name": "陇南", "pinyin": ["long", "nan"] }, { "cidx": [2569, 2576], "fullname": "临夏回族自治州", "id": "622900", "location": { "lat": 35.60122, "lng": 103.21091 }, "name": "临夏", "pinyin": ["lin", "xia"] }, { "cidx": [2577, 2584], "fullname": "甘南藏族自治州", "id": "623000", "location": { "lat": 34.98327, "lng": 102.91102 }, "name": "甘南", "pinyin": ["gan", "nan"] }, { "cidx": [2585, 2591], "fullname": "西宁市", "id": "630100", "location": { "lat": 36.61729, "lng": 101.77782 }, "name": "西宁", "pinyin": ["xi", "ning"] }, { "cidx": [2592, 2597], "fullname": "海东市", "id": "630200", "location": { "lat": 36.48209, "lng": 102.40173 }, "name": "海东", "pinyin": ["hai", "dong"] }, { "cidx": [2598, 2601], "fullname": "海北藏族自治州", "id": "632200", "location": { "lat": 36.95454, "lng": 100.90096 }, "name": "海北", "pinyin": ["hai", "bei"] }, { "cidx": [2602, 2605], "fullname": "黄南藏族自治州", "id": "632300", "location": { "lat": 35.51991, "lng": 102.01507 }, "name": "黄南", "pinyin": ["huang", "nan"] }, { "cidx": [2606, 2610], "fullname": "海南藏族自治州", "id": "632500", "location": { "lat": 36.28663, "lng": 100.62037 }, "name": "海南", "pinyin": ["hai", "nan"] }, { "cidx": [2611, 2616], "fullname": "果洛藏族自治州", "id": "632600", "location": { "lat": 34.47141, "lng": 100.24475 }, "name": "果洛", "pinyin": ["guo", "luo"] }, { "cidx": [2617, 2622], "fullname": "玉树藏族自治州", "id": "632700", "location": { "lat": 33.00528, "lng": 97.0065 }, "name": "玉树", "pinyin": ["yu", "shu"] }, { "cidx": [2623, 2629], "fullname": "海西蒙古族藏族自治州", "id": "632800", "location": { "lat": 37.3771, "lng": 97.37122 }, "name": "海西", "pinyin": ["hai", "xi"] }, { "cidx": [2630, 2635], "fullname": "银川市", "id": "640100", "location": { "lat": 38.48644, "lng": 106.23248 }, "name": "银川", "pinyin": ["yin", "chuan"] }, { "cidx": [2636, 2638], "fullname": "石嘴山市", "id": "640200", "location": { "lat": 38.9841, "lng": 106.38418 }, "name": "石嘴山", "pinyin": ["shi", "zui", "shan"] }, { "cidx": [2639, 2643], "fullname": "吴忠市", "id": "640300", "location": { "lat": 37.99755, "lng": 106.19879 }, "name": "吴忠", "pinyin": ["wu", "zhong"] }, { "cidx": [2644, 2648], "fullname": "固原市", "id": "640400", "location": { "lat": 36.0158, "lng": 106.24259 }, "name": "固原", "pinyin": ["gu", "yuan"] }, { "cidx": [2649, 2651], "fullname": "中卫市", "id": "640500", "location": { "lat": 37.50026, "lng": 105.19676 }, "name": "中卫", "pinyin": ["zhong", "wei"] }, { "cidx": [2652, 2659], "fullname": "乌鲁木齐市", "id": "650100", "location": { "lat": 43.82663, "lng": 87.61688 }, "name": "乌鲁木齐", "pinyin": ["wu", "lu", "mu", "qi"] }, { "cidx": [2660, 2663], "fullname": "克拉玛依市", "id": "650200", "location": { "lat": 45.57999, "lng": 84.88927 }, "name": "克拉玛依", "pinyin": ["ke", "la", "ma", "yi"] }, { "cidx": [2664, 2666], "fullname": "吐鲁番市", "id": "650400", "location": { "lat": 42.9513, "lng": 89.18954 }, "name": "吐鲁番", "pinyin": ["tu", "lu", "fan"] }, { "cidx": [2667, 2669], "fullname": "哈密市", "id": "650500", "location": { "lat": 42.81855, "lng": 93.51538 }, "name": "哈密", "pinyin": ["ha", "mi"] }, { "cidx": [2670, 2676], "fullname": "昌吉回族自治州", "id": "652300", "location": { "lat": 44.01117, "lng": 87.30822 }, "name": "昌吉", "pinyin": ["chang", "ji"] }, { "cidx": [2677, 2680], "fullname": "博尔塔拉蒙古自治州", "id": "652700", "location": { "lat": 44.90597, "lng": 82.06665 }, "name": "博州", "pinyin": ["bo", "zhou"] }, { "cidx": [2681, 2689], "fullname": "巴音郭楞蒙古自治州", "id": "652800", "location": { "lat": 41.76404, "lng": 86.14517 }, "name": "巴州", "pinyin": ["ba", "zhou"] }, { "cidx": [2690, 2698], "fullname": "阿克苏地区", "id": "652900", "location": { "lat": 41.16842, "lng": 80.26008 }, "name": "阿克苏", "pinyin": ["a", "ke", "su"] }, { "cidx": [2699, 2702], "fullname": "克孜勒苏柯尔克孜自治州", "id": "653000", "location": { "lat": 39.7153, "lng": 76.16661 }, "name": "克州", "pinyin": ["ke", "zhou"] }, { "cidx": [2703, 2714], "fullname": "喀什地区", "id": "653100", "location": { "lat": 39.47042, "lng": 75.98976 }, "name": "喀什", "pinyin": ["ka", "shi"] }, { "cidx": [2715, 2722], "fullname": "和田地区", "id": "653200", "location": { "lat": 37.11431, "lng": 79.92247 }, "name": "和田", "pinyin": ["he", "tian"] }, { "cidx": [2723, 2733], "fullname": "伊犁哈萨克自治州", "id": "654000", "location": { "lat": 43.91689, "lng": 81.32416 }, "name": "伊犁", "pinyin": ["yi", "li"] }, { "cidx": [2734, 2740], "fullname": "塔城地区", "id": "654200", "location": { "lat": 46.74532, "lng": 82.98046 }, "name": "塔城", "pinyin": ["ta", "cheng"] }, { "cidx": [2741, 2747], "fullname": "阿勒泰地区", "id": "654300", "location": { "lat": 47.84564, "lng": 88.14023 }, "name": "阿勒泰", "pinyin": ["a", "le", "tai"] }, { "fullname": "石河子市", "id": "659001", "location": { "lat": 44.30653, "lng": 86.07893 }, "name": "石河子", "pinyin": ["shi", "he", "zi"] }, { "fullname": "阿拉尔市", "id": "659002", "location": { "lat": 40.54798, "lng": 81.28067 }, "name": "阿拉尔", "pinyin": ["a", "la", "er"] }, { "fullname": "图木舒克市", "id": "659003", "location": { "lat": 39.86495, "lng": 79.06902 }, "name": "图木舒克", "pinyin": ["tu", "mu", "shu", "ke"] }, { "fullname": "五家渠市", "id": "659004", "location": { "lat": 44.16799, "lng": 87.54017 }, "name": "五家渠", "pinyin": ["wu", "jia", "qu"] }, { "fullname": "北屯市", "id": "659005", "location": { "lat": 47.36327, "lng": 87.80014 }, "name": "北屯", "pinyin": ["bei", "tun"] }, { "fullname": "铁门关市", "id": "659006", "location": { "lat": 41.86868, "lng": 85.67583 }, "name": "铁门关", "pinyin": ["tie", "men", "guan"] }, { "fullname": "双河市", "id": "659007", "location": { "lat": 44.84418, "lng": 82.35501 }, "name": "双河", "pinyin": ["shuang", "he"] }, { "fullname": "可克达拉市", "id": "659008", "location": { "lat": 43.94799, "lng": 81.04476 }, "name": "可克达拉", "pinyin": ["ke", "ke", "da", "la"] }, { "fullname": "昆玉市", "id": "659009", "location": { "lat": 37.20948, "lng": 79.29133 }, "name": "昆玉", "pinyin": ["kun", "yu"] }, { "cidx": [2748, 2759], "fullname": "台北市", "id": "710100", "location": { "lat": 25.030724, "lng": 121.520076 }, "name": "台北", "pinyin": ["tai", "bei"] }, { "cidx": [2760, 2797], "fullname": "高雄市", "id": "710200", "location": { "lat": 22.630576, "lng": 120.306839 }, "name": "高雄", "pinyin": ["gao", "xiong"] }, { "cidx": [2798, 2834], "fullname": "台南市", "id": "710300", "location": { "lat": 22.998601, "lng": 120.187817 }, "name": "台南", "pinyin": ["tai", "nan"] }, { "cidx": [2835, 2863], "fullname": "台中市", "id": "710400", "location": { "lat": 24.143171, "lng": 120.679882 }, "name": "台中", "pinyin": ["tai", "zhong"] }, { "cidx": [2864, 2876], "fullname": "南投县", "id": "710600", "location": { "lat": 23.919619, "lng": 120.670008 }, "name": "南投", "pinyin": ["nan", "tou"] }, { "cidx": [2877, 2883], "fullname": "基隆市", "id": "710700", "location": { "lat": 25.122105, "lng": 121.741526 }, "name": "基隆", "pinyin": ["ji", "long"] }, { "cidx": [2884, 2886], "fullname": "新竹市", "id": "710800", "location": { "lat": 24.784924, "lng": 120.990745 }, "name": "新竹", "pinyin": ["xin", "zhu"] }, { "cidx": [2887, 2888], "fullname": "嘉义市", "id": "710900", "location": { "lat": 23.485079, "lng": 120.472462 }, "name": "嘉义", "pinyin": ["jia", "yi"] }, { "cidx": [2889, 2917], "fullname": "新北市", "id": "711100", "location": { "lat": 25.1853, "lng": 121.663675 }, "name": "新北", "pinyin": ["xin", "bei"] }, { "cidx": [2918, 2929], "fullname": "宜兰县", "id": "711200", "location": { "lat": 24.759707, "lng": 121.754442 }, "name": "宜兰", "pinyin": ["yi", "lan"] }, { "cidx": [2930, 2942], "fullname": "新竹县", "id": "711300", "location": { "lat": 24.839233, "lng": 121.002012 }, "name": "新竹", "pinyin": ["xin", "zhu"] }, { "cidx": [2943, 2955], "fullname": "桃园市", "id": "711400", "location": { "lat": 24.982757, "lng": 121.213608 }, "name": "桃园", "pinyin": ["tao", "yuan"] }, { "cidx": [2956, 2973], "fullname": "苗栗县", "id": "711500", "location": { "lat": 24.696762, "lng": 120.884337 }, "name": "苗栗", "pinyin": ["miao", "li"] }, { "cidx": [2974, 2999], "fullname": "彰化县", "id": "711700", "location": { "lat": 24.068523, "lng": 120.557479 }, "name": "彰化", "pinyin": ["zhang", "hua"] }, { "cidx": [3000, 3017], "fullname": "嘉义县", "id": "711900", "location": { "lat": 23.434473, "lng": 120.624255 }, "name": "嘉义", "pinyin": ["jia", "yi"] }, { "cidx": [3018, 3037], "fullname": "云林县", "id": "712100", "location": { "lat": 23.664943, "lng": 120.480738 }, "name": "云林", "pinyin": ["yun", "lin"] }, { "cidx": [3038, 3070], "fullname": "屏东县", "id": "712400", "location": { "lat": 22.666716, "lng": 120.492005 }, "name": "屏东", "pinyin": ["ping", "dong"] }, { "cidx": [3071, 3086], "fullname": "台东县", "id": "712500", "location": { "lat": 22.764364, "lng": 121.113207 }, "name": "台东", "pinyin": ["tai", "dong"] }, { "cidx": [3087, 3099], "fullname": "花莲县", "id": "712600", "location": { "lat": 24.000674, "lng": 121.59729 }, "name": "花莲", "pinyin": ["hua", "lian"] }, { "cidx": [3100, 3105], "fullname": "澎湖县", "id": "712700", "location": { "lat": 23.552351, "lng": 119.58457 }, "name": "澎湖", "pinyin": ["peng", "hu"] }, { "fullname": "中西区", "id": "810101", "location": { "lat": 22.27629, "lng": 114.16368 }, "name": "中西区", "pinyin": ["zhong", "xi", "qu"] }, { "fullname": "东区", "id": "810102", "location": { "lat": 22.28137, "lng": 114.22914 }, "name": "东区", "pinyin": ["dong", "qu"] }, { "fullname": "九龙城区", "id": "810103", "location": { "lat": 22.30818, "lng": 114.18895 }, "name": "九龙", "pinyin": ["jiu", "long"] }, { "fullname": "观塘区", "id": "810104", "location": { "lat": 22.31057, "lng": 114.2306 }, "name": "观塘区", "pinyin": ["guan", "tang", "qu"] }, { "fullname": "南区", "id": "810105", "location": { "lat": 22.24543, "lng": 114.15806 }, "name": "南区", "pinyin": ["nan", "qu"] }, { "fullname": "深水埗区", "id": "810106", "location": { "lat": 22.32921, "lng": 114.16856 }, "name": "深水埗区", "pinyin": ["shen", "shui", "bu", "qu"] }, { "fullname": "湾仔区", "id": "810107", "location": { "lat": 22.27469, "lng": 114.17778 }, "name": "湾仔区", "pinyin": ["wan", "zi", "qu"] }, { "fullname": "黄大仙区", "id": "810108", "location": { "lat": 22.34003, "lng": 114.19584 }, "name": "黄大仙区", "pinyin": ["huang", "da", "xian", "qu"] }, { "fullname": "油尖旺区", "id": "810109", "location": { "lat": 22.31898, "lng": 114.17738 }, "name": "油尖旺区", "pinyin": ["you", "jian", "wang", "qu"] }, { "fullname": "离岛区", "id": "810110", "location": { "lat": 22.2817, "lng": 113.94691 }, "name": "离岛区", "pinyin": ["li", "dao", "qu"] }, { "fullname": "葵青区", "id": "810111", "location": { "lat": 22.36055, "lng": 114.13654 }, "name": "葵青区", "pinyin": ["kui", "qing", "qu"] }, { "fullname": "北区", "id": "810112", "location": { "lat": 22.49181, "lng": 114.14312 }, "name": "北区", "pinyin": ["bei", "qu"] }, { "fullname": "西贡区", "id": "810113", "location": { "lat": 22.37943, "lng": 114.27699 }, "name": "西贡区", "pinyin": ["xi", "gong", "qu"] }, { "fullname": "沙田区", "id": "810114", "location": { "lat": 22.3827, "lng": 114.19191 }, "name": "沙田区", "pinyin": ["sha", "tian", "qu"] }, { "fullname": "屯门区", "id": "810115", "location": { "lat": 22.38767, "lng": 113.98029 }, "name": "屯门区", "pinyin": ["tun", "men", "qu"] }, { "fullname": "大埔区", "id": "810116", "location": { "lat": 22.448, "lng": 114.16946 }, "name": "大埔区", "pinyin": ["da", "pu", "qu"] }, { "fullname": "荃湾区", "id": "810117", "location": { "lat": 22.37145, "lng": 114.12001 }, "name": "荃湾区", "pinyin": ["quan", "wan", "qu"] }, { "fullname": "元朗区", "id": "810118", "location": { "lat": 22.44243, "lng": 114.03181 }, "name": "元朗区", "pinyin": ["yuan", "lang", "qu"] }, { "fullname": "澳门半岛", "id": "820101", "location": { "lat": 22.18684, "lng": 113.54294 }, "name": "澳门半岛", "pinyin": ["ao", "men", "ban", "dao"] }, { "fullname": "凼仔", "id": "820102", "location": { "lat": 22.15473, "lng": 113.55929 }, "name": "凼仔", "pinyin": ["dang", "zi"] }, { "fullname": "路凼城", "id": "820103", "location": { "lat": 22.13867, "lng": 113.56828 }, "name": "路凼城", "pinyin": ["lu", "dang", "cheng"] }, { "fullname": "路环", "id": "820104", "location": { "lat": 22.11501, "lng": 113.55724 }, "name": "路环", "pinyin": ["lu", "huan"] }] ================================================ FILE: examples/address-book/pages/index/index.js ================================================ import {cityData} from './data' Component({ data: { list: [], }, lifetimes: { attached() { const cities = cityData // 按拼音排序 cities.sort((c1, c2) => { const pinyin1 = c1.pinyin.join('') const pinyin2 = c2.pinyin.join('') return pinyin1.localeCompare(pinyin2) }) // 添加首字母 const map = new Map() for (const city of cities) { const alpha = city.pinyin[0].charAt(0).toUpperCase() if (!map.has(alpha)) map.set(alpha, []) map.get(alpha).push({ name: city.fullname }) } const keys = [] for (const key of map.keys()) { keys.push(key) } keys.sort() const list = [] for (const key of keys) { list.push({ alpha: key, subItems: map.get(key) }) } console.log('address-book list:', list) this.setData({ list }) }, }, }) ================================================ FILE: examples/address-book/pages/index/index.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar", "address-book": "../../components/address-book" }, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/address-book/pages/index/index.wxml ================================================ Address Book 类通讯录列表 ================================================ FILE: examples/address-book/pages/index/index.wxss ================================================ page { display: flex; flex-direction: column; align-items: flex-end; height: 100vh; background-color: #f7f7f7; } .page-container { width: 100vw; flex: 1; overflow: hidden; } .page { color: rgba(0, 0, 0, .9); font-size: 16px; font-family: -apple-system-font, Helvetica Neue, Helvetica, sans-serif; } .page__hd { padding: 40px } .page__bd { padding-bottom: 40px } .page__title { text-align: left; font-size: 20px; font-weight: 400 } .page__desc { margin-top: 5px; color: rgba(0, 0, 0, .5); text-align: left; font-size: 14px } .header { height: 100px; } .cell { height: 50px; justify-content: center; align-items: center; } ================================================ FILE: examples/address-book/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "latest", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": true, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 }, "projectname": "address-book" } ================================================ FILE: examples/address-book/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "address-book", "setting": { "compileHotReLoad": false, "skylineRenderEnable": true }, "libVersion": "latest" } ================================================ FILE: examples/address-book/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/album/app.js ================================================ App({}) ================================================ FILE: examples/album/app.json ================================================ { "pages": [ "pages/album/index", "pages/preview/index" ], "window": { "backgroundColor": "#ffffff", "backgroundTextStyle": "dark", "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "", "navigationBarTextStyle": "black" }, "style": "v2", "sitemapLocation": "sitemap.json", "lazyCodeLoading": "requiredComponents", "componentFramework": "glass-easel", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } } } ================================================ FILE: examples/album/app.wxss ================================================ ================================================ FILE: examples/album/components/album/album-image/index.js ================================================ import EventBus from '../../../utils/event-bus' import { getShared, getRunOnUI } from '../../../utils/worklet' const IMAGE_INIT_WIDTH = 0 const IMAGE_INIT_HEIGHT = 1 const IMAGE_WIDTH = 2 const IMAGE_HEIGHT = 3 const IMAGE_TARGET_WIDTH = 4 const IMAGE_TARGET_HEIGHT = 5 const IMAGE_RATIO = 6 const IN_PREVIEW = 7 let screenWidth = 0 let screenHeight = 0 Component({ properties: { image: { type: Object, value: {}, }, src: { type: String, value: '', }, width: { type: Number, value: 0, }, height: { type: Number, value: 0, }, }, observers: { 'width, height'() { this.renderImage() }, }, lifetimes: { created() { const shared = getShared(this.renderer) this.sharedValues = [ shared(0), // IMAGE_INIT_WIDTH shared(0), // IMAGE_INIT_HEIGHT shared(0), // IMAGE_WIDTH shared(0), // IMAGE_HEIGHT shared(0), // IMAGE_TARGET_WIDTH shared(0), // IMAGE_TARGET_HEIGHT shared(0), // IMAGE_RATIO shared(false), // IN_PREVIEW ] }, attached() { const pageId = this.getPageId() const uniqueId = `${pageId}-${this.data.image.id}` const sharedValues = this.sharedValues ?? [] this.uniqueId = uniqueId getRunOnUI(this.renderer)(() => { 'worklet' if (!globalThis.temp[`${uniqueId}CustomRouteBack`]) { globalThis.temp[`${uniqueId}CustomRouteBack`] = args => { if (!sharedValues[IN_PREVIEW].value) return // 在预览页拖拽返回时会有 scale 效果,所以需要矫正第 0 帧时的宽高,不然会在切 share-element 时突然失去 scale 效果 const targetImageWidth = sharedValues[IMAGE_TARGET_WIDTH].value const targetImageHeight = sharedValues[IMAGE_TARGET_HEIGHT].value const scale = args.scale sharedValues[IMAGE_WIDTH].value = targetImageWidth * scale sharedValues[IMAGE_HEIGHT].value = targetImageHeight * scale } globalThis.eventBus.on(`${pageId}CustomRouteBack`, globalThis.temp[`${uniqueId}CustomRouteBack`]) } })() this.applyAnimatedStyle('.img', () => { 'worklet' let width = `${sharedValues[IMAGE_WIDTH].value}px` if (sharedValues[IMAGE_WIDTH].value === 0) { width = `` } let height = `${sharedValues[IMAGE_HEIGHT].value}px` if (sharedValues[IMAGE_HEIGHT].value === 0) { height = `` } return { width, height, } }) const resetShareValues = () => { sharedValues[IMAGE_WIDTH].value = sharedValues[IMAGE_INIT_WIDTH].value sharedValues[IMAGE_HEIGHT].value = sharedValues[IMAGE_INIT_HEIGHT].value sharedValues[IN_PREVIEW].value = false } // 监听预览页图片切换 this._onPreviewerChange = image => { if (image.id === this.data.image.id) { // 切到当前图片了 sharedValues[IN_PREVIEW].value = true } else { // 切到其他图片了,恢复原样 resetShareValues() } } EventBus.on(`${pageId}PreviewerChange`, this._onPreviewerChange) this._onPreviewerHide = () => { // 预览页销毁了,恢复原样 // 这里可能有返回动画,所以延迟 reset setTimeout(resetShareValues, 500) } EventBus.on(`${pageId}PreviewerDestroy`, this._onPreviewerHide) }, detached() { const pageId = this.getPageId() const uniqueId = this.uniqueId getRunOnUI(this.renderer)(() => { 'worklet' if (globalThis.temp[`${uniqueId}CustomRouteBack`]) { globalThis.eventBus.off(`${pageId}CustomRouteBack`, globalThis.temp[`${uniqueId}CustomRouteBack`]) delete globalThis.temp[`${uniqueId}CustomRouteBack`] } })() EventBus.off(`${pageId}PreviewerChange`, this._onPreviewerChange) EventBus.off(`${pageId}PreviewerDestroy`, this._onPreviewerHide) }, }, methods: { onFrame(evt) { 'worklet' console.log('worklet onFrame', evt) // 进入预览页的动画,会逐帧调用 // 在此回调中需要根据当前容器的大小来调整图片的大小,因为图片是自己设置宽高渲染的,不能根据容器宽高自适应 const rect = evt.current const sharedValues = this.sharedValues ?? [] const cntWidth = rect.width const cntHeight = rect.height const progress = evt.progress // 当前动画进度 const imageRatio = sharedValues[IMAGE_RATIO].value const isPop = evt.direction === 1 let width = cntWidth let height = cntHeight if (imageRatio) { const cntRatio = cntWidth / cntHeight if (cntRatio > imageRatio) height = cntWidth / imageRatio else if (cntRatio <= imageRatio) width = cntHeight * imageRatio // 获取图片的初始大小和目标大小 const initImageWidth = sharedValues[IMAGE_INIT_WIDTH].value const initImageHeight = sharedValues[IMAGE_INIT_HEIGHT].value const targetImageWidth = sharedValues[IMAGE_TARGET_WIDTH].value const targetImageHeight = sharedValues[IMAGE_TARGET_HEIGHT].value if (initImageWidth && initImageHeight && targetImageWidth && targetImageHeight) { if (isPop) { // 退出动画 width = targetImageWidth - (targetImageWidth - initImageWidth) * progress height = targetImageHeight - (targetImageHeight - initImageHeight) * progress } else { // 进入动画 width = initImageWidth + (targetImageWidth - initImageWidth) * progress height = initImageHeight + (targetImageHeight - initImageHeight) * progress } } } sharedValues[IMAGE_WIDTH].value = width sharedValues[IMAGE_HEIGHT].value = height }, onImageLoad(evt) { const { width, height } = evt.detail const sharedValues = this.sharedValues ?? [] const imageRatio = width / height this.imageRatio = imageRatio sharedValues[IMAGE_RATIO].value = imageRatio this.renderImage() }, renderImage() { // 因为目标预览页的 mode 是 aspectFill,为了保证进入预览页的动画过程中不会发生 mode 跳变问题,故此处使用 aspectFill,然后手动裁剪成 aspectFit const sharedValues = this.sharedValues ?? [] const { width: cntWidth, height: cntHeight } = this.data const imageRatio = this.imageRatio if (!screenWidth || !screenHeight) { const systemInfo = wx.getSystemInfoSync() screenWidth = systemInfo.screenWidth screenHeight = systemInfo.screenHeight } if (cntWidth && cntHeight && imageRatio) { let initWidth = cntWidth let initHeight = cntHeight let targetImageWidth = screenWidth let targetImageHeight = screenHeight const cntRatio = cntWidth / cntHeight const targetRatio = screenWidth / screenHeight if (cntRatio > imageRatio) { initHeight = cntWidth / imageRatio } else if (cntRatio < imageRatio) { initWidth = cntHeight * imageRatio } if (targetRatio > imageRatio) { targetImageWidth = targetImageHeight * imageRatio } else if (targetRatio < imageRatio) { targetImageHeight = targetImageWidth / imageRatio } // 在相册页面时的初始大小 sharedValues[IMAGE_INIT_WIDTH].value = initWidth sharedValues[IMAGE_INIT_HEIGHT].value = initHeight // 当前图片的大小 sharedValues[IMAGE_WIDTH].value = initWidth sharedValues[IMAGE_HEIGHT].value = initHeight // 动画到预览页时的目标大小 sharedValues[IMAGE_TARGET_WIDTH].value = targetImageWidth sharedValues[IMAGE_TARGET_HEIGHT].value = targetImageHeight } }, }, }) ================================================ FILE: examples/album/components/album/album-image/index.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/album/album-image/index.wxml ================================================ ================================================ FILE: examples/album/components/album/album-image/index.wxss ================================================ .share-element { position: relative; width: 100%; height: 100%; } .img-cut-cnt { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: transparent; overflow: hidden; } .img { position: absolute; top: 50%; left: 50%; width: 100%; height: 100%; transform: translate(-50%, -50%); transform-origin: center; z-index: 5; } ================================================ FILE: examples/album/components/album/index.js ================================================ import EventBus from '../../utils/event-bus' import { initRoute } from './route' function transformListToLineBlock(list, lineLimit) { const lineList = [] for (let i = 0, len = list.length; i < len; i += lineLimit) { const line = { index: Math.floor(i / lineLimit), list: [] } for (let j = 0; j < lineLimit; j++) { const index = i + j const item = list[index] if (item) line.list.push({ ...item, index, }) } lineList.push(line) } return lineList } Component({ properties: { list: { type: Array, value: [], }, imageWidth: { type: Number, value: 0, }, imageMargin: { type: Number, value: 0, }, lineLimit: { type: Number, value: 3, }, }, data: { showList: [], scrollIntoView: '', }, observers: { 'list, lineLimit'(list, lineLimit) { if (!list.length || !lineLimit) return // 调整为行结构,每行自己排版 this.setData({ showList: transformListToLineBlock(list, lineLimit), }) }, }, lifetimes: { created() { EventBus.initWorkletEventBus(this.renderer) // 初始化 ui 线程的 eventBus initRoute() // 初始化自定义路由 }, attached() { // 预览页发生图片切换时,要将对应 image 滚动到到可视范围内 const pageId = this.getPageId() let scrollIntoViewTimer = null this._onPreviewerChange = image => { const list = this.data.list const index = list.findIndex(item => item.id === image.id) if (index !== -1) { if (scrollIntoViewTimer) clearTimeout(scrollIntoViewTimer) scrollIntoViewTimer = setTimeout(() => { // 可能处于页面切换动画中,故加个延迟再滚动 let lineIndex = Math.floor(index / this.data.lineLimit) this.setData({ scrollIntoView: `line-${lineIndex}` }) }, 500) } } EventBus.on(`${pageId}PreviewerChange`, this._onPreviewerChange) }, detached() { const pageId = this.getPageId() EventBus.off(`${pageId}PreviewerChange`, this._onPreviewerChange) }, }, methods: { onTapImage(evt) { const { index } = evt.currentTarget.dataset || {} const image = this.data.list[index] if (!image) return // 跳转到预览页 wx.navigateTo({ url: `../../pages/preview/index?imageid=${image.id}&sourcepageid=${this.getPageId()}`, routeType: 'fadeToggle', }) }, }, }) ================================================ FILE: examples/album/components/album/index.json ================================================ { "component": true, "usingComponents": { "album-image": "./album-image/index" }, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/album/index.wxml ================================================ ================================================ FILE: examples/album/components/album/index.wxss ================================================ .scroll-list { width: 100%; height: 100%; overflow: hidden; } .line { display: flex; flex-direction: row; justify-content: flex-start; } .album-image { width: 100%; height: 100%; } ================================================ FILE: examples/album/components/album/route.js ================================================ const fastOutSlowIn = wx.worklet.Easing.bezier(0.4, 0.0, 0.2, 1.0).factory() export function initRoute() { wx.router.addRouteBuilder('fadeToggle', ({ primaryAnimation }) => { const handlePrimaryAnimation = () => { 'worklet' return { opacity: fastOutSlowIn(primaryAnimation.value), } } return { opaque: false, handlePrimaryAnimation, transitionDuration: 300, reverseTransitionDuration: 300, canTransitionTo: false, canTransitionFrom: false, } }) } ================================================ FILE: examples/album/components/navigation-bar/index.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const rect = wx.getMenuButtonBoundingClientRect() wx.getSystemInfo({ success: (res) => { this.setData({ statusBarHeight: res.statusBarHeight, innerPaddingRight: `padding-right:${res.windowWidth - rect.left}px`, leftWidth: `width:${res.windowWidth - rect.left}px`, navBarHeight: rect.bottom + rect.top - res.statusBarHeight, }) } }) }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${show ? '1' : '0'};transition: opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data if (data.delta) { wx.navigateBack({ delta: data.delta }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/album/components/navigation-bar/index.json ================================================ { "component": true, "usingComponents": {}, "addGlobalClass": true, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/navigation-bar/index.wxml ================================================ {{title}} ================================================ FILE: examples/album/components/navigation-bar/index.wxss ================================================ .weui-navigation-bar { overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; box-sizing: border-box; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; width: 95px; padding-left: 16px; box-sizing: border-box; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .weui-navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: bold; } @media(prefers-color-scheme: dark) { .weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } .weui-navigation-bar__inner { background-color: #1f1f1f; } } ================================================ FILE: examples/album/components/previewer/index.js ================================================ import EventBus from '../../utils/event-bus' import { getShared, getTiming, getRunOnUI } from '../../utils/worklet' const PreviewerGesture = { Init: 0, // 初始 Moving: 1, // 移动图片 Toggle: 2, // 切换图片 Back: 3, // 退出页面 } const PrimaryAnimationStatus = { DISMISSED: 0, FORWARD: 1, REVERSE: 2, COMPLETED: 3, } const GestureState = { POSSIBLE: 0, BEGIN: 1, ACTIVE: 2, END: 3, CANCELLED: 4, } // sharedValues const TRANSLATE_X = 0 const TRANSLATE_Y = 1 const START_Y = 2 const OPACITY = 3 const SCALE = 4 const MIN_SCALE = 5 const USER_GESTURE_IN_PROGRESS = 6 const GESTURE_STATE = 7 const PAGE_ID = 8 const TEMP_LAST_SCALE = 9 const RENDERER = 10 function clamp(value, min, max) { 'worklet' return Math.min(max, Math.max(min, value)) } function recoverTiming(target, renderer, callback) { 'worklet' return getTiming(renderer)(target, { duration: 200 }, callback) } function calcOpacity(moveY, screenHeight) { 'worklet' const opacityRatio = moveY / (screenHeight / 2) return clamp((1 - opacityRatio) ** 3, 0, 1) // 最透明程度为 0 } function calcScale(moveY, screenHeight) { 'worklet' const scaleRange = 0.4 const scaleRatio = moveY / (screenHeight / 3 * 2) return clamp(1 - scaleRange * scaleRatio, 0.6, 1) // 最小为 0.6 } Component({ properties: { imageId: { type: String, value: '', }, sourcePageId: { type: String, value: '', }, list: { type: Array, value: [], }, }, lifetimes: { created() { EventBus.initWorkletEventBus(this.renderer) // 初始化 ui 线程的 eventBus const shared = getShared(this.renderer) this.sharedValues = [ shared(0), // TRANSLATE_X shared(0), // TRANSLATE_Y shared(0), // START_Y shared(1), // OPACITY shared(1), // SCALE shared(1), // MIN_SCALE shared(false), // USER_GESTURE_IN_PROGRESS shared(0), // GESTURE_STATE shared(0), // PAGE_ID shared(1), // TEMP_LAST_SCALE shared(this.renderer), // RENDERER ] }, attached() { const sourcePageId = this.data.sourcePageId const { screenHeight } = wx.getSystemInfoSync() const pageId = this.getPageId() const sharedValues = this.sharedValues ?? [] sharedValues[PAGE_ID].value = pageId this.customRouteContext = wx.router.getRouteContext(this) getRunOnUI(this.renderer)(() => { 'worklet' // 监听拖拽返回手势 if (!globalThis.temp[`${pageId}GestureBack`]) { globalThis.temp[`${pageId}GestureBack`] = args => { if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Moving) return sharedValues[GESTURE_STATE].value = PreviewerGesture.Back const { moveX, moveY, offsetY } = args // 横向永远跟手走 sharedValues[TRANSLATE_X].value += moveX // 竖向在手指回到原处再往上时增加阻尼 if (sharedValues[TRANSLATE_Y].value < 0) { const fy = 0.52 * ((1 - Math.min(offsetY / screenHeight, 1)) ** 2) const translateY = sharedValues[TRANSLATE_Y].value + Math.ceil(moveY * fy) sharedValues[TRANSLATE_Y].value = translateY } else { sharedValues[TRANSLATE_Y].value += moveY } // 拖动时的渐变 sharedValues[OPACITY].value = calcOpacity(offsetY, screenHeight) // 拖动时的大小变化 const scale = calcScale(offsetY, screenHeight) sharedValues[SCALE].value = scale sharedValues[MIN_SCALE].value = Math.min(scale, sharedValues[MIN_SCALE].value) } globalThis.eventBus.on(`${pageId}Back`, globalThis.temp[`${pageId}GestureBack`]) } // 监听拖拽返回手势结束 if (!globalThis.temp[`${pageId}GestureBackEnd`]) { globalThis.temp[`${pageId}GestureBackEnd`] = () => { const moveY = sharedValues[TRANSLATE_Y].value const scale = sharedValues[SCALE].value const minScale = sharedValues[MIN_SCALE].value const { didPop } = this.customRouteContext || {} if (moveY > 1 && scale <= (minScale + 0.01)) { // 必须是一直处于缩小行为才退页面,否则恢复 globalThis.eventBus.emit(`${sourcePageId}CustomRouteBack`, { scale }) didPop() } else { const renderer = sharedValues[RENDERER].value sharedValues[OPACITY].value = recoverTiming(1, renderer) sharedValues[TRANSLATE_X].value = recoverTiming(0, renderer) sharedValues[TRANSLATE_Y].value = recoverTiming(0, renderer) sharedValues[SCALE].value = recoverTiming(1, renderer, () => { 'worklet' sharedValues[GESTURE_STATE].value = PreviewerGesture.Init }) sharedValues[MIN_SCALE].value = 1 } } globalThis.eventBus.on(`${pageId}BackEnd`, globalThis.temp[`${pageId}GestureBackEnd`]) } // 监听图片切换手势 if (!globalThis.temp[`${pageId}GestureToggle`]) { globalThis.temp[`${pageId}GestureToggle`] = () => { if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Moving) return sharedValues[GESTURE_STATE].value = PreviewerGesture.Toggle } globalThis.eventBus.on(`${pageId}Toggle`, globalThis.temp[`${pageId}GestureToggle`]) } // 监听图片拖动手势 if (!globalThis.temp[`${pageId}GestureMoving`]) { globalThis.temp[`${pageId}GestureMoving`] = () => { sharedValues[GESTURE_STATE].value = PreviewerGesture.Moving } globalThis.eventBus.on(`${pageId}Moving`, globalThis.temp[`${pageId}GestureMoving`]) } })() // 拖拽返回时,被拖动的 share-element,因为放手时的返回动画是以两个页面的 share-element 之间的位置进行移动,故这里移动的是 share-element this.applyAnimatedStyle('#preview-home >>> .need-transform-on-back', () => { 'worklet' return { transform: `translate(${sharedValues[TRANSLATE_X].value}px, ${sharedValues[TRANSLATE_Y].value}px) scale(${sharedValues[SCALE].value})`, } }) const { primaryAnimation, primaryAnimationStatus } = this.customRouteContext || {} // 拖拽返回时,previewer 本体要隐藏 this.applyAnimatedStyle('#preview-home >>> .need-hide-on-back', () => { 'worklet' const status = primaryAnimationStatus.value const isRunningAnimation = status === 1 || status === 2 return { left: (isRunningAnimation || (sharedValues[GESTURE_STATE].value === PreviewerGesture.Back)) ? '9999px' : '0', } }) // 图片背景渐变消失 this.applyAnimatedStyle('#preview-home >>> .preview-middle-self', () => { 'worklet' const status = primaryAnimationStatus.value const opacity = sharedValues[OPACITY].value if (!sharedValues[USER_GESTURE_IN_PROGRESS].value) { // 非手势触发,则加速动画 const value = primaryAnimation.value let factor = value if (status === PrimaryAnimationStatus.FORWARD) { factor *= 3 if (factor > 1) factor = 1 } else if (status === PrimaryAnimationStatus.REVERSE) { factor = 1 - ((1 - factor) * 3) if (factor < 0) factor = 0 } const newOpacity = opacity * factor return { opacity: newOpacity > 1 ? 1 : newOpacity } } else { // 手势触发 return { opacity } } }) }, detached() { const pageId = this.getPageId() getRunOnUI(this.renderer)(() => { 'worklet' const removeList = ['Back', 'BackEnd', 'Toggle', 'Moving'] removeList.forEach(item => { 'worklet' const globalKey = `${pageId}Gesture${item}` if (globalThis.temp[globalKey]) { globalThis.eventBus.off(`${pageId}${item}`, globalThis.temp[globalKey]) delete globalThis.temp[globalKey] } }) })() }, }, methods: { onScale(evt) { 'worklet' const sharedValues = this.sharedValues ?? [] const pageId = sharedValues[PAGE_ID].value if (evt.state === GestureState.BEGIN) { sharedValues[START_Y].value = evt.focalY sharedValues[TEMP_LAST_SCALE].value = 1 sharedValues[GESTURE_STATE].value = PreviewerGesture.Init sharedValues[USER_GESTURE_IN_PROGRESS].value = true } else if (evt.state === GestureState.ACTIVE) { const focalX = evt.focalX const focalY = evt.focalY const moveX = evt.focalDeltaX const moveY = evt.focalDeltaY const offsetY = focalY - sharedValues[START_Y].value if (evt.pointerCount === 2) { // 双指放缩 const pageId = sharedValues[PAGE_ID].value const realScale = evt.scale / sharedValues[TEMP_LAST_SCALE].value sharedValues[TEMP_LAST_SCALE].value = evt.scale globalThis.eventBus.emit(`${pageId}Scale`, { scale: realScale, centerX: focalX, centerY: focalY, }) } else if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Back) { // 处于拖拽返回手势中 globalThis.eventBus.emit(`${pageId}Back`, { moveX, moveY, offsetY, }) } else if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Toggle) { // 处于切换图片手势中 // ignore } else { // 单指拖动图片 globalThis.eventBus.emit(`${pageId}Move`, { moveX, moveY, offsetY, origin: 'move', }) } } else if (evt.state === GestureState.END || evt.state === GestureState.CANCELLED) { const velocityX = evt.velocityX const velocityY = evt.velocityY sharedValues[USER_GESTURE_IN_PROGRESS].value = false if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Back) { // 拖拽返回手势结束 globalThis.eventBus.emit(`${pageId}BackEnd`) } else if (sharedValues[GESTURE_STATE].value === PreviewerGesture.Toggle) { // 切换图片手势结束 sharedValues[GESTURE_STATE].value = PreviewerGesture.Init } else { // 其他手势结束 globalThis.eventBus.emit(`${pageId}End`, { velocityX, velocityY, }) sharedValues[GESTURE_STATE].value = PreviewerGesture.Init } } }, onBack() { // 进入动画返回状态 const sharedValues = this.sharedValues ?? [] sharedValues[GESTURE_STATE].value = PreviewerGesture.Back }, shouldResponseOnMove() { 'worklet' return true }, }, }) ================================================ FILE: examples/album/components/previewer/index.json ================================================ { "component": true, "usingComponents": { "preview-home": "./preview-home/index" }, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/previewer/index.wxml ================================================ ================================================ FILE: examples/album/components/previewer/index.wxss ================================================ .preview-home { width: 100%; height: 100%; display: block; } ================================================ FILE: examples/album/components/previewer/preview-home/index.js ================================================ import EventBus from '../../../utils/event-bus' Component({ properties: { imageId: { type: String, value: '', }, sourcePageId: { type: String, value: '', }, list: { type: Array, value: [], }, }, data: { index: 0, tempIndex: -1, pretty: false, }, observers: { tempIndex(tempIndex) { // 切换图片时会影响前一个页面的图片 const { list, sourcePageId } = this.data const image = list[tempIndex] if (image) EventBus.emit(`${sourcePageId}PreviewerChange`, image) }, }, lifetimes: { attached() { const imageId = this.data.imageId const list = this.data.list let index = 0 if (imageId) { const currentIndex = list.findIndex(item => item.id === imageId) if (currentIndex !== -1) index = currentIndex } this.setData({ index }) }, detached() { // 告诉前一个页面 previewer 将要销毁 EventBus.emit(`${this.data.sourcePageId}PreviewerDestroy`) }, }, methods: { onBeforeRender(evt) { const { index } = evt.detail this.data.index = index // 切换可能来自 preview-list 里面,为避免造成循环触发 beforeRender,此处只改 data 不进行 setData this.setData({ tempIndex: index }) // 先更新快速预览栏 }, onTapImage() { this.setData({ pretty: !this.data.pretty }) }, onBack(evt) { this.triggerEvent('back', evt.detail) }, }, }) ================================================ FILE: examples/album/components/previewer/preview-home/index.json ================================================ { "component": true, "usingComponents": { "preview-list": "../preview-list/index", "navigation-bar": "../../navigation-bar/index" }, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/previewer/preview-home/index.wxml ================================================ ================================================ FILE: examples/album/components/previewer/preview-home/index.wxss ================================================ .preview-cnt { position: absolute; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 5; } .preview-list { width: 100vw; height: 100vh; background-color: #fff; } .need-hide-on-back { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; } .share-element-image { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; background-color: transparent; } .share-element-image .temp-image { background-color: transparent; height: 100%; width: 100%; } .share-element-image .temp-image image { width: 100%; height: 100%; } .preview-middle-self { position: absolute; height: 100vh; width: 100vw; z-index: 0; background-color: #fff; } .navigation-bar-cnt { position: absolute; top: 0; left: 0; width: 100%; z-index: 10; transition: transform .3s ease, opacity .3s ease; } .navigation-bar-cnt.show { transform: none; opacity: 1; } .navigation-bar-cnt.hide { transform: translateY(-50px); opacity: 0; } .navigation-bar { position: absolute; top: 0; left: 0; width: 100%; z-index: 5; } ================================================ FILE: examples/album/components/previewer/preview-image/index.js ================================================ import { getShared, getTiming, getRunOnUI, getRunOnJS } from '../../../utils/worklet' // sharedValues const MOVE_X = 0 const MOVE_Y = 1 const SCALE = 2 const IMG_WIDTH = 3 const IMG_HEIGHT = 4 const IMG_MAX_SCALE = 5 const IMG_MIN_SCALE = 6 const IMG_MIN_MOVE_X = 7 const IMG_MAX_MOVE_X = 8 const IMG_MIN_MOVE_Y = 9 const IMG_MAX_MOVE_Y = 10 const GESTURE_STATE = 11 const TEMP_MOVE_X = 12 const TEMP_MOVE_Y = 13 const TEMP_SCALE = 14 const INIT_MOVE_X = 15 const INIT_MOVE_Y = 16 const INIT_SCALE = 17 const CURRENT_ID = 18 const PAGE_ID = 19 const RENDERER = 20 const DEFAULT_IMG_SCALE_MAX = 10 let screenWidth = 0 let screenHeight = 0 function adjustTiming(target, renderer, callback) { 'worklet' return getTiming(renderer)(target, { duration: 300 }, callback) } function recoverTiming(target, renderer, callback) { 'worklet' return getTiming(renderer)(target, { duration: 200 }, callback) } function vibrateShort(type = 'light') { wx.vibrateShort({ type }) } Component({ properties: { status: { type: Number, value: 0, // 1 切到当前,2 当前相邻 }, image: { type: Object, value: {}, }, }, observers: { status(status) { if (!this.isAttached) return const oldStatus = this.oldStatus if (status === oldStatus) return this.oldStatus = status if (status === 1) { if (this.isImgLoaded) this.triggerEvent('render', this.data.image) } else { this.resetImg() } }, }, lifetimes: { created() { const shared = getShared(this.renderer) this.sharedValues = [ shared(0), // MOVE_X shared(0), // MOVE_Y shared(1), // SCALE shared(0), // IMG_WIDTH shared(0), // IMG_HEIGHT shared(1), // IMG_MAX_SCALE shared(1), // IMG_MIN_SCALE shared(0), // IMG_MIN_MOVE_X shared(0), // IMG_MAX_MOVE_X shared(0), // IMG_MIN_MOVE_Y shared(0), // IMG_MAX_MOVE_Y shared(0), // GESTURE_STATE 0 - 初始,1 - 移动图片,2 - 放缩图片,3 - 惯性移动中 shared(0), // TEMP_MOVE_X shared(0), // TEMP_MOVE_Y shared(1), // TEMP_SCALE shared(0), // INIT_MOVE_X shared(0), // INIT_MOVE_Y shared(1), // INIT_SCALE shared(''), // CURRENT_ID shared(''), // PAGE_ID shared(this.renderer), // RENDERER ] }, attached() { this.isAttached = true const pageId = this.getPageId() const id = this.data.image.id const sharedValues = this.sharedValues ?? [] if (!screenWidth || !screenHeight) { const systemInfo = wx.getSystemInfoSync() screenWidth = systemInfo.screenWidth screenHeight = systemInfo.screenHeight } sharedValues[PAGE_ID].value = pageId sharedValues[CURRENT_ID].value = id // 应用放缩和拖动 this.applyAnimatedStyle('#image', () => { 'worklet' return { width: `${sharedValues[IMG_WIDTH].value}px`, height: `${sharedValues[IMG_HEIGHT].value}px`, transform: `translateX(${sharedValues[MOVE_X].value}px) translateY(${sharedValues[MOVE_Y].value}px) translateZ(0px) scale(${sharedValues[SCALE].value})`, } }) // 计算图片边界 function calcImgLimit(scale, imgWidth, imgHeight) { 'worklet' const viewWidth = imgWidth * scale const viewHeight = imgHeight * scale if (viewWidth > screenWidth) { sharedValues[IMG_MAX_MOVE_X].value = (viewWidth / 2) - (imgWidth / 2) sharedValues[IMG_MIN_MOVE_X].value = screenWidth - (imgWidth / 2) - (viewWidth / 2) } else { sharedValues[IMG_MAX_MOVE_X].value = sharedValues[IMG_MIN_MOVE_X].value = (screenWidth - imgWidth) / 2 } if (viewHeight > screenHeight) { sharedValues[IMG_MAX_MOVE_Y].value = (viewHeight / 2) - (imgHeight / 2) sharedValues[IMG_MIN_MOVE_Y].value = screenHeight - (imgHeight / 2) - (viewHeight / 2) } else { sharedValues[IMG_MAX_MOVE_Y].value = sharedValues[IMG_MIN_MOVE_Y].value = (screenHeight - imgHeight) / 2 } } this.calcImgLimit = calcImgLimit // 监听上层的 move/scale 事件 getRunOnUI(this.renderer)(() => { 'worklet' const renderer = sharedValues[RENDERER].value function adjustImg(args) { const { moveX = null, moveY = null, scale = null, centerX = null, centerY = null, withAnimation = false, } = args if (moveX !== null && moveY !== null) { if (!moveX && !moveY) return const oldMoveX = sharedValues[MOVE_X].value const oldMoveY = sharedValues[MOVE_Y].value let newMoveX = oldMoveX + moveX let newMoveY = oldMoveY + moveY const minMoveX = sharedValues[IMG_MIN_MOVE_X].value const maxMoveX = sharedValues[IMG_MAX_MOVE_X].value const minMoveY = sharedValues[IMG_MIN_MOVE_Y].value const maxMoveY = sharedValues[IMG_MAX_MOVE_Y].value const isReachXEdge = (newMoveX <= minMoveX || newMoveX >= maxMoveX) const isReachYEdge = (newMoveY <= minMoveY || newMoveY >= maxMoveY) const isScaleBigger = sharedValues[SCALE].value - sharedValues[INIT_SCALE].value > 0.001 const moveGap = Math.abs(moveY) - Math.abs(moveX) if (!sharedValues[GESTURE_STATE].value && isReachXEdge && moveGap < 0) { // 图片本身不在移动中 & 到达水平边缘 & 横向移动 globalThis.eventBus.emit(`${pageId}Toggle`, args) } else if (!sharedValues[GESTURE_STATE].value && isReachYEdge && moveGap > 0 && moveY > 0) { // 图片本身不在移动中 & 到达垂直边缘 & 纵向移动 globalThis.eventBus.emit(`${pageId}Back`, args) } else { // 移动图片 const tempOldMoveX = sharedValues[TEMP_MOVE_X].value || oldMoveX const tempOldMoveY = sharedValues[TEMP_MOVE_Y].value || oldMoveY if (isReachXEdge) { // 到达水平边缘,增加阻尼 const offset = tempOldMoveX - oldMoveX const f = 0.52 * ((1 - Math.min(Math.abs(offset) / screenWidth, 1)) ** 2) newMoveX = oldMoveX + Math.ceil(moveX * f) } if (isReachYEdge && isScaleBigger) { // 放大情况下到达垂直边缘,增加阻尼 const offset = tempOldMoveY - oldMoveY const f = 0.52 * ((1 - Math.min(Math.abs(offset) / screenHeight, 1)) ** 2) newMoveY = oldMoveY + Math.ceil(moveY * f) } if (sharedValues[GESTURE_STATE].value === 3) { // 惯性移动中 const tempNewMoveX = Math.min(Math.max(newMoveX, minMoveX), maxMoveX) sharedValues[MOVE_X].value = tempNewMoveX sharedValues[TEMP_MOVE_X].value = tempNewMoveX const tempNewMoveY = Math.min(Math.max(newMoveY, minMoveY), maxMoveY) sharedValues[MOVE_Y].value = tempNewMoveY sharedValues[TEMP_MOVE_Y].value = tempNewMoveY } else { // 水平方向 sharedValues[MOVE_X].value = newMoveX const tempNewMoveX = tempOldMoveX + moveX // 这是真正预期的位置,用于阻尼恢复 sharedValues[TEMP_MOVE_X].value = Math.min(Math.max(tempNewMoveX, minMoveX), maxMoveX) // 垂直方向 if (isScaleBigger) { // 只有放大情况下需要阻尼 sharedValues[MOVE_Y].value = newMoveY const tempNewMoveY = tempOldMoveY + moveY // 这是真正预期的位置,用于阻尼恢复 sharedValues[TEMP_MOVE_Y].value = Math.min(Math.max(tempNewMoveY, minMoveY), maxMoveY) } else { const tempNewMoveY = Math.min(Math.max(newMoveY, minMoveY), maxMoveY) sharedValues[MOVE_Y].value = tempNewMoveY sharedValues[TEMP_MOVE_Y].value = tempNewMoveY } sharedValues[GESTURE_STATE].value = 1 globalThis.eventBus.emit(`${pageId}Moving`) } } } else if (scale !== null && centerX !== null && centerY !== null) { const oldMoveX = sharedValues[MOVE_X].value const oldMoveY = sharedValues[MOVE_Y].value const oldScale = sharedValues[SCALE].value const newScale = oldScale * scale const tempNewScale = Math.min(Math.max(newScale, sharedValues[IMG_MIN_SCALE].value), sharedValues[IMG_MAX_SCALE].value) // 这是真正预期的 scale,用于阻尼恢复 const realScale = tempNewScale / oldScale // 其实就是求放缩时图片中心的偏移变化 const imgWidth = sharedValues[IMG_WIDTH].value const imgHeight = sharedValues[IMG_HEIGHT].value const gapX = centerX - (imgWidth / 2) let newMoveX = gapX - ((gapX - oldMoveX) * realScale) const gapY = centerY - (imgHeight / 2) let newMoveY = gapY - ((gapY - oldMoveY) * realScale) calcImgLimit(tempNewScale, imgWidth, imgHeight) newMoveX = Math.min(Math.max(newMoveX, sharedValues[IMG_MIN_MOVE_X].value), sharedValues[IMG_MAX_MOVE_X].value) newMoveY = Math.min(Math.max(newMoveY, sharedValues[IMG_MIN_MOVE_Y].value), sharedValues[IMG_MAX_MOVE_Y].value) sharedValues[MOVE_X].value = withAnimation ? adjustTiming(newMoveX, renderer) : newMoveX sharedValues[MOVE_Y].value = withAnimation ? adjustTiming(newMoveY, renderer) : newMoveY if (withAnimation) { // 有动画,表示是双击放大,不需要考虑阻尼 sharedValues[SCALE].value = adjustTiming(tempNewScale, renderer, () => { sharedValues[GESTURE_STATE].value = 0 // 动画结束,状态恢复初始 }) sharedValues[TEMP_SCALE].value = tempNewScale } else { let calcNewScale = newScale if (Math.abs(newScale - tempNewScale) > 0.001) { // 超出了边界,补充阻尼计算 const gap = newScale - oldScale const dampingLimit = gap < 0 ? (newScale / tempNewScale) : (tempNewScale / newScale) const f = (gap < 0 ? 0.3 : 0.6) * (Math.min(dampingLimit, 1) ** 2) // 不要问为什么是这个值,凭感觉定的 calcNewScale = oldScale + (gap * f) } sharedValues[SCALE].value = calcNewScale sharedValues[TEMP_SCALE].value = tempNewScale } // 回弹是延迟的,所以这里需要确保没有回弹 sharedValues[TEMP_MOVE_X].value = newMoveX sharedValues[TEMP_MOVE_Y].value = newMoveY sharedValues[GESTURE_STATE].value = 2 globalThis.eventBus.emit(`${pageId}Moving`) } } // 监听图片拖动手势 if (!globalThis.temp[`${pageId}${id}ImgMove`]) { globalThis.temp[`${pageId}${id}ImgMove`] = args => adjustImg(args) globalThis.eventBus.on(`${pageId}${id}Move`, globalThis.temp[`${pageId}${id}ImgMove`]) } // 监听图片放缩手势 if (!globalThis.temp[`${pageId}${id}ImgScale`]) { globalThis.temp[`${pageId}${id}ImgScale`] = args => adjustImg(args) globalThis.eventBus.on(`${pageId}${id}Scale`, globalThis.temp[`${pageId}${id}ImgScale`]) } // 监听手势结束事件 if (!globalThis.temp[`${pageId}${id}ImgEnd`]) { globalThis.temp[`${pageId}${id}ImgEnd`] = args => { // 手势结束 sharedValues[GESTURE_STATE].value = 0 const moveX = sharedValues[MOVE_X].value const targetMoveX = sharedValues[TEMP_MOVE_X].value const needRecoverX = Math.abs(moveX - targetMoveX) > 0.001 const moveY = sharedValues[MOVE_Y].value const targetMoveY = sharedValues[TEMP_MOVE_Y].value const needRecoverY = Math.abs(moveY - targetMoveY) > 0.001 const scale = sharedValues[SCALE].value const targetScale = sharedValues[TEMP_SCALE].value const needRecoverScale = Math.abs(scale - targetScale) > 0.001 const isScaleBigger = sharedValues[SCALE].value - sharedValues[INIT_SCALE].value > 0.001 const { velocityX, velocityY } = args const vx = Math.min(Math.abs(velocityX), 700) const vy = Math.min(Math.abs(velocityY), 700) if (needRecoverX || needRecoverY || needRecoverScale) { // 阻尼回弹 if (needRecoverX) sharedValues[MOVE_X].value = recoverTiming(targetMoveX, renderer) if (needRecoverY) sharedValues[MOVE_Y].value = recoverTiming(targetMoveY, renderer) if (needRecoverScale) { sharedValues[SCALE].value = recoverTiming(targetScale, renderer) getRunOnJS(sharedValues[RENDERER].value)(vibrateShort)() } } else if (isScaleBigger && (vx > 50 || vy > 50)) { // 惯性计算 sharedValues[GESTURE_STATE].value = 3 const a = -0.0015 const directionX = Math.sign(velocityX) const directionY = Math.sign(velocityY) let vx0 = vx / 1000 let vy0 = vy / 1000 const t = 16 const nextTick = () => { 'worklet' // 如果此时进入其他状态,则取消惯性 if (sharedValues[GESTURE_STATE].value !== 3) return let moveX = 0 let moveY = 0 if (vx0 > 0) { // 还有水平速度 const vx1 = a * t + vx0 if (vx1 > 0) { const s = (vx0 * t) + ((a * (t ** 2)) / 2) moveX = s * directionX vx0 = vx1 } else { vx0 = 0 } } if (vy0 > 0) { // 还有垂直速度 const vy1 = a * t + vy0 if (vy1 > 0) { const s = (vy0 * t) + ((a * (t ** 2)) / 2) moveY = s * directionY vy0 = vy1 } else { vy0 = 0 } } if (moveX || moveY) { adjustImg({ moveX, moveY }) setTimeout(nextTick, t) } else { // 回归初始状态 sharedValues[GESTURE_STATE].value = 0 } } setTimeout(nextTick, t) } } globalThis.eventBus.on(`${pageId}${id}End`, globalThis.temp[`${pageId}${id}ImgEnd`]) } // 监听图片双击手势 if (!globalThis.temp[`${pageId}${id}ImgDoubleTap`]) { globalThis.temp[`${pageId}${id}ImgDoubleTap`] = args => adjustImg(args) globalThis.eventBus.on(`${pageId}${id}DoubleTap`, globalThis.temp[`${pageId}${id}ImgDoubleTap`]) } })() }, detached() { this.isAttached = false const id = this.data.image.id const pageId = this.getPageId() getRunOnUI(this.renderer)(() => { 'worklet' const removeList = ['Move', 'Scale', 'End', 'DoubleTap'] removeList.forEach(item => { 'worklet' const globalKey = `${pageId}${id}Img${item}` if (globalThis.temp[globalKey]) { globalThis.eventBus.off(`${pageId}${id}${item}`, globalThis.temp[globalKey]) delete globalThis.temp[globalKey] } }) })() }, }, methods: { resetImg() { const sharedValues = this.sharedValues ?? [] const initMoveX = sharedValues[INIT_MOVE_X].value const initMoveY = sharedValues[INIT_MOVE_Y].value const initScale = sharedValues[INIT_SCALE].value this.calcImgLimit(initScale, sharedValues[IMG_WIDTH].value, sharedValues[IMG_HEIGHT].value) sharedValues[MOVE_X].value = initMoveX sharedValues[MOVE_Y].value = initMoveY sharedValues[TEMP_MOVE_X].value = initMoveX sharedValues[TEMP_MOVE_Y].value = initMoveY sharedValues[SCALE].value = initScale sharedValues[TEMP_SCALE].value = initScale }, onImgLoad(evt) { const { width: imgWidth, height: imgHeight } = evt.detail const sharedValues = this.sharedValues ?? [] if (!screenWidth || !screenHeight) { const systemInfo = wx.getSystemInfoSync() screenWidth = systemInfo.screenWidth screenHeight = systemInfo.screenHeight } const windowRatio = screenWidth / screenHeight sharedValues[IMG_WIDTH].value = imgWidth sharedValues[IMG_HEIGHT].value = imgHeight if (!imgWidth || !imgHeight) return const imgRatio = imgWidth / imgHeight let scale = 1 let moveX = 0 let moveY = 0 if (imgRatio > windowRatio) { scale = screenWidth / imgWidth moveX = (screenWidth - imgWidth) / 2 const scaleHeight = imgHeight * scale moveY = ((screenHeight - scaleHeight) / 2) - ((imgHeight - scaleHeight) / 2) sharedValues[IMG_MAX_SCALE].value = imgWidth > screenWidth ? 2 : DEFAULT_IMG_SCALE_MAX sharedValues[IMG_MIN_SCALE].value = imgWidth > screenWidth ? screenWidth / imgWidth : 1 } else { scale = screenHeight / imgHeight moveY = (screenHeight - imgHeight) / 2 const scaleWidth = imgWidth * scale moveX = ((screenWidth - scaleWidth) / 2) - ((imgWidth - scaleWidth) / 2) sharedValues[IMG_MAX_SCALE].value = imgHeight > screenHeight ? 2 : DEFAULT_IMG_SCALE_MAX sharedValues[IMG_MIN_SCALE].value = imgHeight > screenHeight ? screenHeight / imgHeight : 1 } this.calcImgLimit(scale, imgWidth, imgHeight) sharedValues[MOVE_X].value = moveX sharedValues[MOVE_Y].value = moveY sharedValues[TEMP_MOVE_X].value = moveX sharedValues[TEMP_MOVE_Y].value = moveY sharedValues[SCALE].value = scale sharedValues[TEMP_SCALE].value = scale sharedValues[INIT_MOVE_X].value = moveX sharedValues[INIT_MOVE_Y].value = moveY sharedValues[INIT_SCALE].value = scale this.isImgLoaded = true if (this.data.status === 1) { // 处于 current 状态,直接扔 render 事件 this.triggerEvent('render', this.data.image) } }, onDoubleTap(evt) { 'worklet' const sharedValues = this.sharedValues ?? [] const currentScale = sharedValues[SCALE].value const minScale = sharedValues[IMG_MIN_SCALE].value globalThis.eventBus.emit(`${sharedValues[PAGE_ID].value}${sharedValues[CURRENT_ID].value}DoubleTap`, { scale: currentScale <= minScale ? 2 : 0.0001, // 如果放大过,就缩回默认大小;如果没有放大过则放大 centerX: evt.absoluteX, centerY: evt.absoluteY, withAnimation: true, }) }, }, }) ================================================ FILE: examples/album/components/previewer/preview-image/index.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/previewer/preview-image/index.wxml ================================================ ================================================ FILE: examples/album/components/previewer/preview-image/index.wxss ================================================ .double-tap-gesture-handler { display: block; width: 100%; height: 100%; } .image-wrapper { position: absolute; left: 0; top: 0; width: 100%; height: 100%; transform-origin: center; will-change: transform; } .image { width: 100%; height: 100%; } ================================================ FILE: examples/album/components/previewer/preview-list/index.js ================================================ import { getShared, getRunOnUI, getRunOnJS } from '../../../utils/worklet' const PreviewerGesture = { Init: 0, // 初始 Moving: 1, // 移动图片 Toggle: 2, // 切换图片 Back: 3, // 退出页面 } // sharedValues const GESTURE_STATE = 0 const CURRENT_ID = 1 const RENDERER = 2 Component({ properties: { index: { type: Number, value: 0, }, list: { type: Array, value: [], }, }, data: { currentIndex: -99, needSwiperAnimation: true, }, observers: { list(list) { const index = this.data.index const current = list[index] if (!current) return const sharedValues = this.sharedValues ?? [] sharedValues[CURRENT_ID].value = current.id this.toggleImage(index, true) }, index(index) { const len = this.data.list.length if (!len) return if (index !== this.data.currentIndex && index >= 0 && index < len) { this.toggleImage(index, true) } else { // 设置相同的 index 也给一下 render 事件 const image = this.data.list[index] if (image && image.id === this.currentRenderImage) { // 要求 image 已经渲染完成 this.onImageRender({ detail: image }) } } }, }, lifetimes: { created() { const shared = getShared(this.renderer) this.sharedValues = [ shared(0), // GESTURE_STATE shared(''), // CURRENT_ID shared(this.renderer), // RENDERER ] }, async attached() { const pageId = this.getPageId() const sharedValues = this.sharedValues ?? [] getRunOnUI(this.renderer)(() => { 'worklet' // 监听拖拽返回手势 if (!globalThis.temp[`${pageId}PreviewerBack`]) { globalThis.temp[`${pageId}PreviewerBack`] = () => sharedValues[GESTURE_STATE].value = PreviewerGesture.Back globalThis.eventBus.on(`${pageId}Back`, globalThis.temp[`${pageId}PreviewerBack`]) } // 监听图片切换手势 if (!globalThis.temp[`${pageId}PreviewerToggle`]) { globalThis.temp[`${pageId}PreviewerToggle`] = () => sharedValues[GESTURE_STATE].value = PreviewerGesture.Toggle globalThis.eventBus.on(`${pageId}Toggle`, globalThis.temp[`${pageId}PreviewerToggle`]) } // 监听图片拖动中事件 if (!globalThis.temp[`${pageId}PreviewerMoving`]) { globalThis.temp[`${pageId}PreviewerMoving`] = () => sharedValues[GESTURE_STATE].value = PreviewerGesture.Moving globalThis.eventBus.on(`${pageId}Moving`, globalThis.temp[`${pageId}PreviewerMoving`]) } // 监听图片拖动手势 if (!globalThis.temp[`${pageId}PreviewerMove`]) { globalThis.temp[`${pageId}PreviewerMove`] = args => { // 此处只做转发 const currentId = sharedValues[CURRENT_ID].value globalThis.eventBus.emit(`${pageId}${currentId}Move`, args) } globalThis.eventBus.on(`${pageId}Move`, globalThis.temp[`${pageId}PreviewerMove`]) } // 监听图片放缩手势 if (!globalThis.temp[`${pageId}PreviewerScale`]) { globalThis.temp[`${pageId}PreviewerScale`] = args => { // 此处只做转发 const currentId = sharedValues[CURRENT_ID].value globalThis.eventBus.emit(`${pageId}${currentId}Scale`, args) } globalThis.eventBus.on(`${pageId}Scale`, globalThis.temp[`${pageId}PreviewerScale`]) } // 监听手势结束事件 if (!globalThis.temp[`${pageId}PreviewerEnd`]) { globalThis.temp[`${pageId}PreviewerEnd`] = args => { // 此处只做转发 const currentId = sharedValues[CURRENT_ID].value globalThis.eventBus.emit(`${pageId}${currentId}End`, args) } globalThis.eventBus.on(`${pageId}End`, globalThis.temp[`${pageId}PreviewerEnd`]) } })() }, detached() { const pageId = this.getPageId() getRunOnUI(this.renderer)(() => { 'worklet' const removeList = ['Back', 'Toggle', 'Moving', 'Move', 'Scale', 'End'] removeList.forEach(item => { 'worklet' const globalKey = `${pageId}Previewer${item}` if (globalThis.temp[globalKey]) { globalThis.eventBus.off(`${pageId}${item}`, globalThis.temp[globalKey]) delete globalThis.temp[globalKey] } }) })() }, }, methods: { async toggleImage(index, disableAnimation = false) { const image = this.data.list[index] if (!image) return const sharedValues = this.sharedValues ?? [] sharedValues[CURRENT_ID].value = image.id this.setData({ currentIndex: index, needSwiperAnimation: !disableAnimation, }) this.data.index = index // index 也更新一下,方便其他地方取用 this.triggerEvent('beforerender', { index, image }) }, onImageRender(evt) { const list = this.data.list const index = this.data.currentIndex const image = list[index] || {} if (evt.detail.id !== image.id) return this.currentRenderImage = image.id }, onTapImage() { 'worklet' getRunOnJS(this.sharedValues[RENDERER].value)(this.triggerEvent.bind(this))('tapimage') }, shouldResponseOnMove() { 'worklet' // 当触发图片切换手势时,才让 swiper 工作 const sharedValues = this.sharedValues ?? [] return sharedValues[GESTURE_STATE].value === PreviewerGesture.Toggle }, onSwiperChange(evt) { const { current, source } = evt.detail if (source === 'touch') this.toggleImage(current, false) }, }, }) ================================================ FILE: examples/album/components/previewer/preview-list/index.json ================================================ { "component": true, "usingComponents": { "preview-image": "../preview-image/index" }, "componentFramework": "glass-easel" } ================================================ FILE: examples/album/components/previewer/preview-list/index.wxml ================================================ ================================================ FILE: examples/album/components/previewer/preview-list/index.wxss ================================================ .image-previewer { width: 100vw; height: 100vh; overflow: hidden; } .swiper-cnt { display: flex; flex-direction: row; position: absolute; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 20; } .image { display: block; width: 100%; height: 100%; } ================================================ FILE: examples/album/pages/album/index.js ================================================ import { getAlbum } from '../../utils/store' Page({ data: { imageWidth: 0, imageMargin: 12, // 图片间距 lineLimit: 3, // 每行多少张图片 list: [], }, onLoad() { const { imageMargin, lineLimit } = this.data const { screenWidth } = wx.getSystemInfoSync() this.setData({ imageWidth: (screenWidth - imageMargin * 4) / lineLimit, // 图片宽度 list: getAlbum(), }) }, }) ================================================ FILE: examples/album/pages/album/index.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar/index", "album": "../../components/album/index" }, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/album/pages/album/index.wxml ================================================ ================================================ FILE: examples/album/pages/album/index.wxss ================================================ .cnt { position: absolute; top: 0; width: 100%; height: 100%; display: flex; flex-direction: column; } .album { height: 0; flex: 1; margin-bottom: 12px; } .safe-area-inset-bottom { height: env(safe-area-inset-bottom); } ================================================ FILE: examples/album/pages/preview/index.js ================================================ import { getAlbum } from '../../utils/store' Component({ properties: { imageid: { type: String, value: '', }, sourcepageid: { type: String, value: '', }, }, data: { imageId: '', sourcePageId: '', list: [], }, lifetimes: { attached() { // 因为页面的 onLoad 太迟,所以选用 component 构造器的 attached 生命周期来设置 shareKey,确保 share-element 动画正常执行 const query = this.data const imageId = decodeURIComponent(query.imageid || '') const sourcePageId = decodeURIComponent(query.sourcepageid || '') const list = getAlbum() this.setData({ imageId, sourcePageId, list }) }, }, }) ================================================ FILE: examples/album/pages/preview/index.json ================================================ { "usingComponents": { "previewer": "../../components/previewer/index" }, "navigationStyle": "custom", "backgroundColorContent": "#00000000", "disableScroll": true, "componentFramework": "glass-easel", "renderer": "skyline" } ================================================ FILE: examples/album/pages/preview/index.wxml ================================================ ================================================ FILE: examples/album/pages/preview/index.wxss ================================================ page { background-color: transparent; } .box-cotainer { width: 100vw; height: 100vh; position: relative; } .previewer { width: 100%; height: 100%; } ================================================ FILE: examples/album/project.config.json ================================================ { "description": "项目配置文件", "packOptions": { "ignore": [], "include": [] }, "setting": { "bundle": false, "userConfirmedBundleSwitch": false, "urlCheck": true, "scopeDataCheck": false, "coverView": true, "es6": true, "postcss": true, "compileHotReLoad": false, "lazyloadPlaceholderEnable": false, "preloadBackgroundData": false, "minified": true, "autoAudits": false, "newFeature": false, "uglifyFileName": false, "uploadWithSourceMap": true, "useIsolateContext": true, "nodeModules": false, "enhance": true, "useMultiFrameRuntime": true, "useApiHook": true, "useApiHostProcess": true, "showShadowRootInWxmlPanel": true, "packNpmManually": false, "enableEngineNative": false, "packNpmRelationList": [], "minifyWXSS": true, "showES6CompileOption": false, "minifyWXML": true, "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": false, "compileWorklet": true }, "compileType": "miniprogram", "libVersion": "latest", "appid": "wxe5f52902cf4de896", "projectname": "album-demo", "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 } } ================================================ FILE: examples/album/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "album", "setting": { "compileHotReLoad": false, "skylineRenderEnable": true }, "libVersion": "latest" } ================================================ FILE: examples/album/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/album/utils/event-bus.js ================================================ import { getRunOnUI } from './worklet' const map = {} function on(eventName, handler) { map[eventName] = map[eventName] || [] map[eventName].push(handler) } function off(eventName, handler) { const handlerList = map[eventName] if (!handlerList || !handlerList.length) return const index = handlerList.indexOf(handler) if (index !== -1) handlerList.splice(index, 1) } function emit(eventName, ...args) { const handlerList = map[eventName] if (!handlerList || !handlerList.length) return for (let i = handlerList.length - 1; i >= 0; i--) { handlerList[i](...args) } } function initWorkletEventBus(renderer) { getRunOnUI(renderer)(() => { 'worklet' if (!globalThis.temp) globalThis.temp = {} if (!globalThis.eventBus) { const eventBus = { map: {}, on(eventName, handler) { eventBus.map[eventName] = eventBus.map[eventName] || [] eventBus.map[eventName].push(handler) }, off(eventName, handler) { const handlerList = eventBus.map[eventName] if (!handlerList || !handlerList.length) return const index = handlerList.indexOf(handler) if (index !== -1) handlerList.splice(index, 1) }, emit(eventName, args) { const handlerList = eventBus.map[eventName] if (!handlerList || !handlerList.length) return for (let i = handlerList.length - 1; i >= 0; i--) { handlerList[i](args) } }, } globalThis.eventBus = eventBus } })() } export default { on, off, emit, initWorkletEventBus, } ================================================ FILE: examples/album/utils/store.js ================================================ const imageList = [] export function getAlbum() { if (!imageList.length) { const srcList = [ 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYcv7mQABQVg50gh51-Kb1mlBq0c8Difu3rXn0ldrZdZwVx9REPbKVyZb3E9Wq6YFLA', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYVtRBEXZ74JLECYN4Hc3Cdml3x7cJcbcBj3sIdYb2L_7DJ14X8TAHiZ7Ydct52WIYA', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYfaqohk6ndcC6_CBkUZszfSpKbqUAV7S2xWRbAQ459YsPWAmLKkicEOPS1L3NmnnRA', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYa7k4CAG71Ci0SRPExPB1sGiiVPcYwSD5CAYgq8Ni2RhGQlqIrIwnBlt90mUzL7Lg', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYjda9Dp372N3T05q_nn3PgvoXBoReXvaXBfkthtXQLN7m5_YI6FoTre-xvJBDFLMA', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYTIqRBCdQgzZm3KO3usZT7zM2O0EoylisontlH4TZAC_qfVjFVU4L4CPjLm0mppQwg', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYfa6mRnywhNbBFV5eAt7oTz3zjlNJeujfQx0PVA1ufenPHBvxYXRNJ5chyi6RPaE7A', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYY1OalScOn4EMcQpkPaJ1Sxhri8CScjnhqVfjAZnLuVFl0JAM4VziHhSzHLZXtAaQ', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYSPfXw5qxNvP3f5uJhy0kdkaTAbIZ187IFWmBiluEs8Puw0tXgBlBgGKN-irLPOIDw', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYZB1p48LLH-Pc7Rzr4nN0YF-uZg7FW7zksw_Kjp0BNDHcZp9R9SRKbg0rA1HBaeK3Q', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYXMoGlbo8DkSiia6d-_3Dv15DjMGCEhBkPYd5BrNkSUTPtomAm0CVeHgC244sapKw', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYf3q0W302-kseD8VxLKoItZ6HgneLkgpQSEMIgEKz_xVE7putZxs2YEYqB13Uh37_w', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYRu0VRyVvePJ4pB4_Dvj0ytF-ovjQzMl6WMLyuCeKk3579HNjKLIeNrHE7OprTBx5w', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYc-vygvNNpz1fcZZjZ6B0Jm0X2dpOWJBn4u4T15gwL-1Qr70v6fkFgUswldiPhQG0Q', 'https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYUo-SgGpk2gpFNixuaCMxDadRvvWxqC5jc1ma-oobJf4mWVg3F3iRt1Bkv-sR0l3-Q', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJCk-lm-hNrvdJ9paSPqTXkCKLE6eIuc6zAs3Lr1Qh6jEMxdfgP_rvTftB0SQiZeQNQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJImpE8Sm-oMoeJizjFr8mxotXQSFlSm8ZUD6GLf_ptM5ozLvnc2eczwSN09Y5M0ovg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBAK7KJg9KId_6N1-rJ4OyCCQoJGVMOaTabboo2viRucoxkPvHRkn2fVl6tectlzBg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJGVi_kGQppwXClflw43wtANVzuw0_Y8ij9VzW1O8hr6cR195D87X7zQS_1gjRo8IdQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJHhlMVj_svRRdDMHjtAvwGwU2d_VAJ-y3SigDx2XjjA22Y63oStS-fw5gooV5rX42Q', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJPRaN5CDI6NZFg_qbSxeqF8UBpM4lXJ_1o9S9bsOOxMpuXGLeKyAKleWlAXmVLmQOw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJMQYuLq5q6_CWdtW3RnWbYMaNhzew60nGsQfBtyeogj3vRfGimJBi87ThS577HC6ag', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBalQKoYXBze_mRhoUHIh5OfHiR95JEa18ob-y7uu8W1gpOjO87BbOcki3xM_RrO1Q', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJAqHss14EPHStg3OpHjtIYzOGQgxvsDcACMxQc2waTpAMsJBFxVlp1JkZQJy2gXu3g', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJH2f0R4uXyqnNGlrivO8cKbn0nz1DE_6s22rc91zluwIrqiAVZNREvCeVYAUS8aaZw', 'https://res.wx.qq.com/op_res/SPJ17foR6CQ_1kS3N3iWEk2lg3OAXWsGLLFfwHR64LgMfJQm0jK0YWSG9rVD9cysd0AddG5ZhacUE5mm2jPelQ', ] const thumbnailList = [ 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJDaOeA5r3yDpl5AjlRxMmKPaX0WbJIRr0PjZzsfxIGq1n5Q0o65EpxFGpGpbT9e8QQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJACGFAh31lY9OxoD1iSXsyYpk2adWITN6b7gT7RcBEeDXhppBLYI3JhSMJDyIvPrcg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJJX5J79IIhayRchEZTln15RdpFkFfsqsvng5dqMa12Vm9rmcT-hhw1alfHvxTXVVRw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJApluxMePb-2yj-XeDSDlr-pNZnbKpaNwl4BUXyZ0bKXWMcqKQSGDdOWFPOP08-4Sw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJI8Xazme5-MIIJYUg9t2ibxTp-PtGPUXes9XZj8rRIfyhZJgyfejqwzwpSDBh-K5eg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJHOt1gRAQQbf2Bx8mJoYj_8hoN17WusJ_kEzxqYbkN6X1dRjDMdp0gS9jJf-VAezfg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJNLlVMP9qArsQkkz294mrVFbI2pxwKxf0I1F8S86W4zZ8bcpuDv4xIWioikrD-85Dw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJOY6-yX_tVBt6ZC4ffTUqdnMXaCEKf0mYb8ebfl0l-VUKHzayRrbipPLqsynrTN0FA', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJMgKiLq4XOYd9BtY9HCvD4BvuYDo9ro9XBb3_fQk90z8odHLs4eYvoHwkyKaM3ztZA', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJH3eO7DeTKRV0Ys2vZ2aqUom-xWvymxzS3uiu06syi2vlj8hzWyIK1_inNHTVkuWPw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJA7-uaVSCPgd7Q32x1_vmHrDLaretZxRboZv2wZiNBkRdgihEvRhcWRHrm0vBUggzw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBUmVY1MDz1vsXTJlQCPd-fHF8AsVCi6yVK4BxhjBXs085BQZaI6LoWW4GfnqMRqbg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJKh4kMbGYKCPnBu-ZWsQnn3NEFKDEN0DgdKH_R9v8Snkp-cfiU8dZd_abw5QcyiWBQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJKPDdtcGoJzhB8HdWlDtWaxeIOvdYSmQoihkYQPrHDoGr2PJD31RPzqcz5u8b3rpoQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJFryDUHWInyL4LeRP6y_wIUpf7kQSfrcGPcGeZbLuOtan8ml9SbJ1k0Qdgelf6iEBg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJE1YvpBMZCjoKqX1Sy1xqlqntchR2vwtu82wt0XLsCMUtsJ0x2K5fFvEiaouWbViFg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJIJE5jTu_BNqZ_LR4E82Kj6hBC7UlZ1BDeqhJEKDtQ-vTzpwv8m3Rj16kJtE2dSBAg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBi8YFt3WToEbW0L6-BmqV4_HjXOvB0Z5IJ3GLVtBj8hcRP_O4mf9Ia6T8ITbfkKBw', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJFIvGogPbbDumfRVNxlgPRqYwmSTuhsE2OyzcnJXRc49-6SanEHKsiPM-vIlUlGOFQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJNt8AklXWChR3D1545zHosDzkcT6gNUv9UwevKbgsGE6_Skgr9Et86OA8tw3PgW0ew', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJMNA5xeVo8u0lFyyFwLvexHmQTRi_oaiYuS6wCrZHm3931x3KbVeeLJv8hI2YvL1yQ', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJD3V9rkvi7OQ852nZavb3k2sdVOl_JEjqRbhYBhgPPqhgaIkawklFj-4w5oQ9BR1xg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJDLPwxnddCKwDrDOjA9lWQkB1_2KcFc9L48-AjNV7lMTDV7EvpwWRy3aLVmvXmmi-w', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJNMTtzowAlaXSt-ZZajhbZeYcZj9njs9Czy2iRLoh6m-PrGRaCb7koaoeVzHvMwrFg', 'https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJMQV_yLC-b3ewPS_sYPnmdwvIlB-IlyEjoyVtv13rE7Qulx6GR2H-p5JIIZNDWXg6Q', 'https://res.wx.qq.com/op_res/SPJ17foR6CQ_1kS3N3iWEozVgUwwyXBMXhm7WFzcvPMGQH8Nqoh1jLUayawk0flboyz1IDNkU3foqBvNIgI11Q', ] const now = Date.now() srcList.forEach((src, index) => { const id = (now + index).toString() imageList.push({ id, src, thumbnail: thumbnailList[index], createTime: now - (index * 3600 * 1000) }) }) } return imageList } ================================================ FILE: examples/album/utils/worklet.js ================================================ export function getShared(renderer) { if (renderer === 'skyline') return wx.worklet.shared else return val => ({ value: val }) } export function getTiming(renderer) { 'worklet' if (renderer === 'skyline') return wx.worklet.timing else return (target, options, callback) => { if (typeof callback === 'function') setTimeout(callback, 0) return target } } export function getRunOnUI(renderer) { if (renderer === 'skyline') return wx.worklet.runOnUI else return func => func } export function getRunOnJS(renderer) { 'worklet' if (renderer === 'skyline') return wx.worklet.runOnJS else return func => func } ================================================ FILE: examples/app-bar/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/app-bar/app-bar/index.js ================================================ // components/app-bar/index.js const { shared, timing, Easing } = wx.worklet export const GestureState = { POSSIBLE: 0, BEGIN: 1, ACTIVE: 2, END: 3, CANCELLED: 4, } export const lerp = function (begin, end, t) { 'worklet' return begin + (end - begin) * t } export const clamp = function (cur, lowerBound, upperBound) { 'worklet' if (cur > upperBound) return upperBound if (cur < lowerBound) return lowerBound return cur } const systemInfo = wx.getSystemInfoSync() const { statusBarHeight, screenHeight, screenWidth, safeArea } = systemInfo console.info('@@@ systemInfo', systemInfo) Component({ properties: { }, data: { maxCoverSize: 0, statusBarHeight: 0, musicCover: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElxNcl1yMvnKypRo4MTbjOv7FC3saigGoOBTZibyESC7EXaClnPYhB6pvfb-IRmso6g' }, lifetimes: { attached() { const progress = shared(0) const initCoverSize = 60 // 初始图片大小 const pagePadding = 24 const maxCoverSize = screenWidth - 2 * pagePadding const safeAreaInsetBottom = screenHeight - safeArea.bottom const isIOS = systemInfo.system.indexOf('iOS') >= 0 this.setData({ statusBarHeight, maxCoverSize }) this.applyAnimatedStyle('.cover', () => { 'worklet' const height = initCoverSize + (maxCoverSize - initCoverSize) * progress.value return { width: `${height}px`, height:`${height}px`, } }) this.applyAnimatedStyle('.expand-container', () => { 'worklet' const t = progress.value const maxRadius = 30 const radius = isIOS ? maxRadius * t : 0 const initBarHeight = initCoverSize + 8 * 2 + safeAreaInsetBottom return { top: `${(screenHeight - initBarHeight) * (1 - t)}px`, borderRadius: `${radius}px ${radius}px 0px 0px` } }) this.applyAnimatedStyle('.title-wrap', () => { 'worklet' return { opacity: 1 - progress.value } }) const navBarHeight = statusBarHeight + (isIOS ? 40 : 44) this.applyAnimatedStyle('.nav-bar', () => { 'worklet' const t = progress.value const threshold = 0.8 const opacity = t < threshold ? 0 : (t - threshold) / (1 - threshold) return { opacity, height: `${navBarHeight * progress.value}px` } }) this.progress = progress } }, methods: { close() { this.progress.value = timing(0, { duration: 250, easing: Easing.ease }) }, expand() { this.progress.value = timing(1, { duration: 250, easing: Easing.ease }) }, handleDragUpdate(delta) { 'worklet' const curValue = this.progress.value const newVal = curValue - delta this.progress.value = clamp(newVal, 0.0, 1.0) }, handleDragEnd(velocity) { 'worklet' const t = this.progress.value let animateForward = false if (Math.abs(velocity) >= 1) { animateForward = velocity <= 0 } else { animateForward = t > 0.7 } const animationCurve = Easing.out(Easing.ease) if (animateForward) { this.progress.value = timing( 1.0, { duration: 200, easing: animationCurve, }) } else { this.progress.value = timing( 0.0, { duration: 250, easing: animationCurve, }) } }, handleVerticalDrag(evt) { 'worklet' if (evt.state === GestureState.ACTIVE) { const delta = evt.deltaY / screenHeight this.handleDragUpdate(delta) } else if (evt.state === GestureState.END) { const velocity = evt.velocityY / screenHeight this.handleDragEnd(velocity) } else if (evt.state === GestureState.CANCELLED) { this.handleDragEnd(0.0) } }, }, }) ================================================ FILE: examples/app-bar/app-bar/index.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: examples/app-bar/app-bar/index.wxml ================================================ Skyline 渲染框架入门与实践 小程序技术专员 - binnie 微信学堂 88 人在学 Skyline 渲染框架入门与实践 小程序技术专员 - binnie ================================================ FILE: examples/app-bar/app-bar/index.wxss ================================================ .expand-container { position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 0 24px; padding-bottom: env(safe-area-inset-bottom); pointer-events: auto; overflow: hidden; background-color: #e9f8f7; color: #7e8081; } .hide { display: none; } .nav-bar { overflow: hidden; box-sizing: border-box; } .icon--back { width: 30px; height: 30px; } .title { margin-left: 10px; min-width: 160px; flex: 1; } .title .name { margin-top: 4px; font-size: 12px; } .title-wrap { flex: 1; min-width: 240px; } .footer { padding: 0 24px; } .footer .icon { width: 40px; height: 40px; } .expand-cover { width: 100%; height: 100%; } .music-title { margin-top: 48px; font-size: 20px; font-weight: bold; color: #07c160; } .music-title .name { margin-top: 12px; font-size: 14px; font-weight: 200; color: #b1b2b3; } .cover-area { margin: 8px 0; width: 100%; aspect-ratio: 1 / 1; overflow: hidden; } .cover { /* aspect-ratio: 1 / 1; */ flex-shrink: 0; } .icon { width: 18px; height: 18px; } .row { display: flex; flex-direction: row; align-items: center; } .row-between { display: flex; flex-direction: row; justify-content: space-between; align-items: center; } .column { display: flex; flex-direction: column; } .column-main-center { display: flex; flex-direction: column; justify-content: center; } .column-cross-center { display: flex; flex-direction: column; align-items: center; } .center { display: flex; align-items: center; justify-content: center; } .circle { width: 100%; height: 100%; overflow: hidden; border-radius: 50%; } ================================================ FILE: examples/app-bar/app.js ================================================ // app.js App({}) ================================================ FILE: examples/app-bar/app.json ================================================ { "pages": [ "pages/index/index", "pages/detail/index" ], "window": { "navigationBarTextStyle": "black", "navigationStyle": "custom" }, "style": "v2", "renderer": "skyline", "appBar": {}, "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "defaultContentBox": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json", "lazyCodeLoading": "requiredComponents" } ================================================ FILE: examples/app-bar/app.wxss ================================================ /**app.wxss**/ .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: 200rpx 0; box-sizing: border-box; } ================================================ FILE: examples/app-bar/components/navigation-bar/navigation-bar.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const isSupport = !!wx.getMenuButtonBoundingClientRect const rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null wx.getSystemInfo({ success: (res) => { const ios = !!(res.system.toLowerCase().search('ios') + 1) this.setData({ ios, statusBarHeight: res.statusBarHeight, navBarHeight: rect.bottom - rect.top + 10, innerWidth: isSupport ? `width:${rect.left}px` : '', innerPaddingRight: isSupport ? `padding-right:${res.windowWidth - rect.left}px` : '', leftWidth: isSupport ? `width:${res.windowWidth - rect.left}px` : '', theme: res.theme || 'light', }) } }) if (wx.onThemeChange) { wx.onThemeChange(({theme}) => { this.setData({theme}) }) } }, detached() { if (wx.offThemeChange) { wx.offThemeChange() } }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${ show ? '1' : '0' };-webkit-transition:opacity 0.5s;transition:opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data console.log('---------222',getCurrentPages().length) if (data.delta) { wx.navigateBack({ delta: data.delta }) } // 如果是直接打开的,就默认回首页 if (getCurrentPages().length == 1) { console.log('---------333') wx.switchTab({ url: '/page/component/index', complete: (res) => { console.log(res) } }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/app-bar/components/navigation-bar/navigation-bar.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel", "renderer": "skyline", "styleIsolation": "apply-shared" } ================================================ FILE: examples/app-bar/components/navigation-bar/navigation-bar.wxml ================================================ {{title}} ================================================ FILE: examples/app-bar/components/navigation-bar/navigation-bar.wxss ================================================ .weui-navigation-bar { display: flex; overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; width: 95px; padding-left: 16px; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .weui-navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-color: currentColor; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; align-items: center; justify-content: center; font-weight: bold; } [data-weui-theme=dark].weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } [data-weui-theme=dark] .weui-navigation-bar__inner { background-color: #1f1f1f; } ================================================ FILE: examples/app-bar/pages/detail/index.js ================================================ // pages/detail/index.js const musicList = [ { id: 0, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElxNcl1yMvnKypRo4MTbjOv7FC3saigGoOBTZibyESC7EXaClnPYhB6pvfb-IRmso6g', title: 'Skyline 渲染框架' }, { id: 1, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2El3JJ3FgQX_YP9sI6kJD_nLjnkdN19yZ5nLtS3cqtNUx621vrni0Kjy5uoX_QMlBJgQ', title: '小程序性能优化' }, { id: 2, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElwWbBogi5f0NNRBkuJWfE8HQzysKxBaoCJ-YBr7irwn_uE37dHQTWcHK2uOHIWsQ3Q', title: '医疗行业实践' }, ] Page({ data: { music: musicList[0], albumMusicList: [ { id: 0, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElxNcl1yMvnKypRo4MTbjOv7FC3saigGoOBTZibyESC7EXaClnPYhB6pvfb-IRmso6g', name: 'Skyline 渲染框架', author: '小程序技术专员 - binnie' }, { id: 1, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2El3JJ3FgQX_YP9sI6kJD_nLjnkdN19yZ5nLtS3cqtNUx621vrni0Kjy5uoX_QMlBJgQ', name: '小程序性能优化', author: '小程序性能优化专家' }, { id: 2, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElwWbBogi5f0NNRBkuJWfE8HQzysKxBaoCJ-YBr7irwn_uE37dHQTWcHK2uOHIWsQ3Q', name: '医疗行业实践', author: '小程序医疗行业专家' }, ] }, onLoad(query) { const idx = query.idx if (idx) { this.setData({ music: musicList[idx] }) } }, onReady() { }, onShow() { }, }) ================================================ FILE: examples/app-bar/pages/detail/index.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar/navigation-bar" } } ================================================ FILE: examples/app-bar/pages/detail/index.wxml ================================================ 为你推荐 {{item.name}} {{item.author}} ================================================ FILE: examples/app-bar/pages/detail/index.wxss ================================================ page { display: flex; flex-direction: column; height: 100vh; } .scroll-area { flex: 1; overflow-y: hidden; padding: 0 24px; box-sizing: border-box; margin-bottom: calc(84px + env(safe-area-inset-bottom)); } .intro { padding: 30px; text-align: center; } .cover { width: 250px; height: 250px; } .album-music { padding: 16px 0; border-top: 0.5px solid #f1e9e9; } .album-music-cover { width: 48px; height: 48px; margin-right: 16px; } .row { display: flex; flex-direction: row; } .row-between { display: flex; flex-direction: row; justify-content: space-between; align-items: center; } .column { display: flex; flex-direction: column; } .column-main-center { display: flex; flex-direction: column; justify-content: center; } .column-cross-center { display: flex; flex-direction: column; align-items: center; } .center { display: flex; align-items: center; justify-content: center; } .circle { width: 100%; height: 100%; overflow: hidden; border-radius: 50%; } .author { font-size: 12px; color: #7e8081; margin-top: 6px; } ================================================ FILE: examples/app-bar/pages/index/index.js ================================================ const app = getApp() Page({ data: { back: false, maxCoverSize: 0, musicList: [ { id: 0, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElxNcl1yMvnKypRo4MTbjOv7FC3saigGoOBTZibyESC7EXaClnPYhB6pvfb-IRmso6g', title: 'Skyline 渲染框架' }, { id: 1, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2El3JJ3FgQX_YP9sI6kJD_nLjnkdN19yZ5nLtS3cqtNUx621vrni0Kjy5uoX_QMlBJgQ', title: '小程序性能优化' }, { id: 2, coverImg: 'https://res.wx.qq.com/op_res/Nu9XXzXcXnD1j5EgWQ2ElwWbBogi5f0NNRBkuJWfE8HQzysKxBaoCJ-YBr7irwn_uE37dHQTWcHK2uOHIWsQ3Q', title: '医疗行业实践' }, ] }, onLoad() { // 在小程序示例中展示返回按钮 if (this.route.includes('packageSkylineExamples/examples/')) { this.setData({ back: true }) } }, onShow() { // 仅在 app-bar demo 页面展示 if (typeof this.getAppBar === 'function' ) { const appBarComp = this.getAppBar() // component.getAppBar 在 Skyline 中返回 appBar 组件实例,在 webview 中返回 null if (appBarComp !== null) { appBarComp.setData({ showAppbar: true }) } } }, goDetail(e) { const idx = e.currentTarget.dataset.idx wx.navigateTo({ url: `../detail/index?idx=${idx}`, }) } }) ================================================ FILE: examples/app-bar/pages/index/index.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar/navigation-bar" } } ================================================ FILE: examples/app-bar/pages/index/index.wxml ================================================ 本月课程上新 {{item.title}} 最近学过 {{item.title}} ================================================ FILE: examples/app-bar/pages/index/index.wxss ================================================ page { display: flex; flex-direction: column; height: 100vh; } .scroll-area { flex: 1; overflow-y: hidden; padding: 0 8px; box-sizing: border-box; margin-bottom: calc(84px + env(safe-area-inset-bottom)); } .intro { padding: 30px; text-align: center; } .app-bar { position: absolute; width: 100vw; height: 100vh; pointer-events: none; } .title { margin-top: 16px; margin-left: 8px; font-size: 20px; font-weight: 600; } .cards { /* margin: 24px 0; */ display: flex; flex-direction: row; flex-wrap: wrap; } .card { display: flex; justify-content: center; flex-direction: column; margin: 8px; } .cover { width: 43vw; height: 43vw; margin-bottom: 10px; } ================================================ FILE: examples/app-bar/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "3.3.2", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "auto", "tabSize": 4 } } ================================================ FILE: examples/app-bar/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "app-bar", "setting": { "compileHotReLoad": true, "skylineRenderEnable": true }, "libVersion": "3.3.3" } ================================================ FILE: examples/app-bar/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/associated-scroll-view/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/associated-scroll-view/app.js ================================================ // app.js App({}) ================================================ FILE: examples/associated-scroll-view/app.json ================================================ { "pages": [ "pages/index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "lazyCodeLoading": "requiredComponents", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/associated-scroll-view/app.wxss ================================================ /**app.wxss**/ .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: 200rpx 0; box-sizing: border-box; } ================================================ FILE: examples/associated-scroll-view/components/category-list/category-list.js ================================================ const { shared } = wx.worklet const GestureState = { POSSIBLE: 0, // 0 此时手势未识别,如 panDown等 BEGIN: 1, // 1 手势已识别 ACTIVE: 2, // 2 连续手势活跃状态 END: 3, // 3 手势终止 CANCELLED: 4, // 4 手势取消, } Component({ options: { virtualHost: true, }, properties: { products: { type: Object, value: [], }, max: { type: Number, value: 0, }, index: { type: Number, value: 0, }, }, lifetimes: { created() { this._swiping = shared(false) this._canSwipe = shared(false) this._scrollTop = shared(0) this._scrollHeight = shared(100) this._height = shared(0) }, attached() { this.createSelectorQuery().select('.product-list').boundingClientRect(rect => { this._height.value = rect.height this._scrollHeight.value = rect.height + 1 // 可滚动高度总是比滚动区域高一点 }).exec() }, }, methods: { getCanSwipe() { return this._canSwipe }, setSwipingValue(swiping) { this._swiping = swiping }, shouldScrollViewResp(e) { 'worklet' // 前者是判断到顶往下拉,后者反之 this._canSwipe.value = this._scrollTop.value <= 0 && e.deltaY > 0 && this.properties.index !== 0 || this._scrollTop.value + this._height.value >= this._scrollHeight.value && e.deltaY < 0 && this.properties.index !== this.properties.max // 滑动 swiper 期间 scroll-view 不可滚动 return !this._swiping.value && !this._canSwipe.value }, handleScroll(e) { 'worklet' this._scrollTop.value = e.detail.scrollTop this._scrollHeight.value = e.detail.scrollHeight }, }, }) ================================================ FILE: examples/associated-scroll-view/components/category-list/category-list.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: examples/associated-scroll-view/components/category-list/category-list.wxml ================================================ {{item.name}} {{item.comment}} {{item.sales}}人学过好评度100% ¥ {{item.price}} + ================================================ FILE: examples/associated-scroll-view/components/category-list/category-list.wxss ================================================ .product-list { height: 100%; } .product-item { display: flex; flex-direction: row; padding: 8px; } .product-item:first-child { padding-top: 16px; } .product-item:last-child { padding-bottom: 16px; border-bottom: 1px solid #F9F9F9; } .product-image { width: 133px; height: 100px; border-radius: 5px; } .product-info { flex: 1; padding: 0 8px; font-size: 10px; } .product-info view, .product-info text { margin-bottom: 3px; } .product-name { font-size: 16px; } .product-comment { display: flex; flex-direction: row; } .product-comment text { background-color: #EEE; border-radius: 3px; padding: 1px 3px; } .product-data { display: flex; flex-direction: row; color: gray; } .product-data text { margin-right: 20px; } .product-discount { display: flex; flex-direction: row; } .product-discount text { color: red; border: 0.3px solid red; padding: 0 3px; border-radius: 3px; } .product-price { color: red; vertical-align: baseline; } .product-add-to-cart { position: absolute; right: 12px; bottom: 12px; border-radius: 6px; color: #fff; background-color: #44b549; width: 20px; height: 20px; line-height: 16px; font-weight: 500; text-align: center; } ================================================ FILE: examples/associated-scroll-view/pages/index/index.js ================================================ import { getCategories, getProducts } from "../../util" const { shared } = wx.worklet Component({ data: { categories: getCategories(), selected: 0, products: getProducts(), }, lifetimes: { created() { this._swiping = shared(false) this._canSwipe = [] this._selected = shared(0) this._lastIndex = shared(0) }, attached() { this._canSwipe = this.selectAllComponents('.category-list').map(comp => { comp.setSwipingValue(this._swiping) return comp.getCanSwipe() }) }, }, methods: { shouldSwiperResp() { 'worklet' if (this._lastIndex.value !== this._selected.value) { this._lastIndex.value = this._selected.value // 每次切换 swiper item 时重置,优先给滚动 this._canSwipe[this._selected.value].value = false } return this._canSwipe[this._selected.value].value }, onSwiperStart() { 'worklet' this._swiping.value = true }, onSwiperEnd() { 'worklet' this._swiping.value = false }, onChange(e) { const {current} = e.detail this.setData({ selected: current, }) this._selected.value = current wx.vibrateShort({ type: 'light', }) }, }, }) ================================================ FILE: examples/associated-scroll-view/pages/index/index.json ================================================ { "usingComponents": { "category-list": "../../components/category-list/category-list" }, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/associated-scroll-view/pages/index/index.wxml ================================================ 请输入课程名称 {{item.name}} {{item.name}} ================================================ FILE: examples/associated-scroll-view/pages/index/index.wxss ================================================ view { position: relative; box-sizing: border-box; } page { background-color: #F9F9F9; display: flex; flex-direction: column; height: 100vh; } .navigation-bar { width: 100%; padding-top: calc(env(safe-area-inset-top) + 0.001px); } .navigation-bar-content { height: 44px; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 20px; } .navigation-bar-content.white { color: white; } .navigation-bar-content.black { color: black; } .navigation-bar-content .back, .navigation-bar-content .more { width: 44px; text-align: center; } .navigation-bar-content .search { flex: 1; margin-right: 60px; margin-top: 7px; align-self: flex-start; } .navigation-bar-content .search-input { position: absolute; right: 0; width: 100%; height: 30px; line-height: 30px; padding-left: 20px; border: 0.5px solid #44b549; border-radius: 15px; font-size: 12px; background-color: #F8F8F8; color: #AAA; } .first-category { height: 100px; } .first-category-list { height: 100%; display: flex; flex-direction: row; } .first-category-item { padding: 10px; flex-shrink: 0; } .first-category-item-image { width: 50px; height: 50px; border-radius: 100%; border: 2px solid white; } .first-category-item-name { font-size: 12px; text-align: center; width: 50px; margin-top: 6px; } .main { flex: 1; display: flex; flex-direction: row; } .second-category { font-size: 12px; background-color: #F9F9F9; } .second-category-item { padding: 15px 5px; width: 80px; transition: ease .3s; transition-property: font-size; text-align: center; } .second-category-item.selected { background-color: white; font-size: 14px; font-weight: bold; color: #44b549; } .product-list-wrapper { flex: 1; background-color: white; height: auto; } ================================================ FILE: examples/associated-scroll-view/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "latest", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": true, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "auto", "tabSize": 4 } } ================================================ FILE: examples/associated-scroll-view/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "associated-scroll-view", "setting": { "compileHotReLoad": false, "skylineRenderEnable": true }, "libVersion": "latest", "condition": {} } ================================================ FILE: examples/associated-scroll-view/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/associated-scroll-view/util.js ================================================ export function getCategories() { const categories = [ '中小商户', '商超零售', '品牌服饰', '餐饮', '医疗', '酒旅', '政务', '开发技术', '产品能力', '运营规范', ] const images = [ 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU_LhYxhaP5JTy7TWgezsDY7RW_l_e04fR7oG7sCKmS8hc8mVeZaY6eUWT3nk-ww_ZQ', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU-O3axOjUJGFgutF9Xc1JL1uxXFWYdW85mWG0Zvm5nv7rvP18CJ0q6-RRFM0xWLLog', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU3ywQmrV-rSREDwo0Hp9m7iIZZ7Njvjq_TlOg_0ss0cgQL0pfKOuB2NRpAcwfALxvw', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU1GROxmiPIBOCoA5Es44GxjN0KuCQQsoxEH33l05TCgk04n0dssHAIPxIV2ycSlSJA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU68wmBQzYcfQfuIAh1IKWq7OyG0EWxdWGhotYHFh-k-JpmkJ1Otq-mYUT8Dp3iucvg', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT1ASo_fbE3TppPoza_9I7U7OreGNv8F8ltq80gqQSzxa-86bt-gWalLtgPDAhflj-w', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT-SSHr1ULMcspj1yPw2dBQkxDV-Y_fOHodKNyHbS2JwBEVLnVKF2X_TPOhwZG9m0hQ', 'https://res.wx.qq.com/op_res/Zmvv0fisUjaMjuqWLhWWkuzGktaXJEQt46EaKsCKeT06Z4tROseXN0joI7h2qwzqyx2FUy57cveZL-8iArI8_Q', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJitcJIfp9P4VSWxi3126XeiyZ2BnnH0xg-oIXAUgHBgaHjBMwxzSjSkEkTMRqzlKZw', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJnw5zq4f_XW3swKAowexqAbziuojU5W9v4CJixA-NDJShkfS0ne3KWY_6SB56yqb3g', ] return categories.map((name, i) => { return {name, image: images[i]} }) } export function getProducts() { let products = [ '小程序性能优化课程', '小程序直播企业实践案例', '微信客服轻松配置,入门必修', '小程序如何帮助传统医院数字化?', '帮你快速掌握小商店经营秘诀', '了解小程序开发动态,听官方为你解读新能力', '快速了解微信小程序在医疗行业的应用', '解析常见小程序违规类型', '想做互联网的生意,可以通过微信怎么经营呢?', '政务行业小程序实践' ] let images = [ 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU_LhYxhaP5JTy7TWgezsDY7RW_l_e04fR7oG7sCKmS8hc8mVeZaY6eUWT3nk-ww_ZQ', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU-O3axOjUJGFgutF9Xc1JL1uxXFWYdW85mWG0Zvm5nv7rvP18CJ0q6-RRFM0xWLLog', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU3ywQmrV-rSREDwo0Hp9m7iIZZ7Njvjq_TlOg_0ss0cgQL0pfKOuB2NRpAcwfALxvw', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU1GROxmiPIBOCoA5Es44GxjN0KuCQQsoxEH33l05TCgk04n0dssHAIPxIV2ycSlSJA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU68wmBQzYcfQfuIAh1IKWq7OyG0EWxdWGhotYHFh-k-JpmkJ1Otq-mYUT8Dp3iucvg', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT1ASo_fbE3TppPoza_9I7U7OreGNv8F8ltq80gqQSzxa-86bt-gWalLtgPDAhflj-w', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT-SSHr1ULMcspj1yPw2dBQkxDV-Y_fOHodKNyHbS2JwBEVLnVKF2X_TPOhwZG9m0hQ', 'https://res.wx.qq.com/op_res/Zmvv0fisUjaMjuqWLhWWkuzGktaXJEQt46EaKsCKeT06Z4tROseXN0joI7h2qwzqyx2FUy57cveZL-8iArI8_Q', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJitcJIfp9P4VSWxi3126XeiyZ2BnnH0xg-oIXAUgHBgaHjBMwxzSjSkEkTMRqzlKZw', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJnw5zq4f_XW3swKAowexqAbziuojU5W9v4CJixA-NDJShkfS0ne3KWY_6SB56yqb3g', ] products = products.concat(products).map((name, id) => ({ id, name, image: images[(id % products.length)], comment: '一如既往的好', sales: 6500, discount: 0.01, price: 0.01 })) return products } ================================================ FILE: examples/card_transition/app.js ================================================ App({}) ================================================ FILE: examples/card_transition/app.json ================================================ { "pages": [ "pages/list/list", "pages/detail/detail" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "sitemapLocation": "sitemap.json", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "componentFramework": "glass-easel", "lazyCodeLoading": "requiredComponents" } ================================================ FILE: examples/card_transition/app.wxss ================================================ ================================================ FILE: examples/card_transition/components/card/card.js ================================================ const { shared } = wx.worklet const FlightDirection = { PUSH: 0, POP: 1, } Component({ options: { virtualHost: true, }, properties: { index: { type: Number, value: -1, }, item: { type: Object, value: {}, }, cardWidth: { type: Number, value: 0 }, }, lifetimes: { created() { this.scale = shared(1) this.opacity = shared(0) this.direction = shared(0) this.srcWidth = shared('100%') this.radius = shared(5) const beginRect = shared(undefined) const endRect = shared(undefined) wx.worklet.runOnUI(() => { 'worklet' globalThis['RouteCardSrcRect'] = beginRect globalThis['RouteCardDestRect'] = endRect })() }, attached() { this.applyAnimatedStyle( '.card_wrap', () => { 'worklet' return { width: this.srcWidth.value, transform: `scale(${this.scale.value})`, } }, { immediate: false, flush: 'sync' }, () => {}, ) this.applyAnimatedStyle( '.card_img', () => { 'worklet' return { opacity: this.opacity.value, borderTopRightRadius: this.radius.value, // 不带单位默认是 px borderTopLeftRadius: this.radius.value, } }, { immediate: false, flush: 'sync' }, () => {}, ) this.applyAnimatedStyle( '.card_desc', () => { 'worklet' return { opacity: this.opacity.value, } }, { immediate: false, flush: 'sync' }, () => {}, ) }, }, methods: { navigateTo(e) { const { index, url, content, ratio, nickname } = e.currentTarget.dataset const urlContent = `../../pages/detail/detail?index=${index}&url=${encodeURIComponent(url)}&content=${content}&ratio=${ratio}&nickname=${nickname}` wx.navigateTo({ url: urlContent, routeType: 'CardScaleTransition', }) }, handleFrame(data) { 'worklet' this.direction.value = data.direction if (data.direction === FlightDirection.PUSH) { // 进入 // 飞跃过程中,卡片从 100% 改为固定宽度,通过 scale 手动控制缩放 this.srcWidth.value = `${data.begin.width}px` this.scale.value = data.current.width / data.begin.width this.opacity.value = 1 - data.progress this.radius.value = 0 // this.shareImgHeight.value = data.begin.height } else if (data.direction === FlightDirection.POP) { // 返回 this.scale.value = data.current.width / data.end.width this.opacity.value = data.progress this.radius.value = 5 } // globalThis 是 UI 线程的全局变量,将 share-element 初始和目标尺寸保存起来,用于下一页面的缩放动画的计算 // TODO: 后续计划优化这里的接口设计 if (globalThis['RouteCardSrcRect'] && globalThis['RouteCardSrcRect'].value == undefined) { globalThis['RouteCardSrcRect'].value = data.begin } if (globalThis['RouteCardDestRect'] && globalThis['RouteCardDestRect'].value == undefined) { globalThis['RouteCardDestRect'].value = data.end } }, }, }) ================================================ FILE: examples/card_transition/components/card/card.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel" } ================================================ FILE: examples/card_transition/components/card/card.wxml ================================================ {{item.content}} {{item.nickname}} {{item.like}} ================================================ FILE: examples/card_transition/components/card/card.wxss ================================================ .card { border-radius: 5px; overflow: hidden; flex-shrink: 0; width: 100%; background-color: white; } .card_wrap { position: absolute; transform-origin: 0 0; width: 100%; display: flex; flex-direction: column; } .card_img { width: 100%; border-top-right-radius: 5px; border-top-left-radius: 5px; } .card_content { padding: 8px; font-size: 14px; font-weight: 500; /* height: 54px; */ overflow: hidden; box-sizing: border-box; } .card_footer { padding: 0 8px 8px; display: flex; flex-direction: row; font-size: 12px; color: #666666; line-height: 20px; } .card_avatar { width: 20px; height: 20px; border-radius: 50%; } .card_nickname { flex: 1; padding-left: 4px; } ================================================ FILE: examples/card_transition/components/navigation-bar/index.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const rect = wx.getMenuButtonBoundingClientRect() wx.getSystemInfo({ success: (res) => { this.setData({ statusBarHeight: res.statusBarHeight, innerPaddingRight: `padding-right:${res.windowWidth - rect.left}px`, leftWidth: `width:${res.windowWidth - rect.left}px`, navBarHeight: rect.bottom + rect.top - res.statusBarHeight, }) } }) }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${show ? '1' : '0'};transition: opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data if (data.delta) { wx.navigateBack({ delta: data.delta }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/card_transition/components/navigation-bar/index.json ================================================ { "component": true, "usingComponents": {}, "addGlobalClass": true, "componentFramework": "glass-easel" } ================================================ FILE: examples/card_transition/components/navigation-bar/index.wxml ================================================ {{title}} ================================================ FILE: examples/card_transition/components/navigation-bar/index.wxss ================================================ .weui-navigation-bar { overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; box-sizing: border-box; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; width: 95px; padding-left: 16px; box-sizing: border-box; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .weui-navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: bold; } @media(prefers-color-scheme: dark) { .weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } .weui-navigation-bar__inner { background-color: #1f1f1f; } } ================================================ FILE: examples/card_transition/pages/detail/detail.js ================================================ import { Curves, CurveAnimation, lerp } from '../list/route' import { clamp } from '../list/utils' const { screenWidth } = wx.getSystemInfoSync() const { shared, timing, Easing } = wx.worklet const GestureState = { POSSIBLE: 0, // 0 此时手势未识别,如 panDown等 BEGIN: 1, // 1 手势已识别 ACTIVE: 2, // 2 连续手势活跃状态 END: 3, // 3 手势终止 CANCELLED: 4, // 4 手势取消, } const transLowerBound = -1/3 * screenWidth const transUpperBound = 2/3 * screenWidth Component({ properties: { index: { type: Number, value: -1, }, url: { type: String, value: '', }, content: { type: String, value: '', }, ratio: { type: Number, value: 1 }, nickname: { type: String, value: '', } }, data: { swiperHeight: 0, imageList: [ 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr7lTnuuiwGJPwwjxDVYbDolj05sAxd5cOESVZt4_nl1KwzkiDWTvG56LuhE45xAaZA', 'https://res.wx.qq.com/op_res/Ak_VAL-nLvq6laAMVJA86rf3NAZ2vY86v757dfja16Z95xtoxk4BWWDuTCPT-pD1SjGGIddUsH0l6C8Yu5LJlw' ] }, lifetimes: { created() { this.startX = shared(0) this.startY = shared(0) this.transX = shared(0) this.transY = shared(0) this.isInteracting = shared(false) }, attached() { this.setData({ swiperHeight: screenWidth / this.data.ratio }) this.customRouteContext = wx.router?.getRouteContext(this); const { primaryAnimation, primaryAnimationStatus, userGestureInProgress, shareEleTop } = this.customRouteContext || {} // 根据进入或返回使用不同曲线换算到的值 const _curvePrimaryAnimation = CurveAnimation({ animation: primaryAnimation, animationStatus: primaryAnimationStatus, curve: Easing.in(Curves.fastOutSlowIn), reverseCurve: Easing.out(Curves.fastOutSlowIn) }) this.applyAnimatedStyle('.detail-content', () => { 'worklet' return { opacity: _curvePrimaryAnimation.value } }) this.applyAnimatedStyle('#fake-host', () => { 'worklet' // pan 手势释放后,触发返回动画,userGestureInProgress 由 startUserGesture() 标记 if (userGestureInProgress.value && globalThis['RouteCardSrcRect'] && globalThis['RouteCardSrcRect'].value != undefined ) { const begin = globalThis['RouteCardSrcRect'].value const end = globalThis['RouteCardDestRect'].value const t = 1 - _curvePrimaryAnimation.value const shareEleX = lerp(begin.left, end.left, t) const shareEleY = lerp(begin.top, end.top, t) const shareEleW = lerp(begin.width, end.width, t) const scale = shareEleW / screenWidth const transX = shareEleX // shareEleTop 是完全展开时 share-element 的 top 值,换比例换算 // 使得缩放过程中,最后图片顶部对齐卡片图片顶部 const transY = shareEleY - shareEleTop.value * scale return { transform: `translateX(${transX}px) translateY(${transY}px) scale(${scale})`, transformOrigin: '0 0', } } // pan 手势移动阶段 const transX = this.transX.value const transY = this.transY.value // 根据横坐标位移比例缩放 const scale = clamp(1 - transX / screenWidth * 0.5, 0, 1) return { transform: `translateX(${transX}px) translateY(${transY}px) scale(${scale})`, transformOrigin: '50% 50%' } }, { immediate: false }) }, }, methods: { handlePanGesture(e) { 'worklet' const { startUserGesture, stopUserGesture, primaryAnimation, didPop, } = this.customRouteContext if (e.state === GestureState.BEGIN) { this.startX.value = e.absoluteX this.startY.value = e.absoluteY } else if (e.state === GestureState.ACTIVE) { // 往右滑时 if (e.deltaX > 0 && !this.isInteracting.value) { this.isInteracting.value = true } if (!this.isInteracting.value) return const transX = e.absoluteX - this.startX.value this.transX.value = clamp(transX, transLowerBound, transUpperBound) this.transY.value = e.absoluteY - this.startY.value } else if (e.state === GestureState.END || e.state === GestureState.CANCELLED) { if (!this.isInteracting.value) return this.isInteracting.value = false // 是要返回还是取消返回 let shouldFinish = false if (e.velocityX > 500 || this.transX.value / screenWidth > 0.25) { shouldFinish = true } if (shouldFinish) { startUserGesture() primaryAnimation.value = timing(0.0, { duration: 180, easing: Easing.linear }, () => { 'worklet' stopUserGesture() didPop() }) } else { this.transX.value = timing(0.0, { duration: 100 }) this.transY.value = timing(0.0, { duration: 100 }) } } }, }, }) ================================================ FILE: examples/card_transition/pages/detail/detail.json ================================================ { "usingComponents": {}, "backgroundColorContent": "#00000000", "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/card_transition/pages/detail/detail.wxml ================================================ {{nickname}} 关注 {{content}} 🔥 Skyline 以性能为首要目标,提供更为接近原生的用户体验,Skyline 具有以下特点: 🌟 界面更不容易被逻辑阻塞,进一步减少卡顿 🌟 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销 🌟 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销 🌟 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销 👇👇👇 支持了一些 Web 所缺失的但很重要的能力,以满足开发者实现更好的交互体验: 🔸 Worklet 动画:能够在渲染线程同步运行动画相关逻辑。 🔸 手势系统:在渲染线程同步监听手势、执行手势相关逻辑;支持手势协商处理; 🔸 自定义路由:实现自定义路由动画和交互。 🔸 共享元素动画:将上一个页面的元素“共享”到下一个页面,并伴随着过渡动画。 ❤️ 317 ⭐️ 723 ================================================ FILE: examples/card_transition/pages/detail/detail.wxss ================================================ page { display: flex; flex-direction: column; height: 100vh; background-color: transparent; } view, scroll-view, page, image { box-sizing: border-box; } #fake-host { background-color: #FFF; border-radius: 10px; overflow: hidden; } #page { display: flex; flex-direction: column; height: 100vh; } .navigation-bar { padding-top: 44px; padding-top: env(safe-area-inset-top); background-color: white; } .navigation-bar-content { height: 44px; display: flex; flex-direction: row; padding: 0 20px; justify-content: center; align-items: center; font-size: 14px; } .navigation-bar-avatar { width: 24px; height: 24px; border-radius: 100%; } .navigation-bar-title { flex: 1; padding-left: 8px; } .navigation-bar-follow { color: red; border: .5px solid red; border-radius: 15px; padding: 3px 15px; margin-right: 80px; } .detail-image { width: 100%; height: 100%; } .detail-content { padding: 15px; } .detail-title { font-weight: bold; font-size: 18px; padding-bottom: 3px; } .detail-p { padding-bottom: 2px; } .footer { padding-bottom: env(safe-area-inset-bottom); font-size: 14px; } .footer-content { display: flex; align-items: center; flex-direction: row; height: 50px; } .footer .footer-input { flex: 1; background-color: #EEE; margin-left: 15px; margin-right: 15px; padding-left: 15px; height: 40px; border-radius: 20px; } .footer span { padding-right: 15px; } .footer-icon { font-size: 26px; } ================================================ FILE: examples/card_transition/pages/list/list.js ================================================ import { installRouteBuilder } from './route' import { generateGridList, compareVersion } from './utils' const { screenWidth } = wx.getSystemInfoSync() Component({ data: { padding: 4, gridList: generateGridList(100, 2), cardWidth: (screenWidth - 4 * 2 - 4) / 2, // 减去间距 }, lifetimes: { created() { const {SDKVersion} = wx.getSystemInfoSync() if (compareVersion(SDKVersion, '2.30.1') < 0) { wx.showModal({ content: '基础库版本低于 v2.30.1 可能会有显示问题,建议升级微信体验。', showCancel: false }) } installRouteBuilder() }, }, }) ================================================ FILE: examples/card_transition/pages/list/list.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar/index", "card": "../../components/card/card" }, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/card_transition/pages/list/list.wxml ================================================ ================================================ FILE: examples/card_transition/pages/list/list.wxss ================================================ page { display: flex; flex-direction: column; height: 100vh; } scroll-view { background-color: #f4f4f4; } view, scroll-view, page, image { box-sizing: border-box; } ================================================ FILE: examples/card_transition/pages/list/route.js ================================================ const AnimationStatus = { dismissed: 0, // The animation is stopped at the beginning. forward: 1, // The animation is running from beginning to end. reverse: 2, // The animation is running backwards, from end to beginning. completed: 3, // The animation is stopped at the end. } const { Easing, shared } = wx.worklet export const Curves = { linearToEaseOut: Easing.cubicBezier(0.35, 0.91, 0.33, 0.97), easeInToLinear: Easing.cubicBezier(0.67, 0.03, 0.65, 0.09), fastOutSlowIn: Easing.cubicBezier(0.4, 0.0, 0.2, 1.0), slowOutFastIn: Easing.cubicBezier(0.0, 0.8, 1.0, 0.6), easeOutCubic: Easing.cubicBezier(0.215, 0.61, 0.355, 1.0), } export function CurveAnimation({ animation, animationStatus, curve, reverseCurve }) { const { derived } = wx.worklet return derived(() => { 'worklet' const useForwardCurve = !reverseCurve || animationStatus.value !== AnimationStatus.reverse const activeCurve = useForwardCurve ? curve : reverseCurve const t = animation.value if (!activeCurve) return t if (t === 0 || t === 1) return t return activeCurve(t) }) } export const lerp = (begin, end, t) => { 'worklet' return begin + (end - begin) * t } const ScaleTransitionRouteBuilder = (routeContext) => { const { primaryAnimation, primaryAnimationStatus, userGestureInProgress, } = routeContext const shareEleTop = shared(0) routeContext.shareEleTop = shareEleTop const _curvePrimaryAnimation = CurveAnimation({ animation: primaryAnimation, animationStatus: primaryAnimationStatus, curve: Easing.in(Curves.fastOutSlowIn), reverseCurve: Easing.out(Curves.fastOutSlowIn) }) // 每次路由动画结束(进入或返回)都会重置一下 const reset = () => { 'worklet' if (globalThis['RouteCardSrcRect']) { globalThis['RouteCardSrcRect'].value = undefined } if (globalThis['RouteCardDestRect']) { globalThis['RouteCardDestRect'].value = undefined } } const handlePrimaryAnimation = () => { 'worklet' const status = primaryAnimationStatus.value // 手势返回时,动画在详情页处理,此处顶层节点只做整体透明度淡出 if (userGestureInProgress.value) { return { opacity: Easing.out(Easing.cubicBezier(0.5, 0, 0.7, 0.5)(primaryAnimation.value)), } } if (status == AnimationStatus.dismissed) { reset() return { transform: `translate(0, 0) scale(0)`, } } if (status == AnimationStatus.completed ) { reset() return { transform: `translate(0, 0) scale(1)`, } } let transX = 0 let transY = 0 let scale = status === AnimationStatus.reverse ? 1 : 0 // 进入或者接口返回 if (globalThis['RouteCardSrcRect'] && globalThis['RouteCardSrcRect'].value != undefined) { const begin = globalThis['RouteCardSrcRect'].value const end = globalThis['RouteCardDestRect'].value if (status === AnimationStatus.forward) { shareEleTop.value = end.top } let t = _curvePrimaryAnimation.value if (status === AnimationStatus.reverse || status === AnimationStatus.dismissed) { t = 1 - t } const shareEleX = lerp(begin.left, end.left, t) const shareEleY = lerp(begin.top, end.top, t) const shareEleW = lerp(begin.width, end.width, t) transX = shareEleX if (status === AnimationStatus.reverse) { scale = shareEleW / begin.width transY = shareEleY - begin.top * scale } else { scale = shareEleW / end.width transY = shareEleY - end.top * scale } } return { transform: `translate(${transX}px, ${transY}px) scale(${scale})`, transformOrigin: '0 0', opacity: _curvePrimaryAnimation.value, } } return { opaque: false, handlePrimaryAnimation, transitionDuration: 250, reverseTransitionDuration: 250, canTransitionTo: false, canTransitionFrom: false, barrierColor: "rgba(0, 0, 0, 0.3)", } } let hasInstalled = false export function installRouteBuilder() { if (hasInstalled) { return } wx.router.addRouteBuilder('CardScaleTransition', ScaleTransitionRouteBuilder) hasInstalled = true } ================================================ FILE: examples/card_transition/pages/list/utils.js ================================================ export const lightBlue = { 0: '#E1F5FE', 100: '#B3E5FC', 200: '#81D4FA', 300: '#4FC3F7', 400: '#29B6F6', 500: '#03A9F4', 600: '#039BE5', 700: '#0288D1', 800: '#0277BD', 900: '#01579B', } export const generateList = (childCount) => { const ans = [] for (let i = 0; i < childCount; i++) { ans.push({ id: i, color: lightBlue[`${100 * (i % 9)}`], }) } return ans } const contents = [ '小程序推出 Skyline 新渲染框架啦', '推荐 Skyline,使用后体验流畅很多~', '开发必备!共享元素、自定义路由、手势系统', 'Hayya Hayya!我用小程序啦', ] const nicknames = [ 'REX', 'BINNIE', 'ERIC', 'SANFORD', ] const imageRatio = [ { width: 3, height: 4, imageRatio: 3 / 4, }, { width: 4, height: 3, imageRatio: 4 / 3, }, { width: 1, height: 1, imageRatio: 1 / 1, }, ] const imageList = [ // 3:4 [ 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr_TJOaxvM0jTWnZCPVx5tYhqZIIAWcwZ-wjkthDNgUPon6gB8cS1-4Gmj9Fa0emByQ', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr5TiaeMo-e_G_0VkoAgrUpJDa0vkq7A-ZqnGdXPqENXxwOpNm6WNaukJzkaNpe2l4g', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr3Vg3QwFEkRrtGVFfuis3HPsfPRAimoR3xrmxA6WqSP6gqLYxpQR70H0Mjd82xRvLg', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr57xPb6otBpyKgqlzjXvSaLKB_SPr5oYFTYCYUbk6bCwyLvvPWUVpsNuYRjVNouuDw', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr8oVdhjDzwpGQWkUNT3VLWmNYEetJXErnWq48jD0zVELo45qmUAdu7jCgFskY6Eh8w', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr1x4v1gTqT3MrC7LtVTjQXb_9hd9vbCf12guLPXiMXd0G7IUnLQXkOa-o1eNyAJ_nA', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr7lTnuuiwGJPwwjxDVYbDolj05sAxd5cOESVZt4_nl1KwzkiDWTvG56LuhE45xAaZA' ], // 4:3 [ 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr87sFqvqtkPc7qeZdary_8crGWuX_SOb72lupHA7sWx0dti3JrJXdP_lwm0ZtvINXg', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr3vA4i7lSkWNR0BRe_g4A-_lo5MYYlkks8oHLoZzXjqAm_M3RvDAXtn9UUgZuQtVBA', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr5Ifsj1_cRjONPrw-gUgq8g6BNH8sYQ3kBBQas5JAeMN0zsCBY9gmz3D7kj_GOWfHw', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr1IwceePWSJ_EhG4QedvnFKN6v_mNlNuwG2FkAIoOhx_1fyCDEqtHWSktSrPmLvTpw', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihrzz951X66QJWV_Oj4MT6XImEk-wFlNZP6mJE1Vt-ybtD1UK7ARlhOBl9bizrC5KA9g', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihrxFO1zooQxE0ufna7fMaqrU-Pp4Dm2rw5dFcTdBymLTijegIFw3WcVD1rUyLD4XTig', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr6WcfJCajSnCm4CNu5oQ5HPsPqyzWD-vtFVuJDZOhMpcG1iN0tvOsvS8DUgn3qO8UA', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr4HKYTq7-4l-F47z8u2QbvNsjcTEA3Cu5-4wQpBGPeWKCh66Ho5W42fn3naWuN2NJg', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr8PdfZEyicDsJiFPBw8MAjve2UKbzLds_-IZW_Q0EYUbboQk-31FeTkFmzuNzCfLHg' ], // 1:1 [ 'https://res.wx.qq.com/op_res/KSWft_GRyQ3WEzVUTCSWs7HaJh0lgdPce6Uon3dhNpZ3R3sTVA3NLrOORpMDGaBl5P8QkzHZCaOErPlma2sAow', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihrwcWdDUeblb42H9kVfv14Eru-W62xBL1bUXbfwZbaJG7_JrKvnAKvdVCQJkS3PX3IQ', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr13IooGqagGNd7x5NTGbtrz4g0NrIVLLJ2KSx-BcYpaGMTpnv-pUB_iexsCzQC4wZg', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihrxD9Yj0ZHr0C5YMm7qYRo2fqji9kH4CS6LUyQf4YXzHzK3BW0FFNZiTQb6AK9bp1WA', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr5yBy6GoASjPro9uFIUZVFdiDIjiJObbopuhr7PUXnsTLQ537ujpIBxyX2Ln2gRu0w', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihrzhx4m_v7j5nYGhkUG5h-dulp3X7FxpQVY8L1QzVqPROJHUcK0mO38isUiclpbae_Q', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr_VrnIzjbAVDL2cmG0wjYsNZv1l_lacmGCshp9OEz3QcPnn9YymbITplyQS5T5C-VA', 'https://res.wx.qq.com/op_res/BqgN85sXxTbk1kynEEihr0m2rsO-Y1l6Wsz_sFyu7vJj_ZTfI7GABbstLg4GUDTZVeZCKgDADCmsDjmF8rG7dw' ] ] export const generateGridList = (childCount, columns) => { const ans = [] for (let i = 0; i < childCount; i++) { const ratioIdx = Math.floor(Math.random() * imageRatio.length) const ratio = imageRatio[ratioIdx] const img = imageList[ratioIdx][Math.floor(Math.random() * imageList[ratioIdx].length)] ans.push({ id: i, src: img, ...ratio, like: Math.floor(Math.random() * 10000), content: contents[Math.floor(Math.random() * contents.length)], nickname: nicknames[Math.floor(Math.random() * nicknames.length)], }) } return ans } export const clamp = function (cur, lowerBound, upperBound) { 'worklet'; if (cur > upperBound) return upperBound; if (cur < lowerBound) return lowerBound; return cur; }; export const compareVersion = function (v1, v2) { v1 = v1.split('.') v2 = v2.split('.') const len = Math.max(v1.length, v2.length) while (v1.length < len) { v1.push('0') } while (v2.length < len) { v2.push('0') } for (let i = 0; i < len; i++) { const num1 = parseInt(v1[i], 10) const num2 = parseInt(v2[i], 10) if (num1 > num2) { return 1 } else if (num1 < num2) { return -1 } } return 0 } ================================================ FILE: examples/card_transition/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "latest", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": false, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 } } ================================================ FILE: examples/card_transition/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "card_transition", "setting": { "compileHotReLoad": false, "skylineRenderEnable": false }, "condition": { "miniprogram": { "list": [ { "name": "", "pathName": "detail/detail", "query": "index=1&url=https%3A%2F%2Fpicsum.photos%2F300%2F400%3Frandom%3D1&content=Hayya Hayya!我来小红书啦&ratio=0.75", "launchMode": "default", "scene": null }, { "name": "", "pathName": "list/list", "query": "", "launchMode": "default", "scene": null } ] } }, "libVersion": "latest" } ================================================ FILE: examples/card_transition/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/expanded-scroll-view/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/expanded-scroll-view/app.js ================================================ // app.js App({}) ================================================ FILE: examples/expanded-scroll-view/app.json ================================================ { "pages": [ "pages/index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true } }, "lazyCodeLoading": "requiredComponents", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/expanded-scroll-view/app.wxss ================================================ ================================================ FILE: examples/expanded-scroll-view/pages/index/index.js ================================================ import { getCategory, getProducts } from '../../util' const { shared, timing, decay, derived, runOnUI, runOnJS } = wx.worklet const categories = getCategory() const products = getProducts() const GestureState = { POSSIBLE: 0, // 0 此时手势未识别,如 panDown等 BEGIN: 1, // 1 手势已识别 ACTIVE: 2, // 2 连续手势活跃状态 END: 3, // 3 手势终止 CANCELLED: 4, // 4 手势取消, } const InteractionState = { INITIAL: 0, ANIMATING: 1, UNFOLD: 2, SCROLL: 3, RESET: 4, } const clamp = (num, min, max) => { 'worklet' return Math.min(Math.max(num, min), max) } Component({ data: { statusBarHeight: 0, categories, selected: 0, list: categories.map(category => { return { header: category, data: products } }), expand: false, }, lifetimes: { created() { this._interactionState = shared(0) this._tabsTop = shared(0) this._mainHeight = shared(700) this._startY = shared(0) this._translY = shared(0) this._deltaY = shared(0) this._scrollTop = shared(0) this._canPan = shared(true) }, attached() { const windowInfo = wx.getWindowInfo() this.setData({ statusBarHeight: windowInfo.statusBarHeight }) const updateNavBackColor = this.updateNavBackColor.bind(this) this._listenTranslY = derived(() => { 'worklet' const expand = this._translY.value < -this._tabsTop.value / 2 runOnJS(updateNavBackColor)(expand) }) }, }, methods: { onBannerLoaded() { this.createSelectorQuery().select('.tabs').boundingClientRect(tabs => { this.createSelectorQuery().select('.navigation-bar').boundingClientRect(nav => { this._tabsTop.value = tabs.top - nav.height this.bindAnimatedStyle() }).exec() }).exec() this.createSelectorQuery().select('.main').boundingClientRect(main => { this._mainHeight.value = main.height }).exec() }, updateNavBackColor(expand) { this.setData({expand}) }, bindAnimatedStyle() { /* 下面是 从顶部往上拉的动画部分 */ this.applyAnimatedStyle('.page', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) return { transform: `translateY(${translY}px)` } }) this.applyAnimatedStyle('.navigation-bar', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const opacity = translY / -this._tabsTop.value return { backgroundColor: `rgba(255, 255, 255, ${opacity})` } }) // color 是继承属性,暂不支持 // this.applyAnimatedStyle('.navigation-bar-content .back', () => { // 'worklet' // const translY = clamp(this._translY.value, -this._tabsTop.value, 0) // const value = (1 - translY / -this._tabsTop.value) * 255 | 0 // return { // color: `rgb(${value}, ${value}, ${value})` // } // }) this.applyAnimatedStyle('.search-input', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const percentage = translY / -this._tabsTop.value return { width: `${percentage * 60 + 40}%`, opacity: percentage, } }) /* 下面是 到顶往下拉的动画部分 */ this.applyAnimatedStyle('.main', () => { 'worklet' const translY = clamp(this._translY.value, 0, Number.MAX_VALUE) return { transform: `translateY(${translY}px)` } }) this.applyAnimatedStyle('.header-shop-info-simple', () => { 'worklet' const min = 50 const max = 100 const translY = clamp(this._translY.value, min, max) - min return { opacity: 1 - (translY / (max - min)) } }) this.applyAnimatedStyle('.header-shop-info-detail', () => { 'worklet' const min = 100 const max = 150 const translY = clamp(this._translY.value, min, max) - min return { opacity: translY / (max - min) } }) }, handlePan(e) { 'worklet' const _interactionState = this._interactionState if (this._interactionState.value === InteractionState.ANIMATING) { return } if (this._interactionState.value === InteractionState.RESET) { // 在 gesture active 期间触发的动画,在手指起来时才能重置回 INITIAL if (e.state === GestureState.END || e.state === GestureState.CANCELLED) { this._interactionState.value = InteractionState.INITIAL } return } if (e.state === GestureState.BEGIN) { const lastTranslY = clamp(this._translY.value, -this._tabsTop.value, 0) this._startY.value = e.absoluteY - lastTranslY } if (e.state === GestureState.ACTIVE) { if (this._interactionState.value === InteractionState.UNFOLD) { // 展开状态下,往上滑才折叠起来 if (e.absoluteY - this._startY.value < 0) { this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' _interactionState.value = InteractionState.RESET }) } } else { // 其它情况,跟随手指滑动 this._translY.value = e.absoluteY - this._startY.value } } if (e.state === GestureState.END || e.state === GestureState.CANCELLED) { if (this._translY.value > 100) { // 超过 100 就展开 this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(this._mainHeight.value, { duration: 250 }, () => { 'worklet' _interactionState.value = InteractionState.UNFOLD }) } else if (this._translY.value > 0) { // 没超过 100 但还在下拉状态就回弹 this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' _interactionState.value = InteractionState.INITIAL }) } else if (this._translY.value > -this._tabsTop.value) { // 往上滑就做滚动动画 this._interactionState.value = InteractionState.SCROLL this._translY.value = decay({velocity: e.velocityY, clamp: [-this._tabsTop.value, 0]}) } } }, shouldPanResponse() { 'worklet' return this._canPan.value }, handleScroll(e) { 'worklet' const _interactionState = this._interactionState this._scrollTop.value = e.detail.scrollTop if (this._scrollTop.value < 0 && !e.detail.isDrag && !this._canPan.value && this._interactionState.value !== InteractionState.ANIMATING) { this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' _interactionState.value = InteractionState.INITIAL }) } }, shouldScrollViewResponse(e) { 'worklet' if (this._translY.value > -this._tabsTop.value) { this._canPan.value = true } else { // 触顶 && 往下拉时,pan 手势生效 this._canPan.value = this._scrollTop.value <= 0 && e.deltaY > 0 } return !this._canPan.value }, adjustDeceleration(velocity) { 'worklet' const scrollTop = this._scrollTop.value return scrollTop <= 0 ? 0 : velocity }, }, }) ================================================ FILE: examples/expanded-scroll-view/pages/index/index.json ================================================ { "usingComponents": {}, "disableScroll": true, "navigationStyle": "custom", "renderer": "skyline", "componentFramework": "glass-easel" } ================================================ FILE: examples/expanded-scroll-view/pages/index/index.wxml ================================================ 请输入课程名称 微信学堂 ⭐️ 5.066万人学过好评度100% 开发行业服务商 ⭐️ 5.066万人学过好评度100% 视频讲解、代码演示、文档一应俱全 热门活动 开发 小程序开发从入门到进阶 行业 线上运营方法论 服务商 如何成为微信服务商 公告 「微信学堂」已经上线了 70+ 门专题课程,支持小程序、公众号、企业微信、视频号等产品能力,帮助广大开发者和商家共同成长。
商超零售、房地产、餐饮、酒旅、医疗... 「微信学堂」提供超过10个行业的经营教程。报名成为“微信小程序行业伙伴”,还有机会参加每月的闭门交流会,获取官方一手信息。
课程 评价 {{item}} {{item.header}} {{subItem.name}} {{subItem.comment}} {{subItem.sales}}人学过好评度100% ¥ {{subItem.price}} +
================================================ FILE: examples/expanded-scroll-view/pages/index/index.wxss ================================================ view { position: relative; box-sizing: border-box; } .navigation-bar { position: absolute; z-index: 1; top: 0; width: 100%; padding-top: env(safe-area-inset-top); background-color: transparent; } .navigation-bar-content { height: 44px; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 20px; } .navigation-bar-content.white { color: white; } .navigation-bar-content.black { color: black; } .navigation-bar-content .back, .navigation-bar-content .more { width: 44px; text-align: center; } .navigation-bar-content .search { flex: 1; margin-right: 60px; margin-top: 7px; align-self: flex-start; } .navigation-bar-content .search-input { position: absolute; right: 0; width: 100%; height: 30px; line-height: 30px; padding-left: 20px; border-radius: 15px; font-size: 12px; background-color: #F8F8F8; color: #AAA; opacity: 0; } .page { position: absolute; display: flex; flex-direction: column; width: 100vw; height: 150vh; } .header-banner { position: absolute; width: 100%; height: 140px; z-index: -1; } .header-shop-outer { padding-top: calc(env(safe-area-inset-top) + 70px); background-color: rgba(0, 0, 0, .5); } .header-shop-inner { background-color: white; border-top-left-radius: 10px; border-top-right-radius: 10px; padding: 10px 15px; font-size: 12px; line-height: 1.8; } .header-shop-name { font-size: 20px; font-weight: bold; margin-bottom: 5px; } .header-shop-info-simple { color: #333; } .header-shop-data { display: flex; flex-direction: row; line-height: 1.2; color: gray; margin-bottom: 8px; } .header-shop-data text { padding-right: 5px; } .header-shop-data text:first-child { color: orange; } .header-shop-tags { display: flex; flex-direction: row; margin-bottom: 5px; } .header-shop-tags text { background-color: #EEE; padding: 1px 3px; border-radius: 3px; } .header-shop-info-simple .header-shop-coupon { display: flex; flex-direction: row; margin-bottom: 5px; color: red; } .header-shop-info-simple .header-shop-coupon text { padding: 0 2px; border: 0.5px solid red; border-radius: 2px; margin-right: 5px; } .header-shop-info-detail { position: absolute; top: 0; width: 100%; opacity: 0; color: #333; } .header-shop-info-detail .header-shop-coupon, .header-shop-info-detail .header-shop-announcement { margin-top: 20px; } .header-shop-info-detail .title { font-size: 16px; font-weight: bold; margin-bottom: 5px; } .header-shop-coupon-item { display: flex; flex-direction: row; margin-bottom: 6px; } .header-shop-coupon-item view { background-color: orangered; color: white; font-weight: 500; border-radius: 3px; margin-right: 6px; padding: 0 4px; } .main { flex: 1; overflow: hidden; background-color: white; } .main-banner { margin: 10px 15px; border-radius: 8px; /* height: 50px; */ /* background-color: burlywood; */ } .main-banner image { width: 100%; border-radius: 8px; } .tabs { border-bottom: 0.5px solid #EEE; } .tabs-list { display: flex; flex-direction: row; } .tabs-list text { padding: 10px 15px; } .tabs-indicator { position: absolute; bottom: 0; left: 0; height: 2px; width: 20px; margin: 0 21px; background-color: orange; } .product { height: 100%; display: flex; flex-direction: row; } .product-category { font-size: 12px; background-color: #F9F9F9; } .product-category-item { padding: 15px; } .product-category-item.selected { background-color: white; } .product-list { flex: 1; } .product-group { font-size: 16px; padding: 10px; background-color: white; } .product-item { display: flex; flex-direction: row; padding: 8px; } .product-image { width: 133px; height: 100px; border-radius: 5px; } .product-info { flex: 1; padding: 0 8px; font-size: 10px; } .product-info view { margin-bottom: 3px; } .product-name { font-size: 16px; } .product-comment { display: flex; flex-direction: row; } .product-comment text { background-color: #EEE; border-radius: 3px; padding: 1px 3px; } .product-data { display: flex; flex-direction: row; color: gray; } .product-data text { margin-right: 20px; } .product-discount { display: flex; flex-direction: row; } .product-discount text { color: red; border: 0.3px solid red; padding: 0 3px; border-radius: 3px; } .product-price { color: red; vertical-align: baseline; } .product-add-to-cart { position: absolute; right: 12px; bottom: 12px; border-radius: 6px; color: #fff; background-color: #44b549; width: 20px; height: 20px; line-height: 16px; font-weight: 500; text-align: center; } sticky-header { position: sticky; top: 0; z-index: 1; display: block; } ================================================ FILE: examples/expanded-scroll-view/project.config.json ================================================ { "compileType": "miniprogram", "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "ignoreUploadUnusedFiles": true, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 }, "packOptions": { "ignore": [], "include": [] }, "appid": "wxe5f52902cf4de896" } ================================================ FILE: examples/expanded-scroll-view/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "expanded-scroll-view", "setting": { "compileHotReLoad": false, "skylineRenderEnable": true }, "libVersion": "latest" } ================================================ FILE: examples/expanded-scroll-view/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/expanded-scroll-view/util.js ================================================ export function getCategory() { let categorys = [ '中小商户', '商超零售', '品牌服饰', '餐饮', '医疗', '酒旅', '政务', '开发技术', '产品能力', '运营规范', ] return categorys } export function getProducts() { let products = [ '小程序性能优化课程', '小程序直播企业实践案例', '微信客服轻松配置,入门必修', '小程序如何帮助传统医院数字化?', '帮你快速掌握小商店经营秘诀', '了解小程序开发动态,听官方为你解读新能力', '快速了解微信小程序在医疗行业的应用', '解析常见小程序违规类型', '想做互联网的生意,可以通过微信怎么经营呢?', '政务行业小程序实践' ] let images = [ 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU_LhYxhaP5JTy7TWgezsDY7RW_l_e04fR7oG7sCKmS8hc8mVeZaY6eUWT3nk-ww_ZQ', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU-O3axOjUJGFgutF9Xc1JL1uxXFWYdW85mWG0Zvm5nv7rvP18CJ0q6-RRFM0xWLLog', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU3ywQmrV-rSREDwo0Hp9m7iIZZ7Njvjq_TlOg_0ss0cgQL0pfKOuB2NRpAcwfALxvw', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU1GROxmiPIBOCoA5Es44GxjN0KuCQQsoxEH33l05TCgk04n0dssHAIPxIV2ycSlSJA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU68wmBQzYcfQfuIAh1IKWq7OyG0EWxdWGhotYHFh-k-JpmkJ1Otq-mYUT8Dp3iucvg', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT1ASo_fbE3TppPoza_9I7U7OreGNv8F8ltq80gqQSzxa-86bt-gWalLtgPDAhflj-w', 'https://res.wx.qq.com/op_res/UxKgRAAdvQE0sTh7eCEwT-SSHr1ULMcspj1yPw2dBQkxDV-Y_fOHodKNyHbS2JwBEVLnVKF2X_TPOhwZG9m0hQ', 'https://res.wx.qq.com/op_res/Zmvv0fisUjaMjuqWLhWWkuzGktaXJEQt46EaKsCKeT06Z4tROseXN0joI7h2qwzqyx2FUy57cveZL-8iArI8_Q', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJitcJIfp9P4VSWxi3126XeiyZ2BnnH0xg-oIXAUgHBgaHjBMwxzSjSkEkTMRqzlKZw', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJnw5zq4f_XW3swKAowexqAbziuojU5W9v4CJixA-NDJShkfS0ne3KWY_6SB56yqb3g', ] products = products.map((name, id) => ({ name, image: images[(id % products.length)], comment: '一如既往的好', sales: 6500, discount: 0.01, price: 0.01 })) return products } ================================================ FILE: examples/half-screen/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/half-screen/app.js ================================================ // app.js App({}) ================================================ FILE: examples/half-screen/app.json ================================================ { "pages": [ "pages/index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "lazyCodeLoading": "requiredComponents", "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/half-screen/app.wxss ================================================ ================================================ FILE: examples/half-screen/pages/index/comment-data.js ================================================ // 获取留言列表 export function getCommentList() { return [ { "comment": "为了进一步优化小程序性能,提供更为接近原生的用户体验,我们在 WebView 渲染之外新增了一个渲染引擎 Skyline,其使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "subCommentList": [ { "comment": "界面更不容易被逻辑阻塞,进一步减少卡顿。", "userName": "拖拉机🚜", "userHeadImg": "https://res.wx.qq.com/op_res/0AG3_hOKnGAqBhAhBx_a__0nu3Q_hGgnBQgiQhJMqZrvroKqdtYXhcSUdlp59bXjx7qF-ddTwGCcB-AqzYmlrw", "replyUserName": "binnie" }, { "comment": "无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销。", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "replyUserName": "拖拉机🚜", }, { "comment": "框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销。", "userName": "拖拉机🚜", "userHeadImg": "https://res.wx.qq.com/op_res/0AG3_hOKnGAqBhAhBx_a__0nu3Q_hGgnBQgiQhJMqZrvroKqdtYXhcSUdlp59bXjx7qF-ddTwGCcB-AqzYmlrw", "replyUserName": "binnie" }, { "comment": "框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销。", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "replyUserName": "拖拉机🚜", } ] }, { "comment": "Skyline 以性能为首要目标,因此特性上在满足基本需求的前提下进行了大幅精简,目前 Skyline 只实现了 CSS 特性的子集。在编码上,Skyline 与 WebView 模式保持一致,仍使用 WXML 和 WXSS 编写界面。在不采用 Skyline 新增特性的情况下,适配了 Skyline 的小程序在低版本或未支持 Skyline 的平台上可无缝自动退回到 WebView 渲染。", "userName": "拖拉机🚜", "userHeadImg": "https://res.wx.qq.com/op_res/0AG3_hOKnGAqBhAhBx_a__0nu3Q_hGgnBQgiQhJMqZrvroKqdtYXhcSUdlp59bXjx7qF-ddTwGCcB-AqzYmlrw", "subCommentList": [ { "comment": "基于 Worklet 机制的 动画模块,能够在渲染线程同步运行动画相关逻辑。", "userName": "拖拉机🚜", "userHeadImg": "https://res.wx.qq.com/op_res/0AG3_hOKnGAqBhAhBx_a__0nu3Q_hGgnBQgiQhJMqZrvroKqdtYXhcSUdlp59bXjx7qF-ddTwGCcB-AqzYmlrw", "replyUserName": "binnie" }, { "comment": "基于 Worklet 机制的 手势系统。在渲染线程同步监听手势、执行手势相关逻辑;支持手势协商处理;", "userName": "小苹果🍎", "userHeadImg": "https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBAK7KJg9KId_6N1-rJ4OyCCQoJGVMOaTabboo2viRucoxkPvHRkn2fVl6tectlzBg", "replyUserName": "binnie" }, { "comment": "基于 Worklet 机制的 自定义路由模块,支持实现自定义路由动画和交互。", "userName": "流星雨", "userHeadImg": "https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYf3q0W302-kseD8VxLKoItZ6HgneLkgpQSEMIgEKz_xVE7putZxs2YEYqB13Uh37_w", "replyUserName": "binnie" }, { "comment": "支持 跨页面共享元素,能够将上一个页面的元素“共享”到下一个页面,并伴随着过渡动画。", "userName": "落日余晖", "userHeadImg": "https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYjda9Dp372N3T05q_nn3PgvoXBoReXvaXBfkthtXQLN7m5_YI6FoTre-xvJBDFLMA", "replyUserName": "binnie" }, ] }, { "comment": "Skyline 能很好地保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行。", "userName": "小苹果🍎", "userHeadImg": "https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJBAK7KJg9KId_6N1-rJ4OyCCQoJGVMOaTabboo2viRucoxkPvHRkn2fVl6tectlzBg", "subCommentList": [] }, { "comment": "Skyline 支持了一些 Web 所缺失的但很重要的能力,以满足开发者实现更好的交互体验。", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "subCommentList": [ ] }, { "comment": "worklet 动画可以做到类原生动画般的体验。", "userName": "流星雨", "userHeadImg": "https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYf3q0W302-kseD8VxLKoItZ6HgneLkgpQSEMIgEKz_xVE7putZxs2YEYqB13Uh37_w", "subCommentList": [ { "comment": "提供如 timing、spring 等常见动画方式的封装方法,开发者可自定义动画曲线,同时可对不同的动画类型进行组合、重复,形成交织动画。😄", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "replyUserName": "流星雨" } ] }, { "comment": "Skyline 中 wxs 代码运行在 AppService 线程,而事件产生在 UI 线程,因此 wxs 动画 性能有所降低,为了提升小程序交互体验的效果,我们内置了一批手势组件。", "userName": "落日余晖", "userHeadImg": "https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYjda9Dp372N3T05q_nn3PgvoXBoReXvaXBfkthtXQLN7m5_YI6FoTre-xvJBDFLMA", "subCommentList": [ { "comment": "免去开发者监听 touch 事件,自行计算手势逻辑的复杂步骤;", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "replyUserName": "落日余晖" }, { "comment": "手势组件直接在 UI 线程响应,避免了传递到 JS 线程带来的延迟;", "userName": "落日余晖", "userHeadImg": "https://res.wx.qq.com/op_res/7_miJnK0wxIrh5bV2QqvYYjda9Dp372N3T05q_nn3PgvoXBoReXvaXBfkthtXQLN7m5_YI6FoTre-xvJBDFLMA", "replyUserName": "binnie" } ] }, { "comment": "在连续的 Skyline 页面间跳转时,可实现自定义路由效果,路由动画的曲线、时长均可交由开发者控制。", "userName": "binnie", "userHeadImg": "http://wx.qlogo.cn/mmhead/uI5pczeERTajXl904XSbHwAtGENC5ccKvo2F54sgYeqibHxOXNAFKdg/132", "subCommentList": [] }, { "comment": "在连续的两个 Skyline 页面跳转时,可以将上一个页面的元素“共享”到下一个页面,并伴随着过渡动画。", "userName": "绿意盎然", "userHeadImg": "https://res.wx.qq.com/op_res/0-l2fyKjv3_BR62E3KwTJH2f0R4uXyqnNGlrivO8cKbn0nz1DE_6s22rc91zluwIrqiAVZNREvCeVYAUS8aaZw", "subCommentList": [] }, ] } ================================================ FILE: examples/half-screen/pages/index/index.js ================================================ import { getCommentList } from "./comment-data" const { shared, timing } = wx.worklet const GestureState = { POSSIBLE: 0, // 0 此时手势未识别,如 panDown等 BEGIN: 1, // 1 手势已识别 ACTIVE: 2, // 2 连续手势活跃状态 END: 3, // 3 手势终止 CANCELLED: 4, // 4 手势取消, } Component({ data: { list: getCommentList(), }, lifetimes: { created() { this.transY = shared(1000) this.scrollTop = shared(0) this.startPan = shared(true) this.commentHeight = shared(1000) }, ready() { const query = this.createSelectorQuery() // ready 生命周期里才能获取到首屏的布局信息 query.select('.comment-container').boundingClientRect() query.exec((res) => { this.transY.value = this.commentHeight.value = res[0].height }) // 通过 transY 一个 SharedValue 控制半屏的位置 this.applyAnimatedStyle('.comment-container', () => { 'worklet' return { transform: `translateY(${this.transY.value}px)` } }) }, }, methods: { onTapOpenComment() { this.openComment(300) }, openComment(duration) { 'worklet' this.transY.value = timing(0, { duration }) }, onTapCloseComment() { this.closeComment() }, closeComment() { 'worklet' this.transY.value = timing(this.commentHeight.value, { duration: 200 }) }, // shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商 shouldPanResponse() { 'worklet' return this.startPan.value }, shouldScrollViewResponse(pointerEvent) { 'worklet' // transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效 if (this.transY.value > 0) return false const scrollTop = this.scrollTop.value const { deltaY } = pointerEvent // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效 const result = scrollTop <= 0 && deltaY > 0 this.startPan.value = result return !result }, handlePan(gestureEvent) { 'worklet' if (gestureEvent.state === GestureState.ACTIVE) { const curPosition = this.transY.value const destination = Math.max(0, curPosition + gestureEvent.deltaY) if (curPosition === destination) return this.transY.value = destination } if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) { if (gestureEvent.velocityY > 500 && this.transY.value > 50) { this.closeComment() } else if (this.transY.value > this.commentHeight.value / 2) { this.closeComment() } else { this.openComment(100) } } }, adjustDecelerationVelocity(velocity) { 'worklet' const scrollTop = this.scrollTop.value return scrollTop <= 0 ? 0 : velocity }, handleScroll(evt) { 'worklet' this.scrollTop.value = evt.detail.scrollTop }, }, }) ================================================ FILE: examples/half-screen/pages/index/index.json ================================================ { "usingComponents": {}, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/half-screen/pages/index/index.wxml ================================================ 打开留言 留言 {{item.userName}} {{item.comment}} {{subItem.userName}} 回复 {{subItem.replyUserName}} {{subItem.comment}} ================================================ FILE: examples/half-screen/pages/index/index.wxss ================================================ page { display: flex; flex-direction: column; padding-top: env(safe-area-inset-top); width: 100vw; height: 100vh; color: #1A191E; } page, view { box-sizing: border-box; } pan-gesture-handler, vertical-drag-gesture-handler { display: flex; flex-direction: column; overflow: hidden; } .container { flex: 1; width: 100vw; min-height: auto; overflow: hidden; } .container image { width: 100vw; } .open-comment { display: flex; flex-direction: column; flex-shrink: 0; width: 100%; background-color: white; } .open-comment-wording { height: 66px; display: flex; justify-content: center; align-items: center; } .safe-area-inset-bottom { height: env(safe-area-inset-bottom); } .comment-container { width: 100vw; height: 70vh; display: flex; flex-direction: column; position: absolute; bottom: 0; z-index: 999; background-color: white; border-top-left-radius: 20px; border-top-right-radius: 20px; box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2); transform: translateY(100%); } .comment-header { width: 100%; font-size: 16px; text-align: center; padding: 15px 0; } .close-comment { position: absolute; left: 20px; font-size: 10px; font-weight: bold; background-color: #F8F8F8; border-radius: 100%; width: 20px; height: 20px; line-height: 20px; z-index: 1; } .comment-list { flex: 1; overflow: hidden; } .comment-item { padding: 0 20px 20px; font-size: 13px; line-height: 1.4; } .main-comment, .sub-comment { display: flex; flex-direction: row; } .sub-comment { padding: 10px 22px 0; } .user-head-img { width: 33px; height: 33px; border-radius: 50%; margin-top: 5px; } .others { flex: 1; margin-left: 10px; } .user-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .content { margin-top: 2px; } ================================================ FILE: examples/half-screen/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "latest", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": true, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 } } ================================================ FILE: examples/half-screen/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "half-screen", "setting": { "compileHotReLoad": false, "skylineRenderEnable": true }, "libVersion": "latest" } ================================================ FILE: examples/half-screen/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/product-list/.eslintrc.js ================================================ /* * Eslint config file * Documentation: https://eslint.org/docs/user-guide/configuring/ * Install the Eslint extension before using this feature. */ module.exports = { env: { es6: true, browser: true, node: true, }, ecmaFeatures: { modules: true, }, parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, globals: { wx: true, App: true, Page: true, getCurrentPages: true, getApp: true, Component: true, requirePlugin: true, requireMiniProgram: true, }, // extends: 'eslint:recommended', rules: {}, } ================================================ FILE: examples/product-list/app.js ================================================ // app.js App({}) ================================================ FILE: examples/product-list/app.json ================================================ { "pages": [ "pages/index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "lazyCodeLoading": "requiredComponents", "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/product-list/app.wxss ================================================ /**app.wxss**/ view { box-sizing: border-box; } ================================================ FILE: examples/product-list/components/navigation-bar/index.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const rect = wx.getMenuButtonBoundingClientRect() wx.getSystemInfo({ success: (res) => { this.setData({ statusBarHeight: res.statusBarHeight, innerPaddingRight: `padding-right:${res.windowWidth - rect.left}px`, leftWidth: `width:${res.windowWidth - rect.left}px`, navBarHeight: rect.bottom + rect.top - res.statusBarHeight, }) } }) }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${show ? '1' : '0'};transition: opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data if (data.delta) { wx.navigateBack({ delta: data.delta }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/product-list/components/navigation-bar/index.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel", "addGlobalClass": true } ================================================ FILE: examples/product-list/components/navigation-bar/index.wxml ================================================ {{title}} ================================================ FILE: examples/product-list/components/navigation-bar/index.wxss ================================================ .weui-navigation-bar { overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; box-sizing: border-box; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; /* width: 95px; */ padding-left: 16px; box-sizing: border-box; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .weui-navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: bold; } @media(prefers-color-scheme: dark) { .weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } .weui-navigation-bar__inner { background-color: #1f1f1f; } } ================================================ FILE: examples/product-list/pages/index/index.js ================================================ import { getCategory, getGoods, getVIPCategory } from '../../util' const systemInfo = wx.getSystemInfoSync() const { shared, Easing } = wx.worklet const lerp = function (begin, end, t) { 'worklet' return begin + (end - begin) * t } const clamp = function (cur, lowerBound, upperBound) { 'worklet' if (cur > upperBound) return upperBound if (cur < lowerBound) return lowerBound return cur } Component({ data: { goods: getGoods(30), categorySet: [{ page: 0, categorys: getCategory() }, { page: 1, categorys: getCategory().reverse() }], paddingTop: 44, renderer: 'skyline', vipCategorys: getVIPCategory(), categoryItemWidth: 0, intoView: '', selected: 0 }, lifetimes: { created() { this.searchBarWidth = shared(100) this.navBarOpactiy = shared(1) }, attached() { const padding = 10 * 2 const categoryItemWidth = (systemInfo.windowWidth - padding) / 5 this.setData({ categoryItemWidth, paddingTop: systemInfo.statusBarHeight, renderer: this.renderer }) this.applyAnimatedStyle('.nav-bar', () => { 'worklet' return { opacity: this.navBarOpactiy.value } }) this.applyAnimatedStyle('.search', () => { 'worklet' return { width: `${this.searchBarWidth.value}%`, } }) this.applyAnimatedStyle('.search-container', () => { 'worklet' return { backgroundColor: (this.navBarOpactiy.value > 0 && this.renderer == 'skyline') ? 'transparent' : '#fff' } }) }, }, methods: { chooseVipCategory(evt) { const id = evt.currentTarget.dataset.id this.setData({ intoView: `vip-category-${id}`, selected: parseInt(id, 10) }) }, handleScrollUpdate(evt) { 'worklet' const maxDistance = 60 const scrollTop = clamp(evt.detail.scrollTop, 0, maxDistance) const progress = scrollTop / maxDistance const EasingFn = Easing.cubicBezier(0.4, 0.0, 0.2, 1.0) this.searchBarWidth.value = lerp(100, 70, EasingFn(progress)) this.navBarOpactiy.value = lerp(1, 0, progress) }, }, }) ================================================ FILE: examples/product-list/pages/index/index.json ================================================ { "usingComponents": { "navigation-bar": "../../components/navigation-bar" }, "disableScroll": true, "navigationStyle": "custom" } ================================================ FILE: examples/product-list/pages/index/index.wxml ================================================ 微信学堂 这是skyline实现的~ 搜索 {{category.name}} {{item.name}} {{item.title}} 3万+评价 ================================================ FILE: examples/product-list/pages/index/index.wxss ================================================ .fake-nav-bar { height: 60px; } .search-container { padding: 0 16px 10px 16px; /* margin-top: 44px; */ /* background-color: transparent; */ background-color: #FFF; } .search { display: flex; flex-direction: row; box-sizing: border-box; width: 100%; height: 40px; border-radius: 20px; border: 2px solid #07c160; position: relative; align-items: center; background-color: #fff; } .search-text { color: #8f8888; font-size: 14px; } .search-icon-wrp { display: flex; width: 30px; height: 100%; flex-direction: row; align-items: center; justify-content: center; } .search-icon { width: 16px; height: 16px; } .search-btn { position: absolute; right: 0; width: 60px; height: 100%; border-radius: 20px; background-color: #07c160; display: flex; align-items: center; justify-content: center; color: #FFF; font-size: 16px; /* font-weight: bold; */ } .nav-bar { background-color: #fff; position: absolute; } .nav-left { display: flex; flex-direction: row; align-items: center; } .nav-logo { width: 40px; height: 40px; border-radius: 50%; } .nav-title { margin-left: 2px; font-size: 20px; color: #3f3e3e; /* font-weight: bold; */ } .scroll-area { height: 100vh; } .category-wrp { height: 220px; padding: 10px 0; background-color: #FFF; } .category-list { display: flex; flex-direction: row; justify-content: space-between; flex-wrap: wrap; padding: 0 10px; } .category-item { display: flex; flex-direction: column; align-items: center; margin: 10px 0; font-size: 14px; } .category-icon { width: 50px; height: 50px; margin-bottom: 10px; border-radius: 50%; } .category-name { height: 30px; display: flex; justify-content: center; align-items: center; } .good { background-color: #FFF; border-radius: 6px; overflow: hidden; } .good-icon { width: 100%; } .good-title { width: 100%; line-height: 1.4; margin: 8px 0; padding: 0 5px; font-size: 14px; box-sizing: border-box; } .good-comment { font-size: 12px; color: #ccc; padding-left: 5px; margin-bottom: 10px; } .vip-categorys-list { background-color: #fff; width: 100%; height: 60px; display: flex; flex-direction: row; } .vip-category-item { display: flex; flex-shrink: 0; height: 100%; justify-content: center; align-items: center; padding-right: 20px; padding-left: 40px; } .vip-category-item:first-child { padding-left: 20px; } .vip-category-name { color: #8f8888; font-size: 16px; transition: transform .3s; } .selected { transform: scale(1.2); color: #2c2c2c; font-weight: bold; } ================================================ FILE: examples/product-list/project.config.json ================================================ { "appid": "wxe5f52902cf4de896", "compileType": "miniprogram", "libVersion": "latest", "packOptions": { "ignore": [], "include": [] }, "setting": { "coverView": true, "es6": true, "postcss": true, "minified": true, "enhance": true, "showShadowRootInWxmlPanel": true, "packNpmRelationList": [], "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "condition": false, "skylineRenderEnable": true, "compileWorklet": true }, "condition": {}, "editorSetting": { "tabIndent": "insertSpaces", "tabSize": 2 } } ================================================ FILE: examples/product-list/project.private.config.json ================================================ { "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "product-list", "setting": { "compileHotReLoad": false }, "libVersion": "latest" } ================================================ FILE: examples/product-list/sitemap.json ================================================ { "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", "rules": [{ "action": "allow", "page": "*" }] } ================================================ FILE: examples/product-list/util.js ================================================ export function getCategory() { let categorys = [ '中小商户', '商超零售', '品牌服饰', '餐饮', '医疗', '酒旅', '政务', '开发技术', '产品能力', '运营规范', ] let images = [ 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJgc9If4xjgvN3O4UQclWMiJxMoExkarf71FN-3SSf3Sh-GoatfvTbKcPE-grH-1L9g', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJrmxgZKLSYHHwqx6YfJyqPnSNeIHovelr_r6GLFpsiCuCuBgYKBc68vBi0dJYSMeZA', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJk4e_uP2-FWEQKo5Ijp5itIrlf-qIXozTGY6D595Ri2YIoLCUS7YseOda2JLTAEz7Q', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJj6M4Aqf4ov3F7tRlrb62now5owS_Q6vkhsWjnU_uWVbBR84dTHxG4tzAcjwAqOGZQ', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJrBCMpypwJ_xFgSnas6etb7Y6JuMRRMBJ6cSMmbmSkCkOjCSPDdC_eLEK1_FT-d-PQ', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJitcJIfp9P4VSWxi3126XeiyZ2BnnH0xg-oIXAUgHBgaHjBMwxzSjSkEkTMRqzlKZw', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJnw5zq4f_XW3swKAowexqAbziuojU5W9v4CJixA-NDJShkfS0ne3KWY_6SB56yqb3g', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJu8Q1L10fFiMMnZVYnLoP1GuT0q26CJLtjSRfJAjTvj6DBNuWrzzMD9UYZEb-pznKA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChUzvBckq_FnOrUIIgSP2hJ9cYByUIwgzBEaDCcF5YiPNMmcMg0ewQBn_nMCt-q71vsg', 'https://res.wx.qq.com/op_res/Zmvv0fisUjaMjuqWLhWWkuzGktaXJEQt46EaKsCKeT06Z4tROseXN0joI7h2qwzqyx2FUy57cveZL-8iArI8_Q', ] categorys = categorys.map((name, id) => ({ id, name, icon: images[(id % categorys.length)] //`/images/boy/b${id}.png` })) return categorys } export function getGoods(num) { const titles = [ '小程序性能优化课程基于实际开发场景,提升小程序性能表现,满足用户体验', '解析常见小程序违规类型,帮助大家更好理解平台规则', '快速了解微信小程序在医疗行业的应用', '小程序直播的企业实践案例。', '微信客服轻松配置,入门必修', '想做互联网的生意,可以通过微信怎么经营呢?', '了解小程序开发动态,听官方为你解读新能力', '医保支付、互联网医院、线上问诊...小程序如何帮助传统医院数字化?', '内含开店指引、店铺运营和平台规则,帮你快速掌握小商店经营秘诀', '浅谈连锁零售的私域流量运营' ] const images = [ 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU_LhYxhaP5JTy7TWgezsDY7RW_l_e04fR7oG7sCKmS8hc8mVeZaY6eUWT3nk-ww_ZQ', 'https://res.wx.qq.com/op_res/Zmvv0fisUjaMjuqWLhWWkuzGktaXJEQt46EaKsCKeT06Z4tROseXN0joI7h2qwzqyx2FUy57cveZL-8iArI8_Q', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJrBCMpypwJ_xFgSnas6etb7Y6JuMRRMBJ6cSMmbmSkCkOjCSPDdC_eLEK1_FT-d-PQ', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU-O3axOjUJGFgutF9Xc1JL1uxXFWYdW85mWG0Zvm5nv7rvP18CJ0q6-RRFM0xWLLog', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU3ywQmrV-rSREDwo0Hp9m7iIZZ7Njvjq_TlOg_0ss0cgQL0pfKOuB2NRpAcwfALxvw', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJgc9If4xjgvN3O4UQclWMiJxMoExkarf71FN-3SSf3Sh-GoatfvTbKcPE-grH-1L9g', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJu8Q1L10fFiMMnZVYnLoP1GuT0q26CJLtjSRfJAjTvj6DBNuWrzzMD9UYZEb-pznKA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU1GROxmiPIBOCoA5Es44GxjN0KuCQQsoxEH33l05TCgk04n0dssHAIPxIV2ycSlSJA', 'https://res.wx.qq.com/op_res/RBwYn_b7VGuWuLJ2qBChU68wmBQzYcfQfuIAh1IKWq7OyG0EWxdWGhotYHFh-k-JpmkJ1Otq-mYUT8Dp3iucvg', 'https://res.wx.qq.com/op_res/mGK9l-4vYzVgHuIz_uFeJrmxgZKLSYHHwqx6YfJyqPnSNeIHovelr_r6GLFpsiCuCuBgYKBc68vBi0dJYSMeZA' ] const goods = [] for (let id = 0; id < num; id++) { goods.push({ id, title: titles[(id % titles.length)], icon: images[(id % titles.length)] // `/images/goods/g${(id % num)}.jpg` }) } return goods } export function getVIPCategory() { let vipCategorys = [ '本月最热', '官方经营', '行业实践', '微信服务商' ] vipCategorys = vipCategorys.map((name, id) => ({ id, name })) return vipCategorys } ================================================ FILE: examples/refresher-two-level/app.js ================================================ // app.js App({}) ================================================ FILE: examples/refresher-two-level/app.json ================================================ { "pages": [ "index/index" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "style": "v2", "renderer": "skyline", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, "disableABTest": true, "sdkVersionBegin": "3.0.0", "sdkVersionEnd": "15.255.255" } }, "lazyCodeLoading": "requiredComponents", "componentFramework": "glass-easel", "sitemapLocation": "sitemap.json" } ================================================ FILE: examples/refresher-two-level/app.wxss ================================================ ================================================ FILE: examples/refresher-two-level/components/navigation-bar/index.js ================================================ Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, /** * 组件的属性列表 */ properties: { extClass: { type: String, value: '' }, title: { type: String, value: '' }, background: { type: String, value: '' }, color: { type: String, value: '' }, back: { type: Boolean, value: true }, loading: { type: Boolean, value: false }, animated: { // 显示隐藏的时候opacity动画效果 type: Boolean, value: true }, show: { // 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在 type: Boolean, value: true, observer: '_showChange' }, // back为true的时候,返回的页面深度 delta: { type: Number, value: 1 } }, /** * 组件的初始数据 */ data: { displayStyle: '' }, attached() { const rect = wx.getMenuButtonBoundingClientRect() wx.getSystemInfo({ success: (res) => { this.setData({ statusBarHeight: res.statusBarHeight, innerPaddingRight: `padding-right:${res.windowWidth - rect.left}px`, leftWidth: `width:${res.windowWidth - rect.left}px`, navBarHeight: rect.bottom + rect.top - res.statusBarHeight, }) } }) }, /** * 组件的方法列表 */ methods: { _showChange(show) { const animated = this.data.animated let displayStyle = '' if (animated) { displayStyle = `opacity: ${show ? '1' : '0'};transition: opacity 0.5s;` } else { displayStyle = `display: ${show ? '' : 'none'}` } this.setData({ displayStyle }) }, back() { const data = this.data if (data.delta) { wx.navigateBack({ delta: data.delta }) } this.triggerEvent('back', { delta: data.delta }, {}) } } }) ================================================ FILE: examples/refresher-two-level/components/navigation-bar/index.json ================================================ { "component": true, "usingComponents": {}, "componentFramework": "glass-easel", "addGlobalClass": true } ================================================ FILE: examples/refresher-two-level/components/navigation-bar/index.wxml ================================================ {{title}} ================================================ FILE: examples/refresher-two-level/components/navigation-bar/index.wxss ================================================ .weui-navigation-bar { overflow: hidden; color: rgba(0, 0, 0, .9); width: 100vw; } .weui-navigation-bar__placeholder { background: #f7f7f7; position: relative; } .weui-navigation-bar__inner, .weui-navigation-bar__inner .weui-navigation-bar__left { display: flex; align-items: center; flex-direction: row; } .weui-navigation-bar__inner { position: relative; padding-right: 95px; width: 100vw; box-sizing: border-box; } .weui-navigation-bar__inner .weui-navigation-bar__left { position: relative; /* width: 95px; */ padding-left: 16px; box-sizing: border-box; } .weui-navigation-bar__btn_goback_wrapper { padding: 11px 18px 11px 16px; margin: -11px -18px -11px -16px; } .weui-navigation-bar__inner .weui-navigation-bar__left .weui-navigation-bar__btn_goback { font-size: 12px; width: 12px; height: 24px; background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%; background-size: cover; } .weui-navigation-bar__inner .weui-navigation-bar__center { font-size: 17px; text-align: center; position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: bold; } @media(prefers-color-scheme: dark) { .weui-navigation-bar { color: hsla(0, 0%, 100%, .8); } .weui-navigation-bar__inner { background-color: #1f1f1f; } } ================================================ FILE: examples/refresher-two-level/goods/index.json ================================================ { "usingComponents": { "navigation-bar": "../components/navigation-bar" }, "renderer": "skyline", "componentFramework": "glass-easel" } ================================================ FILE: examples/refresher-two-level/goods/index.less ================================================ @import '../goods.less'; ================================================ FILE: examples/refresher-two-level/goods/index.ts ================================================ import { getCategory, getGoods, getVIPCategory, getExpCategory, getVideoList } from '../util' // pages/goods/index.ts Page({ /** * 页面的初始数据 */ data: { goodsData: { expSelected: 0, expCategorys: getExpCategory(), videoList: getVideoList(20), hasRouteDone: false, } }, back() { wx.navigateBack({}) }, /** * 生命周期函数--监听页面加载 */ onRouteDone() { console.info('@@@ goods page routeDone ') this.setData({ 'goodsData.hasRouteDone': true, }) if (this.eventChannel) { this.eventChannel.emit('nextPageRouteDone', { }); } }, onLoad() { this.eventChannel = this.getOpenerEventChannel() }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady() { }, /** * 生命周期函数--监听页面显示 */ onShow() { }, /** * 生命周期函数--监听页面隐藏 */ onHide() { }, /** * 生命周期函数--监听页面卸载 */ onUnload() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom() { }, /** * 用户点击右上角分享 */ onShareAppMessage() { } }) ================================================ FILE: examples/refresher-two-level/goods/index.wxml ================================================