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.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}}
================================================
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 动画:能够在渲染线程同步运行动画相关逻辑。
🔸 手势系统:在渲染线程同步监听手势、执行手势相关逻辑;支持手势协商处理;
🔸 自定义路由:实现自定义路由动画和交互。
🔸 共享元素动画:将上一个页面的元素“共享”到下一个页面,并伴随着过渡动画。
================================================
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
================================================
ㄑ
请输入课程名称
课程
评价
{{item}}
{{item.header}}
{{subItem.name}}
{{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
================================================
================================================
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}}
================================================
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
================================================
================================================
FILE: examples/refresher-two-level/goods.less
================================================
.exp-room {
.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;
}
width: 100vw;
height: 100vh;
background-color: rgb(3, 3, 41);
.nav-bar {
position: relative;
background-color: rgb(3, 3, 41);
}
.nav-logo {
width: 32px;
height: 32px;
}
.nav-title {
color: #fff;
font-size: 18px;
}
.exp-category-list {
display: flex;
flex-direction: row;
margin: 10px 16px;
}
.exp-category-item {
margin-right: 20px;
}
.exp-category-name {
color: #8f8888;
font-size: 16px;
}
.selected {
transform: scale(1.2);
color: #fff;
font-weight: bold;
}
.scroll-area {
flex: 1;
}
.video-container {
height: 240px;
}
.video {
overflow: hidden;
border-radius: 16px;
margin: 10px 0;
position: relative;
width: 100%;
height: 100%;
}
.video-title {
position: absolute;
top: 16px;
left: 16px;
color: #fff;
font-size: 14px;
}
.expand {
width: 100%;
height: 100%;
}
.refresher-tips {
position: absolute;
bottom: 20px;
width: 100%;
text-align: center;
color: #fff;
}
}
================================================
FILE: examples/refresher-two-level/goods.wxml
================================================
官方讲师课程
{{item.name}}
================================================
FILE: examples/refresher-two-level/index/index.json
================================================
{
"usingComponents": {
"navigation-bar": "../components/navigation-bar"
},
"disableScroll": true,
"renderer": "skyline",
"navigationStyle": "custom",
"componentFramework": "glass-easel"
}
================================================
FILE: examples/refresher-two-level/index/index.less
================================================
@import '../goods.less';
.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;
}
.good-icon {
width: 100%;
border-radius: 6px;
}
.good-title {
width: 100%;
line-height: 1.4;
margin: 8px 0;
padding: 0 5px;
font-size: 14px;
}
.good-comment {
font-size: 12px;
color: #ccc;
padding-left: 5px;
margin-bottom: 10px;
}
.vip-categorys-list {
background-color: #fff;
width: 100%;
height: 60px;
}
.vip-category-item {
display: flex;
height: 100%;
justify-content: center;
padding-right: 20px;
}
.vip-category-name {
color: #8f8888;
font-size: 16px;
transition: transform .3s;
}
.selected {
transform: scale(1.2);
color: #2c2c2c;
font-weight: bold;
}
================================================
FILE: examples/refresher-two-level/index/index.ts
================================================
import { getCategory, getGoods, getVIPCategory, getExpCategory, getVideoList } from '../util'
export enum RefreshStatus {
Idle,
CanRefresh,
Refreshing,
Completed,
Failed,
CanTwoLevel,
TwoLevelOpening,
TwoLeveling,
TwoLevelClosing,
}
const systemInfo = wx.getSystemInfoSync()
const { shared, Easing } = wx.worklet
const EasingFn = Easing.cubicBezier(0.4, 0.0, 0.2, 1.0)
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
}
const secondFloorCover = 'https://res.wx.qq.com/op_res/cgyy76UKHo3KhkAvZN0WW8YtThK1-Kf4a21kiEQxNAvea-eN8S8tcbsLcdAIISVQeFM_E7sR-wtDb2VP5_Cy1w'
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,
padding: [0, 16, 0, 16],
triggered: false,
twoLevelTriggered: false,
isTwoLevel: false,
refreshStatus: '下拉刷新',
secondFloorCover,
goodsData: {
expSelected: 0,
expCategorys: getExpCategory(),
videoList: getVideoList(20),
hasRouteDone: true,
}
},
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'
console.log('111', this.renderer)
return {
backgroundColor: (this.navBarOpactiy.value > 0 && this.renderer == 'skyline') ? 'transparent' : '#fff'
}
})
wx.createSelectorQuery()
.select('#scrollview')
.node()
.exec((res) => {
console.info('@@@ createSelectorQuery ', res)
this.scrollContext = res[0].node;
})
},
},
methods: {
chooseVipCategory(evt) {
const id = evt.currentTarget.dataset.id
this.setData({
intoView: `vip-category-${id}`,
selected: parseInt(id, 10)
})
},
handleScrollStart(evt) {
'worklet'
},
handleScrollUpdate(evt) {
'worklet'
const maxDistance = 60
const scrollTop = clamp(evt.detail.scrollTop, 0, maxDistance)
const progress = EasingFn(scrollTop / maxDistance)
this.searchBarWidth.value = lerp(100, 70, progress)
this.navBarOpactiy.value = lerp(1, 0, progress)
},
handleScrollEnd(evt) {
'worklet'
},
onPulling(e) {
// console.log('onPulling:', e)
},
onRefresh() {
console.info('@@@ onRefresh')
if (this._freshing) return
this._freshing = true
setTimeout(() => {
this.setData({
triggered: false,
})
this._freshing = false
}, 2000)
},
onRestore(e) {
console.log('onRestore:', e)
},
onAbort(e) {
console.log('onAbort', e)
},
closeTwoLevel() {
this.setData({
twoLevelTriggered: false,
})
},
onStatusChange(e) {
const status: RefreshStatus = e.detail.status
const twoLevelModes = [RefreshStatus.TwoLevelOpening, RefreshStatus.TwoLeveling, RefreshStatus.TwoLevelClosing]
const isTwoLevel = twoLevelModes.indexOf(status) >= 0
const refreshStatus = this.buildText(status)
this.setData({
isTwoLevel,
refreshStatus,
})
if (status === RefreshStatus.TwoLeveling) {
const that = this
wx.navigateTo({
url: '../goods/index',
events: {
nextPageRouteDone: function(data) {
console.info('@@@ next page has routeDone')
that.scrollContext.closeTwoLevel({
duration: 1
})
}
},
success(res) {}
})
}
},
buildText(status: RefreshStatus) {
switch (status) {
case RefreshStatus.Idle:
return '下拉刷新'
case RefreshStatus.CanRefresh:
return '松手刷新,下拉进入二楼'
case RefreshStatus.Refreshing:
return '正在刷新'
case RefreshStatus.Completed:
return '刷新成功'
case RefreshStatus.Failed:
return '刷新失败'
case RefreshStatus.CanTwoLevel:
return '松手进入二楼'
default:
return '进入二楼'
}
},
},
})
================================================
FILE: examples/refresher-two-level/index/index.wxml
================================================
微信学堂
{{refreshStatus}}
这是skyline实现的~{{twoLevelStyle}}
搜索
{{category.name}}
{{item.name}}
{{item.title}}
================================================
FILE: examples/refresher-two-level/project.config.json
================================================
{
"appid": "wxe5f52902cf4de896",
"compileType": "miniprogram",
"libVersion": "3.1.5",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": [
"typescript",
"less"
]
},
"condition": {},
"editorSetting": {
"tabIndent": "auto",
"tabSize": 4
},
"projectname": "refresher-two-level"
}
================================================
FILE: examples/refresher-two-level/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
}
export function getExpCategory() {
let expCategorys = [
'推荐',
'产品课程',
'技术课程',
'运营课程'
]
expCategorys = expCategorys.map((name, id) => ({
id,
name
}))
return expCategorys
}
export function getVideoList(num) {
const titles = [
'酷跑春天·跑鞋新品发布会',
'直接全球气候变化',
'幻想奇遇·3D音乐节'
]
const urls = [
'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh3b3EQ9Ngrejypictlz1ZNMJVnBg3fkibIA3F1qajkQlVG2eKtbxltWEGrvYjfrLa5ib54wvnRMDLNDw/0?wx_fmt=jpeg',
'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh2zgPxemO7asicBcbcTHb2icticjEqxJKLtxhVOFVHjmCvto8YLjLpGP9p73ia78sicjvBDObNicocSIZ3A/0?wx_fmt=jpeg',
'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh37FxQAIO6GuOWdvfadyoic6flNp9cm9Czs9djzdyD6pOZcJ5Mfa2XUjZXlUoALaOtEAauGwmbibLnQ/0?wx_fmt=jpeg',
'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh0dEhZyMCWicMCVDXxNb44ZtTA6RT8c2FVt7FGPPO4r1UUCicQERZDXPiaVS5yPjN4HaSvvIwbxOp2rQ/0?wx_fmt=jpeg',
'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh3GeJcbCXCxqXNbRVSvb1757BmGxric35XZecmo9ctlU2dsMIL5PqCjUMUN0OTGcoMPUndO1euRcow/0?wx_fmt=jpeg'
]
const videos = []
for (let id = 0; id < num; id++) {
videos.push({
id,
title: titles[(id % titles.length)],
url: urls[(id % urls.length)]
})
}
return videos
}
================================================
FILE: examples/segmented-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/segmented-half-screen/app.js
================================================
// app.js
App({})
================================================
FILE: examples/segmented-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/segmented-half-screen/app.wxss
================================================
================================================
FILE: examples/segmented-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/segmented-half-screen/pages/index/index.js
================================================
import { getCommentList } from "./comment-data"
function clamp(val, min, max) {
'worklet'
return Math.min(Math.max(val, min), max)
}
const { shared, timing } = wx.worklet
const GestureState = {
POSSIBLE: 0, // 0 此时手势未识别,如 panDown等
BEGIN: 1, // 1 手势已识别
ACTIVE: 2, // 2 连续手势活跃状态
END: 3, // 3 手势终止
CANCELLED: 4, // 4 手势取消,
}
const { screenHeight, statusBarHeight, safeArea} = wx.getSystemInfoSync()
Component({
data: {
scale: 16,
list: getCommentList(),
},
lifetimes: {
created() {
this.transY = shared(1000)
this.scrollTop = shared(0)
this.startPan = shared(true)
this.initTransY = shared(0) // 留言半屏的初始位置
this.upward = shared(false)
},
attached() {
this.setData({
height: screenHeight - statusBarHeight,
})
},
ready() {
const query = this.createSelectorQuery()
// ready 生命周期里才能获取到首屏的布局信息
query.select('.comment-header').boundingClientRect()
query.exec((res) => {
this.transY.value = this.initTransY.value = screenHeight - res[0].height - (screenHeight - safeArea.bottom)
})
// 通过 transY 一个 SharedValue 控制半屏的位置
this.applyAnimatedStyle('.comment-container', () => {
'worklet'
return { transform: `translateY(${this.transY.value}px)` }
})
},
},
methods: {
setMapScale(scale) {
this.setData({ scale })
},
scrollTo(toValue) {
'worklet'
let scale = 18
if (toValue > screenHeight / 2) {
scale = 16
}
wx.worklet.runOnJS(this.setMapScale.bind(this))(scale)
this.transY.value = timing(toValue, { duration: 200 })
},
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
'worklet'
return this.startPan.value
},
shouldScrollViewResponse(pointerEvent) {
'worklet'
// transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效
if (this.transY.value > statusBarHeight) 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) {
// deltaY < 0,往上滑动
this.upward.value = gestureEvent.deltaY < 0
// 当前半屏位置
const curPosition = this.transY.value
// 只能在 [statusBarHeight, screenHeight] 之间移动
const destination = clamp(curPosition + gestureEvent.deltaY, statusBarHeight, screenHeight)
if (curPosition === destination) return
// 改变 transY,来改变半屏的位置
this.transY.value = destination
}
if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) {
if (this.transY.value <= screenHeight / 2) {
// 在上面的位置
if (this.upward.value) {
this.scrollTo(statusBarHeight)
} else {
this.scrollTo(screenHeight / 2)
}
} else if (this.transY.value > screenHeight / 2 && this.transY.value <= this.initTransY.value) {
// 在中间位置的时候
if (this.upward.value) {
this.scrollTo(screenHeight / 2)
} else {
this.scrollTo(this.initTransY.value)
}
} else {
// 在最下面的位置
this.scrollTo(this.initTransY.value)
}
}
},
adjustDecelerationVelocity(velocity) {
'worklet'
const scrollTop = this.scrollTop.value
return scrollTop <= 0 ? 0 : velocity
},
handleScroll(evt) {
'worklet'
this.scrollTop.value = evt.detail.scrollTop
},
// 简单兼容 WebView
handleTouchEnd() {
if (this.renderer === 'skyline') {
return
}
if (this.transY.value === statusBarHeight) {
this.lastTransY = statusBarHeight
this.scrollTo(screenHeight / 2)
} else if (this.transY.value === screenHeight / 2 && this.lastTransY === statusBarHeight) {
this.lastTransY = screenHeight / 2
this.scrollTo(this.initTransY.value)
} else if (this.transY.value === this.initTransY.value) {
this.lastTransY = this.initTransY.value
this.scrollTo(screenHeight / 2)
} else if (this.transY.value === screenHeight / 2 && this.lastTransY === this.initTransY.value) {
this.scrollTo(statusBarHeight)
}
},
},
})
================================================
FILE: examples/segmented-half-screen/pages/index/index.json
================================================
{
"usingComponents": {},
"disableScroll": true,
"navigationStyle": "custom"
}
================================================
FILE: examples/segmented-half-screen/pages/index/index.wxml
================================================
================================================
FILE: examples/segmented-half-screen/pages/index/index.wxss
================================================
page {
display: flex;
flex-direction: column;
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;
overflow: hidden;
}
.container image {
width: 100vw;
}
#myMap {
width: 100vw;
height: 100vh;
}
.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: 100vh;
display: flex;
flex-direction: column;
position: absolute;
top: 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);
}
.comment-header {
width: 100%;
font-size: 16px;
text-align: center;
padding: 15px 0;
}
.comment-handler {
width: 50px;
height: 3px;
border-radius: 3px;
background-color: #EEE;
margin: 0 auto 10px;
}
.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/segmented-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": false,
"compileWorklet": true
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}
================================================
FILE: examples/segmented-half-screen/project.private.config.json
================================================
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "segmented",
"setting": {
"compileHotReLoad": false,
"skylineRenderEnable": true
},
"libVersion": "latest"
}
================================================
FILE: examples/segmented-half-screen/sitemap.json
================================================
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}
================================================
FILE: examples/tab-indicator/.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/tab-indicator/app.js
================================================
// app.js
App({})
================================================
FILE: examples/tab-indicator/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/tab-indicator/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/tab-indicator/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/tab-indicator/components/navigation-bar/index.json
================================================
{
"component": true,
"usingComponents": {},
"componentFramework": "glass-easel",
"addGlobalClass": true
}
================================================
FILE: examples/tab-indicator/components/navigation-bar/index.wxml
================================================
{{title}}
================================================
FILE: examples/tab-indicator/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/tab-indicator/pages/index/index.js
================================================
const ScrollState = {
scrollStart: 0,
scrollUpdate: 1,
scrollEnd: 2,
}
const tabs = [
{
title: '性能优化',
title2: '小程序性能优化实践',
img: 'http://mmbiz.qpic.cn/mmbiz_jpg/PxLPibq1ibyh0U4e0qLqNrULAUzW5UbWbicUN5GyJqd24GR0Ricg5q14VGGBWlicNca8x4xelvDrM1r0ibwAjAsR0bOA/0?wx_fmt=jpeg',
desc: '小程序性能优化课程基于实际开发场景,由资深开发者分享小程序性能优化的各项能力及应用实践,提升小程序性能表现,满足用户体验。',
},
{
title: '新能力解读',
title2: '小程序开发新能力解读',
img: 'http://mmbiz.qpic.cn/sz_mmbiz_png/GEWVeJPFkSH05EZMIBafqzpoZVSXtCE47V7icf0gru4KPUzMjIcIibJPUlXqbZib4VNmTecxef987XEWib2vhwuqbQ/0?wx_fmt=png',
desc: '这个月小程序释放了什么新能力?又有哪些新规则?收藏课程,及时了解小程序开发动态,听官方为你解读新能力。',
},
{
title: '基础开发',
title2: '小程序基础开发之架构、框架、组件',
img: 'http://mmbiz.qpic.cn/sz_mmbiz_jpg/GEWVeJPFkSEz7tgvlaTtv2MYO01RZr0yNgtbEZJzcbRl0deOWmSbX0UfRHPt78UCOxPIVYnhAiaJVib40SviaV1Vw/0?wx_fmt=jpeg',
desc: '小程序基础开发之架构、框架、组件,更多课程正在制作中...',
},
]
Component({
data: {
selectedTab: 0,
tabs,
translateX: 0,
},
lifetimes: {
created() {
const shared = wx.worklet.shared
const { windowWidth } = wx.getSystemInfoSync()
const innerWindowWidth = windowWidth - 48 // 左右 padding 各 24
this._tabWidth = shared(innerWindowWidth / 3) // 通过 boundingClientRect 算也行
this._translateX = shared(0)
this._lastTranslateX = shared(0)
this._scaleX = shared(0.7)
this._windowWidth = shared(windowWidth)
},
attached() {
this.applyAnimatedStyle('.tab-border', () => {
'worklet'
return {
transform: `translateX(${this._translateX.value}px) scaleX(${this._scaleX.value})`,
}
})
},
},
methods: {
onTapTab(evt) {
const { tab } = evt.currentTarget.dataset || {}
this.setData({
selectedTab: tab,
})
},
onTabChanged(evt) {
const index = evt.detail.current
this.setData({
selectedTab: index,
})
if (this.renderer !== 'skyline') {
this.setData({
translateX: this._tabWidth.value * index
})
}
},
// swiper 切换过程中每帧回调,声明为 worklet 函数使其跑在 UI 线程
onTabTransition(evt) {
'worklet'
// 这里 swiper item 是占满了整个屏幕宽度,算出拖动比例,换算成相对 tab width 的偏移
const translateRatio = evt.detail.dx / this._windowWidth.value
this._translateX.value = this._lastTranslateX.value + translateRatio * this._tabWidth.value
// 控制 scale 值,拖到中间时 scale 处于最大值 1,两端递减
const scaleRatio = (this._translateX.value / this._tabWidth.value) % 1
const changedScale = scaleRatio <= 0.5 ? scaleRatio : (1 - scaleRatio) // 最大值 0.5
this._scaleX.value = Math.abs(changedScale) * 0.6 + 0.7
if (evt.detail.state === ScrollState.scrollEnd) {
this._lastTranslateX.value = this._translateX.value
}
},
onTabTransitionEnd(evt) {
'worklet'
this.onTabTransition(evt)
// end
this._lastTranslateX.value = this._translateX.value
}
},
})
================================================
FILE: examples/tab-indicator/pages/index/index.json
================================================
{
"usingComponents": {
"navigation-bar": "../../components/navigation-bar"
},
"disableScroll": true,
"navigationStyle": "custom"
}
================================================
FILE: examples/tab-indicator/pages/index/index.wxml
================================================
{{ item.title }}
{{item.title2}}
{{item.desc}}
================================================
FILE: examples/tab-indicator/pages/index/index.wxss
================================================
view, swiper-item {
box-sizing: border-box;
}
.tab-container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
}
.tab-list {
display: flex;
flex-direction: row;
margin: 5px 24px 0;
position: relative;
}
.tab-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
flex: 1;
height: 50px;
border-bottom: 0.5px solid white;
}
.tab-item.active {
color: #07c160;
}
.tab-border {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
background-color: #07c160;
width: 33.33%;
transform: translateX(0) scaleX(0.7);
z-index: 1;
}
.scroll-list {
flex: 1;
width: 100%;
overflow: hidden;
}
swiper-item {
padding: 30px;
}
.item-image {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
}
.item-title {
padding: 20px 0 8px 0;
font-size: 18px;
}
================================================
FILE: examples/tab-indicator/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/tab-indicator/project.private.config.json
================================================
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "tab-indicator",
"setting": {
"compileHotReLoad": false
},
"libVersion": "latest"
}
================================================
FILE: examples/tab-indicator/sitemap.json
================================================
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}