第一次提交
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<view class="page-content">
|
||||
<!-- 选择器背景 -->
|
||||
<view class="page-mask"></view>
|
||||
<!-- 选择器主体 -->
|
||||
<view class="page-main" :style="'height:60%'">
|
||||
<!-- 操作区域 包含取消和确定 -->
|
||||
<view class="main-operate">
|
||||
<text class="title-txt" @click.stop="handleCancel">取消</text>
|
||||
<text class="title-txt" @click.stop="handleSubmit">确定</text>
|
||||
</view>
|
||||
<!-- 已选择的地址信息 -->
|
||||
<view class="main-select" :class="[selectAreaLevelLimit == 3 ? 'w33_percent' : '']">
|
||||
<text
|
||||
v-if="showIndex == 0 || provinceObj.areaName"
|
||||
:class="showIndex == 0 ? 'select' : ''"
|
||||
@click="anewSelect(0)"
|
||||
>{{ provinceObj.areaName || "请选择" }}</text
|
||||
>
|
||||
<text
|
||||
v-if="showIndex == 1 || cityObj.areaName"
|
||||
:class="showIndex == 1 ? 'select' : ''"
|
||||
@click="anewSelect(1)"
|
||||
>{{ cityObj.areaName || "请选择" }}</text
|
||||
>
|
||||
<text
|
||||
v-if="showIndex == 2 || areaObj.areaName"
|
||||
:class="showIndex == 2 ? 'select' : ''"
|
||||
@click="anewSelect(2)"
|
||||
>{{ areaObj.areaName || "请选择" }}</text
|
||||
>
|
||||
<text
|
||||
v-if="(showIndex == 3 || streetObj.areaName) && selectAreaLevelLimit == 4"
|
||||
:class="showIndex == 3 ? 'select' : ''"
|
||||
@click="anewSelect(3)"
|
||||
>{{ streetObj.areaName || "请选择" }}</text
|
||||
>
|
||||
</view>
|
||||
<!-- 待选择的地址信息 -->
|
||||
<view class="main-list">
|
||||
<view v-if="showIndex == 0">
|
||||
<view
|
||||
class="list-box"
|
||||
:class="provinceObj.areaCode == item.areaCode ? 'active' : ''"
|
||||
@click="handleSelectAddress(0, item)"
|
||||
v-for="item in provinceData"
|
||||
:key="item.areaCode"
|
||||
>
|
||||
<text>{{ item.areaName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="showIndex == 1">
|
||||
<view
|
||||
class="list-box"
|
||||
:class="cityObj.areaCode == item.areaCode ? 'active' : ''"
|
||||
@click="handleSelectAddress(1, item)"
|
||||
v-for="item in cityData"
|
||||
:key="item.areaCode"
|
||||
>
|
||||
<text>{{ item.areaName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="showIndex == 2">
|
||||
<view
|
||||
class="list-box"
|
||||
:class="areaObj.areaCode == item.areaCode ? 'active' : ''"
|
||||
@click="handleSelectAddress(2, item)"
|
||||
v-for="item in areaData"
|
||||
:key="item.areaCode"
|
||||
>
|
||||
<text>{{ item.areaName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="showIndex == 3 && selectAreaLevelLimit == 4">
|
||||
<view
|
||||
class="list-box"
|
||||
:class="streetObj.areaCode == item.areaCode ? 'active' : ''"
|
||||
@click="handleSelectAddress(3, item)"
|
||||
v-for="item in streetsData"
|
||||
:key="item.areaCode"
|
||||
>
|
||||
<text>{{ item.areaName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "UniAddressSelector",
|
||||
props: {
|
||||
areaInfoSelected: {
|
||||
type: Object,
|
||||
default: {
|
||||
|
||||
},
|
||||
},
|
||||
selectAreaLevelLimit:{
|
||||
type: Number,
|
||||
default: 4,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showIndex: 0, //地区显示
|
||||
provinceObj: {},
|
||||
cityObj: {},
|
||||
areaObj: {},
|
||||
streetObj: {},
|
||||
heightCot: 30, //设置屏幕高度 0 ~ 100
|
||||
provinceData: "", // 当前展示的省数据
|
||||
cityData: [], // 当前展示的市数据
|
||||
areaData: [], //当前展示的区数据
|
||||
streetsData: [], //当前展示的区数据
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.areaInfoSelected.fullAreaTextInitial) {
|
||||
let { fullAreaTextInitial } = this.areaInfoSelected;
|
||||
let _arr = fullAreaTextInitial.split(",");
|
||||
|
||||
this.provinceObj = this.areaInfoSelected.provinceObj;
|
||||
this.cityObj = this.areaInfoSelected.cityObj;
|
||||
this.areaObj = this.areaInfoSelected.areaObj;
|
||||
this.streetObj = this.areaInfoSelected.streetObj;
|
||||
|
||||
if (_arr.length == 1) {
|
||||
this.handleGetMap({}, "init");
|
||||
}
|
||||
if (_arr.length == 2) {
|
||||
this.handleGetMap(this.areaInfoSelected.provinceObj, "pro");
|
||||
}
|
||||
if (_arr.length == 3) {
|
||||
this.handleGetMap(this.areaInfoSelected.cityObj, "city");
|
||||
}
|
||||
if (_arr.length == 4) {
|
||||
this.handleGetMap(this.areaInfoSelected.areaObj, "area");
|
||||
}
|
||||
console.log("传入的this.areaInfoSelected", this.areaInfoSelected);
|
||||
} else {
|
||||
this.handleGetMap({}, "init");
|
||||
}
|
||||
this.getScreen();
|
||||
},
|
||||
onShow() {},
|
||||
methods: {
|
||||
//组件高度自适应
|
||||
getScreen() {
|
||||
let that = this;
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
that.heightCot = (res.safeArea.height * 2) / 2;
|
||||
},
|
||||
});
|
||||
},
|
||||
//取消
|
||||
handleCancel() {
|
||||
this.$emit("cancel");
|
||||
},
|
||||
//确定
|
||||
handleSubmit() {
|
||||
const { provinceObj, cityObj, areaObj, streetObj } = this;
|
||||
const arr = [provinceObj, cityObj, areaObj, streetObj];
|
||||
|
||||
let _fullAreaText = [];
|
||||
arr.map((item) => {
|
||||
if (item.areaCode) {
|
||||
_fullAreaText.push(item.areaName);
|
||||
}
|
||||
});
|
||||
if (this.selectAreaLevelLimit == 3) {
|
||||
if (_fullAreaText.length !== 3) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "地址需精确到地区",
|
||||
});
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (_fullAreaText.length !== this.selectAreaLevelLimit) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "地址需精确到街道",
|
||||
});
|
||||
return
|
||||
}
|
||||
}
|
||||
let areaInfoObj = {
|
||||
fullAreaTextInitial: _fullAreaText.toString(),
|
||||
fullAreaText: _fullAreaText.toString().replace(/,/g, ""),
|
||||
provinceCode: provinceObj.areaCode || "",
|
||||
cityCode: cityObj.areaCode || "",
|
||||
areaCode: areaObj.areaCode || "",
|
||||
streetCode: streetObj.areaCode || "",
|
||||
provinceObj,
|
||||
cityObj,
|
||||
areaObj,
|
||||
streetObj,
|
||||
};
|
||||
this.$emit("confirm", areaInfoObj);
|
||||
},
|
||||
//下拉选择
|
||||
anewSelect(num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
this.showIndex = 0;
|
||||
this.cityObj = {};
|
||||
this.areaObj = {};
|
||||
this.streetObj = {};
|
||||
|
||||
this.cityData = [];
|
||||
this.areaData = [];
|
||||
this.streetsData = [];
|
||||
this.handleGetMap({}, "init");
|
||||
break;
|
||||
case 1:
|
||||
this.showIndex = 1;
|
||||
this.streetObj = {};
|
||||
this.areaData = [];
|
||||
this.streetsData = [];
|
||||
if (!this.areaObj.areaCode) return;
|
||||
this.areaObj = {};
|
||||
this.handleGetMap(this.provinceObj, "pro");
|
||||
break;
|
||||
case 2:
|
||||
this.showIndex = 2;
|
||||
this.streetsData = [];
|
||||
if (!this.streetObj.areaCode) return;
|
||||
this.streetObj = {};
|
||||
this.handleGetMap(this.areaObj, "area");
|
||||
break;
|
||||
case 3:
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleGetMap(obj, type) {
|
||||
//name选择名称 type类型
|
||||
let that = this,
|
||||
data = {
|
||||
parentArea: 0,
|
||||
};
|
||||
|
||||
if (type !== "init") {
|
||||
data.parentArea = obj.areaCode;
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
mask: true,
|
||||
});
|
||||
}
|
||||
// 此处$requestGet:我司基于uni.request()封装的get请求方法
|
||||
// this.$apiUrl.storeArea:请求路径 每次请求带上parentArea 即可请求下一级区域数据 接口根据自身情况来决定
|
||||
this.$requestGet({
|
||||
url: this.$apiUrl.storeArea,
|
||||
data,
|
||||
}).then((resp) => {
|
||||
uni.hideLoading();
|
||||
if (resp.state == 200) {
|
||||
let arr = ["init", "pro", "city", "area"];
|
||||
let _obj = {
|
||||
init: "provinceData",
|
||||
pro: "cityData",
|
||||
city: "areaData",
|
||||
area: "streetsData",
|
||||
};
|
||||
this[_obj[type]] = resp.data;
|
||||
this.showIndex = arr.indexOf(type);
|
||||
// if (type === "init") {
|
||||
// that.provinceData = resp.data;
|
||||
// this.showIndex = 0;
|
||||
// } else if (type == "pro") {
|
||||
// that.cityData = resp.data;
|
||||
// this.showIndex = 1;
|
||||
// } else if (type == "city") {
|
||||
// that.areaData = resp.data;
|
||||
// this.showIndex = 2;
|
||||
// } else if (type == "area") {
|
||||
// that.streetsData = resp.data;
|
||||
// this.showIndex = 3;
|
||||
// }
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: resp.msg,
|
||||
position: "center",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSelectAddress(type, obj) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
//选择省
|
||||
this.handleGetMap(obj, "pro");
|
||||
this.provinceObj = obj;
|
||||
this.showIndex = 1;
|
||||
this.cityData = [];
|
||||
this.areaData = [];
|
||||
this.streetsData = [];
|
||||
break;
|
||||
case 1:
|
||||
//选择市
|
||||
this.handleGetMap(obj, "city");
|
||||
this.cityObj = obj;
|
||||
this.showIndex = 2;
|
||||
this.areaData = [];
|
||||
this.streetsData = [];
|
||||
break;
|
||||
case 2:
|
||||
//选择区
|
||||
if (this.selectAreaLevelLimit == 3) {
|
||||
this.areaObj = obj;
|
||||
} else {
|
||||
this.handleGetMap(obj, "area");
|
||||
this.areaObj = obj;
|
||||
this.showIndex = 3;
|
||||
this.streetsData = [];
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
//选择街道
|
||||
this.streetObj = obj;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-mask {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 998;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.page-main {
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
padding: 20rpx;
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
max-height: 60%;
|
||||
.main-operate {
|
||||
padding: 0 10rpx 10rpx 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.title-txt {
|
||||
color: #343434;
|
||||
font-size: 30rpx;
|
||||
&:nth-child(2) {
|
||||
color: #ff4242;
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-select {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
color: #343434;
|
||||
font-size: 30rpx;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
text {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-align: center;
|
||||
}
|
||||
.select {
|
||||
color: #ff4242;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 6rpx;
|
||||
background: #ff4242;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-list {
|
||||
overflow-y: auto;
|
||||
color: #343434;
|
||||
font-size: 30rpx;
|
||||
width: 100%;
|
||||
height: 72%;
|
||||
overflow: auto;
|
||||
.list-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
border-bottom: 2rpx solid #dedede;
|
||||
}
|
||||
.list-box:last-child{
|
||||
border-bottom: none;
|
||||
}
|
||||
.active {
|
||||
color: #ff4242;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
components/Luke-UniappAddressSelector/index.vue
Normal file
61
components/Luke-UniappAddressSelector/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<view class="page-index">
|
||||
<UniAddressSelector
|
||||
v-if="showUniAddressSelector"
|
||||
:areaInfoSelected="areaInfoObj"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm">
|
||||
</UniAddressSelector>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'UniAddressSelector' from './components/UniAddressSelector.vue';
|
||||
|
||||
export default {
|
||||
name: 'index',
|
||||
components: {
|
||||
UniAddressSelector
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showUniAddressSelector: false,
|
||||
areaInfoObj: {
|
||||
provinceObj: {
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
},
|
||||
cityObj: {
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
},
|
||||
cityObj: {
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
},
|
||||
areaObj: {
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
},
|
||||
streetObj: {
|
||||
areaCode: '',
|
||||
areaName: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCancel() {
|
||||
this.showUniAddressSelector = false;
|
||||
},
|
||||
handleConfirm(areaInfoObj) {
|
||||
this.showUniAddressSelector = false;
|
||||
this.areaInfoObj = areaInfoObj;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
31
components/Luke-UniappAddressSelector/readme.md
Normal file
31
components/Luke-UniappAddressSelector/readme.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# UniAddressSelector省市区街道三级四级联动(内附demo)
|
||||
|
||||
### 概述
|
||||
|
||||
1. 支持uniapp(APP、H5、小程序)省市区街道三级四级联动
|
||||
2. 支持默认地区或已选择地区显示
|
||||
3. 数据来源可包括高德地图api、公司封装接口返回
|
||||
|
||||
### 预览
|
||||
|
||||

|
||||
|
||||
### API
|
||||
|
||||
#### props
|
||||
|
||||
| 属性 | <span style="display:inline-block;width:80px">必填</span> | 说明 | 类型 | <span style="display:inline-block;width:80px">默认值</span> |
|
||||
| :------- | ------------- | ------------------------------------------------------------ | ------ | ------ |
|
||||
| areaInfoSelected | 否 | 已选择的地址对象或默认显示地址对象,属性有:provinceObj,cityObjareaObj,streetObj | Object | {} |
|
||||
| selectAreaLevelLimit | 否 | 指定选择的级别数,如只需要选择省市区,则指定值为3 | Number | 4 |
|
||||
|
||||
#### events
|
||||
|
||||
| 事件名 | <span style="display:inline-block;width:200px">说明</span>| 返回值 |
|
||||
| :------- |--------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------ |
|
||||
| cancel | 点击取消按钮时触发 | - |
|
||||
| confirm | 点击确认按钮时触发 | 选中的省市区街道地址对象,包含如下属性:fullAreaText,provinceCode,,cityCode,areaCode,streetCode,provinceObj,cityObj,areaObj,streetObj |
|
||||
|
||||
### 作者想说
|
||||
|
||||
欢迎大家留言、评论与star
|
||||
36
components/add-cart-btn/index.vue
Executable file
36
components/add-cart-btn/index.vue
Executable file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<view class="add-cart" @click.stop="handleAddCart">
|
||||
<text class="icon iconfont" :class="[`icon-jiagou${btnStyle}`]"></text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// 购物车按钮样式 1 2 3
|
||||
btnStyle: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: false,
|
||||
goodsInfo: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAddCart() {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-cart {
|
||||
font-size: 38rpx;
|
||||
padding: 0 20rpx;
|
||||
color: $main-bg;
|
||||
}
|
||||
</style>
|
||||
178
components/add-cart-popup/index.vue
Executable file
178
components/add-cart-popup/index.vue
Executable file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<goods-sku-popup :value="visible" @input="onChangeValue" border-radius="20" :localdata="localdata" :mode="2" :maskCloseAble="true"
|
||||
:priceColor="appTheme.mainBg" :buyNowBackgroundColor="appTheme.mainBg" :addCartColor="appTheme.viceText" :addCartBackgroundColor="appTheme.viceBg"
|
||||
:activedStyle="{ color: appTheme.mainBg, borderColor: appTheme.mainBg, backgroundColor: activedBtnBackgroundColor }"
|
||||
@add-cart="addCart" @buy-now="buyNow" buyNowText="立即购买" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setCartTotalNum } from '@/core/app'
|
||||
import { hex2rgba } from '@/utils/color'
|
||||
import { SpecTypeEnum } from '@/common/enum/goods'
|
||||
import * as CartApi from '@/api/cart'
|
||||
import * as GoodsApi from '@/api/goods'
|
||||
import GoodsSkuPopup from '@/components/goods-sku-popup'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GoodsSkuPopup
|
||||
},
|
||||
props: {
|
||||
// 购物车按钮样式 1 2 3
|
||||
btnStyle: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 是否显示
|
||||
visible: false,
|
||||
// 主商品信息
|
||||
goods: {},
|
||||
// SKU商品信息
|
||||
localdata: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 规格按钮选中时的背景色
|
||||
activedBtnBackgroundColor() {
|
||||
return hex2rgba(this.appTheme.mainBg, 0.1)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 加入购物车事件
|
||||
async handle(goods) {
|
||||
this.goods = goods
|
||||
if (goods.spec_type == SpecTypeEnum.SINGLE.value) {
|
||||
this.singleEvent()
|
||||
}
|
||||
if (goods.spec_type == SpecTypeEnum.MULTI.value) {
|
||||
this.multiEvent()
|
||||
}
|
||||
},
|
||||
|
||||
// 单规格商品事件
|
||||
singleEvent() {
|
||||
const { goods } = this
|
||||
this.addCart({
|
||||
goods_id: goods.goods_id,
|
||||
goods_sku_id: '0',
|
||||
buy_num: 1
|
||||
})
|
||||
},
|
||||
|
||||
// 多规格商品事件
|
||||
async multiEvent() {
|
||||
const app = this
|
||||
const { goods } = app
|
||||
// 获取商品的规格信息
|
||||
const { data: { specData } } = await GoodsApi.specData(goods.goods_id)
|
||||
goods.skuList = specData.skuList
|
||||
goods.specList = specData.specList
|
||||
// 整理SKU商品信息
|
||||
app.localdata = {
|
||||
_id: goods.goods_id,
|
||||
name: goods.goods_name,
|
||||
goods_thumb: goods.goods_image,
|
||||
sku_list: app.getSkuList(),
|
||||
spec_list: app.getSpecList()
|
||||
}
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
// 监听组件显示隐藏
|
||||
onChangeValue(val) {
|
||||
this.visible = val
|
||||
},
|
||||
|
||||
// 整理商品SKU列表 (多规格)
|
||||
getSkuList() {
|
||||
const app = this
|
||||
const { goods: { goods_name, goods_image, skuList } } = app
|
||||
const skuData = []
|
||||
skuList.forEach(item => {
|
||||
skuData.push({
|
||||
_id: item.id,
|
||||
goods_sku_id: item.goods_sku_id,
|
||||
goods_id: item.goods_id,
|
||||
goods_name: goods_name,
|
||||
image: item.image_url ? item.image_url : goods_image,
|
||||
price: item.goods_price * 100,
|
||||
stock: item.stock_num,
|
||||
spec_value_ids: item.spec_value_ids,
|
||||
sku_name_arr: app.getSkuNameArr(item.spec_value_ids)
|
||||
})
|
||||
})
|
||||
return skuData
|
||||
},
|
||||
|
||||
// 获取sku记录的规格值列表
|
||||
getSkuNameArr(specValueIds) {
|
||||
const app = this
|
||||
const defaultData = ['默认']
|
||||
const skuNameArr = []
|
||||
if (specValueIds) {
|
||||
specValueIds.forEach((valueId, groupIndex) => {
|
||||
const specValueName = app.getSpecValueName(valueId, groupIndex)
|
||||
skuNameArr.push(specValueName)
|
||||
})
|
||||
}
|
||||
return skuNameArr.length ? skuNameArr : defaultData
|
||||
},
|
||||
|
||||
// 获取指定的规格值名称
|
||||
getSpecValueName(valueId, groupIndex) {
|
||||
const app = this
|
||||
const { goods: { specList } } = app
|
||||
const res = specList[groupIndex].valueList.find(specValue => {
|
||||
return specValue.spec_value_id == valueId
|
||||
})
|
||||
return res.spec_value
|
||||
},
|
||||
|
||||
// 整理规格数据 (多规格)
|
||||
getSpecList() {
|
||||
const { goods: { specList } } = this
|
||||
const defaultData = [{ name: '默认', list: [{ name: '默认' }] }]
|
||||
const specData = []
|
||||
specList.forEach(group => {
|
||||
const children = []
|
||||
group.valueList.forEach(specValue => {
|
||||
children.push({ name: specValue.spec_value })
|
||||
})
|
||||
specData.push({
|
||||
name: group.spec_name,
|
||||
list: children
|
||||
})
|
||||
})
|
||||
return specData.length ? specData : defaultData
|
||||
},
|
||||
|
||||
// 加入购物车按钮
|
||||
addCart(selectShop) {
|
||||
const app = this
|
||||
const { goods_id, goods_sku_id, buy_num } = selectShop
|
||||
CartApi.add(goods_id, goods_sku_id, buy_num)
|
||||
.then(result => {
|
||||
// 显示成功
|
||||
app.$toast(result.message, 1000, false)
|
||||
// 隐藏当前弹窗
|
||||
app.onChangeValue(false)
|
||||
// 购物车商品总数量
|
||||
const cartTotal = result.data.cartTotal
|
||||
// 缓存购物车数量
|
||||
setCartTotalNum(cartTotal)
|
||||
// 传递给父级
|
||||
app.$emit('addCart', cartTotal)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
56
components/article-video/article-video.vue
Normal file
56
components/article-video/article-video.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<view>
|
||||
<video :style="{width: vWidth + 'px',height: vHeight + 'px'}" :src="src" :poster="poster"></video>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name:"article-video",
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
vWidth: 400,
|
||||
vHeight: 400
|
||||
};
|
||||
},
|
||||
created() {
|
||||
console.error("------------------------------- article-video created-------------------------------------------------");
|
||||
const maxWidth = uni.upx2px(400)
|
||||
const maxHeight = uni.upx2px(400)
|
||||
// 获得长边
|
||||
const wL = this.width > this.height
|
||||
if(wL) {
|
||||
this.vWidth = maxWidth
|
||||
this.vHeight = this.height/this.width * maxWidth
|
||||
}else {
|
||||
this.vHeight = maxHeight
|
||||
this.vWidth = this.width/this.height * maxHeight
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
57
components/avatar-image/index.vue
Executable file
57
components/avatar-image/index.vue
Executable file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<view class="avatar-image">
|
||||
<image class="image"
|
||||
:style="{ width: `${width}rpx`, height: `${width}rpx`, borderWidth: `${borderWidth}rpx`, borderColor: borderColor }"
|
||||
:src="url ? url : '/static/default-avatar.png'"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 90
|
||||
},
|
||||
borderWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-image {
|
||||
.image {
|
||||
display: block;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
208
components/chat-input-bar/chat-input-bar.vue
Normal file
208
components/chat-input-bar/chat-input-bar.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<!-- z-paging聊天输入框 -->
|
||||
|
||||
<template>
|
||||
<view class="chat-input-bar-box">
|
||||
<!-- :style="{'padding-bottom': keyboardHeight + 'px'}" -->
|
||||
<view class="chat-input-bar">
|
||||
<view class="chat-input-container">
|
||||
<input always-embed :adjust-position="false" @blur="onInputBlur" @focus="onInputFocus"
|
||||
class="chat-input" v-model="msg" :cursor-spacing="20" confirm-type="send" type="text"
|
||||
placeholder="请输入内容" @confirm="sendClick">
|
||||
</view>
|
||||
<view class="chat-input-send" @click="openEmoji">
|
||||
<u-icon name="/static/icon/emoji.png" size="28"></u-icon>
|
||||
</view>
|
||||
<view v-show="msg.length == 0" class="chat-input-send" @click="openExt">
|
||||
<u-icon name="plus-circle" size="28"></u-icon>
|
||||
</view>
|
||||
<view v-show="msg.length > 0" class="chat-input-send" @click="sendClick">
|
||||
<u-icon name="/static/icon/send.png" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view v-show="showExt" :style="{height: 100 + 'px'}" class="chat-input-ext">
|
||||
<u-grid :border="false" @click="clickExt">
|
||||
<u-grid-item>
|
||||
<u-icon :customStyle="{paddingTop:20+'rpx'}" name="photo" :size="40"></u-icon>
|
||||
<text class="grid-text">图片</text>
|
||||
</u-grid-item>
|
||||
<!-- <u-grid-item>
|
||||
<u-icon :customStyle="{paddingTop:20+'rpx'}" name="gift" :size="40" ></u-icon>
|
||||
<text class="grid-text">礼物</text>
|
||||
</u-grid-item> -->
|
||||
</u-grid>
|
||||
</view>
|
||||
<view v-show="showEmoji" :style="{height: 200 + 'px'}" class="chat-input-ext">
|
||||
<scroll-view scroll-y="true" style="height: 100%;">
|
||||
<view class="emoj_box">
|
||||
<view class="emoj_box_item" v-for="(item,index) in emojiList" :key="index">
|
||||
<text @click="setEmoj(item,index)" class="emoj_box_img">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<u-safe-bottom></u-safe-bottom>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import emojiList from "./emoji.js"
|
||||
import * as UploadApi from '@/api/upload'
|
||||
export default {
|
||||
name: "chat-input-bar",
|
||||
data() {
|
||||
return {
|
||||
msg: '',
|
||||
showExt: false,
|
||||
showEmoji: false,
|
||||
emojiList
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
sendClick() {
|
||||
if (!this.msg.length) return;
|
||||
this.$emit('send', {
|
||||
content: this.msg,
|
||||
type: 'text'
|
||||
});
|
||||
this.msg = '';
|
||||
},
|
||||
onInputFocus(e) {
|
||||
this.showExt = false
|
||||
this.showEmoji = false
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
})
|
||||
},
|
||||
onInputBlur() {
|
||||
setTimeout(() => {
|
||||
if (!this.showExt && !this.showEmoji) {
|
||||
this.$emit('heightChange')
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
openEmoji() {
|
||||
this.showExt = false
|
||||
this.showEmoji = true
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
})
|
||||
},
|
||||
setEmoj(item) {
|
||||
this.msg = this.msg += item
|
||||
},
|
||||
openExt() {
|
||||
this.showEmoji = false
|
||||
this.showExt = true
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
})
|
||||
},
|
||||
clickExt(index) {
|
||||
if (index === 0) {
|
||||
this.chooseImage()
|
||||
}
|
||||
},
|
||||
chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: (chooseImageRes) => {
|
||||
const tempFilePaths = chooseImageRes.tempFilePaths;
|
||||
UploadApi.uploadFile({
|
||||
filePath: tempFilePaths[0],
|
||||
fileType: 'image',
|
||||
name: 'file'
|
||||
}).then(res => {
|
||||
this.$emit('send', {
|
||||
content: res.data.url,
|
||||
type: 'image'
|
||||
});
|
||||
})
|
||||
this.showExt = false
|
||||
this.showEmoji = false
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-input-bar-box {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.chat-input-ext {
|
||||
/* overflow: hidden; */
|
||||
border-top: solid 1px #f3f3f3;
|
||||
/* background-color: #eeeeee; */
|
||||
}
|
||||
|
||||
.chat-input-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-top: solid 1px #eeeeee;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
padding: 10rpx 15rpx;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
/* #ifndef APP-NVUE */
|
||||
padding: 15rpx;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
padding: 10rpx;
|
||||
/* #endif */
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
margin: 10rpx;
|
||||
border-radius: 50%rpx;
|
||||
padding: 10rpx 0rpx 10rpx 5rpx;
|
||||
|
||||
}
|
||||
|
||||
.chat-input-send-text {
|
||||
color: white;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.emoj_box {
|
||||
padding: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.emoj_box_item{
|
||||
width: 16.66%;
|
||||
text-align: center;
|
||||
}
|
||||
.emoj_box_img {
|
||||
font-size: 60rpx;
|
||||
width: 83rpx;
|
||||
height: 83rpx;
|
||||
text-align: center;
|
||||
line-height: 83rpx;
|
||||
padding: 12rpx;
|
||||
}
|
||||
</style>
|
||||
33
components/chat-input-bar/emoji.js
Normal file
33
components/chat-input-bar/emoji.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default [
|
||||
"😀", "😁", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍",
|
||||
"😘", "😗", "😙", "😚", "☺", "😇", "😐", "😑", "😶", "😏", "😣", "😥", "😮", "😯", "😪",
|
||||
"😫", "😴", "😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "😲", "😷", "😖", "😞", "😟",
|
||||
"😤", "😢", "😭", "😦", "😧", "😨", "😬", "😰", "😱", "😳", "😵", "😡", "😠",
|
||||
"👦", "👧", "👨", "👩", "👴", "👵", "👶", "👱", "👮", "👲", "👳", "👷", "👸", "💂", "🎅", "👰", "👼",
|
||||
"💆", "💇", "🙍", "🙎", "🙅", "🙆", "💁", "🙋", "🙇", "🙌", "🙏", "👤", "👥", "🚶", "🏃", "👯",
|
||||
"💃", "👫", "👬", "👭", "💏", "💑", "👪", "💪", "👈", "👉", "☝", "👆", "👇", "✌", "✋", "👌",
|
||||
"👍", "👎", "✊", "👊", "👋", "👏", "👐", "✍", "👣", "👀", "👂", "👃", "👅", "👄", "💋", "👓",
|
||||
"👔", "👙", "👛", "👜", "👝", "🎒", "💼", "👞", "👟", "👠", "👡", "👢", "👑",
|
||||
"👒", "🎩", "🎓", "💄", "💅", "💍", "🌂", "📶", "📳", "📴", "♻", "🏧","🚮", "🚰", "♿", "🚹", "🚺",
|
||||
"🚻", "🚼", "🚾", "⚠", "🚸", "⛔", "🚫", "🚳", "🚭", "🚯", "🚱", "🚷", "🔞", "💈",
|
||||
"🙈", "🐒", "🐶", "🐕", "🐩", "🐺", "🐱","🐈", "🐯", "🐅", "🐆", "🐴", "🐎", "🐮", "🐂",
|
||||
"🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🐘","🐭",
|
||||
"🐁","🐀","🐹","🐰","🐇","🐻","🐨","🐼","🐾","🐔","🐓","🐣","🐤","🐥",
|
||||
"🐦", "🐧", "🐸", "🐊","🐢", "🐍", "🐲", "🐉", "🐳", "🐋", "🐬", "🐟", "🐠", "🐡",
|
||||
"🐙", "🐚", "🐌", "🐛", "🐜", "🐝", "🐞", "🦋", "💐", "🌸", "💮", "🌹", "🌺",
|
||||
"🌻", "🌼", "🌷", "🌱", "🌲", "🌳", "🌴", "🌵", "🌾", "🌿", "🍀", "🍁", "🍂", "🍃",
|
||||
"🌍","🌎","🌏","🌐","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚",
|
||||
"🌛","🌜","☀","🌝","🌞","⭐","🌟","🌠","☁","⛅","☔","⚡","❄","🔥","💧","🌊",
|
||||
"🏀", "🏈", "🏉", "🎾", "🎱", "🎳", "⛳", "🎣", "🎽", "🎿",
|
||||
"😈", "👿", "👹", "👺", "💀", "☠", "👻", "👽", "👾", "💣",
|
||||
"🌋", "🗻", "🏠", "🏡", "🏢", "🏣", "🏤", "🏥", "🏦", "🏨",
|
||||
"⛲", "🌁", "🌃", "🌆", "🌇", "🎠", "🎡", "🎢", "🚂",
|
||||
"🚌", "🚍", "🚎", "🚏", "🚐", "🚑", "🚒", "🚓", "🚔", "🚕", "🚖", "🚗", "🚘",
|
||||
"💌", "💎", "🔪", "💈", "🚪", "🚽", "🚿", "🛁", "⌛", "⏳", "⌚", "⏰", "🎈", "🎉",
|
||||
"💤", "💢", "💬", "💭", "♨", "🌀", "🔔", "🔕", "✡", "✝", "🔯", "📛", "🔰", "🔱", "⭕", "✅",
|
||||
"☑", "✔", "✖", "❌", "❎", "➕", "➖", "➗", "➰", "➿", "〽", "✳", "✴", "❇", "‼", "⁉", "❓", "❔", "❕", "❗",
|
||||
"🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡",
|
||||
"🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "🕙", "🕥", "🕚", "🕦", "⏱", "⏲", "🕰",
|
||||
"💘", "❤", "💓", "💔", "💕", "💖", "💗", "💙", "💚", "💛", "💜", "💝", "💞", "💟❣",
|
||||
"🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🍎", "🍏", "🍐", "🍑", "🍒", "🍓",
|
||||
]
|
||||
63
components/chat-item-image/chat-item-image.vue
Normal file
63
components/chat-item-image/chat-item-image.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view @click="onClick">
|
||||
<image v-if="widthL" :style="{width, height}" :src="_src" mode="widthFix"></image>
|
||||
<image v-else :style="{width, height}" :src="_src" mode="heightFix"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name:"chat-item-image",
|
||||
props: {
|
||||
src: '',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: '400rpx',
|
||||
height: '400rpx',
|
||||
_src: '',
|
||||
widthL: true
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit("click")
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src: {
|
||||
immediate: true,
|
||||
handler(val){
|
||||
|
||||
uni.getImageInfo({
|
||||
src: val,
|
||||
success: (image)=> {
|
||||
this._src = image.path
|
||||
console.log("image", image);
|
||||
const maxWidth = uni.upx2px(300)
|
||||
const maxHeight = uni.upx2px(300)
|
||||
// 是否越界
|
||||
const wR = image.width < maxWidth
|
||||
const hR = image.height < maxHeight
|
||||
// 获得长边
|
||||
const wL = image.width > image.height
|
||||
console.log(image.width,image.height,wL);
|
||||
if(wR && hR){
|
||||
this.width = image.width + 'px'
|
||||
this.height = image.height + 'px'
|
||||
}else {
|
||||
this.widthL = wL
|
||||
this.width = maxWidth + 'px'
|
||||
this.height = maxHeight + 'px'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
217
components/chat-item/chat-item.vue
Normal file
217
components/chat-item/chat-item.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<!-- z-paging聊天item -->
|
||||
|
||||
<template>
|
||||
<view class="chat-item">
|
||||
<text class="chat-time" v-if="showTime">
|
||||
<!-- {{ item.createTime }} -->
|
||||
{{ time }}
|
||||
</text>
|
||||
<view :class="{'chat-container':true,'chat-location-me':isMe}">
|
||||
<view class="chat-icon-container" @click="$push('sub_pages/member/detail/detail',{userId:item.formUserId})">
|
||||
<u-avatar class="chat-icon" size="40" shape="square" :src="isMe?userInfo.avatar:friendInfo.avatar" mode="aspectFill"></u-avatar>
|
||||
<!-- <image class="chat-icon" :src="isMe?userInfo.avatar:friendInfo.avatar" mode="aspectFill" /> -->
|
||||
</view>
|
||||
<view class="chat-content-container">
|
||||
<text :class="{'chat-user-name':true,'chat-location-me':isMe}">
|
||||
{{isMe?userInfo.nickname:friendInfo.nickname}}
|
||||
</text>
|
||||
<view :class="{'chat-text-container':item.type!='image','isme':isMe, 'chat-image-container': item.type=='image'}">
|
||||
<text v-if="item.type=='text'" :class="{'char-text':true,'char-text-me':isMe}">{{item.content}}</text>
|
||||
<chat-item-image @click="onImageClick" v-if="item.type=='image'" :src="item.content"></chat-item-image>
|
||||
<!-- <image :class="{'char-image':true,'char-image-me':isMe}" mode="widthFix"></image> -->
|
||||
<view v-if="item.type == 'card'" class="hello-card">
|
||||
<view class="hello-card-desc">
|
||||
{{`${ mateData.age ? mateData.age + '岁': '' } ${mateData.position ? mateData.position : ''} ${mateData.city ? mateData.city : ''}`}}
|
||||
</view>
|
||||
<view class="hello-card-desc u-line-2" style="color: #999999; font-size: 26rpx;">
|
||||
<view>很高兴认识你~</view>
|
||||
<view>海底月是天上月,眼前人是心上人!</view>
|
||||
</view>
|
||||
<view class="album" style="padding: 10rpx 0;">
|
||||
<u-album :urls="mateData.files" keyName="thumb" :singleSize="100" singleMode="aspectFill" multipleMode="aspectFill"></u-album>
|
||||
</view>
|
||||
<!-- <image class="hello-card-image" :src="isMe?userInfo.avatar:friendInfo.avatar" mode="widthFix"></image> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
ACCESS_TOKEN, USER_ID,USER_INFO
|
||||
} from '@/store/mutation-types'
|
||||
import storage from '@/utils/storage'
|
||||
import dayjs from "dayjs"
|
||||
import ChatItemImage from "@/components/chat-item-image/chat-item-image.vue"
|
||||
export default {
|
||||
name:"chat-item",
|
||||
components: {ChatItemImage},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {
|
||||
time: '',
|
||||
icon: '',
|
||||
name: '',
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
},
|
||||
friendInfo: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {
|
||||
avatar: '',
|
||||
nickname: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
prevMsgTime: null
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
mateData: {},
|
||||
showTime: true,
|
||||
time: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMe(){
|
||||
console.log(this.item.formUserId == this.userInfo.userId);
|
||||
return this.item.formUserId == this.userInfo.userId
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.userInfo = storage.get(USER_INFO)
|
||||
if(this.item.type == 'card'){
|
||||
this.mateData = JSON.parse(this.item.content)
|
||||
console.log("this.mateData: ",this.mateData);
|
||||
}
|
||||
// 如果是第一条消息或上一条消息间隔三分钟,显示时间
|
||||
this.showTime = !this.prevMsgTime || (dayjs(this.prevMsgTime).diff(dayjs(this.item.createTime), 'minutes') <= -3)
|
||||
if(this.showTime){
|
||||
let isToday = new Date(this.item.createTime).setHours(0,0,0,0) == new Date().setHours(0,0,0,0)
|
||||
this.time = this.$u.timeFormat(this.item.createTime, isToday? 'hh:MM': 'mm-dd hh:MM')
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
onImageClick(){
|
||||
this.$emit("onImageClick")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.chat-time{
|
||||
padding: 4rpx 0rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.chat-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.chat-location-me{
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
}
|
||||
.chat-icon-container{
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.chat-icon{
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
.chat-content-container{
|
||||
margin: 0rpx 15rpx;
|
||||
}
|
||||
.chat-user-name{
|
||||
font-size: 26rpx;
|
||||
color: #888888;
|
||||
}
|
||||
.chat-text-container{
|
||||
display: table;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
max-width: 500rpx;
|
||||
/* #endif */
|
||||
&.isme{
|
||||
display: table;
|
||||
background-color: #ffe3ff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
max-width: 500rpx;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
.hello-card{
|
||||
max-width: 500rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-top: 10rpx;
|
||||
text-align: left;
|
||||
}
|
||||
.hello-card-image{
|
||||
width: 200rpx;
|
||||
}
|
||||
.hello-card-title{
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hello-card-desc{
|
||||
color: #333333;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
.chat-text-container-me{
|
||||
|
||||
|
||||
}
|
||||
|
||||
.chat-image-container{
|
||||
display: table;
|
||||
background-color: unset !important;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
max-width: 500rpx;
|
||||
/* #endif */
|
||||
|
||||
|
||||
}
|
||||
.char-text{
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
max-width: 500rpx;
|
||||
/* #endif */
|
||||
}
|
||||
.char-image{
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
}
|
||||
.char-text-me{
|
||||
/* color: white; */
|
||||
}
|
||||
</style>
|
||||
183
components/countdown/index.vue
Executable file
183
components/countdown/index.vue
Executable file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<view v-if="date" class="count-down">
|
||||
<view :class="[`${theme}-theme`, `separator-${separator}`]">
|
||||
<!-- <block v-if="dynamic.day != '00'">
|
||||
<text class="dynamic-value">{{ dynamic.day }}</text>
|
||||
<text class="separator">{{ separatorText.day }}</text>
|
||||
</block> -->
|
||||
<block v-if="dynamic.day > 0">
|
||||
<text class="dynamic-value">{{ dynamic.day }}</text>
|
||||
<text class="separator">{{ separatorText.day }}</text>
|
||||
</block>
|
||||
<text class="dynamic-value" :style="{ backgroundColor: customBgColor }">{{ dynamic.hou }}</text>
|
||||
<text class="separator">{{ separatorText.hou }}</text>
|
||||
<text class="dynamic-value" :style="{ backgroundColor: customBgColor }">{{ dynamic.min }}</text>
|
||||
<text class="separator">{{ separatorText.min }}</text>
|
||||
<text class="dynamic-value" :style="{ backgroundColor: customBgColor }">{{ dynamic.sec }}</text>
|
||||
<text class="separator">{{ separatorText.sec }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/utils/util';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 截止的时间
|
||||
date: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 分隔符, colon为英文冒号,zh为中文
|
||||
separator: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
},
|
||||
// 组件主题样式, text为纯文本,custom为带背景色
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
// custom样式的背景色
|
||||
customBgColor: {
|
||||
type: String,
|
||||
default: '#252525'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 倒计时数据
|
||||
dynamic: {
|
||||
day: '0',
|
||||
hou: '00',
|
||||
min: '00',
|
||||
sec: '00'
|
||||
},
|
||||
// 分隔符文案
|
||||
separatorText: {
|
||||
day: '天',
|
||||
hou: '时',
|
||||
min: '分',
|
||||
sec: '秒'
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 分隔符文案
|
||||
this.setSeparatorText();
|
||||
// 开始倒计时
|
||||
this.onTime();
|
||||
},
|
||||
methods: {
|
||||
// 分隔符文案
|
||||
setSeparatorText() {
|
||||
const sText = this.separatorText;
|
||||
if (this.separator === 'colon') {
|
||||
sText.day = ':'
|
||||
sText.hou = sText.min = ':'
|
||||
sText.sec = ''
|
||||
}
|
||||
this.separatorText = sText
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
onTime(deep = 0) {
|
||||
const app = this;
|
||||
const dynamic = {};
|
||||
|
||||
// 获取当前时间,同时得到活动结束时间数组
|
||||
const newTime = new Date().getTime();
|
||||
// 对结束时间进行处理渲染到页面
|
||||
const endTime = new Date(formatDate(app.date)).getTime();
|
||||
|
||||
// 如果活动未结束,对时间进行处理
|
||||
if (endTime - newTime <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const diffTime = (endTime - newTime) / 1000;
|
||||
// 获取时、分、秒
|
||||
const day = parseInt(diffTime / 86400),
|
||||
hou = parseInt((diffTime % 86400) / 3600),
|
||||
min = parseInt(((diffTime % 86400) % 3600) / 60),
|
||||
sec = parseInt(((diffTime % 86400) % 3600) % 60);
|
||||
dynamic.day = day;
|
||||
dynamic.hou = app.timeFormat(hou);
|
||||
dynamic.min = app.timeFormat(min);
|
||||
dynamic.sec = app.timeFormat(sec);
|
||||
|
||||
// 渲染,然后每隔一秒执行一次倒计时函数
|
||||
app.dynamic = dynamic;
|
||||
// 判断倒计时是否结束
|
||||
const isEnd = app.isEnd();
|
||||
// 结束后执行回调函数
|
||||
if (isEnd) {
|
||||
deep > 0 && app.$emit('finish');
|
||||
}
|
||||
// 重复执行
|
||||
if (!isEnd) {
|
||||
setTimeout(() => {
|
||||
app.onTime(++deep);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
// 判断倒计时是否结束
|
||||
isEnd() {
|
||||
const { dynamic } = this;
|
||||
return dynamic.day == '00' && dynamic.hou == '00' && dynamic.min == '00' && dynamic.sec == '00';
|
||||
},
|
||||
|
||||
// 小于10的格式化函数
|
||||
timeFormat(value) {
|
||||
return value < 10 ? '0' + value : value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
margin-right: 5px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
background-color: #1989fa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
padding: 0 2rpx;
|
||||
}
|
||||
|
||||
// 纯文本主题
|
||||
.text-theme {
|
||||
|
||||
// 冒号分隔符
|
||||
.separator-colon .separator {
|
||||
padding: 0 5rpx;
|
||||
}
|
||||
|
||||
.dynamic-value {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景主题
|
||||
.custom-theme {
|
||||
.dynamic-value {
|
||||
background: #252525;
|
||||
color: #fff;
|
||||
padding: 2rpx 8rpx;
|
||||
line-height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.separator {
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
components/customer-btn/index.vue
Executable file
77
components/customer-btn/index.vue
Executable file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<button v-if="isShow" class="btn-normal" :open-type="setting.provider == 'mpwxkf' ? 'contact' : ''" @click="handleContact()">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingKeyEnum from '@/common/enum/setting/Key'
|
||||
import SettingModel from '@/common/model/Setting'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
setting: {}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// 是否显示在线客服按钮
|
||||
this.isShow = await SettingModel.isShowCustomerBtn()
|
||||
// 商城客服设置
|
||||
this.setting = await SettingModel.item(SettingKeyEnum.CUSTOMER.value, true)
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 在线客服
|
||||
handleContact() {
|
||||
const app = this
|
||||
const { setting } = app
|
||||
// 企业微信客服
|
||||
if (setting.provider == 'wxqykf') {
|
||||
if (!setting.config.wxqykf.url || !setting.config.wxqykf.corpId) {
|
||||
this.$toast('客服链接和企业ID不能为空')
|
||||
return
|
||||
}
|
||||
// #ifdef H5
|
||||
window.open(setting.config.wxqykf.url)
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.openCustomerServiceChat({
|
||||
extInfo: { url: setting.config.wxqykf.url },
|
||||
corpId: setting.config.wxqykf.corpId,
|
||||
success(res) {}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
uni.share({
|
||||
provider: 'weixin',
|
||||
scene: 'WXSceneSession',
|
||||
type: 0,
|
||||
openCustomerServiceChat: true,
|
||||
corpid: setting.config.wxqykf.corpId,
|
||||
customerUrl: setting.config.wxqykf.url,
|
||||
success(res) {
|
||||
console.log("success:" + JSON.stringify(res));
|
||||
},
|
||||
fail(e) {
|
||||
console.log('errCode', e.errCode, e)
|
||||
if (e.errCode === -8) {
|
||||
app.$toast('很抱歉,您的手机没有安装微信~')
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
66
components/empty/index.vue
Executable file
66
components/empty/index.vue
Executable file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<view v-if="!isLoading" class="empty-content" :style="customStyle">
|
||||
<view class="empty-icon">
|
||||
<image class="image" src="/static/empty.png" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="tips">{{ tips }}</view>
|
||||
<slot name="slot"></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
// 正在加载
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 自定义样式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 提示的问题
|
||||
tips: {
|
||||
type: String,
|
||||
default: '亲,暂无相关数据'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-content {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 140rpx 50rpx;
|
||||
text-align: center;
|
||||
|
||||
.tips {
|
||||
font-size: 28rpx;
|
||||
color: gray;
|
||||
margin: 50rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon .image {
|
||||
width: 280rpx;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
157
components/goods-poster-popup/index.vue
Executable file
157
components/goods-poster-popup/index.vue
Executable file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<u-popup v-model="show" mode="center" :maskCloseAble="false" :closeable="true"
|
||||
:maskCustomStyle="{ background: 'rgba(0, 0, 0, 0.5)' }" border-radius="18" :z-index="12" @close="onClose()">
|
||||
|
||||
<view class="pop-poster pop-example__container">
|
||||
<view class="image__container" @click="handlePreviewImage()">
|
||||
<image v-if="imageUrl" class="image" mode="scaleToFill" :src="imageUrl"></image>
|
||||
</view>
|
||||
<view class="save-btn__container">
|
||||
<view class="save-btn" @click="handleDownload()">保存海报图</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'goods-poster-popup',
|
||||
props: {
|
||||
// true 组件显示 false 组件隐藏
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 获取海报图的api方法
|
||||
apiCall: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
// 获取海报图的api参数
|
||||
apiParam: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// 监听海报图弹层显示隐藏
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
val && this.onShowPopup()
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 是否显示弹窗
|
||||
show: false,
|
||||
// 图片url地址
|
||||
imageUrl: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 显示海报弹窗
|
||||
onShowPopup() {
|
||||
const app = this
|
||||
app.apiCall({ ...app.apiParam, channel: app.platform })
|
||||
.then(result => {
|
||||
app.imageUrl = result.data.imageUrl
|
||||
app.show = true
|
||||
})
|
||||
.catch(err => app.onClose())
|
||||
},
|
||||
|
||||
// 关闭弹窗
|
||||
onClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
|
||||
// 预览图片
|
||||
handlePreviewImage() {
|
||||
uni.previewImage({ urls: [this.imageUrl] })
|
||||
},
|
||||
|
||||
// 保存海报图片
|
||||
handleDownload() {
|
||||
const app = this
|
||||
uni.showLoading({ title: '加载中' })
|
||||
// 下载海报图片
|
||||
uni.downloadFile({
|
||||
url: app.imageUrl,
|
||||
success(res) {
|
||||
console.log(res)
|
||||
uni.hideLoading()
|
||||
// 图片保存到相册
|
||||
app.onSaveImage(res.tempFilePath)
|
||||
},
|
||||
fail(res) {
|
||||
console.log('fail', res)
|
||||
uni.hideLoading()
|
||||
app.$toast('很抱歉,自动保存失败 请点击图片后长按手动保存', 3000)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 图片保存到相册
|
||||
onSaveImage(filePath) {
|
||||
const app = this
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success(data) {
|
||||
app.$success('保存成功')
|
||||
// 关闭弹窗
|
||||
app.onClose()
|
||||
},
|
||||
fail(err) {
|
||||
console.log(err.errMsg)
|
||||
if (err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
|
||||
app.$toast('请允许访问相册后重试 (右上角菜单 - 设置 - 相册)', 3000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pop-poster {
|
||||
width: 560rpx;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
padding: 76rpx 76rpx 40rpx 76rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
// 图片容器
|
||||
.image__container {
|
||||
.image {
|
||||
display: block;
|
||||
width: 420rpx;
|
||||
height: 636rpx;
|
||||
box-shadow: 0 0 25rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存按钮
|
||||
.save-btn__container {
|
||||
margin-top: 30rpx;
|
||||
|
||||
.save-btn {
|
||||
color: rgb(255, 255, 255);
|
||||
color: $main-text;
|
||||
background: linear-gradient(to right, $main-bg, $main-bg2);
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
border-radius: 38rpx;
|
||||
height: 76rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1367
components/goods-sku-popup/index.vue
Executable file
1367
components/goods-sku-popup/index.vue
Executable file
File diff suppressed because it is too large
Load Diff
450
components/goods-sku-popup/number-box/index.vue
Executable file
450
components/goods-sku-popup/number-box/index.vue
Executable file
@@ -0,0 +1,450 @@
|
||||
<!-- 步进器 -->
|
||||
<template>
|
||||
<view class="number-box">
|
||||
<view class="u-icon-minus" @touchstart.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal <= min }"
|
||||
:style="{
|
||||
background: bgColor,
|
||||
height: inputHeight + 'rpx',
|
||||
color: color,
|
||||
fontSize: size + 'rpx',
|
||||
minHeight: '1.4em'
|
||||
}">
|
||||
<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">-</view>
|
||||
</view>
|
||||
<input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" :class="{ 'u-input-disabled': disabled }"
|
||||
v-model="inputVal" class="u-number-input" @blur="onBlur"
|
||||
type="number" :style="{
|
||||
color: color,
|
||||
fontSize: size + 'rpx',
|
||||
background: bgColor,
|
||||
height: inputHeight + 'rpx',
|
||||
width: inputWidth + 'rpx',
|
||||
}" />
|
||||
<view class="u-icon-plus" @touchstart.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal >= max }"
|
||||
:style="{
|
||||
background: bgColor,
|
||||
height: inputHeight + 'rpx',
|
||||
color: color,
|
||||
fontSize: size + 'rpx',
|
||||
minHeight: '1.4em',
|
||||
}">
|
||||
<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* numberBox 步进器
|
||||
* @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入
|
||||
* @tutorial https://www.uviewui.com/components/numberBox.html
|
||||
* @property {Number} value 输入框初始值(默认1)
|
||||
* @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5)
|
||||
* @property {Number} min 用户可输入的最小值(默认0)
|
||||
* @property {Number} max 用户可输入的最大值(默认99999)
|
||||
* @property {Number} step 步长,每次加或减的值(默认1)
|
||||
* @property {Number} stepFirst 步进值,首次增加或最后减的值(默认step值和一致)
|
||||
* @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false)
|
||||
* @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false)
|
||||
* @property {Boolean} positive-integer 是否只能输入正整数(默认true)
|
||||
* @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26)
|
||||
* @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233)
|
||||
* @property {String | Number} input-width 输入框宽度,单位rpx(默认80)
|
||||
* @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50)
|
||||
* @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
|
||||
* @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
|
||||
* @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250)
|
||||
* @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200)
|
||||
* @event {Function} change 输入框内容发生变化时触发,对象形式
|
||||
* @event {Function} blur 输入框失去焦点时触发,对象形式
|
||||
* @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式
|
||||
* @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式
|
||||
* @example <number-box :min="1" :max="100"></number-box>
|
||||
*/
|
||||
export default {
|
||||
name: "NumberBox",
|
||||
emits: ["update:modelValue", "input", "change", "blur", "plus", "minus"],
|
||||
props: {
|
||||
// 预显示的数字
|
||||
value: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#F2F3F5'
|
||||
},
|
||||
// 最小值
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 最大值
|
||||
max: {
|
||||
type: Number,
|
||||
default: 99999
|
||||
},
|
||||
// 步进值,每次加或减的值
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 步进值,首次增加或最后减的值
|
||||
stepFirst: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 是否只能输入 step 的倍数
|
||||
stepStrictly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否禁用加减操作
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// input的字体大小,单位rpx
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 26
|
||||
},
|
||||
// 加减图标的颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#323233'
|
||||
},
|
||||
// input宽度,单位rpx
|
||||
inputWidth: {
|
||||
type: [Number, String],
|
||||
default: 80
|
||||
},
|
||||
// input高度,单位rpx
|
||||
inputHeight: {
|
||||
type: [Number, String],
|
||||
default: 50
|
||||
},
|
||||
// index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可
|
||||
index: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话
|
||||
// 设置disabled为false,disabledInput为true即可
|
||||
disabledInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 输入框于键盘之间的距离
|
||||
cursorSpacing: {
|
||||
type: [Number, String],
|
||||
default: 100
|
||||
},
|
||||
// 是否开启长按连续递增或递减
|
||||
longPress: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 开启长按触发后,每触发一次需要多久
|
||||
pressTime: {
|
||||
type: [Number, String],
|
||||
default: 250
|
||||
},
|
||||
// 是否只能输入大于或等于0的整数(正整数)
|
||||
positiveInteger: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(v1, v2) {
|
||||
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
|
||||
if(!this.changeFromInner) {
|
||||
this.inputVal = v1;
|
||||
// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
|
||||
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
|
||||
// 将changeFromInner设置为false
|
||||
this.$nextTick(function(){
|
||||
this.changeFromInner = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
modelValue(v1, v2) {
|
||||
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
|
||||
if(!this.changeFromInner) {
|
||||
this.inputVal = v1;
|
||||
// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
|
||||
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
|
||||
// 将changeFromInner设置为false
|
||||
this.$nextTick(function(){
|
||||
this.changeFromInner = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
inputVal(v1, v2) {
|
||||
// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
|
||||
if (v1 == '') return;
|
||||
let value = 0;
|
||||
// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
|
||||
let tmp = this.isNumber(v1);
|
||||
if (tmp && v1 >= this.min && v1 <= this.max) value = v1;
|
||||
else value = v2;
|
||||
// 判断是否只能输入大于等于0的整数
|
||||
if(this.positiveInteger) {
|
||||
// 小于0,或者带有小数点,
|
||||
if(v1 < 0 || String(v1).indexOf('.') !== -1) {
|
||||
value = v2;
|
||||
// 双向绑定input的值,必须要使用$nextTick修改显示的值
|
||||
this.$nextTick(() => {
|
||||
this.inputVal = v2;
|
||||
})
|
||||
}
|
||||
}
|
||||
// 发出change事件
|
||||
this.handleChange(value, 'change');
|
||||
},
|
||||
min(v1){
|
||||
if(v1 !== undefined && v1!="" && this.getValue() < v1){
|
||||
this.$emit("input",v1);
|
||||
}
|
||||
},
|
||||
max(v1){
|
||||
if(v1 !== undefined && v1!="" && this.getValue() > v1){
|
||||
this.$emit("input",v1);
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态
|
||||
timer: null, // 用作长按的定时器
|
||||
changeFromInner: false, // 值发生变化,是来自内部还是外部
|
||||
innerChangeTimer: null, // 内部定时器
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.inputVal = Number(this.getValue());
|
||||
},
|
||||
computed: {
|
||||
getCursorSpacing() {
|
||||
// 先将值转为px单位,再转为数值
|
||||
return Number(uni.upx2px(this.cursorSpacing));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValue(){
|
||||
// #ifndef VUE3
|
||||
return this.value;
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
return this.modelValue;
|
||||
// #endif
|
||||
},
|
||||
// 点击退格键
|
||||
btnTouchStart(callback) {
|
||||
// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
|
||||
this[callback]();
|
||||
// 如果没开启长按功能,直接返回
|
||||
if (!this.longPress) return;
|
||||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
|
||||
this.timer = null;
|
||||
this.timer = setInterval(() => {
|
||||
// 执行加或减函数
|
||||
this[callback]();
|
||||
}, this.pressTime);
|
||||
},
|
||||
clearTimer() {
|
||||
this.$nextTick(() => {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
})
|
||||
},
|
||||
minus() {
|
||||
this.computeVal('minus');
|
||||
},
|
||||
plus() {
|
||||
this.computeVal('plus');
|
||||
},
|
||||
// 为了保证小数相加减出现精度溢出的问题
|
||||
calcPlus(num1, num2) {
|
||||
let baseNum, baseNum1, baseNum2;
|
||||
try {
|
||||
baseNum1 = num1.toString().split('.')[1].length;
|
||||
} catch (e) {
|
||||
baseNum1 = 0;
|
||||
}
|
||||
try {
|
||||
baseNum2 = num2.toString().split('.')[1].length;
|
||||
} catch (e) {
|
||||
baseNum2 = 0;
|
||||
}
|
||||
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
|
||||
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度
|
||||
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision);
|
||||
},
|
||||
// 为了保证小数相加减出现精度溢出的问题
|
||||
calcMinus(num1, num2) {
|
||||
let baseNum, baseNum1, baseNum2;
|
||||
try {
|
||||
baseNum1 = num1.toString().split('.')[1].length;
|
||||
} catch (e) {
|
||||
baseNum1 = 0;
|
||||
}
|
||||
try {
|
||||
baseNum2 = num2.toString().split('.')[1].length;
|
||||
} catch (e) {
|
||||
baseNum2 = 0;
|
||||
}
|
||||
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
|
||||
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2;
|
||||
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision);
|
||||
},
|
||||
computeVal(type) {
|
||||
uni.hideKeyboard();
|
||||
if (this.disabled) return;
|
||||
let value = 0;
|
||||
// 新增stepFirst开始
|
||||
// 减
|
||||
if (type === 'minus') {
|
||||
if(this.stepFirst > 0 && this.inputVal == this.stepFirst){
|
||||
value = this.min;
|
||||
}else{
|
||||
value = this.calcMinus(this.inputVal, this.step);
|
||||
}
|
||||
} else if (type === 'plus') {
|
||||
if(this.stepFirst > 0 && this.inputVal < this.stepFirst){
|
||||
value = this.stepFirst;
|
||||
}else{
|
||||
value = this.calcPlus(this.inputVal, this.step);
|
||||
}
|
||||
}
|
||||
if(this.stepStrictly){
|
||||
let strictly = value % this.step;
|
||||
if(strictly > 0){
|
||||
value -= strictly;
|
||||
}
|
||||
}
|
||||
if (value > this.max ) {
|
||||
value = this.max;
|
||||
}else if (value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
// 新增stepFirst结束
|
||||
this.inputVal = value;
|
||||
this.handleChange(value, type);
|
||||
},
|
||||
// 处理用户手动输入的情况
|
||||
onBlur(event) {
|
||||
let val = 0;
|
||||
let value = event.detail.value;
|
||||
// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
|
||||
// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
|
||||
if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min;
|
||||
val = +value;
|
||||
|
||||
// 新增stepFirst开始
|
||||
if(this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal>0){
|
||||
val = this.stepFirst;
|
||||
}
|
||||
// 新增stepFirst结束
|
||||
if(this.stepStrictly){
|
||||
let strictly = val % this.step;
|
||||
if(strictly > 0){
|
||||
val -= strictly;
|
||||
}
|
||||
}
|
||||
if (val > this.max) {
|
||||
val = this.max;
|
||||
} else if (val < this.min) {
|
||||
val = this.min;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.inputVal = val;
|
||||
})
|
||||
this.handleChange(val, 'blur');
|
||||
},
|
||||
handleChange(value, type) {
|
||||
if (this.disabled) return;
|
||||
// 清除定时器,避免造成混乱
|
||||
if(this.innerChangeTimer) {
|
||||
clearTimeout(this.innerChangeTimer);
|
||||
this.innerChangeTimer = null;
|
||||
}
|
||||
// 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果
|
||||
this.changeFromInner = true;
|
||||
// 一定时间内,清除changeFromInner标记,否则内部值改变后
|
||||
// 外部通过程序修改value值,将会无效
|
||||
this.innerChangeTimer = setTimeout(() => {
|
||||
this.changeFromInner = false;
|
||||
}, 150);
|
||||
this.$emit('input', Number(value));
|
||||
this.$emit("update:modelValue", Number(value));
|
||||
this.$emit(type, {
|
||||
// 转为Number类型
|
||||
value: Number(value),
|
||||
index: this.index
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 验证十进制数字
|
||||
*/
|
||||
isNumber(value) {
|
||||
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.number-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-number-input {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin: 0 6rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-icon-plus,
|
||||
.u-icon-minus {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-icon-plus {
|
||||
border-radius: 0 8rpx 8rpx 0;
|
||||
}
|
||||
|
||||
.u-icon-minus {
|
||||
border-radius: 8rpx 0 0 8rpx;
|
||||
}
|
||||
|
||||
.u-icon-disabled {
|
||||
color: #c8c9cc !important;
|
||||
background: #f7f8fa !important;
|
||||
}
|
||||
|
||||
.u-input-disabled {
|
||||
color: #c8c9cc !important;
|
||||
background-color: #f2f3f5 !important;
|
||||
}
|
||||
.num-btn{
|
||||
font-weight:550;
|
||||
position: relative;
|
||||
top:-4rpx;
|
||||
}
|
||||
|
||||
</style>
|
||||
645
components/jyf-parser/jyf-parser.vue
Executable file
645
components/jyf-parser/jyf-parser.vue
Executable file
@@ -0,0 +1,645 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot v-if="!nodes.length" />
|
||||
<!--#ifdef APP-PLUS-NVUE-->
|
||||
<web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
|
||||
<!--#endif-->
|
||||
<!--#ifndef APP-PLUS-NVUE-->
|
||||
<view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
|
||||
<!--#ifdef H5 || MP-360-->
|
||||
<div :id="'rtf'+uid"></div>
|
||||
<!--#endif-->
|
||||
<!--#ifndef H5 || MP-360-->
|
||||
<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
|
||||
<!--#endif-->
|
||||
</view>
|
||||
<!--#endif-->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
var search;
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
import trees from './libs/trees';
|
||||
var cache = {},
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
|
||||
// #endif
|
||||
Parser = require('./libs/MpHtmlParser.js');
|
||||
var dom;
|
||||
// 计算 cache 的 key
|
||||
function hash(str) {
|
||||
for (var i = str.length, val = 5381; i--;)
|
||||
val += (val << 5) + str.charCodeAt(i);
|
||||
return val;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5 || APP-PLUS-NVUE || MP-360
|
||||
var {
|
||||
windowWidth,
|
||||
platform
|
||||
} = uni.getSystemInfoSync(),
|
||||
cfg = require('./libs/config.js');
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
var weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
/**
|
||||
* Parser 富文本组件
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @property {String} html 富文本数据
|
||||
* @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
|
||||
* @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
|
||||
* @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
|
||||
* @property {Number} compress 压缩等级
|
||||
* @property {String} domain 图片、视频等链接的主域名
|
||||
* @property {Boolean} lazyLoad 是否开启图片懒加载
|
||||
* @property {String} loadingImg 图片加载完成前的占位图
|
||||
* @property {Boolean} selectable 是否开启长按复制
|
||||
* @property {Object} tagStyle 标签的默认样式
|
||||
* @property {Boolean} showWithAnimation 是否使用渐显动画
|
||||
* @property {Boolean} useAnchor 是否使用锚点
|
||||
* @property {Boolean} useCache 是否缓存解析结果
|
||||
* @event {Function} parse 解析完成事件
|
||||
* @event {Function} load dom 加载完成事件
|
||||
* @event {Function} ready 所有图片加载完毕事件
|
||||
* @event {Function} error 错误事件
|
||||
* @event {Function} imgtap 图片点击事件
|
||||
* @event {Function} linkpress 链接点击事件
|
||||
* @author JinYufeng
|
||||
* @version 20201029
|
||||
* @listens MIT
|
||||
*/
|
||||
export default {
|
||||
name: 'parser',
|
||||
data() {
|
||||
return {
|
||||
// #ifdef H5 || MP-360
|
||||
uid: this._uid,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 1,
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
showAm: '',
|
||||
// #endif
|
||||
nodes: []
|
||||
}
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
// #endif
|
||||
props: {
|
||||
html: String,
|
||||
autopause: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoscroll: Boolean,
|
||||
autosetTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
compress: Number,
|
||||
loadingImg: String,
|
||||
useCache: Boolean,
|
||||
// #endif
|
||||
domain: String,
|
||||
lazyLoad: Boolean,
|
||||
selectable: Boolean,
|
||||
tagStyle: Object,
|
||||
showWithAnimation: Boolean,
|
||||
useAnchor: Boolean
|
||||
},
|
||||
watch: {
|
||||
html(html) {
|
||||
this.setContent(html);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 图片数组
|
||||
this.imgList = [];
|
||||
this.imgList.each = function(f) {
|
||||
for (var i = 0, len = this.length; i < len; i++)
|
||||
this.setItem(i, f(this[i], i, this));
|
||||
}
|
||||
this.imgList.setItem = function(i, src) {
|
||||
if (i == void 0 || !src) return;
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
// 去重
|
||||
if (src.indexOf('http') == 0 && this.includes(src)) {
|
||||
var newSrc = src.split('://')[0];
|
||||
for (var j = newSrc.length, c; c = src[j]; j++) {
|
||||
if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
|
||||
newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
|
||||
}
|
||||
newSrc += src.substr(j);
|
||||
return this[i] = newSrc;
|
||||
}
|
||||
// #endif
|
||||
this[i] = src;
|
||||
// 暂存 data src
|
||||
if (src.includes('data:image')) {
|
||||
var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
|
||||
if (!info) return;
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
|
||||
fs && fs.writeFile({
|
||||
filePath,
|
||||
data: info[3],
|
||||
encoding: info[2],
|
||||
success: () => this[i] = filePath
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
|
||||
var bitmap = new plus.nativeObj.Bitmap();
|
||||
bitmap.loadBase64Data(src, () => {
|
||||
bitmap.save(filePath, {}, () => {
|
||||
bitmap.clear()
|
||||
this[i] = filePath;
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef H5 || MP-360
|
||||
this.document = document.getElementById('rtf' + this._uid);
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
if (dom) this.document = new dom(this);
|
||||
// #endif
|
||||
if (search) this.search = args => search(this, args);
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.document = this.$refs.web;
|
||||
setTimeout(() => {
|
||||
// #endif
|
||||
if (this.html) this.setContent(this.html);
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
}, 30)
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5 || MP-360
|
||||
if (this._observer) this._observer.disconnect();
|
||||
// #endif
|
||||
this.imgList.each(src => {
|
||||
// #ifdef APP-PLUS
|
||||
if (src && src.includes('_doc')) {
|
||||
plus.io.resolveLocalFileSystemURL(src, entry => {
|
||||
entry.remove();
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
if (src && src.includes(uni.env.USER_DATA_PATH))
|
||||
fs && fs.unlink({
|
||||
filePath: src
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
clearInterval(this._timer);
|
||||
},
|
||||
methods: {
|
||||
// 设置富文本内容
|
||||
setContent(html, append) {
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!html)
|
||||
return this.height = 1;
|
||||
if (append)
|
||||
this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
|
||||
"';document.getElementById('parser').appendChild(b)");
|
||||
else {
|
||||
html =
|
||||
'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
|
||||
this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
|
||||
'</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
|
||||
(this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
|
||||
'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
|
||||
if (platform == 'android') html = html.replace(/%/g, '%25');
|
||||
this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
|
||||
}
|
||||
this.$refs.web.evalJs(
|
||||
'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
|
||||
windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
|
||||
'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.getAttribute("original-src")||a.src||a.getAttribute("data-src")),a.onclick=function(t){t.stopPropagation(),e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(m){m.stopPropagation();var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
|
||||
(this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
|
||||
';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
|
||||
(this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
|
||||
'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
|
||||
)
|
||||
this.nodes = [1];
|
||||
// #endif
|
||||
// #ifdef H5 || MP-360
|
||||
if (!html) {
|
||||
if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
|
||||
return;
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
if (!append) {
|
||||
if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
|
||||
this.rtf = div;
|
||||
} else {
|
||||
if (!this.rtf) this.rtf = div;
|
||||
else this.rtf.appendChild(div);
|
||||
}
|
||||
div.innerHTML = this._handleHtml(html, append);
|
||||
for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
|
||||
style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
|
||||
style.setAttribute('scoped', 'true');
|
||||
}
|
||||
// 懒加载
|
||||
if (!this._observer && this.lazyLoad && IntersectionObserver) {
|
||||
this._observer = new IntersectionObserver(changes => {
|
||||
for (let item, i = 0; item = changes[i++];) {
|
||||
if (item.isIntersecting) {
|
||||
item.target.src = item.target.getAttribute('data-src');
|
||||
item.target.removeAttribute('data-src');
|
||||
this._observer.unobserve(item.target);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
rootMargin: '500px 0px 500px 0px'
|
||||
})
|
||||
}
|
||||
var _ts = this;
|
||||
// 获取标题
|
||||
var title = this.rtf.getElementsByTagName('title');
|
||||
if (title.length && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: title[0].innerText
|
||||
})
|
||||
// 填充 domain
|
||||
var fill = target => {
|
||||
var src = target.getAttribute('src');
|
||||
if (this.domain && src) {
|
||||
if (src[0] == '/') {
|
||||
if (src[1] == '/')
|
||||
target.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
|
||||
else target.src = this.domain + src;
|
||||
} else if (!src.includes('://') && src.indexOf('data:') != 0) target.src = this.domain + '/' + src;
|
||||
}
|
||||
}
|
||||
// 图片处理
|
||||
this.imgList.length = 0;
|
||||
var imgs = this.rtf.getElementsByTagName('img');
|
||||
for (let i = 0, j = 0, img; img = imgs[i]; i++) {
|
||||
if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
|
||||
img.style.height = 'auto';
|
||||
fill(img);
|
||||
if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
|
||||
img.i = j++;
|
||||
_ts.imgList.push(img.getAttribute('original-src') || img.src || img.getAttribute('data-src'));
|
||||
img.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var preview = true;
|
||||
this.ignore = () => preview = false;
|
||||
_ts.$emit('imgtap', this);
|
||||
if (preview) {
|
||||
// uni.previewImage({
|
||||
// current: this.i,
|
||||
// urls: _ts.imgList
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
img.onerror = function() {
|
||||
if (cfg.errorImg)
|
||||
_ts.imgList[this.i] = this.src = cfg.errorImg;
|
||||
_ts.$emit('error', {
|
||||
source: 'img',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
|
||||
img.setAttribute('data-src', img.src);
|
||||
img.removeAttribute('src');
|
||||
this._observer.observe(img);
|
||||
}
|
||||
}
|
||||
// 链接处理
|
||||
var links = this.rtf.getElementsByTagName('a');
|
||||
for (var link of links) {
|
||||
link.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var jump = true,
|
||||
href = this.getAttribute('href');
|
||||
_ts.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
});
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (_ts.useAnchor) {
|
||||
_ts.navigateTo({
|
||||
id: href.substr(1)
|
||||
})
|
||||
}
|
||||
} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
|
||||
return true;
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 视频处理
|
||||
var videos = this.rtf.getElementsByTagName('video');
|
||||
_ts.videoContexts = videos;
|
||||
for (let video, i = 0; video = videos[i++];) {
|
||||
fill(video);
|
||||
video.style.maxWidth = '100%';
|
||||
video.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'video',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
video.onplay = function() {
|
||||
if (_ts.autopause)
|
||||
for (let item, i = 0; item = _ts.videoContexts[i++];)
|
||||
if (item != this) item.pause();
|
||||
}
|
||||
}
|
||||
// 音频处理
|
||||
var audios = this.rtf.getElementsByTagName('audio');
|
||||
for (var audio of audios) {
|
||||
fill(audio);
|
||||
audio.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'audio',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
}
|
||||
// 表格处理
|
||||
if (this.autoscroll) {
|
||||
var tables = this.rtf.getElementsByTagName('table');
|
||||
for (var table of tables) {
|
||||
let div = document.createElement('div');
|
||||
div.style.overflow = 'scroll';
|
||||
table.parentNode.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
}
|
||||
}
|
||||
if (!append) this.document.appendChild(this.rtf);
|
||||
this.$nextTick(() => {
|
||||
this.nodes = [1];
|
||||
this.$emit('load');
|
||||
});
|
||||
setTimeout(() => this.showAm = '', 500);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
// #ifndef H5 || MP-360
|
||||
var nodes;
|
||||
if (!html) return this.nodes = [];
|
||||
var parser = new Parser(html, this);
|
||||
// 缓存读取
|
||||
if (this.useCache) {
|
||||
var hashVal = hash(html);
|
||||
if (cache[hashVal])
|
||||
nodes = cache[hashVal];
|
||||
else {
|
||||
nodes = parser.parse();
|
||||
cache[hashVal] = nodes;
|
||||
}
|
||||
} else nodes = parser.parse();
|
||||
this.$emit('parse', nodes);
|
||||
if (append) this.nodes = this.nodes.concat(nodes);
|
||||
else this.nodes = nodes;
|
||||
if (nodes.length && nodes.title && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: nodes.title
|
||||
})
|
||||
if (this.imgList) this.imgList.length = 0;
|
||||
this.videoContexts = [];
|
||||
this.$nextTick(() => {
|
||||
(function f(cs) {
|
||||
for (var i = cs.length; i--;) {
|
||||
if (cs[i].top) {
|
||||
cs[i].controls = [];
|
||||
cs[i].init();
|
||||
f(cs[i].$children);
|
||||
}
|
||||
}
|
||||
})(this.$children)
|
||||
this.$emit('load');
|
||||
})
|
||||
// #endif
|
||||
var height;
|
||||
clearInterval(this._timer);
|
||||
this._timer = setInterval(() => {
|
||||
// #ifdef H5 || MP-360
|
||||
this.rect = this.rtf.getBoundingClientRect();
|
||||
// #endif
|
||||
// #ifndef H5 || MP-360
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select('#_top').boundingClientRect().exec(res => {
|
||||
if (!res) return;
|
||||
this.rect = res[0];
|
||||
// #endif
|
||||
if (this.rect.height == height) {
|
||||
this.$emit('ready', this.rect)
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
height = this.rect.height;
|
||||
// #ifndef H5 || MP-360
|
||||
});
|
||||
// #endif
|
||||
}, 350);
|
||||
if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
|
||||
// #endif
|
||||
},
|
||||
// 获取文本内容
|
||||
getText(ns = this.nodes) {
|
||||
var txt = '';
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
txt = this._text;
|
||||
// #endif
|
||||
// #ifdef H5 || MP-360
|
||||
txt = this.rtf.innerText;
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
for (var i = 0, n; n = ns[i++];) {
|
||||
if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
else if (n.type == 'br') txt += '\n';
|
||||
else {
|
||||
// 块级标签前后加换行
|
||||
var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
|
||||
'0' && n.name[1] < '7');
|
||||
if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
if (n.children) txt += this.getText(n.children);
|
||||
if (block && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
else if (n.name == 'td' || n.name == 'th') txt += '\t';
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
return txt;
|
||||
},
|
||||
// 锚点跳转
|
||||
in (obj) {
|
||||
if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
|
||||
},
|
||||
navigateTo(obj) {
|
||||
if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!obj.id)
|
||||
weexDom.scrollToElement(this.$refs.web);
|
||||
else
|
||||
this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
|
||||
'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
|
||||
obj.success && obj.success();
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
var d = ' ';
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
d = '>>>';
|
||||
// #endif
|
||||
var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
|
||||
'#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
|
||||
if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
|
||||
else selector.selectViewport().scrollOffset();
|
||||
selector.exec(res => {
|
||||
if (!res[0]) return obj.fail && obj.fail('Label not found')
|
||||
var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
|
||||
if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
|
||||
else uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
obj.success && obj.success();
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 获取视频对象
|
||||
getVideoContext(id) {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
if (!id) return this.videoContexts;
|
||||
else
|
||||
for (var i = this.videoContexts.length; i--;)
|
||||
if (this.videoContexts[i].id == id) return this.videoContexts[i];
|
||||
// #endif
|
||||
},
|
||||
// #ifdef H5 || APP-PLUS-NVUE || MP-360
|
||||
_handleHtml(html, append) {
|
||||
if (!append) {
|
||||
// 处理 tag-style 和 userAgentStyles
|
||||
var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%;display:block}';
|
||||
for (var item in cfg.userAgentStyles)
|
||||
style += `${item}{${cfg.userAgentStyles[item]}}`;
|
||||
for (item in this.tagStyle)
|
||||
style += `${item}{${this.tagStyle[item]}}`;
|
||||
style += '</style>';
|
||||
html = style + html;
|
||||
}
|
||||
// 处理 rpx
|
||||
if (html.includes('rpx'))
|
||||
html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
|
||||
return html;
|
||||
},
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
_message(e) {
|
||||
// 接收 web-view 消息
|
||||
var d = e.detail.data[0];
|
||||
switch (d.action) {
|
||||
case 'load':
|
||||
this.$emit('load');
|
||||
this.height = d.height;
|
||||
this._text = d.text;
|
||||
break;
|
||||
case 'getTitle':
|
||||
if (this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: d.title
|
||||
})
|
||||
break;
|
||||
case 'getImgList':
|
||||
this.imgList.length = 0;
|
||||
for (var i = d.imgList.length; i--;)
|
||||
this.imgList.setItem(i, d.imgList[i]);
|
||||
break;
|
||||
case 'preview':
|
||||
var preview = true;
|
||||
d.img.ignore = () => preview = false;
|
||||
this.$emit('imgtap', d.img);
|
||||
if (preview)
|
||||
// uni.previewImage({
|
||||
// current: d.img.i,
|
||||
// urls: this.imgList
|
||||
// })
|
||||
break;
|
||||
case 'linkpress':
|
||||
var jump = true,
|
||||
href = d.href;
|
||||
this.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
})
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (this.useAnchor)
|
||||
weexDom.scrollToElement(this.$refs.web, {
|
||||
offset: d.offset
|
||||
})
|
||||
} else if (href.includes('://'))
|
||||
plus.runtime.openWeb(href);
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
break;
|
||||
case 'error':
|
||||
if (d.source == 'img' && cfg.errorImg)
|
||||
this.imgList.setItem(d.target.i, cfg.errorImg);
|
||||
this.$emit('error', {
|
||||
source: d.source,
|
||||
target: d.target
|
||||
})
|
||||
break;
|
||||
case 'ready':
|
||||
this.height = d.height;
|
||||
if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
|
||||
this.rect = res[0];
|
||||
this.$emit('ready', res[0]);
|
||||
})
|
||||
break;
|
||||
case 'click':
|
||||
this.$emit('click');
|
||||
this.$emit('tap');
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes _show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
100
components/jyf-parser/libs/CssHandler.js
Executable file
100
components/jyf-parser/libs/CssHandler.js
Executable file
@@ -0,0 +1,100 @@
|
||||
const cfg = require('./config.js'),
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
|
||||
function CssHandler(tagStyle) {
|
||||
var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
|
||||
for (var item in tagStyle)
|
||||
styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
|
||||
this.styles = styles;
|
||||
}
|
||||
CssHandler.prototype.getStyle = function(data) {
|
||||
this.styles = new parser(data, this.styles).parse();
|
||||
}
|
||||
CssHandler.prototype.match = function(name, attrs) {
|
||||
var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
|
||||
if (attrs.class) {
|
||||
var items = attrs.class.split(' ');
|
||||
for (var i = 0, item; item = items[i]; i++)
|
||||
if (tmp = this.styles['.' + item])
|
||||
matched += tmp + ';';
|
||||
}
|
||||
if (tmp = this.styles['#' + attrs.id])
|
||||
matched += tmp + ';';
|
||||
return matched;
|
||||
}
|
||||
module.exports = CssHandler;
|
||||
|
||||
function parser(data, init) {
|
||||
this.data = data;
|
||||
this.floor = 0;
|
||||
this.i = 0;
|
||||
this.list = [];
|
||||
this.res = init;
|
||||
this.state = this.Space;
|
||||
}
|
||||
parser.prototype.parse = function() {
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
return this.res;
|
||||
}
|
||||
parser.prototype.section = function() {
|
||||
return this.data.substring(this.start, this.i);
|
||||
}
|
||||
// 状态机
|
||||
parser.prototype.Space = function(c) {
|
||||
if (c == '.' || c == '#' || isLetter(c)) {
|
||||
this.start = this.i;
|
||||
this.state = this.Name;
|
||||
} else if (c == '/' && this.data[this.i + 1] == '*')
|
||||
this.Comment();
|
||||
else if (!cfg.blankChar[c] && c != ';')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.Comment = function() {
|
||||
this.i = this.data.indexOf('*/', this.i) + 1;
|
||||
if (!this.i) this.i = this.data.length;
|
||||
this.state = this.Space;
|
||||
}
|
||||
parser.prototype.Ignore = function(c) {
|
||||
if (c == '{') this.floor++;
|
||||
else if (c == '}' && !--this.floor) {
|
||||
this.list = [];
|
||||
this.state = this.Space;
|
||||
}
|
||||
}
|
||||
parser.prototype.Name = function(c) {
|
||||
if (cfg.blankChar[c]) {
|
||||
this.list.push(this.section());
|
||||
this.state = this.NameSpace;
|
||||
} else if (c == '{') {
|
||||
this.list.push(this.section());
|
||||
this.Content();
|
||||
} else if (c == ',') {
|
||||
this.list.push(this.section());
|
||||
this.Comma();
|
||||
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.NameSpace = function(c) {
|
||||
if (c == '{') this.Content();
|
||||
else if (c == ',') this.Comma();
|
||||
else if (!cfg.blankChar[c]) this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.Comma = function() {
|
||||
while (cfg.blankChar[this.data[++this.i]]);
|
||||
if (this.data[this.i] == '{') this.Content();
|
||||
else {
|
||||
this.start = this.i--;
|
||||
this.state = this.Name;
|
||||
}
|
||||
}
|
||||
parser.prototype.Content = function() {
|
||||
this.start = ++this.i;
|
||||
if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
|
||||
var content = this.section();
|
||||
for (var i = 0, item; item = this.list[i++];)
|
||||
if (this.res[item]) this.res[item] += ';' + content;
|
||||
else this.res[item] = content;
|
||||
this.list = [];
|
||||
this.state = this.Space;
|
||||
}
|
||||
580
components/jyf-parser/libs/MpHtmlParser.js
Executable file
580
components/jyf-parser/libs/MpHtmlParser.js
Executable file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* html 解析器
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @version 20201029
|
||||
* @author JinYufeng
|
||||
* @listens MIT
|
||||
*/
|
||||
const cfg = require('./config.js'),
|
||||
blankChar = cfg.blankChar,
|
||||
CssHandler = require('./CssHandler.js'),
|
||||
windowWidth = uni.getSystemInfoSync().windowWidth;
|
||||
var emoji;
|
||||
|
||||
function MpHtmlParser(data, options = {}) {
|
||||
this.attrs = {};
|
||||
this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
|
||||
this.data = data;
|
||||
this.domain = options.domain;
|
||||
this.DOM = [];
|
||||
this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
|
||||
options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
|
||||
this.options = options;
|
||||
this.state = this.Text;
|
||||
this.STACK = [];
|
||||
// 工具函数
|
||||
this.bubble = () => {
|
||||
for (var i = this.STACK.length, item; item = this.STACK[--i];) {
|
||||
if (cfg.richOnlyTags[item.name]) return false;
|
||||
item.c = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
this.decode = (val, amp) => {
|
||||
var i = -1,
|
||||
j, en;
|
||||
while (1) {
|
||||
if ((i = val.indexOf('&', i + 1)) == -1) break;
|
||||
if ((j = val.indexOf(';', i + 2)) == -1) break;
|
||||
if (val[i + 1] == '#') {
|
||||
en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
|
||||
if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
|
||||
} else {
|
||||
en = val.substring(i + 1, j);
|
||||
if (cfg.entities[en] || en == amp)
|
||||
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
this.getUrl = url => {
|
||||
if (url[0] == '/') {
|
||||
if (url[1] == '/') url = this.options.prot + ':' + url;
|
||||
else if (this.domain) url = this.domain + url;
|
||||
} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
|
||||
url = this.domain + '/' + url;
|
||||
return url;
|
||||
}
|
||||
this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
|
||||
this.section = () => this.data.substring(this.start, this.i);
|
||||
this.parent = () => this.STACK[this.STACK.length - 1];
|
||||
this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
|
||||
}
|
||||
MpHtmlParser.prototype.parse = function() {
|
||||
if (emoji) this.data = emoji.parseEmoji(this.data);
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
if (this.state == this.Text) this.setText();
|
||||
while (this.STACK.length) this.popNode(this.STACK.pop());
|
||||
return this.DOM;
|
||||
}
|
||||
// 设置属性
|
||||
MpHtmlParser.prototype.setAttr = function() {
|
||||
var name = this.attrName.toLowerCase(),
|
||||
val = this.attrVal;
|
||||
if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
|
||||
else if (val) {
|
||||
if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
|
||||
else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
|
||||
else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
|
||||
}
|
||||
this.attrVal = '';
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
}
|
||||
// 设置文本节点
|
||||
MpHtmlParser.prototype.setText = function() {
|
||||
var back, text = this.section();
|
||||
if (!text) return;
|
||||
text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
|
||||
if (back) {
|
||||
this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
|
||||
let j = this.start + text.length;
|
||||
for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
|
||||
return;
|
||||
}
|
||||
if (!this.pre) {
|
||||
// 合并空白符
|
||||
var flag, tmp = [];
|
||||
for (let i = text.length, c; c = text[--i];)
|
||||
if (!blankChar[c]) {
|
||||
tmp.unshift(c);
|
||||
if (!flag) flag = 1;
|
||||
} else {
|
||||
if (tmp[0] != ' ') tmp.unshift(' ');
|
||||
if (c == '\n' && flag == void 0) flag = 0;
|
||||
}
|
||||
if (flag == 0) return;
|
||||
text = tmp.join('');
|
||||
}
|
||||
this.siblings().push({
|
||||
type: 'text',
|
||||
text: this.decode(text)
|
||||
});
|
||||
}
|
||||
// 设置元素节点
|
||||
MpHtmlParser.prototype.setNode = function() {
|
||||
var node = {
|
||||
name: this.tagName.toLowerCase(),
|
||||
attrs: this.attrs
|
||||
},
|
||||
close = cfg.selfClosingTags[node.name];
|
||||
if (this.options.nodes.length) node.type = 'node';
|
||||
this.attrs = {};
|
||||
if (!cfg.ignoreTags[node.name]) {
|
||||
// 处理属性
|
||||
var attrs = node.attrs,
|
||||
style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
|
||||
styleObj = {};
|
||||
if (attrs.id) {
|
||||
if (this.options.compress & 1) attrs.id = void 0;
|
||||
else if (this.options.useAnchor) this.bubble();
|
||||
}
|
||||
if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
case 'ad': // #ifdef APP-PLUS
|
||||
case 'iframe':
|
||||
// #endif
|
||||
this.bubble();
|
||||
break;
|
||||
case 'font':
|
||||
if (attrs.color) {
|
||||
styleObj['color'] = attrs.color;
|
||||
attrs.color = void 0;
|
||||
}
|
||||
if (attrs.face) {
|
||||
styleObj['font-family'] = attrs.face;
|
||||
attrs.face = void 0;
|
||||
}
|
||||
if (attrs.size) {
|
||||
var size = parseInt(attrs.size);
|
||||
if (size < 1) size = 1;
|
||||
else if (size > 7) size = 7;
|
||||
var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
|
||||
styleObj['font-size'] = map[size - 1];
|
||||
attrs.size = void 0;
|
||||
}
|
||||
break;
|
||||
case 'embed':
|
||||
// #ifndef APP-PLUS
|
||||
var src = node.attrs.src || '',
|
||||
type = node.attrs.type || '';
|
||||
if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
|
||||
node.name = 'video';
|
||||
else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
|
||||
'.aac'))
|
||||
node.name = 'audio';
|
||||
else break;
|
||||
if (node.attrs.autostart)
|
||||
node.attrs.autoplay = 'T';
|
||||
node.attrs.controls = 'T';
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
this.bubble();
|
||||
break;
|
||||
// #endif
|
||||
case 'video':
|
||||
case 'audio':
|
||||
if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
|
||||
else this[`${node.name}Num`]++;
|
||||
if (node.name == 'video') {
|
||||
if (this.videoNum > 3)
|
||||
node.lazyLoad = 1;
|
||||
if (attrs.width) {
|
||||
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
|
||||
attrs.width = void 0;
|
||||
}
|
||||
if (attrs.height) {
|
||||
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
|
||||
attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T';
|
||||
attrs.source = [];
|
||||
if (attrs.src) {
|
||||
attrs.source.push(attrs.src);
|
||||
attrs.src = void 0;
|
||||
}
|
||||
this.bubble();
|
||||
break;
|
||||
case 'td':
|
||||
case 'th':
|
||||
if (attrs.colspan || attrs.rowspan)
|
||||
for (var k = this.STACK.length, item; item = this.STACK[--k];)
|
||||
if (item.name == 'table') {
|
||||
item.flag = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attrs.align) {
|
||||
if (node.name == 'table') {
|
||||
if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';
|
||||
else styleObj['float'] = attrs.align;
|
||||
} else styleObj['text-align'] = attrs.align;
|
||||
attrs.align = void 0;
|
||||
}
|
||||
// 压缩 style
|
||||
var styles = style.split(';');
|
||||
style = '';
|
||||
for (var i = 0, len = styles.length; i < len; i++) {
|
||||
var info = styles[i].split(':');
|
||||
if (info.length < 2) continue;
|
||||
let key = info[0].trim().toLowerCase(),
|
||||
value = info.slice(1).join(':').trim();
|
||||
if (value[0] == '-' || value.includes('safe'))
|
||||
style += `;${key}:${value}`;
|
||||
else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
|
||||
styleObj[key] = value;
|
||||
}
|
||||
if (node.name == 'img') {
|
||||
if (attrs.src && !attrs.ignore) {
|
||||
if (this.bubble())
|
||||
attrs.i = (this.imgNum++).toString();
|
||||
else attrs.ignore = 'T';
|
||||
}
|
||||
if (attrs.ignore) {
|
||||
style += ';-webkit-touch-callout:none';
|
||||
styleObj['max-width'] = '100%';
|
||||
}
|
||||
var width;
|
||||
if (styleObj.width) width = styleObj.width;
|
||||
else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : parseFloat(attrs.width) + 'px';
|
||||
if (width) {
|
||||
styleObj.width = width;
|
||||
attrs.width = '100%';
|
||||
if (parseInt(width) > windowWidth) {
|
||||
styleObj.height = '';
|
||||
if (attrs.height) attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
if (styleObj.height) {
|
||||
attrs.height = styleObj.height;
|
||||
styleObj.height = '';
|
||||
} else if (attrs.height && !attrs.height.includes('%'))
|
||||
attrs.height = parseFloat(attrs.height) + 'px';
|
||||
}
|
||||
for (var key in styleObj) {
|
||||
var value = styleObj[key];
|
||||
if (!value) continue;
|
||||
if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
|
||||
// 填充链接
|
||||
if (value.includes('url')) {
|
||||
var j = value.indexOf('(');
|
||||
if (j++ != -1) {
|
||||
while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
|
||||
value = value.substr(0, j) + this.getUrl(value.substr(j));
|
||||
}
|
||||
}
|
||||
// 转换 rpx
|
||||
else if (value.includes('rpx'))
|
||||
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
|
||||
else if (key == 'white-space' && value.includes('pre') && !close)
|
||||
this.pre = node.pre = true;
|
||||
style += `;${key}:${value}`;
|
||||
}
|
||||
style = style.substr(1);
|
||||
if (style) attrs.style = style;
|
||||
if (!close) {
|
||||
node.children = [];
|
||||
if (node.name == 'pre' && cfg.highlight) {
|
||||
this.remove(node);
|
||||
this.pre = node.pre = true;
|
||||
}
|
||||
this.siblings().push(node);
|
||||
this.STACK.push(node);
|
||||
} else if (!cfg.filter || cfg.filter(node, this) != false)
|
||||
this.siblings().push(node);
|
||||
} else {
|
||||
if (!close) this.remove(node);
|
||||
else if (node.name == 'source') {
|
||||
var parent = this.parent();
|
||||
if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
|
||||
parent.attrs.source.push(node.attrs.src);
|
||||
} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
|
||||
}
|
||||
if (this.data[this.i] == '/') this.i++;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
// 移除标签
|
||||
MpHtmlParser.prototype.remove = function(node) {
|
||||
var name = node.name,
|
||||
j = this.i;
|
||||
// 处理 svg
|
||||
var handleSvg = () => {
|
||||
var src = this.data.substring(j, this.i + 1);
|
||||
node.attrs.xmlns = 'http://www.w3.org/2000/svg';
|
||||
for (var key in node.attrs) {
|
||||
if (key == 'viewbox') src = ` viewBox="${node.attrs.viewbox}"` + src;
|
||||
else if (key != 'style') src = ` ${key}="${node.attrs[key]}"` + src;
|
||||
}
|
||||
src = '<svg' + src;
|
||||
var parent = this.parent();
|
||||
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
|
||||
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
|
||||
this.siblings().push({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
|
||||
style: node.attrs.style,
|
||||
ignore: 'T'
|
||||
}
|
||||
})
|
||||
}
|
||||
if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
|
||||
while (1) {
|
||||
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
|
||||
if (name == 'pre' || name == 'svg') this.i = j;
|
||||
else this.i = this.data.length;
|
||||
return;
|
||||
}
|
||||
this.start = (this.i += 2);
|
||||
while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
|
||||
if (this.section().toLowerCase() == name) {
|
||||
// 代码块高亮
|
||||
if (name == 'pre') {
|
||||
this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
|
||||
.substr(this.i - 5);
|
||||
return this.i = j;
|
||||
} else if (name == 'style')
|
||||
this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
|
||||
else if (name == 'title')
|
||||
this.DOM.title = this.data.substring(j + 1, this.i - 7);
|
||||
if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
|
||||
if (name == 'svg') handleSvg();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 节点出栈处理
|
||||
MpHtmlParser.prototype.popNode = function(node) {
|
||||
// 空白符处理
|
||||
if (node.pre) {
|
||||
node.pre = this.pre = void 0;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].pre)
|
||||
this.pre = true;
|
||||
}
|
||||
var siblings = this.siblings(),
|
||||
len = siblings.length,
|
||||
childs = node.children;
|
||||
if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
|
||||
return siblings.pop();
|
||||
var attrs = node.attrs;
|
||||
// 替换一些标签名
|
||||
if (cfg.blockTags[node.name]) node.name = 'div';
|
||||
else if (!cfg.trustTags[node.name]) node.name = 'span';
|
||||
// 处理列表
|
||||
if (node.c && (node.name == 'ul' || node.name == 'ol')) {
|
||||
if ((node.attrs.style || '').includes('list-style:none')) {
|
||||
for (let i = 0, child; child = childs[i++];)
|
||||
if (child.name == 'li')
|
||||
child.name = 'div';
|
||||
} else if (node.name == 'ul') {
|
||||
var floor = 1;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == 'ul') floor++;
|
||||
if (floor != 1)
|
||||
for (let i = childs.length; i--;)
|
||||
childs[i].floor = floor;
|
||||
} else {
|
||||
for (let i = 0, num = 1, child; child = childs[i++];)
|
||||
if (child.name == 'li') {
|
||||
child.type = 'ol';
|
||||
child.num = ((num, type) => {
|
||||
if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
|
||||
if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
|
||||
if (type == 'i' || type == 'I') {
|
||||
num = (num - 1) % 99 + 1;
|
||||
var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
|
||||
ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
|
||||
res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
|
||||
if (type == 'i') return res.toLowerCase();
|
||||
return res;
|
||||
}
|
||||
return num;
|
||||
})(num++, attrs.type) + '.';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理表格
|
||||
if (node.name == 'table') {
|
||||
var padding = parseFloat(attrs.cellpadding),
|
||||
spacing = parseFloat(attrs.cellspacing),
|
||||
border = parseFloat(attrs.border);
|
||||
if (node.c) {
|
||||
if (isNaN(padding)) padding = 2;
|
||||
if (isNaN(spacing)) spacing = 2;
|
||||
}
|
||||
if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
|
||||
if (node.flag && node.c) {
|
||||
// 有 colspan 或 rowspan 且含有链接的表格转为 grid 布局实现
|
||||
attrs.style = `${attrs.style || ''};${spacing ? `;grid-gap:${spacing}px` : ';border-left:0;border-top:0'}`;
|
||||
var row = 1,
|
||||
col = 1,
|
||||
colNum,
|
||||
trs = [],
|
||||
children = [],
|
||||
map = {};
|
||||
(function f(ns) {
|
||||
for (var i = 0; i < ns.length; i++) {
|
||||
if (ns[i].name == 'tr') trs.push(ns[i]);
|
||||
else f(ns[i].children || []);
|
||||
}
|
||||
})(node.children)
|
||||
for (let i = 0; i < trs.length; i++) {
|
||||
for (let j = 0, td; td = trs[i].children[j]; j++) {
|
||||
if (td.name == 'td' || td.name == 'th') {
|
||||
while (map[row + '.' + col]) col++;
|
||||
var cell = {
|
||||
name: 'div',
|
||||
c: 1,
|
||||
attrs: {
|
||||
style: (td.attrs.style || '') + (border ? `;border:${border}px solid gray` + (spacing ? '' :
|
||||
';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '')
|
||||
},
|
||||
children: td.children
|
||||
}
|
||||
if (td.attrs.colspan) {
|
||||
cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + parseInt(td.attrs.colspan));
|
||||
if (!td.attrs.rowspan) cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + 1);
|
||||
col += parseInt(td.attrs.colspan) - 1;
|
||||
}
|
||||
if (td.attrs.rowspan) {
|
||||
cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + parseInt(td.attrs.rowspan));
|
||||
if (!td.attrs.colspan) cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + 1);
|
||||
for (var k = 1; k < td.attrs.rowspan; k++) map[(row + k) + '.' + col] = 1;
|
||||
}
|
||||
children.push(cell);
|
||||
col++;
|
||||
}
|
||||
}
|
||||
if (!colNum) {
|
||||
colNum = col - 1;
|
||||
attrs.style += `;grid-template-columns:repeat(${colNum},auto)`
|
||||
}
|
||||
col = 1;
|
||||
row++;
|
||||
}
|
||||
node.children = children;
|
||||
} else {
|
||||
attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
|
||||
if (border || padding)
|
||||
(function f(ns) {
|
||||
for (var i = 0, n; n = ns[i]; i++) {
|
||||
if (n.name == 'th' || n.name == 'td') {
|
||||
if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style || ''}`;
|
||||
if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style || ''}`;
|
||||
} else f(n.children || []);
|
||||
}
|
||||
})(childs)
|
||||
}
|
||||
if (this.options.autoscroll) {
|
||||
var table = Object.assign({}, node);
|
||||
node.name = 'div';
|
||||
node.attrs = {
|
||||
style: 'overflow:scroll'
|
||||
}
|
||||
node.children = [table];
|
||||
}
|
||||
}
|
||||
this.CssHandler.pop && this.CssHandler.pop(node);
|
||||
// 自动压缩
|
||||
if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
|
||||
siblings[len - 1] = childs[0];
|
||||
}
|
||||
// 状态机
|
||||
MpHtmlParser.prototype.Text = function(c) {
|
||||
if (c == '<') {
|
||||
var next = this.data[this.i + 1],
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
if (isLetter(next)) {
|
||||
this.setText();
|
||||
this.start = this.i + 1;
|
||||
this.state = this.TagName;
|
||||
} else if (next == '/') {
|
||||
this.setText();
|
||||
if (isLetter(this.data[++this.i + 1])) {
|
||||
this.start = this.i + 1;
|
||||
this.state = this.EndTag;
|
||||
} else this.Comment();
|
||||
} else if (next == '!' || next == '?') {
|
||||
this.setText();
|
||||
this.Comment();
|
||||
}
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.Comment = function() {
|
||||
var key;
|
||||
if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
|
||||
else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
|
||||
else key = '>';
|
||||
if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
|
||||
else this.i += key.length - 1;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
MpHtmlParser.prototype.TagName = function(c) {
|
||||
if (blankChar[c]) {
|
||||
this.tagName = this.section();
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
} else if (this.isClose()) {
|
||||
this.tagName = this.section();
|
||||
this.setNode();
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.AttrName = function(c) {
|
||||
if (c == '=' || blankChar[c] || this.isClose()) {
|
||||
this.attrName = this.section();
|
||||
if (blankChar[c])
|
||||
while (blankChar[this.data[++this.i]]);
|
||||
if (this.data[this.i] == '=') {
|
||||
while (blankChar[this.data[++this.i]]);
|
||||
this.start = this.i--;
|
||||
this.state = this.AttrValue;
|
||||
} else this.setAttr();
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.AttrValue = function(c) {
|
||||
if (c == '"' || c == "'") {
|
||||
this.start++;
|
||||
if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
|
||||
this.attrVal = this.section();
|
||||
this.i++;
|
||||
} else {
|
||||
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
|
||||
this.attrVal = this.section();
|
||||
}
|
||||
this.setAttr();
|
||||
}
|
||||
MpHtmlParser.prototype.EndTag = function(c) {
|
||||
if (blankChar[c] || c == '>' || c == '/') {
|
||||
var name = this.section().toLowerCase();
|
||||
for (var i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == name) break;
|
||||
if (i != -1) {
|
||||
var node;
|
||||
while ((node = this.STACK.pop()).name != name) this.popNode(node);
|
||||
this.popNode(node);
|
||||
} else if (name == 'p' || name == 'br')
|
||||
this.siblings().push({
|
||||
name,
|
||||
attrs: {}
|
||||
});
|
||||
this.i = this.data.indexOf('>', this.i);
|
||||
this.start = this.i + 1;
|
||||
if (this.i == -1) this.i = this.data.length;
|
||||
else this.state = this.Text;
|
||||
}
|
||||
}
|
||||
module.exports = MpHtmlParser;
|
||||
80
components/jyf-parser/libs/config.js
Executable file
80
components/jyf-parser/libs/config.js
Executable file
@@ -0,0 +1,80 @@
|
||||
/* 配置文件 */
|
||||
var cfg = {
|
||||
// 出错占位图
|
||||
errorImg: null,
|
||||
// 过滤器函数
|
||||
filter: null,
|
||||
// 代码高亮函数
|
||||
highlight: null,
|
||||
// 文本处理函数
|
||||
onText: null,
|
||||
// 实体编码列表
|
||||
entities: {
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
semi: ';',
|
||||
nbsp: '\xA0',
|
||||
ensp: '\u2002',
|
||||
emsp: '\u2003',
|
||||
ndash: '–',
|
||||
mdash: '—',
|
||||
middot: '·',
|
||||
lsquo: '‘',
|
||||
rsquo: '’',
|
||||
ldquo: '“',
|
||||
rdquo: '”',
|
||||
bull: '•',
|
||||
hellip: '…'
|
||||
},
|
||||
blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
|
||||
boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
|
||||
// 块级标签,将被转为 div
|
||||
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
|
||||
// 将被移除的标签
|
||||
ignoreTags: makeMap('area,base,canvas,frame,iframe,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'),
|
||||
// 只能被 rich-text 显示的标签
|
||||
richOnlyTags: makeMap('a,colgroup,fieldset,legend'),
|
||||
// 自闭合的标签
|
||||
selfClosingTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
|
||||
// 信任的标签
|
||||
trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
|
||||
// 默认的标签样式
|
||||
userAgentStyles: {
|
||||
address: 'font-style:italic',
|
||||
big: 'display:inline;font-size:1.2em',
|
||||
blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
|
||||
caption: 'display:table-caption;text-align:center',
|
||||
center: 'text-align:center',
|
||||
cite: 'font-style:italic',
|
||||
dd: 'margin-left:40px',
|
||||
mark: 'background-color:yellow',
|
||||
pre: 'font-family:monospace;white-space:pre;overflow:scroll',
|
||||
s: 'text-decoration:line-through',
|
||||
small: 'display:inline;font-size:0.8em',
|
||||
u: 'text-decoration:underline'
|
||||
}
|
||||
}
|
||||
|
||||
function makeMap(str) {
|
||||
var map = Object.create(null),
|
||||
list = str.split(',');
|
||||
for (var i = list.length; i--;)
|
||||
map[list[i]] = true;
|
||||
return map;
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (wx.canIUse('editor')) {
|
||||
cfg.blockTags.pre = void 0;
|
||||
cfg.ignoreTags.rp = true;
|
||||
Object.assign(cfg.richOnlyTags, makeMap('bdi,bdo,caption,rt,ruby'));
|
||||
Object.assign(cfg.trustTags, makeMap('bdi,bdo,caption,pre,rt,ruby'));
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
cfg.ignoreTags.iframe = void 0;
|
||||
Object.assign(cfg.trustTags, makeMap('embed,iframe'));
|
||||
// #endif
|
||||
|
||||
module.exports = cfg;
|
||||
22
components/jyf-parser/libs/handler.wxs
Executable file
22
components/jyf-parser/libs/handler.wxs
Executable file
@@ -0,0 +1,22 @@
|
||||
var inline = {
|
||||
abbr: 1,
|
||||
b: 1,
|
||||
big: 1,
|
||||
code: 1,
|
||||
del: 1,
|
||||
em: 1,
|
||||
i: 1,
|
||||
ins: 1,
|
||||
label: 1,
|
||||
q: 1,
|
||||
small: 1,
|
||||
span: 1,
|
||||
strong: 1,
|
||||
sub: 1,
|
||||
sup: 1
|
||||
}
|
||||
module.exports = {
|
||||
use: function(item) {
|
||||
return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
|
||||
}
|
||||
}
|
||||
506
components/jyf-parser/libs/trees.vue
Executable file
506
components/jyf-parser/libs/trees.vue
Executable file
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<view :class="'interlayer '+(c||'')" :style="s">
|
||||
<block v-for="(n, i) in nodes" v-bind:key="i">
|
||||
<!--图片-->
|
||||
<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap.stop="imgtap">
|
||||
<rich-text v-if="ctrl[i]!=0" :nodes="[{attrs:{src:loading&&(ctrl[i]||0)<2?loading:(lazyLoad&&!ctrl[i]?placeholder:(ctrl[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
|
||||
<image class="_image" :src="lazyLoad&&!ctrl[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
|
||||
:show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
|
||||
@error="error" />
|
||||
</view>
|
||||
<!--文本-->
|
||||
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
|
||||
<!--#ifndef MP-BAIDU-->
|
||||
<text v-else-if="n.name=='br'">\n</text>
|
||||
<!--#endif-->
|
||||
<!--视频-->
|
||||
<view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&ctrl[i]==undefined" :id="n.attrs.id"
|
||||
:class="'_video '+(n.attrs.class||'')" :style="n.attrs.style" :data-i="i" @tap.stop="_loadVideo" />
|
||||
<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||ctrl[i]==0"
|
||||
:controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[ctrl[i]||0]"
|
||||
:unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
|
||||
<!--音频-->
|
||||
<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
|
||||
:src="n.attrs.source[ctrl[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio" @error.native="error"
|
||||
@play.native="play" />
|
||||
<!--链接-->
|
||||
<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
|
||||
:data-attrs="n.attrs" @tap.stop="linkpress">
|
||||
<trees class="_span" c="_span" :nodes="n.children" />
|
||||
</view>
|
||||
<!--广告-->
|
||||
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
|
||||
<!--列表-->
|
||||
<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex;flex-direction:row'">
|
||||
<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
|
||||
<view v-else class="_ul-bef">
|
||||
<view v-if="n.floor%3==0" class="_ul-p1">█</view>
|
||||
<view v-else-if="n.floor%3==2" class="_ul-p2" />
|
||||
<view v-else class="_ul-p1" style="border-radius:50%">█</view>
|
||||
</view>
|
||||
<trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
|
||||
</view>
|
||||
<!--表格-->
|
||||
<view v-else-if="n.name=='table'&&n.c&&n.flag" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:grid'">
|
||||
<trees v-for="(cell,n) in n.children" v-bind:key="n" :class="cell.attrs.class" :c="cell.attrs.class" :style="cell.attrs.style"
|
||||
:s="cell.attrs.style" :nodes="cell.children" />
|
||||
</view>
|
||||
<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
|
||||
<view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
|
||||
<view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
|
||||
<trees v-if="tr.name=='td'" :nodes="tr.children" />
|
||||
<trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
|
||||
:s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!--#ifdef APP-PLUS-->
|
||||
<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
|
||||
:width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<!--#endif-->
|
||||
<!--富文本-->
|
||||
<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
|
||||
<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
|
||||
<!--#endif-->
|
||||
<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
|
||||
:style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs" src="./handler.wxs"></script>
|
||||
<script>
|
||||
global.Parser = {};
|
||||
import trees from './trees'
|
||||
const errorImg = require('../libs/config.js').errorImg;
|
||||
export default {
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
name: 'trees',
|
||||
data() {
|
||||
return {
|
||||
ctrl: [],
|
||||
placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
|
||||
errorImg,
|
||||
loadVideo: typeof plus == 'undefined',
|
||||
// #ifndef MP-ALIPAY
|
||||
c: '',
|
||||
s: ''
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
nodes: Array,
|
||||
lazyLoad: Boolean,
|
||||
loading: String,
|
||||
// #ifdef MP-ALIPAY
|
||||
c: String,
|
||||
s: String
|
||||
// #endif
|
||||
},
|
||||
mounted() {
|
||||
for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
|
||||
this.init();
|
||||
},
|
||||
// #ifdef APP-PLUS
|
||||
beforeDestroy() {
|
||||
this.observer && this.observer.disconnect();
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
init() {
|
||||
for (var i = this.nodes.length, n; n = this.nodes[--i];) {
|
||||
if (n.name == 'img') {
|
||||
this.top.imgList.setItem(n.attrs.i, n.attrs['original-src'] || n.attrs.src);
|
||||
// #ifdef APP-PLUS
|
||||
if (this.lazyLoad && !this.observer) {
|
||||
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
|
||||
top: 500,
|
||||
bottom: 500
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.observer.observe('._img', res => {
|
||||
if (res.intersectionRatio) {
|
||||
for (var j = this.nodes.length; j--;)
|
||||
if (this.nodes[j].name == 'img')
|
||||
this.$set(this.ctrl, j, 1);
|
||||
this.observer.disconnect();
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
// #endif
|
||||
} else if (n.name == 'video' || n.name == 'audio') {
|
||||
var ctx;
|
||||
if (n.name == 'video') {
|
||||
ctx = uni.createVideoContext(n.attrs.id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
);
|
||||
} else if (this.$refs[n.attrs.id])
|
||||
ctx = this.$refs[n.attrs.id][0];
|
||||
if (ctx) {
|
||||
ctx.id = n.attrs.id;
|
||||
this.top.videoContexts.push(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
// APP 上避免 video 错位需要延时渲染
|
||||
setTimeout(() => {
|
||||
this.loadVideo = true;
|
||||
}, 1000)
|
||||
// #endif
|
||||
},
|
||||
play(e) {
|
||||
var contexts = this.top.videoContexts;
|
||||
if (contexts.length > 1 && this.top.autopause)
|
||||
for (var i = contexts.length; i--;)
|
||||
if (contexts[i].id != e.currentTarget.dataset.id)
|
||||
contexts[i].pause();
|
||||
},
|
||||
imgtap(e) {
|
||||
var attrs = e.currentTarget.dataset.attrs;
|
||||
if (!attrs.ignore) {
|
||||
var preview = true,
|
||||
data = {
|
||||
id: e.target.id,
|
||||
src: attrs.src,
|
||||
ignore: () => preview = false
|
||||
};
|
||||
global.Parser.onImgtap && global.Parser.onImgtap(data);
|
||||
this.top.$emit('imgtap', data);
|
||||
if (preview) {
|
||||
var urls = this.top.imgList,
|
||||
current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
loadImg(e) {
|
||||
var i = e.currentTarget.dataset.i;
|
||||
if (this.lazyLoad && !this.ctrl[i]) {
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
this.$set(this.ctrl, i, 0);
|
||||
this.$nextTick(function() {
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
this.$set(this.ctrl, i, 1);
|
||||
// #endif
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
})
|
||||
// #endif
|
||||
} else if (this.loading && this.ctrl[i] != 2) {
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
this.$set(this.ctrl, i, 0);
|
||||
this.$nextTick(function() {
|
||||
// #endif
|
||||
this.$set(this.ctrl, i, 2);
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
linkpress(e) {
|
||||
var jump = true,
|
||||
attrs = e.currentTarget.dataset.attrs;
|
||||
attrs.ignore = () => jump = false;
|
||||
global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
|
||||
this.top.$emit('linkpress', attrs);
|
||||
if (jump) {
|
||||
// #ifdef MP
|
||||
if (attrs['app-id']) {
|
||||
return uni.navigateToMiniProgram({
|
||||
appId: attrs['app-id'],
|
||||
path: attrs.path
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
if (attrs.href) {
|
||||
if (attrs.href[0] == '#') {
|
||||
if (this.top.useAnchor)
|
||||
this.top.navigateTo({
|
||||
id: attrs.href.substring(1)
|
||||
})
|
||||
} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(attrs.href);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.setClipboardData({
|
||||
data: attrs.href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
} else
|
||||
uni.navigateTo({
|
||||
url: attrs.href,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: attrs.href,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
error(e) {
|
||||
var target = e.currentTarget,
|
||||
source = target.dataset.source,
|
||||
i = target.dataset.i;
|
||||
if (source == 'video' || source == 'audio') {
|
||||
// 加载其他 source
|
||||
var index = this.ctrl[i] ? this.ctrl[i].i + 1 : 1;
|
||||
if (index < this.nodes[i].attrs.source.length)
|
||||
this.$set(this.ctrl, i, index);
|
||||
if (e.detail.__args__)
|
||||
e.detail = e.detail.__args__[0];
|
||||
} else if (errorImg && source == 'img') {
|
||||
this.top.imgList.setItem(target.dataset.index, errorImg);
|
||||
this.$set(this.ctrl, i, 3);
|
||||
}
|
||||
this.top && this.top.$emit('error', {
|
||||
source,
|
||||
target,
|
||||
errMsg: e.detail.errMsg
|
||||
});
|
||||
},
|
||||
_loadVideo(e) {
|
||||
this.$set(this.ctrl, e.target.dataset.i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 在这里引入自定义样式 */
|
||||
|
||||
/* 链接和图片效果 */
|
||||
._a {
|
||||
display: inline;
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
._img {
|
||||
/* display: inline-block; */
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef MP-ALIPAY || APP-PLUS */
|
||||
.interlayer {
|
||||
display: inherit;
|
||||
flex-direction: inherit;
|
||||
flex-wrap: inherit;
|
||||
align-content: inherit;
|
||||
align-items: inherit;
|
||||
justify-content: inherit;
|
||||
width: 100%;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* #ifndef MP-ALIPAY */
|
||||
._blockquote,
|
||||
._div,
|
||||
._p,
|
||||
._ol,
|
||||
._ul,
|
||||
._li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
margin-top: -360px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
._ol-bef {
|
||||
width: 36px;
|
||||
margin-right: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
._ul-bef {
|
||||
display: block;
|
||||
margin: 0 12px 0 23px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
._ol-bef,
|
||||
._ul-bef {
|
||||
flex: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
._ul-p1 {
|
||||
display: inline-block;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
overflow: hidden;
|
||||
line-height: 0.3em;
|
||||
}
|
||||
|
||||
._ul-p2 {
|
||||
display: inline-block;
|
||||
width: 0.23em;
|
||||
height: 0.23em;
|
||||
border: 0.05em solid black;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP-WEIXIN || MP-QQ */
|
||||
.__bdo,
|
||||
.__bdi,
|
||||
.__ruby,
|
||||
.__rt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
._video {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
._video::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
content: '';
|
||||
border-color: transparent transparent transparent white;
|
||||
border-style: solid;
|
||||
border-width: 15px 0 15px 30px;
|
||||
}
|
||||
</style>
|
||||
55
components/mescroll-uni/components/mescroll-down.css
Executable file
55
components/mescroll-uni/components/mescroll-down.css
Executable file
@@ -0,0 +1,55 @@
|
||||
/* 下拉刷新区域 */
|
||||
.mescroll-downwarp {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 下拉刷新--内容区,定位于区域底部 */
|
||||
.mescroll-downwarp .downwarp-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
min-height: 60rpx;
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 下拉刷新--提示文本 */
|
||||
.mescroll-downwarp .downwarp-tip {
|
||||
display: inline-block;
|
||||
font-size: 28rpx;
|
||||
vertical-align: middle;
|
||||
margin-left: 16rpx;
|
||||
/* color: gray; 已在style设置color,此处删去*/
|
||||
}
|
||||
|
||||
/* 下拉刷新--旋转进度条 */
|
||||
.mescroll-downwarp .downwarp-progress {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid gray;
|
||||
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.mescroll-downwarp .mescroll-rotate {
|
||||
animation: mescrollDownRotate 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mescrollDownRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
47
components/mescroll-uni/components/mescroll-down.vue
Executable file
47
components/mescroll-uni/components/mescroll-down.vue
Executable file
@@ -0,0 +1,47 @@
|
||||
<!-- 下拉刷新区域 -->
|
||||
<template>
|
||||
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
option: Object , // down的配置项
|
||||
type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
|
||||
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption(){
|
||||
return this.option || {}
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.type === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.rate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
switch (this.type){
|
||||
case 1: return this.mOption.textInOffset;
|
||||
case 2: return this.mOption.textOutOffset;
|
||||
case 3: return this.mOption.textLoading;
|
||||
case 4: return this.mOption.textLoading;
|
||||
default: return this.mOption.textInOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./mescroll-down.css";
|
||||
</style>
|
||||
90
components/mescroll-uni/components/mescroll-empty.vue
Executable file
90
components/mescroll-uni/components/mescroll-empty.vue
Executable file
@@ -0,0 +1,90 @@
|
||||
<!--空布局
|
||||
|
||||
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
|
||||
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
|
||||
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
-->
|
||||
<template>
|
||||
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
|
||||
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
|
||||
<view v-if="tip" class="empty-tip">{{ tip }}</view>
|
||||
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入全局配置
|
||||
import GlobalOption from './../mescroll-uni-option.js';
|
||||
export default {
|
||||
props: {
|
||||
// empty的配置项: 默认为GlobalOption.up.empty
|
||||
option: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
// 使用computed获取配置,用于支持option的动态配置
|
||||
computed: {
|
||||
// 图标
|
||||
icon() {
|
||||
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
|
||||
},
|
||||
// 文本提示
|
||||
tip() {
|
||||
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击按钮
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 无任何数据的空布局 */
|
||||
.mescroll-empty {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 100rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mescroll-empty.empty-fixed {
|
||||
z-index: 99;
|
||||
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
|
||||
top: 100rpx;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-icon {
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-tip {
|
||||
margin-top: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-btn {
|
||||
display: inline-block;
|
||||
margin-top: 40rpx;
|
||||
min-width: 200rpx;
|
||||
padding: 18rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1rpx solid #e04b28;
|
||||
border-radius: 60rpx;
|
||||
color: #e04b28;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-btn:active {
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
83
components/mescroll-uni/components/mescroll-top.vue
Executable file
83
components/mescroll-uni/components/mescroll-top.vue
Executable file
@@ -0,0 +1,83 @@
|
||||
<!-- 回到顶部的按钮 -->
|
||||
<template>
|
||||
<image
|
||||
v-if="mOption.src"
|
||||
class="mescroll-totop"
|
||||
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
|
||||
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
|
||||
:src="mOption.src"
|
||||
mode="widthFix"
|
||||
@click="toTopClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// up.toTop的配置项
|
||||
option: Object,
|
||||
// 是否显示
|
||||
value: false
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption(){
|
||||
return this.option || {}
|
||||
},
|
||||
// 优先显示左边
|
||||
left(){
|
||||
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
|
||||
},
|
||||
// 右边距离 (优先显示左边)
|
||||
right() {
|
||||
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addUnit(num){
|
||||
if(!num) return 0;
|
||||
if(typeof num === 'number') return num + 'rpx';
|
||||
return num
|
||||
},
|
||||
toTopClick() {
|
||||
this.$emit('input', false); // 使v-model生效
|
||||
this.$emit('click'); // 派发点击事件
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 回到顶部的按钮 */
|
||||
.mescroll-totop {
|
||||
z-index: 9990;
|
||||
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
|
||||
right: 20rpx;
|
||||
bottom: 120rpx;
|
||||
width: 72rpx;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s; /* 过渡 */
|
||||
margin-bottom: var(--window-bottom); /* css变量 */
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-totop-safearea {
|
||||
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
|
||||
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示 -- 淡入 */
|
||||
.mescroll-totop-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 -- 淡出且不接收事件*/
|
||||
.mescroll-totop-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
47
components/mescroll-uni/components/mescroll-up.css
Executable file
47
components/mescroll-uni/components/mescroll-up.css
Executable file
@@ -0,0 +1,47 @@
|
||||
/* 上拉加载区域 */
|
||||
.mescroll-upwarp {
|
||||
box-sizing: border-box;
|
||||
min-height: 110rpx;
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/*提示文本 */
|
||||
.mescroll-upwarp .upwarp-tip,
|
||||
.mescroll-upwarp .upwarp-nodata {
|
||||
display: inline-block;
|
||||
font-size: 28rpx;
|
||||
vertical-align: middle;
|
||||
/* color: gray; 已在style设置color,此处删去*/
|
||||
}
|
||||
|
||||
.mescroll-upwarp .upwarp-tip {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/*旋转进度条 */
|
||||
.mescroll-upwarp .upwarp-progress {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid gray;
|
||||
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.mescroll-upwarp .mescroll-rotate {
|
||||
animation: mescrollUpRotate 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mescrollUpRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
39
components/mescroll-uni/components/mescroll-up.vue
Executable file
39
components/mescroll-uni/components/mescroll-up.vue
Executable file
@@ -0,0 +1,39 @@
|
||||
<!-- 上拉加载区域 -->
|
||||
<template>
|
||||
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="isUpLoading">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
option: Object, // up的配置项
|
||||
type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption() {
|
||||
return this.option || {};
|
||||
},
|
||||
// 加载中
|
||||
isUpLoading() {
|
||||
return this.type === 1;
|
||||
},
|
||||
// 没有更多了
|
||||
isUpNoMore() {
|
||||
return this.type === 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './mescroll-up.css';
|
||||
</style>
|
||||
19
components/mescroll-uni/mescroll-body.css
Executable file
19
components/mescroll-uni/mescroll-body.css
Executable file
@@ -0,0 +1,19 @@
|
||||
.mescroll-body {
|
||||
position: relative; /* 下拉刷新区域相对自身定位 */
|
||||
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
|
||||
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
|
||||
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
|
||||
}
|
||||
|
||||
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
|
||||
.mescroll-body.mescorll-sticky{
|
||||
overflow: unset !important
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-safearea {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
352
components/mescroll-uni/mescroll-body.vue
Executable file
352
components/mescroll-uni/mescroll-body.vue
Executable file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<view
|
||||
class="mescroll-body mescroll-render-touch"
|
||||
:class="{'mescorll-sticky': sticky}"
|
||||
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
|
||||
@touchstart="wxsBiz.touchstartEvent"
|
||||
@touchmove="wxsBiz.touchmoveEvent"
|
||||
@touchend="wxsBiz.touchendEvent"
|
||||
@touchcancel="wxsBiz.touchendEvent"
|
||||
:change:prop="wxsBiz.propObserver"
|
||||
:prop="wxsProp"
|
||||
>
|
||||
<!-- 状态栏 -->
|
||||
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
|
||||
|
||||
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
|
||||
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
|
||||
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
|
||||
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 空布局 -->
|
||||
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
|
||||
<view class="mescroll-upwarp--container">
|
||||
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
|
||||
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
|
||||
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="upLoadType===1">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 适配iPhoneX -->
|
||||
<view v-if="safearea" class="mescroll-safearea"></view>
|
||||
|
||||
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
|
||||
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
|
||||
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- app, h5使用renderjs -->
|
||||
<!-- #ifdef APP-PLUS || H5 -->
|
||||
<script module="renderBiz" lang="renderjs">
|
||||
import renderBiz from './wxs/renderjs.js';
|
||||
export default {
|
||||
mixins: [renderBiz]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
|
||||
<script>
|
||||
// 引入mescroll-uni.js,处理核心逻辑
|
||||
import MeScroll from './mescroll-uni.js';
|
||||
// 引入全局配置
|
||||
import GlobalOption from './mescroll-uni-option.js';
|
||||
// 引入空布局组件
|
||||
import MescrollEmpty from './components/mescroll-empty.vue';
|
||||
// 引入回到顶部组件
|
||||
import MescrollTop from './components/mescroll-top.vue';
|
||||
// 引入兼容wxs(含renderjs)写法的mixins
|
||||
import WxsMixin from './wxs/mixins.js';
|
||||
|
||||
export default {
|
||||
mixins: [WxsMixin],
|
||||
components: {
|
||||
MescrollEmpty,
|
||||
MescrollTop
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mescroll: {optDown:{},optUp:{}}, // mescroll实例
|
||||
downHight: 0, //下拉刷新: 容器高度
|
||||
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
|
||||
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
|
||||
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
|
||||
isShowEmpty: false, // 是否显示空布局
|
||||
isShowToTop: false, // 是否显示回到顶部按钮
|
||||
windowHeight: 0, // 可使用窗口的高度
|
||||
windowBottom: 0, // 可使用窗口的底部位置
|
||||
statusBarHeight: 0 // 状态栏高度
|
||||
};
|
||||
},
|
||||
props: {
|
||||
down: Object, // 下拉刷新的参数配置
|
||||
up: Object, // 上拉加载的参数配置
|
||||
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
|
||||
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
|
||||
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
|
||||
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
|
||||
},
|
||||
computed: {
|
||||
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
|
||||
minHeight(){
|
||||
return this.toPx(this.height || '100%') + 'px'
|
||||
},
|
||||
// 下拉布局往下偏移的距离 (px)
|
||||
numTop() {
|
||||
return this.toPx(this.top)
|
||||
},
|
||||
padTop() {
|
||||
return this.numTop + 'px';
|
||||
},
|
||||
// 上拉布局往上偏移 (px)
|
||||
numBottom() {
|
||||
return this.toPx(this.bottom);
|
||||
},
|
||||
padBottom() {
|
||||
return this.numBottom + 'px';
|
||||
},
|
||||
// 是否为重置下拉的状态
|
||||
isDownReset() {
|
||||
return this.downLoadType === 3 || this.downLoadType === 4;
|
||||
},
|
||||
// 过渡
|
||||
transition() {
|
||||
return this.isDownReset ? 'transform 300ms' : '';
|
||||
},
|
||||
translateY() {
|
||||
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.downLoadType === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.downRate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
|
||||
switch (this.downLoadType){
|
||||
case 1: return this.mescroll.optDown.textInOffset;
|
||||
case 2: return this.mescroll.optDown.textOutOffset;
|
||||
case 3: return this.mescroll.optDown.textLoading;
|
||||
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
|
||||
default: return this.mescroll.optDown.textInOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//number,rpx,upx,px,% --> px的数值
|
||||
toPx(num) {
|
||||
if (typeof num === 'string') {
|
||||
if (num.indexOf('px') !== -1) {
|
||||
if (num.indexOf('rpx') !== -1) {
|
||||
// "10rpx"
|
||||
num = num.replace('rpx', '');
|
||||
} else if (num.indexOf('upx') !== -1) {
|
||||
// "10upx"
|
||||
num = num.replace('upx', '');
|
||||
} else {
|
||||
// "10px"
|
||||
return Number(num.replace('px', ''));
|
||||
}
|
||||
} else if (num.indexOf('%') !== -1) {
|
||||
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
|
||||
let rate = Number(num.replace('%', '')) / 100;
|
||||
return this.windowHeight * rate;
|
||||
}
|
||||
}
|
||||
return num ? uni.upx2px(Number(num)) : 0;
|
||||
},
|
||||
// 点击空布局的按钮回调
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick', this.mescroll);
|
||||
},
|
||||
// 点击回到顶部的按钮回调
|
||||
toTopClick() {
|
||||
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
|
||||
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
created() {
|
||||
let vm = this;
|
||||
|
||||
let diyOption = {
|
||||
// 下拉刷新的配置
|
||||
down: {
|
||||
inOffset() {
|
||||
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
outOffset() {
|
||||
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
onMoving(mescroll, rate, downHight) {
|
||||
// 下拉过程中的回调,滑动过程一直在执行;
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
showLoading(mescroll, downHight) {
|
||||
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
beforeEndDownScroll(mescroll){
|
||||
vm.downLoadType = 4;
|
||||
return mescroll.optDown.beforeEndDelay // 延时结束的时长
|
||||
},
|
||||
endDownScroll() {
|
||||
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
|
||||
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
|
||||
if(vm.downLoadType === 4) vm.downLoadType = 0
|
||||
},300)
|
||||
},
|
||||
// 派发下拉刷新的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('down', mescroll);
|
||||
}
|
||||
},
|
||||
// 上拉加载的配置
|
||||
up: {
|
||||
// 显示加载中的回调
|
||||
showLoading() {
|
||||
vm.upLoadType = 1;
|
||||
},
|
||||
// 显示无更多数据的回调
|
||||
showNoMore() {
|
||||
vm.upLoadType = 2;
|
||||
},
|
||||
// 隐藏上拉加载的回调
|
||||
hideUpScroll(mescroll) {
|
||||
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
|
||||
},
|
||||
// 空布局
|
||||
empty: {
|
||||
onShow(isShow) {
|
||||
// 显示隐藏的回调
|
||||
vm.isShowEmpty = isShow;
|
||||
}
|
||||
},
|
||||
// 回到顶部
|
||||
toTop: {
|
||||
onShow(isShow) {
|
||||
// 显示隐藏的回调
|
||||
vm.isShowToTop = isShow;
|
||||
}
|
||||
},
|
||||
// 派发上拉加载的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('up', mescroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
|
||||
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
|
||||
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
|
||||
|
||||
// 初始化MeScroll对象
|
||||
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
|
||||
// init回调mescroll对象
|
||||
vm.$emit('init', vm.mescroll);
|
||||
|
||||
// 设置高度
|
||||
const sys = uni.getSystemInfoSync();
|
||||
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
|
||||
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
|
||||
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是page的scroll,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
if(typeof y === 'string'){
|
||||
// 滚动到指定view (y为css选择器)
|
||||
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
|
||||
let selector;
|
||||
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
|
||||
selector = '#'+y // 不带#和. 则默认为id选择器
|
||||
}else{
|
||||
selector = y
|
||||
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
|
||||
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
|
||||
selector = y.split('>>>')[1].trim()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
|
||||
if (rect) {
|
||||
let top = rect.top
|
||||
top += vm.mescroll.getScrollTop()
|
||||
uni.pageScrollTo({
|
||||
scrollTop: top,
|
||||
duration: t
|
||||
})
|
||||
} else{
|
||||
console.error(selector + ' does not exist');
|
||||
}
|
||||
}).exec()
|
||||
},30)
|
||||
} else{
|
||||
// 滚动到指定位置 (y必须为数字)
|
||||
uni.pageScrollTo({
|
||||
scrollTop: y,
|
||||
duration: t
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
|
||||
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
|
||||
vm.mescroll.optUp.toTop.safearea = vm.safearea;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "./mescroll-body.css";
|
||||
@import "./components/mescroll-down.css";
|
||||
@import './components/mescroll-up.css';
|
||||
</style>
|
||||
65
components/mescroll-uni/mescroll-mixins.js
Executable file
65
components/mescroll-uni/mescroll-mixins.js
Executable file
@@ -0,0 +1,65 @@
|
||||
// mescroll-body 和 mescroll-uni 通用
|
||||
|
||||
// import MescrollUni from "./mescroll-uni.vue";
|
||||
// import MescrollBody from "./mescroll-body.vue";
|
||||
|
||||
const MescrollMixin = {
|
||||
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
|
||||
// MescrollUni,
|
||||
// MescrollBody
|
||||
// },
|
||||
data() {
|
||||
return {
|
||||
mescroll: null //mescroll实例对象
|
||||
}
|
||||
},
|
||||
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
onPullDownRefresh(){
|
||||
this.mescroll && this.mescroll.onPullDownRefresh();
|
||||
},
|
||||
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
onPageScroll(e) {
|
||||
this.mescroll && this.mescroll.onPageScroll(e);
|
||||
},
|
||||
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
onReachBottom() {
|
||||
this.mescroll && this.mescroll.onReachBottom();
|
||||
},
|
||||
methods: {
|
||||
// mescroll组件初始化的回调,可获取到mescroll对象
|
||||
mescrollInit(mescroll) {
|
||||
this.mescroll = mescroll;
|
||||
this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
},
|
||||
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
mescrollInitByRef() {
|
||||
if(!this.mescroll || !this.mescroll.resetUpScroll){
|
||||
let mescrollRef = this.$refs.mescrollRef;
|
||||
if(mescrollRef) this.mescroll = mescrollRef.mescroll
|
||||
}
|
||||
},
|
||||
// 下拉刷新的回调 (mixin默认resetUpScroll)
|
||||
downCallback() {
|
||||
if(this.mescroll.optUp.use){
|
||||
this.mescroll.resetUpScroll()
|
||||
}else{
|
||||
setTimeout(()=>{
|
||||
this.mescroll.endSuccess();
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
// 上拉加载的回调
|
||||
upCallback() {
|
||||
// mixin默认延时500自动结束加载
|
||||
setTimeout(()=>{
|
||||
this.mescroll.endErr();
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MescrollMixin;
|
||||
37
components/mescroll-uni/mescroll-uni-option.js
Executable file
37
components/mescroll-uni/mescroll-uni-option.js
Executable file
@@ -0,0 +1,37 @@
|
||||
// 全局配置
|
||||
// mescroll-body 和 mescroll-uni 通用
|
||||
const GlobalOption = {
|
||||
down: {
|
||||
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textSuccess: '加载成功', // 加载成功的文本
|
||||
textErr: '加载失败', // 加载失败的文本
|
||||
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
},
|
||||
up: {
|
||||
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textNoMore: '亲, 没有更多了', // 没有更多数据的提示文本
|
||||
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
toTop: {
|
||||
// 回到顶部按钮,需配置src才显示
|
||||
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
},
|
||||
empty: {
|
||||
use: true, // 是否显示空布局
|
||||
// icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||
icon: '/static/empty.png',
|
||||
tip: '亲,暂无相关数据' // 提示
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalOption
|
||||
36
components/mescroll-uni/mescroll-uni.css
Executable file
36
components/mescroll-uni/mescroll-uni.css
Executable file
@@ -0,0 +1,36 @@
|
||||
.mescroll-uni-warp{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mescroll-uni-content{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mescroll-uni {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200rpx;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
|
||||
}
|
||||
|
||||
/* 定位的方式固定高度 */
|
||||
.mescroll-uni-fixed{
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: auto; /* 使right生效 */
|
||||
height: auto; /* 使bottom生效 */
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-safearea {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
799
components/mescroll-uni/mescroll-uni.js
Executable file
799
components/mescroll-uni/mescroll-uni.js
Executable file
@@ -0,0 +1,799 @@
|
||||
/* mescroll
|
||||
* version 1.3.3
|
||||
* 2020-09-15 wenju
|
||||
* https://www.mescroll.com
|
||||
*/
|
||||
|
||||
export default function MeScroll(options, isScrollBody) {
|
||||
let me = this;
|
||||
me.version = '1.3.3'; // mescroll版本号
|
||||
me.options = options || {}; // 配置
|
||||
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
|
||||
|
||||
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
|
||||
me.isUpScrolling = false; // 是否在执行上拉加载的回调
|
||||
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
|
||||
|
||||
// 初始化下拉刷新
|
||||
me.initDownScroll();
|
||||
// 初始化上拉加载,则初始化
|
||||
me.initUpScroll();
|
||||
|
||||
// 自动加载
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
|
||||
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
|
||||
if (me.optDown.autoShowLoading) {
|
||||
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
|
||||
} else {
|
||||
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
|
||||
}
|
||||
}
|
||||
// 自动触发上拉加载
|
||||
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
|
||||
setTimeout(function(){
|
||||
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
|
||||
},100)
|
||||
}
|
||||
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
|
||||
}
|
||||
|
||||
/* 配置参数:下拉刷新 */
|
||||
MeScroll.prototype.extendDownScroll = function(optDown) {
|
||||
// 下拉刷新的配置
|
||||
MeScroll.extend(optDown, {
|
||||
use: true, // 是否启用下拉刷新; 默认true
|
||||
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
|
||||
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
|
||||
isLock: false, // 是否锁定下拉刷新,默认false;
|
||||
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
|
||||
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
|
||||
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
|
||||
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textSuccess: '加载成功', // 加载成功的文本
|
||||
textErr: '加载失败', // 加载失败的文本
|
||||
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
|
||||
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
inited: null, // 下拉刷新初始化完毕的回调
|
||||
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
|
||||
outOffset: null, // 下拉的距离大于offset那一刻的回调
|
||||
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
|
||||
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
|
||||
showLoading: null, // 显示下拉刷新进度的回调
|
||||
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
|
||||
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
|
||||
endDownScroll: null, // 结束下拉刷新的回调
|
||||
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
|
||||
callback: function(mescroll) {
|
||||
// 下拉刷新的回调;默认重置上拉加载列表为第一页
|
||||
mescroll.resetUpScroll();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 配置参数:上拉加载 */
|
||||
MeScroll.prototype.extendUpScroll = function(optUp) {
|
||||
// 上拉加载的配置
|
||||
MeScroll.extend(optUp, {
|
||||
use: true, // 是否启用上拉加载; 默认true
|
||||
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
|
||||
isLock: false, // 是否锁定上拉加载,默认false;
|
||||
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
|
||||
callback: null, // 上拉加载的回调;function(page,mescroll){ }
|
||||
page: {
|
||||
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
|
||||
size: 10, // 每页数据的数量
|
||||
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
|
||||
},
|
||||
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
|
||||
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
|
||||
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
inited: null, // 初始化完毕的回调
|
||||
showLoading: null, // 显示加载中的回调
|
||||
showNoMore: null, // 显示无更多数据的回调
|
||||
hideUpScroll: null, // 隐藏上拉加载的回调
|
||||
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
|
||||
toTop: {
|
||||
// 回到顶部按钮,需配置src才显示
|
||||
src: null, // 图片路径,默认null (绝对路径或网络图)
|
||||
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
|
||||
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
|
||||
btnClick: null, // 点击按钮的回调
|
||||
onShow: null, // 是否显示的回调
|
||||
zIndex: 9990, // fixed定位z-index值
|
||||
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
|
||||
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
},
|
||||
empty: {
|
||||
use: true, // 是否显示空布局
|
||||
icon: null, // 图标路径
|
||||
tip: '~ 暂无相关数据 ~', // 提示
|
||||
btnText: '', // 按钮
|
||||
btnClick: null, // 点击按钮的回调
|
||||
onShow: null, // 是否显示的回调
|
||||
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
|
||||
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
|
||||
zIndex: 99 // fixed定位z-index值
|
||||
},
|
||||
onScroll: false // 是否监听滚动事件
|
||||
})
|
||||
}
|
||||
|
||||
/* 配置参数 */
|
||||
MeScroll.extend = function(userOption, defaultOption) {
|
||||
if (!userOption) return defaultOption;
|
||||
for (let key in defaultOption) {
|
||||
if (userOption[key] == null) {
|
||||
let def = defaultOption[key];
|
||||
if (def != null && typeof def === 'object') {
|
||||
userOption[key] = MeScroll.extend({}, def); // 深度匹配
|
||||
} else {
|
||||
userOption[key] = def;
|
||||
}
|
||||
} else if (typeof userOption[key] === 'object') {
|
||||
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
|
||||
}
|
||||
}
|
||||
return userOption;
|
||||
}
|
||||
|
||||
/* 简单判断是否配置了颜色 (非透明,非白色) */
|
||||
MeScroll.prototype.hasColor = function(color) {
|
||||
if(!color) return false;
|
||||
let c = color.toLowerCase();
|
||||
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
|
||||
}
|
||||
|
||||
/* -------初始化下拉刷新------- */
|
||||
MeScroll.prototype.initDownScroll = function() {
|
||||
let me = this;
|
||||
// 配置参数
|
||||
me.optDown = me.options.down || {};
|
||||
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
me.extendDownScroll(me.optDown);
|
||||
|
||||
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
|
||||
if(me.isScrollBody && me.optDown.native){
|
||||
me.optDown.use = false
|
||||
}else{
|
||||
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
|
||||
}
|
||||
|
||||
me.downHight = 0; // 下拉区域的高度
|
||||
|
||||
// 在页面中加入下拉布局
|
||||
if (me.optDown.use && me.optDown.inited) {
|
||||
// 初始化完毕的回调
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
me.optDown.inited(me);
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表touchstart事件 */
|
||||
MeScroll.prototype.touchstartEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
|
||||
this.startPoint = this.getPoint(e); // 记录起点
|
||||
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
|
||||
this.startAngle = 0; // 初始角度
|
||||
this.lastPoint = this.startPoint; // 重置上次move的点
|
||||
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||
this.inTouchend = false; // 标记不是touchend
|
||||
}
|
||||
|
||||
/* 列表touchmove事件 */
|
||||
MeScroll.prototype.touchmoveEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
let me = this;
|
||||
|
||||
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||
let curPoint = me.getPoint(e); // 当前点
|
||||
|
||||
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
||||
// 向下拉 && 在顶部
|
||||
// mescroll-body,直接判定在顶部即可
|
||||
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||
if (moveY > 0 && (
|
||||
(me.isScrollBody && scrollTop <= 0)
|
||||
||
|
||||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
|
||||
)) {
|
||||
// 可下拉的条件
|
||||
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
|
||||
me.optUp.isBoth))) {
|
||||
|
||||
// 下拉的初始角度是否在配置的范围内
|
||||
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||
|
||||
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
|
||||
me.inTouchend = true; // 标记执行touchend
|
||||
me.touchendEvent(); // 提前触发touchend
|
||||
return;
|
||||
}
|
||||
|
||||
me.preventDefault(e); // 阻止默认事件
|
||||
|
||||
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||
|
||||
// 下拉距离 < 指定距离
|
||||
if (me.downHight < me.optDown.offset) {
|
||||
if (me.movetype !== 1) {
|
||||
me.movetype = 1; // 加入标记,保证只执行一次
|
||||
me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
|
||||
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||
|
||||
// 指定距离 <= 下拉距离
|
||||
} else {
|
||||
if (me.movetype !== 2) {
|
||||
me.movetype = 2; // 加入标记,保证只执行一次
|
||||
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
if (diff > 0) { // 向下拉
|
||||
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||
} else { // 向上收
|
||||
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||
}
|
||||
}
|
||||
|
||||
me.downHight = Math.round(me.downHight) // 取整
|
||||
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||
}
|
||||
}
|
||||
|
||||
me.lastPoint = curPoint; // 记录本次移动的点
|
||||
}
|
||||
|
||||
/* 列表touchend事件 */
|
||||
MeScroll.prototype.touchendEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
// 如果下拉区域高度已改变,则需重置回来
|
||||
if (this.isMoveDown) {
|
||||
if (this.downHight >= this.optDown.offset) {
|
||||
// 符合触发刷新的条件
|
||||
this.triggerDownScroll();
|
||||
} else {
|
||||
// 不符合的话 则重置
|
||||
this.downHight = 0;
|
||||
this.endDownScrollCall(this);
|
||||
}
|
||||
this.movetype = 0;
|
||||
this.isMoveDown = false;
|
||||
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 上滑
|
||||
if (isScrollUp) {
|
||||
// 需检查滑动的角度
|
||||
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (angle > 80) {
|
||||
// 检查并触发上拉
|
||||
this.triggerUpScroll(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
MeScroll.prototype.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {
|
||||
x: e.touches[0].pageX,
|
||||
y: e.touches[0].pageY
|
||||
}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {
|
||||
x: e.changedTouches[0].pageX,
|
||||
y: e.changedTouches[0].pageY
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算两点之间的角度: 区间 [0,90]*/
|
||||
MeScroll.prototype.getAngle = function(p1, p2) {
|
||||
let x = Math.abs(p1.x - p2.x);
|
||||
let y = Math.abs(p1.y - p2.y);
|
||||
let z = Math.sqrt(x * x + y * y);
|
||||
let angle = 0;
|
||||
if (z !== 0) {
|
||||
angle = Math.asin(y / z) / Math.PI * 180;
|
||||
}
|
||||
return angle
|
||||
}
|
||||
|
||||
/* 触发下拉刷新 */
|
||||
MeScroll.prototype.triggerDownScroll = function() {
|
||||
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
|
||||
//return true则处于完全自定义状态
|
||||
} else {
|
||||
this.showDownScroll(); // 下拉刷新中...
|
||||
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示下拉进度布局 */
|
||||
MeScroll.prototype.showDownScroll = function() {
|
||||
this.isDownScrolling = true; // 标记下拉中
|
||||
if (this.optDown.native) {
|
||||
uni.startPullDownRefresh(); // 系统自带的下拉刷新
|
||||
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
} else{
|
||||
this.downHight = this.optDown.offset; // 更新下拉区域高度
|
||||
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.prototype.showDownLoadingCall = function(downHight) {
|
||||
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
|
||||
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
|
||||
}
|
||||
|
||||
/* 显示系统自带的下拉刷新时需要处理的业务 */
|
||||
MeScroll.prototype.onPullDownRefresh = function() {
|
||||
this.isDownScrolling = true; // 标记下拉中
|
||||
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
|
||||
/* 结束下拉刷新 */
|
||||
MeScroll.prototype.endDownScroll = function() {
|
||||
if (this.optDown.native) { // 结束原生下拉刷新
|
||||
this.isDownScrolling = false;
|
||||
this.endDownScrollCall(this);
|
||||
uni.stopPullDownRefresh();
|
||||
return
|
||||
}
|
||||
let me = this;
|
||||
// 结束下拉刷新的方法
|
||||
let endScroll = function() {
|
||||
me.downHight = 0;
|
||||
me.isDownScrolling = false;
|
||||
me.endDownScrollCall(me);
|
||||
if(!me.isScrollBody){
|
||||
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||||
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
|
||||
}
|
||||
}
|
||||
// 结束下拉刷新时的回调
|
||||
let delay = 0;
|
||||
if (me.optDown.beforeEndDownScroll) {
|
||||
delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
|
||||
if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
|
||||
}
|
||||
if (typeof delay === 'number' && delay > 0) {
|
||||
setTimeout(endScroll, delay);
|
||||
} else {
|
||||
endScroll();
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.prototype.endDownScrollCall = function() {
|
||||
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
|
||||
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
|
||||
}
|
||||
|
||||
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
|
||||
MeScroll.prototype.lockDownScroll = function(isLock) {
|
||||
if (isLock == null) isLock = true;
|
||||
this.optDown.isLock = isLock;
|
||||
}
|
||||
|
||||
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
|
||||
MeScroll.prototype.lockUpScroll = function(isLock) {
|
||||
if (isLock == null) isLock = true;
|
||||
this.optUp.isLock = isLock;
|
||||
}
|
||||
|
||||
/* -------初始化上拉加载------- */
|
||||
MeScroll.prototype.initUpScroll = function() {
|
||||
let me = this;
|
||||
// 配置参数
|
||||
me.optUp = me.options.up || {use: false}
|
||||
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
me.extendUpScroll(me.optUp);
|
||||
|
||||
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
|
||||
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
|
||||
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
|
||||
|
||||
// 初始化完毕的回调
|
||||
if (me.optUp.inited) {
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
me.optUp.inited(me);
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/*滚动到底部的事件 (仅mescroll-body生效)*/
|
||||
MeScroll.prototype.onReachBottom = function() {
|
||||
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
|
||||
if (!this.optUp.isLock && this.optUp.hasNext) {
|
||||
this.triggerUpScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*列表滚动事件 (仅mescroll-body生效)*/
|
||||
MeScroll.prototype.onPageScroll = function(e) {
|
||||
if (!this.isScrollBody) return;
|
||||
|
||||
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
|
||||
this.setScrollTop(e.scrollTop);
|
||||
|
||||
// 顶部按钮的显示隐藏
|
||||
if (e.scrollTop >= this.optUp.toTop.offset) {
|
||||
this.showTopBtn();
|
||||
} else {
|
||||
this.hideTopBtn();
|
||||
}
|
||||
}
|
||||
|
||||
/*列表滚动事件*/
|
||||
MeScroll.prototype.scroll = function(e, onScroll) {
|
||||
// 更新滚动条的位置
|
||||
this.setScrollTop(e.scrollTop);
|
||||
// 更新滚动内容高度
|
||||
this.setScrollHeight(e.scrollHeight);
|
||||
|
||||
// 向上滑还是向下滑动
|
||||
if (this.preScrollY == null) this.preScrollY = 0;
|
||||
this.isScrollUp = e.scrollTop - this.preScrollY > 0;
|
||||
this.preScrollY = e.scrollTop;
|
||||
|
||||
// 上滑 && 检查并触发上拉
|
||||
this.isScrollUp && this.triggerUpScroll(true);
|
||||
|
||||
// 顶部按钮的显示隐藏
|
||||
if (e.scrollTop >= this.optUp.toTop.offset) {
|
||||
this.showTopBtn();
|
||||
} else {
|
||||
this.hideTopBtn();
|
||||
}
|
||||
|
||||
// 滑动监听
|
||||
this.optUp.onScroll && onScroll && onScroll()
|
||||
}
|
||||
|
||||
/* 触发上拉加载 */
|
||||
MeScroll.prototype.triggerUpScroll = function(isCheck) {
|
||||
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
|
||||
// 是否校验在底部; 默认不校验
|
||||
if (isCheck === true) {
|
||||
let canUp = false;
|
||||
// 还有下一页 && 没有锁定 && 不在下拉中
|
||||
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
|
||||
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
|
||||
canUp = true; // 标记可上拉
|
||||
}
|
||||
}
|
||||
if (canUp === false) return;
|
||||
}
|
||||
this.showUpScroll(); // 上拉加载中...
|
||||
this.optUp.page.num++; // 预先加一页,如果失败则减回
|
||||
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.optUp.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示上拉加载中 */
|
||||
MeScroll.prototype.showUpScroll = function() {
|
||||
this.isUpScrolling = true; // 标记上拉加载中
|
||||
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
|
||||
}
|
||||
|
||||
/* 显示上拉无更多数据 */
|
||||
MeScroll.prototype.showNoMore = function() {
|
||||
this.optUp.hasNext = false; // 标记无更多数据
|
||||
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
|
||||
}
|
||||
|
||||
/* 隐藏上拉区域**/
|
||||
MeScroll.prototype.hideUpScroll = function() {
|
||||
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
|
||||
}
|
||||
|
||||
/* 结束上拉加载 */
|
||||
MeScroll.prototype.endUpScroll = function(isShowNoMore) {
|
||||
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
|
||||
if (isShowNoMore) {
|
||||
this.showNoMore(); // isShowNoMore=true,显示无更多数据
|
||||
} else {
|
||||
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
|
||||
}
|
||||
}
|
||||
this.isUpScrolling = false; // 标记结束上拉加载
|
||||
}
|
||||
|
||||
/* 重置上拉加载列表为第一页
|
||||
*isShowLoading 是否显示进度布局;
|
||||
* 1.默认null,不传参,则显示上拉加载的进度布局
|
||||
* 2.传参true, 则显示下拉刷新的进度布局
|
||||
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
|
||||
*/
|
||||
MeScroll.prototype.resetUpScroll = function(isShowLoading) {
|
||||
if (this.optUp && this.optUp.use) {
|
||||
let page = this.optUp.page;
|
||||
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
|
||||
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
|
||||
page.num = this.startNum; // 重置为第一页
|
||||
page.time = null; // 重置时间为空
|
||||
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
|
||||
if (isShowLoading == null) {
|
||||
this.removeEmpty(); // 移除空布局
|
||||
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
|
||||
} else {
|
||||
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
|
||||
}
|
||||
}
|
||||
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置page.num的值 */
|
||||
MeScroll.prototype.setPageNum = function(num) {
|
||||
this.optUp.page.num = num - 1;
|
||||
}
|
||||
|
||||
/* 设置page.size的值 */
|
||||
MeScroll.prototype.setPageSize = function(size) {
|
||||
this.optUp.page.size = size;
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据量(必传)
|
||||
* totalPage: 总页数(必传)
|
||||
* systime: 服务器时间 (可空)
|
||||
*/
|
||||
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
|
||||
let hasNext;
|
||||
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
|
||||
this.endSuccess(dataSize, hasNext, systime);
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据量(必传)
|
||||
* totalSize: 列表所有数据总数量(必传)
|
||||
* systime: 服务器时间 (可空)
|
||||
*/
|
||||
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
|
||||
let hasNext;
|
||||
if (this.optUp.use && totalSize != null) {
|
||||
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
|
||||
hasNext = loadSize < totalSize; // 是否还有下一页
|
||||
}
|
||||
this.endSuccess(dataSize, hasNext, systime);
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
|
||||
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
|
||||
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
|
||||
*/
|
||||
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
|
||||
let me = this;
|
||||
// 结束下拉刷新
|
||||
if (me.isDownScrolling) {
|
||||
me.isDownEndSuccess = true
|
||||
me.endDownScroll();
|
||||
}
|
||||
|
||||
// 结束上拉加载
|
||||
if (me.optUp.use) {
|
||||
let isShowNoMore; // 是否已无更多数据
|
||||
if (dataSize != null) {
|
||||
let pageNum = me.optUp.page.num; // 当前页码
|
||||
let pageSize = me.optUp.page.size; // 每页长度
|
||||
// 如果是第一页
|
||||
if (pageNum === 1) {
|
||||
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
|
||||
}
|
||||
if (dataSize < pageSize || hasNext === false) {
|
||||
// 返回的数据不满一页时,则说明已无更多数据
|
||||
me.optUp.hasNext = false;
|
||||
if (dataSize === 0 && pageNum === 1) {
|
||||
// 如果第一页无任何数据且配置了空布局
|
||||
isShowNoMore = false;
|
||||
me.showEmpty();
|
||||
} else {
|
||||
// 总列表数少于配置的数量,则不显示无更多数据
|
||||
let allDataSize = (pageNum - 1) * pageSize + dataSize;
|
||||
if (allDataSize < me.optUp.noMoreSize) {
|
||||
isShowNoMore = false;
|
||||
} else {
|
||||
isShowNoMore = true;
|
||||
}
|
||||
me.removeEmpty(); // 移除空布局
|
||||
}
|
||||
} else {
|
||||
// 还有下一页
|
||||
isShowNoMore = false;
|
||||
me.optUp.hasNext = true;
|
||||
me.removeEmpty(); // 移除空布局
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏上拉
|
||||
me.endUpScroll(isShowNoMore);
|
||||
}
|
||||
}
|
||||
|
||||
/* 回调失败,结束下拉刷新和上拉加载 */
|
||||
MeScroll.prototype.endErr = function(errDistance) {
|
||||
// 结束下拉,回调失败重置回原来的页码和时间
|
||||
if (this.isDownScrolling) {
|
||||
this.isDownEndSuccess = false
|
||||
let page = this.optUp.page;
|
||||
if (page && this.prePageNum) {
|
||||
page.num = this.prePageNum;
|
||||
page.time = this.prePageTime;
|
||||
}
|
||||
this.endDownScroll();
|
||||
}
|
||||
// 结束上拉,回调失败重置回原来的页码
|
||||
if (this.isUpScrolling) {
|
||||
this.optUp.page.num--;
|
||||
this.endUpScroll(false);
|
||||
// 如果是mescroll-body,则需往回滚一定距离
|
||||
if(this.isScrollBody && errDistance !== 0){ // 不处理0
|
||||
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
|
||||
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示空布局 */
|
||||
MeScroll.prototype.showEmpty = function() {
|
||||
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
|
||||
}
|
||||
|
||||
/* 移除空布局 */
|
||||
MeScroll.prototype.removeEmpty = function() {
|
||||
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
|
||||
}
|
||||
|
||||
/* 显示回到顶部的按钮 */
|
||||
MeScroll.prototype.showTopBtn = function() {
|
||||
if (!this.topBtnShow) {
|
||||
this.topBtnShow = true;
|
||||
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏回到顶部的按钮 */
|
||||
MeScroll.prototype.hideTopBtn = function() {
|
||||
if (this.topBtnShow) {
|
||||
this.topBtnShow = false;
|
||||
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
MeScroll.prototype.getScrollTop = function() {
|
||||
return this.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 记录滚动条的位置 */
|
||||
MeScroll.prototype.setScrollTop = function(y) {
|
||||
this.scrollTop = y;
|
||||
}
|
||||
|
||||
/* 滚动到指定位置 */
|
||||
MeScroll.prototype.scrollTo = function(y, t) {
|
||||
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
|
||||
}
|
||||
|
||||
/* 自定义scrollTo */
|
||||
MeScroll.prototype.resetScrollTo = function(myScrollTo) {
|
||||
this.myScrollTo = myScrollTo
|
||||
}
|
||||
|
||||
/* 滚动条到底部的距离 */
|
||||
MeScroll.prototype.getScrollBottom = function() {
|
||||
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
|
||||
}
|
||||
|
||||
/* 计步器
|
||||
star: 开始值
|
||||
end: 结束值
|
||||
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
|
||||
t: 计步时长,传0则直接回调end值;不传则默认300ms
|
||||
rate: 周期;不传则默认30ms计步一次
|
||||
* */
|
||||
MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
|
||||
let diff = end - star; // 差值
|
||||
if (t === 0 || diff === 0) {
|
||||
callback && callback(end);
|
||||
return;
|
||||
}
|
||||
t = t || 300; // 时长 300ms
|
||||
rate = rate || 30; // 周期 30ms
|
||||
let count = t / rate; // 次数
|
||||
let step = diff / count; // 步长
|
||||
let i = 0; // 计数
|
||||
let timer = setInterval(function() {
|
||||
if (i < count - 1) {
|
||||
star += step;
|
||||
callback && callback(star, timer);
|
||||
i++;
|
||||
} else {
|
||||
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, rate);
|
||||
}
|
||||
|
||||
/* 滚动容器的高度 */
|
||||
MeScroll.prototype.getClientHeight = function(isReal) {
|
||||
let h = this.clientHeight || 0
|
||||
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
|
||||
h = this.getBodyHeight()
|
||||
}
|
||||
return h
|
||||
}
|
||||
MeScroll.prototype.setClientHeight = function(h) {
|
||||
this.clientHeight = h;
|
||||
}
|
||||
|
||||
/* 滚动内容的高度 */
|
||||
MeScroll.prototype.getScrollHeight = function() {
|
||||
return this.scrollHeight || 0;
|
||||
}
|
||||
MeScroll.prototype.setScrollHeight = function(h) {
|
||||
this.scrollHeight = h;
|
||||
}
|
||||
|
||||
/* body的高度 */
|
||||
MeScroll.prototype.getBodyHeight = function() {
|
||||
return this.bodyHeight || 0;
|
||||
}
|
||||
MeScroll.prototype.setBodyHeight = function(h) {
|
||||
this.bodyHeight = h;
|
||||
}
|
||||
|
||||
/* 阻止浏览器默认滚动事件 */
|
||||
MeScroll.prototype.preventDefault = function(e) {
|
||||
// 小程序不支持e.preventDefault, 已在wxs中禁止
|
||||
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
|
||||
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
|
||||
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
|
||||
}
|
||||
424
components/mescroll-uni/mescroll-uni.vue
Executable file
424
components/mescroll-uni/mescroll-uni.vue
Executable file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<view class="mescroll-uni-warp">
|
||||
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
|
||||
<view class="mescroll-uni-content mescroll-render-touch"
|
||||
@touchstart="wxsBiz.touchstartEvent"
|
||||
@touchmove="wxsBiz.touchmoveEvent"
|
||||
@touchend="wxsBiz.touchendEvent"
|
||||
@touchcancel="wxsBiz.touchendEvent"
|
||||
:change:prop="wxsBiz.propObserver"
|
||||
:prop="wxsProp">
|
||||
<!-- 状态栏 -->
|
||||
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
|
||||
|
||||
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
|
||||
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
|
||||
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
|
||||
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 空布局 -->
|
||||
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
|
||||
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
|
||||
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="upLoadType===1">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 适配iPhoneX -->
|
||||
<view v-if="safearea" class="mescroll-safearea"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
|
||||
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
|
||||
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- app, h5使用renderjs -->
|
||||
<!-- #ifdef APP-PLUS || H5 -->
|
||||
<script module="renderBiz" lang="renderjs">
|
||||
import renderBiz from './wxs/renderjs.js';
|
||||
export default {
|
||||
mixins:[renderBiz]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
|
||||
<script>
|
||||
// 引入mescroll-uni.js,处理核心逻辑
|
||||
import MeScroll from './mescroll-uni.js';
|
||||
// 引入全局配置
|
||||
import GlobalOption from './mescroll-uni-option.js';
|
||||
// 引入空布局组件
|
||||
import MescrollEmpty from './components/mescroll-empty.vue';
|
||||
// 引入回到顶部组件
|
||||
import MescrollTop from './components/mescroll-top.vue';
|
||||
// 引入兼容wxs(含renderjs)写法的mixins
|
||||
import WxsMixin from './wxs/mixins.js';
|
||||
|
||||
export default {
|
||||
mixins: [WxsMixin],
|
||||
components: {
|
||||
MescrollEmpty,
|
||||
MescrollTop
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mescroll: {optDown:{},optUp:{}}, // mescroll实例
|
||||
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
|
||||
downHight: 0, //下拉刷新: 容器高度
|
||||
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
|
||||
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
|
||||
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
|
||||
isShowEmpty: false, // 是否显示空布局
|
||||
isShowToTop: false, // 是否显示回到顶部按钮
|
||||
scrollTop: 0, // 滚动条的位置
|
||||
scrollAnim: false, // 是否开启滚动动画
|
||||
windowTop: 0, // 可使用窗口的顶部位置
|
||||
windowBottom: 0, // 可使用窗口的底部位置
|
||||
windowHeight: 0, // 可使用窗口的高度
|
||||
statusBarHeight: 0 // 状态栏高度
|
||||
}
|
||||
},
|
||||
props: {
|
||||
down: Object, // 下拉刷新的参数配置
|
||||
up: Object, // 上拉加载的参数配置
|
||||
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
|
||||
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
|
||||
fixed: { // 是否通过fixed固定mescroll的高度, 默认true
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 是否使用fixed定位 (当height有值,则不使用)
|
||||
isFixed(){
|
||||
return !this.height && this.fixed
|
||||
},
|
||||
// mescroll的高度
|
||||
scrollHeight(){
|
||||
if (this.isFixed) {
|
||||
return "auto"
|
||||
} else if(this.height){
|
||||
return this.toPx(this.height) + 'px'
|
||||
}else{
|
||||
return "100%"
|
||||
}
|
||||
},
|
||||
// 下拉布局往下偏移的距离 (px)
|
||||
numTop() {
|
||||
return this.toPx(this.top)
|
||||
},
|
||||
fixedTop() {
|
||||
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
|
||||
},
|
||||
padTop() {
|
||||
return !this.isFixed ? this.numTop + 'px' : 0
|
||||
},
|
||||
// 上拉布局往上偏移 (px)
|
||||
numBottom() {
|
||||
return this.toPx(this.bottom)
|
||||
},
|
||||
fixedBottom() {
|
||||
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
|
||||
},
|
||||
padBottom() {
|
||||
return !this.isFixed ? this.numBottom + 'px' : 0
|
||||
},
|
||||
// 是否为重置下拉的状态
|
||||
isDownReset(){
|
||||
return this.downLoadType===3 || this.downLoadType===4
|
||||
},
|
||||
// 过渡
|
||||
transition() {
|
||||
return this.isDownReset ? 'transform 300ms' : '';
|
||||
},
|
||||
translateY() {
|
||||
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 列表是否可滑动
|
||||
scrollable(){
|
||||
return this.downLoadType===0 || this.isDownReset
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.downLoadType === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.downRate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
|
||||
switch (this.downLoadType){
|
||||
case 1: return this.mescroll.optDown.textInOffset;
|
||||
case 2: return this.mescroll.optDown.textOutOffset;
|
||||
case 3: return this.mescroll.optDown.textLoading;
|
||||
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
|
||||
default: return this.mescroll.optDown.textInOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//number,rpx,upx,px,% --> px的数值
|
||||
toPx(num){
|
||||
if(typeof num === "string"){
|
||||
if (num.indexOf('px') !== -1) {
|
||||
if(num.indexOf('rpx') !== -1) { // "10rpx"
|
||||
num = num.replace('rpx', '');
|
||||
} else if(num.indexOf('upx') !== -1) { // "10upx"
|
||||
num = num.replace('upx', '');
|
||||
} else { // "10px"
|
||||
return Number(num.replace('px', ''))
|
||||
}
|
||||
}else if (num.indexOf('%') !== -1){
|
||||
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
|
||||
let rate = Number(num.replace("%","")) / 100
|
||||
return this.windowHeight * rate
|
||||
}
|
||||
}
|
||||
return num ? uni.upx2px(Number(num)) : 0
|
||||
},
|
||||
//注册列表滚动事件,用于下拉刷新和上拉加载
|
||||
scroll(e) {
|
||||
this.mescroll.scroll(e.detail, () => {
|
||||
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
|
||||
})
|
||||
},
|
||||
// 点击空布局的按钮回调
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick', this.mescroll)
|
||||
},
|
||||
// 点击回到顶部的按钮回调
|
||||
toTopClick() {
|
||||
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
|
||||
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
|
||||
},
|
||||
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
|
||||
setClientHeight() {
|
||||
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
|
||||
this.isExec = true; // 避免多次获取
|
||||
this.$nextTick(() => { // 确保dom已渲染
|
||||
this.getClientInfo(data=>{
|
||||
this.isExec = false;
|
||||
if (data) {
|
||||
this.mescroll.setClientHeight(data.height);
|
||||
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
|
||||
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
|
||||
setTimeout(() => {
|
||||
this.setClientHeight()
|
||||
}, this.clientNum * 100)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
// 获取滚动区域的信息
|
||||
getClientInfo(success){
|
||||
let query = uni.createSelectorQuery();
|
||||
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
|
||||
// #endif
|
||||
let view = query.select('#' + this.viewId);
|
||||
view.boundingClientRect(data => {
|
||||
success(data)
|
||||
}).exec();
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
created() {
|
||||
let vm = this;
|
||||
|
||||
let diyOption = {
|
||||
// 下拉刷新的配置
|
||||
down: {
|
||||
inOffset() {
|
||||
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
outOffset() {
|
||||
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
onMoving(mescroll, rate, downHight) {
|
||||
// 下拉过程中的回调,滑动过程一直在执行;
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
showLoading(mescroll, downHight) {
|
||||
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
beforeEndDownScroll(mescroll){
|
||||
vm.downLoadType = 4;
|
||||
return mescroll.optDown.beforeEndDelay // 延时结束的时长
|
||||
},
|
||||
endDownScroll() {
|
||||
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downResetTimer && clearTimeout(vm.downResetTimer)
|
||||
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
|
||||
if(vm.downLoadType===4) vm.downLoadType = 0
|
||||
},300)
|
||||
},
|
||||
// 派发下拉刷新的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('down', mescroll)
|
||||
}
|
||||
},
|
||||
// 上拉加载的配置
|
||||
up: {
|
||||
// 显示加载中的回调
|
||||
showLoading() {
|
||||
vm.upLoadType = 1;
|
||||
},
|
||||
// 显示无更多数据的回调
|
||||
showNoMore() {
|
||||
vm.upLoadType = 2;
|
||||
},
|
||||
// 隐藏上拉加载的回调
|
||||
hideUpScroll(mescroll) {
|
||||
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
|
||||
},
|
||||
// 空布局
|
||||
empty: {
|
||||
onShow(isShow) { // 显示隐藏的回调
|
||||
vm.isShowEmpty = isShow;
|
||||
}
|
||||
},
|
||||
// 回到顶部
|
||||
toTop: {
|
||||
onShow(isShow) { // 显示隐藏的回调
|
||||
vm.isShowToTop = isShow;
|
||||
}
|
||||
},
|
||||
// 派发上拉加载的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('up', mescroll);
|
||||
// 更新容器的高度 (多mescroll的情况)
|
||||
vm.setClientHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
|
||||
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
|
||||
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
|
||||
|
||||
// 初始化MeScroll对象
|
||||
vm.mescroll = new MeScroll(myOption);
|
||||
vm.mescroll.viewId = vm.viewId; // 附带id
|
||||
// init回调mescroll对象
|
||||
vm.$emit('init', vm.mescroll);
|
||||
|
||||
// 设置高度
|
||||
const sys = uni.getSystemInfoSync();
|
||||
if(sys.windowTop) vm.windowTop = sys.windowTop;
|
||||
if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
|
||||
if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
|
||||
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是scrollview,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
|
||||
if(typeof y === 'string'){
|
||||
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
|
||||
vm.getClientInfo(function(rect){
|
||||
let mescrollTop = rect.top // mescroll到顶部的距离
|
||||
let selector;
|
||||
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
|
||||
selector = '#'+y // 不带#和. 则默认为id选择器
|
||||
}else{
|
||||
selector = y
|
||||
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
|
||||
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
|
||||
selector = y.split('>>>')[1].trim()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
|
||||
if (rect) {
|
||||
let curY = vm.mescroll.getScrollTop()
|
||||
let top = rect.top - mescrollTop
|
||||
top += curY
|
||||
if(!vm.isFixed) top -= vm.numTop
|
||||
vm.scrollTop = curY;
|
||||
vm.$nextTick(function() {
|
||||
vm.scrollTop = top
|
||||
})
|
||||
} else{
|
||||
console.error(selector + ' does not exist');
|
||||
}
|
||||
}).exec()
|
||||
})
|
||||
return;
|
||||
}
|
||||
let curY = vm.mescroll.getScrollTop()
|
||||
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
|
||||
vm.scrollTop = curY;
|
||||
vm.$nextTick(function() {
|
||||
vm.scrollTop = y
|
||||
})
|
||||
} else {
|
||||
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
|
||||
vm.scrollTop = step
|
||||
}, t)
|
||||
}
|
||||
})
|
||||
|
||||
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
|
||||
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
|
||||
vm.mescroll.optUp.toTop.safearea = vm.safearea;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 设置容器的高度
|
||||
this.setClientHeight()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./mescroll-uni.css";
|
||||
@import "./components/mescroll-down.css";
|
||||
@import './components/mescroll-up.css';
|
||||
</style>
|
||||
48
components/mescroll-uni/mixins/mescroll-comp.js
Executable file
48
components/mescroll-uni/mixins/mescroll-comp.js
Executable file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
|
||||
*/
|
||||
const MescrollCompMixin = {
|
||||
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
|
||||
onPageScroll(e) {
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom() {
|
||||
this.handleReachBottom()
|
||||
},
|
||||
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
onPullDownRefresh(){
|
||||
this.handlePullDownRefresh()
|
||||
},
|
||||
// mescroll-body写在子子子...组件的情况 (多级)
|
||||
data() {
|
||||
return {
|
||||
mescroll: {
|
||||
onPageScroll: e=>{
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom: ()=>{
|
||||
this.handleReachBottom()
|
||||
},
|
||||
onPullDownRefresh: ()=>{
|
||||
this.handlePullDownRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
handlePageScroll(e){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onPageScroll(e);
|
||||
},
|
||||
handleReachBottom(){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onReachBottom();
|
||||
},
|
||||
handlePullDownRefresh(){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onPullDownRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollCompMixin;
|
||||
59
components/mescroll-uni/mixins/mescroll-more-item.js
Executable file
59
components/mescroll-uni/mixins/mescroll-more-item.js
Executable file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
|
||||
*/
|
||||
const MescrollMoreItemMixin = {
|
||||
// 支付宝小程序不支持props的mixin,需写在具体的页面中
|
||||
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||
props:{
|
||||
i: Number, // 每个tab页的专属下标
|
||||
index: { // 当前tab的下标
|
||||
type: Number,
|
||||
default(){
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
data() {
|
||||
return {
|
||||
downOption:{
|
||||
auto:false // 不自动加载
|
||||
},
|
||||
upOption:{
|
||||
auto:false // 不自动加载
|
||||
},
|
||||
isInit: false // 当前tab是否已初始化
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
// 监听下标的变化
|
||||
index(val){
|
||||
if (this.i === val && !this.isInit) {
|
||||
this.isInit = true; // 标记为true
|
||||
this.mescroll && this.mescroll.triggerDownScroll();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
mescrollInitByRef() {
|
||||
if(!this.mescroll || !this.mescroll.resetUpScroll){
|
||||
// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
|
||||
let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
|
||||
if(mescrollRef) this.mescroll = mescrollRef.mescroll
|
||||
}
|
||||
},
|
||||
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
|
||||
mescrollInit(mescroll) {
|
||||
this.mescroll = mescroll;
|
||||
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
// 自动加载当前tab的数据
|
||||
if(this.i === this.index){
|
||||
this.isInit = true; // 标记为true
|
||||
this.mescroll.triggerDownScroll();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollMoreItemMixin;
|
||||
74
components/mescroll-uni/mixins/mescroll-more.js
Executable file
74
components/mescroll-uni/mixins/mescroll-more.js
Executable file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
|
||||
*/
|
||||
const MescrollMoreMixin = {
|
||||
data() {
|
||||
return {
|
||||
tabIndex: 0, // 当前tab下标
|
||||
mescroll: {
|
||||
onPageScroll: e=>{
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom: ()=>{
|
||||
this.handleReachBottom()
|
||||
},
|
||||
onPullDownRefresh: ()=>{
|
||||
this.handlePullDownRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
|
||||
onPageScroll(e) {
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom() {
|
||||
this.handleReachBottom()
|
||||
},
|
||||
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
onPullDownRefresh(){
|
||||
this.handlePullDownRefresh()
|
||||
},
|
||||
methods:{
|
||||
handlePageScroll(e){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onPageScroll(e);
|
||||
},
|
||||
handleReachBottom(){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onReachBottom();
|
||||
},
|
||||
handlePullDownRefresh(){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onPullDownRefresh();
|
||||
},
|
||||
// 根据下标获取对应子组件的mescroll
|
||||
getMescroll(i){
|
||||
if(!this.mescrollItems) this.mescrollItems = [];
|
||||
if(!this.mescrollItems[i]) {
|
||||
// v-for中的refs
|
||||
let vForItem = this.$refs["mescrollItem"];
|
||||
if(vForItem){
|
||||
this.mescrollItems[i] = vForItem[i]
|
||||
}else{
|
||||
// 普通的refs,不可重复
|
||||
this.mescrollItems[i] = this.$refs["mescrollItem"+i];
|
||||
}
|
||||
}
|
||||
let item = this.mescrollItems[i]
|
||||
return item ? item.mescroll : null
|
||||
},
|
||||
// 切换tab,恢复滚动条位置
|
||||
tabChange(i){
|
||||
let mescroll = this.getMescroll(i);
|
||||
if(mescroll){
|
||||
// 延时(比$nextTick靠谱一些),确保元素已渲染
|
||||
setTimeout(()=>{
|
||||
mescroll.scrollTo(mescroll.getScrollTop(),0)
|
||||
},30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollMoreMixin;
|
||||
109
components/mescroll-uni/wxs/mixins.js
Executable file
109
components/mescroll-uni/wxs/mixins.js
Executable file
@@ -0,0 +1,109 @@
|
||||
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
|
||||
const WxsMixin = {
|
||||
data() {
|
||||
return {
|
||||
// 传入wxs视图层的数据 (响应式)
|
||||
wxsProp: {
|
||||
optDown:{}, // 下拉刷新的配置
|
||||
scrollTop:0, // 滚动条的距离
|
||||
bodyHeight:0, // body的高度
|
||||
isDownScrolling:false, // 是否正在下拉刷新中
|
||||
isUpScrolling:false, // 是否正在上拉加载中
|
||||
isScrollBody:true, // 是否为mescroll-body滚动
|
||||
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
|
||||
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
},
|
||||
|
||||
// 标记调用wxs视图层的方法
|
||||
callProp: {
|
||||
callType: '', // 方法名
|
||||
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
},
|
||||
|
||||
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
|
||||
// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
wxsBiz: {
|
||||
//注册列表touchstart事件,用于下拉刷新
|
||||
touchstartEvent: e=> {
|
||||
this.mescroll.touchstartEvent(e);
|
||||
},
|
||||
//注册列表touchmove事件,用于下拉刷新
|
||||
touchmoveEvent: e=> {
|
||||
this.mescroll.touchmoveEvent(e);
|
||||
},
|
||||
//注册列表touchend事件,用于下拉刷新
|
||||
touchendEvent: e=> {
|
||||
this.mescroll.touchendEvent(e);
|
||||
},
|
||||
propObserver(){}, // 抹平wxs的写法
|
||||
callObserver(){} // 抹平wxs的写法
|
||||
},
|
||||
// #endif
|
||||
|
||||
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
|
||||
// #ifndef APP-PLUS || H5
|
||||
renderBiz: {
|
||||
propObserver(){} // 抹平renderjs的写法
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// wxs视图层调用逻辑层的回调
|
||||
wxsCall(msg){
|
||||
if(msg.type === 'setWxsProp'){
|
||||
// 更新wxsProp数据 (值改变才触发更新)
|
||||
this.wxsProp = {
|
||||
optDown: this.mescroll.optDown,
|
||||
scrollTop: this.mescroll.getScrollTop(),
|
||||
bodyHeight: this.mescroll.getBodyHeight(),
|
||||
isDownScrolling: this.mescroll.isDownScrolling,
|
||||
isUpScrolling: this.mescroll.isUpScrolling,
|
||||
isUpBoth: this.mescroll.optUp.isBoth,
|
||||
isScrollBody:this.mescroll.isScrollBody,
|
||||
t: Date.now()
|
||||
}
|
||||
}else if(msg.type === 'setLoadType'){
|
||||
// 设置inOffset,outOffset的状态
|
||||
this.downLoadType = msg.downLoadType
|
||||
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
|
||||
// 重置是否加载成功的状态
|
||||
this.$set(this.mescroll, 'isDownEndSuccess', null)
|
||||
}else if(msg.type === 'triggerDownScroll'){
|
||||
// 主动触发下拉刷新
|
||||
this.mescroll.triggerDownScroll();
|
||||
}else if(msg.type === 'endDownScroll'){
|
||||
// 结束下拉刷新
|
||||
this.mescroll.endDownScroll();
|
||||
}else if(msg.type === 'triggerUpScroll'){
|
||||
// 主动触发上拉加载
|
||||
this.mescroll.triggerUpScroll(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
// 配置主动触发wxs显示加载进度的回调
|
||||
this.mescroll.optDown.afterLoading = ()=>{
|
||||
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
}
|
||||
// 配置主动触发wxs隐藏加载进度的回调
|
||||
this.mescroll.optDown.afterEndDownScroll = ()=>{
|
||||
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
|
||||
setTimeout(()=>{
|
||||
if(this.downLoadType === 4 || this.downLoadType === 0){
|
||||
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
}
|
||||
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
|
||||
}, delay)
|
||||
}
|
||||
// 初始化wxs的数据
|
||||
this.wxsCall({type: 'setWxsProp'})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
export default WxsMixin;
|
||||
92
components/mescroll-uni/wxs/renderjs.js
Executable file
92
components/mescroll-uni/wxs/renderjs.js
Executable file
@@ -0,0 +1,92 @@
|
||||
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
|
||||
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
|
||||
// https://uniapp.dcloud.io/frame?id=renderjs
|
||||
|
||||
// 与wxs的me实例一致
|
||||
var me = {}
|
||||
|
||||
// 初始化window对象的touch事件 (仅初始化一次)
|
||||
if(window && !window.$mescrollRenderInit){
|
||||
window.$mescrollRenderInit = true
|
||||
|
||||
|
||||
window.addEventListener('touchstart', function(e){
|
||||
if (me.disabled()) return;
|
||||
me.startPoint = me.getPoint(e); // 记录起点
|
||||
}, {passive: true})
|
||||
|
||||
|
||||
window.addEventListener('touchmove', function(e){
|
||||
if (me.disabled()) return;
|
||||
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
|
||||
|
||||
var curPoint = me.getPoint(e); // 当前点
|
||||
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 向下拉
|
||||
if (moveY > 0) {
|
||||
// 可下拉的条件
|
||||
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
|
||||
|
||||
// 只有touch在mescroll的view上面,才禁止bounce
|
||||
var el = e.target;
|
||||
var isMescrollTouch = false;
|
||||
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
|
||||
var cls = el.classList;
|
||||
if (cls && cls.contains('mescroll-render-touch')) {
|
||||
isMescrollTouch = true
|
||||
break;
|
||||
}
|
||||
el = el.parentNode; // 继续检查其父元素
|
||||
}
|
||||
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
|
||||
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, {passive: false})
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
me.getScrollTop = function() {
|
||||
return me.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 是否禁用下拉刷新 */
|
||||
me.disabled = function(){
|
||||
return !me.optDown || !me.optDown.use || me.optDown.native
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
me.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {x: 0,y: 0}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
|
||||
} else {
|
||||
return {x: e.clientX,y: e.clientY}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (实时更新数据)
|
||||
*/
|
||||
function propObserver(wxsProp) {
|
||||
me.optDown = wxsProp.optDown
|
||||
me.scrollTop = wxsProp.scrollTop
|
||||
me.isDownScrolling = wxsProp.isDownScrolling
|
||||
me.isUpScrolling = wxsProp.isUpScrolling
|
||||
me.isUpBoth = wxsProp.isUpBoth
|
||||
}
|
||||
|
||||
/* 导出模块 */
|
||||
const renderBiz = {
|
||||
data() {
|
||||
return {
|
||||
propObserver: propObserver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default renderBiz;
|
||||
268
components/mescroll-uni/wxs/wxs.wxs
Executable file
268
components/mescroll-uni/wxs/wxs.wxs
Executable file
@@ -0,0 +1,268 @@
|
||||
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
|
||||
// https://uniapp.dcloud.io/frame?id=wxs
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
|
||||
|
||||
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
|
||||
var me = {}
|
||||
|
||||
// ------ 自定义下拉刷新动画 start ------
|
||||
|
||||
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
|
||||
me.onMoving = function (ins, rate, downHight){
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
|
||||
'transform': 'translateY(' + downHight + 'px)',
|
||||
'transition': ''
|
||||
})
|
||||
// 环形进度条
|
||||
var progress = ins.selectComponent('.mescroll-wxs-progress')
|
||||
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
|
||||
})
|
||||
}
|
||||
|
||||
/* 显示下拉刷新进度 */
|
||||
me.showLoading = function (ins){
|
||||
me.downHight = me.optDown.offset
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'auto',
|
||||
'transform': 'translateY(' + me.downHight + 'px)',
|
||||
'transition': 'transform 300ms'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* 结束下拉 */
|
||||
me.endDownScroll = function (ins){
|
||||
me.downHight = 0;
|
||||
me.isDownScrolling = false;
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'auto',
|
||||
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
|
||||
'transition': 'transform 300ms'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
|
||||
me.clearTransform = function (ins){
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': '',
|
||||
'transform': '',
|
||||
'transition': ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ------ 自定义下拉刷新动画 end ------
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (实时更新数据)
|
||||
*/
|
||||
function propObserver(wxsProp) {
|
||||
me.optDown = wxsProp.optDown
|
||||
me.scrollTop = wxsProp.scrollTop
|
||||
me.bodyHeight = wxsProp.bodyHeight
|
||||
me.isDownScrolling = wxsProp.isDownScrolling
|
||||
me.isUpScrolling = wxsProp.isUpScrolling
|
||||
me.isUpBoth = wxsProp.isUpBoth
|
||||
me.isScrollBody = wxsProp.isScrollBody
|
||||
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (调用wxs的方法)
|
||||
*/
|
||||
function callObserver(callProp, oldValue, ins) {
|
||||
if (me.disabled()) return;
|
||||
if(callProp.callType){
|
||||
// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
|
||||
if(callProp.callType === 'showLoading'){
|
||||
me.showLoading(ins)
|
||||
}else if(callProp.callType === 'endDownScroll'){
|
||||
me.endDownScroll(ins)
|
||||
}else if(callProp.callType === 'clearTransform'){
|
||||
me.clearTransform(ins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* touch事件
|
||||
*/
|
||||
function touchstartEvent(e, ins) {
|
||||
me.downHight = 0; // 下拉的距离
|
||||
me.startPoint = me.getPoint(e); // 记录起点
|
||||
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
|
||||
me.startAngle = 0; // 初始角度
|
||||
me.lastPoint = me.startPoint; // 重置上次move的点
|
||||
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||
me.inTouchend = false; // 标记不是touchend
|
||||
|
||||
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
|
||||
}
|
||||
|
||||
function touchmoveEvent(e, ins) {
|
||||
var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
|
||||
|
||||
if (me.disabled()) return isPrevent;
|
||||
|
||||
var scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||
var curPoint = me.getPoint(e); // 当前点
|
||||
|
||||
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
||||
// 向下拉 && 在顶部
|
||||
// mescroll-body,直接判定在顶部即可
|
||||
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||
if (moveY > 0 && (
|
||||
(me.isScrollBody && scrollTop <= 0)
|
||||
||
|
||||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
|
||||
)) {
|
||||
// 可下拉的条件
|
||||
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
|
||||
me.isUpBoth))) {
|
||||
|
||||
// 下拉的角度是否在配置的范围内
|
||||
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||
|
||||
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
|
||||
me.inTouchend = true; // 标记执行touchend
|
||||
touchendEvent(e, ins); // 提前触发touchend
|
||||
return isPrevent;
|
||||
}
|
||||
|
||||
isPrevent = false // 小程序是return false
|
||||
|
||||
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||
|
||||
// 下拉距离 < 指定距离
|
||||
if (me.downHight < me.optDown.offset) {
|
||||
if (me.movetype !== 1) {
|
||||
me.movetype = 1; // 加入标记,保证只执行一次
|
||||
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||
|
||||
// 指定距离 <= 下拉距离
|
||||
} else {
|
||||
if (me.movetype !== 2) {
|
||||
me.movetype = 2; // 加入标记,保证只执行一次
|
||||
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
if (diff > 0) { // 向下拉
|
||||
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||
} else { // 向上收
|
||||
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||
}
|
||||
}
|
||||
|
||||
me.downHight = Math.round(me.downHight) // 取整
|
||||
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||
me.onMoving(ins, rate, me.downHight)
|
||||
}
|
||||
}
|
||||
|
||||
me.lastPoint = curPoint; // 记录本次移动的点
|
||||
|
||||
return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
|
||||
}
|
||||
|
||||
function touchendEvent(e, ins) {
|
||||
// 如果下拉区域高度已改变,则需重置回来
|
||||
if (me.isMoveDown) {
|
||||
if (me.downHight >= me.optDown.offset) {
|
||||
// 符合触发刷新的条件
|
||||
me.downHight = me.optDown.offset; // 更新下拉区域高度
|
||||
// me.triggerDownScroll();
|
||||
me.callMethod(ins, {type: 'triggerDownScroll'})
|
||||
} else {
|
||||
// 不符合的话 则重置
|
||||
me.downHight = 0;
|
||||
// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
|
||||
me.callMethod(ins, {type: 'endDownScroll'})
|
||||
}
|
||||
me.movetype = 0;
|
||||
me.isMoveDown = false;
|
||||
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 上滑
|
||||
if (isScrollUp) {
|
||||
// 需检查滑动的角度
|
||||
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (angle > 80) {
|
||||
// 检查并触发上拉
|
||||
// me.triggerUpScroll(true);
|
||||
me.callMethod(ins, {type: 'triggerUpScroll'})
|
||||
}
|
||||
}
|
||||
}
|
||||
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
|
||||
}
|
||||
|
||||
/* 是否禁用下拉刷新 */
|
||||
me.disabled = function(){
|
||||
return !me.optDown || !me.optDown.use || me.optDown.native
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
me.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {x: 0,y: 0}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
|
||||
} else {
|
||||
return {x: e.clientX,y: e.clientY}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算两点之间的角度: 区间 [0,90]*/
|
||||
me.getAngle = function (p1, p2) {
|
||||
var x = Math.abs(p1.x - p2.x);
|
||||
var y = Math.abs(p1.y - p2.y);
|
||||
var z = Math.sqrt(x * x + y * y);
|
||||
var angle = 0;
|
||||
if (z !== 0) {
|
||||
angle = Math.asin(y / z) / Math.PI * 180;
|
||||
}
|
||||
return angle
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
me.getScrollTop = function() {
|
||||
return me.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 获取body的高度 */
|
||||
me.getBodyHeight = function() {
|
||||
return me.bodyHeight || 0;
|
||||
}
|
||||
|
||||
/* 调用逻辑层的方法 */
|
||||
me.callMethod = function(ins, param) {
|
||||
if(ins) ins.callMethod('wxsCall', param)
|
||||
}
|
||||
|
||||
/* 导出模块 */
|
||||
module.exports = {
|
||||
propObserver: propObserver,
|
||||
callObserver: callObserver,
|
||||
touchstartEvent: touchstartEvent,
|
||||
touchmoveEvent: touchmoveEvent,
|
||||
touchendEvent: touchendEvent
|
||||
}
|
||||
123
components/page/diyComponents/article/index.vue
Executable file
123
components/page/diyComponents/article/index.vue
Executable file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<!-- 文章组 -->
|
||||
<view class="diy-article">
|
||||
<view class="article-item" :class="[`show-type__${item.show_type}`]" v-for="(item, index) in dataList" :key="index"
|
||||
@click="onTargetDetail(item.article_id)">
|
||||
<!-- 小图模式 -->
|
||||
<block v-if="item.show_type == 10">
|
||||
<view class="article-item__left flex-box">
|
||||
<view class="article-item__title">
|
||||
<text class="twoline-hide">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="article-item__footer m-top10">
|
||||
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="article-item__image">
|
||||
<image class="image" mode="widthFix" :src="item.image_url"></image>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 大图模式 -->
|
||||
<block v-if="item.show_type == 20">
|
||||
<view class="article-item__title">
|
||||
<text class="twoline-hide">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="article-item__image m-top20">
|
||||
<image class="image" mode="widthFix" :src="item.image_url"></image>
|
||||
</view>
|
||||
<view class="article-item__footer m-top10">
|
||||
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Article",
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 跳转文章详情页
|
||||
*/
|
||||
onTargetDetail(id) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/article/detail?articleId=' + id
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-article {
|
||||
background: #f7f7f7;
|
||||
|
||||
.article-item {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.article-item__title {
|
||||
max-height: 74rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 38rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.article-item__image .image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* 小图模式 */
|
||||
|
||||
.show-type__10 {
|
||||
display: flex;
|
||||
|
||||
.article-item__left {
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.article-item__title {
|
||||
// min-height: 72rpx;
|
||||
}
|
||||
|
||||
.article-item__image .image {
|
||||
width: 240rpx;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 大图模式 */
|
||||
|
||||
.show-type__20 .article-item__image .image {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
166
components/page/diyComponents/banner/index.vue
Executable file
166
components/page/diyComponents/banner/index.vue
Executable file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<view class="diy-banner" :style="{ height: `${imgHeights[imgCurrent]}px` }">
|
||||
<!-- 图片轮播 -->
|
||||
<swiper
|
||||
:autoplay="autoplay"
|
||||
class="swiper-box"
|
||||
:duration="duration"
|
||||
:circular="true"
|
||||
:interval="itemStyle.interval * 1000"
|
||||
@change="_bindChange"
|
||||
>
|
||||
<swiper-item v-for="(dataItem, index) in dataList" :key="index">
|
||||
<image mode="widthFix" class="slide-image" :src="dataItem.imgUrl" @click="onLink(dataItem.link)" @load="_imagesHeight" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 指示点 -->
|
||||
<view class="indicator-dots" :class="itemStyle.btnShape">
|
||||
<view
|
||||
class="dots-item"
|
||||
:class="{ active: imgCurrent == index }"
|
||||
:style="{ backgroundColor: itemStyle.btnColor }"
|
||||
v-for="(dataItem, index) in dataList"
|
||||
:key="index"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin';
|
||||
|
||||
export default {
|
||||
name: 'Banner',
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 私有数据,组件的初始数据
|
||||
* 可用于模版渲染
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
windowWidth: 750,
|
||||
indicatorDots: false, // 是否显示面板指示点
|
||||
autoplay: true, // 是否自动切换
|
||||
duration: 800, // 滑动动画时长
|
||||
imgHeights: [], // 图片的高度
|
||||
imgCurrent: 0 // 当前banne所在滑块指针
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
const app = this;
|
||||
uni.getSystemInfo({
|
||||
success({ windowWidth }) {
|
||||
app.windowWidth = windowWidth > 750 ? 750 : windowWidth;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* 计算图片高度
|
||||
*/
|
||||
_imagesHeight({ detail }) {
|
||||
const app = this;
|
||||
// 获取图片真实宽度
|
||||
const { width, height } = detail;
|
||||
// 宽高比
|
||||
const ratio = width / height;
|
||||
// 计算的高度值
|
||||
const viewHeight = app.windowWidth / ratio;
|
||||
// 把每一张图片的高度记录到数组里
|
||||
app.imgHeights.push(viewHeight);
|
||||
},
|
||||
|
||||
/**
|
||||
* 记录当前指针
|
||||
*/
|
||||
_bindChange(e) {
|
||||
this.imgCurrent = e.detail.current;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-banner {
|
||||
position: relative;
|
||||
|
||||
// swiper组件
|
||||
.swiper-box {
|
||||
height: 100%;
|
||||
|
||||
.slide-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* 指示点 */
|
||||
.indicator-dots {
|
||||
width: 100%;
|
||||
height: 28rpx;
|
||||
padding: 0 20rpx;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 20rpx;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.dots-item {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
margin-right: 8rpx;
|
||||
background-color: #fff;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #313131 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形
|
||||
&.round .dots-item {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
// 正方形
|
||||
&.square .dots-item {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
}
|
||||
|
||||
// 长方形
|
||||
&.rectangle .dots-item {
|
||||
width: 22rpx;
|
||||
height: 14rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
184
components/page/diyComponents/bargain/index.vue
Executable file
184
components/page/diyComponents/bargain/index.vue
Executable file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<!-- 砍价商品组 -->
|
||||
<view class="diy-bargain" :style="{ background: itemStyle.background }">
|
||||
<view class="goods-item" v-for="(goods, idx) in dataList" :key="idx" @click="handleNavDetail(goods)">
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-image">
|
||||
<image class="image" :src="goods.goods_image"></image>
|
||||
</view>
|
||||
<view class="goods-info">
|
||||
<!-- 商品名称 -->
|
||||
<view v-if="inArray('goodsName', itemStyle.show)" class="goods-name">
|
||||
<text class="twoline-hide">{{ goods.goods_name }}</text>
|
||||
</view>
|
||||
<!-- 参与的用户头像 -->
|
||||
<view v-if="inArray('peoples', itemStyle.show) && goods.helpsCount" class="peoples">
|
||||
<view class="user-list">
|
||||
<view v-for="(help, hidx) in goods.helpList" :key="hidx" class="user-item-avatar">
|
||||
<avatar-image :url="help.user.avatar_url" :width="32" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="people__text">
|
||||
<text>{{ goods.helpsCount }}人正在砍价</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商品原价 -->
|
||||
<view v-if="inArray('originalPrice', itemStyle.show)" class="goods-price">
|
||||
<text>¥{{ goods.original_price }}</text>
|
||||
</view>
|
||||
<!-- 砍价低价 -->
|
||||
<view v-if="inArray('floorPrice', itemStyle.show)" class="floor-price">
|
||||
<text class="small">最低¥</text>
|
||||
<text class="big">{{ goods.floor_price }}</text>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="opt-touch">
|
||||
<view class="touch-btn">
|
||||
<text>立即参加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AvatarImage from '@/components/avatar-image'
|
||||
import { inArray } from '@/utils/util'
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AvatarImage
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
data() {
|
||||
return { inArray }
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 跳转到砍价商品详情
|
||||
handleNavDetail(item) {
|
||||
this.$navTo('pages/bargain/goods/index', { activeId: item.active_id, goodsId: item.goods_id })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// diy-砍价商品组
|
||||
.diy-bargain {
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
margin-bottom: 20rpx;
|
||||
background: #fff;
|
||||
padding: 20rpx 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item .goods-image .image {
|
||||
display: block;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
}
|
||||
|
||||
.goods-item .goods-info {
|
||||
width: 498rpx;
|
||||
padding-top: 4rpx;
|
||||
margin-left: 14rpx;
|
||||
position: relative;
|
||||
|
||||
.goods-name {
|
||||
font-size: 28rpx;
|
||||
min-height: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 正在参与的用户
|
||||
.peoples {
|
||||
display: flex;
|
||||
margin-top: 14rpx;
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
margin-right: 10rpx;
|
||||
|
||||
.user-item-avatar {
|
||||
margin-left: -8rpx;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.people__text {
|
||||
font-size: 24rpx;
|
||||
color: #818181;
|
||||
}
|
||||
}
|
||||
|
||||
// 商品原价
|
||||
.goods-price {
|
||||
margin-top: 14rpx;
|
||||
color: #818181;
|
||||
font-size: 24rpx;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
// 砍价底价
|
||||
.floor-price {
|
||||
color: $main-bg;
|
||||
|
||||
.small {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 立即参加按钮
|
||||
.opt-touch {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 10rpx;
|
||||
|
||||
.touch-btn {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
background: #d3a975;
|
||||
border-radius: 30rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
31
components/page/diyComponents/blank/index.vue
Executable file
31
components/page/diyComponents/blank/index.vue
Executable file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<!-- 辅助空白 -->
|
||||
<view class="diy-blank" :style="{ height: `${itemStyle.height}px`, background: itemStyle.background }">
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemStyle: Object
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
199
components/page/diyComponents/coupon/index.vue
Executable file
199
components/page/diyComponents/coupon/index.vue
Executable file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<!-- 优惠券组 -->
|
||||
<view v-if="couponList.length" class="diy-coupon" :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background }">
|
||||
<scroll-view :scroll-x="true">
|
||||
<view class="coupon-wrapper">
|
||||
<view class="coupon-item" :class="{ disable: !dataItem.state.value }" v-for="(dataItem, index) in couponList" :key="index"
|
||||
:style="{ marginRight: `${itemStyle.marginRight}px` }">
|
||||
<text class="before" :style="{ background: itemStyle.background }"></text>
|
||||
<view class="left-content" :style="{ background: itemStyle.couponBgColor }">
|
||||
<view class="content-top">
|
||||
<block v-if="dataItem.coupon_type == 10">
|
||||
<text class="unit">¥</text>
|
||||
<text class="price">{{ dataItem.reduce_price }}</text>
|
||||
</block>
|
||||
<text v-if="dataItem.coupon_type == 20" class="price">{{ dataItem.discount }}折</text>
|
||||
</view>
|
||||
<view class="content-bottom">
|
||||
<text class="f-22">满{{ dataItem.min_price }}元可用</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="right-receive" :style="{ background: itemStyle.receiveBgColor }" @click="handleReceive(index, dataItem)">
|
||||
<block v-if="dataItem.state.value">
|
||||
<text>立即</text>
|
||||
<text>领取</text>
|
||||
</block>
|
||||
<text v-else>{{ dataItem.state.text }}</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
import * as MyCouponApi from '@/api/myCoupon'
|
||||
import { cloneObj } from '@/utils/util'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 优惠券列表
|
||||
couponList: [],
|
||||
// 防止重复提交
|
||||
disable: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 这里监听dataList并写入到data中, 因为领取事件不能直接修改props中的属性
|
||||
dataList: {
|
||||
handler(data) {
|
||||
this.couponList = cloneObj(data)
|
||||
// console.log(this.couponList)
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 立即领取事件
|
||||
handleReceive(index, item) {
|
||||
const app = this
|
||||
if (app.disable || !item.state.value) {
|
||||
return
|
||||
}
|
||||
app.disable = true
|
||||
MyCouponApi.receive(item.coupon_id, {}, { load: false })
|
||||
.then(result => {
|
||||
// 显示领取成功提示
|
||||
app.$success(result.message)
|
||||
// 将优惠券设置为已领取
|
||||
app.setReceived(index, item)
|
||||
})
|
||||
.finally(() => app.disable = false)
|
||||
},
|
||||
|
||||
// 将优惠券设置为已领取
|
||||
setReceived(index, item) {
|
||||
const app = this
|
||||
app.$set(app.couponList, index, {
|
||||
...item,
|
||||
state: { value: 0, text: '已领取' }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-coupon {
|
||||
|
||||
.coupon-wrapper {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
height: 130rpx;
|
||||
padding: 0 24rpx;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-item {
|
||||
flex-shrink: 0;
|
||||
width: 300rpx;
|
||||
height: 130rpx;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin-right: 40rpx;
|
||||
display: flex;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&.disable {
|
||||
.left-content {
|
||||
background: linear-gradient(-113deg, #bdbdbd, #a2a1a2) !important;
|
||||
}
|
||||
|
||||
.right-receive {
|
||||
background-color: #949494 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
top: 45%;
|
||||
left: -16rpx;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 80%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.left-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
background-color: #E5004F;
|
||||
font-size: 24rpx;
|
||||
|
||||
.content-top {
|
||||
.unit {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-receive {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
background-color: #4e4e4e;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
components/page/diyComponents/empty.vue
Executable file
39
components/page/diyComponents/empty.vue
Executable file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<view class="diy-123123">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
257
components/page/diyComponents/goods/index.vue
Executable file
257
components/page/diyComponents/goods/index.vue
Executable file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<!-- 商品组 -->
|
||||
<view class="diy-goods" :style="{ background: itemStyle.background }">
|
||||
<view class="goods-list" :class="[`display__${itemStyle.display}`, `column__${itemStyle.column}`]">
|
||||
<scroll-view :scroll-x="itemStyle.display === 'slide'">
|
||||
<view class="goods-item" v-for="(dataItem, index) in dataList" :key="index" @click="onTargetGoods(dataItem.goods_id)">
|
||||
|
||||
<!-- 单列商品 -->
|
||||
<block v-if="itemStyle.column === 1">
|
||||
<view class="dis-flex">
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-item_left">
|
||||
<image class="image" :src="dataItem.goods_image"></image>
|
||||
</view>
|
||||
<view class="goods-item_right">
|
||||
<!-- 商品名称 -->
|
||||
<view v-if="itemStyle.show.includes('goodsName')" class="goods-name">
|
||||
<text class="twoline-hide">{{ dataItem.goods_name }}</text>
|
||||
</view>
|
||||
<view class="goods-item_desc">
|
||||
<!-- 商品卖点 -->
|
||||
<view v-if="itemStyle.show.includes('sellingPoint')" class="desc-selling_point dis-flex">
|
||||
<text class="oneline-hide">{{ dataItem.selling_point }}</text>
|
||||
</view>
|
||||
<!-- 商品销量 -->
|
||||
<view v-if="itemStyle.show.includes('goodsSales')" class="desc-goods_sales dis-flex">
|
||||
<text>已售{{ dataItem.goods_sales }}件</text>
|
||||
</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="desc_footer">
|
||||
<text v-if="itemStyle.show.includes('goodsPrice')" class="price_x">¥{{ dataItem.goods_price_min }}</text>
|
||||
<text class="price_y col-9"
|
||||
v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0">¥{{ dataItem.line_price_min }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 多列商品 -->
|
||||
<block v-else>
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-image">
|
||||
<image class="image" mode="aspectFill" :src="dataItem.goods_image"></image>
|
||||
</view>
|
||||
<view class="detail">
|
||||
<!-- 商品标题 -->
|
||||
<view v-if="itemStyle.show.includes('goodsName')" class="goods-name twoline-hide">
|
||||
<text class="twoline-hide">{{ dataItem.goods_name }}</text>
|
||||
</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="detail-price oneline-hide">
|
||||
<text v-if="itemStyle.show.includes('goodsPrice')" class="goods-price f-30 col-m">¥{{ dataItem.goods_price_min }}</text>
|
||||
<text v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0"
|
||||
class="line-price col-9 f-24">¥{{ dataItem.line_price_min }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Goods",
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 跳转商品详情页
|
||||
*/
|
||||
onTargetGoods(goodsId) {
|
||||
this.$navTo(`pages/goods/detail`, { goodsId })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-goods {
|
||||
.goods-list {
|
||||
padding: 4rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.goods-item {
|
||||
box-sizing: border-box;
|
||||
padding: 6rpx;
|
||||
|
||||
.goods-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 8rpx;
|
||||
background: #fff;
|
||||
|
||||
.goods-name {
|
||||
min-height: 68rpx;
|
||||
line-height: 1.3;
|
||||
white-space: normal;
|
||||
color: #484848;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
.goods-price {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.line-price {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.display__slide {
|
||||
white-space: nowrap;
|
||||
font-size: 0;
|
||||
|
||||
.goods-item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.display__list {
|
||||
.goods-item {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__2 {
|
||||
.goods-item {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__3 {
|
||||
.goods-item {
|
||||
width: 33.33333%;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__1 {
|
||||
.goods-item {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
line-height: 1.6;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_left {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_right {
|
||||
position: relative;
|
||||
width: 60%;
|
||||
|
||||
.goods-name {
|
||||
margin-top: 20rpx;
|
||||
min-height: 68rpx;
|
||||
line-height: 1.3;
|
||||
white-space: normal;
|
||||
color: #484848;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_desc {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.desc-selling_point {
|
||||
width: 400rpx;
|
||||
font-size: 24rpx;
|
||||
color: #e49a3d;
|
||||
}
|
||||
|
||||
.desc-goods_sales {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.desc_footer {
|
||||
font-size: 24rpx;
|
||||
|
||||
.price_x {
|
||||
margin-right: 16rpx;
|
||||
color: $main-bg;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.price_y {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
212
components/page/diyComponents/groupon/index.vue
Executable file
212
components/page/diyComponents/groupon/index.vue
Executable file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<!-- 拼团商品组 -->
|
||||
<view class="diy-groupon" :style="{ background: itemStyle.background, padding: `${itemStyle.paddingY * 2}rpx ${itemStyle.paddingX * 2}rpx` }">
|
||||
<view class="goods-item--container" v-for="(goods, idx) in dataList" :key="idx" :style="{ marginBottom: `${itemStyle.itemMargin * 2}rpx` }">
|
||||
<view class="goods-item" @click="onTargetGoods(goods)" :class="[`display-${itemStyle.display}`, `border-${itemStyle.itemBorderRadius}`]">
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-item_left">
|
||||
<view v-if="goods.active_type != ActiveTypeEnum.NORMAL.value" class="label">
|
||||
<text>{{ ActiveTypeEnum[goods.active_type].name2 }}</text>
|
||||
</view>
|
||||
<image class="image" :src="goods.goods_image"></image>
|
||||
</view>
|
||||
<view class="goods-item_right">
|
||||
<!-- 商品标题 -->
|
||||
<view v-if="inArray('goodsName', itemStyle.show)" class="goods-name">
|
||||
<text class="twoline-hide">{{ goods.goods_name }}</text>
|
||||
</view>
|
||||
<!-- 商品信息 -->
|
||||
<view class="goods-item_desc">
|
||||
<view class="desc_situation">
|
||||
<u-tag v-if="inArray('peoples', itemStyle.show)" class="people" :color="appTheme.mainBg" :border-color="appTheme.mainBg"
|
||||
:text="`${goods.show_people}人团`" type="error" size="mini" mode="plain" />
|
||||
<u-tag v-if="inArray('activeSales', itemStyle.show) && goods.active_sales" :color="appTheme.mainBg" :border-color="tagBorderColor"
|
||||
:bg-color="tagBackgroundColor" :text="`已团${goods.active_sales}件`" type="error" size="mini" />
|
||||
</view>
|
||||
<view class="desc_footer">
|
||||
<view class="item-prices oneline-hide">
|
||||
<text v-if="inArray('grouponPrice', itemStyle.show)" class="price_x">¥{{ goods.groupon_price }}</text>
|
||||
<text v-if="inArray('grouponPrice', itemStyle.show)" class="price_y cl-9">¥{{ goods.original_price }}</text>
|
||||
</view>
|
||||
<view v-if="inArray('button', itemStyle.show)" class="settlement">去拼团</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inArray } from '@/utils/util'
|
||||
import { hex2rgba } from '@/utils/color'
|
||||
import mixin from '../mixin'
|
||||
import { ActiveTypeEnum } from '@/common/enum/groupon'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
mixins: [mixin],
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inArray,
|
||||
ActiveTypeEnum
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 标签背景色
|
||||
tagBackgroundColor() {
|
||||
return hex2rgba(this.appTheme.mainBg, 0.1)
|
||||
},
|
||||
// 标签边框颜色
|
||||
tagBorderColor() {
|
||||
return hex2rgba(this.appTheme.mainBg, 0.6)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 跳转到拼团商品详情
|
||||
onTargetGoods(item) {
|
||||
this.$navTo('pages/groupon/goods/index', { grouponGoodsId: item.groupon_goods_id })
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-groupon {
|
||||
|
||||
.goods-item--container {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
padding: 28rpx 24rpx;
|
||||
display: flex;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.display-card {
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&.border-round {
|
||||
border-radius: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_left {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: linear-gradient(to right, #ffa600, #f5b914);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_right {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 68rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.goods-item_desc {
|
||||
margin-top: 20rpx;
|
||||
|
||||
.desc_situation {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.3;
|
||||
color: $main-bg;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.people {
|
||||
margin-right: 14rpx;
|
||||
}
|
||||
|
||||
.desc_footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
bottom: 0rpx;
|
||||
min-height: 44rpx;
|
||||
|
||||
.item-status {
|
||||
color: $main-bg;
|
||||
}
|
||||
|
||||
.item-prices {
|
||||
padding-right: 6rpx;
|
||||
|
||||
.price_x {
|
||||
margin-right: 14rpx;
|
||||
color: $main-bg;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.price_y {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.settlement {
|
||||
padding: 0 30rpx;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
background: $main-bg;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
36
components/page/diyComponents/guide/index.vue
Executable file
36
components/page/diyComponents/guide/index.vue
Executable file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<!-- 辅助线 -->
|
||||
<view class="diy-guide" :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background }">
|
||||
<view class="line" :style="{ borderTop: `${itemStyle.lineHeight}px ${itemStyle.lineStyle} ${itemStyle.lineColor}` }">
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemStyle: Object
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-guide .line {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
67
components/page/diyComponents/hotZone/index.vue
Executable file
67
components/page/diyComponents/hotZone/index.vue
Executable file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<!-- 热区 -->
|
||||
<view class="diy-hotZone"
|
||||
:style="{ paddingBottom: `${itemStyle.paddingTop * 2}rpx`, background: itemStyle.background }">
|
||||
<view class="bg-image" :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx 0` }">
|
||||
<image class="image" :src="data.imgUrl" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="zone-item" v-for="(item, index) in data.maps" :key="index"
|
||||
:style="{ width: `${item.width}rpx`, height: `${item.height}rpx`, left: `${item.left}rpx`, top: `${item.top}rpx` }"
|
||||
@click="onLink(item.link)">
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
name: "Images",
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
data: Object
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-hotZone {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.zone-item {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
// background-color: red;
|
||||
}
|
||||
</style>
|
||||
47
components/page/diyComponents/image/index.vue
Executable file
47
components/page/diyComponents/image/index.vue
Executable file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<!-- 单图组 -->
|
||||
<view class="diy-imageSingle" :style="{ paddingBottom: `${itemStyle.paddingTop * 2}rpx`, background: itemStyle.background }">
|
||||
<view class="item-image" v-for="(dataItem, index) in dataList" :key="index" :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx 0` }">
|
||||
<view class="nav-to" @click="onLink(dataItem.link)">
|
||||
<image class="image" :src="dataItem.imgUrl" mode="widthFix"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
name: "Images",
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-imageSingle .item-image .image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
55
components/page/diyComponents/mixin.js
Executable file
55
components/page/diyComponents/mixin.js
Executable file
@@ -0,0 +1,55 @@
|
||||
import { urlDecode } from '@/utils/util'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* link对象点击事件
|
||||
* 支持tabBar页面
|
||||
*/
|
||||
onLink(linkObj) {
|
||||
if (!linkObj) return false
|
||||
// 跳转到指定页面
|
||||
if (linkObj.type === 'PAGE') {
|
||||
this.$navTo(linkObj.param.path, linkObj.param.query)
|
||||
}
|
||||
// 跳转到自定义路径
|
||||
if (linkObj.type === 'CUSTOM') {
|
||||
this.$navTo(linkObj.param.path, urlDecode(linkObj.param.queryStr))
|
||||
}
|
||||
// 跳转到微信小程序
|
||||
// #ifdef MP-WEIXIN
|
||||
if (linkObj.type === 'MP-WEIXIN') {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: linkObj.param.appId,
|
||||
path: linkObj.param.path
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
// 跳转到H5外部链接
|
||||
if (linkObj.type === 'URL') {
|
||||
// #ifdef H5
|
||||
window.open(linkObj.param.url)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(linkObj.param.url)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.setClipboardData({
|
||||
data: linkObj.param.url,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
87
components/page/diyComponents/navBar/index.vue
Executable file
87
components/page/diyComponents/navBar/index.vue
Executable file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<!-- 导航组 -->
|
||||
<view class="diy-navBar" :style="{ background: itemStyle.background, color: itemStyle.textColor }">
|
||||
<view class="data-list" :class="[`avg-sm-${itemStyle.rowsNum}`]">
|
||||
<view class="item-nav" v-for="(dataItem, index) in dataList" :key="index">
|
||||
<view class="nav-to" @click="onLink(dataItem.link)">
|
||||
<view class="item-image">
|
||||
<image class="image" mode="widthFix" :src="dataItem.imgUrl"></image>
|
||||
</view>
|
||||
<view class="item-text oneline-hide">{{ dataItem.text }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
name: "NavBar",
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-navBar .data-list::after {
|
||||
clear: both;
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
|
||||
.item-nav {
|
||||
float: left;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
|
||||
.item-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
margin-bottom: 4px;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.item-image .image {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 分列布局 */
|
||||
|
||||
.diy-navBar .avg-sm-3>.item-nav {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
||||
.diy-navBar .avg-sm-4>.item-nav {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.diy-navBar .avg-sm-5>.item-nav {
|
||||
width: 20%;
|
||||
}
|
||||
</style>
|
||||
40
components/page/diyComponents/notice/index.vue
Executable file
40
components/page/diyComponents/notice/index.vue
Executable file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- 店铺公告 -->
|
||||
<view class="diy-notice" :style="{ paddingTop: `${itemStyle.paddingTop}px`, paddingBottom: `${itemStyle.paddingTop}px` }"
|
||||
@click="onLink(params.link)">
|
||||
<u-notice-bar padding="10rpx 24rpx" :volume-icon="params.showIcon" :autoplay="params.scrollable"
|
||||
:bg-color="itemStyle.background" :color="itemStyle.textColor" :list="[params.text]"></u-notice-bar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemStyle: Object,
|
||||
params: Object
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
37
components/page/diyComponents/officialAccount/index.vue
Executable file
37
components/page/diyComponents/officialAccount/index.vue
Executable file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<!-- 关注公众号 -->
|
||||
<view class="diy-officialAccount">
|
||||
<official-account></official-account>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
33
components/page/diyComponents/richText/index.vue
Executable file
33
components/page/diyComponents/richText/index.vue
Executable file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<!-- 富文本 -->
|
||||
<view class="diy-richText"
|
||||
:style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px`, background: itemStyle.background }">
|
||||
<mp-html :content="params.content" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemStyle: Object,
|
||||
params: Object
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-richText {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
76
components/page/diyComponents/search/index.vue
Executable file
76
components/page/diyComponents/search/index.vue
Executable file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<view class="diy-search">
|
||||
<view class="inner" :class="itemStyle.searchStyle" @click="onTargetSearch">
|
||||
<view class="search-input" :style="{ textAlign: itemStyle.textAlign }">
|
||||
<text class="search-icon iconfont icon-search"></text>
|
||||
<text> {{ params.placeholder }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 跳转到搜索页面
|
||||
*/
|
||||
onTargetSearch() {
|
||||
this.$navTo('pages/search/index')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-search {
|
||||
background: #f1f1f2;
|
||||
padding: 20rpx 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.inner {
|
||||
height: 60rpx;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&.radius {
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
&.round {
|
||||
border-radius: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 60rpx;
|
||||
color: #999;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-icon {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
components/page/diyComponents/service/index.vue
Executable file
90
components/page/diyComponents/service/index.vue
Executable file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<!-- 在线客服 -->
|
||||
<view v-if="isShow" class="diy-service" :style="{ '--right': `${right}px`, '--bottom': `${bottom}px` }">
|
||||
<!-- 拨打电话 -->
|
||||
<block v-if="params.type === 'phone'">
|
||||
<view class="service-icon" @click="onMakePhoneCall">
|
||||
<image class="image" :src="params.image"></image>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 在线聊天 -->
|
||||
<block v-else-if="params.type == 'chat'">
|
||||
<customer-btn>
|
||||
<view class="service-icon">
|
||||
<image class="image" :src="params.image"></image>
|
||||
</view>
|
||||
</customer-btn>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerBtn from '@/components/customer-btn'
|
||||
import { rpx2px } from '@/utils/util'
|
||||
import SettingModel from '@/common/model/Setting'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomerBtn
|
||||
},
|
||||
props: {
|
||||
itemStyle: Object,
|
||||
params: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
right() {
|
||||
return rpx2px(2 * this.itemStyle.right)
|
||||
},
|
||||
bottom() {
|
||||
return rpx2px(2 * this.itemStyle.bottom)
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (this.params.type === 'chat') {
|
||||
this.isShow = await SettingModel.isShowCustomerBtn()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 点击拨打电话
|
||||
*/
|
||||
onMakePhoneCall(e) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: this.params.tel
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-service {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
right: calc(var(--window-right) + var(--right));
|
||||
// 设置ios刘海屏底部横线安全区域
|
||||
bottom: calc(constant(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom));
|
||||
bottom: calc(env(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom));
|
||||
|
||||
.service-icon {
|
||||
padding: 10rpx;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
251
components/page/diyComponents/sharp/index.vue
Executable file
251
components/page/diyComponents/sharp/index.vue
Executable file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<!-- 整点秒杀 -->
|
||||
<view v-if="data.goodsList.data && data.goodsList.data.length" class="diy-sharp" :style="{ background: itemStyle.background }">
|
||||
<!-- 秒杀会场信息 -->
|
||||
<view class="sharp-top" @click="handleNavMore()">
|
||||
<view class="sharp-top--left">
|
||||
<view class="sharp-modular">
|
||||
<text class="iconfont icon-miaosha-b"></text>
|
||||
<text class="modular-name">限时秒杀</text>
|
||||
</view>
|
||||
<view class="sharp-active-status">
|
||||
<text>{{ data.active.sharp_modular_text }}</text>
|
||||
</view>
|
||||
<!-- 倒计时 -->
|
||||
<view v-if="data.active.status == GoodsStatusEnum.STATE_BEGIN.value" class="active-count-down">
|
||||
<count-down :date="data.active.count_down_time" separator="colon" theme="custom" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="sharp-top--right">
|
||||
<view class="sharp-more">
|
||||
<text class="sharp-more-text">更多</text>
|
||||
<text class="sharp-more-arrow iconfont icon-arrow-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商品列表 -->
|
||||
<view class="goods-list display__list clearfix" :class="[`column__${itemStyle.column}`]">
|
||||
<view class="goods-item" v-for="(goods, idx) in data.goodsList.data" :key="idx"
|
||||
@click="handleNavDetail(goods.sharp_goods_id)">
|
||||
<!-- 单列商品 -->
|
||||
<block v-if="itemStyle.column == 1">
|
||||
</block>
|
||||
<!-- 两列三列 -->
|
||||
<block v-else>
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-image">
|
||||
<image class="image" mode="aspectFill" :src="goods.goods_image"></image>
|
||||
</view>
|
||||
<view class="detail">
|
||||
<!-- 商品标题 -->
|
||||
<view v-if="inArray('goodsName', itemStyle.show)" class="goods-name">
|
||||
<text class="twoline-hide"> {{ goods.goods_name }}</text>
|
||||
</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="detail-price">
|
||||
<text v-if="inArray('seckillPrice', itemStyle.show)" class="goods-price c-red">
|
||||
<text class="small-unit">¥</text>
|
||||
<text>{{ goods.seckill_price_min }}</text>
|
||||
</text>
|
||||
<text v-if="inArray('originalPrice', itemStyle.show) && goods.original_price > 0"
|
||||
class="line-price">¥{{ goods.original_price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inArray } from '@/utils/util'
|
||||
import CountDown from '@/components/countdown'
|
||||
import { ActiveStatusEnum, GoodsStatusEnum } from '@/common/enum/sharp'
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CountDown
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
data: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 枚举类
|
||||
ActiveStatusEnum,
|
||||
GoodsStatusEnum,
|
||||
// 公共函数
|
||||
inArray
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 跳转到秒杀会场页
|
||||
handleNavMore() {
|
||||
this.$navTo('pages/sharp/index')
|
||||
},
|
||||
|
||||
// 跳转到秒杀商品详情页
|
||||
handleNavDetail(sharpGoodsId) {
|
||||
const { data } = this
|
||||
this.$navTo('pages/sharp/goods/index', {
|
||||
activeTimeId: data.active.active_time_id,
|
||||
sharpGoodsId
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// diy-秒杀商品组
|
||||
.diy-sharp {
|
||||
|
||||
.sharp-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sharp-top--left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sharp-modular {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
background: #FB571D;
|
||||
padding: 10rpx 30rpx 10rpx 24rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
border-top-right-radius: 30rpx;
|
||||
|
||||
.modular-name {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.sharp-active-status {
|
||||
color: #616161;
|
||||
font-size: 28rpx;
|
||||
margin-left: 20rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
// 更多
|
||||
.sharp-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 24rpx;
|
||||
color: #616161;
|
||||
font-size: 26rpx;
|
||||
|
||||
.sharp-more-arrow {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 商品列表
|
||||
.goods-list {
|
||||
padding: 4rpx;
|
||||
|
||||
&.display__list {
|
||||
.goods-item {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__2 {
|
||||
.goods-item {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__3 {
|
||||
.goods-item {
|
||||
width: 33.33333%;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
padding: 6rpx;
|
||||
|
||||
.goods-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 4rpx;
|
||||
background: #fff;
|
||||
|
||||
.goods-name {
|
||||
font-size: 26rpx;
|
||||
min-height: 68rpx;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
line-height: 40rpx;
|
||||
|
||||
.goods-price {
|
||||
color: red;
|
||||
font-size: 30rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.line-price {
|
||||
font-size: 24rpx;
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.small-unit {
|
||||
font-size: 26rpx;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
components/page/diyComponents/shop/index.vue
Executable file
97
components/page/diyComponents/shop/index.vue
Executable file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<!-- 门店列表 -->
|
||||
<view class="diy-shop">
|
||||
<view class="shop-item dis-flex flex-y-center" v-for="(dataItem, index) in dataList" :key="index"
|
||||
@click="handleNavDetail(dataItem.shop_id)">
|
||||
<view class="shop-item__logo">
|
||||
<image class="image" :src="dataItem.logo_url"></image>
|
||||
</view>
|
||||
<view class="shop-item__content">
|
||||
<view class="shop-item__title">
|
||||
<span>{{ dataItem.shop_name }}</span>
|
||||
</view>
|
||||
<view class="shop-item__address oneline-hide">
|
||||
<span>门店地址:{{ dataItem.region.province }}{{ dataItem.region.city }}{{ dataItem.region.region }}{{ dataItem.address }}</span>
|
||||
</view>
|
||||
<view class="shop-item__phone">
|
||||
<span>联系电话:{{ dataItem.phone }}</span>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 跳转到门店详情页
|
||||
handleNavDetail(shopId) {
|
||||
this.$navTo('pages/shop/detail', { shopId })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-item {
|
||||
padding: 16rpx 30rpx;
|
||||
min-height: 180rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.shop-item__logo {
|
||||
margin-right: 30rpx;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 130rpx;
|
||||
height: 130rpx;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.shop-item__content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.shop-item__title {
|
||||
font-size: 28rpx;
|
||||
color: #535353;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.shop-item__address,
|
||||
.shop-item__phone {
|
||||
color: #919396;
|
||||
}
|
||||
|
||||
.shop-item__address {
|
||||
width: 520rpx;
|
||||
}
|
||||
</style>
|
||||
102
components/page/diyComponents/special/index.vue
Executable file
102
components/page/diyComponents/special/index.vue
Executable file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<!-- 头条快报 -->
|
||||
<view class="diy-special" :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background }">
|
||||
<view class="special-left" @click="handleNavMore()">
|
||||
<image class="image" mode="widthFix" :src="params.image" />
|
||||
</view>
|
||||
<div class="special-content" :class="[`display_${params.display}`]">
|
||||
<swiper :autoplay="true" :interval="1500" :duration="800" :circular="true" :vertical="true"
|
||||
:display-multiple-items="itemStyle.display">
|
||||
<swiper-item v-for="(dataItm, idx) in dataList" :key="idx">
|
||||
<view class="content-item oneline-hide" @click="handleNavDetail(dataItm.article_id)">
|
||||
<text :style="{ color: itemStyle.textColor }">{{ dataItm.title }}</text>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</div>
|
||||
<div class="special-more" @click="handleNavMore()">
|
||||
<text class="iconfont icon-arrow-right"></text>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 跳转到文章详情页
|
||||
handleNavDetail(articleId) {
|
||||
this.$navTo('pages/article/detail', { articleId })
|
||||
},
|
||||
|
||||
// 跳转到更多
|
||||
handleNavMore() {
|
||||
this.$navTo('pages/article/index')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// diy-头条快报
|
||||
.diy-special {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
|
||||
.special-left {
|
||||
padding: 10rpx 20rpx;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 140rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.special-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.content-item {
|
||||
padding: 4rpx 0;
|
||||
font-size: 13px;
|
||||
color: #141414;
|
||||
}
|
||||
|
||||
&.display_1 {
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
&.display_2 {
|
||||
height: 88rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.special-more {
|
||||
padding: 24rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
components/page/diyComponents/video/index.vue
Executable file
38
components/page/diyComponents/video/index.vue
Executable file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<!-- 视频组 -->
|
||||
<view class="diy-video" :style="{ padding: `${itemStyle.paddingTop}px 0` }">
|
||||
<video class="video" :style="{ height: `${itemStyle.height}px` }" :src="params.videoUrl" :poster="params.poster"
|
||||
:autoplay="params.autoplay == 1" controls></video>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Videos',
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-video .video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
171
components/page/diyComponents/window/index.vue
Executable file
171
components/page/diyComponents/window/index.vue
Executable file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<!-- 图片橱窗 -->
|
||||
<view class="diy-window" :style="{ background: itemStyle.background, padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<!-- matrix -->
|
||||
<view v-if="itemStyle.layout > -1" class="data-list" :class="[`avg-sm-${itemStyle.layout}`]">
|
||||
<view v-for="(dataItem, index) in dataList" :key="index" class="data-item" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<view class="item-image" @click="onLink(dataItem.link)">
|
||||
<image class="image" mode="widthFix" :src="dataItem.imgUrl"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- display -->
|
||||
<view v-else class="display">
|
||||
<view class="display-left" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<image class="image" @click="onLink(dataList[0].link)" :src="dataList[0].imgUrl"></image>
|
||||
</view>
|
||||
<view class="display-right">
|
||||
<view v-if="dataList.length >= 2 " class="display-right1" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<image class="image" @click="onLink(dataList[1].link)" :src="dataList[1].imgUrl"></image>
|
||||
</view>
|
||||
<view class="display-right2">
|
||||
<view v-if="dataList.length >= 3 " class="left" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<image class="image" @click="onLink(dataList[2].link)" :src="dataList[2].imgUrl"></image>
|
||||
</view>
|
||||
<view v-if="dataList.length >= 4 " class="right" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px` }">
|
||||
<image class="image" @click="onLink(dataList[3].link)" :src="dataList[3].imgUrl"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../mixin'
|
||||
export default {
|
||||
name: "Window",
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
itemIndex: String,
|
||||
itemStyle: Object,
|
||||
params: Object,
|
||||
dataList: Array
|
||||
},
|
||||
|
||||
mixins: [mixin],
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-window .data-list::after {
|
||||
clear: both;
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
|
||||
.diy-window .data-list .data-item {
|
||||
float: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .data-list .image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 分列布局 */
|
||||
|
||||
.diy-window .avg-sm-2>.data-item {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.diy-window .avg-sm-3>.data-item {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
||||
.diy-window .avg-sm-4>.data-item {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.diy-window .avg-sm-5>.data-item {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
/* 橱窗样式 */
|
||||
|
||||
.diy-window {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display {
|
||||
height: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-bottom: 50%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display .image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.diy-window .display .display-left {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display .display-right {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display .display-right1 {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.diy-window .display .display-right2 {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display .display-right2 .left {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.diy-window .display .display-right2 .right {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
156
components/page/index.vue
Executable file
156
components/page/index.vue
Executable file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<view class="page-items">
|
||||
<block v-for="(item, index) in items" :key="index">
|
||||
<!-- 搜索框 -->
|
||||
<block v-if="item.type === 'search'">
|
||||
<Search :itemStyle="item.style" :params="item.params" />
|
||||
</block>
|
||||
<!-- 图片组 -->
|
||||
<block v-if="item.type === 'image'">
|
||||
<Images :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 轮播图 -->
|
||||
<block v-if="item.type === 'banner'">
|
||||
<Banner :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 图片橱窗 -->
|
||||
<block v-if="item.type === 'window'">
|
||||
<Window :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 视频 -->
|
||||
<block v-if="item.type === 'video'">
|
||||
<Videos :itemStyle="item.style" :params="item.params" />
|
||||
</block>
|
||||
<!-- 文章组 -->
|
||||
<block v-if="item.type === 'article'">
|
||||
<Article :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 店铺公告 -->
|
||||
<block v-if="item.type === 'notice'">
|
||||
<Notice :itemStyle="item.style" :params="item.params" />
|
||||
</block>
|
||||
<!-- 导航 -->
|
||||
<block v-if="item.type === 'navBar'">
|
||||
<NavBar :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 商品 -->
|
||||
<block v-if="item.type === 'goods'">
|
||||
<Goods :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 在线客服 -->
|
||||
<block v-if="item.type === 'service'">
|
||||
<Service :itemStyle="item.style" :params="item.params" />
|
||||
</block>
|
||||
<!-- 辅助空白 -->
|
||||
<block v-if="item.type === 'blank'">
|
||||
<Blank :itemStyle="item.style" />
|
||||
</block>
|
||||
<!-- 辅助线 -->
|
||||
<block v-if="item.type === 'guide'">
|
||||
<Guide :itemStyle="item.style" />
|
||||
</block>
|
||||
<!-- 富文本 -->
|
||||
<block v-if="item.type === 'richText'">
|
||||
<RichText :itemStyle="item.style" :params="item.params" />
|
||||
</block>
|
||||
<!-- 头条快报 -->
|
||||
<block v-if="item.type === 'special'">
|
||||
<Special :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 关注公众号 -->
|
||||
<block v-if="item.type === 'officialAccount'">
|
||||
<DiyOfficialAccount />
|
||||
</block>
|
||||
<!-- 线下门店 -->
|
||||
<block v-if="item.type === 'shop'">
|
||||
<Shop :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 优惠券 -->
|
||||
<block v-if="item.type === 'coupon'">
|
||||
<Coupon :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 砍价商品 -->
|
||||
<block v-if="item.type === 'bargain'">
|
||||
<Bargain :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 整点秒杀 -->
|
||||
<block v-if="item.type === 'sharp'">
|
||||
<Sharp :itemStyle="item.style" :params="item.params" :data="item.data" />
|
||||
</block>
|
||||
<!-- 拼团商品 -->
|
||||
<block v-if="item.type === 'groupon'">
|
||||
<Groupon :itemStyle="item.style" :params="item.params" :dataList="item.data" />
|
||||
</block>
|
||||
<!-- 图片组 -->
|
||||
<block v-if="item.type === 'hotZone'">
|
||||
<HotZone :itemStyle="item.style" :params="item.params" :data="item.data" />
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './diyComponents/search'
|
||||
import Images from './diyComponents/image'
|
||||
import Banner from './diyComponents/banner'
|
||||
import Window from './diyComponents/window'
|
||||
import HotZone from './diyComponents/hotZone'
|
||||
import Videos from './diyComponents/video'
|
||||
import Article from './diyComponents/article'
|
||||
import Notice from './diyComponents/notice'
|
||||
import NavBar from './diyComponents/navBar'
|
||||
import Goods from './diyComponents/goods'
|
||||
import Service from './diyComponents/service'
|
||||
import Blank from './diyComponents/blank'
|
||||
import Guide from './diyComponents/guide'
|
||||
import RichText from './diyComponents/richText'
|
||||
import Special from './diyComponents/special'
|
||||
import DiyOfficialAccount from './diyComponents/officialAccount'
|
||||
import Shop from './diyComponents/shop'
|
||||
import Coupon from './diyComponents/coupon'
|
||||
import Bargain from './diyComponents/bargain'
|
||||
import Sharp from './diyComponents/sharp'
|
||||
import Groupon from './diyComponents/groupon'
|
||||
|
||||
export default {
|
||||
name: "Page",
|
||||
components: {
|
||||
Search,
|
||||
Images,
|
||||
Banner,
|
||||
Window,
|
||||
HotZone,
|
||||
Videos,
|
||||
Article,
|
||||
Notice,
|
||||
NavBar,
|
||||
Goods,
|
||||
Service,
|
||||
Blank,
|
||||
Guide,
|
||||
RichText,
|
||||
Special,
|
||||
DiyOfficialAccount,
|
||||
Shop,
|
||||
Coupon,
|
||||
Bargain,
|
||||
Sharp,
|
||||
Groupon
|
||||
},
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// 组件样式
|
||||
</style>
|
||||
323
components/recommended/index.vue
Executable file
323
components/recommended/index.vue
Executable file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<!-- 推荐的商品 -->
|
||||
<view v-if="!isLoading && setting.enabled && goodsList.length" :style="appThemeStyle" class="recommended">
|
||||
<!-- 商品推荐标题 -->
|
||||
<view class="header">
|
||||
<view class="left">
|
||||
<image class="image"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAAXNSR0IArs4c6QAABoNJREFUaAXtWH1sFEUUnzd7d6UtkEBbIFKtpJRalIBiDBLUNAoEBCmKVaJo2oIlKR8BoiDBWMEvCCRAhdDyJR9G0wQb1CAUaTWKSkBjFJS0KSVSgVDKh4Kld7fzfG+2C9fjDu56B/GPne52Z2Znfu/93sfM7AnhFMcCjgUcCzgWcCzgWMCxgGOB/4sFIF6KtK3Y9gyimo5CJEsQOz13Ji6H/HwzHvivtRwtUEotECD6kMI1AO6Zy1KymoKxXcEd0bax/JC77dKRNQrNafZcheJh74nWAdQusvs680REeLXl6EqlzFl6PlmKrjwhvNnliIOLAXyBuDF5Btd+1KPtin8HKpUryGyBBQAwweNJh5mTTwb2R1qvRDQOtvyxiQi9FGqOlDBqWcrAvYHvZGAjmvqVD7YM8LZ5DyCaucA8gGwWUNiqPr/qFdAVcXU11icQkR3hiGggFMnBgJ0i41u14XEwzR9RYBYwAhG5SsgmBXDWnZR9JFjgzdqlZ850bTrr/xIVTgg3lmSdNzzwbfD7qMl4yzZyku+mZO8hhCI8apFnkOpMSN8S21yAL0Dxgx1iOlh4cPv1v5tSWs2WGmFiLhA0oAbuMIzwvRRiU97rntPS4QU1OEAiKlhZafiaL6xAE2az/lYBgZqL9WQ4aja7pDvPPXvK9/aoSJ6lzUfvuOLFaoK4lzyuNUP2MmlvcdJ9l4nIxOBcsfEjIoPbt3f3X/z3E6VwjCWFp9tWYyIkiNoS4LBbyfEwp+A4j4i0LPqzPpNyby9K7HfVGQyvyVhPkHDeNODJ5Sk5P4TDvSkZXL++n89nfkFRNFATYOu3Kw+63g6NsMvTFZ6HoqJ/wgkL1b/4RMMgn+mvJm/00USuEuDQtTwkDDhNtVFL++T8FgrD7rshGV95+QihVBVhpjIRzYFmdggt0oCsttJdMnUeLcecRBGX0sb6YQpxF+cfExF6MeFMZBIcXvSUcNxwGU+83Tur4WbAYcn4y9e+TJMrUAkPQbKviY5FxPIQ/wcf3SWuklfW85Boylt1dSPJ8lUoIdn2ACuvSZEgLVPC7+5EHFmadk9Ee1VIMrhhbZ6p8FNCBMsb17xikdLTzlF9krukuDYaEjx2SV3D07Srf0zWIEOR2nT+sbzBNqM2wwMcTHQnj1mYnn7dqhVOXsjjjEL1hgVLqNbVPp+lkHjEOreU42B6cX044HD97xxpKEQ/VtBiYbSnniDv8+Khp3AfSlGb3K3nhPlpaVHlX0gyBJdukSBkqlgJb6lHm+Q+lydpEhQUXAincLj+d39tmEub4XKCtDQnL1Ce0W2FF4cYXTtThHxuVlpaWziccP0hyaBU+2nDmshUuGjJWqhYJ/sPnAm5uX7rTeT/3//l2BIyyiKdDKw0JzgLoKSngym1qSrlthHZmYV0PooanzUJScZwy3nKi4NIQH/2ChjQSrvyfGPajLLI1bdG0nxY+nNjGWlcwiFKKx89LE9oQsSEQ4z+yhZmZ86mPtuG0Ypqd3eIabh1a7IwL4ymzOwqEhJr4MVpTSGG3bCrFtH108HGD0062mhJzIP/mBBdfHNbGLh44X1Zb94QLIKXDHlLyubGxi4tp7GS8mA8hxKb21qp2vcPliw5aXDOgsGZq+KhxC0hs/G75m4XjcufUzg9pjhqOCHo0l5gYpZUEw0omn9/vy3xIMIYFmy80Ain/NDJ1NZW327ywlCtPJPQt0WK+8AQbZTsk+cOvbsqjqLjS2Z1bVM6SrOaFMwJ3M1tr9D+QQUu0Vkrb95DGfviSYSxjHgBrvvmRBZtfl9TaPXnqOIksbcTXr10CIA8Z4Axeu6wu677sIqHHlpGrEDrapoG+5XaQzi9tRd0nlw7MFp94pRLipEzRmRE/fUZqX4xe6Ziz1/D6Ry3lzyRypaxrWPVLd+Qk455pMgteSSjLlLFOjPOlt2ZuaKiumkI+mE/5UeSzhFC4admpJdjSncJhz2uxFHFj6ad6pSQKCaFPAFEOh+8sIrUTbJ/0OBc4ezgU4NV5IEuSWLs1OFp5yLFjGVcTGTodPAAH0VYd713tHvFOqbAV6kJnon5w3tdikXBaObqxTKaCYFjDYQ6PiXyryj2ryn6WxNFVYbqOy4/9/YRYb1iIkM+WWAoCjb+HtGEOOHlpp5j0p8dOxaiPsIHGqoz9ZgWABa4uerMEAVmIX3+JxOTzwqf6ruzM4o4cxwLOBZwLOBYwLGAY4HbaIH/AFWphTyuhclNAAAAAElFTkSuQmCC">
|
||||
</image>
|
||||
</view>
|
||||
<view class="title">{{ setting.style.title }}</view>
|
||||
<image class="image"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAAXNSR0IArs4c6QAABgxJREFUaAXtmGtsVUUQgGf2lD6wBMEI2GBLVX5IISY8Sy9iILxSoEEaMRoBRSSYQEn4ZVBjE2KIiYiESHxATCAkKlQhPpCHJNLe1qoNIaIQK4mtiII0NWBpb3t2x5nTnuvt7Tm397blEXNOcrpnd2dn55vZ2d1bgOAJPBB4IPBA4IHAA4EHAg8EHrhdPIBehhARNlab9WRoKQu0gbJ25YXwgJdsKm1Uvz6jLdL2EgHMQ4IraMG2zHHvHU9FRyJZT5iGSr2dgMpiByLijtyQ2sAl29K3p/XMmsPsqAWxo1Hhq5nj3nm5P3pdfT1gmk/RnVdbTBNPqlwht0TAvbkz1Cqe2Hbbki3bf1oz0dZU5ymPWJE1HFZgzrvXPfuTbOxh8PUWGOkFIvo4Wssbw7qC6ikjSf1RMQM4KlqJ/yAqbW2ik3RuXU58Vyr1HjA5Iahnz1/0U0IEJQ2X9OHLP1K2n4xXewam1SJy/vk/k1rtyHftZ5+f5C+SuKcHDIMYUGoFILb7DiWY1dqsT1yopbt8ZeI68MG3mgCt1Y7+uL5olSBH2/pk5MxzpdG2FD56wMjYvCL8CkEt4hxp8dPFEZqi283JxipKemlkFby9DxWUcISu+eoFGGwM7o+cXvuin4xfe48NIFbwQhUVajJfcK4Mi23v9o34q0I1JzeE57u1J6hEzq6ZYDR9yg7Ji4oZdh0hkAEulVvuyxymnsWxOyJRuQQfCWFk3G/VNEFrc5TT3z+BAf+0LDXv3iL8IcFc3brol7Uj2lrNQTI43YHQAsHmyNsFJt+86GvSIWMJhl6/3E2BR6VXGBnTGKb7yZhjHKF8Dx1OE0/bbCm1cHQIa/xk4tudQ/Sq3gUGnuKtMhoRBwY4AzhKnUCqgTsXZzyyLaGzkoIRIyQ3iBygcfFGuXUGagFUj+bNwGNuWzJlpKaM84M2c5QwNiqdMJ1QnMPXFKkn0ua+9rmfzqRhRIHsXrpD8ykOU/wUOrsgqifHhLDCV8ajww5vLNW22cORGCxAztu17Jzl5yw96EgDNQuLt4Q9VEgsk39GT8OmrGHWbPbfCd9RROlozIeNlfYqXxmPjrTQGxWorZmgrd+5BNAKUF678wUuybYG6Q71gsdwpyklGBkxogD/yR1pFTPQIT+lnFsW33d2nw/rjX4yXu3ps7fWpevsqWx4HdoM1CFAXDKIvAJG2hrtNVbaUoaRQTgWI7khq5QPwD1Sj30kZ3ljArm8dRjaei6sN8f29/aN88ovDro7a6YyaQf+A7E6I8RgHC3PJSZ6U8qZeEOcnwpV5k2ORJlAOJtPVxmt8wz8i2Ln+BlqHcMnfeMW3eaT8nI+dzaRhjTJGz5/TikrbT4u2/RXvC1S7xeMq/B8pS43QK84RwQ3GjZZoiOWO1AChLhvYpF6moFSunHTR1seAGM/DKQuw9Ahx7G4zPcAHRAYgfq5Um/QCNu0eLQLwoXTTOVETcFno3Ksx/LzMdGFU9T16RkwGJn9TJW90ibcTUiWRMeFcnZa7pdIMeDXgyyrpLAQr8qYgXwGFEYMO1VNS4wxH2igjCiMC8azCSQvuTqVqRbMmoxXbmsYMa42TLP5gnqIN4ZsWWLucovCsQwDnTOWmltciBcGCmjAI+MaVl1NU20yhzmHhjv5wh0umGwOThtiAzLQ4kKsd8f1p7xhMGLUiW+pwHSYo4YoJzYqsd+g8BJYav7SaXi6PyAy9obCyARHaimfgY5zDt0nG4CzZXfljkBJnc34myO06PHp6HsgOmK9/LnhMDL/l9/TPe0RjhDQ+Og2zTM7y477ZbfTgNf5R97c5UVY3YvNvt19us74avPpWDAZ/1BD1Ew2/huJBJ9HYHOYun/T4A4ye31UJNV8UyLjWnLkNN3R3KIPagNznLzh2Z2SwZyziEsLrTGrQ9jgjkmlvCmRcQ2a/xC2qKHWQo7Ix050uMOJEENJlPi3mU7Phj6fPTcVRqCWFWB7ZshaphW+72wAXSByYWPAHSsYWOT68rCqW/fsrLFX8h3nGb4VpJPC/esK1faE/1e7daYGMwceCDwQeCDwQOCB/5cH/gVYZIBBAaIwagAAAABJRU5ErkJggg==">
|
||||
</image>
|
||||
</view>
|
||||
<!-- 商品列表 -->
|
||||
<view class="goods-list" :class="[`display__${setting.style.display}`, `column__${setting.style.column}`]">
|
||||
<view class="goods-item" v-for="(dataItem, index) in goodsList" :key="index" @click="onTargetGoods(dataItem.goods_id)">
|
||||
<!-- 单列商品 -->
|
||||
<block v-if="setting.style.column === 1">
|
||||
<view class="dis-flex">
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-item_left">
|
||||
<image class="image" :src="dataItem.goods_image"></image>
|
||||
</view>
|
||||
<view class="goods-item_right">
|
||||
<!-- 商品名称 -->
|
||||
<view v-if="setting.style.show.includes('goodsName')" class="goods-name">
|
||||
<text class="twoline-hide">{{ dataItem.goods_name }}</text>
|
||||
</view>
|
||||
<view class="goods-item_desc">
|
||||
<!-- 商品卖点 -->
|
||||
<view v-if="setting.style.show.includes('sellingPoint')" class="desc-selling_point dis-flex">
|
||||
<text class="oneline-hide">{{ dataItem.selling_point }}</text>
|
||||
</view>
|
||||
<!-- 商品销量 -->
|
||||
<view v-if="setting.style.show.includes('goodsSales')" class="desc-goods_sales dis-flex">
|
||||
<text>已售{{ dataItem.goods_sales }}件</text>
|
||||
</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="desc_footer">
|
||||
<text v-if="setting.style.show.includes('goodsPrice')" class="price_x">¥{{ dataItem.goods_price_min }}</text>
|
||||
<text v-if="setting.style.show.includes('linePrice') && dataItem.line_price_min > 0"
|
||||
class="price_y col-9">¥{{ dataItem.line_price_min }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 多列商品 -->
|
||||
<block v-else>
|
||||
<!-- 商品图片 -->
|
||||
<view class="goods-image">
|
||||
<image class="image" mode="aspectFill" :src="dataItem.goods_image"></image>
|
||||
</view>
|
||||
<view class="detail">
|
||||
<!-- 商品标题 -->
|
||||
<view v-if="setting.style.show.includes('goodsName')" class="goods-name">
|
||||
<text class="twoline-hide">{{ dataItem.goods_name }}</text>
|
||||
</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="detail-price oneline-hide">
|
||||
<text v-if="setting.style.show.includes('goodsPrice')" class="goods-price">¥{{ dataItem.goods_price_min }}</text>
|
||||
<text v-if="setting.style.show.includes('linePrice') && dataItem.line_price_min > 0"
|
||||
class="line-price col-9 f-24">¥{{ dataItem.line_price_min }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as GoodsApi from '@/api/goods'
|
||||
import SettingKeyEnum from '@/common/enum/setting/Key'
|
||||
import SettingModel from '@/common/model/Setting'
|
||||
|
||||
export default {
|
||||
name: "Recommended",
|
||||
data() {
|
||||
return {
|
||||
// 正在加载
|
||||
isLoading: true,
|
||||
// 商品列表
|
||||
goodsList: [],
|
||||
// 推荐商品设置
|
||||
setting: {
|
||||
enabled: 1,
|
||||
style: {
|
||||
show: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const app = this
|
||||
app.isLoading = true
|
||||
await app.getSetting()
|
||||
if (app.setting.enabled) {
|
||||
await app.getRecommended()
|
||||
}
|
||||
app.isLoading = false
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
* 更新属性和数据的方法与更新页面数据的方法类似
|
||||
*/
|
||||
methods: {
|
||||
|
||||
// 获取推荐商品列表
|
||||
async getRecommended() {
|
||||
const app = this
|
||||
await GoodsApi.recommended().then(result => app.goodsList = result.data.goodsList)
|
||||
},
|
||||
|
||||
// 获取商城设置
|
||||
async getSetting() {
|
||||
const app = this
|
||||
await SettingModel.item(SettingKeyEnum.RECOMMENDED.value)
|
||||
.then(setting => app.setting = setting)
|
||||
},
|
||||
|
||||
// 跳转商品详情页
|
||||
onTargetGoods(goodsId) {
|
||||
this.$navTo(`pages/goods/detail`, { goodsId })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recommended {
|
||||
margin-top: 60rpx;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 26rpx;
|
||||
color: #333333;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
// background: rgb(246, 246, 246);
|
||||
padding: 4rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
box-sizing: border-box;
|
||||
padding: 6rpx;
|
||||
|
||||
.goods-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 8rpx;
|
||||
background: #fff;
|
||||
|
||||
.goods-name {
|
||||
min-height: 68rpx;
|
||||
line-height: 1.3;
|
||||
white-space: normal;
|
||||
color: #484848;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.detail-price {
|
||||
.goods-price {
|
||||
margin-right: 8rpx;
|
||||
font-size: 30rpx;
|
||||
color: $main-bg;
|
||||
}
|
||||
|
||||
.line-price {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.display__slide {
|
||||
white-space: nowrap;
|
||||
font-size: 0;
|
||||
|
||||
.goods-item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.display__list {
|
||||
.goods-item {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__2 {
|
||||
.goods-item {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__3 {
|
||||
.goods-item {
|
||||
width: 33.33333%;
|
||||
}
|
||||
}
|
||||
|
||||
&.column__1 {
|
||||
.goods-item {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
line-height: 1.6;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_left {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_right {
|
||||
position: relative;
|
||||
width: 60%;
|
||||
|
||||
.goods-name {
|
||||
margin-top: 20rpx;
|
||||
min-height: 68rpx;
|
||||
line-height: 1.3;
|
||||
white-space: normal;
|
||||
color: #484848;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-item_desc {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.desc-selling_point {
|
||||
width: 400rpx;
|
||||
font-size: 24rpx;
|
||||
color: #e49a3d;
|
||||
}
|
||||
|
||||
.desc-goods_sales {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.desc_footer {
|
||||
font-size: 24rpx;
|
||||
|
||||
.price_x {
|
||||
margin-right: 16rpx;
|
||||
color: $main-bg;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.price_y {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
components/search/index.vue
Executable file
69
components/search/index.vue
Executable file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-wrapper">
|
||||
<view class="index-search" @click="onClick">
|
||||
<view class="index-cont-search t-c">
|
||||
<text class="search-icon iconfont icon-search"></text>
|
||||
<text class="search-text">{{ tips }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tips: {
|
||||
type: String,
|
||||
default: '搜索商品'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('event')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-wrapper {
|
||||
background: #fff;
|
||||
padding: 13rpx;
|
||||
}
|
||||
|
||||
.index-search {
|
||||
border-bottom: 0;
|
||||
background: #fff;
|
||||
border-radius: 50rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: #6d6d6d;
|
||||
box-sizing: border-box;
|
||||
line-height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.index-cont-search {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
background: #f7f7f7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.index-cont-search .search-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.index-cont-search .search-text {
|
||||
margin-left: 14rpx;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
187
components/select-region/select-region.vue
Executable file
187
components/select-region/select-region.vue
Executable file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view v-if="isLoading" class="loading">
|
||||
<u-loading mode="circle"></u-loading>
|
||||
</view>
|
||||
<view v-else class="field-body" @click="handleSelect()">
|
||||
<view class="field-value oneline-hide">{{ valueText ? valueText: placeholder }}</view>
|
||||
</view>
|
||||
<u-select v-model="show" mode="mutil-column-auto" :list="options" :default-value="defaultValue"
|
||||
@confirm="onConfirm"></u-select>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Emitter from '@/uview-ui/libs/util/emitter'
|
||||
import { isEmpty } from '@/utils/util'
|
||||
import RegionModel from '@/common/model/Region'
|
||||
|
||||
// 根据选中的value集获取索引集keys
|
||||
// 用于设置默认选中
|
||||
const findOptionsKey = (data, searchValue, deep = 1, keys = []) => {
|
||||
const index = data.findIndex(item => item.value === searchValue[deep - 1])
|
||||
if (index > -1) {
|
||||
keys.push(index)
|
||||
if (data[index].children) {
|
||||
findOptionsKey(data[index].children, searchValue, ++deep, keys)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SelectRegion',
|
||||
mixins: [Emitter],
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
// v-model 指定选中项
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// 未选中时的提示文字
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择省/市/区'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 正在加载
|
||||
isLoading: true,
|
||||
// 是否显示
|
||||
show: false,
|
||||
// 默认选中的值
|
||||
defaultValue: [],
|
||||
// 选中项内容(文本展示)
|
||||
valueText: '',
|
||||
// 级联选择器数据
|
||||
options: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听v-model
|
||||
value(val) {
|
||||
// 设置默认选中的值
|
||||
this.valueText = val.map(item => item.label).join('/')
|
||||
this.setDefaultValue(val)
|
||||
// 将当前的值发送到 u-form-item 进行校验
|
||||
this.dispatch('u-form-item', 'on-form-change', val)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 获取地区数据
|
||||
this.getTreeData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// 打开选择器
|
||||
handleSelect() {
|
||||
this.show = true
|
||||
},
|
||||
|
||||
// 获取地区数据
|
||||
getTreeData() {
|
||||
const app = this
|
||||
app.isLoading = true
|
||||
RegionModel.getTreeData()
|
||||
.then(regions => {
|
||||
// 格式化级联选择器数据
|
||||
this.options = this.getOptions(regions)
|
||||
})
|
||||
.finally(() => app.isLoading = false)
|
||||
},
|
||||
|
||||
// 确认选择后的回调
|
||||
onConfirm(value) {
|
||||
// 绑定到v-model执行的值
|
||||
this.$emit('input', value)
|
||||
this.$emit('change', value)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置默认选中的值
|
||||
* 该操作是为了每次打开选择器时聚焦到上次选择
|
||||
* @param {Object} value
|
||||
*/
|
||||
setDefaultValue(value) {
|
||||
const values = value.map(item => item.value)
|
||||
const options = this.options
|
||||
this.defaultValue = findOptionsKey(options, values)
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化级联选择器数据
|
||||
* @param {*} regions 地区数据
|
||||
*/
|
||||
getOptions(regions) {
|
||||
const { getOptions, getChildren } = this
|
||||
const options = []
|
||||
for (const index in regions) {
|
||||
const item = regions[index]
|
||||
const children = getChildren(item)
|
||||
const optionItem = {
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}
|
||||
if (children !== false) {
|
||||
optionItem.children = getOptions(children)
|
||||
}
|
||||
options.push(optionItem)
|
||||
}
|
||||
return options
|
||||
},
|
||||
|
||||
// 获取子集地区
|
||||
getChildren(item) {
|
||||
if (item.city) {
|
||||
return item.city
|
||||
}
|
||||
if (item.region) {
|
||||
return item.region
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 根据省市区名称获取optionItem
|
||||
getOptionItemByNames({ provinceName, cityName, countyName }) {
|
||||
const app = this
|
||||
const data = []
|
||||
app.options.forEach(provinceItem => {
|
||||
if (provinceItem.label == provinceName) {
|
||||
data.push({ label: provinceItem.label, value: provinceItem.value })
|
||||
provinceItem.children.forEach(cityItem => {
|
||||
if (cityItem.label == cityName) {
|
||||
data.push({ label: cityItem.label, value: cityItem.value })
|
||||
cityItem.children.forEach(countyItem => {
|
||||
if (countyItem.label == countyName) {
|
||||
data.push({ label: countyItem.label, value: countyItem.value })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding-left: 10rpx;
|
||||
// text-align: center;
|
||||
}
|
||||
</style>
|
||||
369
components/share-sheet/index.vue
Executable file
369
components/share-sheet/index.vue
Executable file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<view class="sharesheet" :class="{ show: value }">
|
||||
<view class="mask-class sharesheet__mask" @click="onMaskClick"></view>
|
||||
<view class="sharesheet__container">
|
||||
<!-- 分享选项列表 -->
|
||||
<view class="sharesheet__list">
|
||||
<!-- 选项按钮: 发送给朋友(仅支持小程序) -->
|
||||
<!-- #ifdef MP -->
|
||||
<button class="share-item btn-normal" open-type="share" @click="handleCancel()">
|
||||
<view class="item-image" :style="{ backgroundColor: '#44DB74' }">
|
||||
<text class="iconfont icon-weixin"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>发送给朋友</text>
|
||||
</view>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<!-- 选项按钮: APP端分享给微信好友 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-if="enabledAppShareWeixin" class="share-item" @click="handleShareWechat()">
|
||||
<view class="item-image" :style="{ backgroundColor: '#44DB74' }">
|
||||
<text class="iconfont icon-weixin"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>微信好友</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 选项按钮: APP端分享到微信朋友圈 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-if="enabledAppShareWeixin" class="share-item" @click="handleShareWechatTimeline()">
|
||||
<view class="item-image" :style="{ backgroundColor: '#69D416' }">
|
||||
<text class="iconfont icon-timeline"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>微信朋友圈</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="share-item" @click="handlePoster()">
|
||||
<view class="item-image" :style="{ backgroundColor: '#38beec' }">
|
||||
<text class="iconfont icon-poster"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>生成海报</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="share-item" @click="handleCopyLink()">
|
||||
<view class="item-image" :style="{ backgroundColor: '#38beec' }">
|
||||
<text class="iconfont icon-link"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>复制链接</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="share-item">
|
||||
<view class="item-image" :style="{ backgroundColor: '#FE8A4F' }">
|
||||
<text class="iconfont icon-weibo"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>新浪微博</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<!-- <view class="share-item">
|
||||
<view class="item-image" :style="{ backgroundColor: '#56C0F2' }">
|
||||
<text class="iconfont icon-qq"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>QQ好友</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<!-- <view class="share-item">
|
||||
<view class="item-image" :style="{ backgroundColor: '#FFBB0D' }">
|
||||
<text class="iconfont icon-qzone"></text>
|
||||
</view>
|
||||
<view class="item-name">
|
||||
<text>QQ空间</text>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
<!-- 取消按钮 -->
|
||||
<view v-if="cancelText" class="sharesheet__footer" @click="handleCancel()">
|
||||
<view class="btn-cancel">{{ cancelText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 海报图弹层 -->
|
||||
<GoodsPosterPopup v-model="showGoodsPosterPopup" :apiCall="posterApiCall" :apiParam="posterApiParam" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 参考的uniapp文档 -->
|
||||
<!-- https://uniapp.dcloud.io/component/button?id=button -->
|
||||
<!-- https://uniapp.dcloud.io/api/plugins/share -->
|
||||
|
||||
<script>
|
||||
import Config from '@/core/config'
|
||||
import { getCurrentPage, buildUrL } from '@/core/app'
|
||||
import { inArray } from '@/utils/util'
|
||||
import GoodsPosterPopup from '@/components/goods-poster-popup'
|
||||
import SettingModel from '@/common/model/Setting'
|
||||
|
||||
export default {
|
||||
name: 'ShareSheet',
|
||||
components: {
|
||||
GoodsPosterPopup
|
||||
},
|
||||
props: {
|
||||
// true 组件显示 false 组件隐藏
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 点击遮罩层取消
|
||||
cancelWithMask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 分享链接的标题
|
||||
shareTitle: {
|
||||
type: String,
|
||||
default: '商品分享'
|
||||
},
|
||||
// 分享链接的封面图
|
||||
shareImageUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 取消按钮文字
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '关闭'
|
||||
},
|
||||
// 获取海报图的api方法
|
||||
posterApiCall: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
// 获取海报图的api参数
|
||||
posterApiParam: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 是否启用分享到微信聊天 (仅APP端支持)
|
||||
enabledAppShareWeixin: Config.get('enabledAppShareWeixin'),
|
||||
// 是否显示商品海报图弹层
|
||||
showGoodsPosterPopup: false,
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化方法
|
||||
created() {
|
||||
this.initSharesheet()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// 初始化选择项
|
||||
initSharesheet() {
|
||||
const app = this
|
||||
// 获取支持的服务商 (仅APP端支持)
|
||||
uni.getProvider({
|
||||
service: 'share',
|
||||
success({ provider }) {
|
||||
if (!inArray('weixin', provider)) {
|
||||
app.enabledAppShareWeixin = false
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 点击遮罩层(关闭菜单)
|
||||
onMaskClick() {
|
||||
if (this.cancelWithMask) {
|
||||
this.handleCancel()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取分享链接 (H5外链)
|
||||
getShareUrl() {
|
||||
const { path, query } = getCurrentPage()
|
||||
return new Promise((resolve, reject) => {
|
||||
// 获取h5站点地址
|
||||
SettingModel.h5Url(true)
|
||||
.then(baseUrl => {
|
||||
// 生成完整的分享链接
|
||||
const shareUrl = buildUrL(baseUrl, path, query)
|
||||
resolve(shareUrl)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 复制商品链接
|
||||
handleCopyLink() {
|
||||
const app = this
|
||||
app.getShareUrl().then(shareUrl => {
|
||||
// 复制到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => app.$toast('链接复制成功,快去发送给朋友吧~'),
|
||||
fail: err => app.$toast('很遗憾,复制失败'),
|
||||
complete: () => app.handleCancel()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// APP发送给微信好友
|
||||
// #ifdef APP-PLUS
|
||||
handleShareWechat() {
|
||||
const app = this
|
||||
uni.share({
|
||||
provider: 'weixin',
|
||||
scene: 'WXSceneSession',
|
||||
type: 0,
|
||||
href: app.getShareUrl(),
|
||||
title: app.shareTitle,
|
||||
summary: '我发现了一个很不错的商品,赶快来看看吧!',
|
||||
imageUrl: app.shareImageUrl,
|
||||
success(res) {
|
||||
console.log("success:" + JSON.stringify(res));
|
||||
},
|
||||
fail({ errCode }) {
|
||||
if (errCode === -8) {
|
||||
app.$toast('很抱歉,您的手机没有安装微信~')
|
||||
}
|
||||
},
|
||||
complete: () => app.handleCancel()
|
||||
})
|
||||
},
|
||||
// #endif
|
||||
|
||||
// APP分享到微信朋友圈
|
||||
// #ifdef APP-PLUS
|
||||
handleShareWechatTimeline() {
|
||||
const app = this
|
||||
uni.share({
|
||||
provider: 'weixin',
|
||||
scene: 'WXSenceTimeline',
|
||||
type: 0,
|
||||
href: app.getShareUrl(),
|
||||
title: app.shareTitle,
|
||||
summary: '我发现了一个很不错的商品,赶快来看看吧!',
|
||||
imageUrl: app.shareImageUrl,
|
||||
success(res) {
|
||||
console.log("success:" + JSON.stringify(res));
|
||||
},
|
||||
fail({ errCode }) {
|
||||
if (errCode === -8) {
|
||||
app.$toast('很抱歉,您的手机没有安装微信~')
|
||||
}
|
||||
},
|
||||
complete: () => app.handleCancel()
|
||||
})
|
||||
},
|
||||
// #endif
|
||||
|
||||
// 关闭菜单
|
||||
handleCancel() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
|
||||
// 生成二维码海报
|
||||
handlePoster() {
|
||||
this.showGoodsPosterPopup = true
|
||||
this.handleCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sharesheet {
|
||||
background-color: #f8f8f8;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.sharesheet__mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: var(--window-left);
|
||||
right: var(--window-right);
|
||||
bottom: var(--window-bottom);
|
||||
z-index: 12;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sharesheet__container {
|
||||
position: fixed;
|
||||
left: var(--window-left);
|
||||
right: var(--window-right);
|
||||
bottom: var(--window-bottom);
|
||||
background: #ffffff;
|
||||
transform: translate3d(0, 50%, 0);
|
||||
transform-origin: center;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 13;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
border-top-left-radius: 26rpx;
|
||||
border-top-right-radius: 26rpx;
|
||||
padding: 50rpx 30rpx 0 30rpx;
|
||||
// 设置ios刘海屏底部横线安全区域
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) + 30rpx);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
|
||||
}
|
||||
|
||||
.sharesheet__list {
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: -35rpx;
|
||||
|
||||
.share-item {
|
||||
flex: 0 0 25%;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.item-name,
|
||||
.item-image {
|
||||
width: 140rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 38rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
margin-top: 12rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sharesheet__footer {
|
||||
background: #fff;
|
||||
margin-top: 40rpx;
|
||||
|
||||
.btn-cancel {
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示状态
|
||||
.show {
|
||||
.sharesheet__mask {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sharesheet__container {
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
297
components/shortcut/index.vue
Executable file
297
components/shortcut/index.vue
Executable file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<view class="shortcut" :style="{ '--right': `${rightPx}px`, '--bottom': `${bottomPx}px` }">
|
||||
<!-- 首页 -->
|
||||
<view class="nav-item" :class="[isShow ? 'show_80' : (transparent ? '' : 'hide_80')]" @click="onTargetPage(0)">
|
||||
<text class="iconfont icon-home"></text>
|
||||
</view>
|
||||
<!-- 分类页 -->
|
||||
<view class="nav-item" :class="[isShow ? 'show_60' : (transparent ? '' : 'hide_60')]" @click="onOrder">
|
||||
<text class="iconfont icon-cate"></text>
|
||||
</view>
|
||||
<!-- 购物车 -->
|
||||
<view class="nav-item" :class="[isShow ? 'show_40' : (transparent ? '' : 'hide_40')]" @click="onTargetPage(1)">
|
||||
<text class="iconfont icon-cart"></text>
|
||||
</view>
|
||||
<!-- 个人中心 -->
|
||||
<view class="nav-item" :class="[isShow ? 'show_20' : (transparent ? '' : 'hide_20')]" @click="onTargetPage(3)">
|
||||
<text class="iconfont icon-profile"></text>
|
||||
</view>
|
||||
<!-- 显示隐藏开关 -->
|
||||
<view class="nav-item nav-item__switch" :class="{ shortcut_click_show: isShow }" @click="onToggleShow()">
|
||||
<text class='iconfont icon-daohang'></text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getTabBarLinks } from '@/core/app'
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
* 用于组件自定义设置
|
||||
*/
|
||||
props: {
|
||||
right: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 100
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 弹窗显示控制
|
||||
isShow: false,
|
||||
transparent: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
rightPx() {
|
||||
return uni.upx2px(this.right)
|
||||
},
|
||||
bottomPx() {
|
||||
return uni.upx2px(this.bottom)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 导航菜单切换事件
|
||||
*/
|
||||
onToggleShow() {
|
||||
const app = this
|
||||
app.isShow = !app.isShow
|
||||
app.transparent = false
|
||||
},
|
||||
|
||||
/**
|
||||
* 导航页面跳转
|
||||
*/
|
||||
onTargetPage(index = 0) {
|
||||
const tabLinks = getTabBarLinks()
|
||||
this.$navTo(tabLinks[index])
|
||||
},
|
||||
|
||||
onOrder(){
|
||||
this.$navTo('pages/checkout/checkout')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 快捷导航 */
|
||||
.shortcut {
|
||||
position: fixed;
|
||||
right: calc(var(--window-right) + var(--right));
|
||||
bottom: calc(var(--window-bottom) + var(--bottom));
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
line-height: 1;
|
||||
z-index: 5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 导航菜单元素 */
|
||||
.nav-item {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item .iconfont {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
/* 导航开关 */
|
||||
|
||||
.nav-item__switch {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shortcut_click_show {
|
||||
margin-bottom: 0;
|
||||
background: #ff5454;
|
||||
}
|
||||
|
||||
/* 显示动画 */
|
||||
|
||||
.show_80 {
|
||||
bottom: 384rpx;
|
||||
animation: show_80 0.3s forwards;
|
||||
}
|
||||
|
||||
.show_60 {
|
||||
bottom: 288rpx;
|
||||
animation: show_60 0.3s forwards;
|
||||
}
|
||||
|
||||
.show_40 {
|
||||
bottom: 192rpx;
|
||||
animation: show_40 0.3s forwards;
|
||||
}
|
||||
|
||||
.show_20 {
|
||||
bottom: 96rpx;
|
||||
animation: show_20 0.3s forwards;
|
||||
}
|
||||
|
||||
@keyframes show_20 {
|
||||
from {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 96rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show_40 {
|
||||
from {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 192rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show_60 {
|
||||
from {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 288rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show_80 {
|
||||
from {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 384rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏动画 */
|
||||
|
||||
.hide_80 {
|
||||
bottom: 0;
|
||||
animation: hide_80 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hide_60 {
|
||||
bottom: 0;
|
||||
animation: hide_60 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hide_40 {
|
||||
bottom: 0;
|
||||
animation: hide_40 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hide_20 {
|
||||
bottom: 0;
|
||||
animation: hide_20 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes hide_20 {
|
||||
from {
|
||||
bottom: 96rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide_40 {
|
||||
from {
|
||||
bottom: 192rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide_60 {
|
||||
from {
|
||||
bottom: 288rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide_80 {
|
||||
from {
|
||||
bottom: 384rpx;
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
components/static/add.png
Normal file
BIN
components/static/add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
components/static/audio.png
Normal file
BIN
components/static/audio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
components/static/emoji.png
Normal file
BIN
components/static/emoji.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
components/static/jianpan.png
Normal file
BIN
components/static/jianpan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 966 B |
1675
components/video-swiper/index.vue
Normal file
1675
components/video-swiper/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user