first commit

This commit is contained in:
2025-08-06 17:17:48 +08:00
commit dc5fba137c
453 changed files with 85995 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
<template>
<view :class="'active-area ' + type">
<view class="a-header">
<slot name="header"></slot>
</view>
<view class="public-class a-main">
<slot name="main-header"></slot>
<view v-if="type == 'hot'">
<goods-list type="home-hot" :list="lists"></goods-list>
</view>
<scroll-view v-else style="white-space: nowrap;" scroll-x="true" scroll-with-animation="true">
<navigator v-for="(item, index) in lists" :key="index"
:style="{background: type == 'seckill' && '#f8f8f8'}" class="goods-item bg-white" hover-class="none"
:url="'/pages/goods_details/goods_details?id=' + item.id">
<custom-image width="240rpx" height="240rpx" radius="10rpx" lazy-load :src="item.image">
</custom-image>
<view style="padding: 8rpx">
<view class="goods-name line1">{{item.name}}</view>
<view class="row wrap">
<price-format class="mr10" :weight="500" :price="item.seckill_price || item.price" :first-size="32"
:second-size="22" :subscript-size="22" color="#FF2C3C"></price-format>
<price-format :price="item.min_price || item.market_price" color="#999999" :second-size="22"
:first-size="22" :subscript-size="22" :line-through="true"></price-format>
</view>
</view>
</navigator>
</scroll-view>
</view>
</view>
</template>
<script>
import {
trottle,
getRect
} from "@/utils/tools";
export default {
data() {
return {
currentSwiper: 0,
};
},
components: {},
props: {
type: {
type: String,
default: ''
},
lists: {
type: Array,
default: () => []
}
},
watch: {
"lists": function(value) {
if (!value) {
return;
}
}
},
beforeMount: function() {
},
destroyed: function() {},
methods: {}
};
</script>
<style lang="scss" scoped>
.active-area {
background-repeat: no-repeat;
background-size: 100% auto;
border-radius: 10rpx;
padding-bottom: 20rpx;
&.seckill {
background-color: $-color-white;
background-image: url(../../static/images/home_seckill_bg.png);
}
.a-main {
position: relative;
box-sizing: border-box;
.goods-item {
width: 240rpx;
margin-left: 16rpx;
display: inline-block;
border-radius: 10rpx;
overflow: hidden;
&:first-of-type {
margin-left: 20rpx;
}
&:last-of-type {
margin-right: 20rpx;
}
}
}
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<view class="address row bg-white">
<image class="icon-md mr20" src="/static/images/icon_address.png"></image>
<view class="flex1 mr20">
<view class="black md" v-if="!address.contact && isSelect">设置收货地址</view>
<view v-else>
<text class="name md mr10">{{address.contact}}</text>
<text class="phone md">{{address.telephone}}</text>
<view class="area sm mt10 lighter">
{{address.province}} {{address.city}} {{address.district}} {{address.address}}
</view>
</view>
</view>
<image v-if="isSelect" class="icon-sm" src="/static/images/arrow_right.png"></image>
</view>
</template>
<script>
export default {
props:{
isSelect: {
type: Boolean,
default: true
},
address: {
type: [Object, Array],
default: () => ({})
}
},
data() {
return {
};
}
}
</script>
<style lang="scss">
.address {
min-height: 164rpx;
padding: 0 24rpx;
border-radius: 14rpx;
margin: 20rpx 20rpx 0;
}
</style>
@@ -0,0 +1,423 @@
<template>
<view>
<view>
<view class="sale-list" v-if="type == 'normal'">
<view
v-for="(items, index) in lists"
:key="index"
class="sale-item bg-white mt20"
>
<!-- <view class="sale-header row">
<view class="store-name nr ml10 normal">成交时间: {{items.time}}</view>
</view> -->
<view
v-for="(item, index2) in items.order_goods"
:key="index2"
class="goods-item"
>
<view class="sale-goods-show row">
<view class="goods-img">
<custom-image
width="100%"
height="100%"
radius="6rpx"
lazy-load
:src="item.image"
/>
</view>
<view class="goods-desc">
<view class="goods-name line2 nr">{{ item.goods_name }}</view>
<view class="row-between mt20">
<price-format
class="sm"
:firstSize="30"
:secondSize="30"
:showSubscript="true"
:subscriptSize="26"
:price="item.goods_price"
/>
<view class="nr">x{{ item.goods_num }}</view>
</view>
</view>
</view>
<view
:class="
'sale-footer row-end ' +
(items.after_sale.able_apply == 1 ? '' : 'bottom-opacity')
"
>
<view
class="btn row-center bd-primary primary br60"
@click="
goPage(
$event,
items.after_sale.able_apply,
'/bundle/pages/apply_refund/apply_refund',
items.order_id,
item.item_id
)
"
>
申请售后
</view>
</view>
</view>
</view>
</view>
<view class="sale-list" v-else-if="type == 'apply'">
<view
v-for="(items, index) in lists"
:key="index"
class="sale-item bg-white mt20"
>
<view class="sale-header row-between">
<view class="row">
<view class="store-name nr ml10 mr10"
>申请时间: {{ items.time }}</view
>
</view>
<view class="primary nr">{{ items.after_sale.type_text }}</view>
</view>
<navigator
v-for="(item, index2) in items.order_goods"
:key="index2"
hover-class="none"
class="sale-goods-show"
:url="
'/bundle/pages/after_sales_detail/after_sales_detail?afterSaleId=' +
items.after_sale.after_sale_id +
'&order_id=' +
items.order_id
"
>
<view class="row">
<view class="goods-img">
<custom-image
width="100%"
height="100%"
radius="6rpx"
lazy-load
:src="item.image"
/>
</view>
<view class="goods-desc">
<view class="goods-name line2 nr">{{ item.goods_name }}</view>
<view class="row-between mt20">
<view>
<price-format
:firstSize="26"
:price="item.goods_price"
weight="600"
:showSubscript="true"
/>
</view>
<view class="nr">x{{ item.goods_num }}</view>
</view>
<view class="row-between mt20">
<view>
<price-format
:firstSize="26"
:price="items.after_sale.refund_price"
weight="600"
:showSubscript="true"
color="red"
/>
</view>
<view class="nr">退款金额</view>
</view>
</view>
</view>
<view class="sale-status mt20 row">
<view class="nr" style="font-weight: bold">申请状态</view>
<view class="nr ml20">{{ items.after_sale.desc }}</view>
</view>
</navigator>
<view class="sale-footer row-end">
<view
class="row-center normal br60 mr20 grey-btn nr"
@tap="showDialog(items.after_sale.after_sale_id)"
>撤销申请</view
>
<navigator
hover-class="none"
:url="
'/bundle/pages/input_express_info/input_express_info?id=' +
items.after_sale.after_sale_id
"
class="row-center normal br60 grey-btn nr"
:hidden="items.after_sale.status != 2"
>填写快递单号
</navigator>
</view>
</view>
</view>
<view class="sale-list" v-else>
<navigator
v-for="(items, index) in lists"
:key="index"
hover-class="none"
class="sale-item bg-white mt20"
:url="
'/bundle/pages/after_sales_detail/after_sales_detail?afterSaleId=' +
items.after_sale.after_sale_id
"
>
<view class="sale-header row-between">
<view class="row">
<!-- <image style="width: 40rpx;height: 40rpx" src="/images/icon_shop.png"></image> -->
<view class="store-name nr ml10 mr10"
>申请时间: {{ items.time }}</view
>
</view>
<view class="primary nr">{{ items.after_sale.type_text }}</view>
</view>
<view
v-for="(item, index2) in items.order_goods"
:key="index2"
class="sale-goods-show"
>
<view class="row">
<view class="goods-img">
<custom-image
lazy-load
width="100%"
height="100%"
radius="6rpx"
:src="item.image"
/>
</view>
<view class="goods-desc">
<view class="goods-name line2 nr">{{ item.goods_name }}</view>
<view class="row-between mt20 row-between">
<price-format
:firstSize="26"
:price="item.goods_price"
weight="600"
:showSubscript="true"
/>
<view class="nr">x{{ item.goods_num }}</view>
</view>
<view class="row-between mt20">
<view>
<price-format
:firstSize="26"
:price="items.after_sale.refund_price"
weight="600"
:showSubscript="true"
color="red"
/>
</view>
<view class="nr">退款金额</view>
</view>
</view>
</view>
<view class="sale-status mt20 row">
<view class="nr" style="font-weight: bold">申请状态</view>
<view class="nr ml20">{{ items.after_sale.desc }}</view>
</view>
</view>
</navigator>
</view>
<loading-footer :status="loadingStatus" slotEmpty>
<view class="data-null column-center" slot="empty">
<image class="img-null" src="/static/images/order_null.png"></image>
<text class="nr muted">暂无售后数据</text>
</view>
</loading-footer>
</view>
<u-modal
v-model="confirmDialog"
confirm-text="确定"
:showCancelButton="true"
:show-title="false"
confirm-color="#FF2C3C"
@confirm="cancelApplyFun"
@cancel="hideDialog"
>
<view class="column-center tips-dialog" style="padding: 20rpx 0">
<image class="icon-lg" src="/static/images/icon_warning.png"></image>
<view style="margin-top: 30rpx">是否要撤销申请</view>
</view>
</u-modal>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | LikeShop100%开源免费商用电商系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,保留版权即可
// | 商业版本务必购买商业授权,以免引起法律纠纷
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | Gitee下载:https://gitee.com/likeshop_gitee/likeshop
// | 访问官网:https://www.likemarket.net
// | 访问社区:https://home.likemarket.net
// | 访问手册:http://doc.likemarket.net
// | 微信公众号:好象科技
// | 好象科技开发团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | Author: LikeShopTeam
// +----------------------------------------------------------------------
import { AfterSaleType, loadingType } from "@/utils/type";
import {
getAfterSaleList,
cancelApply,
afterSaleDetail,
applyAgain,
} from "@/api/user";
import { loadingFun } from "@/utils/tools";
export default {
data() {
return {
lists: [],
page: 1,
loadingStatus: loadingType.LOADING,
confirmDialog: false,
};
},
components: {},
props: {
type: {
type: String,
default: AfterSaleType.NORMAL,
},
},
created() {
console.log("type", this.type);
uni.$on("refreshsale", () => {
this.reflesh();
});
},
beforeMount() {
this.getAfterSaleListFun();
},
destroyed: function () {
uni.$off("refreshsale");
},
methods: {
cancelApplyFun() {
console.log(this.id, "id");
cancelApply({
id: this.id,
}).then((res) => {
if (res.code == 1) {
this.$toast({
title: res.msg,
});
uni.$emit("refreshsale");
}
});
},
getAfterSaleListFun() {
let { lists, loadingStatus, page } = this;
loadingFun(getAfterSaleList, page, lists, loadingStatus, {
type: this.type,
}).then((res) => {
console.log(res, "res");
if (res) {
this.page = res.page;
this.loadingStatus = res.status;
this.lists = res.dataList;
}
});
},
goPage(e, able_apply, url, order_id, item_id) {
if (able_apply != 1) {
return;
}
url = url + "?order_id=" + order_id + "&item_id=" + item_id;
uni.navigateTo({
url: url,
});
},
reflesh() {
this.page = 1;
this.lists = [];
this.loadingStatus = loadingType.LOADING;
this.getAfterSaleListFun();
},
showDialog(id) {
console.log(id, "showDialog");
this.id = id;
this.confirmDialog = true;
},
hideDialog() {
this.comfirmDialog = false;
},
},
};
</script>
<style lang="scss">
.sale-list {
/* padding: 20rpx 0; */
.sale-item {
.sale-goods-show {
padding: 20rpx 24rpx;
.goods-img {
height: 160rpx;
width: 160rpx;
}
.goods-desc {
margin-left: 24rpx;
flex: 1;
}
.sale-status {
padding: 20rpx 40rpx;
background-color: #f6f6f6;
border-radius: 6rpx;
}
}
.sale-header {
padding: 20rpx 24rpx;
.store-name {
font-family: PingFang SC;
line-height: 40rpx;
}
}
.sale-footer {
padding: 0 24rpx 22rpx;
.btn {
padding: 9rpx 34rpx;
font-family: PingFang SC;
border: 1px solid $-color-primary;
}
.grey-btn {
border: 1px solid #cccccc;
padding: 9rpx 34rpx;
font-family: PingFang SC;
}
}
}
}
.bottom-opacity {
opacity: 0.5;
}
.data-null {
padding-top: 150rpx;
}
.tips-dialog {
height: 230rpx;
width: 100%;
}
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<view class="bargain-list-container">
<view class="bargain-list mt20" v-for="(item, index) in lists" :key="item.id">
<view class="header row-between">
<view class="count-down-container row">
<text class="muted xs" style="margin-left: 8rpx;">发起时间{{item.create_time}}</text>
</view>
<view class="primary sm">
{{item.status_text}}
</view>
</view>
<navigator class="content row" hover-class="none" :url="'/bundle/pages/bargain_process/bargain_process?bargainId='+item.id">
<custom-image width="180rpx" height="180rpx" :src="item.image" radius="10rpx" />
<view class="goods-info">
<view class="goods-name line2 nr normal">
{{item.name}}
</view>
<view class="mt10 row">
<view class="sm primary">
已砍至<text class="sm" style="font-weight: 500;line-height: 48rpx;"><text class="xl">{{item.current_price}}</text></text>
</view>
<view class="xs muted ml20">
原价<text style="text-decoration: line-through;"> ¥{{item.price}}</text>
</view>
</view>
<view class="xxs" style="color: #FFA200;line-height: 30rpx;margin-top: 4rpx;">
{{item.bargain_tips}}
</view>
</view>
</navigator>
<view class="footer row-end">
<!-- <view hover-class="none" class="purchase-btn primary row-center br60 footer-btn" v-if="item.buy_btn" @click="onPurchaseClick(item.id, item.item_id)">
直接购买
</view> -->
<navigator hover-class="none" :url="'/bundle/pages/bargain_process/bargain_process?bargainId='+item.id" class="bargain-btn footer-btn white row-center br60" v-if="item.btn_tips != ''">
{{item.btn_tips}}
</navigator>
<!-- <navigator hover-class="none" class="bargain-btn white row-center br60 footer-btn" v-if="item.pay_btn" :url="'/pages/order_details/order_details?id=' + item.order_id">
去支付
</navigator> -->
</view>
</view>
<loading-footer :status="loadingStatus" slotEmpty>
<view class="data-null column-center" slot="empty">
<image class="img-null" src="/static/images/order_null.png" />
<view class="xs muted">
暂无砍价记录
</view>
</view>
</loading-footer>
</view>
</template>
<script>
import {loadingType} from '@/utils/type'
import {getBargainActivityList} from "@/api/activity"
import {loadingFun} from '@/utils/tools'
export default {
data() {
return {
loadingStatus: loadingType.LOADING,
page: 1,
lists: []
}
},
props: {
bargainType: {
type: Number,
default: -1
}
},
mounted() {
this.$getBargainActivityList()
},
onReachBottom() {
},
methods: {
$getBargainActivityList() {
let {page, lists, loadingStatus, bargainType} = this;
loadingFun(getBargainActivityList, page, lists, loadingStatus, {type: bargainType}).then(res => {
if(res) {
this.page = res.page;
this.lists = res.dataList;
this.loadingStatus = res.status;
}
})
},
onPurchaseClick(bargainId, itemId) {
let goods = [{
num: 1,
item_id: itemId
}]
let params = {
goods
}
uni.navigateTo({
url: '/pages/confirm_order/confirm_order?data=' + encodeURIComponent(JSON.stringify(params)) + "&bargain_launch_id=" + bargainId
});
}
}
}
</script>
<style lang="scss">
.bargain-list-container {
.bargain-list {
background-color: $-color-white;
.header {
height: 80rpx;
padding: 20rpx 20rpx;
}
.content {
padding: 30rpx 24rpx 18rpx;
.goods-info {
margin-left: 24rpx;
flex: 1;
.goods-name {
line-height: 40rpx;
}
}
}
.footer {
padding: 19rpx 24rpx;
.bargain-btn {
// 继续砍价 按钮
height: 62rpx;
padding: 0 44rpx;
background-color: $-color-primary;
}
.purchase-btn {
// 直接购买
height: 62rpx;
padding: 0 44rpx;
background-color: rgba(255, 44, 60, 0.1);
}
.footer-btn {
&:not(:last-child) {
margin-right: 20rpx;
}
}
}
}
.data-null {
margin-top: 150rpx;
}
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<view class="bubble-tips-container" :style="{top: top, left: left}">
<view class="bubble-content row" v-show="showBubble" v-for="item in currentList" :key="item.id">
<image class="bubble-img" :src="item.user.avatar" />
<view class="xs">
{{item.template}}
</view>
</view>
</view>
</template>
<script>
import {
getBubbleLists
} from "@/api/app";
import Cache from "@/utils/cache"
export default {
name: "BubbleTips",
props: {
// 是否停止切换
discharge: {
type: Boolean,
default: false
},
top: {
type: String,
default: "40rpx"
},
left: {
type: String,
default: "20rpx"
},
updateTime: {
type: Number,
default: 5 * 60 * 1000
}
},
data() {
return {
index: Cache.get("currentIndex") || 0,
list: [],
currentList: [],
timer: null,
showBubble: false,
}
},
watch: {
index(newValue, oldValue) {
if (this.index - this.list.length >= 0) {
this.showBubble = false;
let timer = setTimeout(() => {
Cache.set("currentIndex", 0)
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
this.fadeUpBubble();
clearTimeout(timer)
}, 2000)
} else {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
this.fadeUpBubble();
return;
}
},
discharge() {
// 为了让活性的页面停止计时器
if (this.discharge) {
// 停止
Cache.set("currentIndex", this.index);
clearInterval(this.timer);
this.timer = null;
return false;
} else {
// 继续切换
let currentInex = Cache.get("currentInex") || this.list.length;
if (currentInex - this.list.length < 0) {
if (this.timer) {
setInterval(this.timer)
this.timer = null;
}
this.fadeUpBubble()
}
}
}
},
methods: {
$getBubbleLists() {
getBubbleLists().then(res => {
if (res.code == 1) {
this.list = res.data.lists;
var requestTime = res.data.time * 1000;
Cache.set("bubbleList", JSON.stringify(this.list), 300);
Cache.set("requestTime", requestTime);
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.fadeUpBubble()
}
})
},
fadeUpBubble() {
let requestTime = Cache.get("requestTime");
let currentTime = new Date();
this.showBubble = true;
this.index = Cache.get("currentIndex") || 0;
this.list = Cache.get("bubbleList") ? JSON.parse(Cache.get("bubbleList")) : [];
if (currentTime.getTime() - requestTime >= this.updateTime) {
this.$getBubbleLists();
Cache.set("currentIndex", 0, 300)
return;
}
this.timer = setInterval(() => {
this.currentList = this.list.slice(this.index, this.index + 1);
Cache.set("currentIndex", ++this.index);
}, 4000);
}
},
created() {
var index = Cache.get("currentIndex") || 0;
var requestTime = Cache.get("requestTime");
var currentTime = new Date();
var currentList = Cache.get("bubbleList") ? JSON.parse(Cache.get("bubbleList")) : [];
if (currentList.length <= 0) {
this.$getBubbleLists();
Cache.set("currentIndex", 0)
} else if (index - currentList.length >= 0) {
Cache.set("currentIndex", 0);
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
this.fadeUpBubble();
} else if (currentTime.getTime() - requestTime >= this.updateTime) {
this.$getBubbleLists();
Cache.set("currentIndex", 0)
} else {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.fadeUpBubble()
}
},
onLoad() {},
destroyed() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null
}
}
}
</script>
<style lang="scss">
.bubble-tips-container {
position: absolute;
z-index: 98;
.bubble-content {
padding: 4rpx 20rpx 4rpx 10rpx;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 120rpx;
.bubble-img {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin-right: 10rpx;
}
}
}
</style>
+319
View File
@@ -0,0 +1,319 @@
<template>
<view class="cate-four row">
<view class="aside">
<scroll-view style="height: 100%" scroll-y="true" scroll-with-animation="true">
<block v-for="(item, index) in cateList" :key="index">
<view :class="'one-item sm ' + (index == selectIndex ? 'active' : '')" @click="changeActive(index)">
<text class="name">{{ item.name }}</text>
<view v-if="index == selectIndex" class="active-line bg-primary"></view>
</view>
</block>
</scroll-view>
</view>
<view class="main bg-body">
<scroll-view style="height: 100%" scroll-y="true" scroll-with-animation="true" @scrolltolower="getGoodsSearchFun">
<view class="main-wrap">
<view style="border-radius: 10rpx;">
<swipers :pid="4" height="200rpx" previous-margin="0" padding="20rpx 0 0" radius="10rpx">
</swipers>
</view>
<view class="cate-two mt20" v-if="currentType">
<view v-for="(sitem, sindex) in cateTwoList" :key="sindex" class="two-item bg-white mb20">
<navigator class="title row-between" v-if="sitem.type == 1" hover-class="none"
:url="`/pages/goods_search/goods_search?id=${sitem.id}&name=${sitem.name}&type=${sitem.type}`">
<text class="name bold sm">{{sitem.name}}</text>
<u-icon name="arrow-right"></u-icon>
</navigator>
<view class="title row-between" v-else>
<text class="name bold sm">{{sitem.name}}</text>
</view>
<view class="three-list row wrap">
<navigator v-for="(titem, tindex) in sitem.sons" :key="tindex"
class="three-item column-center mb20" hover-class="none"
:url="`/pages/goods_search/goods_search?id=${titem.id}&name=${titem.name}&type=${titem.type}`">
<u-image mode="aspectFit" width="150rpx" height="150rpx" :src="titem.image">
</u-image>
<view class="text mt20 xs">{{ titem.name }}</view>
</navigator>
</view>
</view>
</view>
<view class="goods" v-else>
<view class="row condition bg-white mt20">
<view :class="'tag row-center ' + (comprehensive ? 'primary' : '')" @tap="onNormal">综合</view>
<view class="tag row-center" data-type="priceSort" @tap="onPriceSort">
<text :class="priceSort ? 'primary' : ''">价格</text>
<view>
<trigonometry direction="up" size="small" :color="priceSort == 'desc' ? '#FF5058' : '#333'">
</trigonometry>
<trigonometry size="small" :color="priceSort == 'asc' ? '#FF5058' : '#333'"></trigonometry>
</view>
</view>
<view class="tag row-center" data-type="saleSort" @tap="onSaleSort">
<text :class="saleSort ? 'primary' : ''">销量</text>
<view>
<trigonometry direction="up" size="small" :color="saleSort == 'desc' ? '#FF5058' : '#333'">
</trigonometry>
<trigonometry size="small" :color="saleSort == 'asc' ? '#FF5058' : '#333'"></trigonometry>
</view>
</view>
</view>
<view class="goods-list">
<navigator hover-class="none" class="row item bg-white mt20" v-for="(item, index) in goodsList" :key="index" :url="`/pages/goods_details/goods_details?id=${item.id}`">
<u-image width="200rpx" height="200rpx" border-radius="14rpx" :src="item.image"></u-image>
<view class="flex1 ml20 mr10">
<view class="line2">{{item.name}}</view>
<view class="muted" >
<text class="xxs">原价</text> <price-format :subscriptSize="22" :firstSize="22" :secondSize="22" :price="item.market_price"></price-format>
</view>
<view class="primary mt10">
<price-format :price="item.price" :subscriptSize="22" :firstSize="34" :secondSize="26"></price-format>
</view>
</view>
</navigator>
</view>
<loading-footer :status="status" :slot-empty="true">
<view slot="empty" class="column-center" style="padding: 200rpx 0 0">
<image class="img-null" src="/static/images/goods_null.png"></image>
<text class="lighter sm">暂无商品</text>
</view>
</loading-footer>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import {
arraySlice,
trottle,
loadingFun
} from '@/utils/tools'
import {
getGoodsSearch,
} from '@/api/store';
import {
loadingType
} from '@/utils/type';
export default {
name: "cate-four",
props: {
list: {
type: Array,
default: () => ([])
}
},
data() {
return {
page:1,
status: loadingType.LOADING,
selectIndex: 0,
cateList: [],
cateTwoList: [],
goodsList: [],
priceSort: '',
saleSort: '',
};
},
created() {
this.onNormal = trottle(this.onNormal, 500, this);
this.onPriceSort = trottle(this.onPriceSort, 500, this);
this.onSaleSort = trottle(this.onSaleSort, 500, this);
},
methods: {
changeActive(index) {
const {
cateList
} = this
this.selectIndex = index
this.cateTwoList = cateList[this.selectIndex].sons || []
this.onRefresh()
},
onRefresh() {
this.page = 1
this.goodsList = []
this.status = loadingType.LOADING
this.$nextTick(() => {
this.getGoodsSearchFun();
});
},
onNormal() {
this.priceSort = ''
this.saleSort = ''
this.onRefresh();
},
onPriceSort() {
let {
priceSort
} = this;
this.saleSort = ''
this.priceSort = priceSort == 'asc' ? 'desc' : 'asc'
this.onRefresh();
},
onSaleSort() {
let {
saleSort
} = this;
this.priceSort = ''
this.saleSort = saleSort == 'desc' ? 'asc' : 'desc'
this.onRefresh();
},
async getGoodsSearchFun() {
let {
page,
goodsList,
priceSort,
saleSort,
status,
cateList,
selectIndex
} = this;
const item = cateList[selectIndex]
if(item.type == 0) return
if (status == loadingType.FINISHED) return;
const params = {
category_id: item.id,
page_no: page,
price: priceSort,
sales_sum: saleSort
}
const data = await loadingFun(getGoodsSearch, page, goodsList, status, params)
if (!data) return
this.page = data.page
this.goodsList = data.dataList
this.status = data.status
},
},
watch: {
list: {
handler(val) {
if(!val.length) return
let index = val.findIndex((item) => item.type == 1)
this.selectIndex = index == -1 ? 0 : index
this.cateList = val
this.cateTwoList = val[this.selectIndex] ? val[this.selectIndex].sons : []
this.getGoodsSearchFun()
}
}
},
computed: {
comprehensive() {
const {
priceSort,
saleSort
} = this
if (priceSort == '' && saleSort == '') {
return true;
}
return false;
},
currentType() {
const {
cateList,
selectIndex
} = this
return cateList[selectIndex] ? cateList[selectIndex].type == 0 : true
}
},
}
</script>
<style lang="scss">
$header-height: 94rpx;
.cate-four {
height: calc(100vh - #{$header-height} - var(--window-top) - var(--window-bottom));
background-color: #fff;
.aside {
width: 180rpx;
flex: none;
height: 100%;
.one-item {
position: relative;
text-align: center;
padding: 26rpx 10rpx;
&.active {
color: $-color-primary;
font-size: 26rpx;
font-weight: bold;
}
.active-line {
position: absolute;
width: 6rpx;
height: 30rpx;
left: 4rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
.main {
height: 100%;
flex: 1;
.main-wrap {
position: relative;
padding: 0 20rpx;
.goods {
.item {
border-radius: 14rpx;
overflow: hidden;
.add {
width: 46rpx;
height: 46rpx;
line-height: 46rpx;
text-align: center;
}
}
}
.two-item {
border-radius: 10rpx;
.title {
height: 90rpx;
padding: 0 20rpx;
.line {
width: 40rpx;
height: 1px;
background-color: #BBBBBB;
}
}
.three-list {
align-items: flex-start;
padding: 0 10rpx;
.three-item {
width: 33%;
.text {
text-align: center;
}
}
}
}
}
}
.condition {
height: 80rpx;
border-radius: 10rpx;
.tag {
flex: 1;
}
}
}
</style>
+300
View File
@@ -0,0 +1,300 @@
<template>
<scroll-view scroll-y="true" class="cate-scroll" @scrolltolower="getGoodsSearchFun">
<view class="cate-list">
<view class="cate-nav bg-white" v-if="navList.length && type == 2 || cate.type == 0">
<swiper :style="'height:' + navSwiperH + 'rpx;'" @change="swiperChange">
<swiper-item v-for="(items, index) in navList" :key="index">
<view class="nav-list row wrap">
<view v-for="(item, index2) in items" :key="index2" @tap="changeCate(item)"
class="nav-item column-center">
<view class="icon-wrap" :class="{active: id == item.id}">
<u-image width="82rpx" height="82rpx" border-radius="50%" :src="item.image"></u-image>
</view>
<view style="width: 90%;" class="name xs line1 u-text-center" :class="{primary: id == item.id}">{{item.name}}</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="dots" v-if="navList.length > 1">
<view v-for="(item, index) in navList" :key="index"
:class="'dot ' + (index == currentSwiper ? 'active' : '')"></view>
</view>
</view>
<swipers :pid="4" height="267rpx" previous-margin="0" padding="20rpx 20rpx 0" radius="10rpx"></swipers>
<view v-if="cate.type !== 0">
<view class="row condition bg-white mt20">
<view :class="'tag row-center ' + (comprehensive ? 'primary' : '')" @tap="onNormal">综合</view>
<view class="tag row-center" data-type="priceSort" @tap="onPriceSort">
<text :class="priceSort ? 'primary' : ''">价格</text>
<view>
<trigonometry direction="up" size="small" :color="priceSort == 'desc' ? '#FF5058' : '#333'">
</trigonometry>
<trigonometry size="small" :color="priceSort == 'asc' ? '#FF5058' : '#333'"></trigonometry>
</view>
</view>
<view class="tag row-center" data-type="saleSort" @tap="onSaleSort">
<text :class="saleSort ? 'primary' : ''">销量</text>
<view>
<trigonometry direction="up" size="small" :color="saleSort == 'desc' ? '#FF5058' : '#333'">
</trigonometry>
<trigonometry size="small" :color="saleSort == 'asc' ? '#FF5058' : '#333'"></trigonometry>
</view>
</view>
<view class="tag row-center" @tap="changeType">
<image class="icon-sm"
:src=" goodsType === 'one' ? '/static/images/icon_double.png' : '/static/images/icon_one.png'">
</image>
</view>
</view>
<view class="goods">
<view class="goods-list">
<view v-show="goodsType == 'double'" class="double">
<goods-list type="double" :list="goodsList"></goods-list>
</view>
<view v-show="goodsType == 'one'" class="one" style="padding: 0 20rpx">
<goods-list :list="goodsList" type="one"></goods-list>
</view>
</view>
<loading-footer :status="status" :slot-empty="true">
<view slot="empty" class="column-center" style="padding: 200rpx 0">
<image class="img-null" src="/static/images/goods_null.png"></image>
<text class="lighter">暂无商品</text>
</view>
</loading-footer>
</view>
</view>
</view>
</scroll-view>
</template>
<script>
import {
mapGetters,
mapActions
} from 'vuex'
import {
arraySlice,
trottle,
loadingFun
} from '@/utils/tools'
import {
getGoodsSearch,
} from '@/api/store';
import {
loadingType
} from '@/utils/type';
export default {
props: {
cate: {
type: Object,
default: () => ({})
},
type: {
type: Number,
default: 1
}
},
data() {
return {
navSwiperH: 374,
navList: [],
currentSwiper: 0,
status: loadingType.LOADING,
page: 1,
goodsType: 'double',
goodsList: [],
priceSort: '',
saleSort: '',
id: '',
};
},
created() {
this.onNormal = trottle(this.onNormal, 500, this);
this.onPriceSort = trottle(this.onPriceSort, 500, this);
this.onSaleSort = trottle(this.onSaleSort, 500, this);
this.getGoodsSearchFun()
},
watch: {
cate: {
immediate: true,
handler(val) {
let navList = []
console.log(val)
if (val.type === 0) {
navList = val.sons[0].sons || []
} else {
navList = val.sons || []
}
if (navList.length <= 5) {
this.navSwiperH = 200
} else {
this.navSwiperH = 374
}
this.id = val.id
this.navList = arraySlice(navList)
}
}
},
computed: {
...mapGetters(["appConfig"]),
comprehensive() {
const {
priceSort,
saleSort
} = this
if (priceSort == '' && saleSort == '') {
return true;
}
return false;
}
},
methods: {
changeCate(item) {
if(this.cate.type === 0) {
uni.navigateTo({
url: `/pages/goods_search/goods_search?id=${item.id}&name=${item.name}&type=${item.type}`
})
return
}
if(this.id == item.id) {
this.id = this.cate.id
this.onRefresh()
return
}
this.id = item.id
this.onRefresh()
},
swiperChange(e) {
this.currentSwiper = e.detail.current
},
changeType() {
this.goodsType = this.goodsType === 'one' ? 'double' : 'one'
},
onNormal() {
this.priceSort = ''
this.saleSort = ''
this.onRefresh();
},
onPriceSort() {
let {
priceSort
} = this;
this.saleSort = ''
this.priceSort = priceSort == 'asc' ? 'desc' : 'asc'
this.onRefresh();
},
onSaleSort() {
let {
saleSort
} = this;
this.priceSort = ''
this.saleSort = saleSort == 'desc' ? 'asc' : 'desc'
this.onRefresh();
},
onRefresh() {
this.showHistory = false
this.page = 1
this.goodsList = []
this.status = loadingType.LOADING
this.$nextTick(() => {
this.getGoodsSearchFun();
});
},
async getGoodsSearchFun() {
let {
page,
goodsList,
priceSort,
saleSort,
status
} = this;
if (status == loadingType.FINISHED) return;
const params = {
category_id: this.id,
page_no: page,
price: priceSort,
sales_sum: saleSort
}
const data = await loadingFun(getGoodsSearch, page, goodsList, status, params)
if (!data) return
this.page = data.page
this.goodsList = data.dataList
this.status = data.status
},
}
};
</script>
<style lang="scss">
.cate-scroll {
height: calc(100vh - 174rpx - var(--window-bottom));
}
.cate-list {
.cate-nav {
position: relative;
border-radius: 20rpx;
margin: 20rpx 20rpx 0;
.nav-item {
width: 20%;
margin-top: 30rpx;
.icon-wrap {
border: 1px solid transparent;
border-radius: 50%;
&.active {
border-color: $-color-primary;
}
}
.name {
margin-top: 14rpx;
}
}
.dots {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 20rpx;
display: flex;
.dot {
width: 10rpx;
height: 6rpx;
border-radius: 6rpx;
margin-right: 10rpx;
background-color: rgba(255, 44, 60, 0.4);
&.active {
width: 20rpx;
background-color: $-color-primary;
}
}
}
}
.condition {
height: 80rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
.tag {
flex: 1;
}
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<view class="cate-one">
<tabs :active="selectIndex" @change="changeShow" :config="{itemWidth: 150}">
<tab v-for="(item, index) in cateList" :key="index" :title="item.name">
<cate-list v-if="item.isShow" :cate="item"></cate-list>
</tab>
</tabs>
</view>
</template>
<script>
export default {
name:"cate-one",
props: {
list: {
type: Array,
default: () => ([])
}
},
data() {
return {
selectIndex: 0,
cateList: []
};
},
methods:{
changeShow(index) {
this.selectIndex = index
this.cateList[this.selectIndex].isShow = true
},
},
watch: {
list: {
immediate: true,
handler(val) {
let index = val.findIndex((item) => item.type == 1)
this.selectIndex = index == -1 ? 0 : index
val.forEach((item, index) => item.isShow = this.selectIndex == index ? true : false)
this.cateList = val
}
}
},
}
</script>
<style lang="scss">
</style>
+162
View File
@@ -0,0 +1,162 @@
<template>
<view class="cate-three row">
<view class="aside">
<scroll-view style="height: 100%" scroll-y="true" scroll-with-animation="true">
<block v-for="(item, index) in cateList" :key="index">
<view :class="'one-item sm ' + (index == selectIndex ? 'active' : '')" @click="changeActive(index)">
<text class="name">{{ item.name }}</text>
<view v-if="index == selectIndex" class="active-line bg-primary"></view>
</view>
</block>
</scroll-view>
</view>
<view class="main bg-body">
<scroll-view style="height: 100%" scroll-y="true" scroll-with-animation="true">
<view class="main-wrap">
<view style="border-radius: 10rpx;">
<swipers :pid="4" height="200rpx" previous-margin="0" padding="20rpx 0 0" radius="10rpx">
</swipers>
</view>
<view class="cate-two mt20">
<view v-for="(sitem, sindex) in cateTwoList" :key="sindex" class="two-item bg-white mb20">
<navigator class="title row-between" v-if="sitem.type == 1" hover-class="none"
:url="`/pages/goods_search/goods_search?id=${sitem.id}&name=${sitem.name}&type=${sitem.type}`">
<text class="name bold sm">{{sitem.name}}</text>
<u-icon name="arrow-right"></u-icon>
</navigator>
<view class="title row-between" v-else>
<text class="name bold sm">{{sitem.name}}</text>
</view>
<view class="three-list row wrap">
<navigator v-for="(titem, tindex) in sitem.sons" :key="tindex"
class="three-item column-center mb20" hover-class="none"
:url="`/pages/goods_search/goods_search?id=${titem.id}&name=${titem.name}&type=${titem.type}`">
<u-image mode="aspectFit" width="150rpx" height="150rpx" :src="titem.image">
</u-image>
<view class="text mt20 xs">{{ titem.name }}</view>
</navigator>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: "cate-three",
props: {
list: {
type: Array,
default: () => ([])
}
},
data() {
return {
selectIndex: 0,
cateList: [],
cateTwoList: []
};
},
methods: {
changeActive(index) {
const {
cateList
} = this
this.selectIndex = index
this.cateTwoList = cateList[this.selectIndex].sons
},
},
watch: {
list: {
immediate: true,
handler(val) {
let index = val.findIndex((item) => item.type == 1)
this.selectIndex = index == -1 ? 0 : index
this.cateList = val
this.cateTwoList = val[this.selectIndex] ? val[this.selectIndex].sons : []
}
}
},
}
</script>
<style lang="scss">
$header-height: 94rpx;
.cate-three {
height: calc(100vh - #{$header-height} - var(--window-top) - var(--window-bottom));
background-color: #fff;
.aside {
width: 180rpx;
flex: none;
height: 100%;
.one-item {
position: relative;
text-align: center;
padding: 26rpx 10rpx;
&.active {
color: $-color-primary;
font-size: 26rpx;
font-weight: bold;
}
.active-line {
position: absolute;
width: 6rpx;
height: 30rpx;
left: 4rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
.main {
height: 100%;
flex: 1;
.main-wrap {
position: relative;
padding: 0 20rpx;
.two-item {
border-radius: 10rpx;
.title {
height: 90rpx;
padding: 0 20rpx;
.line {
width: 40rpx;
height: 1px;
background-color: #BBBBBB;
}
}
.three-list {
align-items: flex-start;
padding: 0 10rpx;
.three-item {
width: 33%;
.text {
text-align: center;
}
}
}
}
}
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<view class="cate-two">
<tabs :active="selectIndex" @change="changeShow" :config="{itemWidth: 150}">
<tab v-for="(item, index) in cateList" :key="index" :title="item.name">
<cate-list v-if="item.isShow" :type="2" :cate="item"></cate-list>
</tab>
</tabs>
</view>
</template>
<script>
export default {
name:"cate-two",
props: {
list: {
type: Array,
default: () => ([])
}
},
data() {
return {
selectIndex: 0,
cateList: []
};
},
methods:{
changeShow(index) {
this.selectIndex = index
this.cateList[this.selectIndex].isShow = true
},
},
watch: {
list: {
immediate: true,
handler(val) {
let index = val.findIndex((item) => item.type == 1)
this.selectIndex = index == -1 ? 0 : index
val.forEach((item, index) => item.isShow = this.selectIndex == index ? true : false)
this.cateList = val
}
}
},
}
</script>
<style lang="scss">
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<!-- components/comment_list/comment_list.wxml -->
<view height="calc(100vh - 80rpx)">
<view class="comment-list">
<view v-for="(item, index) in list" :key="index" class="comment-item bg-white mt20">
<view class="xs muted ml20" style="padding-top: 20rpx" v-if="item.create_time"
>评价时间{{ item.create_time }}</view
>
<navigator
class="comment-goods row"
:url="'/pages/goods_details/goods_details?id=' + item.goods_id"
hover-class="none"
>
<custom-image
width="160rpx"
height="160rpx"
radius="6rpx"
lazy-load
:src="item.image"
/>
<view class="goods-desc">
<view class="goods-name line2 nr">{{ item.goods_name }}</view>
<view class="row-between mt20" v-show="!(type == 2)">
<price-format
:price="item.goods_price"
:showSubscript="true"
:subscriptSize="26"
:firstSize="30"
:secondSize="30"
/>
<view class="nr">x{{ item.goods_num }}</view>
</view>
<view v-show="!(type == 1)" class="row mt20">
<view class="sm mr10">评分</view>
<u-rate
:disabled="true"
v-model="item.goods_comment"
active-color="#FF2C3C"
:size="24"
/>
</view>
</view>
</navigator>
<view
v-show="!(type == 2)"
class="evaluate-footer row-end"
@click="goCommentGoods(item)"
>
<view class="btn row-center primary br60">评价商品</view>
</view>
<!-- <navigator v-show="!(type == 2)" class="evaluate-footer row-end" hover-class="none" :url="'/bundle/pages/goods_reviews/goods_reviews?id=' + item.id">
<view class="btn row-center primary br60">评价商品</view>
</navigator> -->
<view v-show="!(type == 1 || !item.comment)" class="evaluate-footer">
<view class="preview-evaluate">{{ item.comment }}</view>
</view>
<view
v-show="item.comment_image && !(type == 1 || !item.comment_image.length)"
class="row wrap"
style="padding: 20rpx 10rpx"
>
<view
v-for="(i, idx) in item.comment_image"
:key="idx"
class="comment-img"
:data-current="idx"
:data-uri="item"
@tap="previewImage"
>
<custom-image width="160rpx" height="160rpx" radius="6rpx" :src="i" />
</view>
</view>
</view>
</view>
<loading-footer :status="status" slotEmpty>
<view class="data-null column-center" slot="empty">
<image class="img-null" src="/static/images/news_null.png"></image>
<text class="sm muted">暂无其他评价</text>
</view>
</loading-footer>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
import { loadingType } from '../../utils/type'
import { getOrderCommentList } from '../../api/store'
import { loadingFun } from '@/utils/tools'
export default {
data() {
return {
list: [],
status: loadingType.LOADING,
page: 1
}
},
components: {},
props: {
type: {
type: Number | String
}
},
beforeMount() {
console.log(this.type)
this.getOrderCommentListFun()
},
methods: {
getOrderCommentListFun() {
let { page, type, status, list } = this
loadingFun(getOrderCommentList, page, list, status, { type: type }).then((res) => {
if (res) {
this.page = res.page
this.list = res.dataList
this.status = res.status
}
})
},
goCommentGoods({ id, del }) {
if (+del) return
uni.navigateTo({
url: `/bundle/pages/goods_reviews/goods_reviews?id=${id}`
})
},
goPage(e) {
let { url } = e.currentTarget.dataset
if (this.type == 1) {
uni.navigateTo({
url: url
})
}
},
previewImage(e) {
const { current, uri } = e.currentTarget.dataset
let urls = [uri]
uni.previewImage({
current,
// 当前显示图片的http链接
urls // 需要预览的图片http链接列表
})
}
}
}
</script>
<style lang="scss">
/* components/comment_list/comment_list.wxss */
.comment-list {
.comment-goods {
padding: 20rpx 24rpx;
.goods-desc {
margin-left: 24rpx;
flex: 1;
}
.btn {
padding: 6rpx 34rpx;
border: 1px solid $-color-primary;
}
}
}
.comment-item {
.comment-img {
// &:not(:nth-last-of-type(4n + 1)) {
// margin-right: 20rpx;
// }
margin-right: 20rpx;
margin-bottom: 20rpx;
}
.evaluate-footer {
padding: 0 24rpx 28rpx;
.btn {
width: 178rpx;
height: 52rpx;
border: 1px solid $-color-primary;
}
}
}
.data-null {
padding-top: 200rpx;
}
.evaluate-footer .preview-evaluate {
background-color: #f6f6f6;
padding: 20rpx;
border-radius: 10rpx;
}
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<view class="coupon-list">
<view v-for="(item, index) in list" :key="index" class="mb20">
<view :class="'coupon-item row ' + (btnType == 1 || btnType == 2 ? 'gray': '')">
<view class="price white column-center">
<view class="xl">
<price-format :first-size="60" :second-size="50" :subscript-size="34" :price="item.money" :weight="500" />
</view>
<view class="sm" style="text-align: center">{{item.use_condition}}</view>
</view>
<view class="info ml20">
<view class="bold lg mb10">{{item.name}}</view>
<view class="xs lighter mb20">{{item.use_time_tips}}</view>
<view class="xs lighter ">{{item.coupon_type}}</view>
</view>
<button type="primary" :class="'btn br60 white xs ' + (btnType != 3 ? 'plain': '')" @tap="onHandle(item.id)">
{{getBtn}}
</button>
<image v-if="item.is_get" class="receive" src="/static/images/coupon_receive.png"></image>
</view>
<view style="padding: 14rpx 20rpx" class="bg-white" v-if="item.tips" @tap="onShowTips(index)">
<view class="row-between">
<view class="xs">使用说明</view>
<u-icon :class="showTips[index] ? 'rotate' : ''" name="arrow-down" />
</view>
<view v-show="showTips[index]" class="mt10 xs">{{item.tips}}</view>
</view>
</view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
import { getCoupon } from '../../api/user';
export default {
data() {
return {
showTips: []
};
},
components: {
},
props: {
list: {
type: Array,
default: () => []
},
btnType: {
// 0 去使用 1已使用 2已过期 3领取
type: Number
}
},
watch: {
list: {
handler: function (val) {
let arr = val.map(item => {
return 0;
});
this.showTips = arr
},
immediate: true,
deep: true
}
},
computed: {
},
computed: {
getBtn() {
var text = ''
switch (this.btnType) {
case 0:
text = '去使用';
break;
case 1:
text = '已使用';
break;
case 2:
text = '已过期';
break;
case 3:
text = '领取';
break;
}
return text
}
},
methods: {
onHandle(id) {
this.id = id;
const {
btnType
} = this;
switch (btnType) {
case 0:
uni.switchTab({
url: '/pages/index/index'
});
break;
case 1:
// text = '去使用';
break;
case 2:
// text = '已使用';
break;
case 3:
this.getCouponFun();
break;
}
},
onShowTips(index) {
const {
showTips
} = this;
this.showTips[index] = showTips[index] ? 0 : 1
// 拷贝数组
this.showTips = Object.assign([], this.showTips);
},
getCouponFun() {
getCoupon(this.id).then(res => {
if (res.code == 1) {
this.$toast({title: res.msg})
this.$emit('reflash');
}
});
}
}
};
</script>
<style lang="scss" >
/* components/coupon-list/coupon-list.wxss */
.coupon-list {
padding: 20rpx 24rpx;
.coupon-item {
position: relative;
height: 200rpx;
background-image: url(../../static/images/coupon_bg.png);
background-size: 100% 100%;
&.gray {
background-image: url(../../static/images/coupon_bg_grey.png);
.btn{
&.plain {
color: #CCCCCC;
}
}
}
.price {
width: 200rpx;
}
.btn {
line-height: 52rpx;
height: 52rpx;
position: absolute;
right: 20rpx;
bottom: 20rpx;
width: 120rpx;
text-align: center;
padding: 0;
text-align: center;
box-sizing: border-box;
&.plain {
background-color: #fff;
color: var(--primary-color);
border: 1px solid currentColor;
}
}
.receive {
position: absolute;
right: 30rpx;
top: 0rpx;
width: 99rpx;
height: 77rpx;
}
}
.icon {
transition: all 0.4s;
}
.rotate {
transform: rotateZ(-180deg);
}
}
</style>
+115
View File
@@ -0,0 +1,115 @@
<template>
<view>
<scroll-view style="height: 640rpx;background-color: #F6F6F6" scroll-y="true">
<view class="coupon-obj">
<view v-for="(item, index) in coupons" :key="index" class="mb20 bg-white" @tap="onSelect(item.id)">
<view class="coupon-item row">
<view class="price white column-center">
<view class="xl">
<price-format :subscript-size="34" :first-size="60" :second-size="50" :price="item.money" :weight="500"></price-format>
</view>
<view class="nr">{{item.use_condition}}</view>
</view>
<view class="row-between" style="flex: 1">
<view class="info ml20">
<view class="bold md bold mb10 line1">{{item.name}}</view>
<view class="xxs lighter mb10">{{item.coupon_type}}</view>
<view class="xxs lighter">{{item.use_time_tips}}</view>
</view>
<checkbox v-if="type == 0" :checked="selectId == item.id" class="mr20"></checkbox>
</view>
</view>
<view class="xs" v-if="item.tips" style="padding: 14rpx 20rpx">
{{item.tips}}
</view>
</view>
</view>
<view v-if="coupons.length == 0" class="column-center" style="padding-top: 50rpx">
<image class="img-null" src="/static/images/coupon_null.png"></image>
<text class="muted">暂无优惠券</text>
</view>
</scroll-view>
<view class="column-center">
<view class="bg-primary white row-center br60 mb10 lg" style="margin-top: 12rpx;width: 710rpx;height: 74rpx" @tap="onConfirmCoupon">
确定
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
coupons: [],
selectId: ""
};
},
props: {
list: {
type: Array,
default: () => []
},
type: {
type: Number
},
couponId: [Number, String]
},
watch: {
list: {
handler(val) {
this.coupons = val
},
immediate: true,
},
couponId: {
handler(val) {
this.selectId = val
},
immediate: true,
}
},
methods: {
onSelect(id) {
const {
coupons,
type,
selectId
} = this;
if (type == 1) return;
this.selectId = id == selectId ? "" : id
},
onConfirmCoupon() {
this.$emit("change", this.selectId)
}
}
};
</script>
<style>
.coupon-obj {
padding: 20rpx 24rpx;
}
.coupon-obj .coupon-item {
position: relative;
height: 160rpx;
background-image: url(../../static/images/coupon_bg.png);
background-size: 100% 100%;
}
.coupon-obj .coupon-item .price {
width: 200rpx;
}
.coupon-obj .coupon-item .btn {
position: absolute;
right: 20rpx;
bottom: 20rpx;
padding: 0 49rpx;
}
</style>
+76
View File
@@ -0,0 +1,76 @@
<template>
<view class="cu-progress" :style="'height: ' + progressHeight + '; width: ' + progressWidth + ';background-color: ' + progressColor">
<view class="cu-progress-bar" :style="'background-color: ' + progressBarColor + ';left: ' + barLeft + 'rpx; width:' + barWidth + 'rpx'">
</view>
</view>
</template>
<script>
export default {
data() {
return {
currentSwiper: 0,
// 剩余滑行距离
offset: 0,
barLeft: 0
};
},
components: {},
props: {
progressBarColor: {
type: String,
default: '#01B55B'
},
progressWidth: {
type: Number,
default: 90
},
progressHeight: {
type: Number,
default: 6
},
progressColor: {
type: String,
default: '#E5E5E5'
},
left: {
type: Number,
default: 0
},
barWidth: {
type: Number,
default: 30
}
},
watch: {
'left': function (value) {
this.barLeft = value / 100 * this.offset
}
},
beforeMount: function () {
this.offset = this.progressWidth - this.barWidth
},
destroyed: function () {},
methods: {
}
};
</script>
<style>
.cu-progress {
background-color: #E5E5E5;
height: 6rpx;
width: 90rpx;
position: relative;
}
.cu-progress .cu-progress-bar {
height: 100%;
width: 30rpx;
position: absolute;
left: 0;
}
</style>
+157
View File
@@ -0,0 +1,157 @@
<template>
<view
:style="[viewStyle]"
:class="{ 'custom-image': true, 'image-round': round }"
@click="onClick"
>
<image
v-if="!error"
:src="src"
:mode="mode"
:lazy-load="lazyLoad"
class="image"
:show-menu-by-longpress="showMenuByLongpress"
@load="onLoaded"
@error="onErrored"
></image>
<view v-if="loading && showLoading" class="loading-wrap image">
<slot v-if="useLoadingSlot" name="loading"></slot>
<u-icon color="#aaa" v-else name="photo-fill" size="45"></u-icon>
</view>
<view v-if="error && showError" class="error-wrap image">
<slot v-if="useErrorSlot" name="error"></slot>
<u-icon color="#aaa" v-else name="error-circle-fill" size="45"></u-icon>
<text class="sm">加载失败</text>
</view>
</view>
</template>
<script>
export default {
props: {
src: {
type: String
},
round: Boolean,
width: {
type: null
},
height: {
type: null
},
radius: null,
lazyLoad: {
type: Boolean,
default: true
},
useErrorSlot: Boolean,
useLoadingSlot: Boolean,
showMenuByLongpress: Boolean,
mode: {
type: String,
default: 'scaleToFill'
},
showError: {
type: Boolean,
default: true
},
showLoading: {
type: Boolean,
default: true
},
customStyle: {
type: Object,
default: () => {}
}
},
data() {
return {
error: false,
loading: true,
viewStyle: {}
}
},
created() {
this.setStyle()
},
methods: {
setStyle() {
const { width, height, radius } = this
let style = {}
if (width) {
style.width = width
}
if (height) {
style.height = height
}
if (radius) {
style['overflow'] = 'hidden'
style['border-radius'] = radius
}
this.viewStyle = style
if (this.customStyle) {
this.viewStyle = Object.assign(this.viewStyle, this.customStyle)
}
},
onLoaded(event) {
this.loading = false
this.$emit('load', event.detail)
},
onErrored(event) {
this.error = false
this.loading = true
this.$emit('error', event.detail)
},
onClick(event) {
this.$emit('click', event.detail)
}
},
watch: {
src() {
this.error = false
this.loading = true
},
width() {
this.setStyle()
},
height() {
this.setStyle()
}
}
}
</script>
<style lang="scss">
.custom-image {
position: relative;
display: block;
width: 100%;
height: 100%;
&.image-round {
overflow: hidden;
border-radius: 50%;
}
.image {
display: block;
width: 100%;
height: 100%;
}
.loading-wrap,
.error-wrap {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #969799;
font-size: 28rpx;
background-color: #f7f8fa;
}
}
</style>
+67
View File
@@ -0,0 +1,67 @@
<template>
<view class="download-nav-container row-between" v-if="showDownload" :style="{top: top + 'px'}">
<view class="sm white">
{{appConfig.download_doc}}
</view>
<view class="row">
<view class="white primary-btn row-center br60 xs" @click="go2DownLoad">
下载APP
</view>
<u-icon name="close" size="32" color="#fff" @click="showDownload = false" />
</view>
</view>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name:"download-nav",
props: {
top: {
type: Number,
default: 0
}
},
data() {
return {
showDownload: true
};
},
computed: {
...mapGetters(['appConfig'])
},
methods: {
go2DownLoad() {
// #ifdef H5
uni.getSystemInfo({
success: res => {
res.platform == 'ios' ? window.open(this.appConfig.ios_download) : window.open(this.appConfig.android_download);
},
fail: res => {
window.open(this.appConfig.android_download)
}
})
// #endif
}
}
}
</script>
<style lang="scss">
.download-nav-container {
height: 80rpx;
background-color: #333333;
padding: 0 30rpx;
position: fixed;
width: 100%;
z-index: 871;
top: 0;
.primary-btn {
background-color: $-color-primary;
height: 54rpx;
width: 154rpx;
margin-right: 30rpx;
}
}
</style>
@@ -0,0 +1,53 @@
<template>
<view class="goods-bargain-container">
<view class="row-center" style="height: 100rpx;">
<view class="bLine" />
<view class="lg bold" style="margin: 0 30rpx;">
更多砍价商品
</view>
<view class="bLine" />
</view>
<goods-list type="double" :list="lists" :isBargain="true"></goods-list>
</view>
</template>
<script>
import {getBargainList} from '@/api/activity'
import {loadingType} from '@/utils/type'
import {loadingFun} from '@/utils/tools'
export default {
data() {
return {
page: 1,
loadingStatus: loadingType.LOADING,
lists: []
}
},
mounted() {
this.$getBargainList()
},
methods: {
$getBargainList() {
let {page, loadingStatus, lists} = this;
loadingFun(getBargainList, page, lists, loadingStatus).then(res => {
if(res) {
this.page = res.page;
this.lists = res.dataList;
this.loadingStatus = res.status
}
})
}
}
}
</script>
<style lang="scss">
.goods-bargain-container {
padding-bottom: 40rpx;
.bLine {
height: 2rpx;
background-color: #CCCCCC;
width: 58rpx;
}
}
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<view class="goods-like">
<view class="title row-center">
<text class="line"></text>
<view class="row">
<image class="mr20" src="/static/images/icon_like.png"></image>
<text class="bold xxl">猜你喜欢</text>
</view>
<text class="line"></text>
</view>
<view class="goods">
<scroll-view style="height: 340rpx; white-space: nowrap;" scroll-x="true" scroll-with-animation="true"
@scroll="scrollBarChange">
<navigator v-for="(item, index) in list" :key="index" hover-class="none"
:url="'/pages/goods_details/goods_details?id=' + item.id" class="goods-item">
<custom-image width="240rpx" height="240rpx" radius="10rpx" lazy-load :src="item.image">
</custom-image>
<view class="goods-name line1 mt10">{{item.name}}</view>
<view class="price">
<price-format color="#FF2C3C" :subscript-size="22" :second-size="22" :price="item.price"
:weight="500"></price-format>
<price-format class="ml10" :show-subscript="true" color="#999" :line-through="true"
:subscript-size="22" :first-size="22" :second-size="22" :price="item.market_price">
</price-format>
</view>
</navigator>
</scroll-view>
<view class="row-center mb10 mt20" v-if="list.length > 3">
<cu-progress progressBarColor="#FF2C3C" :left="progressPer"></cu-progress>
</view>
</view>
</view>
</template>
<script>
import {
getRect
} from '@/utils/tools';
export default {
data() {
return {
progressPer: 0
};
},
components: {},
props: {
list: {
type: Array,
default: () => []
}
},
watch: {
list: {
handler: function() {
this.$nextTick(function() {
getRect(".goods-like", false, this).then(res => {
this.rectWidth = res.width;
});
})
},
immediate: true,
deep: true
}
},
methods: {
scrollBarChange(e) {
let {
progressPer
} = this;
let {
scrollLeft,
scrollWidth
} = e.detail;
progressPer = scrollLeft / (scrollWidth - this.rectWidth) * 100;
progressPer = Number(progressPer.toFixed(0))
this.progressPer = progressPer
}
}
};
</script>
<style lang="scss">
.goods-like .title {
height: 100rpx;
padding: 0 20rpx;
}
.goods-like .title .line {
width: 58rpx;
height: 1px;
background-color: #ccc;
margin: 0 22rpx;
}
.goods-like .title image {
width: 38rpx;
height: 38rpx;
}
.goods-like .title .like {
padding: 1rpx 11rpx;
background: linear-gradient(180deg, rgba(255, 44, 60, 1) 0%, rgba(249, 95, 47, 1) 100%);
border-radius: 100px 100px 100px 0px;
}
.goods-like .goods {
padding-bottom: 10rpx;
}
.goods-like .goods .goods-item {
width: 240rpx;
margin-left: 20rpx;
display: inline-block;
}
.goods-like .goods .goods-item:last-of-type {
margin-right: 20rpx;
}
</style>
+195
View File
@@ -0,0 +1,195 @@
<template>
<view class="goods-list">
<view v-if="type === 'double'" class="goods-double row-between">
<navigator v-for="(item, index) in list" :key="index" class="item bg-white mt20" hover-class="none" open-type="navigate"
:url="'/pages/goods_details/goods_details?id=' + (isBargain ? item.goods_id : item.id)">
<view class="goods-img" style="width: 347rpx;height:347rpx;">
<custom-image :lazy-load="true" width="347rpx" height="347rpx" radius="10rpx" lazy-load :src="item.image"></custom-image>
</view>
<view class="goods-info">
<view class="goods-name line2">{{item.name}}</view>
<view class="price mt10 row">
<price-format color="#FF2C3C" class="mr10" :first-size="34" :second-size="26" :subscript-size="26" :price="item.price" :weight="500"></price-format>
<price-format class="muted" :firstSize="24" :secondSize="24" :subscript-size="24" line-through :price="item.market_price || item.activity_price"></price-format>
</view>
</view>
</navigator>
</view>
<view v-if="type === 'hot'" class="goods-hot">
<navigator v-for="(item, index) in list" :key="index" class="item bg-white mt20 row" hover-class="none" open-type="navigate"
:url="'/pages/goods_details/goods_details?id=' + item.id">
<view class="goods-img" style="width: 180rpx;height:180rpx;">
<custom-image :lazy-load="true" width="180rpx" height="180rpx" radius="6rpx" lazy-load :src="item.image"></custom-image>
</view>
<view class="goods-info ml20">
<view class="goods-name line2 mb10">{{item.name}}</view>
<text class="sale br60 xxs">已有{{item.sales_sum}}人购买</text>
<view class="row-between mt10">
<view class="price mt10 row">
<price-format color="#FF2C3C" class="mr10" :first-size="34" :second-size="26" :subscript-size="26" :price="item.price" :weight="500"></price-format>
<price-format class="muted" :firstSize="24" :secondSize="24" :subscript-size="24" line-through :price="item.market_price"></price-format>
</view>
<image class="icon-md" src="/static/images/icon_go_red.png"></image>
</view>
</view>
<image class="paixu" :src="'/static/images/No.' + (index < 3 ? index : 3) + '.png'"></image>
<view class="number xxs">{{ index + 1 }}</view>
</navigator>
</view>
<view v-if="type === 'home-hot'" class="goods-home-hot goods-hot">
<navigator v-for="(item, index) in list" :key="index" class="item bg-white mb20 row" hover-class="none" open-type="navigate"
:url="'/pages/goods_details/goods_details?id=' + item.id">
<view class="goods-img">
<custom-image :lazy-load="true" width="240rpx" height="240rpx" radius="6rpx" lazy-load :src="item.image"></custom-image>
</view>
<view class="goods-info ml20 mr20">
<view class="goods-name line2 mb10">{{item.name}}</view>
<text class="sale br60 xxs">已有{{item.sales_sum}}人购买</text>
<view class="row-between mt10">
<view class="price mt10 row">
<price-format color="#FF2C3C" class="mr10" :first-size="34" :second-size="26" :subscript-size="26" :price="item.price" :weight="500"></price-format>
<price-format class="muted" :firstSize="24" :secondSize="24" :subscript-size="24" line-through :price="item.market_price"></price-format>
</view>
<button type="primary" class="br60" size="xs">去购买</button>
</view>
</view>
<image class="paixu" :src="'/static/images/No.' + (index < 3 ? index : 3) + '.png'"></image>
<view class="number">{{ index + 1 }}</view>
</navigator>
</view>
<view v-if="type === 'new'" class="goods-new">
<navigator v-for="(item, index) in list" :key="index" class="item bg-white mt20 row" hover-class="none" open-type="navigate"
:url="'/pages/goods_details/goods_details?id=' + item.id">
<view class="goods-img">
<custom-image :lazy-load="true" width="240rpx" height="240rpx" radius="10rpx" lazy-load :src="item.image"></custom-image>
</view>
<view class="goods-info ml20 mr20 flex1">
<view class="goods-name line2 mb20">{{item.name}}</view>
<view class="row-between muted xxs ">
<view class="line-through">
<text>原价</text>
<price-format :second-size="22" :first-size="22" :subscript-size="22" :price="item.market_price"></price-format>
</view>
<view>{{item.sales_sum}}人购买</view>
</view>
<view class="row-between mt10">
<price-format color="#FF2C3C" :first-size="38" :subscript-size="26" :second-size="26" :price="item.price"
:weight="500"></price-format>
<button type="primary" class="br60" size="xs">立即抢购</button>
</view>
</view>
</navigator>
</view>
<view v-if="type === 'one'" class="goods-one mt20">
<navigator v-for="(item, index) in list" :key="index" class="item bg-white row" hover-class="none" open-type="navigate"
:url="'/pages/goods_details/goods_details?id=' + item.id">
<view class="goods-img" style="width: 200rpx;height:200rpx;">
<custom-image :lazy-load="true" width="200rpx" height="200rpx" radius="6rpx" lazy-load :src="item.image"></custom-image>
</view>
<view class="goods-info ml20">
<view class="goods-name line2 mb10">{{item.name}}</view>
<view class="row-between mt10">
<view class="price mt10 row">
<price-format color="#FF2C3C" class="mr10" :first-size="34" :second-size="26" :subscript-size="26" :price="item.price" :weight="500"></price-format>
<price-format class="muted" :firstSize="24" :secondSize="24" :subscript-size="24" line-through :price="item.market_price"></price-format>
</view>
</view>
</view>
</navigator>
</view>
</view>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'double'
},
list: {
type: Array,
default: () => []
},
isBargain: {
type: Boolean,
default: false
}
},
data() {
return {
};
}
}
</script>
<style lang="scss" scoped>
.goods-list {
.goods-double {
flex-wrap: wrap;
padding: 0 20rpx;
align-items: stretch;
.item {
width: 347rpx;
border-radius: 10rpx;
.goods-info {
padding: 10rpx;
}
}
}
.goods-hot {
&.goods-home-hot {
.item {
padding: 0;
.paixu,
.number {
left: 10rpx;
}
}
}
.item {
position: relative;
padding: 30rpx 20rpx;
border-radius: 10rpx;
.goods-info {
width: 450rpx;
.sale {
padding: 4rpx 18rpx;
color: #F79C0C;
background-color: rgba(247, 156, 12, .1);
}
}
.paixu,
.number {
position: absolute;
top: 0;
left: 27rpx;
width: 50rpx;
height: 54rpx;
line-height: 60rpx;
text-align: center;
color: #621E09;
}
}
}
.goods-one .item {
padding: 20rpx;
&:not(:last-of-type) {
margin-bottom: 20rpx;
}
}
.goods-new .item {
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.16);
border-radius: 10rpx;
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<template>
<view class="group-list">
<navigator v-for="(item, index) in groupList" :key="index" class="item bg-white mt20" hover-class="none" :url="'/pages/order_details/order_details?id=' + item.order_id">
<view class="group-header row-between">
<view>
<view v-if="item.team_end_time">{{item.team_end_time}}</view>
<view v-else>
<view class="row" v-if="getTeamCountTime(item) >= 0">
<view class="sm mr10">距离结束</view>
<u-count-down :timestamp="getTeamCountTime(item)" color="#fff" bg-color="#FF2C3C" separator-color="#FF2C3C" font-size="24"
height="36" separator-size="26" @end="reflesh"></u-count-down>
</view>
</view>
</view>
<view :class="item.status == 2 ? 'muted' : 'primary'">{{getGroupStatus(item.status)}}
</view>
</view>
<view class="group-con">
<view v-if="item.type" class="team-chief xs white">团长</view>
<order-goods :team="{need: item.need}" :list="[{name: item.name, spec_value_str: item.spec_value_str,image: item.spec_image,goods_num: item.total_num, goods_id: item.goods_id, goods_price: item.goods_price}]">
</order-goods>
<view class="all-price row-end">
<text class="muted xs">{{item.total_num}}件商品总金额</text>
<price-format show-subscript :subscript-size="30" :first-size="30" :second-size="30" :price="item.order_amount"></price-format>
</view>
</view>
<view class="group-footer row" v-if="item.pay_status == 0">
<view style="flex: 1"></view>
<view>
<navigator v-if="item.pay_status == 0" hover-class="none" :url="'/pages/order_details/order_details?id=' + item.order_id">
<button size="sm" class="br60 lighter btn" type="primary" hover-class="none">
去付款
</button>
</navigator>
</view>
</view>
</navigator>
<loading-footer :status="status" :slot-empty="true" @refresh="reload">
<view slot="empty" class="column-center" style="padding-top: 200rpx">
<image class="img-null" src="/static/images/goods_null.png"></image>
<text class="lighter">暂无拼团</text>
</view>
</loading-footer>
</view>
</template>
<script>
import {
loadingType
} from '@/utils/type';
import {
loadingFun
} from '@/utils/tools';
import {
getUserGroup
} from '@/api/activity';
export default {
data() {
return {
page: 1,
groupList: [],
status: loadingType.LOADING
};
},
props: {
groupType: {
type: Number
}
},
created: function() {},
beforeMount: function() {
this.getUserGroupFun();
},
destroyed: function() { // 在组件实例被从页面节点树移除时执行
},
methods: {
reflesh() {
this.page = 1
this.groupList = []
this.status = loadingType.LOADING;
this.getUserGroupFun();
},
reload() {
this.status = loadingType.LOADING
this.getUserGroupFun();
},
async getUserGroupFun() {
let {
page,
groupType,
groupList,
status
} = this;
const data = await loadingFun(getUserGroup, page, groupList, status, {
status: groupType
})
if (!data) return
this.page = data.page
this.groupList = data.dataList
this.status = data.status
}
},
computed: {
getGroupStatus() {
return (status) => {
var text = ''
switch (status) {
case 0:
text = '拼团中';
break;
case 1:
text = '拼团成功';
break;
case 2:
text = '拼团失败';
break;
}
return text
}
},
getTeamCountTime() {
return (item) => item.count_time = Math.min(item.found_end_time, item.end_time) - Date.now()/1000
}
}
};
</script>
<style lang="scss">
.group-list {
min-height: calc(100vh - 80rpx);
padding: 0 20rpx;
overflow: hidden;
.item {
border-radius: 10rpx;
position: relative;
.group-header {
height: 80rpx;
padding: 0 24rpx;
border-bottom: 1px dotted #E5E5E5;
}
.team-chief {
position: absolute;
z-index: 100;
top: 105rpx;
padding: 4rpx 20rpx;
border-radius: 0 60rpx 60rpx 0;
background: linear-gradient(87deg, #F95F2F 0%, #FF2C3C 100%);
}
.all-price {
text-align: right;
padding: 0 24rpx 20rpx;
}
.group-footer {
height: 100rpx;
border-top: $-solid-border;
padding: 0 24rpx;
.btn {
width: 244rpx;
}
}
}
}
</style>
+218
View File
@@ -0,0 +1,218 @@
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
<template>
<view class="every-day-coupon bg-white mb20" v-if="list.length">
<view class="coupon-header row-between" @tap="goPage">
<view class="normal lg title">每日领券</view>
<view class="row">
<view class="muted sm mr5">进入领券中心</view>
<image src="/static/images/arrow_right.png" class="icon-sm"></image>
</view>
</view>
<view class="coupon-scroll">
<scroll-view style="heigth: 138rpx" scroll-x="true" scroll-with-animation="true" @scroll="scrollBarChange">
<view class="coupon-contain row">
<view v-for="(item, index) in list" :key="index" :class="'coupon-item mr20 row-between ' + (item.is_get ? 'recieve' : '')"
@tap="onRecive(item.id)">
<view class="coupon-left">
<view class="row info">
<price-format :subscript-size="30" :first-size="56" :second-size="50" :price="item.money"></price-format>
<view class="sm ml10" style="align-self: flex-end;line-height: 50rpx;margin-bottom: 3rpx">
优惠券
</view>
</view>
<view class="desc xs">{{item.use_condition}}</view>
</view>
<view class="coupon-right xxs primary column-center">立即领取</view>
<image v-if="item.is_get" class="coupon-img" src="/static/images/home_img_ receive.png"></image>
</view>
</view>
</scroll-view>
</view>
<view class="row-center mt20" v-if="list.length > 2">
<cu-progress progressBarColor="#FF2C3C" :left="progressPer"></cu-progress>
</view>
</view>
</template>
<script>
import {
toLogin
} from '@/utils/login';
import {
getCoupon
} from '@/api/user';
import {
getRect
} from '@/utils/tools'
export default {
data() {
return {
progressPer: 0
};
},
components: {},
props: {
list: {
type: Array,
default: () => []
}
},
watch: {
list: {
handler() {
this.$nextTick(function(){
getRect(".coupon-scroll", false, this).then(res => {
this.rectWidth = res.width;
});
})
},
immediate: true,
deep: true
}
},
methods: {
scrollBarChange(e) {
let {
progressPer
} = this;
let {
scrollLeft,
scrollWidth
} = e.detail;
progressPer = scrollLeft / (scrollWidth - this.rectWidth) * 100;
this.progressPer = Number(progressPer.toFixed(0));
},
onRecive(id) {
if (!this.isLogin) {
toLogin();
return;
}
getCoupon(id).then(res => {
if (res.code == 1) {
this.$toast({
title: res.msg
})
this.$emit('reflash');
}
});
},
goPage() {
if (!this.isLogin) {
toLogin();
return;
}
uni.navigateTo({
url: '/pages/user_getcoupon/user_getcoupon'
});
}
}
};
</script>
<style lang="scss">
.every-day-coupon {
background-image: url(../../static/images/home_bg_coupon.png);
background-size: 100% 100%;
background-repeat: no-repeat;
height: 264rpx;
width: 710rpx;
margin: 0 20rpx;
padding: 0 20rpx;
border-radius: 14rpx;
box-sizing: border-box;
.coupon-header {
height: 90rpx;
.title {
margin-left: 20rpx;
font-weight: bold;
}
}
.coupon-contain {
.coupon-item {
height: 134rpx;
width: 326rpx;
flex: none;
position: relative;
background: url(../../static/images/home_bg_coupon_red.png);
background-size: 100% 100%;
&.recieve {
background-image: url(../../static/images/home_bg_coupon_gray.png);
.coupon-right {
background-color: #E5E5E5;
color: $-color-muted;
}
}
.coupon-left {
width: 269rpx;
padding-left: 20rpx;
color: white;
.info {
height: 70rpx;
border-bottom: 3rpx dashed #fff;
.money-logo {
font-weight: bold;
align-self: flex-end;
line-height: 50rpx;
margin-right: 6rpx;
margin-bottom: 4rpx
}
}
.desc {
margin-top: 10rpx;
}
}
.coupon-right {
width: 57rpx;
height: 100%;
background-color: #FFE5E6;
border-radius: 0rpx 14rpx 14rpx 0rpx;
writing-mode: tb;
}
.coupon-img {
height: 77rpx;
width: 99rpx;
position: absolute;
top: 0;
right: 73rpx;
}
}
}
}
</style>
@@ -0,0 +1,84 @@
<template>
<view>
<l-painter :css="`width: 600rpx;height: 960rpx;`" isCanvasToTempFilePath
@success="handleSuccess" custom-style="position: fixed; left: 200%">
<l-painter-image :src="config.poster"
css="position:absolute; width: 600rpx;height: 960rpx;object-fit: cover; border-radius: 20rpx;" />
<l-painter-view
css="position:absolute;background-color: #ffffff;width:100%;height: 240rpx;border-radius: 14rpx; bottom: 0;left: 0;padding-top: 30rpx;">
<l-painter-view css="width: 330rpx; display: inline-block;padding-left: 30rpx;">
<l-painter-view>
<l-painter-image :src="config.avatar"
css="width: 80rpx; height: 80rpx; border-radius: 50%;" />
<l-painter-text :text="config.nickname"
css="margin-top: 10rpx; margin-left: 20rpx; color: #333333; font-size: 34rpx;line-clamp:1;font-weight: bold;width:220rpx;" />
</l-painter-view>
<l-painter-view css="margin-top: 30rpx;line-height: 34rpx;">
<l-painter-text text="邀请你一起来赚大钱" css="color: #333333; font-size: 28rpx;" />
</l-painter-view>
<l-painter-view css="margin-top: 20rpx;">
<l-painter-text :text="`邀请码:${config.code}`" css="color: #FF2C3C; font-size: 28rpx;" />
</l-painter-view>
</l-painter-view>
<l-painter-view css="display: inline-block;margin-top:10rpx;">
<l-painter-view css="padding-left: 30rpx;">
<!-- #ifdef H5 || APP-PLUS -->
<l-painter-qrcode css="width: 180rpx; height: 180rpx;" :text="config.link">
</l-painter-qrcode>
<!-- #endif -->
<!-- #ifdef MP -->
<l-painter-image :src="config.qrCode" css="width: 180rpx; height: 180rpx;" />
<!-- #endif -->
</l-painter-view>
</l-painter-view>
</l-painter-view>
</l-painter>
</view>
</template>
<script>
import lPainter from '@/components/lime-painter/components/l-painter/l-painter.vue'
import lPainterImage from '@/components/lime-painter/components/l-painter-image/l-painter-image.vue'
import lPainterText from '@/components/lime-painter/components/l-painter-text/l-painter-text.vue'
import lPainterView from '@/components/lime-painter/components/l-painter-view/l-painter-view.vue'
import lPainterQrcode from '@/components/lime-painter/components/l-painter-qrcode/l-painter-qrcode.vue'
export default {
name: "share-poster",
components: {
lPainter,
lPainterImage,
lPainterText,
lPainterView,
lPainterQrcode
},
props: {
config: {
type: Object,
default: () => ({})
},
goodsId: {
type: [Number, String],
default: ''
},
qrcode: {
type: [String],
default: ''
}
},
data() {
return {
};
},
methods: {
handleSuccess(val) {
this.$emit('success', val)
}
}
}
</script>
<style>
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<view class="root" :style="{width,height}">
<image :style="{width,height}" class="posterImg" :src="posterUrl" mode="aspectFit"></image>
<view :style="{width,height}" @click="state=!state" class="box">
<image class="playIcon" src="/static/images/icon_play.png" mode="widthFix"></image>
</view>
<video :id="videoId" :style="{height,width:state?'750rpx':'1rpx'}" @pause="state=0" @timeupdate="timeupdate" @fullscreenchange="fullscreenchange" class="video" :src="url"></video>
<!-- <progress :style="{'top':height,width}" class="progress" :percent="currentTime?parseInt(currentTime/duration*100):0" show-info border-radius="5" active></progress> -->
</view>
</template>
<script>
export default {
computed:{
posterUrl(){
if(this.poster) return this.poster
return this.url + '?x-oss-process=video/snapshot,t_'+(parseInt(this.currentTime*1000))+',f_jpg,w_800,m_fast'
}
},
created() {
this.videoId = Date.now() + Math.ceil(Math.random()*10000000)+"";
},
mounted() {
this.VideoContext = uni.createVideoContext(this.videoId)
},
methods:{
fullscreenchange(e){
this.state = e.detail.fullScreen
},
timeupdate(e){
this.duration = e.detail.duration
this.currentTime = e.detail.currentTime
}
},
watch: {
state(state, oldValue) {
//console.log(state,'state');
if(!state){
this.VideoContext.pause()
}else{
this.VideoContext.play()
setTimeout(()=>{
this.VideoContext.requestFullScreen({direction:this.direction})
},10)
}
}
},
data() {
return {
VideoContext:{},
state:false,
currentTime:0,
duration:0,
videoId:''
};
},
props: {
poster: {
type: [String,Boolean],
default(){
return ''
}
},
url: {
type: String,
default(){
return ''
}
},
direction: {
type: Number,
default(){
return 0
}
},
width: {
type: String,
default(){
return "750rpx";
}
},
height: {
type: String,
default(){
return "450rpx";
}
}
},
}
</script>
<style lang="scss" scoped>
.root{
position:relative;
width: 750rpx;
height: 300px;
overflow: hidden;
}
.posterImg,.video,.box{
display: flex;
width: 750rpx;
height: 300px;
//border: solid 1px red;absolute
position:absolute;
}
.video{
margin-left: -2000px;
}
.posterImg{
//border: solid red 1px;
}
.box{
justify-content: center;
align-items: center;
}
.playIcon{
width: 100rpx;
}
</style>
+54
View File
@@ -0,0 +1,54 @@
## 1.8.5.52021-08-17
- chore: 更新文档,删除 replace
- fix: 修复 text 值为 number时报错
## 1.8.5.42021-08-16
- fix: 字节小程序兼容
## 1.8.5.32021-08-15
- fix: 修复线性渐变与css现实效果不一致的问题
- chore: 更新文档
## 1.8.5.22021-08-13
- chore: 增加`background-image``background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印
- 注意:这个功能H5暂时无法使用,因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!!
## 1.8.5.12021-08-10
- fix: 修复因`margin`报错问题
## 1.8.52021-08-09
- chore: 增加margin支持`auto`,以达到居中效果
## 1.8.42021-08-06
- chore: 增加判断缓存文件条件
- fix: 修复css 多余空格报错问题
## 1.8.32021-08-04
- tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位:position: 'absolute'
- fix: 修复只有一个view子元素时不计算高度的问题
## 1.8.22021-08-03
- fix: 修复 path-type 为 `url` 无效问题
- fix: 修复 qrcode `text` 为空时报错问题
- fix: 修复 image `src` 动态设置时不生效问题
- feat: 增加 css 属性 `min-width` `max-width`
## 1.8.12021-08-02
- fix: 修复无法加载本地图片
## 1.8.02021-08-02
- chore 文档更新
- 使用旧版的同学不要升级!
## 1.8.0-beta2021-07-30
- ## 全新布局方式 不兼容旧版!
- chore: 布局方式变更
- tips: 微信canvas 2d 不支持真机调试
## 1.6.62021-07-09
- chore: 统一命名规范,无须主动引入组件
## 1.6.52021-06-08
- chore: 去掉console
## 1.6.42021-06-07
- fix: 修复 数字 为纯字符串时不转换的BUG
## 1.6.32021-06-06
- fix: 修复 PC 端放大的BUG
## 1.6.22021-05-31
- fix: 修复 报`adaptor is not a function`错误
- fix: 修复 text 多行高度
- fix: 优化 默认文字的基准线
- feat: `@progress`事件,监听绘制进度
## 1.6.12021-02-28
- 删除多余节点
## 1.6.02021-02-26
- 调整为uni_modules目录规范
- 修复:transform的rotate不能为负数问题
- 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64``url`
@@ -0,0 +1,141 @@
import {base64ToPath} from '../l-painter/utils.js'
const styles = (v = '') => v.split(';').filter(v => v && !/^[\n\s]+$/.test(v)).map(v => {
const item = v.split(':');
return {
[item[0]
.replace(/-([a-z])/g, function() {
return arguments[1].toUpperCase()
})
.replace(/\s+/g, '')
]: item?. [1]?.replace(/^\s+/, '')?.replace(/\s+$/, '') || ''
}
})
export function parent(parent) {
return {
provide() {
return {
[parent]: this
}
},
data() {
return {
el: {
css: {},
views: []
},
}
},
watch: {
css: {
handler(v) {
if (this.canvasId) {
this.el.css = typeof v == 'object' ? v : v && Object.assign(...styles(v)) || {}
this.canvasWidth = this.el.css?.width || this.canvasWidth
this.canvasHeight = this.el.css?.height || this.canvasHeight
}
},
immediate: true
}
}
}
}
export function children(parent, options = {}) {
const indexKey = options.indexKey || 'index'
return {
inject: {
[parent]: {
default: null
}
},
watch: {
el: {
handler(v, o) {
if (JSON.stringify(v) != JSON.stringify(o))
this.bindRelation()
},
deep: true,
immediate: true
},
src: {
handler(v, o) {
if (v != o)
this.bindRelation()
},
immediate: true
},
text: {
handler(v, o) {
if (v != o) this.bindRelation()
},
immediate: true
},
css: {
handler(v, o) {
if (v != o)
this.el.css = typeof v == 'object' ? v : v && Object.assign(...styles(v)) || {}
},
immediate: true
},
replace: {
handler(v, o) {
if (JSON.stringify(v) != JSON.stringify(o))
this.bindRelation()
},
deep: true,
immediate: true
},
},
created() {
Object.defineProperty(this, 'parent', {
get: () => {
return this[parent]
},
})
Object.defineProperty(this, 'index', {
get: () => {
this.bindRelation();
return this.parent?.el.views?.indexOf(this.el)
},
});
this.el.type = this.type
},
beforeDestroy() {
if (this.parent) {
this.parent.el.views = this.parent.el.views.filter(
(item) => item._uid !== this._uid
);
}
},
methods: {
bindRelation() {
if (!this.el._uid) {
this.el._uid = this._uid
}
if (['text', 'qrcode'].includes(this.type)) {
this.el.text = this.$slots?.default?. [0]?.text || this.text?.replace(/\\n/g, '\n')
}
if (this.type == 'text' && this.replace) {
this.el.replace = this.replace
}
if (this.type == 'image') {
this.el.src = this.src
}
// || this.parent.el.views.indexOf(this.el) !== -1
if (!this.parent) {
return;
}
let views = this.parent.el.views || [];
if (views.indexOf(this.el) !== -1) {
this.parent.el.views = views.map(v => v._uid == this._uid ? this.el : v)
} else {
this.parent.el.views = [...views, this.el];
}
}
},
mounted() {
this.bindRelation()
},
}
}
@@ -0,0 +1,27 @@
<template>
<view></view>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-image',
mixins:[children('painter')],
props: {
css: [String, Object],
src: String
},
data() {
return {
type: 'image',
el: {
css: {},
src: null
},
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,27 @@
<template>
<view></view>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-qrcode',
mixins:[children('painter')],
props: {
css: [String, Object],
text: String
},
data() {
return {
type: 'qrcode',
el: {
css: {},
text: null
},
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,28 @@
<template>
<text style="opacity: 0;"><slot/></text>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-text',
mixins:[children('painter')],
props: {
css: [String, Object],
text: [String, Number],
replace: Object,
},
data() {
return {
type: 'text',
el: {
css: {},
text: null
},
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,26 @@
<template>
<view><slot/></view>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-view',
mixins:[children('painter'), parent('painter')],
props: {
css: [String, Object],
},
data() {
return {
type: 'view',
el: {
css: {},
views:[]
},
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,523 @@
<template>
<view v-if="canvasId && size" class="lime-painter" :style="size + customStyle">
<!-- #ifndef APP-NVUE -->
<canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
<canvas class="lime-painter__canvas" v-else :canvas-id="canvasId" :style="size" :id="canvasId" :width="boardWidth * dpr" :height="boardHeight * dpr"></canvas>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view
:style="size"
ref="webview"
class="lime-painter__canvas"
@pagefinish="onPageFinish"
@error="onError"
@onPostMessage="onMessage"
></web-view>
<!-- #endif -->
<slot/>
</view>
</template>
<script>
import { toPx, compareVersion, sleep, base64ToPath, pathToBase64, getImageInfo, isBase64 } from './utils';
import {parent} from '../common/relation'
// #ifndef APP-NVUE
import {Painter} from './painter'
// #endif
// #ifdef APP-NVUE
import painterScript from './nvue'
// #endif
export default {
name: 'lime-painter',
mixins:[parent('painter')],
props: {
board: Object,
pathType: {
type: String,
// default: 'url'
// 'base64'、'url'
},
fileType: {
type: String,
default: 'png'
},
quality: {
type: Number,
default: 1
},
css: [String, Object],
width: [Number, String],
height: [Number, String],
pixelRatio: Number,
customStyle: String,
isCanvasToTempFilePath: Boolean,
sleep: {
type: Number,
default: 1000 / 30
},
beforeDelay: {
type: Number,
default: 100
},
afterDelay: {
type: Number,
default: 100
},
// #ifdef MP-WEIXIN || MP-TOUTIAO||MP-ALIPAY
type: {
type: String,
default: '2d'
},
// #endif
},
data() {
return {
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: true,
// #endif
// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: false,
// #endif
canvasHeight: 150,
canvasWidth: null,
isDrawIng: false,
isPC: false,
inited: false,
name: 'view',
progress: 0,
// #ifdef APP-NVUE
tempFilePath: [],
// #endif
};
},
computed: {
canvasId() {
return `l-painter${this._uid}`
},
size() {
if(this.boardWidth && this.boardHeight) {
return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
}
},
dpr() {
return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
},
boardWidth() {
const {width = 0, canvasWidth = 0} = this
const {width: boardWidth = 0} = this.board?.css || this.board || {}
return Math.max(toPx(width || boardWidth), toPx(canvasWidth));
},
boardHeight() {
const {height= 0, canvasHeight =0} = this
const {height: boardHeight = 0} = this.board?.css || this.board || {}
return Math.max(toPx(height || boardHeight), toPx(canvasHeight));
}
},
watch: {
canvasWidth(v) {
if(this.el.css && !this.el.css?.width) {
this.el.css.width = v
}
},
size(v) {
// #ifdef MP-WEIXIN
if (this.use2dCanvas) {
this.inited = false;
}
// #endif
// #ifdef MP-ALIPAY
this.inited = false;
// #endif
},
},
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
created() {
const { SDKVersion, version, platform, environment } = uni.getSystemInfoSync();
// #ifdef MP-WEIXIN
// ios wx7.0.20 createImage bug
this.isPC = /windows/i.test(platform)
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) && !this.isPC;
// #endif
// #ifdef MP-TOUTIAO
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
// #endif
// #ifdef MP-TOUTIAO
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.8.0') >= 0;
// #endif
},
// #endif
mounted() {
// #ifdef APP-NVUE
this.webViewInit()
// #endif
this.$nextTick(() => {
setTimeout(() => {
if(this.board) {
this.$watch('board', this.watchRender, {deep: true,immediate: true});
} else if(this.el.views.length) {
this.$watch('el', this.watchRender, {deep: true,immediate: true});
}
},30)
})
},
methods: {
async watchRender(val) {
this.progress = 0
if (JSON.stringify(val) === '{}' || !val) return;
clearTimeout(this.rendertimer)
this.rendertimer = setTimeout(() => {
this.render(val);
}, this.beforeDelay)
},
async setFilePath(path, isEmit) {
let filePath = path
const {pathType} = this
if(pathType == 'base64' && !isBase64(path)) {
filePath = await pathToBase64(path)
} else if(pathType == 'url' && isBase64(path)) {
filePath = await base64ToPath(path)
}
if(isEmit) {
this.$emit('success', filePath);
}
return filePath
},
// #ifdef APP-NVUE
onError(e) {
console.log('onError', e)
},
// onPagestart() {
// console.log('onPagestart')
// },
// onPageFinish() {
// this.$refs.webview.evalJS(`init()`)
// },
// onReceivedTitle() {
// console.log('onReceivedTitle')
// },
onMessage(e) {
const res = e?.detail?.data[0] || null;
if (res?.event) {
if(res.event == 'inited') {
this.inited = true
}
if(res.event == 'layoutChange') {
const data = JSON.parse(res.data)
this.canvasWidth = data.width;
this.canvasHeight = data.height;
}
if(res.event == 'progressChange') {
this.progress = res.data * 1
}
if(res.event == 'file') {
this.tempFilePath.push(res.data)
if(this.tempFilePath.length > 7) {
this.tempFilePath.shift()
}
return
}
if(res.event == 'success') {
if(res.data) {
this.tempFilePath.push(res.data)
if(this.tempFilePath.length > 8) {
this.tempFilePath.shift()
}
if(this.isCanvasToTempFilePath) {
this.setFilePath(this.tempFilePath.join(''), true)
}
} else {
this.$emit('fail')
}
return
}
this.$emit(res.event, JSON.parse(res.data));
} else if (res?.file) {
this.file = res.data;
} else {
console.error(res);
}
},
async webViewInit() {
await sleep(30)
// await this.getWebViewInited()
const webview = this.$refs.webview;
webview.evalJS(painterScript)
},
getWebViewInited() {
if(this.inited) return Promise.resolve(this.inited);
return new Promise((resolve) => {
this.$watch(
'inited',
async val => {if(val) {resolve(val)}},
{immediate: true}
);
})
},
getTempFilePath() {
if(this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
return new Promise((resolve) => {
this.$watch(
'tempFilePath',
async val => {if(val.length == 8) {resolve(val.join(''))}}
);
})
},
getWebViewDone() {
if(this.progress == 1) return Promise.resolve(this.progress);
return new Promise((resolve) => {
this.$watch(
'progress',
async val => {
if(val == 1) {
this.$emit('done')
resolve(val)
}},
{immediate: true}
);
})
},
async render(args) {
const newNode = await this.calcImage(args);
await this.getWebViewInited()
const webview = this.$refs.webview;
webview.evalJS(`source(${JSON.stringify(newNode)})`)
if(this.isCanvasToTempFilePath) {
await this.getWebViewDone()
await sleep(this.afterDelay)
const params = {fileType: this.fileType, quality: this.quality}
webview.evalJS(`save(${JSON.stringify(params)})`)
}
},
async calcImage(args) {
let node = JSON.parse(JSON.stringify(args))
const url = node.url || node.src
if(node.type === "image" && url && !isBase64(url)) {
const suffix = url.match(/\.(\w+)$/)[1]
const {width = 0, height = 0, path, naturalSrc} = await getImageInfo(url)
const src = await pathToBase64(path)
node.src = src.replace(/^data:application[\w\/]+;base64/,'data:image/'+suffix+';base64')
} else if(node.views?.length) {
for (let i = 0; i < node.views.length; i++) {
node.views[i] = await this.calcImage(node.views[i])
}
}
return node
},
async canvasToTempFilePath(args = {}){
this.tempFilePath = []
this.$refs.webview.evalJS(`save(${JSON.stringify(args)})`)
try{
let tempFilePath = await this.getTempFilePath()
tempFilePath = await this.setFilePath(tempFilePath)
args.success({errMsg: "canvasToTempFilePath:ok", tempFilePath})
}catch(e){
args.fail({error: e})
}
}
// #endif
// #ifndef APP-NVUE
getParentWeith() {
uni.createSelectorQuery()
.in(this)
.select(`.lime-painter`)
.boundingClientRect()
.exec(res => {
this.canvasWidth = Math.ceil(res[0].width)
this.canvasHeight = res[0].height
})
},
async update(args, single) {
this.painter = null;
// #ifdef MP-WEIXIN
if (this.use2dCanvas) {
this.ctx = null;
this.inited = false;
}
// #endif
// #ifdef MP-ALIPAY
this.inited = false;
// #endif
this.isDrawIng = false;
await new Promise(resolve => this.$nextTick(resolve));
await sleep(200);
return this.render(args, single);
},
async render(args = {}, single = false) {
if (this.isDrawIng) {
return this.update(args, single);
}
this.isDrawIng = true;
const isArg = JSON.stringify(args) != '{}';
const ctx = await this.getContext();
let { use2dCanvas, boardWidth, boardHeight, canvas, afterDelay } = this;
if (use2dCanvas && !canvas) {
return Promise.reject(new Error('render: fail canvas has not been created'));
}
this.boundary = {
top: 0,
left: 0,
width: boardWidth,
height: boardHeight
};
// if (!single) {
// ctx.clearRect(0, 0, boardWidth, boardHeight);
// }
if(!this.painter) {
this.painter = new Painter({context: ctx, canvas, width: boardWidth, height: boardHeight, pixelRatio: this.dpr}, this)
}
this.painter.listen('progressChange', (v) => {
this.$emit('progress', v)
})
const {width, height} = await this.painter.source(args)
this.canvasHeight = toPx(this.el.css?.height) || height
this.canvasWidth = toPx(this.el.css?.width) || width
this.boundary.height = this.canvasHeight
this.boundary.width = this.canvasWidth
await sleep(this.sleep);
await this.painter.render()
await new Promise(resolve => this.$nextTick(resolve));
if (!use2dCanvas && !single) {
await this.canvasDraw();
}
if (afterDelay && use2dCanvas) {
await sleep(afterDelay);
}
this.$emit('done');
if (this.isCanvasToTempFilePath && !single && this.isDrawIng) {
this.canvasToTempFilePath()
.then(async res => {
this.$emit('success', res.tempFilePath)
})
.catch(err => {
this.$emit('fail', new Error(JSON.stringify(err)));
});
}
this.isDrawIng = false;
return Promise.resolve({ ctx, draw: this.painter, node: this.node });
},
async custom(cb) {
const { ctx, draw } = await this.render({}, true);
ctx.save();
await cb(ctx, draw);
ctx.restore();
return Promise.resolve(true);
},
async single(args = {}) {
const res = await this.render(args, true);
return Promise.resolve(res);
},
canvasDraw(flag = false) {
return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this.afterDelay)));
},
async getContext() {
if (this.ctx && this.inited) {
return Promise.resolve(this.ctx);
}
this.getParentWeith()
const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
const _getContext = () => {
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.boundingClientRect()
.exec(res => {
if (res) {
const ctx = uni.createCanvasContext(this.canvasId, this);
if (!this.inited) {
this.inited = true;
this.use2dCanvas = false;
this.canvas = res;
}
if(this.isPC) {
ctx.scale(1/dpr, 1/dpr);
}
// #ifdef MP-ALIPAY
ctx.scale(dpr, dpr);
// #endif
this.ctx = ctx
resolve(this.ctx);
}
});
});
};
// #ifndef MP-WEIXIN
return _getContext();
// #endif
if (!use2dCanvas) {
return _getContext();
}
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.node()
.exec(res => {
let { node: canvas } = res[0];
if (!canvas) {
this.use2dCanvas = false;
resolve(this.getContext());
}
const ctx = canvas.getContext(type);
if (!this.inited) {
this.inited = true;
this.use2dCanvas = true;
this.canvas = canvas;
}
this.ctx = ctx
resolve(this.ctx);
});
});
},
canvasToTempFilePath(args = {}) {
const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
return new Promise((resolve, reject) => {
let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
let destWidth = width * dpr;
let destHeight = height * dpr;
// #ifdef MP-ALIPAY
width = destWidth;
height = destHeight;
// #endif
const success = async (res) => {
const tempFilePath = await this.setFilePath(res.tempFilePath)
resolve(Object.assign(res, {tempFilePath}))
}
const copyArgs = Object.assign({
x,
y,
width,
height,
destWidth,
destHeight,
canvasId,
fileType,
quality,
success,
fail: reject
}, args);
if (use2dCanvas) {
delete copyArgs.canvasId;
copyArgs.canvas = this.canvas;
}
uni.canvasToTempFilePath(copyArgs, this);
});
}
// #endif
}
};
</script>
<style>
.lime-painter, .lime-painter__canvas {
// #ifndef APP-NVUE
width: 100%;
// #endif
// #ifdef APP-NVUE
flex: 1;
// #endif
}
</style>
@@ -0,0 +1,103 @@
const painterContent = `
var cache = [];
var painter = null;
var canvas = null;
var context = null;
var timer = null;
var pixelRatio = 1;
console.log = function(...args) {
postMessage(args);
};
function stringify(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
return;
}
cache.push(value);
}
return value;
};
function emit(event, data) {
let dataStr = typeof data !== 'object' && data !== null ? data : JSON.stringify(data, stringify);
postMessage({
event,
data: dataStr
});
cache = [];
};
function postMessage(data) {
uni.postMessage({
data
});
};
function init() {
canvas = document.querySelector('#lime-painter');
context = canvas.getContext('2d');
pixelRatio = window.devicePixelRatio;
painter = new Painter({
id: 'lime-painter',
context,
canvas,
pixelRatio,
width: canvas.offsetWidth,
height: canvas.offsetHeight
});
emit('inited', true);
painter.listen('progressChange', (v) => {
emit('progressChange', v);
});
};
function save(args) {
delete args.success;
delete args.fail;
clearTimeout(timer);
timer = setTimeout(() => {
const path = painter.save(args);
if(typeof path == 'string') {
const index = Math.ceil(path.length / 8);
for (var i = 0; i < 8; i++) {
if(i == 7) {
emit('success', path.substr(i * index, index));
} else {
emit('file', path.substr(i * index, index));
}
};
} else {
emit('fail', '');
};
}, 30);
};
async function source(args) {
let res = await painter.source(args);
emit('layoutChange', res);
await painter.render();
};
`
export default `
document.write("<canvas id='lime-painter'>不支持cavnas</canvas>");
let meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0';
document.head.appendChild(meta);
let styleEl = document.createElement('style');
styleEl.setAttribute('type', 'text/css');
styleEl.textContent='html,body,#lime-painter{padding: 0; margin: 0; width:100%;height:100%}';
document.head.appendChild(styleEl);
var script = document.createElement("script");
script.language = "javascript";
script.src = "https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js";
script.onload = function() {
var script = document.createElement("script");
script.language = "javascript";
script.src = "https://cdn.jsdelivr.net/gh/liangei/image@latest/lime-ui/lime-painter/painter.js";
script.onload = function() {init()};
document.head.appendChild(script);
};
document.head.appendChild(script);
var script = document.createElement("script");
script.language = "javascript";
script.text = "${painterContent}";
document.body.appendChild(script);
`
File diff suppressed because one or more lines are too long
@@ -0,0 +1,414 @@
const networkReg = /^(http|\/\/)/;
export const isBase64 = (path) => /^data:image\/(\w+);base64/.test(path);
export function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
const isDev = ['devtools'].includes(uni.getSystemInfoSync().platform)
// 缓存图片
let cache = {}
export function isNumber(value) {
return /^-?\d+(\.\d+)?$/.test(value);
}
export function toPx(value, baseSize, isDecimal = false) {
// 如果是数字
if (typeof value === 'number') {
return value
}
// 如果是字符串数字
if (isNumber(value)) {
return value * 1
}
// 如果有单位
if (typeof value === 'string') {
const reg = /^-?([0-9]+)?([.]{1}[0-9]+){0,1}(em|rpx|px|%)$/g
const results = reg.exec(value);
if (!value || !results) {
return 0;
}
const unit = results[3];
value = parseFloat(value);
let res = 0;
if (unit === 'rpx') {
res = uni.upx2px(value);
} else if (unit === 'px') {
res = value * 1;
} else if (unit === '%') {
res = value * toPx(baseSize) / 100;
} else if (unit === 'em') {
res =value * toPx(baseSize || 14);
}
return isDecimal ? res.toFixed(2) * 1 : Math.round(res);
}
return 0
}
// 计算版本
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
// #ifdef MP
export const prefix = () => {
// #ifdef MP-TOUTIAO
return tt
// #endif
// #ifdef MP-WEIXIN
return wx
// #endif
// #ifdef MP-BAIDU
return swan
// #endif
// #ifdef MP-ALIPAY
return my
// #endif
// #ifdef MP-QQ
return qq
// #endif
// #ifdef MP-360
return qh
// #endif
}
// #endif
const base64ToArrayBuffer = (data) => {
// #ifndef MP-WEIXIN || APP-PLUS
/**
* Base64Binary.decode(base64_string);
* Base64Binary.decodeArrayBuffer(base64_string);
*/
const Base64Binary = {
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
/* will return a Uint8Array type */
decodeArrayBuffer(input) {
const bytes = (input.length/4) * 3;
const ab = new ArrayBuffer(bytes);
this.decode(input, ab);
return ab;
},
removePaddingChars(input) {
const lkey = this._keyStr.indexOf(input.charAt(input.length - 1));
if(lkey == 64){
return input.substring(0,input.length - 1);
}
return input;
},
decode(input, arrayBuffer) {
//get last chars to see if are valid
input = this.removePaddingChars(input);
input = this.removePaddingChars(input);
const bytes = parseInt((input.length / 4) * 3, 10);
let uarray;
let chr1, chr2, chr3;
let enc1, enc2, enc3, enc4;
let i = 0;
let j = 0;
if (arrayBuffer)
uarray = new Uint8Array(arrayBuffer);
else
uarray = new Uint8Array(bytes);
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
for (i=0; i<bytes; i+=3) {
//get the 3 octects in 4 ascii chars
enc1 = this._keyStr.indexOf(input.charAt(j++));
enc2 = this._keyStr.indexOf(input.charAt(j++));
enc3 = this._keyStr.indexOf(input.charAt(j++));
enc4 = this._keyStr.indexOf(input.charAt(j++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
uarray[i] = chr1;
if (enc3 != 64) uarray[i+1] = chr2;
if (enc4 != 64) uarray[i+2] = chr3;
}
return uarray;
}
}
return Base64Binary.decodeArrayBuffer(data)
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
return uni.base64ToArrayBuffer(data)
// #endif
}
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
return new Promise((resolve, reject) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!format) {
console.error('ERROR_BASE64SRC_PARSE')
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
let pre = prefix()
const filePath = `${pre.env.USER_DATA_PATH}/${time}.${format}`
//let buffer = base64ToArrayBuffer(bodyData)
console.log(filePath)
fs.writeFile({
filePath,
data: base64.replace(/^data:\S+\/\S+;base64,/, ''),
encoding: 'base64',
// data: buffer,
// encoding: 'binary',
success() {
resolve(filePath)
},
fail(err) {
console.log(base64,'!!!!!!')
console.error('获取base64图片失败', JSON.stringify(err))
reject(err)
}
})
// #endif
// #ifdef H5
// mime类型
let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
//base64 解码
let byteString = atob(base64.split(',')[1]);
//创建缓冲数组
let arrayBuffer = new ArrayBuffer(byteString.length);
//创建视图
let intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
intArray[i] = byteString.charCodeAt(i);
}
resolve(URL.createObjectURL(new Blob([intArray], { type: mimeString })))
// #endif
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
}, (error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
// #endif
})
}
/**
* 路径转base64
* @param {Object} string
*/
export function pathToBase64(path) {
if(/^data:/.test(path)) return path
return new Promise((resolve, reject) => {
// #ifdef H5
const _canvas = ()=> {
let image = new Image();
image.setAttribute("crossOrigin",'Anonymous');
image.onload = function() {
let canvas = document.createElement('canvas');
// 获取图片原始宽高
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
// 将图片插入画布并开始绘制
canvas.getContext('2d').drawImage(image, 0, 0);
let result = canvas.toDataURL('image/png')
resolve(result);
canvas.height = canvas.width = 0
}
image.src = path
image.onerror = (error) => {
console.error(`urlToBase64 error: ${path}`, JSON.stringify(error))
reject(new Error('urlToBase64 error'));
};
}
const _fileReader = (blob) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
resolve(e.target.result);
};
fileReader.readAsDataURL(blob);
fileReader.onerror = (error) => {
console.error('blobToBase64 error:', JSON.stringify(error))
reject(new Error('blobToBase64 error'));
};
}
const isFileReader = typeof FileReader === 'function'
if(networkReg.test(path) && isFileReader ) {
window.URL = window.URL || window.webkitURL;
const xhr = new XMLHttpRequest();
xhr.open("get", path, true);
xhr.timeout = 2000;
xhr.responseType = "blob";
xhr.onload = function() {
if(this.status == 200) {
_fileReader(this.response)
} else {
_canvas()
}
}
xhr.onreadystatechange = function() {
if(this.status === 0) {
console.error('图片跨域了,得后端处理咯')
}
}
xhr.send();
} else if(/^blob/.test(path) && isFileReader){
_fileReader(path)
} else {
_canvas()
}
// #endif
// #ifdef MP
if(uni.canIUse('getFileSystemManager')) {
uni.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: (res) => {
resolve('data:image/png;base64,' + res.data)
},
fail: (error) => {
console.error('urlToBase64 error:', JSON.stringify(error))
reject(error)
}
})
}
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.onload = (data) => { resolve(data.target.result)}
fileReader.onerror = (error) => {
console.error('pathToBase64 error:', JSON.stringify(error))
reject(error)
}
fileReader.readAsDataURL(file)
}, (error) => {
console.error('pathToBase64 error:', JSON.stringify(error))
reject(error)
})
}, (error) => {
console.error('pathToBase64 error:', JSON.stringify(error))
reject(error)
})
// #endif
})
}
// #ifdef APP-PLUS
const getLocalFilePath = (path)=> {
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
return path
}
if (path.indexOf('file://') === 0) {
return path
}
if (path.indexOf('/storage/emulated/0/') === 0) {
return path
}
if (path.indexOf('/') === 0) {
const localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substr(1)
}
}
return '_www/' + path
}
// #endif
export function getImageInfo(img, isH5PathToBase64 = false) {
return new Promise(async (resolve, reject) => {
// const base64Reg = /^data:image\/(\w+);base64/
const localReg = /^\.|^\/(?=[^\/])/;
// #ifdef H5
if(networkReg.test(img) && isH5PathToBase64) {
img = await pathToBase64(img)
}
// #endif
// #ifndef MP-ALIPAY
if(isBase64(img)) {
if(isDev || !cache[img]) {
const imgName = img
img = await base64ToPath(img)
cache[imgName] = img
} else {
img = cache[img]
}
}
// #endif
if(cache[img] && cache[img].errMsg) {
resolve(cache[img])
} else {
uni.getImageInfo({
src: img,
success: (image) => {
// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
image.path = localReg.test(img) ? `/${image.path}` : image.path;
// #endif
// #ifdef H5
image.path = image.path.replace(/^\./, window.location.origin)
// #endif
image.naturalSrc = img
if(isDev) {
resolve(image)
} else {
cache[img] = image
resolve(cache[img])
}
},
fail(err) {
resolve({path: img})
console.error(`getImageInfo:fail ${img} failed ${JSON.stringify(err)}`);
}
})
}
})
}
+756
View File
@@ -0,0 +1,756 @@
# Painter 画板 测试版
> uniapp 海报画板,更优雅的海报生成方案
> [查看更多 站点1](https://limeui.qcoon.cn/#/painter)
> [查看更多 站点2](http://liangei.gitee.io/limeui/#/painter)
> Q群:806744170
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
| √ | √ | √ | 未测 | √ | √ | √ |
## 代码演示
### 基本用法
- 插件提供JSON及HTML的方式绘制海报
- 插件参考了 css 块状流布局模拟css schema方式,放弃了之前使用的绝对定位布局。
#### 方式一 HTML
- 插件提供了`l-painter-view``l-painter-text``l-painter-image``l-painter-qrcode`四种类型组件
- 通过 `css` 属性绘制样式,与style使用方式保持一致。 因为style是保留字段,所以命名为`css`,如果有大佬知道如何破解请告之。
```html
<l-painter >
<l-painter-view css="background: #07c160; height: 120rpx; width: 120rpx; display: inline-block"></l-painter-view>
<l-painter-view css="background: #1989fa; height: 120rpx; width: 120rpx; border-top-right-radius: 60rpx; border-bottom-left-radius: 60rpx; display: inline-block; margin: 0 30rpx;"></l-painter-view>
<l-painter-view css="background: #ff9d00; height: 120rpx; width: 120rpx; border-radius: 50%; display: inline-block"></l-painter-view>
</l-painter>
```
#### 方式二 JSON schema
- 在json里四种类型组件的`type``view``text``image``qrcode`
- 通过 `board` 设置海报所需的 JSON schema 数据进行绘制
- 所有类型的schema都具有`css`字段,css的样式属性key值使用驼峰命名如:`lineHeight`
```html
<l-painter :board="poster" />
```
```js
data() {
return {
poster: {
css: {
// json 方式务必填写画板宽度
width: '750rpx'
},
views: [
{
css: {
background: "#07c160",
height: "120rpx",
width: "120rpx",
display: "inline-block"
},
type: "view"
},
{
css: {
background: "#1989fa",
height: "120rpx",
width: "120rpx",
borderTopRightRadius: "60rpx",
borderBottomLeftRadius: "60rpx",
display: "inline-block",
margin: "0 30rpx"
},
views: [],
type: "view"
},
{
css: {
background: "#ff9d00",
height: "120rpx",
width: "120rpx",
borderRadius: "50%",
display: "inline-block"
},
views: [],
type: "view"
},
]
}
}
}
```
### View 容器
- 类似于 `div` 可以嵌套承载更多的 view、text、imageqrcode共同构建一颗完整的节点树
- 在JSON schema里具有 `views` 的数组字段,用于嵌套承载节点。
#### 方式一 HTML
```html
<l-painter >
<l-painter-view css="background: #f0f0f0; padding-top: 100rpx;">
<l-painter-view css="background: #d9d9d9; width: 33.33%; height: 100rpx; display: inline-block"></l-painter-view>
<l-painter-view css="background: #bfbfbf; width: 66.66%; height: 100rpx; display: inline-block"></l-painter-view>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON schema
```js
{
css: {
width: '750rpx'
},
views: [
{
type: 'view',
css: {
background: '#f0f0f0',
paddingTop: '100rpx'
},
views: [
{
type: 'view',
css: {
background: '#d9d9d9',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
},
{
type: 'view',
css: {
background: '#bfbfbf',
width: '66.66%',
height: '100rpx',
display: 'inline-block'
}
}
],
}
]
}
```
### Text 文本
- 通过 `text` 属性填写文本内容。
- 支持`\n`换行符
- 支持省略号,使用css的`line-clamp`设置行数,当文字内容超过会显示省略号。
- 支持`text-decoration`
#### 方式一 HTML
```html
<l-painter >
<l-painter-view css="background: #e0e2db; padding: 30rpx; color: #222a29">
<l-painter-text
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"/>
<l-painter-text
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
css="text-align:center; padding-top: 20rpx; text-decoration: line-through "
/>
<l-painter-text
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
css="text-align:right; padding-top: 20rpx"
/>
<l-painter-text
text="水调歌头\n明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。"
css="line-clamp: 3; padding-top: 20rpx"
/>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON schema
```js
// 基础用法
{
type: 'text',
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
css: {
// 设置居中对齐
textAlign: 'center',
// 设置中划线
textDecoration: 'line-through'
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
css: {
// 设置右对齐
textAlign: 'right',
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
css: {
// 设置行数,超出显示省略号
lineClamp: 3,
}
}
```
### Image 图片
- 通过 `src` 属性填写图片路径。
- 图片路径支持:网络图片,本地static里的图片路径,缓存路径
- 通过 `css``object-fit`属性可以设置图片的填充方式,可选值见下方CSS表格。
- 通过 `css``object-position`配合 `object-fit` 可以设置图片的对齐方式,类似于`background-position`,详情见下方CSS表格。
- 使用网络图片时:小程序需要去公众平台配置 [downloadFile](https://mp.weixin.qq.com/) 域名
- 使用网络图片时:**H5需要决跨域问题**
#### 方式一 HTML
```html
<l-painter >
<!-- 基础用法 -->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx"
/>
<!-- 填充方式 -->
<!-- css object-fit 设置 填充方式 见下方表格-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; background: #eee"
/>
<!-- css object-position 设置 图片的对齐方式-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; object-position: 50% 50%; background: #eee"
/>
</l-painter>
```
#### 方式二 JSON schema
```js
// 基础用法
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx'
}
},
// 填充方式
// css objectFit 设置 填充方式 见下方表格
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain'
}
},
// css objectPosition 设置 图片的对齐方式
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain',
objectPosition: '50% 50%'
}
}
```
### Qrcode 二维码
- 通过`text`属性填写需要生成二维码的文本。
- 通过 `css` 里的 `color` 可设置生成码点的颜色。
- 通过 `css` 里的 `background`可设置背景色。
- 通过 `css `里的 `width``height`设置尺寸。
#### 方式一 HTML
```html
<l-painter>
<l-painter-qrcode
text="limeui.qcoon.cn"
css="width: 200rpx; height: 200rpx"
/>
</l-painter>
```
#### 方式二 JSON schema
```js
{
type: 'qrcode',
text: 'limeui.qcoon.cn',
css: {
width: '200rpx',
height: '200rpx',
}
}
```
### 生成图片
- 1、通过设置`isCanvasToTempFilePath`自动生成图片并在 `@success` 事件里接收海报临时路径
- 2、通过调用内部方法生成图片:
```html
<l-painter ref="painter">...code</l-painter>
```
```js
// 主动调用方式只能在绘制完成之后
// @done 事件表示绘制完成
this.$refs.painter.canvasToTempFilePath({
// 在nvue里是jpeg
fileType: 'jpg',
quality: 1,
success: (res) => {
console.log(res.tempFilePath)
}
})
```
### 海报示例
- 提供一份示例,只把插件当成生成图片的工具,弹窗之类的功能另外寻找。
- 通过设置`isCanvasToTempFilePath`主动生成图片,再由 `@success` 事件接收海报临时路径
- 使用`custom-style`样式把画板移到屏幕之外,因为可能canvas的层级比较高,无法覆盖。
- **注意**:海报画板不能隐藏 否则无法生成图片。
#### 方式一 HTML
```html
<image :src="path" mode="widthFix"></image>
<l-painter isCanvasToTempFilePath @success="path = $event"
custom-style="position: fixed; left: 200%"
css="width: 750rpx; padding-bottom: 40rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%)">
<l-painter-image src="https://cdn.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg" css="margin-left: 40rpx; margin-top: 40rpx; width: 84rpx; height: 84rpx; border-radius: 50%;"/>
<l-painter-view css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block">
<l-painter-text text="隔壁老王" css="display: block; padding-bottom: 10rpx; color: #fff; font-size: 32rpx; fontWeight: bold"/>
<l-painter-text text="为您挑选了一个好物" css="color: rgba(255,255,255,.7); font-size: 24rpx"/>
</l-painter-view>
<l-painter-view css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)">
<l-painter-image src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg" css="object-fit: cover; object-position: 50% 50%; width: 606rpx; height: 606rpx; border-radius: 12rpx;"/>
<l-painter-view css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;">
<l-painter-text text="¥" css="vertical-align: bottom"/>
<l-painter-text text="39" css="vertical-align: bottom; font-size: 58rpx"/>
<l-painter-text text=".39" css="vertical-align: bottom"/>
<l-painter-text text="¥59.99" css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"/>
</l-painter-view>
<l-painter-view css="margin-top: 32rpx; font-size: 26rpx; color: #8c5400">
<l-painter-text text="自营" css="color: #212121; background: #ffb400;"/>
<l-painter-text text="30天最低价" css="margin-left: 16rpx; background: #fff4d9; text-decoration: line-through;"/>
<l-painter-text text="满减优惠" css="margin-left: 16rpx; background: #fff4d9"/>
<l-painter-text text="超高好评" css="margin-left: 16rpx; background: #fff4d9"/>
</l-painter-view>
<l-painter-view css="margin-top: 30rpx">
<l-painter-text css="line-clamp: 2; color: #333333; line-height: 1.8em; font-size: 36rpx; width: 478rpx; padding-right:32rpx; box-sizing: border-box" text="360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝" :replace="{word: ['儿童', '9X'], color: 'red'}"></l-painter-text>
<l-painter-qrcode css="width: 128rpx; height: 128rpx;" text="limeui.qcoon.cn"></l-painter-qrcode>
</l-painter-view>
</l-painter-view>
</l-painter>
```
```js
data() {
return {
path: ''
}
}
```
#### 方式二 JSON schema
```html
<image :src="path" mode="widthFix"></image>
<l-painter
:board="poster"
isCanvasToTempFilePath
@success="path = $event"
custom-style="position: fixed; left: 200%"
/>
```
```js
data() {
return {
path: '',
poster: {
css: {
width: "750rpx",
paddingBottom: "40rpx",
background: "linear-gradient(,#000 0%, #ff5000 100%)"
},
views: [
{
src: "https://cdn.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg",
type: "image",
css: {
background: "#fff",
objectFit: "cover",
marginLeft: "40rpx",
marginTop: "40rpx",
width: "84rpx",
border: "2rpx solid #fff",
boxSizing: "border-box",
height: "84rpx",
borderRadius: "50%"
}
},
{
type: "view",
css: {
marginTop: "40rpx",
paddingLeft: "20rpx",
display: "inline-block"
},
views: [
{
text: "隔壁老王",
type: "text",
css: {
display: "block",
paddingBottom: "10rpx",
color: "#fff",
fontSize: "32rpx",
fontWeight: "bold"
}
},
{
text: "为您挑选了一个好物",
type: "text",
css: {
color: "rgba(255,255,255,.7)",
fontSize: "24rpx"
},
}
],
},
{
css: {
marginLeft: "40rpx",
marginTop: "30rpx",
padding: "32rpx",
boxSizing: "border-box",
background: "#fff",
borderRadius: "16rpx",
width: "670rpx",
boxShadow: "0 20rpx 58rpx rgba(0,0,0,.15)"
},
views: [
{
src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
type: "image",
css: {
objectFit: "cover",
objectPosition: "50% 50%",
width: "606rpx",
height: "606rpx"
},
}, {
css: {
marginTop: "32rpx",
color: "#FF0000",
fontWeight: "bold",
fontSize: "28rpx",
lineHeight: "1em"
},
views: [{
text: "¥",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "39",
type: "text",
css: {
verticalAlign: "bottom",
fontSize: "58rpx"
},
}, {
text: ".39",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "¥59.99",
type: "text",
css: {
verticalAlign: "bottom",
paddingLeft: "10rpx",
fontWeight: "normal",
textDecoration: "line-through",
color: "#999999"
}
}],
type: "view"
}, {
css: {
marginTop: "32rpx",
fontSize: "26rpx",
color: "#8c5400"
},
views: [{
text: "自营",
type: "text",
css: {
color: "#212121",
background: "#ffb400"
},
}, {
text: "30天最低价",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9",
textDecoration: "line-through"
},
}, {
text: "满减优惠",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}, {
text: "超高好评",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}],
type: "view"
}, {
css: {
marginTop: "30rpx"
},
views: [
{
text: "360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝",
type: "text",
css: {
paddingRight: "32rpx",
boxSizing: "border-box",
lineClamp: 2,
color: "#333333",
lineHeight: "1.8em",
fontSize: "36rpx",
width: "478rpx"
},
}, {
text: "limeui.qcoon.cn",
type: "qrcode",
css: {
width: "128rpx",
height: "128rpx",
},
}],
type: "view"
}],
type: "view"
}
]
}
}
}
```
### 原生小程序
- 插件里的`painter.js`支持在原生小程序中使用
- new Painter之后在`source`里传入JSON schema
- 再调用`render`绘制海报
- 如需生成图片,请查看微信小程序cavnas的[canvasToTempFilePath](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html)
```html
<canvas type="2d" id="painter" style="width: 100%"></canvas>
```
```js
import {Painter} from './painter'
page({
data: {
poster: {
css: {
width: '750rpx'
},
views: [
{
type: 'view',
css: {
background: '#d2d4c8',
paddingTop: '100rpx'
},
views: [
{
type: 'view',
css: {
background: '#5f7470',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
},
{
type: 'view',
css: {
background: '#889696',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
},
{
type: 'view',
css: {
background: '#b8bdb5',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
}
],
}
]
}
},
async onLoad() {
const res = await this.getCentext()
const painter = new Painter(res)
// 返回计算布局后的整个内容尺寸
const {width, height} = await painter.source(this.data.poster)
// 得到计算后的尺寸后 可给canvas尺寸赋值,达到动态响应效果
// 渲染
await painter.render()
},
// 获取canvas 2d
// 非2d也可以使用这里只是举个例子
getCentext() {
return new Promise(resolve => {
wx.createSelectorQuery()
.select(`#painter`)
.node()
.exec(res => {
let { node: canvas } = res[0];
resolve({
canvas,
context: canvas.getContext('2d'),
width: canvas.width,
height: canvas.height,
pixelRatio: 2
})
})
})
},
})
```
### Nvue
- 插件是通过 `web-view` 支持 `app-nvue`
- 默认是远端的文件,如果需要本地化,请按下方步骤:
- 1、去码云把[hybrid](https://gitee.com/liangei/lime-painter/tree/master/examples/uni/hybrid)目录的文件,放到自己项目的根目录。
- 2、给插件文件`l-painter.vue``web-view``src`加上根目录的`hybrid路径`并注释`this.webViewInit()`
```html
// 只加上路径 其它参数不要改
<web-view src="/hybrid/html/lime-ui/lime-painter/index.html" />
```
```js
// 153行 注释该方法
// this.webViewInit()
```
### 旧版更新(1.6.x)
- 由于1.8.x版放弃了以定位的方式,所以1.6.x版更新之后要每个样式都加上`position: absolute`
- 旧版的 `image` mode 模式被放弃,使用`object-fit`
- 旧版的 `isRenderImage` 改成 `is-canvas-to-temp-filePath`
- 旧版的 `maxLines` 改成 `line-clamp`
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| ------------- | ------------ | ---------------- | ------------ |
| board | JSON schema方式的海报元素对象集 | <em>object</em> | - |
| css | 海报最外层的样式,可以理解为`body` | <em>object</em> | 参数请向下看 |
| custom-style | canvas自定义样式 | <em>string</em> | |
| is-canvas-to-temp-filePath | 是否生成图片,在`@success`事件接收图片地址 | <em>boolean</em> | `false` |
| after-delay | 生成图片错乱,可延时生成图片 | <em>number</em> | `100` |
| type | canvas 类型,对微信头条支付宝小程序可有效,可选值:`2d`、`` | <em>string</em> | `2d` |
| file-type | 生成图片的后缀类型, 可选值:`png`、`jpg`,在nvue里是`jpeg` | <em>string</em> | `png` |
| path-type | 生成图片路径类型,可选值`url`、`base64` | <em>string</em> | `-` |
| pixel-ratio | 生成图片的像素密度,默认为对应手机的像素密度 | <em>number</em> | `-` |
<!-- | width | 画板的宽度,一般只用于通过内部方法时加上 | <em>number</em> | `` |
| height | 画板的高度 ,同上 | <em>number</em> | `` | -->
### css
| 属性名 | 支持的值或类型 | 默认值 |
| ------------- | ------------ | ---------------- |
| (min\max)width | 支持`%`、`rpx`、`px` | - |
| height | 同上 | - |
| color | `string` | - |
| position | 定位,可选值:`absolute`、`fixed` | - |
| ↳ left、top、right、bottom | 配合`position`才生效,支持`%`、`rpx`、`px` | - |
| margin | 可简写或各方向分别写,如:`margin-top`,支持`auto`、`rpx`、`px` | - |
| padding | 可简写或各方向分别写,支持`rpx`、`px` | - |
| border | 可简写或各个值分开写:`border-width`、`border-style` 、`border-color`,简写请按顺序写| - |
| line-clamp | `number`,超过行数显示省略号 | - |
| background | 支持渐变,但必须写百分比!如:`linear-gradient(,#ff971b 0%, #ff5000 100%)`、`radial-gradient(#0ff 15%, #f0f 60%)`,目前radial-gradient 渐变的圆心为元素中点,半径为最长边,不支持设置 | - |
| vertical-align | 文字垂直对齐,可选值:`bottom`、`top`、`middle` | `middle` |
| line-height | 文字行高,支持`rpx`、`px`、`em`| `1.4em` |
| font-weight | 文字粗细,可选值:`normal`、`bold`| `normal` |
| font-size | 文字大小,`string`,支持`rpx`、`px` | `14px` |
| text-decoration | 文本修饰,可选值:`underline` 、`line-through`、`overline`| - |
| text-align | 文本水平对齐,可选值:`right` 、`center`| - |
| display | 框类型,可选值:`block`、`inline-block`、`none`,当为`none`时是不渲染该段 | - |
| border-radius | 圆角边框,支持`%`、`rpx`、`px` | - |
| box-sizing | 可选值:`border-box` | - |
| box-shadow | 投影 | - |
| background-image | view元素设置背景纹理,注意这里的背景纹理一般是用于纹理平铺,无法代替`image`元素。如:水印 | - |
| background-repeat | 设置是否及如何重复背景纹理,可选值:`repeat`、`repeat-x`、`repeat-y`、`no-repeat` | `repeat` |
| [object-fit](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit/) | 图片元素适应容器方式,类似于`mode`,可选值:`cover`、 `contain`、 `fill`、 `none` | - |
| [object-position](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-position) | 图片的对齐方式,配合`object-fit`使用 | - |
### 图片填充模式 object-fit
| 名称 | 含义 |
| ------- | ------------ |
| contain | 保持宽高缩放图片,使图片的长边能完全显示出来 |
| cover | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边 |
| fill | 拉伸图片,使图片填满元素 |
| none | 保持图片原有尺寸 |
### 事件 Events
| 事件名 | 说明 | 回调 |
| ------- | ------------ | -------------- |
| success | 生成图片成功,若使用了`is-canvas-to-temp-filePath` 可以接收图片地址 | path |
| fail | 生成图片失败 | {error: error} |
| done | 绘制成功 | |
| progress | 绘制进度 | number |
## 常见问题
- 1、H5端使用网络图片需要解决跨域问题。
- 2、小程序使用网络图片需要去公众平台增加下载白名单!二级域名也需要配!
- 3、H5端生成图片是base64,有时显示只有一半可以使用原生标签`<IMG/>`
- 4、发生保存图片倾斜变形或提示native buffer exceed size limit时,使用pixel-ratio="2"参数,降分辨率。
- 5、h5保存图片不需要调接口,提示用户长按图片保存。
- 6、IOS APP 请勿使用HBX2.9.3.20201014的版本!这个版本无法生成图片。
- 7、组件不能隐藏,包含`v-if``v-show`、`display:none`、`opacity:0`
- 8、微信小程序 canvas 2d不支持真机调试,请使用真机预览方式。
- 9、华为手机APP上无法生成图片,请使用HBX2.9.11++
- 10、苹果微信7.0.20存在闪退和图片无法onload为微信bug,请到码云上升级本插件
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![输入图片说明](https://cdn.jsdelivr.net/gh/liangei/image@latest/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")
@@ -0,0 +1,64 @@
<template>
<view class="loading-footer row-center" :style="'color: ' + color">
<view v-if="status === 'loading' " class="loading row">
<loading :color="color" class="mr20"></loading>
<text :style="'color: ' + color">{{loadingText}}</text>
</view>
<view v-if="status === 'finished'" class="finished">{{ finishedText }}</view>
<view v-if="status === 'error'" @click="onRefresh">{{ errorText }}</view>
<view v-if="status === 'empty'" class="empty">
<text v-if="!slotEmpty">暂无数据</text>
<slot name="empty" v-else></slot>
</view>
</view>
</template>
<script>
export default {
data() {
return {};
},
components: {
},
props: {
status: {
type: String,
default: 'loading'
},
errorText: {
type: String,
default: '加载失败,点击重新加载'
},
loadingText: {
type: String,
default: '加载中...'
},
finishedText: {
type: String,
default: '我可是有底线的~'
},
slotEmpty: {
type: Boolean,
default: false
},
color: {
type: String,
default: "#666"
}
},
methods: {
onRefresh() {
this.$emit('refresh');
}
}
};
</script>
<style>
.loading-footer {
padding: 30rpx 0;
color: #666;
}
</style>
+52
View File
@@ -0,0 +1,52 @@
<template>
<view :class="'loading ' + ( type == 'flex' ? 'flex' : '' )" :style="{backgroundColor, }">
<loading :color="color" :size="size"></loading>
</view>
</template>
<script>
export default {
data() {
return {};
},
props: {
type: {
type: String,
default: 'fixed'
},
backgroundColor: {
type: String,
default: '#fff'
},
color: {
type: String,
},
size: {
type: Number,
default: 40
}
},
methods: {}
};
</script>
<style>
.loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.loading.flex {
position: static;
flex: 1;
width: 100%;
}
</style>
+203
View File
@@ -0,0 +1,203 @@
<template>
<view :class="'loading ' + ( vertical ? 'loading--vertical' : '' )">
<view :class="'loading__spinner loading__spinner--' + type" :style="{color, width: size + 'rpx', height: size + 'rpx'}">
<view v-for="(item, index) in array12" :key="index" v-if="type === 'spinner'" class="loading__dot"></view>
</view>
<view class="loading__text" :style="{'font-size': textSize + 'rpx', color}">
<slot></slot>
</view>
</view>
</template>
<script>
export default {
props: {
color: String,
vertical: Boolean,
type: {
type: String,
default: 'spinner'
},
size: {
type: Number,
default: 40
},
textSize: String
},
data() {
return {
array12: Array.from({
length: 12
})
}
}
}
</script>
<style>
:host {
font-size: 0;
line-height: 1;
}
.loading {
display: inline-flex;
align-items: center;
justify-content: center;
color: #c8c9cc;
}
.loading__spinner {
position: relative;
box-sizing: border-box;
width: 45rpx;
max-width: 100%;
max-height: 100%;
height: 45rpx;
animation: rotate .8s linear infinite;
}
.loading__spinner--spinner {
-webkit-animation-timing-function: steps(12);
animation-timing-function: steps(12);
}
.loading__spinner--circular {
border: 2rpx solid transparent;
border-top-color: initial;
border-radius: 100%;
}
.loading__text {
margin-left: 16rpx;
color: #969799;
font-size: 28rpx;
line-height: 40rpx;
}
.loading__text:empty {
display: none;
}
.loading--vertical {
-webkit-flex-direction: column;
flex-direction: column;
}
.loading--vertical .loading__text {
margin: 16rpx 0 0;
}
.loading__dot {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.loading__dot:before {
display: block;
width: 4rpx;
height: 25%;
margin: 0 auto;
background-color: currentColor;
border-radius: 40%;
content: " ";
}
.loading__dot:first-of-type {
-webkit-transform: rotate(30deg);
transform: rotate(30deg);
opacity: 1;
}
.loading__dot:nth-of-type(2) {
-webkit-transform: rotate(60deg);
transform: rotate(60deg);
opacity: .9375;
}
.loading__dot:nth-of-type(3) {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
opacity: .875;
}
.loading__dot:nth-of-type(4) {
-webkit-transform: rotate(120deg);
transform: rotate(120deg);
opacity: .8125;
}
.loading__dot:nth-of-type(5) {
-webkit-transform: rotate(150deg);
transform: rotate(150deg);
opacity: .75;
}
.loading__dot:nth-of-type(6) {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
opacity: .6875;
}
.loading__dot:nth-of-type(7) {
-webkit-transform: rotate(210deg);
transform: rotate(210deg);
opacity: .625;
}
.loading__dot:nth-of-type(8) {
-webkit-transform: rotate(240deg);
transform: rotate(240deg);
opacity: .5625;
}
.loading__dot:nth-of-type(9) {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
opacity: .5;
}
.loading__dot:nth-of-type(10) {
-webkit-transform: rotate(300deg);
transform: rotate(300deg);
opacity: .4375;
}
.loading__dot:nth-of-type(11) {
-webkit-transform: rotate(330deg);
transform: rotate(330deg);
opacity: .375;
}
.loading__dot:nth-of-type(12) {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
opacity: .3125;
}
@-webkit-keyframes rotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
@keyframes rotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
</style>
+188
View File
@@ -0,0 +1,188 @@
<template name="protocol-popup">
<view @touchmove.stop.prevent="clear" v-show="showPopup">
<view class="popup_mask1" @touchmove.stop.prevent="clear"></view>
<view class="popup_mask" @touchmove.stop.prevent="clear"></view>
<view class="popup_content">
<view class="title">{{ title }}</view>
<view class="content">
请你务必审慎阅读充分理解服务协议隐私政策各条款包括但不限于为了更好的向你提供服务我们需要收集你的设备标识操作日志等信息用于分析优化应用性能你可阅读
<text class="link" @click="linkClick(1)">服务协议</text>
<text class="link" @click="linkClick(2)">隐私政策</text>
了解详细信息如果你同意请点击下面按钮开始接受我们的服务
</view>
<view class="btn">
<view class="flex-1">
<button class="plain" @click="back">拒绝仅浏览</button>
</view>
<view class="flex-1">
<button @click="confirm">同意并继续</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: "lyg-popup",
props: {
title: {
type: String,
default: "用户使用及隐私保护政策提示"
},
// 协议路径
protocolPath: {
type: String
},
// 政策路径
policyPath: {
type: String
},
policyStorageKey: {
type: String,
default: "has_read_privacy"
}
},
data() {
return {
showPopup: false
};
},
created: function() {
var that = this;
console.log("lyg-popup created");
const platform = uni.getSystemInfoSync().osName
const agree = uni.getStorageSync(this.policyStorageKey)
if (platform === 'android' || agree) {
this.showPopup = false;
uni.showTabBar({})
} else {
this.showPopup = true;
uni.hideTabBar({})
}
},
methods: {
// 查看隐私协议或服务协议
linkClick (num) {
switch (num) {
case 1:
// 跳转服务协议
this.$Router.push(this.protocolPath)
break
case 2:
// 跳转隐私政策
this.$Router.push(this.policyPath)
break
}
},
// 禁止滚动
clear() {
return;
},
back() {
this.$emit('popupState', false)
// #ifdef APP-PLUS
if (uni.getSystemInfoSync().platform == 'ios') {
const threadClass = plus.ios.importClass("NSThread");
const mainThread = plus.ios.invoke(threadClass, "mainThread");
// plus.ios.invoke(mainThread, "exit");
//上面的不起效果下面的
plus.ios.import("UIApplication").sharedApplication().performSelector("exit")
} else if (uni.getSystemInfoSync().platform == 'android') {
plus.runtime.quit();
}
// #endif
},
// 关闭弹框
confirm() {
this.showPopup = false;
this.$emit('popupState', true);
uni.showTabBar({})
uni.setStorageSync(this.policyStorageKey, true)
}
}
};
</script>
<style lang="scss">
.popup_mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
z-index: 1299;
}
.popup_mask {
opacity: 1;
}
.popup_mask1 {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
z-index: 1298;
background-color: #ffffff;
}
.popup_content {
overflow: hidden;
box-sizing: border-box;
padding: 40upx 20upx 0 20upx;
position: fixed;
bottom: 30%;
border-radius: 8px;
left: 50%;
margin-left: -40%;
right: 0;
min-height: 400upx;
background: #ffffff;
width: 80%;
z-index: 1300;
.title {
text-align: center;
line-height: 120rpx;
font-size: 32rpx;
font-weight: 600;
}
.content {
padding: 0 32rpx;
text-indent: 1em;
.link {
color: #4982db;
}
}
.btn {
display: flex;
flex: 1;
margin-top: 30rpx;
>view {
margin: 20rpx;
flex: 1;
}
button {
color: $-color-white;
background-color: $-color-primary;
}
.plain {
color: $-color-primary;
background-color: $-color-white;
border: 1px solid $-color-primary;
}
}
}
</style>
+196
View File
@@ -0,0 +1,196 @@
<template>
<view>
<u-popup
v-model="showPop"
mode="bottom"
border-radius="14"
:safe-area-inset-bottom="true"
:maskCloseAble="false"
>
<view class="popup-content">
<view class="popup-header">
<image class="logo" mode="heightFix" :src="logo"></image>
<text class="title">{{ title }}</text>
</view>
<view class="popup-tips">
建议使用您的微信头像和昵称以便获得更好的体验
</view>
<view class="popup-form">
<form @submit="handleSubmit">
<u-form-item required label="头像" :labelWidth="120">
<button
class="select-avatar"
hover-class="none"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<image
class="avatar"
:src="avatar"
v-if="avatar"
/>
<div class="select-btn" v-else>
<u-icon name="plus" :size="30" />
添加图片
</div>
</button>
</u-form-item>
<u-form-item required label="昵称" :labelWidth="120">
<input
style="height: 60rpx"
:value="userInfo.nickname"
name="nickname"
type="nickname"
placeholder="请输入昵称"
/>
</u-form-item>
<button
class="submit-btn bg-primary"
form-type="submit"
>
确定
</button>
<view class="close-btn" @tap="onClose">暂不登录</view>
</form>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { uploadFile } from "@/utils/tools";
export default {
name: "mplogin-popup",
props: {
logo: {
type: String,
required: true
},
value: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
}
},
data() {
return {
avatar: "",
};
},
methods: {
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl;
if (!avatarUrl) {
return;
}
uni.showLoading({
title: "正在上传中...",
mask: true,
});
uploadFile(avatarUrl)
.then((res) => {
uni.hideLoading();
this.avatar = res.url;
})
.catch(() => {
uni.hideLoading();
this.$toast({
title: "上传失败",
});
});
},
handleSubmit(e) {
const { nickname } = e.detail.value
const {avatar} = this
if(!avatar) return this.$toast({
title: '请添加头像'
})
if(!nickname) return this.$toast({
title: '请输入昵称'
})
this.$emit('update', {
avatar,
nickname
})
},
onClose() {
this.showPop = false
this.$emit('close')
}
},
computed: {
showPop: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
}
};
</script>
<style lang="scss">
.popup-content {
padding: 40rpx 40rpx 80rpx;
.popup-header {
display: flex;
align-items: center;
.title {
font-size: 38rpx;
font-weight: bold;
}
.logo {
flex: none;
height: 80rpx;
margin-right: 20rpx;
border-radius: 4rpx;
}
}
.popup-tips {
color: #999;
margin-top: 40rpx;
}
.popup-form {
margin-top: 55rpx;
.select-avatar {
width: 120rpx;
height: 120rpx;
color: #999;
.avatar {
width: 100%;
height: 100%;
}
.select-btn {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: 1px dotted #ccc;
font-size: 22rpx;
border-radius: 4rpx;
}
}
.submit-btn {
height: 82rpx;
line-height: 82rpx;
border-radius: 41px;
color: #fff;
font-size: 30rpx;
margin: 80rpx 35rpx 60rpx;
}
.close-btn {
color: #999;
font-size: 30rpx;
text-align: center;
}
}
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<!-- components/my-coupons/my-coupons.wxml -->
<view class="my-coupons">
<coupon-list v-if="!showNull" :list="couponList" :btnType="type"></coupon-list>
<view v-else class="column-center" style="padding-top: 200rpx">
<image class="img-null" src="/static/images/coupon_null.png"></image>
<text class="muted">暂无优惠券</text>
</view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
import { getMyCoupon } from '../../api/user';
export default {
data() {
return {
couponList: [],
showNull: false
};
},
props: {
type: {
type: Number,
default: 0
}
},
beforeMount: function () {
this.getMyCouponFun();
},
methods: {
getMyCouponFun() {
const {
type
} = this;
getMyCoupon({
type
}).then(res => {
if (res.code == 1) {
this.$emit('getnum', {
detail: res.data.length
});
if (res.data.length <= 0) {
this.showNull = true;
return;
}
this.couponList = res.data;
}
});
}
}
};
</script>
<style>
/* components/my-coupons/my-coupons.wxss */
.my-coupons {
min-height: calc(100vh - 80rpx);
}
</style>
+239
View File
@@ -0,0 +1,239 @@
<template>
<view class="container">
<view class="my-lottery row-between wrap" v-if="status">
<view v-for="(item, index) in lotteryData" :key="index" class="lottery-item column-center" :class="{
active: activeIndex == index,
'lotty-btn': !item.id && item.name,
}" @tap="onLotteryClick(item.id)">
<image :src="item.url" style="width: 80rpx;height: 80rpx"></image>
<text :class="!item.id ? 'xs mt20' : 'nr mt10'"
:style="'color: ' + (!item.id ? '#ED3720' : '#743C3C') + ';font-weight: 600;'">{{item.name}}</text>
</view>
</view>
<view class="activity-null row-center" v-else>
活动暂未开始
</view>
</view>
</template>
<script>
import {
userLottery
} from "@/api/user"; // 顺序
// 顺序
const LOTTERY_ORDER = [0, 1, 2, 5, 8, 7, 6, 3];
export default {
data() {
return {
// 是否在抽奖中
isLottery: false,
// 表示位移的总次数
currentIndex: 0,
// 中奖索引
luckyOrder: -1,
// 转盘的旋转速度,实际是转盘旋转间隔时间setTimeout的值
speed: 200,
// 当前活跃的坐标
activeIndex: -1
};
},
components: {},
props: {
// 转盘数据
lotteryData: {
type: Array,
default: () => []
},
// 最少转动多少圈
circleTimes: {
type: Number,
default: 30
},
status: {
type: Number | String,
default: 0
}
},
beforeMount() {
this.lotteryTimer = null;
},
methods: {
onLotteryClick(id) {
if (!id) {
this.start();
}
},
userLotteryFun() {
userLottery().then(res => {
if (res.code == 1) {
let {
id
} = res.data;
let index = this.lotteryData.findIndex(item => {
return item.id == id;
});
switch (index) {
case 0:
index = 0;
break;
case 1:
index = 1;
break;
case 2:
index = 2;
break;
case 3:
index = 7;
break;
case 5:
index = 3;
break;
case 6:
index = 6;
break;
case 7:
index = 5;
break;
case 8:
index = 4;
break;
default:
index = -1;
break;
}
this.luckyOrder = index
this.result = res.data
.text; // console.log("id:", id, "index", index, LOTTERY_ORDER[index], "####");
// console.log(this.data.lotteryData, "data");
this.$emit("begin");
this.startLotteryFun();
} else {
this.reset()
}
});
},
startLotteryFun() {
let {
currentIndex
} = this;
let activeIndex = this.getHighLightItemIndexFun(currentIndex);
this.activeIndex = activeIndex
const currentOrder = currentIndex % 8;
this.currentIndex = ++currentIndex;
if (currentIndex > this.circleTimes + 8 && this.luckyOrder == currentOrder) {
if (this.lotteryTimer) {
clearTimeout(this.lotteryTimer);
}
setTimeout(() => {
this.stopCallbackFun(LOTTERY_ORDER[this.luckyOrder]);
setTimeout(() => {
this.reset();
this.activeIndex = -1
}, 1000);
}, 500);
} else {
if (currentIndex < this.circleTimes) {
this.speed -= 10
} else if (currentIndex > this.circleTimes + 8 && this.luckyOrder == currentOrder + 1) {
this.speed += 80
} else {
this.speed += 20
}
if (this.speed < 50) {
this.speed = 50
}
this.lotteryTimer = setTimeout(this.startLotteryFun.bind(this), this.speed);
}
},
// luckyIndex: 中奖在列表中的index
stopCallbackFun(luckyIndex) {
this.$emit("finish", {
detail: this.result
});
},
// 根据 currentIndex 获取当前应该高亮列表中的第几个奖品
getHighLightItemIndexFun(currentIndex) {
return LOTTERY_ORDER[currentIndex % LOTTERY_ORDER.length];
},
// 根据奖品在数据中的index,获取奖品在转盘中的位置
getLuckyItemOrderFun(index) {},
start(e) {
// 如果还没开始转动,开始转动转盘
if (!this.isLottery) {
this.isLottery = true
this.userLotteryFun();
}
},
// 停止转动
stop(index, callback) {
this.luckyOrder = this.getLuckyItemOrderFun(index);
console.log("stop, ###", index);
this._stopCallback = callback;
},
// 重设转盘
reset() {
this.isLottery = false;
this.currentIndex = 0;
this.speed = 100;
this.luckyOrder = -1
}
}
};
</script>
<style>
.my-lottery {
background-image: url(../../static/images/choujiang_light.png);
width: 640rpx;
height: 640rpx;
background-size: 100% 100%;
padding: 35rpx 40rpx;
}
.my-lottery .lottery-item {
width: 180rpx;
height: 180rpx;
background-image: url(../../static/images/choujiang_block.png);
background-size: 100% 100%;
}
.my-lottery .lottery-item.lotty-btn {
background-image: url(../../static/images/choujiang_button.png);
}
.container .activity-null {
color: #FEE5BF;
font-size: 40rpx;
width: 640rpx;
height: 640rpx;
}
.active {
opacity: 0.7;
}
</style>
+172
View File
@@ -0,0 +1,172 @@
<template>
<view class="navbar" @tap.stop="showFloat = false">
<u-navbar :background="background" :title="title" :title-color="titleColor" :border-bottom="borderBottom"
:immersive="immersive" :title-bold="true" :is-back="false">
<view class="navbar-left" slot="left">
<u-icon :name="backIcon" :size="36" @click="goBack"></u-icon>
<view class="line"></view>
<view class="navbar-lists" @tap.stop="showFloat = !showFloat">
<u-icon :name="require('@/static/images/icon_list.png')" :size="32"></u-icon>
<view class="navbar-float" v-show="showFloat">
<navigator v-for="(item, index) in navLists" :key="index" :url="item.url" :open-type="item.type"
class="float-item" hover-class="none">
<u-icon :name="item.icon" :size="44"></u-icon>
<text class="ml20">{{ item.name }}</text>
</navigator>
</view>
</view>
</view>
</u-navbar>
<view class="mask" v-show="showFloat" @touchstart="showFloat = false">
</view>
</view>
</template>
<script>
export default {
props: {
// 导航内容
title: String,
titleColor: {
type: String,
default: '#000000'
},
// 导航的背景颜色
background: {
type: Object,
default: () => ({
background: '#ffffff'
})
},
// 是否显示底部边框
borderBottom: {
type: Boolean,
default: false
},
immersive: {
type: Boolean,
default: false
}
},
data() {
return {
isIndex: false,
navLists: [{
url: '/pages/index/index',
name: '首页',
icon: require('@/static/images/icon_home.png'),
type: 'switchTab'
}, {
url: '/pages/goods_search/goods_search',
name: '搜索',
icon: require('@/static/images/icon_search.png'),
type: 'navigate'
}, {
url: '/pages/shop_cart/shop_cart',
name: '购物车',
icon: require('@/static/images/icon_carts.png'),
type: 'switchTab'
}, {
url: '/pages/user/user',
name: '个人中心',
icon: require('@/static/images/icon_user.png'),
type: 'switchTab'
}],
showFloat: false
};
},
methods: {
goBack() {
if (!this.isIndex) {
uni.navigateBack()
return
}
uni.switchTab({
url: '/pages/index/index'
})
}
},
computed: {
backIcon() {
const iconName = this.isIndex ? 'icon_home' : 'icon_back'
return require(`@/static/images/${iconName}.png`)
}
},
created() {
setTimeout(() => {
let pages = getCurrentPages();
if (pages.length == 1) {
this.isIndex = true
}
})
}
}
</script>
<style lang="scss">
.navbar {
.navbar-left {
display: flex;
padding: 12rpx 25rpx;
border-radius: 30px;
background: rgba(255, 255, 255, 0.3);
border: 1rpx solid rgba(0, 0, 0, 0.1);
.line {
width: 1px;
height: 36rpx;
background: rgba(0, 0, 0, 0.2);
margin: 0 25rpx;
}
.navbar-lists {
display: flex;
justify-content: center;
position: relative;
.navbar-float {
position: absolute;
top: 40px;
width: 258rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 14rpx;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.06);
&::before {
content: '';
display: block;
position: absolute;
left: 50%;
width: 0;
height: 0;
border: 14rpx solid transparent;
border-bottom-color: #fff;
transform: translate(-50%, -100%);
}
.float-item {
padding: 20rpx 0;
display: flex;
align-items: center;
&:not(:last-of-type) {
border-bottom: 1px solid #e5e5e5;
}
}
}
}
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<u-modal v-model="show" :show-cancel-button ="true" :content="getTipsText" @confirm="onConfirm" confirm-color="#ff2c3c"></u-modal>
</template>
<script>
import {
cancelOrder,
delOrder,
confirmOrder
} from '@/api/order';
export default {
props: {
type: Number,
orderId: [Number, String]
},
data() {
return {
show: false
};
},
methods: {
open() {
this.show = true
},
close() {
this.show = false
},
async onConfirm() {
const {
type,
orderId
} = this;
let res = null;
switch (type) {
case 0:
res = await cancelOrder(orderId);
break;
case 1:
res = await delOrder(orderId);
break;
case 2:
res = await confirmOrder(orderId);
break;
}
if (res.code == 1) {
this.close()
this.$emit("refresh")
this.$toast({
title: res.msg
});
}
},
},
computed: {
getTipsText() {
const {
type
} = this
switch (type) {
case 0:
return "确认取消订单吗?"
case 1:
return "确认删除订单吗?"
case 2:
return "确认收货吗?"
}
}
}
}
</script>
<style>
</style>
+218
View File
@@ -0,0 +1,218 @@
<template>
<view class="order-goods bg-white">
<view v-for="(item, index) in list" :key="index" class="item-wrap">
<view class="item row" @tap="toGoods(item.goods_id)">
<view class="goods-img">
<custom-image
width="180rpx"
radius="10rpx"
height="180rpx"
lazy-load
:src="item.image_str || item.image"
></custom-image>
</view>
<view class="goods-info ml20 flex1">
<view class="goods-name line2 mb10">
<u-tag
class="mr10"
v-if="team.need"
:text="team.need + '人团'"
size="mini"
type="primary"
mode="plain"
/>
{{ item.goods_name || item.name }}</view
>
<view class="goods-spec xs muted mb20">{{
item.spec_value_str || item.spec_value
}}</view>
<view class="row-between">
<view class="goods-price row">
<view class="primary">
<price-format
v-if="!item.is_member && order_type === 0"
:weight="500"
:subscript-size="24"
:first-size="34"
:second-size="24"
:price="item.original_price || item.goods_price"
></price-format>
</view>
<view class="vip-price row" v-if="item.is_member && order_type === 0">
<view class="price-name xxs">会员价</view>
<view style="padding: 0 10rpx">
<price-format
:price="item.goods_price"
:first-size="22"
:second-size="22"
:subscript-size="22"
:weight="500"
color="#7B3200"
></price-format>
</view>
</view>
<view
class="vip-price row"
v-if="order_type === 1 || order_type === 2 || order_type === 3"
>
<view class="price-name xxs" style="background-color: #e74346">
<text v-if="order_type === 1">秒杀价</text>
<text v-if="order_type === 2">拼团价</text>
<text v-if="order_type === 3">砍价</text>
</view>
<view style="padding: 0 10rpx">
<price-format
:price="item.goods_price"
:first-size="22"
:second-size="22"
:subscript-size="22"
:weight="500"
color="#7B3200"
></price-format>
</view>
</view>
</view>
<view class="goods-num sm">x{{ item.goods_num }}</view>
</view>
</view>
</view>
<template v-if="mode === 'comfirm'">
<view class="delivery" v-if="delivery === 1 && !item.is_express"
>该商品不支持快递配送</view
>
<view class="delivery" v-if="delivery === 2 && !item.is_selffetch"
>该商品不支持门店自提</view
>
</template>
<view class="goods-footer row" v-if="link">
<view style="flex: 1"></view>
<navigator
class="mr20"
hover-class="none"
:url="'/bundle/pages/goods_reviews/goods_reviews?id=' + item.id"
v-if="item.comment_btn"
>
<button size="xs" class="plain br60" hover-class="none">评价晒图</button>
</navigator>
<navigator
v-if="item.refund_btn"
hover-class="none"
:url="
'/bundle/pages/apply_refund/apply_refund?order_id=' +
item.order_id +
'&item_id=' +
item.item_id
"
>
<button size="xs" class="plain br60" hover-class="none">申请退款</button>
</navigator>
<view v-if="item.after_status_desc" style="color: orange">
{{ item.after_status_desc }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
components: {},
props: {
list: {
type: Array,
default: () => []
},
link: {
type: Boolean,
default: false
},
team: {
type: [Object, Array],
default: () => ({})
},
delivery: {
type: Number,
default: 1
},
// order | comfirm
mode: {
type: String,
default: 'order'
},
order_type: {
type: Number,
default: 0
}
},
methods: {
toGoods(id) {
if (!this.link) return
uni.navigateTo({
url: `/pages/goods_details/goods_details?id=${id}`
})
}
}
}
</script>
<style lang="scss">
.order-goods {
.item {
padding: 20rpx 24rpx;
.vip-price {
// margin: 0 10rpx;
background-color: #ffe9ba;
line-height: 30rpx;
border-radius: 6rpx;
overflow: hidden;
.price-name {
background-color: #101010;
padding: 3rpx 10rpx;
color: #ffd4b7;
position: relative;
overflow: hidden;
&::after {
content: '';
display: block;
width: 20rpx;
height: 20rpx;
position: absolute;
right: -15rpx;
background-color: #ffe9ba;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
}
}
}
}
.goods-footer {
height: 70rpx;
align-items: flex-start;
padding: 0 24rpx;
.plain {
border: 1px solid #999;
height: 52rpx;
line-height: 52rpx;
font-size: 26rpx;
}
}
.delivery {
display: inline-block;
margin-left: calc(180rpx + 20rpx * 2);
padding: 4rpx 15rpx;
border-radius: 60px;
font-size: 20rpx;
background-color: #f4f4f4;
color: #999999;
}
}
</style>
+479
View File
@@ -0,0 +1,479 @@
// +---------------------------------------------------------------------- // |
likeshop开源商城系统 //
+---------------------------------------------------------------------- // |
欢迎阅读学习系统程序代码建议反馈是我们前进的动力 // |
gitee下载https://gitee.com/likeshop_gitee // |
github下载https://github.com/likeshop-github // |
访问官网https://www.likeshop.cn // | 访问社区:https://home.likeshop.cn // |
访问手册http://doc.likeshop.cn // | 微信公众号:likeshop技术社区 // |
likeshop系列产品在giteegithub等公开渠道开源版本可免费商用未经许可不能去除前后端官方版权标识
// |
likeshop系列产品收费版本务必购买商业授权购买去版权授权后方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布 // |
likeshop团队版权所有并拥有最终解释权 //
+---------------------------------------------------------------------- // |
author: likeshop.cn.team //
+----------------------------------------------------------------------
<template>
<view>
<view class="order-list">
<navigator
v-for="(item, index) in orderList"
:key="index"
hover-class="none"
class="order-item bg-white mt20"
:url="'/pages/order_details/order_details?id=' + item.id"
>
<view class="order-header row-between">
<view class="row">
<view v-if="item.delivery_type == 2" class="mr10">
<u-tag
text="自提"
size="mini"
type="primary"
mode="dark"
bg-color="#0cc21e"
/>
</view>
<view v-if="item.order_type == 1" class="mr10">
<u-tag text="秒杀" size="mini" type="primary" mode="plain" />
</view>
<view v-if="item.order_type == 2" class="mr10">
<u-tag text="拼团" size="mini" type="primary" mode="plain" />
</view>
<view v-if="item.order_type == 3" class="mr10">
<u-tag text="砍价" size="mini" type="primary" mode="plain" />
</view>
订单编号{{ item.order_sn }}
</view>
<view :class="item.order_status == 4 ? 'muted' : 'primary'">{{
item.order_status_desc
}}</view>
</view>
<view class="order-con">
<order-goods
:list="item.order_goods"
:order_type="item.order_type"
></order-goods>
<view class="all-price row-end">
<text class="muted xs"
>{{ goodCount(item.order_goods) }}件商品总金额</text
>
<price-format
:subscript-size="30"
:first-size="30"
:second-size="30"
:price="item.order_amount"
></price-format>
</view>
</view>
<view
class="order-footer row"
v-if="
item.pickup_btn ||
item.cancel_btn ||
item.delivery_btn ||
item.take_btn ||
item.del_btn ||
item.pay_btn ||
item.comment_btn
"
>
<view style="flex: 1">
<view
class="primary sm row"
style="line-height: 26rpx"
v-if="getCancelTime(item.order_cancel_time) > 0"
><u-count-down
separator="zh"
:timestamp="getCancelTime(item.order_cancel_time)"
separator-color="#FF2C3C"
color="#FF2C3C"
:separator-size="26"
:font-size="26"
bg-color="transparent"
@end="reflesh"
></u-count-down
></view>
</view>
<view v-if="item.cancel_btn">
<button
size="sm"
class="plain br60 lighter"
hover-class="none"
@tap.stop="cancelOrder(item.id)"
>
取消订单
</button>
</view>
<view
v-if="item.delivery_btn"
@tap.stop="
goPage(
'/bundle/pages/goods_logistics/goods_logistics?id=' + item.id
)
"
>
<button size="sm" class="btn plain br60 lighter" hover-class="none">
查看物流
</button>
</view>
<view v-if="item.del_btn">
<button
size="sm"
class="btn plain br60 lighter"
hover-class="none"
@tap.stop="delOrder(item.id)"
>
删除订单
</button>
</view>
<view v-if="item.pay_btn" class="ml20">
<button
size="sm"
class="btn bg-primary br60 white"
@tap.stop="payNow(item.id)"
>
立即付款
</button>
</view>
<view v-if="item.comment_btn" class="ml20">
<button
size="sm"
hover-class="none"
class="btn plain btn br60 primary red"
>
去评价
</button>
</view>
<view v-if="item.pickup_btn" class="ml20">
<button
size="sm"
hover-class="none"
class="btn plain btn br60 primary red"
>
查看提货码
</button>
</view>
<view v-if="item.take_btn" class="ml20">
<button
size="sm"
class="btn plain br60 primary red"
hover-class="none"
@tap.stop="comfirmOrder(item.id, item.pay_way)"
>
确认收货
</button>
</view>
</view>
</navigator>
<loading-footer :status="status" :slot-empty="true" @refresh="reload">
<view slot="empty" class="column-center" style="padding-top: 200rpx">
<image class="img-null" src="/static/images/goods_null.png"></image>
<text class="lighter">暂无订单</text>
</view>
</loading-footer>
</view>
<order-dialog
ref="orderDialog"
:order-id="orderId"
:type="type"
@refresh="reflesh"
></order-dialog>
<loading-view
v-if="showLoading"
background-color="transparent"
:size="50"
></loading-view>
</view>
</template>
<script>
import {
getOrderList,
cancelOrder,
delOrder,
confirmOrder,
getwxReceiveDetail,
getwechatSyncCheck,
} from "@/api/order";
import { compareWeChatVersion } from "@/utils/tools";
import { prepay } from "@/api/app";
import { loadingType } from "@/utils/type";
import { wxpay, alipay } from "@/utils/pay";
import { loadingFun } from "@/utils/tools";
export default {
data() {
return {
page: 1,
orderList: [],
status: loadingType.LOADING,
showCancel: false,
type: 0,
orderId: "",
showLoading: false,
pay_way: "",
};
},
components: {},
props: {
orderType: {
type: String,
},
},
created: function () {
uni.$on("refreshorder", () => {
this.reflesh();
});
uni.$on("payment", (params) => {
if (params.result) {
this.reflesh();
uni.navigateBack();
setTimeout(() => this.$toast({ title: "支付成功" }), 0.5 * 1000);
}
});
},
beforeMount: function () {
this.getOrderListFun();
},
destroyed: function () {
uni.$off(["payment", "refreshorder"]);
},
methods: {
reflesh() {
this.page = 1;
this.orderList = [];
this.status = loadingType.LOADING;
this.type = 0;
this.getOrderListFun();
},
reload() {
this.status = loadingType.LOADING;
this.getOrderListFun();
},
orderDialog() {
this.$refs.orderDialog.open();
},
delOrder(id) {
this.orderId = id;
this.type = 1;
this.$nextTick(() => {
this.orderDialog();
});
},
// 小程序确认收货
comfirmReceive(transaction_id) {
return new Promise((resolve, reject) => {
wx.openBusinessView({
businessType: "weappOrderConfirm",
extraData: {
transaction_id,
},
success({ extraData }) {
if (extraData.status == "success") {
resolve("确认收货");
} else {
resolve("取消收货");
}
},
fail(err) {
reject(err);
},
});
});
},
//查询是否收货成功
querycomfirmReceive(id) {
return new Promise((resolve, reject) => {
getwechatSyncCheck({ id })
.then(({ data }) => {
if (data.order.order_state === 4) {
resolve("已确认收货");
} else {
reject("未确认收货");
}
})
.catch((err) => {
reject(err);
});
});
},
comfirmOrder(id, pay_way) {
this.orderId = id;
this.pay_way = pay_way;
this.type = 2;
this.$nextTick(async () => {
// #ifdef MP-WEIXIN
let res = {};
if (this.pay_way === 1) {
res = await getwechatSyncCheck({ id: this.orderId });
console.log(res);
}
if (
compareWeChatVersion("2.6.0") === 1 &&
wx.openBusinessView &&
this.pay_way === 1 &&
res.data.order.order_state !== 1
) {
try {
const { data } = await getwxReceiveDetail({
order_id: this.orderId,
});
await this.comfirmReceive(data.transaction_id);
await this.querycomfirmReceive(this.orderId);
await confirmOrder(this.orderId);
} catch (error) {
console.log(error);
}
this.reflesh();
} else {
this.orderDialog();
}
// #endif
// #ifndef MP-WEIXIN
this.orderDialog();
// #endif
});
},
cancelOrder(id) {
this.orderId = id;
this.type = 0;
this.$nextTick(() => {
this.orderDialog();
});
},
payNow(id) {
// this.showLoading = true
uni.navigateTo({
url: `/pages/payment/payment?from=${"order"}&order_id=${id}`,
});
// prepay({
// from: 'order',
// order_id: id
// }).then(res => {
// let args = res.data;
// this.showLoading = false
// if (res.code == 1) {
// wxpay(args).then((resPay) => {
// if(resPay == 'success') {
// this.$toast({
// title: "支付成功"
// })
// uni.$emit("refreshorder")
// }
// })
// }else if(res.code == 20001){
// alipay(args).then((resPay) => {
// if(resPay == 'success') {
// this.$toast({
// title: "支付成功"
// })
// uni.$emit("refreshorder")
// }
// })
// }
// });
},
async getOrderListFun() {
let { page, orderType, orderList, status } = this;
const data = await loadingFun(getOrderList, page, orderList, status, {
type: orderType,
});
if (!data) return;
this.page = data.page;
this.orderList = data.dataList;
this.status = data.status;
},
goPage(url) {
uni.navigateTo({
url,
});
},
goodCount(goodLists) {
console.log(goodLists);
let count = 0;
goodLists.forEach((item) => {
count += item.goods_num;
});
return count;
},
},
computed: {
getOrderStatus() {
return (status) => {
let text = "";
switch (status) {
case 0:
text = "待支付";
break;
case 1:
text = "待发货";
break;
case 2:
text = "待收货";
break;
case 3:
text = "已完成";
break;
case 4:
text = "订单已关闭";
break;
}
return text;
};
},
getCancelTime() {
return (time) => time - Date.now() / 1000;
},
},
};
</script>
<style lang="scss">
.order-list {
// min-height: calc(100vh - 80rpx);
padding: 0 20rpx;
overflow: hidden;
.order-item {
border-radius: 10rpx;
.order-header {
height: 80rpx;
padding: 0 24rpx;
border-bottom: 1px dotted #e5e5e5;
}
.all-price {
text-align: right;
padding: 0 24rpx 20rpx;
}
.order-footer {
height: 100rpx;
border-top: $-solid-border;
padding: 0 24rpx;
.plain {
border: 1px solid #bbbbbb;
&.red {
border-color: $-color-primary;
}
}
}
}
}
</style>
+137
View File
@@ -0,0 +1,137 @@
<template>
<picker mode="multiSelector"
:value="multiIndex"
:range="multiArray"
@change="handleValueChange"
@columnchange="handleColumnChange">
<slot></slot>
</picker>
</template>
<script>
const CHINA_REGIONS = require('./regions.json')
export default {
props:{
defaultRegions:{
type:Array,
default(){
return []
}
},
defaultRegionCode:{
type:String
},
defaultRegion:[String,Array]
},
data() {
return {
cityArr:CHINA_REGIONS[0].child,
districtArr:CHINA_REGIONS[0].child[0].child,
multiIndex: [0, 0, 0],
isInitMultiArray:true,
}
},
watch:{
defaultRegion:{
handler(region,oldRegion){
if(Array.isArray(region)){
// 避免传的是字面量的时候重复触发
oldRegion = oldRegion || []
if(region.join('')!==oldRegion.join('')){
this.handleDefaultRegion(region)
}
}else if(region&&region.length == 6){
this.handleDefaultRegion(region)
}else{
console.warn('defaultRegion非有效格式')
}
},
immediate:true,
}
},
computed:{
multiArray(){
return this.pickedArr.map(arr=>arr.map(item=>item.name))
},
pickedArr(){
// 进行初始化
if(this.isInitMultiArray){
return [
CHINA_REGIONS,
CHINA_REGIONS[0].child,
CHINA_REGIONS[0].child[0].child
]
}
return [CHINA_REGIONS,this.cityArr,this.districtArr];
}
},
methods: {
handleColumnChange(e){
// console.log(e);
this.isInitMultiArray = false;
const that = this;
let col = e.detail.column;
let row = e.detail.value;
that.multiIndex[col] = row;
try{
switch(col){
case 0:
if(CHINA_REGIONS[that.multiIndex[0]].child.length==0){
that.cityArr = that.districtArr = [CHINA_REGIONS[that.multiIndex[0]]]
break;
}
that.cityArr = CHINA_REGIONS[that.multiIndex[0]].child
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].child[that.multiIndex[1]].child
break;
case 1:
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].child[that.multiIndex[1]].child
break;
case 2:
break;
}
}catch(e){
// console.log(e);
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].child[0].child
}
},
handleValueChange(e){
// 结构赋值
let [index0,index1,index2] = e.detail.value;
let [arr0,arr1,arr2] = this.pickedArr;
let address = [arr0[index0],arr1[index1],arr2[index2]];
// console.log(address);
this.$emit('getRegion',address)
},
handleDefaultRegion(region){
const isCode = !Array.isArray(region)
this.isInitMultiArray = false;
let children = CHINA_REGIONS
for(let i=0;i<3;i++){
for(let j=0;j<children.length;j++){
let condition = isCode?children[j].value==region.slice(0,(i+1)*2):children[j].name.includes(region[i]);
if(condition){
// 匹配成功进行赋值
// console.log(i,j,children.length-1);
children = children[j].child;
if(i==0){
this.cityArr = children
}else if(i==1){
this.districtArr = children
}
this.$set(this.multiIndex,i,j)
// console.log(this.multiIndex);
break;
}else{
// 首次匹配失败就用默认的初始化
// console.log(i,j,children.length-1);
if(i==0 && j==(children.length-1)){
this.isInitMultiArray = true;
}
}
}
}
}
},
}
</script>
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
<template>
<text :style="{color, 'font-weight': weight}" :class="(lineThrough ? 'line-through' : '') + ' price-format'">
<text v-if="showSubscript" :style="{'font-size': subscriptSize + 'rpx', 'margin-right': '2rpx'}">¥</text>
<text :style="{'font-size': firstSize + 'rpx', 'margin-right': '1rpx'}">{{priceSlice.first}}</text>
<text v-if="priceSlice.second" :style="{'font-size': secondSize + 'rpx'}">.{{priceSlice.second}}</text>
</text>
</template>
<script>
export default {
data() {
return {
priceSlice: {
}
};
},
components: {},
props: {
firstSize: {
type: [String, Number],
default: 28
},
secondSize: {
type: [String, Number],
default: 28
},
color: {
type: String
},
weight: {
type:[String, Number] ,
default: 400
},
price: {
type: [String, Number],
default: ""
},
showSubscript: {
type: Boolean,
default: true
},
subscriptSize: {
type: [String, Number],
default: 28
},
lineThrough: {
type: Boolean,
default: false
}
},
created() {
this.priceFormat()
},
watch: {
price(val) {
this.priceFormat()
}
},
methods: {
priceFormat() {
let {
price
} = this;
let priceSlice = {}
if(price !== null && price !== '' && price !== undefined) {
price = parseFloat(price);
price = String(price).split('.');
priceSlice.first = price[0];
priceSlice.second = price[1];
this.priceSlice = priceSlice
}
}
}
};
</script>
<style>
.price-format {
font-family: Avenir, SourceHanSansCN, PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei, sans-serif;
}
</style>
+117
View File
@@ -0,0 +1,117 @@
<template>
<view>
<u-popup
v-model="value"
mode="bottom"
zIndex="9999"
border-radius="14"
:maskCloseAble="false"
safeAreaInsetBottom
>
<view class="privacy-containter" v-if="appConfig">
<view class="privacy-containter-tital"
>{{ appConfig.name || "" }}隐私政策</view
>
<view class="privacy-containter-tip"
>欢迎你使用{{
appConfig.name || ""
}}请仔细阅读以下内容并作出适当的选择</view
>
<view class="privacy-containter-outline">隐私政策概要</view>
<view class=""
>主要说明我们所处理的信息种类方式和目的你所享有的权益第三方插件信息等</view
>
<view class="privacy-containter-read primary" @click="handleOpen"
>点击阅读隐私政策小程序完整版 ></view
>
<button
class="btn-comfirm"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
同意并继续
</button>
<button class="btn-cancel" @click="handlecancel">取消</button>
</view>
</u-popup>
</view>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
props: {
value: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
computed: {
...mapGetters(["appConfig"]),
},
methods: {
handleOpen() {
wx.openPrivacyContract({
success: (res) => {
console.log(res);
},
fail: (err) => {
console.log(err);
},
});
},
handlecancel() {
this.$toast({
title: "须同意后才可继续使用",
});
},
handleAgreePrivacyAuthorization() {
this.$emit("input", false);
uni.showTabBar();
// if (this.appConfig.is_open_nearby) {
// // this.initLocationFunc();
// }
},
},
};
</script>
<style lang="scss">
.privacy-containter {
background-color: white;
height: 750rpx;
padding: 42rpx;
&-tital {
text-align: center;
font-weight: 500;
font-size: 36rpx;
}
&-tip {
margin: 50rpx 0;
}
&-outline {
font-weight: 500;
margin-bottom: 15rpx;
}
&-read {
margin-top: 15rpx;
}
.btn-comfirm {
background-color: $-color-primary;
color: white;
height: 80rpx;
line-height: 80rpx;
margin-top: 50rpx;
}
.btn-cancel {
background-color: #f7f7f7;
height: 80rpx;
line-height: 80rpx;
margin-top: 30rpx;
}
}
</style>
@@ -0,0 +1,283 @@
<template>
<view class="swiper-wrap">
<swiper
class="swiper"
ref="swiper"
:autoplay="autoplay"
:circular="circular"
:interval="interval"
:duration="duration"
@change="swiperChange"
>
<block v-for="(item, index) in urls" :key="index">
<swiper-item @tap="previewImage(index)">
<view v-if="item.type == 'video'" class="video-wrap">
<!-- #ifdef H5 || MP-WEIXIN -->
<video
id="myVideo"
class="my-video"
:enable-progress-gesture="false"
:controls="showControls"
:show-progress="true"
:show-fullscreen-btn="showControls"
:src="item.url"
:show-center-play-btn="false"
:muted="false"
show-mute-btn="true"
@error="videoErrorCallback"
:show-play-btn="showControls"
@click="play"
@ended="playEnd"
@fullscreenchange="fullscreenchange"
></video>
<image
v-show="showPlay"
@tap.stop="play"
src="/static/images/icon_play.png"
class="icon-play"
>
</image>
<!-- <image v-show="showPlay" style="right: 50rpx;bottom: 50rpx;" @tap.stop="play" src="/static/images/all.png" class="icon-play">
</image> -->
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<j-video
:url="item.url"
height="750rpx"
width="750rpx"
:poster="urls[1].url"
></j-video>
<!-- #endif -->
</view>
<u-image
v-else
width="750rpx"
height="750rpx"
:src="item.url"
mode="aspectFill"
></u-image>
</swiper-item>
</block>
</swiper>
<view class="dots black sm bg-white br60" id="bottom"
>{{ currentSwiper + 1 }}/{{ urls.length }}</view
>
</view>
</template>
<script>
var app = getApp();
export default {
data() {
return {
currentSwiper: 0,
urls: [],
showPlay: true,
showControls: false,
autoplay: true,
start: 0,
};
},
components: {},
props: {
imgUrls: {
type: Array,
default: () => [],
},
circular: {
type: Boolean,
default: true,
},
interval: {
type: Number,
default: 3000,
},
duration: {
type: Number,
default: 500,
},
video: {
type: String,
},
isShow: {
type: Boolean,
default: false,
},
},
watch: {
imgUrls: {
handler(val) {
this.urls = val.map((item) => {
return {
url: item.uri,
type: "image",
};
});
if (this.video) {
this.urls.unshift({
url: this.video,
type: "video",
});
this.autoplay = false;
this.$nextTick(() => {
this.videoContext = uni.createVideoContext("myVideo", this);
this.videoContexts = uni.createVideoContext("videos", this);
});
}
},
immediate: true,
},
isShow: {
handler(val) {
if (val == true && !this.showPlay) {
this.videoContext.stop();
} else if (val == false && this.showPlay == false) {
this.videoContext.play();
}
},
},
},
methods: {
swiperChange(e) {
this.currentSwiper = e.detail.current;
if (e.detail.current !== 0 && this.video != "") {
try {
this.showPlay = true;
this.videoContext.stop();
this.videoContexts.stop();
} catch (error) {
console.log("err==>", err);
}
}
},
videoErrorCallback(err) {
console.log("err==>", err);
},
previewImage(current) {
let index = current;
if (current == 0 && this.video) {
return false;
}
// #ifdef MP-WEIXIN
wx.previewMedia({
current,
indicator: "default",
sources: this.urls,
});
//#endif
// #ifdef H5 || APP-PLUS
if (this.video) {
index = current - 1;
}
if (this.urls[current].type == "video") {
this.videoContext.requestFullScreen();
} else {
uni.previewImage({
indicator: "default",
index,
urls: this.imgUrls.map((item) => item.uri),
});
}
//#endif
},
play() {
if (this.start == 0) {
this.start = 1;
this.showPlay = false;
this.videoContext.play();
} else {
this.start = 0;
this.showPlay = true;
this.videoContext.stop();
}
},
playEnd() {
this.start = 0;
this.showPlay = true;
},
fullscreenchange(e) {
const { fullScreen } = e.detail;
!fullScreen && this.videoContext.stop();
this.showControls = fullScreen ? true : false;
this.showPlay = fullScreen ? false : true;
},
},
};
</script>
<style lang="scss">
.swiper-wrap {
width: 100%;
height: 750rpx;
position: relative;
.close {
width: 250rpx;
height: 250rpx;
top: 230rpx;
right: 50rpx;
z-index: 1000;
position: relative;
position: fixed;
.close-item {
top: 0rpx;
right: 0rpx;
position: absolute;
z-index: 1001;
}
}
}
.swiper-wrap .swiper {
width: 100%;
height: 100%;
position: relative;
}
.swiper-wrap .swiper .slide-image {
width: 100%;
height: 100%;
}
.swiper-wrap .dots {
position: absolute;
right: 24rpx;
bottom: 24rpx;
display: flex;
height: 34rpx;
padding: 0 15rpx;
}
.swiper-wrap .video-wrap {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.swiper-wrap .my-video {
width: 100%;
height: 100%;
}
.swiper-wrap .icon-play {
width: 90rpx;
height: 90rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999;
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<!--components/recommend/recommend.wxml-->
<view class="recommend" v-if="goodsList.length">
<!-- <view class="header row-center">
<image class="title" src="/images/recommend_title.png" />
</view> -->
<view class="goods-title row-center">
<text class="line"></text>
<view class="row">
<image class="mr10" src="/static/images/icon_like.png"></image>
<text class="bold xxl">好物优选</text>
</view>
<text class="line"></text>
</view>
<goods-list :list="goodsList"></goods-list>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | likeshop开源商城系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | gitee下载:https://gitee.com/likeshop_gitee
// | github下载:https://github.com/likeshop-github
// | 访问官网:https://www.likeshop.cn
// | 访问社区:https://home.likeshop.cn
// | 访问手册:http://doc.likeshop.cn
// | 微信公众号:likeshop技术社区
// | likeshop系列产品在gitee、github等公开渠道开源版本可免费商用,未经许可不能去除前后端官方版权标识
// | likeshop系列产品收费版本务必购买商业授权,购买去版权授权后,方可去除前后端官方版权标识
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | likeshop团队版权所有并拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
import { getBestList } from '../../api/store';
import goodsList from "../goods-list/goods-list";
export default {
data() {
return {
goodsList: []
};
},
components: {
goodsList
},
props: {},
beforeMount: function () {
this.getBestListFun();
},
destroyed: function () {// 在组件实例被从页面节点树移除时执行
},
methods: {
getBestListFun() {
getBestList({
page_no: 1,
page_size: 6
}).then(res => {
if (res.code == 1) {
this.goodsList = res.data.list
}
});
}
}
};
</script>
<style>
/* components/recommend/recommend.wxss */
.recommend {
padding-bottom: 50rpx;
}
.recommend .header .title {
width: 468rpx;
height: 45rpx;
margin: 10rpx 0;
}
.recommend .goods-title {
height: 100rpx;
}
.recommend .goods-title .line {
width: 58rpx;
height: 1px;
background-color: #ccc;
margin: 0 22rpx;
}
.recommend .goods-title image {
width: 38rpx;
height: 38rpx;
}
</style>
+183
View File
@@ -0,0 +1,183 @@
<template>
<view>
<u-modal
:value="showTips"
ref="uModal"
show-cancel-button
:confirm-color="primaryColor"
async-close
confirm-text="前往绑定"
@confirm="toSetting"
@cancel="goBack"
>
<view class="slot-content row-center" style="padding: 40rpx">
您暂未绑定手机号请先绑定
</view>
</u-modal>
<u-modal
ref="uModalSet"
:value="!showTips && showSetPwd"
title="设置转账密码"
show-cancel-button
:confirm-color="primaryColor"
async-close
confirm-text="确定"
@confirm="onSetPwd"
@cancel="goBack"
>
<view class="slot-content" style="padding: 40rpx">
<view style="font-size: 42rpx">{{ mobile }}</view>
<view class="mt20">
<u-input
type="password"
v-model="setPwd"
:border="true"
placeholder="请设置您的转账密码"
style="width: 100%"
/>
</view>
<view class="mt20">
<u-input
type="password"
v-model="comfirmPwd"
:border="true"
placeholder="再次确认转账密码"
style="width: 100%"
/>
</view>
</view>
</u-modal>
<u-modal
ref="uModalInput"
:value="showConfirm"
show-cancel-button
:confirm-color="primaryColor"
async-close
confirm-text="确定"
@confirm="onConfirm"
@cancel="toSetting"
cancel-text="忘记密码"
title="请输入密码"
>
<view class="slot-content row-center" style="padding: 40rpx">
<u-icon class="icon-close" name="close" @click="showInputPwd"></u-icon>
<u-input
type="password"
v-model="pwd"
:border="true"
placeholder="请输入您的转账密码"
style="width: 100%"
/>
</view>
</u-modal>
</view>
</template>
<script>
import { hasPayPassword, setPassword } from '@/api/user'
import { mapGetters } from 'vuex'
export default {
name: 'set-pay-pwd',
data() {
return {
showSetPwd: false,
showConfirm: false,
pwd: '',
setPwd: '',
comfirmPwd: ''
}
},
methods: {
onSetPwd() {
let { setPwd, comfirmPwd } = this
this.$refs.uModalSet.clearLoading()
if (!setPwd) {
this.$toast({
title: '请填写转账密码'
})
return
}
if (!comfirmPwd) {
this.$toast({
title: '请填写确认密码'
})
return
}
if (setPwd != comfirmPwd) {
this.$toast({
title: '两次密码输入不一致'
})
return
}
if (setPwd.length < 4 || setPwd.length > 8) {
this.$toast({
title: '请输入长度为4-8位的密码'
})
return
}
let data = {
pay_password: setPwd
}
setPassword(data).then((res) => {
if (res.code == 1) {
this.$toast({
title: res.msg
})
this.showSetPwd = false
}
})
},
goBack() {
uni.navigateBack()
},
toSetting() {
uni.navigateTo({
url: '/bundle/pages/user_profile/user_profile'
})
this.$refs.uModal.clearLoading()
},
hasPayWord() {
console.log(11)
hasPayPassword().then((res) => {
console.log(res)
if (res.code == 0) {
this.showSetPwd = true
}
})
},
onConfirm() {
this.$refs.uModalInput.clearLoading()
this.$emit('confirm', this.pwd)
},
showInputPwd() {
setTimeout(() => {
this.pwd = ''
}, 1000)
this.showConfirm = !this.showConfirm
}
},
computed: {
...mapGetters(['userInfo']),
mobile() {
if (this.userInfo.mobile) {
return this.userInfo.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
},
showTips() {
console.log(this.userInfo)
return Boolean(!this.userInfo.mobile)
}
}
}
</script>
<style lang="scss" scoped>
.icon-close {
position: absolute;
top: 30rpx;
right: 30rpx;
}
</style>
+155
View File
@@ -0,0 +1,155 @@
<template>
<view>
<l-painter
css="width: 640rpx; padding-bottom: 35rpx; background-color: #ffffff; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15);border-radius: 10rpx;"
isCanvasToTempFilePath @success="handleSuccess" @fail="handleFail" custom-style="position: fixed; left: 200%">
<l-painter-view css="padding-left: 40rpx;">
<l-painter-image :src="config.avatar"
css="margin-top: 15rpx; width: 72rpx; height: 72rpx; border-radius: 50%;" />
<l-painter-view css="margin-top: 30rpx; padding-left: 20rpx; display: inline-block">
<l-painter-text :text="`来自${config.nickname}的分享`"
css="display: block; padding-bottom: 10rpx; color: #333333; font-size: 28rpx;line-clamp:1;width: 100%;" />
</l-painter-view>
<l-painter-image :src="config.image"
css="object-fit: cover; object-position: center; width: 560rpx; height: 560rpx;margin-top: 15rpx;" />
<l-painter-view css="margin-top: 30rpx;">
<!-- 商品分享海报文字内容 -->
<l-painter-view :css="`display: ${type == 1 ? 'inline-block' : 'none'}; width: 400rpx;`">
<l-painter-view
:css="`vertical-align: bottom; color: #FF2C3C; font-size: 30rpx; line-height: 1em;`">
<l-painter-text text="¥" css="vertical-align: bottom;font-size: 28rpx;" />
<l-painter-text :text="price.prev" css="vertical-align: bottom; font-size: 38rpx;" />
<l-painter-text :text="price.next" css="vertical-align: bottom; font-size: 30rpx;" />
<l-painter-text :text="marketPrice"
css="vertical-align: bottom; padding-left: 10rpx;font-size: 28rpx; font-weight: normal; text-decoration: line-through; color: #999999" />
</l-painter-view>
<l-painter-view css="margin-top:30rpx;">
<l-painter-text
css="line-clamp: 2; color: #333333; line-height: 1.5em;font-size: 30rpx; width: 378rpx; padding-right:22rpx; box-sizing: border-box"
:text="config.name"></l-painter-text>
</l-painter-view>
</l-painter-view>
<!-- 砍价分享海报文字内容 -->
<l-painter-view :css="`display: ${type == 2 ? 'inline-block' : 'none'}; width: 400rpx;`">
<l-painter-view>
<l-painter-text
:css="`line-clamp: 2; color: #FF2C3C; line-height: 1.5em;font-size: 32rpx; width: 375rpx; padding-right:22rpx; box-sizing: border-box`"
:text="bShareTitle"></l-painter-text>
</l-painter-view>
<l-painter-view css="margin-top:8rpx;">
<l-painter-text
css="line-clamp: 2; color: #F95F2F; line-height: 1.5em;font-size: 24rpx; width: 378rpx; padding-right:22rpx; box-sizing: border-box"
:text="bShareIntro"></l-painter-text>
</l-painter-view>
<l-painter-view css="margin-top:8rpx;">
<l-painter-text
css="line-clamp: 2; color: #333333; line-height: 1.5em;font-size: 28rpx; width: 378rpx; padding-right:22rpx; box-sizing: border-box"
:text="config.name"></l-painter-text>
</l-painter-view>
</l-painter-view>
<l-painter-view css="display: inline-block;">
<!-- #ifdef H5 || APP-PLUS -->
<l-painter-qrcode css="width: 168rpx; height: 168rpx;" :text="link">
</l-painter-qrcode>
<!-- #endif -->
<!-- #ifdef MP -->
<l-painter-image :src="qrcode" css="width: 168rpx; height: 168rpx;" />
<!-- #endif -->
<l-painter-text text="长按识别二维码"
css="display: block; padding-top: 10rpx; color: #999999;font-size: 24rpx;" />
</l-painter-view>
</l-painter-view>
</l-painter-view>
</l-painter>
</view>
</template>
<script>
import lPainter from '@/components/lime-painter/components/l-painter/l-painter.vue'
import lPainterImage from '@/components/lime-painter/components/l-painter-image/l-painter-image.vue'
import lPainterText from '@/components/lime-painter/components/l-painter-text/l-painter-text.vue'
import lPainterView from '@/components/lime-painter/components/l-painter-view/l-painter-view.vue'
import lPainterQrcode from '@/components/lime-painter/components/l-painter-qrcode/l-painter-qrcode.vue'
export default {
name: "share-poster",
components: {
lPainter,
lPainterImage,
lPainterText,
lPainterView,
lPainterQrcode
},
props: {
config: { // 相关配置
type: Object,
default: () => ({})
},
shareId: { // 分享id
type: [Number, String],
default: ''
},
qrcode: { // 二维码数据
type: [String],
default: ''
},
// pagePath:{
// type: String,
// default: ''
// },
link: {
type: String,
default: ''
},
type: { // 生成海报需使用 0-会员分享海报 1-商品详情 2-砍价活动
type: [String, Number],
default: 1,
},
bShareTitle: { // 分享海报标题
type: String,
default: '我正在参与砍价 还差一步',
},
bShareIntro: { // 分享海报简介
type: String,
default: '帮忙砍一刀',
}
},
data() {
return {
};
},
computed: {
price() {
let {
price
} = this.config
if (price == undefined) return {}
price = String(parseFloat(price)).split('.')
return {
prev: price[0],
next: price[1] ? `.${price[1]}` : ''
}
},
marketPrice() {
return `${parseFloat(this.config.marketPrice)}`
}
},
methods: {
handleSuccess(val) {
this.$emit('success', val)
},
handleFail() {
this.$emit('fail')
}
}
}
</script>
<style>
</style>
+279
View File
@@ -0,0 +1,279 @@
<template>
<view class="">
<u-popup class="share-popup" v-model="showshare" mode="bottom" border-radius="14" :closeable="true"
:safe-area-inset-bottom="true" :mask-close-able="false">
<view class="row row-center md bold mt30 mb30">分享至</view>
<view class="row row-around share-tab">
<view class="column column-center" @tap="getPoster">
<image mode="widthFix" class="share-icon" src="/static/images/icon_generate_poster.png"></image>
<view class="" style="margin: 15rpx 0;">生成海报</view>
</view>
<!-- #ifdef MP-WEIXIN-->
<button open-type="share" class="column column-center" hover-class="none">
<image class="share-icon" src="/static/images/icon_wechat.png"></image>
<view class="">微信好友</view>
</button>
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<view oclass="column column-center" @tap="shareWx">
<image class="share-icon" src="/static/images/icon_wechat.png"></image>
<view class="">微信好友</view>
</view>
<!-- #endif -->
</view>
<view class="row row-center bg-body cancel xl" @tap="showshare=false">取消</view>
</u-popup>
<u-popup class="share-poster" v-model="showPoster" mode="center" :closeable="true"
:safe-area-inset-bottom="true">
<!-- #ifndef H5 -->
<image style="width: 640rpx;" mode="widthFix" :src="poster"></image>
<!-- #endif -->
<!-- #ifdef H5 -->
<img style="width: 640rpx;" :src="poster" />
<!-- #endif -->
<button class="row row-center save-btn" size="lg" @tap="savePoster">
<!-- #ifndef H5 -->
保存图片到相册
<!-- #endif -->
<!-- #ifdef H5 -->
长按保存图片到相册
<!-- #endif -->
</button>
</u-popup>
<!-- #ifdef H5 -->
<u-popup :custom-style="{'background': 'none'}" class="share-tips" v-model="showTips" mode="top">
<view style="overflow: hidden;">
<image src="/static/images/share_arrow.png" class="share-arrow" />
<view class="white" style="text-align: center;margin-top: 280rpx;">
<view class="bold lg">立即分享给好友吧</view>
<view class="sm m-t-10">点击屏幕右上角将本页面分享给好友</view>
</view>
</view>
</u-popup>
<!-- #endif -->
<poster v-if="enablePoster" :type="type" :share-id="shareId" :config="config"
:qrcode="mnpQrcode" :link="getLink" @success="handleSuccess" @fail="handleFail"
:b-share-title="bargainShare.share_title" :b-share-intro="bargainShare.share_intro"/>
</view>
</template>
<script>
import {
mapGetters,
} from 'vuex'
import {
// apiMnpQrCode,
getShareMnQrcode
} from "@/api/app"
import {
baseURL,
basePath
} from '@/config/app'
import poster from './poster.vue'
// import {TtAppNameEnum} from '@/utils/enum'
export default {
components: {
poster
},
props: {
value: {
type: Boolean // 是否开启此弹窗
},
shareId: {
type: [String, Number], // 商品id或其他活动id
default: ''
},
config: { // 各种配置
type: Object,
default: () => ({})
},
pagePath: { // 跳转路径
type: String,
default: ''
},
type: { // 生成海报需使用 0-会员分享海报 1-商品详情 2-砍价活动
type: [String, Number],
default: 1
}
},
data() {
return {
poster: "",
enablePoster: false,
showPoster: false,
showTips: false,
mnpQrcode: '',
bargainShare: { // 砍价分享title和intro
share_title: '',
share_intro: '',
},
};
},
computed: {
getLink() {
return `${baseURL}${basePath}/${this.pagePath}?id=${this.shareId}&invite_code=${this.$store.getters.inviteCode}`
},
showshare: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
watch: {
showPoster(val) {
if (!val) {
this.enablePoster = false
}
}
},
methods: {
async getPoster() {
if (!this.isLogin) {
return uni.navigateTo({
url: '/pages/login/login'
});
}
uni.showLoading({
title: '正在生成中'
})
// #ifdef MP-WEIXIN
if (!this.mnpQrcode) {
// #ifdef MP-WEIXIN
const res = await this.getMnpQrcode()
// #endif
this.mnpQrcode = res.data.qr_code.replaceAll("\r\n", "")
if(this.type == 2) {
this.bargainShare = res.data.extra
}
}
this.enablePoster = true
// #endif
// #ifdef APP-PLUS || H5
this.enablePoster = true
// #endif
},
// 获取商品页面二维码数据
getMnpQrcode() {
return new Promise((resolve, reject) => {
getShareMnQrcode({
id: this.shareId, // 商品id或其他活动id
url: this.pagePath, // 跳转页面路径
type: this.type, // 0-会员分享海报 1-商品详情 2-砍价活动
}).then((res) => {
console.log('shareRes', res)
resolve(res)
}).catch(() => {
reject()
})
})
// return new Promise((resolve, reject) => {
// resolve({qr_code: 'https://likeshop.yixiangonline.com/uploads/images/background/20201209/17ca8666f3122f0ea83801a7c333b58f.png'})
// })
},
handleSuccess(val) {
this.poster = val
uni.hideLoading()
this.showPoster = true
this.showshare = false
},
handleFail() {
uni.hideLoading({
success: () => {
this.$toast({
title: '生成失败'
})
}
})
},
shareWx() {
// #ifdef H5
this.showTips = true
this.showshare = false
// #endif
// #ifdef APP-PLUS
uni.share({
provider: "weixin",
scene: "WXSceneSession",
type: 0,
href: this.getLink,
title: this.config.name,
summary: '',
imageUrl: this.config.image,
success: (res) => {
console.log('分享成功');
},
fail: (err) => {
this.$toast({
title: err.errMsg
})
}
});
// #endif
},
async savePoster() {
uni.saveImageToPhotosAlbum({
filePath: this.poster,
success: res => {
this.showPoster = false
this.$toast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
this.$toast({
title: '保存失败'
});
console.log(err)
}
})
}
}
}
</script>
<style lang="scss">
.share-popup {
.share-tab {
margin: 40rpx 0 140rpx;
.share-icon {
width: 100rpx;
height: 100rpx;
}
}
.cancel {
height: 98rpx;
}
}
.share-poster {
.share-img {
width: 640rpx;
border-radius: 12rpx;
}
.save-btn {
// @include background_color();
background-color: $-color-primary;
color: #fff;
margin-top: 20rpx;
}
}
.share-tips .share-arrow {
width: 140rpx;
height: 250rpx;
float: right;
margin: 15rpx 31rpx 0 0;
}
</style>
+493
View File
@@ -0,0 +1,493 @@
<template>
<u-popup
v-model="showPop"
mode="bottom"
border-radius="14"
:closeable="true"
@close="onClose"
:safe-area-inset-bottom="true"
>
<view class="bg-white spec-contain">
<view class="spec-header row">
<custom-image
class="goods-img mr20"
radius="10rpx"
@tap="previewImage(checkedGoods.image)"
:src="checkedGoods.image"
></custom-image>
<view class="goods-info" v-if="!isBargain">
<view class="primary row">
<price-format
:first-size="46"
:second-size="32"
:subscript-size="32"
:price="group ? checkedGoods.team_price : checkedGoods.price"
:weight="500"
></price-format>
<view
class="vip-price row"
v-if="!group && !isSeckill && checkedGoods.member_price"
>
<view class="price-name xxs">会员价</view>
<view style="padding: 0 11rpx">
<price-format
:price="checkedGoods.member_price"
:first-size="22"
:second-size="22"
:subscript-size="22"
:weight="500"
color="#7B3200"
></price-format>
</view>
</view>
</view>
<view class="sm" v-show="showStock">
库存{{ checkedGoods.stock }}
</view>
<view class="sm">
<!-- <text>已选择{{checkedGoods.spec_value_str}}{{goodsNum}}</text> -->
<text>{{ specValueText }}</text>
</view>
</view>
<view class="goods-info" v-else>
<view class="xs">
最低可砍至<text class="sm" style="color: #f95f2f"
>¥{{ checkedGoods.activity_price }}</text
>
</view>
<view class="muted xs mt10"> 原价 ¥{{ checkedGoods.price }} </view>
<view class="sm lighter mt20">
<!-- <text>已选择{{checkedGoods.spec_value_str}}</text> -->
<text>{{ specValueText }}</text>
</view>
</view>
</view>
<view class="spec-main">
<scroll-view style="max-height: 600rpx" scroll-y="true">
<view class="spec-list">
<view v-for="(item, index) in specList" :key="index" class="spec">
<view class="name mb20">
{{ item.name }}
<text class="primary xs ml20">{{
checkedGoods.spec_value_ids_arr[index] == ""
? "请选择" + item.name
: ""
}}</text>
</view>
<view class="row wrap">
<!-- <view v-for="(specitem, index2) in item.spec_value" :key="index2" :class="'spec-item sm ' + ( specitem.checked ? 'checked' : '' )"
@tap="choseSpecItem(item.id, specitem.id)">
{{ specitem.value }}
</view> -->
<view
v-for="(specitem, index2) in item.spec_value"
:key="index2"
:class="
'spec-item sm ' +
(checkedGoods.spec_value_ids_arr[index] == specitem.id
? 'checked'
: '') +
(isDisable(specitem.id) ? 'disabled' : '')
"
@click="choseSpecItem(index, index2)"
>
{{ specitem.value }}
</view>
</view>
</view>
</view>
</scroll-view>
<view class="good-num row-between ml20 mr20">
<view class="label">数量</view>
<u-number-box
v-model="goodsNum"
:min="1"
:max="checkedGoods.stock"
:disabled="disabledNumberBox"
></u-number-box>
</view>
</view>
<view
class="btns row-between bg-white"
:class="
specValueText.indexOf('请选择') != -1 || checkedGoods.stock == 0
? 'disabled'
: ''
"
>
<button
v-if="showAdd"
class="add-cart br60 white btn"
size="lg"
@tap="onClick('addcart')"
>
{{ yellowBtnText }}
</button>
<button
v-if="showBuy"
class="bg-primary br60 white btn"
size="lg"
@tap="onClick('buynow')"
>
{{ redBtnText }}
</button>
<button
v-if="showConfirm"
class="bg-primary br60 white btn"
size="lg"
@tap="onClick('confirm')"
>
确定
</button>
</view>
</view>
</u-popup>
</template>
<script>
export default {
data() {
return {
checkedGoods: {
stock: 0,
}, //
outOfStock: [], //
specList: [], //
disable: [], //
goodsNum: 1,
showPop: false,
};
},
components: {},
props: {
show: {
type: Boolean,
},
goods: {
type: Object,
},
showAdd: {
type: Boolean,
default: true,
},
showBuy: {
type: Boolean,
default: true,
},
showConfirm: {
type: Boolean,
default: false,
},
redBtnText: {
type: String,
default: "立即购买",
},
yellowBtnText: {
type: String,
default: "加入购物车",
},
group: {
type: Boolean,
default: false,
},
isSeckill: {
type: Boolean,
default: false,
},
showStock: {
type: Boolean,
default: true,
},
disabledNumberBox: {
type: Boolean,
default: false,
},
isBargain: {
type: Boolean,
default: false,
},
goodsType: {
type: Number,
},
},
mounted() {},
computed: {
//
specValueText() {
let arr = this.checkedGoods.spec_value_ids?.split(",");
let spec_str = "";
if (arr)
arr.forEach((item, index) => {
if (item == "") spec_str += this.specList[index].name + ",";
});
if (this.checkedGoods?.stock != 0 && spec_str == "")
return `已选择 ${this.checkedGoods.spec_value_str} ${this.goodsNum}`;
else return `请选择 ${spec_str.slice(0, spec_str.length - 1)}`;
},
},
watch: {
goods(value) {
this.specList = value.goods_spec || [];
let goodsItem = value.goods_item || [];
this.outOfStock = goodsItem.filter((item) => item.stock == 0);
// 0
const resultArr = goodsItem.filter((item) => item.stock != 0);
if (resultArr.length != 0) {
resultArr[0].spec_value_ids_arr =
resultArr[0].spec_value_ids.split(",");
this.checkedGoods = resultArr[0];
} else {
//
goodsItem[0].spec_value_ids_arr = [];
this.disable = goodsItem.map((item) => item.spec_value_ids.split(","));
this.checkedGoods = goodsItem[0];
}
},
specList(value) {
if (this.checkedGoods.stock == 0) return;
const res = this.goods.goods_item.filter((item) => {
return this.checkedGoods.spec_value_ids === item.spec_value_ids;
});
// 0
const idsArr = this.checkedGoods.spec_value_ids_arr;
const outOfStock = this.outOfStock;
//
const getArrGather = this.getArrResult(idsArr, outOfStock);
//
this.disable = this.getOutOfStockArr(getArrGather, idsArr);
if (res.length != 0) {
console.log(res, "-----");
let result = JSON.parse(JSON.stringify(res[0]));
result.spec_value_ids_arr = result.spec_value_ids.split(",");
if (this.goodsNum > result.stock) {
this.goodsNum = result.stock;
}
this.checkedGoods = result;
//
this.$emit("change", {
detail: this.checkedGoods,
});
}
},
show(val) {
this.showPop = val;
},
},
created() {
console.log("spec");
},
methods: {
isDisable(e) {
const res = this.disable.filter((item) => item == e);
if (res.length != 0) return true;
else return false;
},
onClose() {
this.$emit("close");
},
onClick(type) {
let { checkedGoods, goodsNum } = this;
if (this.specValueText.indexOf("请选择") != -1)
return this.$toast({
title: this.specValueText,
});
if (checkedGoods.stock == 0)
return this.$toast({
title: "当前选择库存不足",
});
checkedGoods.goodsNum = goodsNum;
this.$emit(type, {
detail: checkedGoods,
});
},
//
choseSpecItem(index, index2) {
const id = this.specList[index].spec_value[index2].id;
//
const disable = this.disable.filter((item) => item == id);
if (disable.length != 0) return;
let idsArr = this.checkedGoods.spec_value_ids_arr;
if (id == idsArr[index]) idsArr[index] = "";
else idsArr[index] = id;
//
this.checkedGoods.spec_value_ids_arr = idsArr;
this.checkedGoods.spec_value_ids = idsArr.join(",");
//
this.specList = [...this.specList];
},
//
getOutOfStockArr(arr, arr1, result = []) {
arr.forEach((el) => {
if (el.num >= arr1.length - 1) {
result.push(...el.different);
}
});
return result;
},
//
getArrIdentical(arr1, arr2, arr = [], num = 0) {
arr1.forEach((el) => {
arr2.forEach((el2) => {
if (el == el2) {
num += 1;
arr.push(el);
}
});
});
return {
num, //n
different: this.getArrDifference(
[...new Set(arr)].map(Number),
arr2.map(Number)
),
identical: [...new Set(arr)],
};
},
//
getArrResult(arr, outOfStock, result = []) {
outOfStock.forEach((item) => {
const res = this.getArrIdentical(arr, item.spec_value_ids.split(","));
if (res != undefined && res.length != 0) {
result.push(res);
}
});
return result;
},
//
getArrDifference(arr1, arr2) {
return arr1.concat(arr2).filter(function (v, i, arr) {
return arr.indexOf(v) == arr.lastIndexOf(v);
});
},
//
previewImage(current) {
uni.previewImage({
current,
urls: [current], // http
});
},
},
};
</script>
<style lang="scss" scoped>
.spec-contain {
border-radius: 10rpx 10rpx 0 0;
overflow: hidden;
position: relative;
.close {
position: absolute;
right: 10rpx;
top: 10rpx;
}
.spec-header {
padding: 30rpx 20rpx;
padding-right: 70rpx;
align-items: flex-start;
border: $-solid-border;
.vip-price {
margin: 0 24rpx;
background-color: #ffe9ba;
line-height: 36rpx;
border-radius: 6rpx;
overflow: hidden;
.price-name {
background-color: #101010;
padding: 3rpx 12rpx;
color: #ffd4b7;
position: relative;
overflow: hidden;
&::after {
content: "";
display: block;
width: 20rpx;
height: 20rpx;
position: absolute;
right: -15rpx;
background-color: #ffe9ba;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
}
}
}
.goods-img {
width: 180rpx;
height: 180rpx;
flex: none;
}
}
.spec-main {
.spec-list {
padding: 30rpx 20rpx;
.spec-item {
line-height: 52rpx;
padding: 0 20rpx;
background-color: #f4f4f4;
border-radius: 60rpx;
margin: 0 20rpx 20rpx 0;
border: 1px solid #f4f4f4;
// &.checked {
// background-color: $-color-primary;
// color: #fff;
// }
&.checked {
background-color: #ffe9eb;
color: $-color-primary;
border-color: $-color-primary;
}
&.disabled {
opacity: 0.5;
text-decoration: line-through;
}
}
}
}
//
.disabled {
opacity: 0.4;
}
.btns {
height: 120rpx;
padding: 0 30rpx;
margin-top: 80rpx;
.add-cart {
background-color: #ff9e1e;
}
.btn {
margin: 0 10rpx;
flex: 1;
}
}
}
</style>
+200
View File
@@ -0,0 +1,200 @@
<template>
<view class="spread-order">
<view class="content">
<view class="order-container">
<view v-for="(item, index) in lists" :key="item.order_sn" class="order-item bg-white mt20">
<view class="order-header row-between">
<view>订单编号:{{item.order_sn}}</view>
<view class="white guide-shop-btn row-center">{{item.status}}</view>
</view>
<view class="order-content row">
<view class="order-goods-img">
<u-image width="100%" height="100%" radius="6px" :src="item.image" />
</view>
<view class="order-goods-info ml20">
<view class="name row sm">{{item.goods_name}}</view>
<view class="row-between">
<view class="xs">数量<text class="normal nr">{{item.goods_num}}</text></view>
<view class="xs">
付款金额
<price-format showSubscript :subScriptSize="28" :firstSize="28" :secondSize="28" weight="bold" :price="item.pay_price" />
</view>
</view>
<view class="pre-income muted">预估收益
<price-format showSubscript :subScriptSize="28" :firstSize="28" :secondSize="28" color="#FF2C3C" weight="bold" :price="item.money" />
</view>
</view>
</view>
<view class="order-footer row-between">
<view class="time muted sm">{{item.create_time}}</view>
<view class="static sm" :style="{color: item.status == '待返佣' ? '#FF2C3C' : item.status == '已失效' ? '#909090' : '#00c735'}">{{item.status}}</view>
</view>
</view>
<loading-footer slotEmpty :status="loadingStatus">
<view slot="empty" class="data-null column-center">
<image class="img-null" src="/static/images/news_null.png"></image>
<text class="sm muted">暂无其他评价</text>
</view>
</loading-footer>
</view>
</view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | likeshop
// +----------------------------------------------------------------------
// |
// | giteehttps://gitee.com/likeshop_gitee
// | githubhttps://github.com/likeshop-github
// | 访https://www.likeshop.cn
// | 访https://home.likeshop.cn
// | 访http://doc.likeshop.cn
// | likeshop
// | likeshopgiteegithub
// | likeshop
// |
// | likeshop
// +----------------------------------------------------------------------
// | author: likeshop.cn.team
// +----------------------------------------------------------------------
import { userOrderPromoteOrder } from "@/utils/type";
import { loadingType } from "@/utils/type";
import { getPromoteOrder } from "@/api/user";
import {loadingFun} from '@/utils/tools'
export default {
data() {
return {
lists: [],
page: 1,
loadingStatus: loadingType.LOADING,
confirmDialog: false
};
},
components: {
},
props: {
type: {
type: Number | String,
default: userOrderPromoteOrder.ALL
}
},
created() {
this.$on('RESET_LIST', this.reflesh, this);
},
beforeMount() {
this.getPromoteOrderFun(); // this.$getAfterSaleList()
},
methods: {
reflesh() {
this.page = 1;
this.lists = [];
this.loadingStatus = loadingType.LOADING; // this.$getAfterSaleList();
},
getPromoteOrderFun() {
let {
loadingStatus,
lists,
page
} = this;
loadingFun(getPromoteOrder, page, lists, loadingStatus, {status: this.type}).then(res => {
if(res) {
this.page = res.page;
this.lists = res.dataList
this.loadingStatus = res.status
}
})
}
}
};
</script>
<style lang="scss">
.spread-order {
.spread-header {
height: 240rpx;
background-color: var(--primary-color);
padding-top: 60rpx;
.deal-num {
flex: 1;
align-self: flex-start;
.num {
font-size: 42rpx;
}
.explain {
line-height: 34rpx;
margin-top: 16rpx;
}
}
.income-num {
flex: 1;
align-self: flex-start;
.explain {
line-height: 34rpx;
margin-top: 12rpx;
}
}
}
.content {
padding: 0 20rpx;
/* margin-top: -120rpx; */
.order-container {
.order-item {
border-radius: 14rpx;
.order-header {
padding: 20rpx 30rpx;
border-bottom: var(--border);
.status {
/* background: linear-gradient(80deg, #F95F2F 0%, #FF2C3C 100%);
border-radius: 4rpx;
width: 134rpx;
height: 42rpx;
font-size: 24rpx;
line-height: 34rpx; */
color: #F95F2F;
}
}
.order-content {
padding: 20rpx 30rpx 20rpx 20rpx;
border-bottom: var(--border);
.order-goods-img {
width: 140rpx;
height: 140rpx;
flex: none;
}
.order-goods-info {
text-align: left;
flex: 1;
.name {
line-height: 36rpx;
}
.pre-income {
line-height: 34rpx;
margin-top: 8rpx;
}
}
}
.order-footer {
padding: 20rpx 30rpx 20rpx 20rpx;
.static {
color: #F95F2F;
}
.wait-return {
color: #F95F2F;
}
}
}
}
}
}
.data-null {
padding-top: 200rpx;
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<view class="swiper-wrap" :style="{height, padding}" v-if="lists.length">
<view class="swiper-con" :style="{borderRadius: radius}">
<template v-if="isSwiper">
<swiper class="swiper" :autoplay="autoplay" :circular="circular" @change="swiperChange"
:previous-margin="previousMargin" display-multiple-items="1">
<swiper-item v-for="(item, index) in lists" :key="index">
<view :data-item="item" style="width:100%;height:100%;" @click="goPage(item)">
<u-image mode="aspectFill" :width="'calc(100% - ' + previousMargin + ')'" height="100%"
:border-radius="radius" :src="item.image"></u-image>
</view>
</swiper-item>
</swiper>
<view class="dots" v-if="lists.length > 1">
<view v-for="(item, index) in lists" :key="index"
:class="'dot ' + (index == currentSwiper ? 'active' : '')"></view>
</view>
</template>
<template v-lese v-for="(item, index) in lists" >
<view :key="index" :data-item="item" style="width:100%;height:100%;" @click="goPage(item)" v-if="index < 1">
<u-image mode="aspectFill" :width="'calc(100% - ' + previousMargin + ')'" height="100%"
:border-radius="radius" :src="item.image"></u-image>
</view>
</template>
</view>
</view>
</template>
<script>
import {
getAdList
} from '@/api/store';
import {
paramsToStr
} from '@/utils/tools'
export default {
data() {
return {
lists: [],
currentSwiper: 0
};
},
components: {},
props: {
pid: {
type: Number
},
circular: {
type: Boolean,
default: true
},
autoplay: {
type: Boolean,
default: true
},
height: {
type: String
},
radius: {
type: String,
default: '10rpx'
},
padding: {
type: String,
default: '0rpx'
},
previousMargin: {
type: String,
default: "0rpx"
},
isSwiper: {
type: Boolean,
default: true
}
},
created() {
this.getAdListFun();
},
watch: {
'pid': function(value) {
this.getAdListFun();
}
},
methods: {
async getAdListFun() {
const {
code,
data
} = await getAdList({
pid: this.pid,
client: 1
})
if (code == 1) {
this.lists = data
}
},
swiperChange(e) {
this.currentSwiper = e.detail.current
},
goPage(item) {
let {
link,
link_type,
params,
is_tab
} = item;
switch (link_type) {
case 1:
""
case 2:
if (is_tab) {
uni.switchTab({
url: link
});
} else {
uni.navigateTo({
url: link + paramsToStr(params)
});
}
break;
case 3:
uni.navigateTo({
url: '/pages/webview/webview?url=' + link
});
break;
}
}
}
};
</script>
<style lang="scss">
.swiper-wrap {
overflow: hidden;
box-sizing: content-box;
.swiper-con {
position: relative;
height: 100%;
overflow: hidden;
transform: translateY(0);
}
.swiper {
width: 100%;
height: 100%;
position: relative;
.slide-image {
height: 100%;
}
}
.dots {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 20rpx;
display: flex;
.dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
margin-right: 10rpx;
background-color: #fff;
&.active {
width: 16rpx;
border-radius: 8rpx;
background-color: $-color-primary;
}
}
}
}
</style>
+98
View File
@@ -0,0 +1,98 @@
<template>
<view :class="{ active, inactive: !active, tab: true }" :style="shouldShow ? '' : 'display: none;'">
<slot v-if="shouldRender"></slot>
</view>
</template>
<script>
export default {
props: {
dot: {
type: Boolean,
},
info: {
type: null,
},
title: {
type: String,
},
titleStyle: {
type: String,
},
name: {
type: [Number, String],
value: ''
}
},
inject: ['tabs'],
data() {
return {
active: false,
shouldShow: false,
shouldRender: false
}
},
created() {
this.tabs.childrens.push(this)
},
mounted() {
this.update()
},
methods: {
getComputedName: function() {
if (this.data.name !== '') {
return this.data.name;
}
return this.index;
},
updateRender: function(active, parent) {
let parentData = parent;
this.inited = this.inited || active;
this.active = active
this.shouldRender = this.inited
this.shouldShow = active
},
update: function() {
if (this.tabs) {
this.tabs.updateTabs();
}
}
},
computed: {
changeData() {
const {
dot,
info,
title,
titleStyle
} = this
return {
dot,
info,
title,
titleStyle
}
}
},
watch: {
changeData(value) {
this.update()
},
},
}
</script>
<style>
.tab.active {
height: auto;
}
.tab.inactive {
height: 0;
overflow: visible;
}
</style>
+252
View File
@@ -0,0 +1,252 @@
<template>
<view class="_tab-box" :style="{fontSize: defaultConfig.fontSize + 'rpx', color: defaultConfig.color,}">
<scroll-view id="_scroll" :scroll-x="true" class="scroll-view-h" scroll-with-animation :scroll-left="slider.scrollLeft" :style="{ backgroundColor: defaultConfig.bgColor}">
<view class="_scroll-content">
<view class="_tab-item-box" :class="[defaultConfig.itemWidth ? '_clamp' : '_flex']">
<block v-for="(item, index) in tabList" :key="index">
<view class="_item" :id="'_tab_'+index" :class="{ '_active': tagIndex === index }" :style="{color: tagIndex == index ? defaultConfig.activeColor : defaultConfig.color, 'width': defaultConfig.itemWidth ? defaultConfig.itemWidth + 'rpx' : ''}"
@click="tabClick(index)">{{ item.title }}</view>
</block>
</view>
<view class="_underline" :style="{
transform: 'translateX(' + slider.left + 'px)',
width: slider.width + 'px',
height: defaultConfig.underLineHeight + 'rpx',
backgroundColor: defaultConfig.underLineColor,
}" />
</view>
</scroll-view>
<view class="tab-content">
<view>
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tabs',
props: {
active: {
type: Number,
default: 0
},
config: {
type: Object,
default: () => {
return {}
}
},
},
provide() {
return {
tabs: this
}
},
data() {
return {
tabList: [],
tagIndex: 0,
slider: {
left: 0,
width: 0,
scrollLeft: 0
},
scorll: {},
defaultConfig: {
bgColor: "#fff",
// key
// rpx
fontSize: 26,
//
color: '#333',
//
activeColor: '#FF2C3C',
// item 0
itemWidth: 0,
// 线 rpx
underLinePadding: 10,
// 线 rpx underLinePadding
underLineWidth: 0,
// 线 rpx
underLineHeight: 4,
// 线
underLineColor: '#FF2C3C',
},
};
},
watch: {
},
created() {
this.childrens = []
},
mounted() {
this.updateTabs();
},
methods: {
updateTabs() {
this.tabList = this.childrens.map((item) => {
const {
title,
info,
name,
dot,
titleStyle,
active,
updateRender
} = item
return {
title,
info,
name,
dot,
titleStyle,
active,
updateRender
}
})
this.updateConfig();
this.tagIndex = this.active;
this.$nextTick(() => {
this.calcScrollPosition();
})
},
updateConfig() {
this.defaultConfig = Object.assign(this.defaultConfig, this.config);
},
calcScrollPosition() {
const query = uni.createSelectorQuery().in(this);
query.select('#_scroll').boundingClientRect((res) => {
this.scorll = res;
this.updateTabWidth();
}).exec();
},
updateTabWidth(index = 0) {
let data = this.tabList;
if (data.length == 0) return false;
const query = uni.createSelectorQuery().in(this);
query.select('#_tab_' + index).boundingClientRect((res) => {
data[index]._slider = {
width: res.width,
left: res.left,
scrollLeft: res.left - (data[index - 1] ? data[index - 1]._slider.width : 0),
};
if (this.tagIndex == index) {
this.tabToIndex(this.tagIndex);
}
index++;
if (data.length > index) {
this.updateTabWidth(index);
}
}).exec();
},
tabToIndex(index) {
let _slider = this.tabList[index]._slider;
let width = uni.upx2px(this.defaultConfig.underLineWidth);
if (!width) {
if (this.defaultConfig.itemWidth) {
width = uni.upx2px(this.defaultConfig.itemWidth)/3;
} else {
width = this.tabList[index]['title'].length * uni.upx2px(this.defaultConfig.fontSize);
}
width += uni.upx2px(this.defaultConfig.underLinePadding) * 2;
}
this.childrens.forEach((item, ind) => {
let active = ind === index;
if (active !== item.active || !item.inited) {
item.updateRender(active, this);
}
});
let scorll_left = this.scorll.left || 0;
this.slider = {
left: _slider.left - scorll_left + (_slider.width - width) / 2,
width: width,
scrollLeft: _slider.scrollLeft - scorll_left,
}
},
tabClick(index) {
this.tagIndex = index;
this.tabToIndex(index);
this.$emit('change', index);
}
}
}
</script>
<style lang="scss" scoped>
._tab-box {
width: 100%;
font-size: 26rpx;
position: relative;
z-index: 10;
.scroll-view-h {
height: 80rpx;
line-height: 80rpx;
white-space: nowrap;
width: 100%;
box-sizing: border-box;
._scroll-content {
width: 100%;
height: 100%;
position: relative;
display: inline-block;
._tab-item-box {
height: 100%;
display: inline-block;
&._flex {
display: flex;
._item {
flex: 1;
padding: 0 20rpx;
}
}
&._clamp {
._item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
._item {
height: 100%;
display: inline-block;
text-align: center;
position: relative;
text-align: center;
color: #333;
&._active {
color: #e54d42;
}
}
}
._underline {
height: 4rpx;
background-color: #e54d42;
border-radius: 6rpx;
transition: transform .5s;
position: absolute;
bottom: 0;
}
}
}
}
</style>
File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
<template xlang="wxml" minapp="mpvue">
<view class="tki-qrcode">
<!-- #ifndef MP-ALIPAY -->
<canvas class="tki-qrcode-canvas" :canvas-id="cid" :style="{width:cpSize+'px',height:cpSize+'px'}" />
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<canvas :id="cid" :width="cpSize" :height="cpSize" class="tki-qrcode-canvas" />
<!-- #endif -->
<image v-show="show" :src="result" :style="{width:cpSize+'px',height:cpSize+'px'}" />
</view>
</template>
<script>
import QRCode from "./qrcode.js"
let qrcode
export default {
name: "tki-qrcode",
props: {
cid: {
type: String,
default: 'tki-qrcode-canvas'
},
size: {
type: Number,
default: 200
},
unit: {
type: String,
default: 'upx'
},
show: {
type: Boolean,
default: true
},
val: {
type: String,
default: ''
},
background: {
type: String,
default: '#ffffff'
},
foreground: {
type: String,
default: '#000000'
},
pdground: {
type: String,
default: '#000000'
},
icon: {
type: String,
default: ''
},
iconSize: {
type: Number,
default: 40
},
lv: {
type: Number,
default: 3
},
onval: {
type: Boolean,
default: false
},
loadMake: {
type: Boolean,
default: false
},
usingComponents: {
type: Boolean,
default: true
},
showLoading: {
type: Boolean,
default: true
},
loadingText: {
type: String,
default: '二维码生成中'
},
},
data() {
return {
result: '',
}
},
methods: {
_makeCode() {
let that = this
if (!this._empty(this.val)) {
qrcode = new QRCode({
context: that, //
canvasId:that.cid, // canvas-id
usingComponents: that.usingComponents, //
showLoading: that.showLoading, // loading
loadingText: that.loadingText, // loading
text: that.val, //
size: that.cpSize, //
background: that.background, //
foreground: that.foreground, //
pdground: that.pdground, //
correctLevel: that.lv, //
image: that.icon, //
imageSize: that.iconSize,//
cbResult: function (res) { //
that._result(res)
},
});
} else {
uni.showToast({
title: '二维码内容不能为空',
icon: 'none',
duration: 2000
});
}
},
_clearCode() {
this._result('')
qrcode.clear()
},
_saveCode() {
let that = this;
if (this.result != "") {
uni.saveImageToPhotosAlbum({
filePath: that.result,
success: function () {
uni.showToast({
title: '二维码保存成功',
icon: 'success',
duration: 2000
});
}
});
}
},
_result(res) {
this.result = res;
this.$emit('result', res)
},
_empty(v) {
let tp = typeof v,
rt = false;
if (tp == "number" && String(v) == "") {
rt = true
} else if (tp == "undefined") {
rt = true
} else if (tp == "object") {
if (JSON.stringify(v) == "{}" || JSON.stringify(v) == "[]" || v == null) rt = true
} else if (tp == "string") {
if (v == "" || v == "undefined" || v == "null" || v == "{}" || v == "[]") rt = true
} else if (tp == "function") {
rt = false
}
return rt
}
},
watch: {
size: function (n, o) {
if (n != o && !this._empty(n)) {
this.cSize = n
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 100);
}
}
},
val: function (n, o) {
if (this.onval) {
if (n != o && !this._empty(n)) {
setTimeout(() => {
this._makeCode()
}, 0);
}
}
}
},
computed: {
cpSize() {
if(this.unit == "upx"){
return uni.upx2px(this.size)
}else{
return this.size
}
}
},
mounted: function () {
if (this.loadMake) {
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 0);
}
}
},
}
</script>
<style>
.tki-qrcode {
position: relative;
}
.tki-qrcode-canvas {
position: fixed;
top: -99999upx;
left: -99999upx;
z-index: -99999;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<view :class="'trigonometry ' + (direction === 'up' ? 'up' : '') + ' ' + (size === 'small' ? 'small' : '')" :style="'color:' + color + ';opacity: ' + opacity"></view>
</template>
<script>
export default {
data() {
return {};
},
components: {},
props: {
color: {
type: String,
default: ''
},
direction: {
type: String
},
size: {
type: String
},
opacity: {
type: String,
default: '0.8'
}
},
methods: {}
};
</script>
<style>
.trigonometry {
border-color: transparent transparent currentcolor currentcolor;
border-style: solid;
border-width: 3px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
opacity: .8;
margin: -3px 10rpx 0;
}
.up {
margin-top: 1rpx;
-webkit-transform: rotate(135deg);
transform: rotate(135deg);
}
.small {
border-width: 2px;
margin-top: -2px;
}
.small.up {
margin-top: 2px;
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<template>
<view class="uploader-container row wrap">
<view class="upload-image-box" v-for="(item, index) in fileList" :key="index" :style="{width: previewSize, height: previewSize}">
<custom-image mode="aspectFit" class="img-preview" radius="10rpx" :src="item.url" :width="previewSize" :height="previewSize" />
<view class="close-icon row-center" @click="deleteImage($event, index)">
<u-icon name="close" size="30" color="white" />
</view>
</view>
<view
class="uplader-upload row-center"
:style="{width: previewSize, height: previewSize}"
@click="handleImage"
v-show="(fileList.length == 0 || mutiple) && fileList.length < maxUpload"
v-if="!useSlot"
>
<u-icon size="48" color="#dcdee0" name="camera" />
<view type="image" accept="image/*" class="uploader-input" />
</view>
<view
class="uplader-upload-slot row-center"
@click="handleImage"
v-show="(fileList.length == 0 || mutiple) && fileList.length < maxUpload"
v-else
>
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: "uploader",
props: {
fileList: {
type: Array,
default: () => []
},
//
mutiple: {
type: Boolean,
default: false
},
//
maxUpload: {
type: Number,
default: 1
},
previewSize: {
type: String,
default: "160rpx"
},
//
deletable: {
type: Boolean,
default: false,
},
useSlot: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
create() {
},
methods: {
handleImage() {
uni.chooseImage({
count: this.mutiple ? this.maxUpload : 1,
success: (res) => {
this.$emit("after-read", res.tempFiles)
}
})
},
deleteImage(e, index) {
this.$emit('delete', index)
}
}
}
</script>
<style lang="scss">
.uploader-container {
.upload-image-box {
position: relative;
margin-right: 8rpx;
margin-bottom: 8rpx;
.img-preview {
}
.close-icon {
position: absolute;
right: -20rpx;
top: -15rpx;
width: 40rpx;
height: 40rpx;
background-color: red;
border-radius: 50%;
z-index: 20;
}
}
.uplader-upload {
position: relative;
width: 160rpx;
height: 160rpx;
background-color: #f7f8fa;
.uploader-input {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 0;
top: 0;
left: 0;
z-index: 10;
cursor: pointer;
}
}
.uplader-upload-slot {
position: relative;
min-width: 160rpx;
min-height: 160rpx;
.uploader-input {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 0;
top: 0;
left: 0;
z-index: 10;
cursor: pointer;
}
}
}
</style>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 www.uviewui.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+106
View File
@@ -0,0 +1,106 @@
<p align="center">
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
</p>
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
<h3 align="center">多平台快速开发的UI框架</h3>
## 说明
uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
## 特性
- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
- 众多的常用页面和布局,让您专注逻辑,事半功倍
- 详尽的文档支持,现代化的演示效果
- 按需引入,精简打包体积
## 安装
```bash
# npm方式安装
npm i uview-ui
```
## 快速上手
1. `main.js`引入uView库
```js
// main.js
import uView from 'uview-ui';
Vue.use(uView);
```
2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
```css
/* App.vue */
<style lang="scss">
@import "uview-ui/index.scss";
</style>
```
3. `uni.scss`引入全局scss变量文件
```css
/* uni.scss */
@import "uview-ui/theme.scss";
```
4. `pages.json`配置easycom规则(按需引入)
```js
// pages.json
{
"easycom": {
// npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
// npm安装方式
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
// 下载安装方式
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
},
// 此为本身已有的内容
"pages": [
// ......
]
}
```
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
## 使用方法
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
```html
<template>
<u-button>按钮</u-button>
</template>
```
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
## 链接
- [官方文档](https://uviewui.com/)
- [更新日志](https://uviewui.com/components/changelog.html)
- [升级指南](https://uviewui.com/components/changelog.html)
- [关于我们](https://uviewui.com/cooperation/about.html)
## 预览
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
<!-- ## 捐赠uView的研发
uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
<img src="https://uviewui.com/common/wechat.png" width="220" >
<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
-->
## 版权信息
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
@@ -0,0 +1,190 @@
<template>
<u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
{{tips.text}}
</view>
<block v-for="(item, index) in list" :key="index">
<view
@touchmove.stop.prevent
@tap="itemClick(index)"
:style="[itemStyle(index)]"
class="u-action-sheet-item u-line-1"
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
:hover-stay-time="150"
>
<text>{{item.text}}</text>
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
</view>
</block>
<view class="u-gab" v-if="cancelBtn">
</view>
<view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
:hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
</u-popup>
</template>
<script>
/**
* actionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单供用户选择并返回结果本组件功能类似于uni的uni.showActionSheetAPI配置更加灵活所有平台都表现一致
* @tutorial https://www.uviewui.com/components/actionSheet.html
* @property {Array<Object>} list 按钮的文字数组见官方文档示例
* @property {Object} tips 顶部的提示文字见官方文档示例
* @property {String} cancel-text 取消按钮的提示文字
* @property {Boolean} cancel-btn 是否显示底部的取消按钮默认true
* @property {Number String} border-radius 弹出部分顶部左右的圆角值单位rpx默认0
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭默认true
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配默认false
* @property {Number String} z-index z-index值默认1075
* @property {String} cancel-text 取消按钮的提示文字
* @event {Function} click 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
props: {
// actionsheet
maskCloseAble: {
type: Boolean,
default: true
},
// rpx
list: {
type: Array,
default () {
//
// return [{
// text: '',
// color: '',
// fontSize: ''
// }]
return [];
}
},
//
tips: {
type: Object,
default () {
return {
text: '',
color: '',
fontSize: '26'
}
}
},
//
cancelBtn: {
type: Boolean,
default: true
},
// iPhoneX
safeAreaInsetBottom: {
type: Boolean,
default: false
},
//
value: {
type: Boolean,
default: false
},
//
borderRadius: {
type: [String, Number],
default: 0
},
// z-index
zIndex: {
type: [String, Number],
default: 0
},
//
cancelText: {
type: String,
default: '取消'
}
},
computed: {
//
tipsStyle() {
let style = {};
if (this.tips.color) style.color = this.tips.color;
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
return style;
},
//
itemStyle() {
return (index) => {
let style = {};
if (this.list[index].color) style.color = this.list[index].color;
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
//
if (this.list[index].disabled) style.color = '#c0c4cc';
return style;
}
},
uZIndex() {
// z-index使
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
methods: {
//
close() {
// inputpropsvalue
// vue
this.popupClose();
this.$emit('close');
},
//
popupClose() {
this.$emit('input', false);
},
// item
itemClick(index) {
// disabled
if(this.list[index].disabled) return;
this.$emit('click', index);
this.$emit('input', false);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-tips {
font-size: 26rpx;
text-align: center;
padding: 34rpx 0;
line-height: 1;
color: $u-tips-color;
}
.u-action-sheet-item {
@include vue-flex;;
line-height: 1;
justify-content: center;
align-items: center;
font-size: 32rpx;
padding: 34rpx 0;
flex-direction: column;
}
.u-action-sheet-item__subtext {
font-size: 24rpx;
color: $u-tips-color;
margin-top: 20rpx;
}
.u-gab {
height: 12rpx;
background-color: rgb(234, 234, 236);
}
.u-actionsheet-cancel {
color: $u-main-color;
}
</style>
@@ -0,0 +1,256 @@
<template>
<view class="u-alert-tips" v-if="show" :class="[
!show ? 'u-close-alert-tips': '',
type ? 'u-alert-tips--bg--' + type + '-light' : '',
type ? 'u-alert-tips--border--' + type + '-disabled' : '',
]" :style="{
backgroundColor: bgColor,
borderColor: borderColor
}">
<view class="u-icon-wrap">
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
</view>
<view class="u-alert-content" @tap.stop="click">
<view class="u-alert-title" :style="[uTitleStyle]">
{{title}}
</view>
<view v-if="description" class="u-alert-desc" :style="[descStyle]">
{{description}}
</view>
</view>
<view class="u-icon-wrap">
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
:size="22" class="u-close-icon" :style="{
top: description ? '18rpx' : '24rpx'
}"></u-icon>
</view>
<text v-if="closeAble && closeText" class="u-close-text" :style="{
top: description ? '18rpx' : '24rpx'
}">{{closeText}}</text>
</view>
</template>
<script>
/**
* alertTips 警告提示
* @description 警告提示展现需要关注的信息
* @tutorial https://uviewui.com/components/alertTips.html
* @property {String} title 显示的标题文字
* @property {String} description 辅助性文字颜色比title浅一点字号也小一点可选
* @property {String} type 关闭按钮(默认为叉号icon图标)
* @property {String} icon 图标名称
* @property {Object} icon-style 图标的样式对象形式
* @property {Object} title-style 标题的样式对象形式
* @property {Object} desc-style 描述的样式对象形式
* @property {String} close-able 用文字替代关闭图标close-able为true时有效
* @property {Boolean} show-icon 是否显示左边的辅助图标
* @property {Boolean} show 显示或隐藏组件
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
*/
export default {
name: 'u-alert-tips',
props: {
//
title: {
type: String,
default: ''
},
// success/warning/info/error
type: {
type: String,
default: 'warning'
},
//
description: {
type: String,
default: ''
},
//
closeAble: {
type: Boolean,
default: false
},
//
closeText: {
type: String,
default: ''
},
//
showIcon: {
type: Boolean,
default: false
},
// coloricon
color: {
type: String,
default: ''
},
//
bgColor: {
type: String,
default: ''
},
//
borderColor: {
type: String,
default: ''
},
//
show: {
type: Boolean,
default: true
},
// icon
icon: {
type: String,
default: ''
},
// icon
iconStyle: {
type: Object,
default() {
return {}
}
},
//
titleStyle: {
type: Object,
default() {
return {}
}
},
//
descStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
}
},
computed: {
uTitleStyle() {
let style = {};
//
style.fontWeight = this.description ? 500 : 'normal';
// stylestyle
return this.$u.deepMerge(style, this.titleStyle);
},
uIcon() {
// icon使type
return this.icon ? this.icon : this.$u.type2icon(this.type);
},
uIconType() {
// 使type
return Object.keys(this.iconStyle).length ? '' : this.type;
}
},
methods: {
//
click() {
this.$emit('click');
},
//
close() {
this.$emit('close');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-alert-tips {
@include vue-flex;
align-items: center;
padding: 16rpx 30rpx;
border-radius: 8rpx;
position: relative;
transition: all 0.3s linear;
border: 1px solid #fff;
&--bg--primary-light {
background-color: $u-type-primary-light;
}
&--bg--info-light {
background-color: $u-type-info-light;
}
&--bg--success-light {
background-color: $u-type-success-light;
}
&--bg--warning-light {
background-color: $u-type-warning-light;
}
&--bg--error-light {
background-color: $u-type-error-light;
}
&--border--primary-disabled {
border-color: $u-type-primary-disabled;
}
&--border--success-disabled {
border-color: $u-type-success-disabled;
}
&--border--error-disabled {
border-color: $u-type-error-disabled;
}
&--border--warning-disabled {
border-color: $u-type-warning-disabled;
}
&--border--info-disabled {
border-color: $u-type-info-disabled;
}
}
.u-close-alert-tips {
opacity: 0;
visibility: hidden;
}
.u-icon {
margin-right: 16rpx;
}
.u-alert-title {
font-size: 28rpx;
color: $u-main-color;
}
.u-alert-desc {
font-size: 26rpx;
text-align: left;
color: $u-content-color;
}
.u-close-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
}
.u-close-hover {
color: red;
}
.u-close-text {
font-size: 24rpx;
color: $u-tips-color;
position: absolute;
top: 20rpx;
right: 20rpx;
line-height: 1;
}
</style>
@@ -0,0 +1,290 @@
<template>
<view class="content">
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
<canvas
class="cropper"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
canvas-id="cropper"
id="cropper"
></canvas>
<canvas
class="cropper"
:disable-scroll="true"
:style="{
position: 'fixed',
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
height: `${cropperOpt.height * cropperOpt.pixelRatio}`
}"
canvas-id="targetId"
id="targetId"
></canvas>
</view>
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
<!-- #ifdef H5 -->
<view class="upload" @tap="uploadTap">选择图片</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="upload" @tap="uploadTap">重新选择</view>
<!-- #endif -->
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
</view>
</view>
</template>
<script>
import WeCropper from './weCropper.js';
export default {
props: {
// lineWidth-(rpx)color:
// mask-rgba"rgba(0, 0, 0, 0.35)"
boundStyle: {
type: Object,
default() {
return {
lineWidth: 4,
borderColor: 'rgb(245, 245, 245)',
mask: 'rgba(0, 0, 0, 0.35)'
};
}
}
// // rpx
// rectWidth: {
// type: [String, Number],
// default: 400
// },
// // rpx
// rectHeight: {
// type: [String, Number],
// default: 400
// },
// // rpx
// destWidth: {
// type: [String, Number],
// default: 400
// },
// // rpx
// destHeight: {
// type: [String, Number],
// default: 400
// },
// // "png""jpg"
// fileType: {
// type: String,
// default: 'jpg',
// },
// //
// // H5使
// quality: {
// type: [Number, String],
// default: 1
// }
},
data() {
return {
//
bottomNavHeight: 50,
originWidth: 200,
width: 0,
height: 0,
cropperOpt: {
id: 'cropper',
targetId: 'targetCropper',
pixelRatio: 1,
width: 0,
height: 0,
scale: 2.5,
zoom: 8,
cut: {
x: (this.width - this.originWidth) / 2,
y: (this.height - this.originWidth) / 2,
width: this.originWidth,
height: this.originWidth
},
boundStyle: {
lineWidth: uni.upx2px(this.boundStyle.lineWidth),
mask: this.boundStyle.mask,
color: this.boundStyle.borderColor
}
},
//
// px
destWidth: 200,
// px
rectWidth: 200,
// 'png'"jpg"
fileType: 'jpg',
src: '', //
};
},
onLoad(option) {
let rectInfo = uni.getSystemInfoSync();
this.width = rectInfo.windowWidth;
this.height = rectInfo.windowHeight - this.bottomNavHeight;
this.cropperOpt.width = this.width;
this.cropperOpt.height = this.height;
this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
if (option.destWidth) this.destWidth = option.destWidth;
if (option.rectWidth) {
let rectWidth = Number(option.rectWidth);
this.cropperOpt.cut = {
x: (this.width - rectWidth) / 2,
y: (this.height - rectWidth) / 2,
width: rectWidth,
height: rectWidth
};
}
this.rectWidth = option.rectWidth;
if (option.fileType) this.fileType = option.fileType;
//
this.cropper = new WeCropper(this.cropperOpt)
.on('ready', ctx => {
// wecropper is ready for work!
})
.on('beforeImageLoad', ctx => {
// before picture loaded, i can do something
})
.on('imageLoad', ctx => {
// picture loaded
})
.on('beforeDraw', (ctx, instance) => {
// before canvas draw,i can do something
});
// page.json
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000'
});
uni.chooseImage({
count: 1, // 9
sizeType: ['compressed'], //
sourceType: ['album', 'camera'], //
success: res => {
this.src = res.tempFilePaths[0];
// datasrc
this.cropper.pushOrign(this.src);
}
});
},
methods: {
touchStart(e) {
this.cropper.touchStart(e);
},
touchMove(e) {
this.cropper.touchMove(e);
},
touchEnd(e) {
this.cropper.touchEnd(e);
},
getCropperImage(isPre = false) {
if(!this.src) return this.$u.toast('请先选择图片再裁剪');
let cropper_opt = {
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath
destWidth: Number(this.destWidth),
fileType: this.fileType
};
this.cropper.getCropperImage(cropper_opt, (path, err) => {
if (err) {
uni.showModal({
title: '温馨提示',
content: err.message
});
} else {
if (isPre) {
uni.previewImage({
current: '', // http
urls: [path] // http
});
} else {
uni.$emit('uAvatarCropper', path);
this.$u.route({
type: 'back'
});
}
}
});
},
uploadTap() {
const self = this;
uni.chooseImage({
count: 1, // 9
sizeType: ['original', 'compressed'], //
sourceType: ['album', 'camera'], //
success: (res) => {
self.src = res.tempFilePaths[0];
// datasrc
self.cropper.pushOrign(this.src);
}
});
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.content {
background: rgba(255, 255, 255, 1);
}
.cropper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 11;
}
.cropper-buttons {
background-color: #000000;
color: #eee;
}
.cropper-wrapper {
position: relative;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: #000;
}
.cropper-buttons {
width: 100vw;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
font-size: 28rpx;
}
.cropper-buttons .upload,
.cropper-buttons .getCropperImage {
width: 50%;
text-align: center;
}
.cropper-buttons .upload {
text-align: left;
padding-left: 50rpx;
}
.cropper-buttons .getCropperImage {
text-align: right;
padding-right: 50rpx;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
<template>
<view class="u-avatar" :style="[wrapStyle]" @tap="click">
<image
@error="loadError"
:style="[imgStyle]"
class="u-avatar__img"
v-if="!uText && avatar"
:src="avatar"
:mode="imgMode"
></image>
<text class="u-line-1" v-else-if="uText" :style="{
fontSize: '38rpx'
}">{{uText}}</text>
<slot v-else></slot>
<view class="u-avatar__sex" v-if="showSex" :class="['u-avatar__sex--' + sexIcon]" :style="[uSexStyle]">
<u-icon :name="sexIcon" size="20"></u-icon>
</view>
<view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
<u-icon :name="levelIcon" size="20"></u-icon>
</view>
</view>
</template>
<script>
let base64Avatar = "data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
/**
* avatar 头像
* @description 本组件一般用于展示头像的地方如个人中心或者评论列表页的用户头像展示等场所
* @tutorial https://www.uviewui.com/components/avatar.html
* @property {String} bg-color 背景颜色一般显示文字时用默认#ffffff
* @property {String} src 头像路径如加载失败将会显示默认头像
* @property {String Number} size 头像尺寸可以为指定字符串(large, default, mini)或者数值单位rpx默认default
* @property {String} mode 显示类型见上方说明默认circle
* @property {String} sex-icon 性别图标man-woman-默认man
* @property {String} level-icon 等级图标默认level
* @property {String} sex-bg-color 性别图标背景颜色
* @property {String} level-bg-color 等级图标背景颜色
* @property {String} show-sex 是否显示性别图标默认false
* @property {String} show-level 是否显示等级图标默认false
* @property {String} img-mode 头像图片的裁剪类型与uni的image组件的mode参数一致如效果达不到需求可尝试传widthFix值默认aspectFill
* @property {String} index 用户传递的标识符值如果是列表循环可穿v-for的index值
* @event {Function} click 头像被点击
* @example <u-avatar :src="src"></u-avatar>
*/
export default {
name: 'u-avatar',
props: {
//
bgColor: {
type: String,
default: 'transparent'
},
//
src: {
type: String,
default: ''
},
// large-default-mini-rpx
//
size: {
type: [String, Number],
default: 'default'
},
// square-circle-
mode: {
type: String,
default: 'circle'
},
//
text: {
type: String,
default: ''
},
//
imgMode: {
type: String,
default: 'aspectFill'
},
//
index: {
type: [String, Number],
default: ''
},
// man-woman-
sexIcon: {
type: String,
default: 'man'
},
//
levelIcon: {
type: String,
default: 'level'
},
//
levelBgColor: {
type: String,
default: ''
},
//
sexBgColor: {
type: String,
default: ''
},
//
showSex: {
type: Boolean,
default: false
},
//
showLevel: {
type: Boolean,
default: false
}
},
data() {
return {
error: false,
// props
avatar: this.src ? this.src : base64Avatar,
}
},
watch: {
src(n) {
//
if(!n) {
// null''undefined
this.avatar = base64Avatar;
this.error = true;
} else {
this.avatar = n;
this.error = false;
}
}
},
computed: {
wrapStyle() {
let style = {};
style.height = this.size == 'large' ? '120rpx' : this.size == 'default' ?
'90rpx' : this.size == 'mini' ? '70rpx' : this.size + 'rpx';
style.width = style.height;
style.flex = `0 0 ${style.height}`;
style.backgroundColor = this.bgColor;
style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
if(this.text) style.padding = `0 6rpx`;
return style;
},
imgStyle() {
let style = {};
style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
return style;
},
//
uText() {
return String(this.text)[0];
},
//
uSexStyle() {
let style = {};
if(this.sexBgColor) style.backgroundColor = this.sexBgColor;
return style;
},
//
uLevelStyle() {
let style = {};
if(this.levelBgColor) style.backgroundColor = this.levelBgColor;
return style;
}
},
methods: {
//
loadError() {
this.error = true;
this.avatar = base64Avatar;
},
click() {
this.$emit('click', this.index);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-avatar {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
border-radius: 10px;
position: relative;
&__img {
width: 100%;
height: 100%;
}
&__sex {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
top: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
&--man {
background-color: $u-type-primary;
}
&--woman {
background-color: $u-type-error;
}
&--none {
background-color: $u-type-warning;
}
}
&__level {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
bottom: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
background-color: $u-type-warning;
}
}
</style>
@@ -0,0 +1,153 @@
<template>
<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
bottom: bottom + 'rpx',
right: right + 'rpx',
borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
zIndex: uZIndex,
opacity: opacity
}, customStyle]">
<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
<view class="u-back-top__content__tips">
{{tips}}
</view>
</view>
<slot v-else />
</view>
</template>
<script>
export default {
name: 'u-back-top',
props: {
// circle-square-
mode: {
type: String,
default: 'circle'
},
//
icon: {
type: String,
default: 'arrow-upward'
},
//
tips: {
type: String,
default: ''
},
//
duration: {
type: [Number, String],
default: 100
},
//
scrollTop: {
type: [Number, String],
default: 0
},
// rpx
top: {
type: [Number, String],
default: 400
},
// rpx
bottom: {
type: [Number, String],
default: 200
},
// rpx
right: {
type: [Number, String],
default: 40
},
//
zIndex: {
type: [Number, String],
default: '9'
},
//
iconStyle: {
type: Object,
default() {
return {
color: '#909399',
fontSize: '38rpx'
}
}
},
//
customStyle: {
type: Object,
default() {
return {}
}
}
},
watch: {
showBackTop(nVal, oVal) {
//
// v-if
if(nVal) {
this.uZIndex = this.zIndex;
this.opacity = 1;
} else {
this.uZIndex = -1;
this.opacity = 0;
}
}
},
computed: {
showBackTop() {
// scrollToppxtop(rpx)
// px
return this.scrollTop > uni.upx2px(this.top);
},
},
data() {
return {
//
opacity: 0,
// z-index-1
uZIndex: -1
}
},
methods: {
backToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-back-top {
width: 80rpx;
height: 80rpx;
position: fixed;
z-index: 9;
@include vue-flex;
flex-direction: column;
justify-content: center;
background-color: #E1E1E1;
color: $u-content-color;
align-items: center;
transition: opacity 0.4s;
&__content {
@include vue-flex;
flex-direction: column;
align-items: center;
&__tips {
font-size: 24rpx;
transform: scale(0.8);
line-height: 1;
}
}
}
</style>
@@ -0,0 +1,216 @@
<template>
<view v-if="show" class="u-badge" :class="[
isDot ? 'u-badge-dot' : '',
size == 'mini' ? 'u-badge-mini' : '',
type ? 'u-badge--bg--' + type : ''
]" :style="[{
top: offset[0] + 'rpx',
right: offset[1] + 'rpx',
fontSize: fontSize + 'rpx',
position: absolute ? 'absolute' : 'static',
color: color,
backgroundColor: bgColor
}, boxStyle]"
>
{{showText}}
</view>
</template>
<script>
/**
* badge 角标
* @description 本组件一般用于展示头像的地方如个人中心或者评论列表页的用户头像展示等场所
* @tutorial https://www.uviewui.com/components/badge.html
* @property {String Number} count 展示的数字大于 overflowCount 时显示为 ${overflowCount}+为0且show-zero为false时隐藏
* @property {Boolean} is-dot 不展示数字只有一个小点默认false
* @property {Boolean} absolute 组件是否绝对定位为true时offset参数才有效默认true
* @property {String Number} overflow-count 展示封顶的数字值默认99
* @property {String} type 使用预设的背景颜色默认error
* @property {Boolean} show-zero 当数值为 0 是否展示 Badge默认false
* @property {String} size Badge的尺寸设为mini会得到小一号的Badge默认default
* @property {Array} offset 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值单位rpxabsolute为true时有效默认[20, 20]
* @property {String} color 字体颜色默认#ffffff
* @property {String} bgColor 背景颜色优先级比type高如设置type参数会失效
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合优先级比offset高如设置offset参数会失效默认false
* @example <u-badge type="error" count="7"></u-badge>
*/
export default {
name: 'u-badge',
props: {
// primary,warning,success,error,info
type: {
type: String,
default: 'error'
},
// default, mini
size: {
type: String,
default: 'default'
},
//
isDot: {
type: Boolean,
default: false
},
//
count: {
type: [Number, String],
},
//
overflowCount: {
type: Number,
default: 99
},
// 0 Badge
showZero: {
type: Boolean,
default: false
},
//
offset: {
type: Array,
default: () => {
return [20, 20]
}
},
// offset
absolute: {
type: Boolean,
default: true
},
//
fontSize: {
type: [String, Number],
default: '24'
},
//
color: {
type: String,
default: '#ffffff'
},
// badge
bgColor: {
type: String,
default: ''
},
// badgeoffset
isCenter: {
type: Boolean,
default: false
}
},
computed: {
// badge
boxStyle() {
let style = {};
if(this.isCenter) {
style.top = 0;
style.right = 0;
// Y-50%badgebadgeX50%
style.transform = "translateY(-50%) translateX(50%)";
} else {
style.top = this.offset[0] + 'rpx';
style.right = this.offset[1] + 'rpx';
style.transform = "translateY(0) translateX(0)";
}
// miniscal()
if(this.size == 'mini') {
style.transform = style.transform + " scale(0.8)";
}
return style;
},
// isDot
showText() {
if(this.isDot) return '';
else {
if(this.count > this.overflowCount) return `${this.overflowCount}+`;
else return this.count;
}
},
//
show() {
// count0showZerofalse
if(this.count == 0 && this.showZero == false) return false;
else return true;
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-badge {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
justify-content: center;
align-items: center;
line-height: 24rpx;
padding: 4rpx 8rpx;
border-radius: 100rpx;
z-index: 9;
&--bg--primary {
background-color: $u-type-primary;
}
&--bg--error {
background-color: $u-type-error;
}
&--bg--success {
background-color: $u-type-success;
}
&--bg--info {
background-color: $u-type-info;
}
&--bg--warning {
background-color: $u-type-warning;
}
}
.u-badge-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
line-height: 1;
}
.u-badge-mini {
transform: scale(0.8);
transform-origin: center center;
}
// .u-primary {
// background: $u-type-primary;
// color: #fff;
// }
// .u-error {
// background: $u-type-error;
// color: #fff;
// }
// .u-warning {
// background: $u-type-warning;
// color: #fff;
// }
// .u-success {
// background: $u-type-success;
// color: #fff;
// }
// .u-black {
// background: #585858;
// color: #fff;
// }
.u-info {
background-color: $u-type-info;
color: #fff;
}
</style>
@@ -0,0 +1,596 @@
<template>
<button
id="u-wave-btn"
class="u-btn u-line-1 u-fix-ios-appearance"
:class="[
'u-size-' + size,
plain ? 'u-btn--' + type + '--plain' : '',
loading ? 'u-loading' : '',
shape == 'circle' ? 'u-round-circle' : '',
hairLine ? showHairLineBorder : 'u-btn--bold-border',
'u-btn--' + type,
disabled ? `u-btn--${type}--disabled` : '',
]"
:hover-start-time="Number(hoverStartTime)"
:hover-stay-time="Number(hoverStayTime)"
:disabled="disabled"
:form-type="formType"
:open-type="openType"
:app-parameter="appParameter"
:hover-stop-propagation="hoverStopPropagation"
:send-message-title="sendMessageTitle"
send-message-path="sendMessagePath"
:lang="lang"
:data-name="dataName"
:session-from="sessionFrom"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
@getphonenumber="getphonenumber"
@getuserinfo="getuserinfo"
@error="error"
@opensetting="opensetting"
@launchapp="launchapp"
:style="[customStyle, {
overflow: ripple ? 'hidden' : 'visible'
}]"
@tap.stop="click($event)"
:hover-class="getHoverClass"
:loading="loading"
>
<slot></slot>
<view
v-if="ripple"
class="u-wave-ripple"
:class="[waveActive ? 'u-wave-active' : '']"
:style="{
top: rippleTop + 'px',
left: rippleLeft + 'px',
width: fields.targetWidth + 'px',
height: fields.targetWidth + 'px',
'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
}"
></view>
</button>
</template>
<script>
/**
* button 按钮
* @description Button 按钮
* @tutorial https://www.uviewui.com/components/button.html
* @property {String} size 按钮的大小
* @property {Boolean} ripple 是否开启点击水波纹效果
* @property {String} ripple-bg-color 水波纹的背景色ripple为true时有效
* @property {String} type 按钮的样式类型
* @property {Boolean} plain 按钮是否镂空背景色透明
* @property {Boolean} disabled 是否禁用
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
* @property {Boolean} shape 按钮外观形状见文档说明
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台 ios 上为雪花Android上为圆圈)
* @property {String} form-type 用于 <form> 组件点击分别会触发 <form> 组件的 submit/reset 事件
* @property {String} open-type 开放能力
* @property {String} data-name 额外传参参数用于小程序的data-xxx属性通过target.dataset.name获取
* @property {String} hover-class 指定按钮按下去的样式类 hover-class="none" 没有点击态效果(App-nvue 平台暂不支持)
* @property {Number} hover-start-time 按住后多久出现点击态单位毫秒
* @property {Number} hover-stay-time 手指松开后点击态保留时间单位毫秒
* @property {Object} custom-style 对按钮的自定义样式对象形式见文档说明
* @event {Function} click 按钮点击
* @event {Function} getphonenumber open-type="getPhoneNumber"时有效
* @event {Function} getuserinfo 用户点击该按钮时会返回获取到的用户信息从返回参数的detail中获取到的值同uni.getUserInfo
* @event {Function} error 当使用开放能力时发生错误的回调
* @event {Function} opensetting 在打开授权设置页并关闭后回调
* @event {Function} launchapp 打开 APP 成功的回调
* @example <u-button>月落</u-button>
*/
export default {
name: 'u-button',
props: {
//
hairLine: {
type: Boolean,
default: true
},
// defaultprimaryerrorwarningsuccess
type: {
type: String,
default: 'default'
},
// defaultmediummini
size: {
type: String,
default: 'default'
},
// circlesquare
shape: {
type: String,
default: 'square'
},
//
plain: {
type: Boolean,
default: false
},
//
disabled: {
type: Boolean,
default: false
},
//
loading: {
type: Boolean,
default: false
},
// uniappbutton
// https://uniapp.dcloud.io/component/button
openType: {
type: String,
default: ''
},
// <form> <form> submit/reset
// submitreset
formType: {
type: String,
default: ''
},
// APP APP open-type=launchApp
// QQ
appParameter: {
type: String,
default: ''
},
//
hoverStopPropagation: {
type: Boolean,
default: false
},
// zh_CN zh_TW en
lang: {
type: String,
default: 'en'
},
// open-type="contact"
sessionFrom: {
type: String,
default: ''
},
// open-type="contact"
//
sendMessageTitle: {
type: String,
default: ''
},
// open-type="contact"
//
sendMessagePath: {
type: String,
default: ''
},
// open-type="contact"
//
sendMessageImg: {
type: String,
default: ''
},
// true""
// open-type="contact"
showMessageCard: {
type: Boolean,
default: false
},
//
hoverBgColor: {
type: String,
default: ''
},
//
rippleBgColor: {
type: String,
default: ''
},
//
ripple: {
type: Boolean,
default: false
},
//
hoverClass: {
type: String,
default: ''
},
//
customStyle: {
type: Object,
default() {
return {};
}
},
// data-xxxtarget.dataset.name
dataName: {
type: String,
default: ''
},
//
throttleTime: {
type: [String, Number],
default: 1000
},
//
hoverStartTime: {
type: [String, Number],
default: 20
},
//
hoverStayTime: {
type: [String, Number],
default: 150
},
},
computed: {
// bgColor
getHoverClass() {
// hover-class
if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
let hoverClass = '';
hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
return hoverClass;
},
// 'primary', 'success', 'error', 'warning'
showHairLineBorder() {
if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
return '';
} else {
return 'u-hairline-border';
}
}
},
data() {
return {
rippleTop: 0, // Y
rippleLeft: 0, // X
fields: {}, //
waveActive: false //
};
},
methods: {
//
click(e) {
// this.throttle
this.$u.throttle(() => {
// disabledloading
if (this.loading === true || this.disabled === true) return;
//
if (this.ripple) {
//
this.waveActive = false;
this.$nextTick(function() {
this.getWaveQuery(e);
});
}
this.$emit('click', e);
}, this.throttleTime);
},
//
getWaveQuery(e) {
this.getElQuery().then(res => {
//
let data = res[0];
//
if (!data.width || !data.width) return;
// (border-radius)
//
data.targetWidth = data.height > data.width ? data.height : data.width;
if (!data.targetWidth) return;
this.fields = data;
let touchesX = '',
touchesY = '';
// #ifdef MP-BAIDU
touchesX = e.changedTouches[0].clientX;
touchesY = e.changedTouches[0].clientY;
// #endif
// #ifdef MP-ALIPAY
touchesX = e.detail.clientX;
touchesY = e.detail.clientY;
// #endif
// #ifndef MP-BAIDU || MP-ALIPAY
touchesX = e.touches[0].clientX;
touchesY = e.touches[0].clientY;
// #endif
// xytouchesYdata.top
// `transform-origin`centerview
//
this.rippleTop = touchesY - data.top - data.targetWidth / 2;
this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
this.$nextTick(() => {
this.waveActive = true;
});
});
},
//
getElQuery() {
return new Promise(resolve => {
let queryInfo = '';
// uniapp
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
queryInfo = uni.createSelectorQuery().in(this);
//#ifdef MP-ALIPAY
queryInfo = uni.createSelectorQuery();
//#endif
queryInfo.select('.u-btn').boundingClientRect();
queryInfo.exec(data => {
resolve(data);
});
});
},
// uniapp
getphonenumber(res) {
this.$emit('getphonenumber', res);
},
getuserinfo(res) {
this.$emit('getuserinfo', res);
},
error(res) {
this.$emit('error', res);
},
opensetting(res) {
this.$emit('opensetting', res);
},
launchapp(res) {
this.$emit('launchapp', res);
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-btn::after {
border: none;
}
.u-btn {
position: relative;
border: 0;
//border-radius: 10rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
// hidden
overflow: visible;
line-height: 1;
@include vue-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0 40rpx;
z-index: 1;
box-sizing: border-box;
transition: all 0.15s;
&--bold-border {
border: 1px solid #ffffff;
}
&--default {
color: $u-content-color;
border-color: #c0c4cc;
background-color: #ffffff;
}
&--primary {
color: #ffffff;
border-color: $u-type-primary;
background-color: $u-type-primary;
}
&--success {
color: #ffffff;
border-color: $u-type-success;
background-color: $u-type-success;
}
&--error {
color: #ffffff;
border-color: $u-type-error;
background-color: $u-type-error;
}
&--warning {
color: #ffffff;
border-color: $u-type-warning;
background-color: $u-type-warning;
}
&--default--disabled {
color: #ffffff;
border-color: #e4e7ed;
background-color: #ffffff;
}
&--primary--disabled {
color: #ffffff!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-disabled!important;
}
&--success--disabled {
color: #ffffff!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-disabled!important;
}
&--error--disabled {
color: #ffffff!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-disabled!important;
}
&--warning--disabled {
color: #ffffff!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-disabled!important;
}
&--primary--plain {
color: $u-type-primary!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-light!important;
}
&--success--plain {
color: $u-type-success!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-light!important;
}
&--error--plain {
color: $u-type-error!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-light!important;
}
&--warning--plain {
color: $u-type-warning!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-light!important;
}
}
.u-hairline-border:after {
content: ' ';
position: absolute;
pointer-events: none;
// border-boxscale0.5border-boxborder
box-sizing: border-box;
// (scale())
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
left: 0;
top: 0;
width: 199.8%;
height: 199.7%;
-webkit-transform: scale(0.5, 0.5);
transform: scale(0.5, 0.5);
border: 1px solid currentColor;
z-index: 1;
}
.u-wave-ripple {
z-index: 0;
position: absolute;
border-radius: 100%;
background-clip: padding-box;
pointer-events: none;
user-select: none;
transform: scale(0);
opacity: 1;
transform-origin: center;
}
.u-wave-ripple.u-wave-active {
opacity: 0;
transform: scale(2);
transition: opacity 1s linear, transform 0.4s linear;
}
.u-round-circle {
border-radius: 100rpx;
}
.u-round-circle::after {
border-radius: 100rpx;
}
.u-loading::after {
background-color: hsla(0, 0%, 100%, 0.35);
}
.u-size-default {
font-size: 30rpx;
height: 80rpx;
line-height: 80rpx;
}
.u-size-medium {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 26rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0 80rpx;
}
.u-size-mini {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 22rpx;
padding-top: 1px;
height: 50rpx;
line-height: 50rpx;
padding: 0 20rpx;
}
.u-primary-plain-hover {
color: #ffffff !important;
background: $u-type-primary-dark !important;
}
.u-default-plain-hover {
color: $u-type-primary-dark !important;
background: $u-type-primary-light !important;
}
.u-success-plain-hover {
color: #ffffff !important;
background: $u-type-success-dark !important;
}
.u-warning-plain-hover {
color: #ffffff !important;
background: $u-type-warning-dark !important;
}
.u-error-plain-hover {
color: #ffffff !important;
background: $u-type-error-dark !important;
}
.u-info-plain-hover {
color: #ffffff !important;
background: $u-type-info-dark !important;
}
.u-default-hover {
color: $u-type-primary-dark !important;
border-color: $u-type-primary-dark !important;
background-color: $u-type-primary-light !important;
}
.u-primary-hover {
background: $u-type-primary-dark !important;
color: #fff;
}
.u-success-hover {
background: $u-type-success-dark !important;
color: #fff;
}
.u-info-hover {
background: $u-type-info-dark !important;
color: #fff;
}
.u-warning-hover {
background: $u-type-warning-dark !important;
color: #fff;
}
.u-error-hover {
background: $u-type-error-dark !important;
color: #fff;
}
</style>
@@ -0,0 +1,639 @@
<template>
<u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
<view class="u-calendar">
<view class="u-calendar__header">
<view class="u-calendar__header__text" v-if="!$slots['tooltip']">
{{toolTip}}
</view>
<slot v-else name="tooltip" />
</view>
<view class="u-calendar__action u-flex u-row-center">
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__text">{{ showTitle }}</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
</view>
</view>
<view class="u-calendar__week-day">
<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
</view>
<view class="u-calendar__content">
<!-- 前置空白部分 -->
<block v-for="(item, index) in weekdayArr" :key="index">
<view class="u-calendar__content__item"></view>
</block>
<view class="u-calendar__content__item" :class="{
'u-hover-class':openDisAbled(year,month,index+1),
'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
@tap="dateClick(index)">
<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
<view>{{ index + 1 }}</view>
</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
</view>
<view class="u-calendar__content__bg-month">{{month}}</view>
</view>
<view class="u-calendar__bottom">
<view class="u-calendar__bottom__choose">
<text>{{mode == 'date' ? activeDate : startDate}}</text>
<text v-if="endDate">{{endDate}}</text>
</view>
<view class="u-calendar__bottom__btn">
<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script>
/**
* calendar 日历
* @description 此组件用于单个选择日期范围选择日期等日历被包裹在底部弹起的容器中
* @tutorial http://uviewui.com/components/calendar.html
* @property {String} mode 选择日期的模式date-为单个日期range-为选择日期范围
* @property {Boolean} v-model 布尔值变量用于控制日历的弹出与收起
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
* @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
* @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
* @property {String Number} max-year 可切换的最大年份(默认2050)
* @property {String Number} min-year 最小可选日期(默认1950)
* @property {String Number} min-date 可切换的最小年份(默认1950-01-01)
* @property {String Number} max-date 最大可选日期(默认当前日期)
* @property {String Number} 弹窗顶部左右两边的圆角值单位rpx(默认20)
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
* @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
* @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
* @property {String} color 日期字体的默认颜色(默认#303133)
* @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
* @property {String Number} z-index 弹出时的z-index值(默认10075)
* @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
* @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
* @property {String} range-color 选择范围内字体颜色(默认#2979ff)
* @property {String} start-text 起始日期底部的提示文字(默认 '开始')
* @property {String} end-text 结束日期底部的提示文字(默认 '结束')
* @property {String} btn-type 底部确定按钮的主题(默认 'primary')
* @property {String} toolTip 顶部提示文字如设置名为tooltip的slot此参数将失效(默认 '选择日期')
* @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
* @example <u-calendar v-model="show" :mode="mode"></u-calendar>
*/
export default {
name: 'u-calendar',
props: {
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// Picker
maskCloseAble: {
type: Boolean,
default: true
},
//
value: {
type: Boolean,
default: false
},
// z-index
zIndex: {
type: [String, Number],
default: 0
},
//
changeYear: {
type: Boolean,
default: true
},
//
changeMonth: {
type: Boolean,
default: true
},
// date-range-+
mode: {
type: String,
default: 'date'
},
//
maxYear: {
type: [Number, String],
default: 2050
},
//
minYear: {
type: [Number, String],
default: 1950
},
// ()
minDate: {
type: [Number, String],
default: '1950-01-01'
},
/**
* 最大可选日期
* 默认最大值为今天之后的日期不可选
* 2030-12-31
* */
maxDate: {
type: [Number, String],
default: ''
},
//
borderRadius: {
type: [String, Number],
default: 20
},
//
monthArrowColor: {
type: String,
default: '#606266'
},
//
yearArrowColor: {
type: String,
default: '#909399'
},
//
color: {
type: String,
default: '#303133'
},
// |
activeBgColor: {
type: String,
default: '#2979ff'
},
// |
activeColor: {
type: String,
default: '#ffffff'
},
//
rangeBgColor: {
type: String,
default: 'rgba(41,121,255,0.13)'
},
//
rangeColor: {
type: String,
default: '#2979ff'
},
// mode=range
startText: {
type: String,
default: '开始'
},
// mode=range
endText: {
type: String,
default: '结束'
},
//
btnType: {
type: String,
default: 'primary'
},
//
isActiveCurrent: {
type: Boolean,
default: true
},
// mode=date
isChange: {
type: Boolean,
default: false
},
//
closeable: {
type: Boolean,
default: true
},
//
toolTip: {
type: String,
default: '选择日期'
}
},
data() {
return {
// ,1-7
weekday: 1,
weekdayArr:[],
//
days: 0,
daysArr:[],
showTitle: '',
year: 2020,
month: 0,
day: 0,
startYear: 0,
startMonth: 0,
startDay: 0,
endYear: 0,
endMonth: 0,
endDay: 0,
today: '',
activeDate: '',
startDate: '',
endDate: '',
isStart: true,
min: null,
max: null,
weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
};
},
computed: {
dataChange() {
return `${this.mode}-${this.minDate}-${this.maxDate}`;
},
uZIndex() {
// z-index使
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
watch: {
dataChange(val) {
this.init()
}
},
created() {
this.init()
},
methods: {
getColor(index, type) {
let color = type == 1 ? '' : this.color;
let day = index + 1
let date = `${this.year}-${this.month}-${day}`
let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
let start = this.startDate.replace(/\-/g, '/')
let end = this.endDate.replace(/\-/g, '/')
if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
color = type == 1 ? this.activeBgColor : this.activeColor;
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
color = type == 1 ? this.rangeBgColor : this.rangeColor;
}
return color;
},
init() {
let now = new Date();
this.year = now.getFullYear();
this.month = now.getMonth() + 1;
this.day = now.getDate();
this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
this.activeDate = this.today;
this.min = this.initDate(this.minDate);
this.max = this.initDate(this.maxDate || this.today);
this.startDate = "";
this.startYear = 0;
this.startMonth = 0;
this.startDay = 0;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.isStart = true;
this.changeData();
},
//
initDate(date) {
let fdate = date.split('-');
return {
year: Number(fdate[0] || 1920),
month: Number(fdate[1] || 1),
day: Number(fdate[2] || 1)
}
},
openDisAbled: function(year, month, day) {
let bool = true;
let date = `${year}/${month}/${day}`;
// let today = this.today.replace(/\-/g, '/');
let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
let timestamp = new Date(date).getTime();
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
bool = false;
}
return bool;
},
generateArray: function(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start);
},
formatNum: function(num) {
return num < 10 ? '0' + num : num + '';
},
//
getMonthDay(year, month) {
let days = new Date(year, month, 0).getDate();
return days;
},
getWeekday(year, month) {
let date = new Date(`${year}/${month}/01 00:00:00`);
return date.getDay();
},
checkRange(year) {
let overstep = false;
if (year < this.minYear || year > this.maxYear) {
uni.showToast({
title: "日期超出范围啦~",
icon: 'none'
})
overstep = true;
}
return overstep;
},
changeMonthHandler(isAdd) {
if (isAdd) {
let month = this.month + 1;
let year = month > 12 ? this.year + 1 : this.year;
if (!this.checkRange(year)) {
this.month = month > 12 ? 1 : month;
this.year = year;
this.changeData();
}
} else {
let month = this.month - 1;
let year = month < 1 ? this.year - 1 : this.year;
if (!this.checkRange(year)) {
this.month = month < 1 ? 12 : month;
this.year = year;
this.changeData();
}
}
},
changeYearHandler(isAdd) {
let year = isAdd ? this.year + 1 : this.year - 1;
if (!this.checkRange(year)) {
this.year = year;
this.changeData();
}
},
changeData() {
this.days = this.getMonthDay(this.year, this.month);
this.daysArr=this.generateArray(1,this.days)
this.weekday = this.getWeekday(this.year, this.month);
this.weekdayArr=this.generateArray(1,this.weekday)
this.showTitle = `${this.year}${this.month}`;
if (this.isChange && this.mode == 'date') {
this.btnFix(true);
}
},
dateClick: function(day) {
day += 1;
if (!this.openDisAbled(this.year, this.month, day)) {
this.day = day;
let date = `${this.year}-${this.month}-${day}`;
if (this.mode == 'date') {
this.activeDate = date;
} else {
let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
if (this.isStart || compare) {
this.startDate = date;
this.startYear = this.year;
this.startMonth = this.month;
this.startDay = this.day;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.activeDate = "";
this.isStart = false;
} else {
this.endDate = date;
this.endYear = this.year;
this.endMonth = this.month;
this.endDay = this.day;
this.isStart = true;
}
}
}
},
close() {
// v-modelfalse
this.$emit('input', false);
},
getWeekText(date) {
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
let week = date.getDay();
return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
},
btnFix(show) {
if (!show) {
this.close();
}
if (this.mode == 'date') {
let arr = this.activeDate.split('-')
let year = this.isChange ? this.year : Number(arr[0]);
let month = this.isChange ? this.month : Number(arr[1]);
let day = this.isChange ? this.day : Number(arr[2]);
//
let days = this.getMonthDay(year, month);
let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
let weekText = this.getWeekText(result);
let isToday = false;
if (`${year}-${month}-${day}` == this.today) {
//
isToday = true;
}
this.$emit('change', {
year: year,
month: month,
day: day,
days: days,
result: result,
week: weekText,
isToday: isToday,
// switch: show //
});
} else {
if (!this.startDate || !this.endDate) return;
let startMonth = this.formatNum(this.startMonth);
let startDay = this.formatNum(this.startDay);
let startDate = `${this.startYear}-${startMonth}-${startDay}`;
let startWeek = this.getWeekText(startDate)
let endMonth = this.formatNum(this.endMonth);
let endDay = this.formatNum(this.endDay);
let endDate = `${this.endYear}-${endMonth}-${endDay}`;
let endWeek = this.getWeekText(endDate);
this.$emit('change', {
startYear: this.startYear,
startMonth: this.startMonth,
startDay: this.startDay,
startDate: startDate,
startWeek: startWeek,
endYear: this.endYear,
endMonth: this.endMonth,
endDay: this.endDay,
endDate: endDate,
endWeek: endWeek
});
}
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-calendar {
color: $u-content-color;
&__header {
width: 100%;
box-sizing: border-box;
font-size: 30rpx;
background-color: #fff;
color: $u-main-color;
&__text {
margin-top: 30rpx;
padding: 0 60rpx;
@include vue-flex;
justify-content: center;
align-items: center;
}
}
&__action {
padding: 40rpx 0 40rpx 0;
&__icon {
margin: 0 16rpx;
}
&__text {
padding: 0 16rpx;
color: $u-main-color;
font-size: 32rpx;
line-height: 32rpx;
font-weight: bold;
}
}
&__week-day {
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
&__text {
flex: 1;
text-align: center;
}
}
&__content {
width: 100%;
@include vue-flex;
flex-wrap: wrap;
padding: 6px 0;
box-sizing: border-box;
background-color: #fff;
position: relative;
&--end-date {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&--start-date {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
&__item {
width: 14.2857%;
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
position: relative;
z-index: 2;
&__inner {
height: 84rpx;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 32rpx;
position: relative;
border-radius: 50%;
&__desc {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
transform: scale(0.75);
transform-origin: center center;
position: absolute;
left: 0;
text-align: center;
bottom: 2rpx;
}
}
&__tips {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
position: absolute;
left: 0;
transform: scale(0.8);
transform-origin: center center;
text-align: center;
bottom: 8rpx;
z-index: 2;
}
}
&__bg-month {
position: absolute;
font-size: 130px;
line-height: 130px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #e4e7ed;
z-index: 1;
}
}
&__bottom {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #fff;
padding: 0 40rpx 30rpx;
box-sizing: border-box;
font-size: 24rpx;
color: $u-tips-color;
&__choose {
height: 50rpx;
}
&__btn {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,257 @@
<template>
<view class="u-keyboard" @touchmove.stop.prevent="() => {}">
<view class="u-keyboard-grids">
<block>
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
<view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
v-for="(item, j) in group" :key="j">
{{ item }}
</view>
</view>
<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
hover-class="u-hover-class">
<u-icon :size="38" name="backspace" :bold="true"></u-icon>
</view>
<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
<text class="zh" :class="[!abc ? 'active' : 'inactive']"></text>
/
<text class="en" :class="[abc ? 'active' : 'inactive']"></text>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
name: "u-keyboard",
props: {
//
random: {
type: Boolean,
default: false
}
},
data() {
return {
// abc=truebac=false
abc: false
};
},
computed: {
areaList() {
let data = [
'京',
'沪',
'粤',
'津',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'陕',
'蒙',
'吉',
'闽',
'贵',
'渝',
'川',
'青',
'琼',
'宁',
'挂',
'藏',
'港',
'澳',
'新',
'使',
'学'
];
let tmp = [];
//
if (this.random) data = this.$u.randomArray(data);
//
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
},
EngKeyBoardList() {
let data = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
'Z',
'X',
'C',
'V',
'B',
'N',
'M'
];
let tmp = [];
if (this.random) data = this.$u.randomArray(data);
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
}
},
methods: {
//
carInputClick(i, j) {
let value = '';
//
if (this.abc) value = this.EngKeyBoardList[i][j];
else value = this.areaList[i][j];
this.$emit('change', value);
},
// |
changeCarInputMode() {
this.abc = !this.abc;
},
// 退
backspaceClick() {
this.$emit('backspace');
clearInterval(this.timer); //
this.timer = null;
this.timer = setInterval(() => {
this.$emit('backspace');
}, 250);
},
clearTimer() {
clearInterval(this.timer);
this.timer = null;
},
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-keyboard-grids {
background: rgb(215, 215, 217);
padding: 24rpx 0;
position: relative;
}
.u-keyboard-grids-item {
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-keyboard-grids-btn {
text-decoration: none;
width: 62rpx;
flex: 0 0 64rpx;
height: 80rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
font-size: 36rpx;
text-align: center;
line-height: 80rpx;
background-color: #fff;
margin: 8rpx 5rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx #888992;
font-weight: 500;
justify-content: center;
}
.u-carinput-hover {
background-color: rgb(185, 188, 195) !important;
}
.u-keyboard-back {
position: absolute;
width: 96rpx;
right: 22rpx;
bottom: 32rpx;
height: 80rpx;
background-color: rgb(185, 188, 195);
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
box-shadow: 0 2rpx 0rpx #888992;
}
.u-keyboard-change {
font-size: 24rpx;
box-shadow: 0 2rpx 0rpx #888992;
position: absolute;
width: 96rpx;
left: 22rpx;
line-height: 1;
bottom: 32rpx;
height: 80rpx;
background-color: #ffffff;
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
}
.u-keyboard-change .inactive.zh {
transform: scale(0.85) translateY(-10rpx);
}
.u-keyboard-change .inactive.en {
transform: scale(0.85) translateY(10rpx);
}
.u-keyboard-change .active {
color: rgb(237, 112, 64);
font-size: 30rpx;
}
.u-keyboard-change .zh {
transform: translateY(-10rpx);
}
.u-keyboard-change .en {
transform: translateY(10rpx);
}
</style>
@@ -0,0 +1,299 @@
<template>
<view
class="u-card"
@tap.stop="click"
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
:style="{
borderRadius: borderRadius + 'rpx',
margin: margin,
boxShadow: boxShadow
}"
>
<view
v-if="showHead"
class="u-card__head"
:style="[{padding: padding + 'rpx'}, headStyle]"
:class="{
'u-border-bottom': headBorderBottom
}"
@tap="headClick"
>
<view v-if="!$slots.head" class="u-flex u-row-between">
<view class="u-card__head--left u-flex u-line-1" v-if="title">
<image
:src="thumb"
class="u-card__head--left__thumb"
mode="aspectfull"
v-if="thumb"
:style="{
height: thumbWidth + 'rpx',
width: thumbWidth + 'rpx',
borderRadius: thumbCircle ? '100rpx' : '6rpx'
}"
></image>
<text
class="u-card__head--left__title u-line-1"
:style="{
fontSize: titleSize + 'rpx',
color: titleColor
}"
>
{{ title }}
</text>
</view>
<view class="u-card__head--right u-line-1" v-if="subTitle">
<text
class="u-card__head__title__text"
:style="{
fontSize: subTitleSize + 'rpx',
color: subTitleColor
}"
>
{{ subTitle }}
</text>
</view>
</view>
<slot name="head" v-else />
</view>
<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
<view
v-if="showFoot"
class="u-card__foot"
@tap="footClick"
:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
:class="{
'u-border-top': footBorderTop
}"
>
<slot name="foot" />
</view>
</view>
</template>
<script>
/**
* card 卡片
* @description 卡片组件一般用于多个列表条目且风格统一的场景
* @tutorial https://www.uviewui.com/components/card.html
* @property {Boolean} full 卡片与屏幕两侧是否留空隙默认false
* @property {String} title 头部左边的标题
* @property {String} title-color 标题颜色默认#303133
* @property {String | Number} title-size 标题字体大小单位rpx默认30
* @property {String} sub-title 头部右边的副标题
* @property {String} sub-title-color 副标题颜色默认#909399
* @property {String | Number} sub-title-size 副标题字体大小默认26
* @property {Boolean} border 是否显示边框默认true
* @property {String | Number} index 用于标识点击了第几个卡片
* @property {String} box-shadow 卡片外围阴影字符串形式默认none
* @property {String} margin 卡片与屏幕两边和上下元素的间距需带单位"30rpx 20rpx"默认30rpx
* @property {String | Number} border-radius 卡片整体的圆角值单位rpx默认16
* @property {Object} head-style 头部自定义样式对象形式
* @property {Object} body-style 中部自定义样式对象形式
* @property {Object} foot-style 底部自定义样式对象形式
* @property {Boolean} head-border-bottom 是否显示头部的下边框默认true
* @property {Boolean} foot-border-top 是否显示底部的上边框默认true
* @property {Boolean} show-head 是否显示头部默认true
* @property {Boolean} show-head 是否显示尾部默认true
* @property {String} thumb 缩略图路径如设置将显示在标题的左边不建议使用相对路径
* @property {String | Number} thumb-width 缩略图的宽度高等于宽单位rpx默认60
* @property {Boolean} thumb-circle 缩略图是否为圆形默认false
* @event {Function} click 整个卡片任意位置被点击时触发
* @event {Function} head-click 卡片头部被点击时触发
* @event {Function} body-click 卡片主体部分被点击时触发
* @event {Function} foot-click 卡片底部部分被点击时触发
* @example <u-card padding="30" title="card"></u-card>
*/
export default {
name: 'u-card',
props: {
//
full: {
type: Boolean,
default: false
},
//
title: {
type: String,
default: ''
},
//
titleColor: {
type: String,
default: '#303133'
},
// rpx
titleSize: {
type: [Number, String],
default: '30'
},
//
subTitle: {
type: String,
default: ''
},
//
subTitleColor: {
type: String,
default: '#909399'
},
// rpx
subTitleSize: {
type: [Number, String],
default: '26'
},
// full=false()
border: {
type: Boolean,
default: true
},
//
index: {
type: [Number, String, Object],
default: ''
},
// "30rpx 30rpx""20rpx 20rpx 30rpx 30rpx"
margin: {
type: String,
default: '30rpx'
},
// card
borderRadius: {
type: [Number, String],
default: '16'
},
//
headStyle: {
type: Object,
default() {
return {};
}
},
//
bodyStyle: {
type: Object,
default() {
return {};
}
},
//
footStyle: {
type: Object,
default() {
return {};
}
},
//
headBorderBottom: {
type: Boolean,
default: true
},
//
footBorderTop: {
type: Boolean,
default: true
},
//
thumb: {
type: String,
default: ''
},
// rpx
thumbWidth: {
type: [String, Number],
default: '60'
},
//
thumbCircle: {
type: Boolean,
default: false
},
// headbodyfoot
padding: {
type: [String, Number],
default: '30'
},
//
showHead: {
type: Boolean,
default: true
},
//
showFoot: {
type: Boolean,
default: true
},
//
boxShadow: {
type: String,
default: 'none'
}
},
data() {
return {};
},
methods: {
click() {
this.$emit('click', this.index);
},
headClick() {
this.$emit('head-click', this.index);
},
bodyClick() {
this.$emit('body-click', this.index);
},
footClick() {
this.$emit('foot-click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-card {
position: relative;
overflow: hidden;
font-size: 28rpx;
background-color: #ffffff;
box-sizing: border-box;
&-full {
// 0
margin-left: 0 !important;
margin-right: 0 !important;
width: 100%;
}
&--border:after {
border-radius: 16rpx;
}
&__head {
&--left {
color: $u-main-color;
&__thumb {
margin-right: 16rpx;
}
&__title {
max-width: 400rpx;
}
}
&--right {
color: $u-tips-color;
margin-left: 6rpx;
}
}
&__body {
color: $u-content-color;
}
&__foot {
color: $u-tips-color;
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<view class="u-cell-box">
<view class="u-cell-title" v-if="title" :style="[titleStyle]">
{{title}}
</view>
<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
<slot />
</view>
</view>
</template>
<script>
/**
* cellGroup 单元格父组件Group
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等搭配u-cell-item
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 分组标题
* @property {Boolean} border 是否显示外边框默认true
* @property {Object} title-style 分组标题的的样式对象形式{'font-size': '24rpx'} {'fontSize': '24rpx'}
* @example <u-cell-group title="设置喜好">
*/
export default {
name: "u-cell-group",
props: {
//
title: {
type: String,
default: ''
},
// list
border: {
type: Boolean,
default: true
},
//
// {'font-size': '24rpx'} {'fontSize': '24rpx'}
titleStyle: {
type: Object,
default () {
return {};
}
}
},
data() {
return {
index: 0,
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell-box {
width: 100%;
}
.u-cell-title {
padding: 30rpx 32rpx 10rpx 32rpx;
font-size: 30rpx;
text-align: left;
color: $u-tips-color;
}
.u-cell-item-box {
background-color: #FFFFFF;
flex-direction: row;
}
</style>
@@ -0,0 +1,316 @@
<template>
<view
@tap="click"
class="u-cell"
:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
hover-stay-time="150"
:hover-class="hoverClass"
:style="{
backgroundColor: bgColor
}"
>
<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
<view class="u-flex" v-else>
<slot name="icon"></slot>
</view>
<view
class="u-cell_title"
:style="[
{
width: titleWidth ? titleWidth + 'rpx' : 'auto'
},
titleStyle
]"
>
<block v-if="title !== ''">{{ title }}</block>
<slot name="title" v-else></slot>
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
<block v-if="label !== ''">{{ label }}</block>
<slot name="label" v-else></slot>
</view>
</view>
<view class="u-cell__value" :style="[valueStyle]">
<block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
<slot v-else></slot>
</view>
<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
<slot name="right-icon"></slot>
</view>
<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
</view>
</template>
<script>
/**
* cellItem 单元格Item
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等搭配u-cell-group使用
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 左侧标题
* @property {String} icon 左侧图标名只支持uView内置图标见Icon 图标
* @property {Object} icon-style 左边图标的样式对象形式
* @property {String} value 右侧内容
* @property {String} label 标题下方的描述信息
* @property {Boolean} border-bottom 是否显示cell的下边框默认true
* @property {Boolean} border-top 是否显示cell的上边框默认false
* @property {Boolean} center 是否使内容垂直居中默认false
* @property {String} hover-class 是否开启点击反馈none为无效果默认true
* // @property {Boolean} border-gap border-bottomtrueCelltrue
* @property {Boolean} arrow 是否显示右侧箭头默认true
* @property {Boolean} required 箭头方向可选值默认right
* @property {Boolean} arrow-direction 是否显示左边表示必填的星号默认false
* @property {Object} title-style 标题样式对象形式
* @property {Object} value-style 右侧内容样式对象形式
* @property {Object} label-style 标题下方描述信息的样式对象形式
* @property {String} bg-color 背景颜色默认transparent
* @property {String Number} index 用于在click事件回调中返回标识当前是第几个Item
* @property {String Number} title-width 标题的宽度单位rpx
* @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
*/
export default {
name: 'u-cell-item',
props: {
// (uView)src
icon: {
type: String,
default: ''
},
//
title: {
type: [String, Number],
default: ''
},
//
value: {
type: [String, Number],
default: ''
},
//
label: {
type: [String, Number],
default: ''
},
//
borderBottom: {
type: Boolean,
default: true
},
//
borderTop: {
type: Boolean,
default: false
},
// cellcell线线
// 1.4.0border-topborder-bottom
// borderGap: {
// type: Boolean,
// default: true
// },
// cellnone
hoverClass: {
type: String,
default: 'u-cell-hover'
},
//
arrow: {
type: Boolean,
default: true
},
//
center: {
type: Boolean,
default: false
},
//
required: {
type: Boolean,
default: false
},
// rpx
titleWidth: {
type: [Number, String],
default: ''
},
// right|up|downright
arrowDirection: {
type: String,
default: 'right'
},
//
titleStyle: {
type: Object,
default() {
return {};
}
},
//
valueStyle: {
type: Object,
default() {
return {};
}
},
//
labelStyle: {
type: Object,
default() {
return {};
}
},
//
bgColor: {
type: String,
default: 'transparent'
},
// cell
index: {
type: [String, Number],
default: ''
},
// 使lable
useLabelSlot: {
type: Boolean,
default: false
},
// rpxicon
iconSize: {
type: [Number, String],
default: 34
},
//
iconStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
};
},
computed: {
arrowStyle() {
let style = {};
if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
else style.transform = 'rotate(0deg)';
return style;
}
},
methods: {
click() {
this.$emit('click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell {
@include vue-flex;
align-items: center;
position: relative;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
width: 100%;
padding: 26rpx 32rpx;
font-size: 28rpx;
line-height: 54rpx;
color: $u-content-color;
background-color: #fff;
text-align: left;
}
.u-cell_title {
font-size: 28rpx;
}
.u-cell__left-icon-wrap {
margin-right: 10rpx;
font-size: 32rpx;
}
.u-cell__right-icon-wrap {
margin-left: 10rpx;
color: #969799;
font-size: 28rpx;
}
.u-cell__left-icon-wrap,
.u-cell__right-icon-wrap {
@include vue-flex;
align-items: center;
height: 48rpx;
}
.u-cell-border:after {
position: absolute;
/* #ifndef APP-NVUE */
box-sizing: border-box;
content: ' ';
pointer-events: none;
border-bottom: 1px solid $u-border-color;
/* #endif */
right: 0;
left: 0;
top: 0;
transform: scaleY(0.5);
}
.u-cell-border {
position: relative;
}
.u-cell__label {
margin-top: 6rpx;
font-size: 26rpx;
line-height: 36rpx;
color: $u-tips-color;
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
}
.u-cell__value {
overflow: hidden;
text-align: right;
/* #ifndef APP-NVUE */
vertical-align: middle;
/* #endif */
color: $u-tips-color;
font-size: 26rpx;
}
.u-cell__title,
.u-cell__value {
flex: 1;
}
.u-cell--required {
/* #ifndef APP-NVUE */
overflow: visible;
/* #endif */
@include vue-flex;
align-items: center;
}
.u-cell--required:before {
position: absolute;
/* #ifndef APP-NVUE */
content: '*';
/* #endif */
left: 8px;
margin-top: 4rpx;
font-size: 14px;
color: $u-type-error;
}
.u-cell_right {
line-height: 1;
}
</style>
@@ -0,0 +1,123 @@
<template>
<view class="u-checkbox-group u-clearfix">
<slot></slot>
</view>
</template>
<script>
import Emitter from '../../libs/util/emitter.js';
/**
* checkboxGroup 开关选择器父组件Group
* @description 复选框组件一般用于需要多个选择的场景该组件功能完整使用方便
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} max 最多能选中多少个checkbox默认999
* @property {String Number} size 组件整体的大小单位rpx默认40
* @property {Boolean} disabled 是否禁用所有checkbox默认false
* @property {String Number} icon-size 图标大小单位rpx默认20
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
* @property {String} width 宽度需带单位
* @property {String} width 宽度需带单位
* @property {String} shape 外观形状shape-方形circle-圆形(默认circle)
* @property {Boolean} wrap 是否每个checkbox都换行默认false
* @property {String} active-color 选中时的颜色应用到所有子Checkbox组件默认#2979ff
* @event {Function} change 任一个checkbox状态发生变化时触发回调为一个对象
* @example <u-checkbox-group></u-checkbox-group>
*/
export default {
name: 'u-checkbox-group',
mixins: [Emitter],
props: {
// checkbox
max: {
type: [Number, String],
default: 999
},
// name
// value: {
// default: Array,
// default() {
// return []
// }
// },
//
disabled: {
type: Boolean,
default: false
},
//
name: {
type: [Boolean, String],
default: ''
},
//
labelDisabled: {
type: Boolean,
default: false
},
// squarecircle
shape: {
type: String,
default: 'square'
},
//
activeColor: {
type: String,
default: '#2979ff'
},
//
size: {
type: [String, Number],
default: 34
},
// checkboxu-checkbox-group
width: {
type: String,
default: 'auto'
},
// checkbox
wrap: {
type: Boolean,
default: false
},
// rpx
iconSize: {
type: [String, Number],
default: 20
},
},
data() {
return {
}
},
created() {
// childrendata
this.children = [];
},
methods: {
emitEvent() {
let values = [];
this.children.map(val => {
if(val.value) values.push(val.name);
})
this.$emit('change', values);
// checkbox
//
setTimeout(() => {
// u-form-item
this.dispatch('u-form-item', 'on-form-change', values);
}, 60)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox-group {
/* #ifndef MP || APP-NVUE */
display: inline-flex;
flex-wrap: wrap;
/* #endif */
}
</style>
@@ -0,0 +1,284 @@
<template>
<view class="u-checkbox" :style="[checkboxStyle]">
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
<u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
</view>
<view class="u-checkbox__label" @tap="onClickLabel" :style="{
fontSize: $u.addUnit(labelSize)
}">
<slot />
</view>
</view>
</template>
<script>
/**
* checkbox 复选框
* @description 该组件需要搭配checkboxGroup组件使用以便用户进行操作时获得当前复选框组的选中情况
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} icon-size 图标大小单位rpx默认20
* @property {String Number} label-size label字体大小单位rpx默认28
* @property {String Number} name checkbox组件的标示符
* @property {String} shape 形状见官网说明默认circle
* @property {Boolean} disabled 是否禁用
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
* @property {String} active-color 选中时的颜色如设置CheckboxGroup的active-color将失效
* @event {Function} change 某个checkbox状态发生变化时触发回调为一个对象
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
*/
export default {
name: "u-checkbox",
props: {
// checkbox
name: {
type: [String, Number],
default: ''
},
// squarecircle
shape: {
type: String,
default: ''
},
//
value: {
type: Boolean,
default: false
},
//
disabled: {
type: [String, Boolean],
default: ''
},
//
labelDisabled: {
type: [String, Boolean],
default: ''
},
// checkboxGroupactiveColor
activeColor: {
type: String,
default: ''
},
// rpx
iconSize: {
type: [String, Number],
default: ''
},
// labelrpx
labelSize: {
type: [String, Number],
default: ''
},
//
size: {
type: [String, Number],
default: ''
},
},
data() {
return {
parentDisabled: false,
newParams: {},
};
},
created() {
// provide/inject使created
this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
// u-checkbox-groupthischildren
this.parent && this.parent.children.push(this);
},
computed: {
// u-checkbox-group
isDisabled() {
return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
},
// label
isLabelDisabled() {
return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
},
// size34rpx
checkboxSize() {
return this.size ? this.size : (this.parent ? this.parent.size : 34);
},
// 20
checkboxIconSize() {
return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
},
//
elActiveColor() {
return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
},
//
elShape() {
return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
},
iconStyle() {
let style = {};
// v-modelfalse
if (this.elActiveColor && this.value && !this.isDisabled) {
style.borderColor = this.elActiveColor;
style.backgroundColor = this.elActiveColor;
}
style.width = this.$u.addUnit(this.checkboxSize);
style.height = this.$u.addUnit(this.checkboxSize);
return style;
},
// checkbox
iconColor() {
return this.value ? '#ffffff' : 'transparent';
},
iconClass() {
let classes = [];
classes.push('u-checkbox__icon-wrap--' + this.elShape);
if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
// ","
return classes.join(' ');
},
checkboxStyle() {
let style = {};
if(this.parent && this.parent.width) {
style.width = this.parent.width;
// #ifdef MP
// 使float
style.float = 'left';
// #endif
// #ifndef MP
// H5APP使flex
style.flex = `0 0 ${this.parent.width}`;
// #endif
}
if(this.parent && this.parent.wrap) {
style.width = '100%';
// #ifndef MP
// H5APP使flex100%
style.flex = '0 0 100%';
// #endif
}
return style;
}
},
methods: {
onClickLabel() {
if (!this.isLabelDisabled && !this.isDisabled) {
this.setValue();
}
},
toggle() {
if (!this.isDisabled) {
this.setValue();
}
},
emitEvent() {
this.$emit('change', {
value: !this.value,
name: this.name
})
// u-checkbox-group
// this.$emit('input')
setTimeout(() => {
if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
}, 80);
},
// inputinputv-model
setValue() {
//
let checkedNum = 0;
if(this.parent && this.parent.children) {
// valuetrue1()
this.parent.children.map(val => {
if (val.value) checkedNum++;
})
}
//
if (this.value == true) {
this.emitEvent();
this.$emit('input', !this.value);
} else {
//
if(this.parent && checkedNum >= this.parent.max) {
return this.$u.toast(`最多可选${this.parent.max}`);
}
// max
this.emitEvent();
this.$emit('input', !this.value);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
overflow: hidden;
user-select: none;
line-height: 1.8;
&__icon-wrap {
color: $u-content-color;
flex: none;
display: -webkit-flex;
@include vue-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 42rpx;
height: 42rpx;
color: transparent;
text-align: center;
transition-property: color, border-color, background-color;
font-size: 20px;
border: 1px solid #c8c9cc;
transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 0
&__icon {
line-height: 0;
}
/* #endif */
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 6rpx;
}
&--checked {
color: #fff;
background-color: $u-type-primary;
border-color: $u-type-primary;
}
&--disabled {
background-color: #ebedf0;
border-color: #c8c9cc;
}
&--disabled--checked {
color: #c8c9cc !important;
}
}
&__label {
word-wrap: break-word;
margin-left: 10rpx;
margin-right: 24rpx;
color: $u-content-color;
font-size: 30rpx;
&--disabled {
color: #c8c9cc;
}
}
}
</style>
@@ -0,0 +1,220 @@
<template>
<view
class="u-circle-progress"
:style="{
width: widthPx + 'px',
height: widthPx + 'px',
backgroundColor: bgColor
}"
>
<!-- 支付宝小程序不支持canvas-id属性必须用id属性 -->
<canvas
class="u-canvas-bg"
:canvas-id="elBgId"
:id="elBgId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<canvas
class="u-canvas"
:canvas-id="elId"
:id="elId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<slot></slot>
</view>
</template>
<script>
/**
* circleProgress 环形进度条
* @description 展示操作或任务的当前进度比如上传文件是一个圆形的进度条注意此组件的percent值只能动态增加不能动态减少
* @tutorial https://www.uviewui.com/components/circleProgress.html
* @property {String Number} percent 圆环进度百分比值为数值类型0-100
* @property {String} inactive-color 圆环的底色默认为灰色(该值无法动态变更)默认#ececec
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)默认#19be6b
* @property {String Number} width 整个圆环组件的宽度高度默认等于宽度值单位rpx默认200
* @property {String Number} border-width 圆环的边框宽度单位rpx默认14
* @property {String Number} duration 整个圆环执行一圈的时间单位ms默认呢1500
* @property {String} type 如设置active-color值将会失效
* @property {String} bg-color 整个组件背景颜色默认为白色
* @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
*/
export default {
name: 'u-circle-progress',
props: {
//
percent: {
type: Number,
default: 0,
// 0100
validator: val => {
return val >= 0 && val <= 100;
}
},
//
inactiveColor: {
type: String,
default: '#ececec'
},
//
activeColor: {
type: String,
default: '#19be6b'
},
// 线rpx
borderWidth: {
type: [Number, String],
default: 14
},
// rpx
width: {
type: [Number, String],
default: 200
},
// ms
duration: {
type: [Number, String],
default: 1500
},
//
type: {
type: String,
default: ''
},
//
bgColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
// #ifdef MP-WEIXIN
elBgId: 'uCircleProgressBgId', // 使this.$u.guid()id
elId: 'uCircleProgressElId',
// #endif
// #ifndef MP-WEIXIN
elBgId: this.$u.guid(), // id
elId: this.$u.guid(),
// #endif
widthPx: uni.upx2px(this.width), // px
borderWidthPx: uni.upx2px(this.borderWidth), // px
startAngle: -Math.PI / 2, // canvas312
progressContext: null, // canvas
newPercent: 0, //
oldPercent: 0 //
};
},
watch: {
percent(nVal, oVal = 0) {
if (nVal > 100) nVal = 100;
if (nVal < 0) oVal = 0;
// this.percent
this.newPercent = nVal;
this.oldPercent = oVal;
setTimeout(() => {
//
//
this.drawCircleByProgress(oVal);
}, 50);
}
},
created() {
// 使
this.newPercent = this.percent;
this.oldPercent = 0;
},
computed: {
// type
circleColor() {
if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
else return this.activeColor;
}
},
mounted() {
// h5this.$nextTick()(HX2.4.7)
setTimeout(() => {
this.drawProgressBg();
this.drawCircleByProgress(this.oldPercent);
}, 50);
},
methods: {
drawProgressBg() {
let ctx = uni.createCanvasContext(this.elBgId, this);
ctx.setLineWidth(this.borderWidthPx); //
ctx.setStrokeStyle(this.inactiveColor); // 线
ctx.beginPath(); //
// (110,110)100
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
ctx.stroke(); //
ctx.draw();
},
drawCircleByProgress(progress) {
// this.data使
let ctx = this.progressContext;
if (!ctx) {
ctx = uni.createCanvasContext(this.elId, this);
this.progressContext = ctx;
}
//
ctx.setLineCap('round');
// 线
ctx.setLineWidth(this.borderWidthPx);
ctx.setStrokeStyle(this.circleColor);
// 100
let time = Math.floor(this.duration / 100);
// 2π100
// 312this.startAngle
let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
ctx.beginPath();
// canvas
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
ctx.stroke();
ctx.draw();
//
if (this.newPercent > this.oldPercent) {
//
progress++;
//
if (progress > this.newPercent) return;
} else {
//
progress--;
if (progress < this.newPercent) return;
}
setTimeout(() => {
// time
this.drawCircleByProgress(progress);
}, time);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-circle-progress {
position: relative;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
}
.u-canvas-bg {
position: absolute;
}
.u-canvas {
position: absolute;
}
</style>
@@ -0,0 +1,156 @@
<template>
<view class="u-col" :class="[
'u-col-' + span
]" :style="{
padding: `0 ${Number(gutter)/2 + 'rpx'}`,
marginLeft: 100 / 12 * offset + '%',
flex: `0 0 ${100 / 12 * span}%`,
alignItems: uAlignItem,
justifyContent: uJustify,
textAlign: textAlign
}"
@tap="click">
<slot></slot>
</view>
</template>
<script>
/**
* col 布局单元格
* @description 通过基础的 12 分栏迅速简便地创建布局搭配<u-row>使用
* @tutorial https://www.uviewui.com/components/layout.html
* @property {String Number} span 栅格占据的列数总12等分默认0
* @property {String} text-align 文字水平对齐方式默认left
* @property {String Number} offset 分栏左边偏移计算方式与span相同默认0
* @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
*/
export default {
name: "u-col",
props: {
// 12
span: {
type: [Number, String],
default: 12
},
// (12)
offset: {
type: [Number, String],
default: 0
},
// `start`(`flex-start`)`end`(`flex-end`)`center``around`(`space-around`)`between`(`space-between`)
justify: {
type: String,
default: 'start'
},
// topcenterbottom
align: {
type: String,
default: 'center'
},
//
textAlign: {
type: String,
default: 'left'
},
//
stop: {
type: Boolean,
default: true
}
},
data() {
return {
gutter: 20, // colu-row
}
},
created() {
this.parent = false;
},
mounted() {
//
this.parent = this.$u.$parent.call(this, 'u-row');
if (this.parent) {
this.gutter = this.parent.gutter;
}
},
computed: {
uJustify() {
if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
else return this.justify;
},
uAlignItem() {
if (this.align == 'top') return 'flex-start';
if (this.align == 'bottom') return 'flex-end';
else return this.align;
}
},
methods: {
click(e) {
this.$emit('click');
}
}
}
</script>
<style lang="scss">
@import "../../libs/css/style.components.scss";
.u-col {
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
float: left;
/* #endif */
}
.u-col-0 {
width: 0;
}
.u-col-1 {
width: calc(100%/12);
}
.u-col-2 {
width: calc(100%/12 * 2);
}
.u-col-3 {
width: calc(100%/12 * 3);
}
.u-col-4 {
width: calc(100%/12 * 4);
}
.u-col-5 {
width: calc(100%/12 * 5);
}
.u-col-6 {
width: calc(100%/12 * 6);
}
.u-col-7 {
width: calc(100%/12 * 7);
}
.u-col-8 {
width: calc(100%/12 * 8);
}
.u-col-9 {
width: calc(100%/12 * 9);
}
.u-col-10 {
width: calc(100%/12 * 10);
}
.u-col-11 {
width: calc(100%/12 * 11);
}
.u-col-12 {
width: calc(100%/12 * 12);
}
</style>
@@ -0,0 +1,204 @@
<template>
<view class="u-collapse-item" :style="[itemStyle]">
<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
<block v-if="!$slots['title-all']">
<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
isShow && activeStyle && !arrow ? activeStyle : '']">
{{ title }}
</view>
<slot v-else name="title" />
<view class="u-icon-wrap">
<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
class="u-arrow-down-icon" name="arrow-down"></u-icon>
</view>
</block>
<slot v-else name="title-all" />
</view>
<view class="u-collapse-body" :style="[{
height: isShow ? height + 'px' : '0'
}]">
<view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
/**
* collapseItem 手风琴Item
* @description 通过折叠面板收纳内容区域搭配u-collapse使用
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {String} title 面板标题
* @property {String Number} index 主要用于事件的回调标识那个Item被点击
* @property {Boolean} disabled 面板是否可以打开或收起默认false
* @property {Boolean} open 设置某个面板的初始状态是否打开默认false
* @property {String Number} name 唯一标识符如不设置默认用当前collapse-item的索引值
* @property {String} align 标题的对齐方式默认left
* @property {Object} active-style 不显示箭头时可以添加当前选择的collapse-item活动样式对象形式
* @event {Function} change 某个item被打开或者收起时触发
* @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
*/
export default {
name: "u-collapse-item",
props: {
//
title: {
type: String,
default: ''
},
//
align: {
type: String,
default: 'left'
},
//
disabled: {
type: Boolean,
default: false
},
// collapse
open: {
type: Boolean,
default: false
},
//
name: {
type: [Number, String],
default: ''
},
//
activeStyle: {
type: Object,
default () {
return {}
}
},
//
index: {
type: [String, Number],
default: ''
}
},
data() {
return {
isShow: false,
elId: this.$u.guid(),
height: 0, // body
headStyle: {}, //
bodyStyle: {}, //
itemStyle: {}, // item
arrowColor: '', //
hoverClass: '', //
arrow: true, //
};
},
watch: {
open(val) {
this.isShow = val;
}
},
created() {
this.parent = false;
// u-collapseu-collapse便u-collapse-item
this.isShow = this.open;
},
methods: {
//
init() {
this.parent = this.$u.$parent.call(this, 'u-collapse');
if(this.parent) {
this.nameSync = this.name ? this.name : this.parent.childrens.length;
this.parent.childrens.push(this);
this.headStyle = this.parent.headStyle;
this.bodyStyle = this.parent.bodyStyle;
this.arrowColor = this.parent.arrowColor;
this.hoverClass = this.parent.hoverClass;
this.arrow = this.parent.arrow;
this.itemStyle = this.parent.itemStyle;
}
this.$nextTick(() => {
this.queryRect();
});
},
// collapsehead
headClick() {
if (this.disabled) return;
if (this.parent && this.parent.accordion == true) {
this.parent.childrens.map(val => {
// falsethis.isShow = !this.isShow;
if (this != val) {
val.isShow = false;
}
});
}
this.isShow = !this.isShow;
//
this.$emit('change', {
index: this.index,
show: this.isShow
})
//
if (this.isShow) this.parent && this.parent.onChange();
this.$forceUpdate();
},
//
queryRect() {
// $uGetRectuViewhttps://www.uviewui.com/js/getRect.html
// this.$uGetRectthis.$u.getRect
this.$uGetRect('#' + this.elId).then(res => {
this.height = res.height;
})
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-collapse-head {
position: relative;
@include vue-flex;
justify-content: space-between;
align-items: center;
color: $u-main-color;
font-size: 30rpx;
line-height: 1;
padding: 24rpx 0;
text-align: left;
}
.u-collapse-title {
flex: 1;
overflow: hidden;
}
.u-arrow-down-icon {
transition: all 0.3s;
margin-right: 20rpx;
margin-left: 14rpx;
}
.u-arrow-down-icon-active {
transform: rotate(180deg);
transform-origin: center center;
}
.u-collapse-body {
overflow: hidden;
transition: all 0.3s;
}
.u-collapse-content {
overflow: hidden;
font-size: 28rpx;
color: $u-tips-color;
text-align: left;
}
</style>
@@ -0,0 +1,99 @@
<template>
<view class="u-collapse">
<slot />
</view>
</template>
<script>
/**
* collapse 手风琴
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {Boolean} accordion 是否手风琴模式默认true
* @property {Boolean} arrow 是否显示标题右侧的箭头默认true
* @property {String} arrow-color 标题右侧箭头的颜色默认#909399
* @property {Object} head-style 标题自定义样式对象形式
* @property {Object} body-style 主体自定义样式对象形式
* @property {String} hover-class 样式类名按下时有效默认u-hover-class
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式参数activeNames类型为String否则为Array)
* @example <u-collapse></u-collapse>
*/
export default {
name:"u-collapse",
props: {
//
accordion: {
type: Boolean,
default: true
},
//
headStyle: {
type: Object,
default () {
return {}
}
},
//
bodyStyle: {
type: Object,
default () {
return {}
}
},
// item
itemStyle: {
type: Object,
default () {
return {}
}
},
//
arrow: {
type: Boolean,
default: true
},
//
arrowColor: {
type: String,
default: '#909399'
},
// "none"
hoverClass: {
type: String,
default: 'u-hover-class'
}
},
created() {
this.childrens = []
},
data() {
return {
}
},
methods: {
//
init() {
this.childrens.forEach((vm, index) => {
vm.init();
})
},
// collapse itemcollapse item
onChange() {
let activeItem = [];
this.childrens.forEach((vm, index) => {
if (vm.isShow) {
activeItem.push(vm.nameSync);
}
})
// activeItem1
if (this.accordion) activeItem = activeItem.join('');
this.$emit('change', activeItem);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,237 @@
<template>
<view
class="u-notice-bar"
:style="{
background: computeBgColor,
padding: padding
}"
:class="[
type ? `u-type-${type}-light-bg` : ''
]"
>
<view class="u-icon-wrap">
<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
</view>
<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
<view
class="u-news-item u-line-1"
:style="[textStyle]"
@tap="click(index)"
:class="['u-type-' + type]"
>
{{ item }}
</view>
</swiper-item>
</swiper>
<view class="u-icon-wrap">
<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
</view>
</view>
</template>
<script>
export default {
props: {
//
list: {
type: Array,
default() {
return [];
}
},
// success|error|primary|info|warning
type: {
type: String,
default: 'warning'
},
//
volumeIcon: {
type: Boolean,
default: true
},
//
moreIcon: {
type: Boolean,
default: false
},
//
closeIcon: {
type: Boolean,
default: false
},
//
autoplay: {
type: Boolean,
default: true
},
// 使
color: {
type: String,
default: ''
},
//
bgColor: {
type: String,
default: ''
},
// row-column-
direction: {
type: String,
default: 'row'
},
//
show: {
type: Boolean,
default: true
},
// rpx
fontSize: {
type: [Number, String],
default: 26
},
// ms
duration: {
type: [Number, String],
default: 2000
},
//
volumeSize: {
type: [Number, String],
default: 34
},
// rpx
speed: {
type: Number,
default: 160
},
//
isCircular: {
type: Boolean,
default: true
},
// horizontal-vertical-
mode: {
type: String,
default: 'horizontal'
},
// play-paused-
playState: {
type: String,
default: 'play'
},
//
// HX2.6.11App 2.5.5+H5 2.5.5+
disableTouch: {
type: Boolean,
default: true
},
//
padding: {
type: [Number, String],
default: '18rpx 24rpx'
}
},
computed: {
// uview
computeColor() {
if (this.color) return this.color;
// 使content-color
else if(this.type == 'none') return '#606266';
else return this.type;
},
//
textStyle() {
let style = {};
if (this.color) style.color = this.color;
else if(this.type == 'none') style.color = '#606266';
style.fontSize = this.fontSize + 'rpx';
return style;
},
//
vertical() {
if(this.mode == 'horizontal') return false;
else return true;
},
//
computeBgColor() {
if (this.bgColor) return this.bgColor;
else if(this.type == 'none') return 'transparent';
}
},
data() {
return {
// animation: false
};
},
methods: {
//
click(index) {
this.$emit('click', index);
},
//
close() {
this.$emit('close');
},
//
getMore() {
this.$emit('getMore');
},
change(e) {
let index = e.detail.current;
if(index == this.list.length - 1) {
this.$emit('end');
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
padding: 18rpx 24rpx;
overflow: hidden;
}
.u-swiper {
font-size: 26rpx;
height: 32rpx;
@include vue-flex;
align-items: center;
flex: 1;
margin-left: 12rpx;
}
.u-swiper-item {
@include vue-flex;
align-items: center;
overflow: hidden;
}
.u-news-item {
overflow: hidden;
}
.u-right-icon {
margin-left: 12rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
.u-left-icon {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
</style>
@@ -0,0 +1,321 @@
<template>
<view class="u-countdown">
<view v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))" :style="{ fontSize: fontSize + 'rpx'}">
{{ d }}
</view>
<view
:style="{fontSize: separatorSize + 'rpx', 'margin-right': '6rpx' }"
v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
>
</view>
<view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
{{ h }}
</view>
</view>
<view
class="u-countdown-colon"
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
v-if="showHours"
>
{{ separator == 'colon' ? ':' : '时' }}
</view>
<view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
{{ i }}
</view>
</view>
<view
class="u-countdown-colon"
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
v-if="showMinutes"
>
{{ separator == 'colon' ? ':' : '分' }}
</view>
<view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
{{ s }}
</view>
</view>
<view
class="u-countdown-colon"
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
v-if="showSeconds && separator == 'zh'"
>
</view>
</view>
</template>
<script>
/**
* countDown 倒计时
* @description 该组件一般使用于某个活动的截止时间上通过数字的变化给用户明确的时间感受提示用户进行某一个行为操作
* @tutorial https://www.uviewui.com/components/countDown.html
* @property {String Number} timestamp 倒计时单位为秒
* @property {Boolean} autoplay 是否自动开始倒计时如果为false需手动调用开始方法见官网说明默认true
* @property {String} separator 分隔符colon为英文冒号zh为中文默认colon
* @property {String Number} separator-size 分隔符的字体大小单位rpx默认30
* @property {String} separator-color 分隔符的颜色默认#303133
* @property {String Number} font-size 倒计时字体大小单位rpx默认30
* @property {Boolean} show-border 是否显示倒计时数字的边框默认false
* @property {Boolean} hide-zero-day "天"的部分为0时隐藏该字段 默认true
* @property {String} border-color 数字边框的颜色默认#303133
* @property {String} bg-color 倒计时数字的背景颜色默认#ffffff
* @property {String} color 倒计时数字的颜色默认#303133
* @property {String} height 数字高度值(宽度等同此值)设置边框时看情况是否需要设置此值单位rpx默认auto
* @property {Boolean} show-days 是否显示倒计时的"天"部分默认true
* @property {Boolean} show-hours 是否显示倒计时的"时"部分默认true
* @property {Boolean} show-minutes 是否显示倒计时的"分"部分默认true
* @property {Boolean} show-seconds 是否显示倒计时的"秒"部分默认true
* @event {Function} end 倒计时结束
* @event {Function} change 每秒触发一次回调为当前剩余的倒计秒数
* @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
*/
export default {
name: 'u-count-down',
props: {
//
timestamp: {
type: [Number, String],
default: 0
},
//
autoplay: {
type: Boolean,
default: true
},
// (colon)(zh)false"11:22""1122"
separator: {
type: String,
default: 'colon'
},
// rpx
separatorSize: {
type: [Number, String],
default: 30
},
//
separatorColor: {
type: String,
default: "#303133"
},
//
color: {
type: String,
default: '#303133'
},
// rpx
fontSize: {
type: [Number, String],
default: 30
},
//
bgColor: {
type: String,
default: '#fff'
},
// rpx
height: {
type: [Number, String],
default: 'auto'
},
//
showBorder: {
type: Boolean,
default: false
},
//
borderColor: {
type: String,
default: '#303133'
},
//
showSeconds: {
type: Boolean,
default: true
},
//
showMinutes: {
type: Boolean,
default: true
},
//
showHours: {
type: Boolean,
default: true
},
//
showDays: {
type: Boolean,
default: true
},
// ""0
hideZeroDay: {
type: Boolean,
default: false
}
},
watch: {
//
timestamp(newVal, oldVal) {
//
this.clearTimer();
this.start();
}
},
data() {
return {
d: '00', //
h: '00', //
i: '00', //
s: '00', //
timer: null ,//
seconds: 0, //
};
},
computed: {
// itemitem
itemStyle() {
let style = {};
if(this.height) {
style.height = this.height + 'rpx';
style.width = this.height + 'rpx';
}
if(this.showBorder) {
style.borderStyle = 'solid';
style.borderColor = this.borderColor;
style.borderWidth = '1px';
}
if(this.bgColor) {
style.backgroundColor = this.bgColor;
}
return style;
},
//
letterStyle() {
let style = {};
if(this.fontSize) style.fontSize = this.fontSize + 'rpx';
if(this.color) style.color = this.color;
return style;
}
},
mounted() {
//
this.autoplay && this.timestamp && this.start();
},
methods: {
//
start() {
//
this.clearTimer();
if (this.timestamp <= 0) return;
this.seconds = Number(this.timestamp);
this.formatTime(this.seconds);
this.timer = setInterval(() => {
this.seconds--;
// change
this.$emit('change', this.seconds);
if (this.seconds < 0) {
return this.end();
}
this.formatTime(this.seconds);
}, 1000);
},
//
formatTime(seconds) {
// 0
seconds <= 0 && this.end();
let [day, hour, minute, second] = [0, 0, 0, 0];
day = Math.floor(seconds / (60 * 60 * 24));
//
// hour()
hour = Math.floor(seconds / (60 * 60)) - day * 24;
// showHour
let showHour = null;
if(this.showDays) {
showHour = hour;
} else {
//
showHour = Math.floor(seconds / (60 * 60));
}
minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
// 10"0"
showHour = showHour < 10 ? '0' + showHour : showHour;
minute = minute < 10 ? '0' + minute : minute;
second = second < 10 ? '0' + second : second;
day = day < 10 ? '0' + day : day;
this.d = day;
this.h = showHour;
this.i = minute;
this.s = second;
},
//
end() {
this.clearTimer();
this.$emit('end', {});
},
reset() {
this.clearTimer();
this.seconds = Number(this.timestamp);
this.s = this.timestamp;
console.log(this.s)
},
//
clearTimer() {
if(this.timer) {
//
clearInterval(this.timer);
this.timer = null;
}
}
},
beforeDestroy() {
clearInterval(this.timer);
this.timer = null;
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-countdown {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
.u-countdown-item {
@include vue-flex;
align-items: center;
justify-content: center;
padding: 2rpx;
border-radius: 6rpx;
white-space: nowrap;
transform: translateZ(0);
}
.u-countdown-time {
margin: 0;
padding: 0;
}
.u-countdown-colon {
@include vue-flex;
justify-content: center;
padding: 0 5rpx;
line-height: 1;
align-items: center;
padding-bottom: 4rpx;
}
.u-countdown-scale {
transform: scale(0.9);
transform-origin: center center;
}
</style>
@@ -0,0 +1,241 @@
<template>
<view
class="u-count-num"
:style="{
fontSize: fontSize + 'rpx',
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>
{{ displayValue }}
</view>
</template>
<script>
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景目标要求是一个递增的值
* @tutorial https://www.uviewui.com/components/countTo.html
* @property {String Number} start-val 开始值
* @property {String Number} end-val 结束值
* @property {String Number} duration 滚动过程所需的时间单位ms默认2000
* @property {Boolean} autoplay 是否自动开始滚动默认true
* @property {String Number} decimals 要显示的小数位数见官网说明默认0
* @property {Boolean} use-easing 滚动结束时是否缓动结尾见官网说明默认true
* @property {String} separator 千位分隔符见官网说明
* @property {String} color 字体颜色默认#303133
* @property {String Number} font-size 字体大小单位rpx默认50
* @property {Boolean} bold 字体是否加粗默认false
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
*/
export default {
name: 'u-count-to',
props: {
// 0
startVal: {
type: [Number, String],
default: 0
},
//
endVal: {
type: [Number, String],
default: 0,
required: true
},
// ms
duration: {
type: [Number, String],
default: 2000
},
//
autoplay: {
type: Boolean,
default: true
},
//
decimals: {
type: [Number, String],
default: 0
},
// 使
useEasing: {
type: Boolean,
default: true
},
//
decimal: {
type: [Number, String],
default: '.'
},
//
color: {
type: String,
default: '#303133'
},
//
fontSize: {
type: [Number, String],
default: 50
},
//
bold: {
type: Boolean,
default: false
},
// (23,321.05",")
separator: {
type: String,
default: ''
}
},
data() {
return {
localStartVal: this.startVal,
displayValue: this.formatNumber(this.startVal),
printVal: null,
paused: false, //
localDuration: Number(this.duration),
startTime: null, //
timestamp: null, //
remaining: null, //
rAF: null,
lastTime: 0 //
};
},
computed: {
countDown() {
return this.startVal > this.endVal;
}
},
watch: {
startVal() {
this.autoplay && this.start();
},
endVal() {
this.autoplay && this.start();
}
},
mounted() {
this.autoplay && this.start();
},
methods: {
easingFn(t, b, c, d) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
},
requestAnimationFrame(callback) {
const currTime = new Date().getTime();
// 使setTimteout60
const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
const id = setTimeout(() => {
callback(currTime + timeToCall);
}, timeToCall);
this.lastTime = currTime + timeToCall;
return id;
},
cancelAnimationFrame(id) {
clearTimeout(id);
},
//
start() {
this.localStartVal = this.startVal;
this.startTime = null;
this.localDuration = this.duration;
this.paused = false;
this.rAF = this.requestAnimationFrame(this.count);
},
//
reStart() {
if (this.paused) {
this.resume();
this.paused = false;
} else {
this.stop();
this.paused = true;
}
},
//
stop() {
this.cancelAnimationFrame(this.rAF);
},
// ()
resume() {
this.startTime = null;
this.localDuration = this.remaining;
this.localStartVal = this.printVal;
this.requestAnimationFrame(this.count);
},
//
reset() {
this.startTime = null;
this.cancelAnimationFrame(this.rAF);
this.displayValue = this.formatNumber(this.startVal);
},
count(timestamp) {
if (!this.startTime) this.startTime = timestamp;
this.timestamp = timestamp;
const progress = timestamp - this.startTime;
this.remaining = this.localDuration - progress;
if (this.useEasing) {
if (this.countDown) {
this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
} else {
this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
}
} else {
if (this.countDown) {
this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
} else {
this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
}
}
if (this.countDown) {
this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
} else {
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
}
this.displayValue = this.formatNumber(this.printVal);
if (progress < this.localDuration) {
this.rAF = this.requestAnimationFrame(this.count);
} else {
this.$emit('end');
}
},
//
isNumber(val) {
return !isNaN(parseFloat(val));
},
formatNumber(num) {
// numNumbertoFixed
num = Number(num);
num = num.toFixed(Number(this.decimals));
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? this.decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (this.separator && !this.isNumber(this.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + this.separator + '$2');
}
}
return x1 + x2;
},
destroyed() {
this.cancelAnimationFrame(this.rAF);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
}
</style>
@@ -0,0 +1,153 @@
<template>
<view class="u-divider" :style="{
height: height == 'auto' ? 'auto' : height + 'rpx',
backgroundColor: bgColor,
marginBottom: marginBottom + 'rpx',
marginTop: marginTop + 'rpx'
}" @tap="click">
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
<view v-if="useSlot" class="u-divider-text" :style="{
color: color,
fontSize: fontSize + 'rpx'
}"><slot /></view>
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
</view>
</template>
<script>
/**
* divider 分割线
* @description 区隔内容的分割线一般用于页面底部"没有更多"的提示
* @tutorial https://www.uviewui.com/components/divider.html
* @property {String Number} half-width 文字左或右边线条宽度数值或百分比数值时单位为rpx
* @property {String} border-color 线条颜色优先级高于type默认#dcdfe6
* @property {String} color 文字颜色默认#909399
* @property {String Number} fontSize 字体大小单位rpx默认26
* @property {String} bg-color 整个divider的背景颜色默认呢#ffffff
* @property {String Number} height 整个divider的高度单位rpx默认40
* @property {String} type 将线条设置主题色默认primary
* @property {Boolean} useSlot 是否使用slot传入内容如果不传入中间不会有空隙默认true
* @property {String Number} margin-top 与前一个组件的距离单位rpx默认0
* @property {String Number} margin-bottom 与后一个组件的距离单位rpx0
* @event {Function} click divider组件被点击时触发
* @example <u-divider color="#fa3534">长河落日圆</u-divider>
*/
export default {
name: 'u-divider',
props: {
// divider线()rpx
halfWidth: {
type: [Number, String],
default: 150
},
// divider线
borderColor: {
type: String,
default: '#dcdfe6'
},
// primary|info|success|warning|error
type: {
type: String,
default: 'primary'
},
//
color: {
type: String,
default: '#909399'
},
// rpx
fontSize: {
type: [Number, String],
default: 26
},
// divider
bgColor: {
type: String,
default: '#ffffff'
},
// dividerrpx
height: {
type: [Number, String],
default: 'auto'
},
//
marginTop: {
type: [String, Number],
default: 0
},
//
marginBottom: {
type: [String, Number],
default: 0
},
// 使slotslot
useSlot: {
type: Boolean,
default: true
}
},
computed: {
lineStyle() {
let style = {};
if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
else style.width = this.halfWidth + 'rpx';
// borderColortype
if(this.borderColor) style.borderColor = this.borderColor;
return style;
}
},
methods: {
click() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-divider {
width: 100%;
position: relative;
text-align: center;
@include vue-flex;
justify-content: center;
align-items: center;
overflow: hidden;
flex-direction: row;
}
.u-divider-line {
border-bottom: 1px solid $u-border-color;
transform: scale(1, 0.5);
transform-origin: center;
&--bordercolor--primary {
border-color: $u-type-primary;
}
&--bordercolor--success {
border-color: $u-type-success;
}
&--bordercolor--error {
border-color: $u-type-primary;
}
&--bordercolor--info {
border-color: $u-type-info;
}
&--bordercolor--warning {
border-color: $u-type-warning;
}
}
.u-divider-text {
white-space: nowrap;
padding: 0 16rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
</style>
@@ -0,0 +1,132 @@
<template>
<view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
<block v-if="!$slots.default && !$slots.$default">
<scroll-view scroll-y="true" :style="{
height: $u.addUnit(height)
}">
<view class="u-dropdown-item__options">
<u-cell-group>
<u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
:key="index" :title-style="{
color: value == item.value ? activeColor : inactiveColor
}">
<u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
</u-cell-item>
</u-cell-group>
</view>
</scroll-view>
</block>
<slot v-else />
</view>
</template>
<script>
/**
* dropdown-item 下拉菜单
* @description 该组件一般用于向下展开菜单同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String | Number} v-model 双向绑定选项卡选择值
* @property {String} title 菜单项标题
* @property {Array[Object]} options 选项数据如果传入了默认slot此参数无效
* @property {Boolean} disabled 是否禁用此选项卡默认false
* @property {String | Number} duration 选项卡展开和收起的过渡时间单位ms默认300
* @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)默认auto
* @example <u-dropdown-item title="标题"></u-dropdown-item>
*/
export default {
name: 'u-dropdown-item',
props: {
// value
value: {
type: [Number, String, Array],
default: ''
},
//
title: {
type: [String, Number],
default: ''
},
// slot
options: {
type: Array,
default () {
return []
}
},
//
disabled: {
type: Boolean,
default: false
},
//
height: {
type: [Number, String],
default: 'auto'
},
},
data() {
return {
active: false, //
activeColor: '#2979ff', //
inactiveColor: '#606266', //
}
},
computed: {
// propsu-dropdown
propsChange() {
return `${this.title}-${this.disabled}`;
}
},
watch: {
propsChange(n) {
// init()
//
if (this.parent) this.parent.init();
}
},
created() {
//
this.parent = false;
},
methods: {
init() {
// u-dropdown
let parent = this.$u.$parent.call(this, 'u-dropdown');
if (parent) {
this.parent = parent;
//
this.activeColor = parent.activeColor;
this.inactiveColor = parent.inactiveColor;
// thischildren()
// push
let exist = parent.children.find(val => {
return this === val;
})
if (!exist) parent.children.push(this);
if (parent.children.length == 1) this.active = true;
// childrentitlemenuList
parent.menuList.push({
title: this.title,
disabled: this.disabled
});
}
},
// cell
cellClick(value) {
// v-model
this.$emit('input', value);
// (u-dropdown)
this.parent.close();
// value
this.$emit('change', value);
}
},
mounted() {
this.init();
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,298 @@
<template>
<view class="u-dropdown">
<view class="u-dropdown__menu" :style="{
height: $u.addUnit(height)
}" :class="{
'u-border-bottom': borderBottom
}">
<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
<view class="u-flex">
<text class="u-dropdown__menu__item__text" :style="{
color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
fontSize: $u.addUnit(titleSize)
}">{{item.title}}</text>
<view class="u-dropdown__menu__item__arrow" :class="{
'u-dropdown__menu__item__arrow--rotate': index === current
}">
<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
</view>
</view>
</view>
</view>
<view class="u-dropdown__content" :style="[contentStyle, {
transition: `opacity ${duration / 1000}s linear`,
top: $u.addUnit(height),
height: contentHeight + 'px'
}]"
@tap="maskClick" @touchmove.stop.prevent>
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
<slot></slot>
</view>
<view class="u-dropdown__content__mask"></view>
</view>
</view>
</template>
<script>
/**
* dropdown 下拉菜单
* @description 该组件一般用于向下展开菜单同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String} active-color 标题和选项卡选中的颜色默认#2979ff
* @property {String} inactive-color 标题和选项卡未选中的颜色默认#606266
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单默认true
* @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单默认true
* @property {String | Number} duration 选项卡展开和收起的过渡时间单位ms默认300
* @property {String | Number} height 标题菜单的高度单位任意默认80
* @property {String | Number} border-radius 菜单展开内容下方的圆角值单位任意默认0
* @property {Boolean} border-bottom 标题菜单是否显示下边框默认false
* @property {String | Number} title-size 标题的字体大小单位任意数值默认为rpx单位默认28
* @event {Function} open 下拉菜单被打开时触发
* @event {Function} close 下拉菜单被关闭时触发
* @example <u-dropdown></u-dropdown>
*/
export default {
name: 'u-dropdown',
props: {
//
activeColor: {
type: String,
default: '#2979ff'
},
//
inactiveColor: {
type: String,
default: '#606266'
},
//
closeOnClickMask: {
type: Boolean,
default: true
},
//
closeOnClickSelf: {
type: Boolean,
default: true
},
//
duration: {
type: [Number, String],
default: 300
},
// rpx
height: {
type: [Number, String],
default: 80
},
//
borderBottom: {
type: Boolean,
default: false
},
//
titleSize: {
type: [Number, String],
default: 28
},
//
borderRadius: {
type: [Number, String],
default: 0
},
// icon
menuIcon: {
type: String,
default: 'arrow-down'
},
//
menuIconSize: {
type: [Number, String],
default: 26
}
},
data() {
return {
showDropdown: true, // ,
menuList: [], //
active: false, //
// false""current0
// TX使===使==
current: 99999,
//
contentStyle: {
zIndex: -1,
opacity: 0
},
//
highlightIndex: 99999,
contentHeight: 0
}
},
computed: {
//
popupStyle() {
let style = {};
// Y100%
style.transform = `translateY(${this.active ? 0 : '-100%'})`
style['transition-duration'] = this.duration / 1000 + 's';
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
return style;
}
},
created() {
// (u-dropdown-item)thisdata
this.children = [];
},
mounted() {
this.getContentHeight();
},
methods: {
init() {
// init
//
this.menuList = [];
this.children.map(child => {
child.init();
})
},
//
menuClick(index) {
//
if (this.menuList[index].disabled) return;
//
if (index === this.current && this.closeOnClickSelf) {
this.close();
//
setTimeout(() => {
this.children[index].active = false;
}, this.duration)
return;
}
this.open(index);
},
//
open(index) {
//
// this.highlightIndex = 9999;
//
this.contentStyle = {
zIndex: 11,
}
//
this.active = true;
this.current = index;
// v-if
// display: nonenvuedisplay
this.children.map((val, idx) => {
val.active = index == idx ? true : false;
})
this.$emit('open', this.current);
},
//
close() {
this.$emit('close', this.current);
// current
this.active = false;
this.current = 99999;
// 0
this.contentStyle = {
zIndex: -1,
opacity: 0
}
},
//
maskClick() {
//
if (!this.closeOnClickMask) return;
this.close();
},
//
highlight(index = undefined) {
this.highlightIndex = index !== undefined ? index : 99999;
},
//
getContentHeight() {
// dropdown
//
// this.$u.sys()uView
let windowHeight = this.$u.sys().windowHeight;
this.$uGetRect('.u-dropdown__menu').then(res => {
// dropdownH5uniappbug(bughx2.8.11)
// H5bugtop沿bottom
// H5uni
// bottonres.top
this.contentHeight = windowHeight - res.bottom;
})
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-dropdown {
flex: 1;
width: 100%;
position: relative;
&__menu {
@include vue-flex;
position: relative;
z-index: 11;
height: 80rpx;
&__item {
flex: 1;
@include vue-flex;
justify-content: center;
align-items: center;
&__text {
font-size: 28rpx;
color: $u-content-color;
}
&__arrow {
margin-left: 6rpx;
transition: transform .3s;
align-items: center;
@include vue-flex;
&--rotate {
transform: rotate(180deg);
}
}
}
}
&__content {
position: absolute;
z-index: 8;
width: 100%;
left: 0px;
bottom: 0;
overflow: hidden;
&__mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, .3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
&__popup {
position: relative;
z-index: 10;
transition: all 0.3s;
transform: translate3D(0, -100%, 0);
overflow: hidden;
}
}
}
</style>
@@ -0,0 +1,193 @@
<template>
<view class="u-empty" v-if="show" :style="{
marginTop: marginTop + 'rpx'
}">
<u-icon
:name="src ? src : 'empty-' + mode"
:custom-style="iconStyle"
:label="text ? text : icons[mode]"
label-pos="bottom"
:label-color="color"
:label-size="fontSize"
:size="iconSize"
:color="iconColor"
margin-top="14"
></u-icon>
<view class="u-slot-wrap">
<slot name="bottom"></slot>
</view>
</view>
</template>
<script>
/**
* empty 内容为空
* @description 该组件用于需要加载内容但是加载的第一页数据就为空提示一个"没有内容"的场景 我们精心挑选了十几个场景的图标方便您使用
* @tutorial https://www.uviewui.com/components/empty.html
* @property {String} color 文字颜色默认#c0c4cc
* @property {String} text 文字提示默认无内容
* @property {String} src 自定义图标路径如定义mode参数会失效
* @property {String Number} font-size 提示文字的大小单位rpx默认28
* @property {String} mode 内置的图标见官网说明默认data
* @property {String Number} img-width 图标的宽度单位rpx默认240
* @property {String} img-height 图标的高度单位rpx默认auto
* @property {String Number} margin-top 组件距离上一个元素之间的距离默认0
* @property {Boolean} show 是否显示组件默认true
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
*/
export default {
name: "u-empty",
props: {
//
src: {
type: String,
default: ''
},
//
text: {
type: String,
default: ''
},
//
color: {
type: String,
default: '#c0c4cc'
},
//
iconColor: {
type: String,
default: '#c0c4cc'
},
//
iconSize: {
type: [String, Number],
default: 120
},
// rpx
fontSize: {
type: [String, Number],
default: 26
},
//
mode: {
type: String,
default: 'data'
},
// rpx
imgWidth: {
type: [String, Number],
default: 120
},
// rpx
imgHeight: {
type: [String, Number],
default: 'auto'
},
//
show: {
type: Boolean,
default: true
},
//
marginTop: {
type: [String, Number],
default: 0
},
iconStyle: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
icons: {
car: '购物车为空',
page: '页面不存在',
search: '没有搜索结果',
address: '没有收货地址',
wifi: '没有WiFi',
order: '订单为空',
coupon: '没有优惠券',
favor: '暂无收藏',
permission: '无权限',
history: '无历史记录',
news: '无新闻列表',
message: '消息列表为空',
list: '列表为空',
data: '数据为空'
},
// icons: [{
// icon: 'car',
// text: ''
// },{
// icon: 'page',
// text: ''
// },{
// icon: 'search',
// text: ''
// },{
// icon: 'address',
// text: ''
// },{
// icon: 'wifi',
// text: 'WiFi'
// },{
// icon: 'order',
// text: ''
// },{
// icon: 'coupon',
// text: ''
// },{
// icon: 'favor',
// text: ''
// },{
// icon: 'permission',
// text: ''
// },{
// icon: 'history',
// text: ''
// },{
// icon: 'news',
// text: ''
// },{
// icon: 'message',
// text: ''
// },{
// icon: 'list',
// text: ''
// },{
// icon: 'data',
// text: ''
// }],
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-empty {
@include vue-flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.u-image {
margin-bottom: 20rpx;
}
.u-slot-wrap {
@include vue-flex;
justify-content: center;
align-items: center;
margin-top: 20rpx;
}
</style>
@@ -0,0 +1,384 @@
<template>
<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
<view class="u-label" :class="[required ? 'u-required' : '']" :style="{
justifyContent: justifyContent,
flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
}">
<view class="u-icon-wrap" v-if="icon">
<u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
</view>
<slot name="icon"></slot>
<text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
</view>
<view class="fild-body">
<view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
:placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
:focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
@tap="fieldClick" />
<input
v-else
:style="[fieldStyle]"
:type="type"
class="u-flex-1 u-field__input-wrap"
:value="value"
:password="password || this.type === 'password'"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
:maxlength="inputMaxlength"
:focus="focus"
:confirmType="confirmType"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
@confirm="onConfirm"
@tap="fieldClick"
/>
</view>
<u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
<view class="u-button-wrap"><slot name="right" /></view>
<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
</view>
</view>
<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
paddingLeft: labelWidth + 'rpx'
}">{{ errorMessage }}</view>
</view>
</template>
<script>
/**
* field 输入框
* @description 借助此组件可以实现表单的输入 "text""textarea"类型的此外借助uView的picker和actionSheet组件可以快速实现上拉菜单时间地区选择等 为表单解决方案的利器
* @tutorial https://www.uviewui.com/components/field.html
* @property {String} type 输入框的类型默认text
* @property {String} icon label左边的图标限uView的图标名称
* @property {Object} icon-style 左边图标的样式对象形式
* @property {Boolean} right-icon 输入框右边的图标名称限uView的图标名称默认false
* @property {Boolean} required 是否必填左边您显示红色"*"默认false
* @property {String} label 输入框左边的文字提示
* @property {Boolean} password 是否密码输入方式(用点替换文字)type为text时有效默认false
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容且获得焦点时才显示)点击可清空输入框内容默认true
* @property {Number String} label-width label的宽度单位rpx默认130
* @property {String} label-align label的文字对齐方式默认left
* @property {Object} field-style 自定义输入框的样式对象形式
* @property {Number | String} clear-size 清除图标的大小单位rpx默认30
* @property {String} input-align 输入框内容对齐方式默认left
* @property {Boolean} border-bottom 是否显示field的下边框默认true
* @property {Boolean} border-top 是否显示field的上边框默认false
* @property {String} icon-color 左边通过icon配置的图标的颜色默认#606266
* @property {Boolean} auto-height 是否自动增高输入区域type为textarea时有效默认true
* @property {String Boolean} error-message 显示的错误提示内容如果为空字符串或者false则不显示错误信息
* @property {String} placeholder 输入框的提示文字
* @property {String} placeholder-style placeholder的样式(内联样式字符串)"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点默认false
* @property {Boolean} fixed 如果type为textarea且在一个"position:fixed"的区域需要指明为true默认false
* @property {Boolean} disabled 是否不可输入默认false
* @property {Number String} maxlength 最大输入长度设置为 -1 的时候不限制最大长度默认140
* @property {String} confirm-type 设置键盘右下角按钮的文字仅在type="text"时生效默认done
* @event {Function} input 输入框内容发生变化时触发
* @event {Function} focus 输入框获得焦点时触发
* @event {Function} blur 输入框失去焦点时触发
* @event {Function} confirm 点击完成按钮时触发
* @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
* @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发这样设计是考虑到传递右边的图标一般都为需要弹出"picker"等操作时的场景点击倒三角图标理应发出此事件见上方说明
* @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
*/
export default {
name:"u-field",
props: {
icon: String,
rightIcon: String,
// arrowDirection: {
// type: String,
// default: 'right'
// },
required: Boolean,
label: String,
password: Boolean,
clearable: {
type: Boolean,
default: true
},
// rpx
labelWidth: {
type: [Number, String],
default: 130
},
// left|center|right
labelAlign: {
type: String,
default: 'left'
},
inputAlign: {
type: String,
default: 'left'
},
iconColor: {
type: String,
default: '#606266'
},
autoHeight: {
type: Boolean,
default: true
},
errorMessage: {
type: [String, Boolean],
default: ''
},
placeholder: String,
placeholderStyle: String,
focus: Boolean,
fixed: Boolean,
value: [Number, String],
type: {
type: String,
default: 'text'
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
confirmType: {
type: String,
default: 'done'
},
// lable left-top-
labelPosition: {
type: String,
default: 'left'
},
//
fieldStyle: {
type: Object,
default() {
return {}
}
},
//
clearSize: {
type: [Number, String],
default: 30
},
// lable
iconStyle: {
type: Object,
default() {
return {}
}
},
//
borderTop: {
type: Boolean,
default: false
},
//
borderBottom: {
type: Boolean,
default: true
},
//
trim: {
type: Boolean,
default: true
}
},
data() {
return {
focused: false,
itemIndex: 0,
};
},
computed: {
inputWrapStyle() {
let style = {};
style.textAlign = this.inputAlign;
// lableleftinput
if(this.labelPosition == 'left') {
style.margin = `0 8rpx`;
} else {
// labletopinput
style.marginRight = `8rpx`;
}
return style;
},
rightIconStyle() {
let style = {};
if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
else style.transform = 'roate(0deg)';
return style;
},
labelStyle() {
let style = {};
if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
if(this.labelAlign == 'center') style.justifyContent = 'center';
if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
return style;
},
// unicomputedstyle.justifyContent = 'center'
justifyContent() {
if(this.labelAlign == 'left') return 'flex-start';
if(this.labelAlign == 'center') return 'center';
if(this.labelAlign == 'right') return 'flex-end';
},
// uniappinputmaxlength
inputMaxlength() {
return Number(this.maxlength)
},
// label
fieldInnerStyle() {
let style = {};
if(this.labelPosition == 'left') {
style.flexDirection = 'row';
} else {
style.flexDirection = 'column';
}
return style;
}
},
methods: {
onInput(event) {
let value = event.detail.value;
//
if(this.trim) value = this.$u.trim(value);
this.$emit('input', value);
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
// 使@touchstarthx2.8.4
// @blur
setTimeout(() => {
this.focused = false;
}, 100)
this.$emit('blur', event);
},
onConfirm(e) {
this.$emit('confirm', e.detail.value);
},
onClear(event) {
this.$emit('input', '');
},
rightIconClick() {
this.$emit('right-icon-click');
this.$emit('click');
},
fieldClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-field {
font-size: 28rpx;
padding: 20rpx 28rpx;
text-align: left;
position: relative;
color: $u-main-color;
}
.u-field-inner {
@include vue-flex;
align-items: center;
}
.u-textarea-inner {
align-items: flex-start;
}
.u-textarea-class {
min-height: 96rpx;
width: auto;
font-size: 28rpx;
}
.fild-body {
@include vue-flex;
flex: 1;
align-items: center;
}
.u-arror-right {
margin-left: 8rpx;
}
.u-label-text {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
.u-label-left-gap {
margin-left: 6rpx;
}
.u-label-postion-top {
flex-direction: column;
align-items: flex-start;
}
.u-label {
width: 130rpx;
flex: 1 1 130rpx;
text-align: left;
position: relative;
@include vue-flex;
align-items: center;
}
.u-required::before {
content: '*';
position: absolute;
left: -16rpx;
font-size: 14px;
color: $u-type-error;
height: 9px;
line-height: 1;
}
.u-field__input-wrap {
position: relative;
overflow: hidden;
font-size: 28rpx;
height: 48rpx;
flex: 1;
width: auto;
}
.u-clear-icon {
@include vue-flex;
align-items: center;
}
.u-error-message {
color: $u-type-error;
font-size: 26rpx;
text-align: left;
}
.placeholder-style {
color: rgb(150, 151, 153);
}
.u-input-class {
font-size: 28rpx;
}
.u-button-wrap {
margin-left: 8rpx;
}
</style>
@@ -0,0 +1,431 @@
<template>
<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
<view class="u-form-item__body" :style="{
flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
}">
<!-- 微信小程序中将一个参数设置空字符串结果会变成字符串"true" -->
<view class="u-form-item--left" :style="{
width: uLabelWidth,
flex: `0 0 ${uLabelWidth}`,
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
}">
<!-- 为了块对齐 -->
<view class="u-form-item--left__content" v-if="required || leftIcon || label">
<!-- nvue不支持伪元素before -->
<text v-if="required" class="u-form-item--left__content--required">*</text>
<view class="u-form-item--left__content__icon" v-if="leftIcon">
<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
</view>
<view class="u-form-item--left__content__label" :style="[elLabelStyle, {
'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
}]">
{{label}}
</view>
</view>
</view>
<view class="u-form-item--right u-flex">
<view class="u-form-item--right__content">
<view class="u-form-item--right__content__slot ">
<slot />
</view>
<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
<slot name="right" />
</view>
</view>
</view>
</view>
<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
}">{{validateMessage}}</view>
</view>
</template>
<script>
import Emitter from '../../libs/util/emitter.js';
import schema from '../../libs/util/async-validator';
//
schema.warning = function() {};
/**
* form-item 表单item
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等
* @tutorial http://uviewui.com/components/form.html
* @property {String} label 左侧提示文字
* @property {Object} prop 表单域model对象的属性名在使用 validateresetFields 方法的情况下该属性是必填的
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
* @property {String} label-position 表单域提示文字的位置left-左侧top-上方
* @property {String Number} label-width 提示文字的宽度单位rpx默认90
* @property {Object} label-style lable的样式对象形式
* @property {String} label-align lable的对齐方式
* @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
* @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
* @property {Object} left-icon-style 左侧图标的样式对象形式
* @property {Object} right-icon-style 右侧图标的样式对象形式
* @property {Boolean} required 是否显示左边的"*"这里仅起展示作用如需校验必填请通过rules配置必填规则(默认false)
* @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
*/
export default {
name: 'u-form-item',
mixins: [Emitter],
inject: {
uForm: {
default () {
return null
}
}
},
props: {
// inputlabel
label: {
type: String,
default: ''
},
//
prop: {
type: String,
default: ''
},
// 线
borderBottom: {
type: [String, Boolean],
default: ''
},
// labelleft-top-
labelPosition: {
type: String,
default: ''
},
// labelrpx
labelWidth: {
type: [String, Number],
default: ''
},
// lable
labelStyle: {
type: Object,
default () {
return {}
}
},
// lable
labelAlign: {
type: String,
default: ''
},
//
rightIcon: {
type: String,
default: ''
},
//
leftIcon: {
type: String,
default: ''
},
//
leftIconStyle: {
type: Object,
default () {
return {}
}
},
//
rightIconStyle: {
type: Object,
default () {
return {}
}
},
// rules
required: {
type: Boolean,
default: false
}
},
data() {
return {
initialValue: '', //
// isRequired: false, // "*"propsrequiredrules
validateState: '', //
validateMessage: '', //
// message-border-input
errorType: ['message'],
fieldValue: '', // input
// computedthis.parentdata
parentData: {
borderBottom: true,
labelWidth: 90,
labelPosition: 'left',
labelStyle: {},
labelAlign: 'left',
}
};
},
watch: {
validateState(val) {
this.broadcastInputError();
},
// u-formerrorType
"uForm.errorType"(val) {
this.errorType = val;
this.broadcastInputError();
},
},
computed: {
// labelcomputed
uLabelWidth() {
// label('true')labelauto
return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
.elLabelWidth)) : '100%';
},
showError() {
return type => {
// errorTypenonetoast
if (this.errorType.indexOf('none') >= 0) return false;
else if (this.errorType.indexOf(type) >= 0) return true;
else return false;
}
},
// label
elLabelWidth() {
// label90使(0)u-form
return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
.labelWidth :
90);
},
// label
elLabelStyle() {
return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
{});
},
// label
elLabelPosition() {
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
'left');
},
// label
elLabelAlign() {
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
},
// label线
elBorderBottom() {
// borderBottom使
return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
true;
}
},
methods: {
broadcastInputError() {
// truefalsetrue
this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
},
// required
setRules() {
let that = this;
// "*"propsrequiredrules
// u-formu-form-item
// let rules = this.getRules();
// if (rules.length) {
// this.isRequired = rules.some(rule => {
// // undefined
// return rule.required;
// });
// }
// blur
this.$on('on-form-blur', that.onFieldBlur);
// change
this.$on('on-form-change', that.onFieldChange);
},
// u-formrulesu-form-item
getRules() {
//
let rules = this.parent.rules;
rules = rules ? rules[this.prop] : [];
//
return [].concat(rules || []);
},
// blur
onFieldBlur() {
this.validation('blur');
},
// change
onFieldChange() {
this.validation('change');
},
// rule
getFilteredRule(triggerType = '') {
let rules = this.getRules();
// triggerType
if (!triggerType) return rules;
// blurchange
// 使indexOftrigger['blur','change']
// trigger
return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
},
//
validation(trigger, callback = () => {}) {
//
this.fieldValue = this.parent.model[this.prop];
// blurchange
let rules = this.getFilteredRule(trigger);
// u-form
// count
if (!rules || rules.length === 0) {
return callback('');
}
//
this.validateState = 'validating';
// async-validator
let validator = new schema({
[this.prop]: rules
});
validator.validate({
[this.prop]: this.fieldValue
}, {
firstFields: true
}, (errors, fields) => {
//
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
//
callback(this.validateMessage);
});
},
// u-form-item
resetField() {
this.parent.model[this.prop] = this.initialValue;
// `success`
this.validateState = 'success';
}
},
// u-form
mounted() {
// provide/inject使created
this.parent = this.$u.$parent.call(this, 'u-form');
if (this.parent) {
// parentDataparentparentData
Object.keys(this.parentData).map(key => {
this.parentData[key] = this.parent[key];
});
// propuForm(u-form-input使uForm)
if (this.prop) {
//
this.parent.fields.push(this);
this.errorType = this.parent.errorType;
//
this.initialValue = this.fieldValue;
// $nextTicku-formrulesref
// $nextTickrefu-form
this.$nextTick(() => {
this.setRules();
})
}
}
},
// u-form
beforeDestroy() {
// prop
if (this.parent && this.prop) {
this.parent.fields.map((item, index) => {
if (item === this) this.parent.fields.splice(index, 1);
})
}
},
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-form-item {
@include vue-flex;
// align-items: flex-start;
padding: 20rpx 0;
font-size: 28rpx;
color: $u-main-color;
box-sizing: border-box;
line-height: $u-form-item-height;
flex-direction: column;
&__border-bottom--error:after {
border-color: $u-type-error;
}
&__body {
@include vue-flex;
}
&--left {
@include vue-flex;
align-items: center;
&__content {
position: relative;
@include vue-flex;
align-items: center;
padding-right: 10rpx;
flex: 1;
&__icon {
margin-right: 8rpx;
}
&--required {
position: absolute;
left: -16rpx;
vertical-align: middle;
color: $u-type-error;
padding-top: 6rpx;
}
&__label {
@include vue-flex;
align-items: center;
flex: 1;
}
}
}
&--right {
flex: 1;
&__content {
@include vue-flex;
align-items: center;
flex: 1;
&__slot {
flex: 1;
/* #ifndef MP */
@include vue-flex;
align-items: center;
/* #endif */
}
&__icon {
margin-left: 10rpx;
color: $u-light-color;
font-size: 30rpx;
}
}
}
&__message {
font-size: 24rpx;
line-height: 24rpx;
color: $u-type-error;
margin-top: 12rpx;
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<view class="u-form"><slot /></view>
</template>
<script>
/**
* form 表单
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等
* @tutorial http://uviewui.com/components/form.html
* @property {Object} model 表单数据对象
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
* @property {String} label-position 表单域提示文字的位置left-左侧top-上方
* @property {String Number} label-width 提示文字的宽度单位rpx默认90
* @property {Object} label-style lable的样式对象形式
* @property {String} label-align lable的对齐方式
* @property {Object} rules 通过ref设置见官网说明
* @property {Array} error-type 错误的提示方式数组形式见上方说明(默认['message'])
* @example <u-form :model="form" ref="uForm"></u-form>
*/
export default {
name: 'u-form',
props: {
// form
model: {
type: Object,
default() {
return {};
}
},
//
// rules: {
// type: [Object, Function, Array],
// default() {
// return {};
// }
// },
// message-border-input
// border-bottom-none-
errorType: {
type: Array,
default() {
return ['message', 'toast']
}
},
// 线
borderBottom: {
type: Boolean,
default: true
},
// labelleft-top-
labelPosition: {
type: String,
default: 'left'
},
// labelrpx
labelWidth: {
type: [String, Number],
default: 90
},
// lable
labelAlign: {
type: String,
default: 'left'
},
// lable
labelStyle: {
type: Object,
default() {
return {}
}
},
},
provide() {
return {
uForm: this
};
},
data() {
return {
rules: {}
};
},
created() {
// formu-form-item
// data
this.fields = [];
},
methods: {
setRules(rules) {
this.rules = rules;
},
// u-form-itemu-form-itemresetField()
resetFields() {
this.fields.map(field => {
field.resetField();
});
},
//
validate(callback) {
return new Promise(resolve => {
// u-form-item
let valid = true; //
let count = 0; //
let errorArr = []; //
this.fields.map(field => {
// u-form-itemvalidation
field.validation('', error => {
// u-form-item
if (error) {
valid = false;
errorArr.push(error);
}
// u-form-itempromisethen
if (++count === this.fields.length) {
resolve(valid); // promisethen
// toast
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
this.$u.toast(errorArr[0]);
}
//
if (typeof callback == 'function') callback(valid);
}
});
});
});
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,52 @@
<template>
<u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
<view class="u-update-content">
<rich-text :nodes="content"></rich-text>
</view>
</u-modal>
</template>
<script>
export default {
data() {
return {
show: false,
content: `
1. 修复badge组件的size参数无效问题<br>
2. 新增Modal模态框组件<br>
3. 新增压窗屏组件可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
4. 修复键盘组件在微信小程序上遮罩无效的问题
`,
}
},
onReady() {
this.show = true;
},
methods: {
cancel() {
this.closeModal();
},
confirm() {
this.closeModal();
},
closeModal() {
uni.navigateBack();
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-full-content {
background-color: #00C777;
}
.u-update-content {
font-size: 26rpx;
color: $u-content-color;
line-height: 1.7;
padding: 30rpx;
}
</style>
@@ -0,0 +1,54 @@
<template>
<view class="u-gap" :style="[gapStyle]"></view>
</template>
<script>
/**
* gap 间隔槽
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景方便用户风格统一减少工作量
* @tutorial https://www.uviewui.com/components/gap.html
* @property {String} bg-color 背景颜色默认#f3f4f6
* @property {String Number} height 分割槽高度单位rpx默认30
* @property {String Number} margin-top 与前一个组件的距离单位rpx默认0
* @property {String Number} margin-bottom 与后一个组件的距离单位rpx0
* @example <u-gap height="80" bg-color="#bbb"></u-gap>
*/
export default {
name: "u-gap",
props: {
bgColor: {
type: String,
default: 'transparent ' //
},
//
height: {
type: [String, Number],
default: 30
},
//
marginTop: {
type: [String, Number],
default: 0
},
//
marginBottom: {
type: [String, Number],
default: 0
},
},
computed: {
gapStyle() {
return {
backgroundColor: this.bgColor,
height: this.height + 'rpx',
marginTop: this.marginTop + 'rpx',
marginBottom: this.marginBottom + 'rpx'
};
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
</style>
@@ -0,0 +1,126 @@
<template>
<view class="u-grid-item" :hover-class="parentData.hoverClass"
:hover-stay-time="200" @tap="click" :style="{
background: bgColor,
width: width,
}">
<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
<slot />
</view>
</view>
</template>
<script>
/**
* gridItem 提示
* @description 宫格组件一般用于同时展示多个同类项目的场景可以给宫格的项目设置徽标组件(badge)或者图标等也可以扩展为左右滑动的轮播形式搭配u-grid使用
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String} bg-color 宫格的背景颜色默认#ffffff
* @property {String Number} index 点击宫格时返回的值
* @property {Object} custom-style 自定义样式对象形式
* @event {Function} click 点击宫格触发
* @example <u-grid-item></u-grid-item>
*/
export default {
name: "u-grid-item",
props: {
//
bgColor: {
type: String,
default: '#ffffff'
},
// index
index: {
type: [Number, String],
default: ''
},
//
customStyle: {
type: Object,
default() {
return {
padding: '30rpx 0'
}
}
}
},
data() {
return {
parentData: {
hoverClass: '', //
col: 3, //
border: true, //
}
};
},
created() {
//
this.updateParentData();
// this.parentupdateParentData()
this.parent.children.push(this);
},
computed: {
// grid-item
width() {
return 100 / Number(this.parentData.col) + '%';
},
},
methods: {
//
updateParentData() {
// mixin
this.getParentData('u-grid');
},
click() {
this.$emit('click', this.index);
this.parent && this.parent.click(this.index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-grid-item {
box-sizing: border-box;
background: #fff;
@include vue-flex;
align-items: center;
justify-content: center;
position: relative;
flex-direction: column;
/* #ifdef MP */
position: relative;
float: left;
/* #endif */
}
.u-grid-item-hover {
background: #f7f7f7 !important;
}
.u-grid-marker-box {
position: absolute;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
line-height: 0;
}
.u-grid-marker-wrap {
position: absolute;
}
.u-grid-item-box {
padding: 30rpx 0;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,108 @@
<template>
<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
</template>
<script>
/**
* grid 宫格布局
* @description 宫格组件一般用于同时展示多个同类项目的场景可以给宫格的项目设置徽标组件(badge)或者图标等也可以扩展为左右滑动的轮播形式
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String Number} col 宫格的列数默认3
* @property {Boolean} border 是否显示宫格的边框默认true
* @property {Boolean} hover-class 点击宫格的时候是否显示按下的灰色背景默认false
* @event {Function} click 点击宫格触发
* @example <u-grid :col="3" @click="click"></u-grid>
*/
export default {
name: 'u-grid',
props: {
//
col: {
type: [Number, String],
default: 3
},
//
border: {
type: Boolean,
default: true
},
//
align: {
type: String,
default: 'left'
},
// "none"
hoverClass: {
type: String,
default: 'u-hover-class'
}
},
data() {
return {
index: 0,
}
},
watch: {
//
parentData() {
if(this.children.length) {
this.children.map(child => {
// (u-radio)updateParentData()
typeof(child.updateParentData) == 'function' && child.updateParentData();
})
}
},
},
created() {
// childrendata
this.children = [];
},
computed: {
//
parentData() {
return [this.hoverClass, this.col, this.size, this.border];
},
//
gridStyle() {
let style = {};
switch(this.align) {
case 'left':
style.justifyContent = 'flex-start';
break;
case 'center':
style.justifyContent = 'center';
break;
case 'right':
style.justifyContent = 'flex-end';
break;
default: style.justifyContent = 'flex-start';
};
return style;
}
},
methods: {
click(index) {
this.$emit('click', index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-grid {
width: 100%;
/* #ifdef MP */
position: relative;
box-sizing: border-box;
overflow: hidden;
/* #endif */
/* #ifndef MP */
@include vue-flex;
flex-wrap: wrap;
align-items: center;
/* #endif */
}
</style>
@@ -0,0 +1,336 @@
<template>
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
<text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
@touchstart="touchstart">
<text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
class="u-icon__decimal">
</text>
</text>
<!-- 这里进行空字符串判断如果仅仅是v-if="label"可能会出现传递0的时候结果也无法显示 -->
<text v-if="label !== ''" class="u-icon__label" :style="{
color: labelColor,
fontSize: $u.addUnit(labelSize),
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
}">{{ label }}
</text>
</view>
</template>
<script>
/**
* icon 图标
* @description 基于字体的图标集包含了大多数常见场景的图标
* @tutorial https://www.uviewui.com/components/icon.html
* @property {String} name 图标名称见示例图标集
* @property {String} color 图标颜色默认inherit
* @property {String | Number} size 图标字体大小单位rpx默认32
* @property {String | Number} label-size label字体大小单位rpx默认28
* @property {String} label 图标右侧的label文字默认28
* @property {String} label-pos label文字相对于图标的位置只能right或bottom默认right
* @property {String} label-color label字体颜色默认#606266
* @property {Object} custom-style icon的样式对象形式
* @property {String} custom-prefix 自定义字体图标库时需要写上此值
* @property {String | Number} margin-left label在右侧时与图标的距离单位rpx默认6
* @property {String | Number} margin-top label在下方时与图标的距离单位rpx默认6
* @property {String | Number} margin-bottom label在上方时与图标的距离单位rpx默认6
* @property {String | Number} margin-right label在左侧时与图标的距离单位rpx默认6
* @property {String} label-pos label相对于图标的位置只能right或bottom默认right
* @property {String} index 一个用于区分多个图标的值点击图标时通过click事件传出
* @property {String} hover-class 图标按下去的样式类用法同uni的view组件的hover-class参数详情见官网
* @property {String} width 显示图片小图标时的宽度
* @property {String} height 显示图片小图标时的高度
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {Boolean} show-decimal-icon 是否为DecimalIcon
* @property {String} inactive-color 背景颜色可接受主题色仅Decimal时有效
* @property {String | Number} percent 显示的百分比仅Decimal时有效
* @event {Function} click 点击图标时触发
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
*/
export default {
name: 'u-icon',
props: {
//
name: {
type: String,
default: ''
},
//
color: {
type: String,
default: ''
},
// rpx
size: {
type: [Number, String],
default: 'inherit'
},
//
bold: {
type: Boolean,
default: false
},
// index
index: {
type: [Number, String],
default: ''
},
//
hoverClass: {
type: String,
default: ''
},
// 便
customPrefix: {
type: String,
default: 'uicon'
},
//
label: {
type: [String, Number],
default: ''
},
// label
labelPos: {
type: String,
default: 'right'
},
// label
labelSize: {
type: [String, Number],
default: '28'
},
// label
labelColor: {
type: String,
default: '#606266'
},
// label()
marginLeft: {
type: [String, Number],
default: '6'
},
// label()
marginTop: {
type: [String, Number],
default: '6'
},
// label()
marginRight: {
type: [String, Number],
default: '6'
},
// label()
marginBottom: {
type: [String, Number],
default: '6'
},
// mode
imgMode: {
type: String,
default: 'widthFix'
},
//
customStyle: {
type: Object,
default() {
return {}
}
},
//
width: {
type: [String, Number],
default: ''
},
//
height: {
type: [String, Number],
default: ''
},
//
top: {
type: [String, Number],
default: 0
},
// DecimalIcon
showDecimalIcon: {
type: Boolean,
default: false
},
// Decimal
inactiveColor: {
type: String,
default: '#ececec'
},
// Decimal
percent: {
type: [Number, String],
default: '50'
}
},
computed: {
customClass() {
let classes = []
classes.push(this.customPrefix + '-' + this.name)
// uViewu-iconfont
if (this.customPrefix == 'uicon') {
classes.push('u-iconfont')
} else {
classes.push(this.customPrefix)
}
//
if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
classes.push('u-icon__icon--' + this.inactiveColor)
} else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
// 使[a, b, c]
//
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(' ')
//#endif
return classes
},
iconStyle() {
let style = {}
style = {
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
fontWeight: this.bold ? 'bold' : 'normal',
//
top: this.$u.addUnit(this.top)
}
//
if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
style.color = this.inactiveColor
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
return style
},
// name"/"
isImg() {
return this.name.indexOf('/') !== -1
},
imgStyle() {
let style = {}
// widthheight使使size
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
return style
},
decimalIconStyle() {
let style = {}
style = {
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
fontWeight: this.bold ? 'bold' : 'normal',
//
top: this.$u.addUnit(this.top),
width: this.percent + '%'
}
//
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
return style
},
decimalIconClass() {
let classes = []
classes.push(this.customPrefix + '-' + this.name)
// uViewu-iconfont
if (this.customPrefix == 'uicon') {
classes.push('u-iconfont')
} else {
classes.push(this.customPrefix)
}
//
if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
else classes.push('u-icon__icon--primary')
// 使[a, b, c]
//
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(' ')
//#endif
return classes
}
},
methods: {
click() {
this.$emit('click', this.index)
},
touchstart() {
this.$emit('touchstart', this.index)
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
@import '../../iconfont.css';
.u-icon {
display: inline-flex;
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
position: relative;
&--primary {
color: $u-type-primary;
}
&--success {
color: $u-type-success;
}
&--error {
color: $u-type-error;
}
&--warning {
color: $u-type-warning;
}
&--info {
color: $u-type-info;
}
}
&__decimal {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
}
&__img {
height: auto;
will-change: transform;
}
&__label {
line-height: 1;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More