1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

通过微信开发者工具 商城模板 创建新小程序

This commit is contained in:
2023-03-06 23:52:24 +08:00
parent ada464a8cc
commit c21ff901d5
393 changed files with 52952 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
Component({
externalClasses: ['wr-class'],
options: {
multipleSlots: true,
},
properties: {
show: {
type: Boolean,
observer(show) {
this.setData({ visible: show });
},
},
closeBtn: {
type: Boolean,
value: false,
},
},
data: { visible: false },
methods: {
reset() {
this.triggerEvent('reset');
},
confirm() {
this.triggerEvent('confirm');
},
close() {
this.triggerEvent('showFilterPopupClose');
this.setData({ visible: false });
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-popup": "tdesign-miniprogram/popup/popup"
}
}

View File

@@ -0,0 +1,18 @@
<t-popup
visible="{{visible}}"
placement="right"
bind:visible-change="close"
data-index="5"
close-btn="{{closeBtn}}"
>
<view class="content">
<slot name="filterSlot" />
<view class="filter-btns-wrap">
<view class="filter-btn btn-reset" bind:tap="reset">重置</view>
<view class="filter-btn btn-confirm" bind:tap="confirm" data-index="5">
确定
</view>
</view>
</view>
</t-popup>

View File

@@ -0,0 +1,39 @@
.content .filter-btns-wrap {
width: 100%;
position: absolute;
bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
flex-direction: row;
border-radius: 10rpx 0 0 10rpx;
padding: 16rpx 32rpx;
border-top: 1rpx solid #e5e5e5;
box-sizing: border-box;
}
.filter-btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
font-weight: 500;
height: 80rpx;
}
.btn-reset {
color: #fa4126;
background: rgba(255, 255, 255, 1);
position: relative;
border: 1rpx solid #fa4126;
border-radius: 84rpx 0 0 84rpx;
}
.btn-confirm {
border-radius: 0 84rpx 84rpx 0;
border: 1rpx solid #fa4126;
}
.btn-confirm {
color: #fff;
background: #fa4126;
}

View File

@@ -0,0 +1,84 @@
Component({
externalClasses: ['wr-class'],
options: {
multipleSlots: true,
},
properties: {
overall: {
type: Number,
value: 1,
observer(overall) {
this.setData({
overall,
});
},
},
layout: {
type: Number,
value: 1,
observer(layout) {
this.setData({
layout,
});
},
},
sorts: {
type: String,
value: '',
observer(sorts) {
this.setData({
sorts,
});
},
},
color: {
type: String,
value: '#FA550F',
},
},
data: {
layout: 1,
overall: 1,
sorts: '',
},
methods: {
onChangeShowAction() {
const { layout } = this.data;
const nextLayout = layout === 1 ? 0 : 1;
this.triggerEvent('change', { ...this.properties, layout: nextLayout });
},
handlePriseSort() {
const { sorts } = this.data;
this.triggerEvent('change', {
...this.properties,
overall: 0,
sorts: sorts === 'desc' ? 'asc' : 'desc',
});
},
open() {
this.triggerEvent('showFilterPopup', {
show: true,
});
},
onOverallAction() {
const { overall } = this.data;
const nextOverall = overall === 1 ? 0 : 1;
const nextData = {
sorts: '',
prices: [],
};
this.triggerEvent('change', {
...this.properties,
...nextData,
overall: nextOverall,
});
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -0,0 +1,37 @@
<!-- 过滤组件 -->
<view class="wr-class filter-wrap">
<view class="filter-left-content">
<view class="filter-item {{overall === 1 ? 'filter-active-item' : ''}}" bindtap="onOverallAction">
综合
</view>
<view class="filter-item" bind:tap="handlePriseSort">
<text style="color: {{sorts !== '' ? color : '' }}">价格</text>
<view class="filter-price">
<t-icon
prefix="wr"
name="arrow_drop_up"
size="18rpx"
style="color:{{sorts === 'asc' ? color : '#bbb'}}"
/>
<t-icon
prefix="wr"
name="arrow_drop_down"
size="18rpx"
style="color:{{sorts === 'desc' ? color : '#bbb'}}"
/>
</view>
</view>
<view class="filter-item {{prices.length ? 'filter-active-item' : ''}}" bindtap="open" data-index="5">
筛选
<t-icon
name="filter"
prefix="wr"
color="#333"
size="32rpx"
/>
</view>
</view>
</view>
<!-- 筛选弹框 -->
<slot name="filterPopup" />

View File

@@ -0,0 +1,50 @@
.filter-wrap {
width: 100%;
height: 88rpx;
display: flex;
justify-content: space-between;
position: relative;
background: #fff;
}
.filter-right-content {
height: 100%;
flex-basis: 100rpx;
text-align: center;
line-height: 88rpx;
}
.filter-left-content {
height: 100%;
display: flex;
flex-grow: 2;
flex-flow: row nowrap;
justify-content: space-between;
}
.filter-left-content .filter-item {
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
line-height: 36rpx;
font-weight: 400;
color: rgba(51, 51, 51, 1);
}
.filter-left-content .filter-item .filter-price {
display: flex;
flex-direction: column;
margin-left: 6rpx;
justify-content: space-between;
}
.filter-left-content .filter-item .wr-filter {
margin-left: 8rpx;
}
.filter-left-content .filter-active-item {
color: #fa550f;
}

View File

@@ -0,0 +1,141 @@
Component({
options: {
addGlobalClass: true,
},
properties: {
id: {
type: String,
value: '',
observer(id) {
this.genIndependentID(id);
if (this.properties.thresholds?.length) {
this.createIntersectionObserverHandle();
}
},
},
data: {
type: Object,
observer(data) {
if (!data) {
return;
}
let isValidityLinePrice = true;
if (data.originPrice && data.price && data.originPrice < data.price) {
isValidityLinePrice = false;
}
this.setData({ goods: data, isValidityLinePrice });
},
},
currency: {
type: String,
value: '¥',
},
thresholds: {
type: Array,
value: [],
observer(thresholds) {
if (thresholds && thresholds.length) {
this.createIntersectionObserverHandle();
} else {
this.clearIntersectionObserverHandle();
}
},
},
},
data: {
independentID: '',
goods: { id: '' },
isValidityLinePrice: false,
},
lifetimes: {
ready() {
this.init();
},
detached() {
this.clear();
},
},
pageLifeTimes: {},
methods: {
clickHandle() {
this.triggerEvent('click', { goods: this.data.goods });
},
clickThumbHandle() {
this.triggerEvent('thumb', { goods: this.data.goods });
},
addCartHandle(e) {
const { id } = e.currentTarget;
const { id: cardID } = e.currentTarget.dataset;
this.triggerEvent('add-cart', {
...e.detail,
id,
cardID,
goods: this.data.goods,
});
},
genIndependentID(id) {
let independentID;
if (id) {
independentID = id;
} else {
independentID = `goods-card-${~~(Math.random() * 10 ** 8)}`;
}
this.setData({ independentID });
},
init() {
const { thresholds, id } = this.properties;
this.genIndependentID(id);
if (thresholds && thresholds.length) {
this.createIntersectionObserverHandle();
}
},
clear() {
this.clearIntersectionObserverHandle();
},
intersectionObserverContext: null,
createIntersectionObserverHandle() {
if (this.intersectionObserverContext || !this.data.independentID) {
return;
}
this.intersectionObserverContext = this.createIntersectionObserver({
thresholds: this.properties.thresholds,
}).relativeToViewport();
this.intersectionObserverContext.observe(
`#${this.data.independentID}`,
(res) => {
this.intersectionObserverCB(res);
},
);
},
intersectionObserverCB() {
this.triggerEvent('ob', {
goods: this.data.goods,
context: this.intersectionObserverContext,
});
},
clearIntersectionObserverHandle() {
if (this.intersectionObserverContext) {
try {
this.intersectionObserverContext.disconnect();
} catch (e) {}
this.intersectionObserverContext = null;
}
},
},
});

View File

@@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"price": "/components/price/index",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "/components/webp-image/index"
}
}

View File

@@ -0,0 +1,63 @@
<view
id="{{independentID}}"
class="goods-card"
bind:tap="clickHandle"
data-goods="{{ goods }}"
>
<view class="goods-card__main">
<view class="goods-card__thumb" bind:tap="clickThumbHandle">
<t-image
wx:if="{{ !!goods.thumb }}"
t-class="goods-card__img"
src="{{ goods.thumb }}"
mode="aspectFill"
lazy-load
/>
</view>
<view class="goods-card__body">
<view class="goods-card__upper">
<view wx:if="{{ goods.title }}" class="goods-card__title">
{{ goods.title }}
</view>
<view wx:if="{{ goods.tags && !!goods.tags.length }}" class="goods-card__tags">
<view
wx:for="{{ goods.tags }}"
wx:key="index"
wx:for-item="tag"
class="goods-card__tag"
data-index="{{index}}"
>
{{tag}}
</view>
</view>
</view>
<view class="goods-card__down">
<price
wx:if="{{ goods.price }}"
wr-class="spec-for-price"
symbol-class="spec-for-symbol"
symbol="{{currency}}"
price="{{goods.price}}"
/>
<price
wx:if="{{ goods.originPrice && isValidityLinePrice }}"
wr-class="goods-card__origin-price"
symbol="{{currency}}"
price="{{goods.originPrice}}"
type="delthrough"
/>
<t-icon
class="goods-card__add-cart"
prefix="wr"
name="cartAdd"
id="{{independentID}}-cart"
data-id="{{independentID}}"
catchtap="addCartHandle"
size="48rpx"
color="#FA550F"
/>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,133 @@
.goods-card {
box-sizing: border-box;
font-size: 24rpx;
border-radius: 0 0 16rpx 16rpx;
border-bottom: none;
}
.goods-card__main {
position: relative;
display: flex;
line-height: 1;
padding: 0;
background: transparent;
width: 342rpx;
border-radius: 0 0 16rpx 16rpx;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
flex-direction: column;
}
.goods-card__thumb {
flex-shrink: 0;
position: relative;
width: 340rpx;
height: 340rpx;
}
.goods-card__thumb:empty {
display: none;
margin: 0;
}
.goods-card__img {
display: block;
width: 100%;
height: 100%;
border-radius: 16rpx 16rpx 0 0;
overflow: hidden;
}
.goods-card__body {
display: flex;
flex: 1 1 auto;
background: #fff;
border-radius: 0 0 16rpx 16rpx;
padding: 16rpx 24rpx 18rpx;
flex-direction: column;
}
.goods-card__upper {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1 1 auto;
}
.goods-card__title {
flex-shrink: 0;
font-size: 28rpx;
color: #333;
font-weight: 400;
display: -webkit-box;
height: 72rpx;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-word;
line-height: 36rpx;
}
.goods-card__tags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 8rpx 0 0 0;
}
.goods-card__tag {
color: #fa4126;
background: transparent;
font-size: 20rpx;
border: 1rpx solid #fa4126;
padding: 0 8rpx;
border-radius: 16rpx;
line-height: 30rpx;
margin: 0 8rpx 8rpx 0;
display: block;
overflow: hidden;
white-space: nowrap;
word-break: keep-all;
text-overflow: ellipsis;
}
.goods-card__down {
display: flex;
position: relative;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
line-height: 32rpx;
margin: 8rpx 0 0 0;
}
.goods-card__origin-price {
white-space: nowrap;
font-weight: 700;
order: 2;
color: #bbbbbb;
font-size: 24rpx;
margin: 0 0 0 8rpx;
}
.goods-card__add-cart {
order: 3;
margin: auto 0 0 auto;
position: absolute;
bottom: 0;
right: 0;
}
.spec-for-price {
font-size: 36rpx;
white-space: nowrap;
font-weight: 700;
order: 1;
color: #fa4126;
margin: 0;
}
.spec-for-symbol {
font-size: 24rpx;
}

View File

@@ -0,0 +1,62 @@
Component({
externalClasses: ['wr-class'],
properties: {
goodsList: {
type: Array,
value: [],
},
id: {
type: String,
value: '',
observer: (id) => {
this.genIndependentID(id);
},
},
thresholds: {
type: Array,
value: [],
},
},
data: {
independentID: '',
},
lifetimes: {
ready() {
this.init();
},
},
methods: {
onClickGoods(e) {
const { index } = e.currentTarget.dataset;
this.triggerEvent('click', { ...e.detail, index });
},
onAddCart(e) {
const { index } = e.currentTarget.dataset;
this.triggerEvent('addcart', { ...e.detail, index });
},
onClickGoodsThumb(e) {
const { index } = e.currentTarget.dataset;
this.triggerEvent('thumb', { ...e.detail, index });
},
init() {
this.genIndependentID(this.id || '');
},
genIndependentID(id) {
if (id) {
this.setData({ independentID: id });
} else {
this.setData({
independentID: `goods-list-${~~(Math.random() * 10 ** 8)}`,
});
}
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"goods-card": "/components/goods-card/index"
}
}

View File

@@ -0,0 +1,16 @@
<view class="goods-list-wrap wr-class" id="{{independentID}}">
<block wx:for="{{goodsList}}" wx:for-item="item" wx:key="index">
<goods-card
id="{{independentID}}-gd-{{index}}"
data="{{item}}"
currency="{{item.currency || '¥'}}"
thresholds="{{thresholds}}"
class="goods-card-inside"
data-index="{{index}}"
bind:thumb="onClickGoodsThumb"
bind:click="onClickGoods"
bind:add-cart="onAddCart"
/>
</block>
</view>

View File

@@ -0,0 +1,7 @@
.goods-list-wrap {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
padding: 0;
background: #fff;
}

View File

@@ -0,0 +1,54 @@
Component({
externalClasses: ['wr-class', 'wr-class--no-more'],
options: { multipleSlots: true },
properties: {
status: {
type: Number,
value: 0,
},
loadingText: {
type: String,
value: '加载中...',
},
noMoreText: {
type: String,
value: '没有更多了',
},
failedText: {
type: String,
value: '加载失败,点击重试',
},
color: {
type: String,
value: '#BBBBBB',
},
failedColor: {
type: String,
value: '#FA550F',
},
size: {
type: null,
value: '40rpx',
},
loadingBackgroundColor: {
type: String,
value: '#F5F5F5',
},
listIsEmpty: {
type: Boolean,
value: false,
},
},
methods: {
/** 点击处理 */
tapHandle() {
// 失败重试
if (this.data.status === 3) {
this.triggerEvent('retry');
}
},
},
});

View File

@@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"t-loading": "tdesign-miniprogram/loading/loading",
"t-divider": "tdesign-miniprogram/divider/divider"
}
}

View File

@@ -0,0 +1,31 @@
<view
class="load-more wr-class"
style="{{listIsEmpty && (status === 0 || status === 2) ? 'display: none' : '' }}"
bindtap="tapHandle"
>
<!-- 加载中 -->
<t-loading
t-class="t-class-loading"
t-class-text="t-class-loading-text"
t-class-indicator="t-class-indicator"
loading="{{status === 1}}"
text="加载中..."
theme="circular"
size="40rpx"
/>
<!-- 已全部加载 -->
<t-divider wx:if="{{status === 2}}" t-class="t-class-divider" t-class-content="t-class-divider-content">
<text slot="content">{{noMoreText}}</text>
</t-divider>
<!-- 加载失败 -->
<view class="load-more__error" wx:if="{{status===3}}">
加载失败
<text class="load-more__refresh-btn" bind:tap="tapHandle">刷新</text>
</view>
</view>
<!-- 支持通过slot传入页面/列表的空态load-more来控制空态的显示状态 -->
<slot wx:if="{{listIsEmpty && (status === 0 || status === 2)}}" name="empty" />

View File

@@ -0,0 +1,35 @@
.load-more {
font-size: 24rpx;
height: 100rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.load-more .t-class-loading {
display: flex;
justify-content: center;
--td-loading-color: #fa4126;
}
.load-more .t-class-loading-text {
color: #bbbbbb;
}
.t-class-divider-content {
margin: 0 10rpx;
color: #bbbbbb;
}
.load-more .t-class-indicator {
color: #b9b9b9 !important;
}
.load-more__error {
margin: auto;
}
.load-more__refresh-btn {
margin-left: 16rpx;
color: #fa4126;
}

View File

@@ -0,0 +1,23 @@
Component({
externalClasses: ['wr-class'],
properties: {
position: {
type: String,
value: 'static',
},
noMask: Boolean,
type: {
type: String,
value: 'circular',
},
vertical: Boolean,
size: {
type: String,
value: '50rpx',
},
backgroundColor: {
type: String,
value: 'rgba(0, 0, 0, .6)',
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-loading": "tdesign-miniprogram/loading/loading"
}
}

View File

@@ -0,0 +1,11 @@
<view class="t-class loading-content {{position}}" style="{{(position === 'static' || noMask) ? 'visibility: hidden;' : ''}} background-color: {{backgroundColor}}">
<t-loading
t-class="loading"
theme="{{type}}"
layout="{{vertical}}"
size="{{size}}"
>
<slot/>
</t-loading>
</view>

View File

@@ -0,0 +1,23 @@
.loading-content {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
position: relative;
}
.loading-content.absolute {
position: absolute;
z-index: 1;
left: 0;
top: 0;
}
.loading-content.fixed {
position: fixed;
z-index: 1;
left: 0;
top: 0;
}
.loading-content .loading {
width: 100%;
height: 100%;
visibility: visible;
}

View File

@@ -0,0 +1,71 @@
Component({
externalClasses: ['wr-class', 'symbol-class', 'decimal-class'],
useStore: [],
properties: {
priceUnit: {
type: String,
value: 'fen',
}, // 价格单位,分 | 元, fenyuan
price: {
type: null,
value: '',
observer(price) {
this.format(price);
},
}, // 价格, 以分为单位
type: {
type: String,
value: '', //
}, // main 粗体, lighter 细体, mini 黑色, del 中划线, delthrough 中划线,包括货币符号
symbol: {
type: String,
value: '¥', // '¥',
}, // 货币符号,默认是人民币符号¥
fill: Boolean, // 是否自动补齐两位小数
decimalSmaller: Boolean, // 小数字号小一点
lineThroughWidth: {
type: null,
value: '0.12em',
}, // 划线价线条高度
},
data: {
pArr: [],
},
methods: {
format(price) {
price = parseFloat(`${price}`);
const pArr = [];
if (!isNaN(price)) {
const isMinus = price < 0;
if (isMinus) {
price = -price;
}
if (this.properties.priceUnit === 'yuan') {
const priceSplit = price.toString().split('.');
pArr[0] = priceSplit[0];
pArr[1] = !priceSplit[1]
? '00'
: priceSplit[1].length === 1
? `${priceSplit[1]}0`
: priceSplit[1];
} else {
price = Math.round(price * 10 ** 8) / 10 ** 8; // 恢复精度丢失
price = Math.ceil(price); // 向上取整
pArr[0] = price >= 100 ? `${price}`.slice(0, -2) : '0';
pArr[1] = `${price + 100}`.slice(-2);
}
if (!this.properties.fill) {
// 如果 fill 为 false 不显示小数末尾的0
if (pArr[1] === '00') pArr[1] = '';
else if (pArr[1][1] === '0') pArr[1] = pArr[1][0];
}
if (isMinus) {
pArr[0] = `-${pArr[0]}`;
}
}
this.setData({ pArr });
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,21 @@
<wxs module="utils">
var REGEXP = getRegExp('^\d+(\.\d+)?$');
function addUnit(value) {
if (value == null) {
return '';
}
return REGEXP.test('' + value) ? value + 'rpx' : value;
}
module.exports = {
addUnit: addUnit
};
</wxs>
<view class="price {{type}} wr-class">
<view wx:if="{{type === 'delthrough'}}" class="line" style="height:{{utils.addUnit(lineThroughWidth)}};" />
<view class="symbol symbol-class">{{symbol}}</view>
<view class="pprice">
<view class="integer inline">{{pArr[0]}}</view>
<view wx:if="{{pArr[1]}}" class="decimal inline {{decimalSmaller ? 'smaller' : ''}} decimal-class">.{{pArr[1]}}</view>
</view>
</view>

View File

@@ -0,0 +1,66 @@
:host {
display: inline-block;
display: inline-block;
font-weight: inherit;
}
.inline {
display: inline;
white-space: nowrap;
}
.price {
display: inline;
color: inherit;
font-size: inherit;
text-decoration: inherit;
}
.lighter {
font-weight: 400;
font-size: 32rpx;
}
.mini {
font-size: 24rpx;
color: #5d5d5d;
font-weight: 400;
}
.del .pprice {
font-size: 32rpx;
color: #9b9b9b;
text-decoration: line-through;
font-weight: 400;
}
.delthrough {
position: relative;
}
.delthrough .line {
position: absolute;
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
margin: 0;
background-color: currentColor;
}
.symbol {
display: inline;
color: inherit;
font-size: inherit;
font-size: 0.8em;
}
.pprice {
display: inline;
margin: 0 0 0 4rpx;
}
.integer {
color: inherit;
font-size: inherit;
}
.decimal {
color: inherit;
font-size: inherit;
}
.decimal.smaller {
font-size: 0.8em;
vertical-align: baseline;
}

View File

@@ -0,0 +1,79 @@
let ARRAY = [];
Component({
externalClasses: ['wr-class'],
options: {
multipleSlots: true,
},
properties: {
disabled: Boolean,
leftWidth: {
type: Number,
value: 0,
},
rightWidth: {
type: Number,
value: 0,
},
asyncClose: Boolean,
},
attached() {
ARRAY.push(this);
},
detached() {
ARRAY = ARRAY.filter((item) => item !== this);
},
/**
* Component initial data
*/
data: {
wrapperStyle: '',
asyncClose: false,
closed: true,
},
/**
* Component methods
*/
methods: {
open(position) {
this.setData({ closed: false });
this.triggerEvent('close', {
position,
instance: this,
});
},
close() {
this.setData({ closed: true });
},
closeOther() {
ARRAY.filter((item) => item !== this).forEach((item) => item.close());
},
noop() {
return;
},
onClick(event) {
const { key: position = 'outside' } = event.currentTarget.dataset;
this.triggerEvent('click', position);
if (this.data.closed) {
return;
}
if (this.data.asyncClose) {
this.triggerEvent('close', {
position,
instance: this,
});
} else {
this.close();
}
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,174 @@
<wxs module="swipe">
var THRESHOLD = 0.3;
var MIN_DISTANCE = 10;
var owner;
var state;
var getState = function(ownerInstance) {
owner = ownerInstance;
state = owner.getState();
state.leftWidth = state.leftWidth || 0;
state.rightWidth = state.rightWidth || 0;
state.offset = state.offset || 0;
state.startOffset = state.startOffset || 0;
};
var initRightWidth = function(newVal, oldVal, ownerInstance) {
getState(ownerInstance);
state.rightWidth = newVal;
if (state.offset < 0) {
swipeMove(-state.rightWidth);
}
};
var initLeftWidth = function(newVal, oldVal, ownerInstance) {
getState(ownerInstance);
state.leftWidth = newVal;
if (state.offset > 0) {
swipeMove(state.leftWidth);
}
}
var resetTouchStatus = function() {
state.direction = '';
state.deltaX = 0;
state.deltaY = 0;
state.offsetX = 0;
state.offsetY = 0;
};
var touchMove = function(event) {
var touchPoint = event.touches[0];
state.deltaX = touchPoint.clientX - state.startX;
state.deltaY = touchPoint.clientY - state.startY;
state.offsetX = Math.abs(state.deltaX);
state.offsetY = Math.abs(state.deltaY);
state.direction = state.direction || getDirection(state.offsetX, state.offsetY);
};
var getDirection = function(x, y) {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > MIN_DISTANCE) {
return 'vertical';
}
return '';
};
var range = function(num, min, max) {
return Math.min(Math.max(num, min), max);
};
var swipeMove = function(_offset = 0) {
state.offset = range(
_offset,
-state.rightWidth,
+state.leftWidth,
);
var transform = 'translate3d(' + state.offset + 'px, 0, 0)';
var transition = state.dragging
? 'none'
: 'transform .6s cubic-bezier(0.18, 0.89, 0.32, 1)';
owner.selectComponent('#wrapper').setStyle({
'-webkit-transform': transform,
'-webkit-transition': transition,
'transform': transform,
'transition': transition
});
};
var close = function() {
swipeMove(0);
};
var onCloseChange = function(newVal, oldVal, ownerInstance) {
getState(ownerInstance);
if (newVal === oldVal) return;
if (newVal) {
close();
}
};
var touchStart = function(event) {
resetTouchStatus();
state.startOffset = state.offset;
var touchPoint = event.touches[0];
state.startX = touchPoint.clientX;
state.startY = touchPoint.clientY;
owner.callMethod('closeOther');
};
var startDrag = function(event, ownerInstance) {
getState(ownerInstance);
touchStart(event);
};
var onDrag = function(event, ownerInstance) {
getState(ownerInstance);
touchMove(event);
if (state.direction !== 'horizontal') {
return;
}
state.dragging = true;
swipeMove(state.startOffset + state.deltaX);
};
var open = function(position) {
var _offset = position === 'left' ? +state.leftWidth : -state.rightWidth;
owner.callMethod('open', { position: position });
swipeMove(_offset);
};
var endDrag = function(event, ownerInstance) {
getState(ownerInstance);
state.dragging = false;
// 左/右侧有可滑动区域且当前不是已open状态且滑动幅度超过阈值时open左/右侧(滚动到该侧的最边上)
if (+state.rightWidth > 0 && -state.startOffset < +state.rightWidth && -state.offset > +state.rightWidth * THRESHOLD) {
open('right');
} else if (+state.leftWidth > 0 && state.startOffset < +state.leftWidth && state.offset > +state.leftWidth * THRESHOLD) {
open('left');
} else {
// 仅在有发生侧滑的情况下自动关闭由js控制是否异步关闭
if (state.startOffset !== state.offset) {
close();
}
}
};
module.exports = {
initLeftWidth: initLeftWidth,
initRightWidth: initRightWidth,
startDrag: startDrag,
onDrag: onDrag,
endDrag: endDrag,
onCloseChange: onCloseChange
};
</wxs>
<view
class="wr-class wr-swipeout"
data-key="cell"
capture-bind:tap="onClick"
bindtouchstart="{{disabled || swipe.startDrag}}"
capture-bind:touchmove="{{disabled || swipe.onDrag}}"
bindtouchend="{{disabled || swipe.endDrag}}"
bindtouchcancel="{{disabled || swipe.endDrag}}"
closed="{{closed}}"
change:closed="{{swipe.onCloseChange}}"
leftWidth="{{leftWidth}}"
rightWidth="{{rightWidth}}"
change:leftWidth="{{swipe.initLeftWidth}}"
change:rightWidth="{{swipe.initRightWidth}}"
>
<view id="wrapper">
<view wx:if="{{ leftWidth }}" class="wr-swipeout__left" data-key="left" catch:tap="onClick">
<slot name="left" />
</view>
<slot />
<view wx:if="{{ rightWidth }}" class="wr-swipeout__right" data-key="right" catch:tap="onClick">
<slot name="right" />
</view>
</view>
</view>

View File

@@ -0,0 +1,18 @@
.wr-swipeout {
position: relative;
overflow: hidden;
}
.wr-swipeout__left,
.wr-swipeout__right {
position: absolute;
top: 0;
height: 100%;
}
.wr-swipeout__left {
left: 0;
transform: translate3d(-100%, 0, 0);
}
.wr-swipeout__right {
right: 0;
transform: translate3d(100%, 0, 0);
}

View File

@@ -0,0 +1,86 @@
/*
* @Author: rileycai
* @Date: 2022-03-14 14:21:26
* @LastEditTime: 2022-03-14 15:23:04
* @LastEditors: rileycai
* @Description: webp-image组件对t-image包裹了一层主要实现图片裁剪、webp压缩功能
* @FilePath: /tdesign-miniprogram-starter/components/webp-image/index.js
*/
const systemInfo = wx.getSystemInfoSync();
Component({
externalClasses: ['t-class', 't-class-load'],
properties: {
loadFailed: {
type: String,
value: 'default',
},
loading: {
type: String,
value: 'default',
},
src: {
type: String,
value: '',
},
mode: {
type: String,
value: 'aspectFill',
},
webp: {
type: Boolean,
value: true,
},
lazyLoad: {
type: Boolean,
value: false,
},
showMenuByLongpress: {
type: Boolean,
value: false,
},
},
data: {
thumbHeight: 375,
thumbWidth: 375,
systemInfo,
},
lifetimes: {
ready() {
const { mode } = this.properties;
// 获取容器的真实宽高,设置图片的裁剪宽度
this.getRect('.J-image').then((res) => {
if (res) {
const { width, height } = res;
this.setData(
mode === 'heightFix'
? {
thumbHeight: this.px2rpx(height) || 375,
}
: {
thumbWidth: this.px2rpx(width) || 375,
},
);
}
});
},
},
methods: {
px2rpx(px) {
return (750 / (systemInfo.screenWidth || 375)) * px;
},
getRect(selector) {
return new Promise((resolve) => {
if (!this.selectorQuery) {
this.selectorQuery = this.createSelectorQuery();
}
this.selectorQuery.select(selector).boundingClientRect(resolve).exec();
});
},
onLoad(e) {
this.triggerEvent('load', e.detail);
},
onError(e) {
this.triggerEvent('error', e.detail);
},
},
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"t-image": "tdesign-miniprogram/image/image"
}
}

View File

@@ -0,0 +1,14 @@
<wxs src="./utils.wxs" module="Utils" />
<t-image
t-class="J-image"
src="{{Utils.getSrc({src, thumbWidth: thumbWidth || 0, thumbHeight: thumbHeight || 0, systemInfo, webp, mode})}}"
t-class="t-class"
t-class-load="t-class-load"
mode="{{ mode }}"
lazy="{{ lazyLoad }}"
show-menu-by-longpress="{{showMenuByLongpress}}"
error="{{loadFailed}}"
loading="{{loading}}"
binderror="onError"
bindload="onLoad"
/>

View File

@@ -0,0 +1,140 @@
var isString = function (value) {
return typeof value === 'string';
};
var isNumber = function (value) {
return typeof value === 'number';
};
var getFileExt = function (src) {
var fileUrl = src.split('?')[0];
var splitUlr = fileUrl.split('/');
var filepath = splitUlr[splitUlr.length - 1];
return filepath.split('.')[1] || 'jpg';
};
function isUrl(url) {
// NOCC:ToolNameCheck(非敏感词)
var urlReg = getRegExp(
'/[(http(s)?)://(www.)?a-zA-Z0-9@:%._+~#=]{2,256}.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/',
'ig',
);
return urlReg.test(url);
}
function rpx2px(rpx, screenWidth) {
// px / systemWidth = rpx / 750
var result = (rpx * (screenWidth || 375)) / 750;
return Math.round(result);
}
function imageMogr(url, options) {
if (!isString(url) || !url) return '';
if (
url.indexOf('qlogo.cn') !== -1 ||
url.indexOf('wxfile://') === 0 ||
url.indexOf('http://tmp/wx') === 0 ||
url.indexOf('imageMogr2') !== -1
) {
//qlogo.cn域名或者本地图片不做转换
return url;
} //强制转https
if (url.indexOf('http://') === 0) {
url = url.replace('http://', 'https://');
} else if (url.indexOf('//') === 0) {
url = 'https:' + url;
}
if (!options) return url;
var width = Math.ceil(options.width),
height = Math.ceil(options.height),
format = options.format,
_optionsQuality = options.quality,
quality = _optionsQuality === undefined ? 70 : _optionsQuality,
_optionsStrip = options.strip,
strip = _optionsStrip === undefined ? true : _optionsStrip,
crop = options.crop;
var isValidWidth = isNumber(width) && width > 0;
var isValidHeight = isNumber(height) && height > 0;
var imageMogrStr = '';
var size = '';
if (isValidWidth && isValidHeight) {
size = ''.concat(width, 'x').concat(height);
} else if (isValidWidth) {
size = ''.concat(width, 'x');
} else if (isValidHeight) {
size = 'x'.concat(height);
}
if (size) {
//缩放或者裁剪
imageMogrStr += '/'.concat(crop ? 'crop' : 'thumbnail', '/').concat(size);
if (crop) {
//裁剪目前需求只有以图片中心为基准
imageMogrStr += '/gravity/center';
}
}
if (isNumber(quality)) {
//质量变换
imageMogrStr += '/quality/'.concat(quality);
}
if (strip) {
//去除元信息
imageMogrStr += '/strip';
}
var ext = getFileExt(url);
// gif 图片不做格式转换,否则会损坏动图
if (ext === 'gif') {
imageMogrStr += '/cgif/1';
} else if (format) {
//格式转换
imageMogrStr += '/format/'.concat(format);
}
if (format === 'jpg' || (!format && (ext === 'jpg' || ext === 'jpeg'))) {
//渐进式 jpg 加载
imageMogrStr += '/interlace/1';
}
if (!imageMogrStr) return url;
return ''
.concat(url)
.concat(url.indexOf('?') !== -1 ? '&' : '?', 'imageMogr2')
.concat(imageMogrStr);
}
function getSrc(options) {
if (!options.src) return '';
if (options.thumbWidth || options.thumbHeight) {
return imageMogr(options.src, {
width:
options.mode !== 'heightFix'
? rpx2px(options.thumbWidth, options.systemInfo.screenWidth) *
options.systemInfo.pixelRatio
: null,
height:
options.mode !== 'widthFix'
? rpx2px(options.thumbHeight, options.systemInfo.screenWidth) *
options.systemInfo.pixelRatio
: null,
format: options.webp ? 'webp' : null,
});
}
return '';
}
module.exports = {
imageMogr: imageMogr,
getSrc: getSrc,
};