项目初始化
This commit is contained in:
319
sheep/ui/su-coupon/su-coupon.vue
Normal file
319
sheep/ui/su-coupon/su-coupon.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<view class="ui-coupon-wrap">
|
||||
<!-- xs: 一行三个,竖向 -->
|
||||
<view
|
||||
v-if="props.size === 'xs'"
|
||||
class="xs-coupon-card ss-flex ss-flex-col ss-row-between"
|
||||
:style="[cardStyle]"
|
||||
@tap="
|
||||
sheep.$router.go('/pages/coupon/detail', {
|
||||
id: couponId,
|
||||
})
|
||||
"
|
||||
>
|
||||
<view class="ss-flex ss-flex-col ss-row-center ss-col-center">
|
||||
<view class="face-value-box ss-flex ss-col-bottom ss-m-t-50 ss-m-b-28">
|
||||
<view class="value-text ss-m-r-4">{{ type === 'reduce' ? value : Number(value) }}</view>
|
||||
<view class="value-unit">{{ type === 'reduce' ? '元' : '折' }}</view>
|
||||
</view>
|
||||
<view class="title-text">{{ props.title }}</view>
|
||||
</view>
|
||||
<view class="card-bottom ss-m-b-30 ss-flex ss-row-center">
|
||||
<slot name="btn">
|
||||
<button class="ss-reset-button card-btn">{{ state.stateMap[props.state] }}</button>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- md: 一行两个,横向 -->
|
||||
<view
|
||||
v-if="props.size === 'md'"
|
||||
class="md-coupon-card ss-flex ss-row-between"
|
||||
:style="[cardStyle]"
|
||||
@tap="
|
||||
sheep.$router.go('/pages/coupon/detail', {
|
||||
id: couponId,
|
||||
})
|
||||
"
|
||||
>
|
||||
<view class="card-left ss-flex ss-flex-col ss-row-between ss-col-top ss-m-l-40">
|
||||
<view class="face-value-box ss-flex ss-col-bottom ss-m-t-28">
|
||||
<view class="value-text ss-m-r-4">{{ type === 'reduce' ? value : Number(value) }}</view>
|
||||
<view class="value-unit">{{ type === 'reduce' ? '元' : '折' }}</view>
|
||||
</view>
|
||||
<view class="ss-m-b-28">
|
||||
<view class="title-text ss-m-b-10">{{ props.title }}</view>
|
||||
<view class="surplus-text" v-if="props.surplus">仅剩:{{ props.surplus }}张</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-right ss-flex ss-row-center">
|
||||
<slot name="btn">
|
||||
<button class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center">
|
||||
<view class="btn-text">{{ state.stateMap[props.state] }}</view>
|
||||
</button>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- lg: 一行一个,横向 -->
|
||||
<view
|
||||
v-if="props.size === 'lg'"
|
||||
class="lg-coupon-card ss-flex ss-row-between"
|
||||
:style="[cardStyle]"
|
||||
@tap="
|
||||
sheep.$router.go('/pages/coupon/detail', {
|
||||
id: couponId,
|
||||
})
|
||||
"
|
||||
>
|
||||
<view class="card-left ss-flex ss-flex-col ss-row-between ss-col-top ss-m-l-40">
|
||||
<view class="face-value-box ss-flex ss-col-bottom ss-m-t-28">
|
||||
<view class="value-text ss-m-r-4">{{ type === 'reduce' ? value : Number(value) }}</view>
|
||||
<view class="value-unit">{{ type === 'reduce' ? '元' : '折' }}</view>
|
||||
</view>
|
||||
<view class="ss-m-b-20">
|
||||
<view class="title-text ss-m-b-10">{{ props.title }}</view>
|
||||
<view class="sellby-text">有效期:{{ props.sellBy }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-right ss-flex ss-flex-col ss-col-center ss-row-center">
|
||||
<slot name="btn">
|
||||
<button class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center">
|
||||
{{ state.stateMap[props.state] }}
|
||||
</button>
|
||||
</slot>
|
||||
<view class="surplus-text ss-m-t-24" v-if="props.surplus">仅剩:{{ props.surplus }}张</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 优惠券 卡片
|
||||
*
|
||||
* @property {String} size = ['xs','md','lg'] - 类型 xs:一行三个,md:一行两个,lg:一行一个
|
||||
* @property {String} textColor - 文字颜色
|
||||
* @property {String} background - 背景
|
||||
* @property {String} btnBg - 按钮背景
|
||||
* @property {String} btnTextColor - 按钮文字颜色
|
||||
* @property {Number} state = [0,1] - 状态,0:未领取,1:已领取
|
||||
* @property {String} title - 标题
|
||||
* @property {String | Number} value - 面值
|
||||
* @property {String} sellBy - 有效期
|
||||
* @property {String | Number} surplus - 剩余
|
||||
*
|
||||
*/
|
||||
import { computed, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
stateMap: {
|
||||
0: '立即领取',
|
||||
1: '去使用',
|
||||
},
|
||||
});
|
||||
|
||||
// 接受参数
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '#FF6000',
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '#FFC19C',
|
||||
},
|
||||
btnBg: {
|
||||
type: String,
|
||||
default: '#fff',
|
||||
},
|
||||
btnTextColor: {
|
||||
type: String,
|
||||
default: '#FF6000',
|
||||
},
|
||||
state: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
couponId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '这是优惠券',
|
||||
},
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 50,
|
||||
},
|
||||
sellBy: {
|
||||
type: String,
|
||||
default: '2019.11.25至2019.12.25',
|
||||
},
|
||||
surplus: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const cardStyle = computed(() => {
|
||||
return {
|
||||
background: props.background,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// xs
|
||||
.xs-coupon-card {
|
||||
width: 227rpx;
|
||||
// height: 145px;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.value-text {
|
||||
font-size: 50rpx;
|
||||
line-height: 50rpx;
|
||||
font-weight: bold;
|
||||
color: v-bind('textColor');
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
color: v-bind('textColor');
|
||||
font-size: 24rpx;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: v-bind('textColor');
|
||||
font-size: 24rpx;
|
||||
line-height: 30rpx;
|
||||
width: 150rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
width: 140rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
border-style: solid;
|
||||
border-color: v-bind('btnTextColor');
|
||||
border-width: 1px;
|
||||
color: v-bind('btnTextColor');
|
||||
background-color: v-bind('btnBg');
|
||||
font-size: 24rpx;
|
||||
line-height: 50rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// md
|
||||
|
||||
.md-coupon-card {
|
||||
width: 330rpx;
|
||||
height: 168rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.card-right,
|
||||
.card-left {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 36rpx;
|
||||
line-height: 36rpx;
|
||||
font-weight: bold;
|
||||
color: v-bind('textColor');
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
color: v-bind('textColor');
|
||||
font-size: 22rpx;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
|
||||
.title-text,
|
||||
.surplus-text {
|
||||
color: v-bind('textColor');
|
||||
font-size: 22rpx;
|
||||
line-height: 22rpx;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
width: 60rpx;
|
||||
height: 100%;
|
||||
|
||||
.btn-text {
|
||||
color: v-bind('btnTextColor');
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
writing-mode: vertical-lr;
|
||||
writing-mode: tb-lr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lg
|
||||
.lg-coupon-card {
|
||||
width: 708rpx;
|
||||
height: 168rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.card-right,
|
||||
.card-left {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 50rpx;
|
||||
line-height: 50rpx;
|
||||
font-weight: bold;
|
||||
color: v-bind('textColor');
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
color: v-bind('textColor');
|
||||
font-size: 22rpx;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.title-text,
|
||||
.sellby-text,
|
||||
.surplus-text {
|
||||
color: v-bind('textColor');
|
||||
font-size: 22rpx;
|
||||
line-height: 22rpx;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
width: 200rpx;
|
||||
.card-btn {
|
||||
width: 140rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
border-style: solid;
|
||||
border-color: v-bind('btnTextColor');
|
||||
border-width: 1px;
|
||||
color: v-bind('btnTextColor');
|
||||
background-color: v-bind('btnBg');
|
||||
font-size: 24rpx;
|
||||
line-height: 50rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
894
sheep/ui/su-data-checkbox/su-data-checkbox.vue
Normal file
894
sheep/ui/su-data-checkbox/su-data-checkbox.vue
Normal file
@@ -0,0 +1,894 @@
|
||||
<template>
|
||||
<view class="uni-data-checklist" :style="{ 'margin-top': isTop + 'px' }">
|
||||
<template v-if="!isLocal">
|
||||
<view class="uni-data-loading">
|
||||
<uni-load-more
|
||||
v-if="!mixinDatacomErrorMessage"
|
||||
status="loading"
|
||||
iconType="snow"
|
||||
:iconSize="18"
|
||||
:content-text="contentText"
|
||||
></uni-load-more>
|
||||
<text v-else>{{ mixinDatacomErrorMessage }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<checkbox-group
|
||||
v-if="multiple"
|
||||
class="checklist-group"
|
||||
:class="{ 'is-list': mode === 'list' || wrap }"
|
||||
@change="chagne"
|
||||
>
|
||||
<label
|
||||
class="checklist-box"
|
||||
:class="[
|
||||
'is--' + mode,
|
||||
item.selected ? 'is-checked' : '',
|
||||
disabled || !!item.disabled ? 'is-disable' : '',
|
||||
index !== 0 && mode === 'list' ? 'is-list-border' : '',
|
||||
]"
|
||||
:style="item.styleBackgroud"
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
>
|
||||
<checkbox
|
||||
class="hidden"
|
||||
hidden
|
||||
:disabled="disabled || !!item.disabled"
|
||||
:value="item[map.value] + ''"
|
||||
:checked="item.selected"
|
||||
/>
|
||||
<view
|
||||
v-if="(mode !== 'tag' && mode !== 'list') || (mode === 'list' && icon === 'left')"
|
||||
class="checkbox__inner"
|
||||
:style="item.styleIcon"
|
||||
>
|
||||
<view class="checkbox__inner-icon"></view>
|
||||
</view>
|
||||
<view
|
||||
class="checklist-content"
|
||||
:class="{ 'list-content': mode === 'list' && icon === 'left' }"
|
||||
>
|
||||
<text class="checklist-text" :style="item.styleIconText">{{ item[map.text] }}</text>
|
||||
<view
|
||||
v-if="mode === 'list' && icon === 'right'"
|
||||
class="checkobx__list"
|
||||
:style="item.styleBackgroud"
|
||||
></view>
|
||||
</view>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
<radio-group
|
||||
v-else
|
||||
class="checklist-group"
|
||||
:class="{ 'is-list': mode === 'list', 'is-wrap': wrap }"
|
||||
@change="chagne"
|
||||
>
|
||||
<!-- -->
|
||||
<label
|
||||
class="checklist-box"
|
||||
:class="[
|
||||
'is--' + mode,
|
||||
item.selected ? 'is-checked' : '',
|
||||
disabled || !!item.disabled ? 'is-disable' : '',
|
||||
index !== 0 && mode === 'list' ? 'is-list-border' : '',
|
||||
]"
|
||||
:style="item.styleBackgroud"
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
>
|
||||
<radio
|
||||
class="hidden"
|
||||
hidden
|
||||
:disabled="disabled || item.disabled"
|
||||
:value="item[map.value] + ''"
|
||||
:checked="item.selected"
|
||||
/>
|
||||
<view
|
||||
v-if="(mode !== 'tag' && mode !== 'list') || (mode === 'list' && icon === 'left')"
|
||||
class="radio__inner"
|
||||
:style="item.styleBackgroud"
|
||||
>
|
||||
<view class="radio__inner-icon" :style="item.styleIcon"></view>
|
||||
</view>
|
||||
<view
|
||||
class="checklist-content"
|
||||
:class="{ 'list-content': mode === 'list' && icon === 'left' }"
|
||||
>
|
||||
<text class="checklist-text" :style="item.styleIconText">{{ item[map.text] }}</text>
|
||||
<view
|
||||
v-if="mode === 'list' && icon === 'right'"
|
||||
:style="item.styleRightIcon"
|
||||
class="checkobx__list"
|
||||
></view>
|
||||
</view>
|
||||
</label>
|
||||
</radio-group>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* DataChecklist 数据选择器
|
||||
* @description 通过数据渲染 checkbox 和 radio
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
|
||||
* @property {String} mode = [default| list | button | tag] 显示模式
|
||||
* @value default 默认横排模式
|
||||
* @value list 列表模式
|
||||
* @value button 按钮模式
|
||||
* @value tag 标签模式
|
||||
* @property {Boolean} multiple = [true|false] 是否多选
|
||||
* @property {Array|String|Number} value 默认值
|
||||
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}]
|
||||
* @property {Number|String} min 最小选择个数 ,multiple为true时生效
|
||||
* @property {Number|String} max 最大选择个数 ,multiple为true时生效
|
||||
* @property {Boolean} wrap 是否换行显示
|
||||
* @property {String} icon = [left|right] list 列表模式下icon显示位置
|
||||
* @property {Boolean} selectedColor 选中颜色
|
||||
* @property {Boolean} emptyText 没有数据时显示的文字 ,本地数据无效
|
||||
* @property {Boolean} selectedTextColor 选中文本颜色,如不填写则自动显示
|
||||
* @property {Object} map 字段映射, 默认 map={text:'text',value:'value'}
|
||||
* @value left 左侧显示
|
||||
* @value right 右侧显示
|
||||
* @event {Function} change 选中发生变化触发
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'uniDataChecklist',
|
||||
mixins: [uniCloud.mixinDatacom || {}],
|
||||
emits: ['input', 'update:modelValue', 'change'],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: [Array, String, Number],
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
// TODO vue3
|
||||
modelValue: {
|
||||
type: [Array, String, Number],
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
localdata: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
min: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
max: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
selectedColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedTextColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
map: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
text: 'text',
|
||||
value: 'value',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
localdata: {
|
||||
handler(newVal) {
|
||||
this.range = newVal;
|
||||
this.dataList = this.getDataList(this.getSelectedValue(newVal));
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
mixinDatacomResData(newVal) {
|
||||
this.range = newVal;
|
||||
this.dataList = this.getDataList(this.getSelectedValue(newVal));
|
||||
},
|
||||
value(newVal) {
|
||||
this.dataList = this.getDataList(newVal);
|
||||
// fix by mehaotian is_reset 在 uni-forms 中定义
|
||||
if (!this.is_reset) {
|
||||
this.is_reset = false;
|
||||
this.formItem && this.formItem.setValue(newVal);
|
||||
}
|
||||
},
|
||||
modelValue(newVal) {
|
||||
this.dataList = this.getDataList(newVal);
|
||||
if (!this.is_reset) {
|
||||
this.is_reset = false;
|
||||
this.formItem && this.formItem.setValue(newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataList: [],
|
||||
range: [],
|
||||
contentText: {
|
||||
contentdown: '查看更多',
|
||||
contentrefresh: '加载中',
|
||||
contentnomore: '没有更多',
|
||||
},
|
||||
isLocal: true,
|
||||
styles: {
|
||||
selectedColor: '#2979ff',
|
||||
selectedTextColor: '#666',
|
||||
},
|
||||
isTop: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dataValue() {
|
||||
if (this.value === '') return this.modelValue;
|
||||
if (this.modelValue === '') return this.value;
|
||||
return this.value;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.form = this.getForm('uniForms');
|
||||
this.formItem = this.getForm('uniFormsItem');
|
||||
// this.formItem && this.formItem.setValue(this.value)
|
||||
|
||||
if (this.formItem) {
|
||||
this.isTop = 6;
|
||||
if (this.formItem.name) {
|
||||
// 如果存在name添加默认值,否则formData 中不存在这个字段不校验
|
||||
if (!this.is_reset) {
|
||||
this.is_reset = false;
|
||||
this.formItem.setValue(this.dataValue);
|
||||
}
|
||||
this.rename = this.formItem.name;
|
||||
this.form.inputChildrens.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.localdata && this.localdata.length !== 0) {
|
||||
this.isLocal = true;
|
||||
this.range = this.localdata;
|
||||
this.dataList = this.getDataList(this.getSelectedValue(this.range));
|
||||
} else {
|
||||
if (this.collection) {
|
||||
this.isLocal = false;
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.mixinDatacomGet()
|
||||
.then((res) => {
|
||||
this.mixinDatacomResData = res.result.data;
|
||||
if (this.mixinDatacomResData.length === 0) {
|
||||
this.isLocal = false;
|
||||
this.mixinDatacomErrorMessage = this.emptyText;
|
||||
} else {
|
||||
this.isLocal = true;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.mixinDatacomErrorMessage = err.message;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 获取父元素实例
|
||||
*/
|
||||
getForm(name = 'uniForms') {
|
||||
let parent = this.$parent;
|
||||
let parentName = parent.$options.name;
|
||||
while (parentName !== name) {
|
||||
parent = parent.$parent;
|
||||
if (!parent) return false;
|
||||
parentName = parent.$options.name;
|
||||
}
|
||||
return parent;
|
||||
},
|
||||
chagne(e) {
|
||||
const values = e.detail.value;
|
||||
|
||||
let detail = {
|
||||
value: [],
|
||||
data: [],
|
||||
};
|
||||
|
||||
if (this.multiple) {
|
||||
this.range.forEach((item) => {
|
||||
if (values.includes(item[this.map.value] + '')) {
|
||||
detail.value.push(item[this.map.value]);
|
||||
detail.data.push(item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const range = this.range.find((item) => item[this.map.value] + '' === values);
|
||||
if (range) {
|
||||
detail = {
|
||||
value: range[this.map.value],
|
||||
data: range,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.formItem && this.formItem.setValue(detail.value);
|
||||
// TODO 兼容 vue2
|
||||
this.$emit('input', detail.value);
|
||||
// // TOTO 兼容 vue3
|
||||
this.$emit('update:modelValue', detail.value);
|
||||
this.$emit('change', {
|
||||
detail,
|
||||
});
|
||||
if (this.multiple) {
|
||||
// 如果 v-model 没有绑定 ,则走内部逻辑
|
||||
// if (this.value.length === 0) {
|
||||
this.dataList = this.getDataList(detail.value, true);
|
||||
// }
|
||||
} else {
|
||||
this.dataList = this.getDataList(detail.value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取渲染的新数组
|
||||
* @param {Object} value 选中内容
|
||||
*/
|
||||
getDataList(value) {
|
||||
// 解除引用关系,破坏原引用关系,避免污染源数据
|
||||
let dataList = JSON.parse(JSON.stringify(this.range));
|
||||
let list = [];
|
||||
if (this.multiple) {
|
||||
if (!Array.isArray(value)) {
|
||||
value = [];
|
||||
}
|
||||
}
|
||||
dataList.forEach((item, index) => {
|
||||
item.disabled = item.disable || item.disabled || false;
|
||||
if (this.multiple) {
|
||||
if (value.length > 0) {
|
||||
let have = value.find((val) => val === item[this.map.value]);
|
||||
item.selected = have !== undefined;
|
||||
} else {
|
||||
item.selected = false;
|
||||
}
|
||||
} else {
|
||||
item.selected = value === item[this.map.value];
|
||||
}
|
||||
|
||||
list.push(item);
|
||||
});
|
||||
return this.setRange(list);
|
||||
},
|
||||
/**
|
||||
* 处理最大最小值
|
||||
* @param {Object} list
|
||||
*/
|
||||
setRange(list) {
|
||||
let selectList = list.filter((item) => item.selected);
|
||||
let min = Number(this.min) || 0;
|
||||
let max = Number(this.max) || '';
|
||||
list.forEach((item, index) => {
|
||||
if (this.multiple) {
|
||||
if (selectList.length <= min) {
|
||||
let have = selectList.find((val) => val[this.map.value] === item[this.map.value]);
|
||||
if (have !== undefined) {
|
||||
item.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectList.length >= max && max !== '') {
|
||||
let have = selectList.find((val) => val[this.map.value] === item[this.map.value]);
|
||||
if (have === undefined) {
|
||||
item.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setStyles(item, index);
|
||||
list[index] = item;
|
||||
});
|
||||
return list;
|
||||
},
|
||||
/**
|
||||
* 设置 class
|
||||
* @param {Object} item
|
||||
* @param {Object} index
|
||||
*/
|
||||
setStyles(item, index) {
|
||||
// 设置自定义样式
|
||||
item.styleBackgroud = this.setStyleBackgroud(item);
|
||||
item.styleIcon = this.setStyleIcon(item);
|
||||
item.styleIconText = this.setStyleIconText(item);
|
||||
item.styleRightIcon = this.setStyleRightIcon(item);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取选中值
|
||||
* @param {Object} range
|
||||
*/
|
||||
getSelectedValue(range) {
|
||||
if (!this.multiple) return this.dataValue;
|
||||
let selectedArr = [];
|
||||
range.forEach((item) => {
|
||||
if (item.selected) {
|
||||
selectedArr.push(item[this.map.value]);
|
||||
}
|
||||
});
|
||||
return this.dataValue && this.dataValue.length > 0 ? this.dataValue : selectedArr;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置背景样式
|
||||
*/
|
||||
setStyleBackgroud(item) {
|
||||
let styles = {};
|
||||
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff';
|
||||
if (this.mode !== 'list') {
|
||||
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6';
|
||||
}
|
||||
if (this.mode === 'tag') {
|
||||
styles['background-color'] = item.selected ? selectedColor : '#f5f5f5';
|
||||
}
|
||||
let classles = '';
|
||||
for (let i in styles) {
|
||||
classles += `${i}:${styles[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
setStyleIcon(item) {
|
||||
let styles = {};
|
||||
let classles = '';
|
||||
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff';
|
||||
styles['background-color'] = item.selected ? selectedColor : '#fff';
|
||||
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6';
|
||||
|
||||
if (!item.selected && item.disabled) {
|
||||
styles['background-color'] = '#F2F6FC';
|
||||
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6';
|
||||
}
|
||||
|
||||
for (let i in styles) {
|
||||
classles += `${i}:${styles[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
setStyleIconText(item) {
|
||||
let styles = {};
|
||||
let classles = '';
|
||||
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff';
|
||||
if (this.mode === 'tag') {
|
||||
styles.color = item.selected
|
||||
? this.selectedTextColor
|
||||
? this.selectedTextColor
|
||||
: '#fff'
|
||||
: '#666';
|
||||
} else {
|
||||
styles.color = item.selected
|
||||
? this.selectedTextColor
|
||||
? this.selectedTextColor
|
||||
: selectedColor
|
||||
: '#666';
|
||||
}
|
||||
if (!item.selected && item.disabled) {
|
||||
styles.color = '#999';
|
||||
}
|
||||
|
||||
for (let i in styles) {
|
||||
classles += `${i}:${styles[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
setStyleRightIcon(item) {
|
||||
let styles = {};
|
||||
let classles = '';
|
||||
if (this.mode === 'list') {
|
||||
styles['border-color'] = item.selected ? this.styles.selectedColor : '#DCDFE6';
|
||||
}
|
||||
for (let i in styles) {
|
||||
classles += `${i}:${styles[i]};`;
|
||||
}
|
||||
|
||||
return classles;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$checked-color: var(--ui-BG-Main);
|
||||
$border-color: #dcdfe6;
|
||||
$disable: 0.4;
|
||||
|
||||
@mixin flex {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-data-loading {
|
||||
@include flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.uni-data-checklist {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
flex: 1;
|
||||
// 多选样式
|
||||
.checklist-group {
|
||||
@include flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.is-list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checklist-box {
|
||||
@include flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
margin-right: 25px;
|
||||
|
||||
.hidden {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 文字样式
|
||||
.checklist-content {
|
||||
@include flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.checklist-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-left: 5px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.checkobx__list {
|
||||
border-right-width: 1px;
|
||||
border-right-color: #007aff;
|
||||
border-right-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #007aff;
|
||||
border-bottom-style: solid;
|
||||
height: 12px;
|
||||
width: 6px;
|
||||
left: -5px;
|
||||
transform-origin: center;
|
||||
transform: rotate(45deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 多选样式
|
||||
.checkbox__inner {
|
||||
/* #ifndef APP-NVUE */
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
.checkbox__inner-icon {
|
||||
position: absolute;
|
||||
/* #ifdef APP-NVUE */
|
||||
top: 2px;
|
||||
/* #endif */
|
||||
/* #ifndef APP-NVUE */
|
||||
top: 1px;
|
||||
/* #endif */
|
||||
left: 5px;
|
||||
height: 8px;
|
||||
width: 4px;
|
||||
border-right-width: 1px;
|
||||
border-right-color: #fff;
|
||||
border-right-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #fff;
|
||||
border-bottom-style: solid;
|
||||
opacity: 0;
|
||||
transform-origin: center;
|
||||
transform: rotate(40deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 单选样式
|
||||
.radio__inner {
|
||||
@include flex;
|
||||
/* #ifndef APP-NVUE */
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 16px;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
|
||||
.radio__inner-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认样式
|
||||
&.is--default {
|
||||
// 禁用
|
||||
&.is-disable {
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
.checkbox__inner {
|
||||
background-color: #f2f6fc;
|
||||
border-color: $border-color;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.radio__inner {
|
||||
background-color: #f2f6fc;
|
||||
border-color: $border-color;
|
||||
}
|
||||
.checklist-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中
|
||||
&.is-checked {
|
||||
.checkbox__inner {
|
||||
border-color: $checked-color;
|
||||
background-color: $checked-color;
|
||||
|
||||
.checkbox__inner-icon {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
.radio__inner {
|
||||
border-color: $checked-color;
|
||||
.radio__inner-icon {
|
||||
opacity: 1;
|
||||
background-color: $checked-color;
|
||||
}
|
||||
}
|
||||
.checklist-text {
|
||||
color: $checked-color;
|
||||
}
|
||||
// 选中禁用
|
||||
&.is-disable {
|
||||
.checkbox__inner {
|
||||
opacity: $disable;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
opacity: $disable;
|
||||
}
|
||||
.radio__inner {
|
||||
opacity: $disable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
&.is--button {
|
||||
margin-right: 10px;
|
||||
padding: 5px 10px;
|
||||
border: 1px $border-color solid;
|
||||
border-radius: 3px;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
// 禁用
|
||||
&.is-disable {
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
border: 1px #eee solid;
|
||||
opacity: $disable;
|
||||
.checkbox__inner {
|
||||
background-color: #f2f6fc;
|
||||
border-color: $border-color;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
.radio__inner {
|
||||
background-color: #f2f6fc;
|
||||
border-color: $border-color;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
.checklist-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
border-color: $checked-color;
|
||||
.checkbox__inner {
|
||||
border-color: $checked-color;
|
||||
background-color: $checked-color;
|
||||
.checkbox__inner-icon {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.radio__inner {
|
||||
border-color: $checked-color;
|
||||
|
||||
.radio__inner-icon {
|
||||
opacity: 1;
|
||||
background-color: $checked-color;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
color: $checked-color;
|
||||
}
|
||||
|
||||
// 选中禁用
|
||||
&.is-disable {
|
||||
opacity: $disable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式
|
||||
&.is--tag {
|
||||
margin-right: 10px;
|
||||
padding: 5px 10px;
|
||||
border: 1px $border-color solid;
|
||||
border-radius: 3px;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.checklist-text {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 禁用
|
||||
&.is-disable {
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
opacity: $disable;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background-color: $checked-color;
|
||||
border-color: $checked-color;
|
||||
|
||||
.checklist-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 列表样式
|
||||
&.is--list {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
padding: 10px 15px;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
&.is-list-border {
|
||||
border-top: 1px #eee solid;
|
||||
}
|
||||
|
||||
// 禁用
|
||||
&.is-disable {
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
.checkbox__inner {
|
||||
background-color: #f2f6fc;
|
||||
border-color: $border-color;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
.checklist-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.checkbox__inner {
|
||||
border-color: $checked-color;
|
||||
background-color: $checked-color;
|
||||
|
||||
.checkbox__inner-icon {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
.radio__inner {
|
||||
.radio__inner-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.checklist-text {
|
||||
color: $checked-color;
|
||||
}
|
||||
|
||||
.checklist-content {
|
||||
.checkobx__list {
|
||||
opacity: 1;
|
||||
border-color: $checked-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中禁用
|
||||
&.is-disable {
|
||||
.checkbox__inner {
|
||||
opacity: $disable;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
opacity: $disable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
269
sheep/ui/su-dialog/su-dialog.vue
Normal file
269
sheep/ui/su-dialog/su-dialog.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<su-popup :show="show" type="center" @close="closeDialog">
|
||||
<view class="uni-popup-dialog">
|
||||
<view class="uni-dialog-title">
|
||||
<text class="uni-dialog-title-text" :class="['uni-popup__' + dialogType]">
|
||||
{{ titleText }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="mode === 'base'" class="uni-dialog-content">
|
||||
<slot>
|
||||
<text class="uni-dialog-content-text">{{ content }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-else class="uni-dialog-content">
|
||||
<slot>
|
||||
<input
|
||||
class="uni-dialog-input"
|
||||
v-model="val"
|
||||
type="text"
|
||||
:placeholder="placeholderText"
|
||||
:focus="focus"
|
||||
/>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="uni-dialog-button-group">
|
||||
<view class="uni-dialog-button" @click="closeDialog">
|
||||
<text class="uni-dialog-button-text">{{ closeText }}</text>
|
||||
</view>
|
||||
<view class="uni-dialog-button uni-border-left" @click="onOk">
|
||||
<text class="uni-dialog-button-text uni-button-color">{{ okText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* PopUp 弹出层-对话框样式
|
||||
* @description 弹出层-对话框样式
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
|
||||
* @property {String} value input 模式下的默认值
|
||||
* @property {String} placeholder input 模式下输入提示
|
||||
* @property {String} type = [success|warning|info|error] 主题样式
|
||||
* @value success 成功
|
||||
* @value warning 提示
|
||||
* @value info 消息
|
||||
* @value error 错误
|
||||
* @property {String} mode = [base|input] 模式、
|
||||
* @value base 基础对话框
|
||||
* @value input 可输入对话框
|
||||
* @property {String} content 对话框内容
|
||||
* @property {Boolean} beforeClose 是否拦截取消事件
|
||||
* @event {Function} confirm 点击确认按钮触发
|
||||
* @event {Function} close 点击取消按钮触发
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'SuDialog',
|
||||
emits: ['confirm', 'close'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'error',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'base',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
beforeClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogType: 'error',
|
||||
focus: false,
|
||||
val: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
okText() {
|
||||
return this.confirmText || '确认';
|
||||
},
|
||||
closeText() {
|
||||
return this.cancelText || '取消';
|
||||
},
|
||||
placeholderText() {
|
||||
return this.placeholder || '';
|
||||
},
|
||||
titleText() {
|
||||
return this.title || '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
type(val) {
|
||||
this.dialogType = val;
|
||||
},
|
||||
mode(val) {
|
||||
if (val === 'input') {
|
||||
this.dialogType = 'info';
|
||||
}
|
||||
},
|
||||
value(val) {
|
||||
this.val = val;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.mode === 'input') {
|
||||
this.dialogType = 'info';
|
||||
this.val = this.value;
|
||||
} else {
|
||||
this.dialogType = this.type;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.focus = true;
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
onOk() {
|
||||
if (this.mode === 'input') {
|
||||
this.$emit('confirm', this.val);
|
||||
} else {
|
||||
this.$emit('confirm');
|
||||
}
|
||||
if (this.beforeClose) return;
|
||||
},
|
||||
/**
|
||||
* 点击取消按钮
|
||||
*/
|
||||
closeDialog() {
|
||||
this.$emit('close');
|
||||
if (this.beforeClose) return;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.uni-popup-dialog {
|
||||
width: 300px;
|
||||
border-radius: 11px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.uni-dialog-title {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.uni-dialog-title-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.uni-dialog-content {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.uni-dialog-content-text {
|
||||
font-size: 14px;
|
||||
color: #6c6c6c;
|
||||
}
|
||||
|
||||
.uni-dialog-button-group {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
border-top-color: #f5f5f5;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.uni-dialog-button {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.uni-border-left {
|
||||
border-left-color: #f0f0f0;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.uni-dialog-button-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.uni-button-color {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.uni-dialog-input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
border: 1px #eee solid;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border-radius: 5px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.uni-popup__success {
|
||||
color: #4cd964;
|
||||
}
|
||||
|
||||
.uni-popup__warn {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.uni-popup__error {
|
||||
color: #dd524d;
|
||||
}
|
||||
|
||||
.uni-popup__info {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
217
sheep/ui/su-fixed/su-fixed.vue
Normal file
217
sheep/ui/su-fixed/su-fixed.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<view class="ui-fixed">
|
||||
<view
|
||||
class="ui-fixed-box"
|
||||
:id="`fixed-${uuid}`"
|
||||
:class="[{ fixed: state.fixed }]"
|
||||
:style="[
|
||||
{
|
||||
left: sticky ? 'auto' : '0px',
|
||||
top: state.fixed && !bottom ? (noNav ? val : val + sys_navBar) + 'px' : 'auto',
|
||||
bottom: insetHeight,
|
||||
zIndex: index + sheep.$zIndex.navbar,
|
||||
},
|
||||
!alway ? { opacity: state.opacityVal } : '',
|
||||
]"
|
||||
>
|
||||
<view
|
||||
class="ui-fixed-content"
|
||||
@tap="toTop"
|
||||
:style="[{ zIndex: index + sheep.$zIndex.navbar }]"
|
||||
>
|
||||
<slot></slot>
|
||||
<view
|
||||
v-if="safeAreaInsets.bottom && bottom && isInset"
|
||||
class="inset-bottom"
|
||||
:style="[{ height: safeAreaInsets.bottom + 'px' }]"
|
||||
></view>
|
||||
</view>
|
||||
<view class="ui-fixed-bottom" :class="[bg]" v-if="bottom"></view>
|
||||
<view
|
||||
class="ui-fixed-bg"
|
||||
:class="[ui, bg]"
|
||||
:style="[
|
||||
{ zIndex: index + sheep.$zIndex.navbar - 1 },
|
||||
bgStyles,
|
||||
opacity ? { opacity: state.opacityVal } : '',
|
||||
]"
|
||||
></view>
|
||||
</view>
|
||||
<view
|
||||
class="skeleton"
|
||||
:style="[{ height: state.content.height + 'px', width: width + 'px' }]"
|
||||
v-if="sticky ? state.fixed : placeholder && state.fixed"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import { getCurrentInstance, unref, onMounted, reactive, nextTick, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
const { safeAreaInsets } = sheep.$platform.device;
|
||||
|
||||
const vm = getCurrentInstance();
|
||||
|
||||
const uuid = sheep.$helper.guid();
|
||||
const sys_navBar = sheep.$platform.navbar;
|
||||
const state = reactive({
|
||||
content: {},
|
||||
fixed: true,
|
||||
scrollTop: 0,
|
||||
opacityVal: 0,
|
||||
});
|
||||
const insetHeight = computed(() => {
|
||||
if (state.fixed && props.bottom) {
|
||||
if (props.isInset) {
|
||||
return props.val + 'px';
|
||||
} else {
|
||||
return props.val + safeAreaInsets.bottom + 'px';
|
||||
}
|
||||
} else {
|
||||
return 'auto';
|
||||
}
|
||||
});
|
||||
const props = defineProps({
|
||||
noNav: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bottom: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bgStyles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
val: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
alway: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
opacity: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
index: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
placeholder: {
|
||||
type: [Boolean],
|
||||
default: false,
|
||||
},
|
||||
sticky: {
|
||||
type: [Boolean],
|
||||
default: false,
|
||||
},
|
||||
noFixed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
clickTo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
//是否需要安全区
|
||||
isInset: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
state.fixed = !unref(props.sticky);
|
||||
onPageScroll((e) => {
|
||||
let top = e.scrollTop;
|
||||
state.scrollTop = top;
|
||||
state.opacityVal = top > sheep.$platform.navbar ? 1 : top * 0.01;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
computedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
const computedQuery = () => {
|
||||
uni.createSelectorQuery()
|
||||
.in(vm)
|
||||
.select(`#fixed-${uuid}`)
|
||||
.boundingClientRect((data) => {
|
||||
if (data != null) {
|
||||
state.content = data;
|
||||
if (unref(props.sticky)) {
|
||||
setFixed(state.scrollTop);
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
};
|
||||
|
||||
const setFixed = (value) => {
|
||||
if (unref(props.bottom)) {
|
||||
state.fixed =
|
||||
value >=
|
||||
state.content.bottom -
|
||||
sheep.$platform.device.windowHeight +
|
||||
state.content.height +
|
||||
unref(props.val);
|
||||
} else {
|
||||
state.fixed =
|
||||
value >=
|
||||
state.content.top -
|
||||
(unref(props.noNav) ? unref(props.val) : unref(props.val) + sheep.$platform.navbar);
|
||||
}
|
||||
};
|
||||
|
||||
const toTop = () => {
|
||||
if (props.hasToTop) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: state.content.top,
|
||||
duration: 100,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-fixed {
|
||||
.ui-fixed-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.ui-fixed-content {
|
||||
position: relative;
|
||||
}
|
||||
.ui-fixed-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inset-bottom {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
130
sheep/ui/su-image/su-image.vue
Normal file
130
sheep/ui/su-image/su-image.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<image
|
||||
v-if="!state.isError"
|
||||
class="su-img"
|
||||
:style="customStyle"
|
||||
:draggable="false"
|
||||
:mode="mode"
|
||||
:src="sheep.$url.cdn(src)"
|
||||
@tap="onImgPreview"
|
||||
@load="onImgLoad"
|
||||
@error="onImgError"
|
||||
></image>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 图片组件
|
||||
*
|
||||
* @property {String} src - 图片地址
|
||||
* @property {Number} mode - 裁剪方式
|
||||
* @property {String} isPreview - 是否开启预览
|
||||
* @property {Number} previewList - 预览列表
|
||||
* @property {String} current - 预览首张下标
|
||||
*
|
||||
* @event {Function} load - 图片加载完毕触发
|
||||
* @event {Function} error - 图片加载错误触发
|
||||
*
|
||||
*/
|
||||
|
||||
import { reactive, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 组件数据
|
||||
const state = reactive({
|
||||
isError: false,
|
||||
imgHeight: 600,
|
||||
});
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
errorSrc: {
|
||||
type: String,
|
||||
default: '/static/img/shop/empty_network.png',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'widthFix',
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
previewList: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
current: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['load', 'error']);
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
height: (props.height || state.imgHeight) + 'rpx',
|
||||
width: props.width ? props.width + 'rpx' : '100%',
|
||||
borderRadius: props.radius ? props.radius + 'rpx' : '',
|
||||
};
|
||||
});
|
||||
|
||||
// 图片加载完成
|
||||
function onImgLoad(e) {
|
||||
if (props.height === 0) {
|
||||
state.imgHeight = (e.detail.height / e.detail.width) * 750;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
function onImgError(e) {
|
||||
state.isError = true;
|
||||
emits('error', e);
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function onImgPreview() {
|
||||
if (!props.isPreview) return;
|
||||
uni.previewImage({
|
||||
urls: props.previewList.length < 1 ? [props.src] : props.previewList,
|
||||
current: props.current,
|
||||
longPressActions: {
|
||||
itemList: ['发送给朋友', '保存图片', '收藏'],
|
||||
success: function (data) {
|
||||
console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
|
||||
},
|
||||
fail: function (err) {
|
||||
console.log(err.errMsg);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.su-img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
365
sheep/ui/su-inner-navbar/su-inner-navbar.vue
Normal file
365
sheep/ui/su-inner-navbar/su-inner-navbar.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<su-fixed
|
||||
:noFixed="props.noFixed"
|
||||
:alway="props.alway"
|
||||
:bgStyles="props.bgStyles"
|
||||
:val="0"
|
||||
:index="props.zIndex"
|
||||
noNav
|
||||
:bg="props.bg"
|
||||
:ui="props.ui"
|
||||
:opacity="props.opacity"
|
||||
:placeholder="props.placeholder"
|
||||
>
|
||||
<su-status-bar />
|
||||
<!--
|
||||
:class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
|
||||
-->
|
||||
<view class="ui-navbar-box">
|
||||
<view
|
||||
class="ui-bar ss-p-x-20"
|
||||
:class="state.isDark ? 'text-white' : 'text-black'"
|
||||
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
|
||||
>
|
||||
<view class="icon-box ss-flex">
|
||||
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
|
||||
<text class="sicon-back" v-if="hasHistory" />
|
||||
<text class="sicon-home" v-else />
|
||||
</view>
|
||||
<view class="line"></view>
|
||||
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
|
||||
<text class="sicon-more" />
|
||||
</view>
|
||||
</view>
|
||||
<slot name="center">
|
||||
<view class="center navbar-title">{{ title }}</view>
|
||||
</slot>
|
||||
<!-- #ifdef MP -->
|
||||
<view :style="[state.capsuleStyle]"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</su-fixed>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 标题栏 - 基础组件navbar
|
||||
*
|
||||
* @param {Number} zIndex = 100 - 层级
|
||||
* @param {Boolean} back = true - 是否返回上一页
|
||||
* @param {String} backtext = '' - 返回文本
|
||||
* @param {String} bg = 'bg-white' - 公共Class
|
||||
* @param {String} status = '' - 状态栏颜色
|
||||
* @param {Boolean} alway = true - 是否常驻
|
||||
* @param {Boolean} opacity = false - 是否开启透明渐变
|
||||
* @param {Boolean} noFixed = false - 是否浮动
|
||||
* @param {String} ui = '' - 公共Class
|
||||
* @param {Boolean} capsule = false - 是否开启胶囊返回
|
||||
* @param {Boolean} stopBack = false - 是否禁用返回
|
||||
* @param {Boolean} placeholder = true - 是否开启占位
|
||||
* @param {Object} bgStyles = {} - 背景样式
|
||||
*
|
||||
*/
|
||||
|
||||
import { computed, reactive, onBeforeMount, ref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
|
||||
|
||||
// 本地数据
|
||||
const state = reactive({
|
||||
statusCur: '',
|
||||
capsuleStyle: {},
|
||||
capsuleBack: {},
|
||||
isDark: true,
|
||||
});
|
||||
|
||||
const sys_statusBar = sheep.$platform.device.statusBarHeight;
|
||||
const sys_navBar = sheep.$platform.navbar;
|
||||
|
||||
const props = defineProps({
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
|
||||
title: {
|
||||
//返回文本
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'bg-white',
|
||||
},
|
||||
// 常驻
|
||||
alway: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
opacity: {
|
||||
//是否开启滑动渐变
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
noFixed: {
|
||||
//是否浮动
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
capsule: {
|
||||
//是否开启胶囊返回
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
stopBack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: [Boolean],
|
||||
default: false,
|
||||
},
|
||||
bgStyles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['navback', 'clickLeft']);
|
||||
const hasHistory = sheep.$router.hasHistory();
|
||||
|
||||
onBeforeMount(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
onPageScroll((e) => {
|
||||
let top = e.scrollTop;
|
||||
state.isDark = top < sheep.$platform.navbar;
|
||||
});
|
||||
|
||||
function onClickLeft() {
|
||||
if (hasHistory) {
|
||||
sheep.$router.back();
|
||||
} else {
|
||||
sheep.$router.go('/pages/tabbar/index');
|
||||
}
|
||||
emits('clickLeft');
|
||||
}
|
||||
function onClickRight() {
|
||||
showMenuTools();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
// #ifdef MP-ALIPAY
|
||||
my.hideAllFavoriteMenu();
|
||||
// #endif
|
||||
state.capsuleStyle = {
|
||||
width: sheep.$platform.capsule.width + 'px',
|
||||
height: sheep.$platform.capsule.height + 'px',
|
||||
};
|
||||
|
||||
state.capsuleBack = state.capsuleStyle;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-box {
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 30rpx;
|
||||
width: 134rpx;
|
||||
height: 56rpx;
|
||||
margin-left: 8rpx;
|
||||
border: 1px solid rgba(#fff, 0.4);
|
||||
.line {
|
||||
width: 2rpx;
|
||||
height: 24rpx;
|
||||
background: #e5e5e7;
|
||||
}
|
||||
.sicon-back {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.sicon-home {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.sicon-more {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.icon-button {
|
||||
width: 67rpx;
|
||||
height: 56rpx;
|
||||
&-left:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 30rpx 0px 0px 30rpx;
|
||||
}
|
||||
&-right:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 0px 30rpx 30rpx 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.navbar-title {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
.tools-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
.ui-navbar-box {
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
|
||||
.ui-bar {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
@include flex-bar;
|
||||
|
||||
.back {
|
||||
@include flex-bar;
|
||||
|
||||
.back-icon {
|
||||
@include flex-center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin: 0 10rpx;
|
||||
font-size: 46rpx !important;
|
||||
|
||||
&.opacityIcon {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(127, 127, 127, 0.5);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 200%;
|
||||
width: 200%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.1;
|
||||
border: 1px solid currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY */
|
||||
._icon-back {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.capsule {
|
||||
@include flex-bar;
|
||||
border-radius: 100px;
|
||||
position: relative;
|
||||
|
||||
&.dark {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 60%;
|
||||
width: 1px;
|
||||
left: 50%;
|
||||
top: 20%;
|
||||
background-color: currentColor;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 200%;
|
||||
width: 200%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.1;
|
||||
border: 1px solid currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.capsule-back,
|
||||
.capsule-home {
|
||||
@include flex-center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.isFristPage {
|
||||
.capsule-back,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
@include flex-bar;
|
||||
|
||||
.right-content {
|
||||
@include flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
@include flex-center;
|
||||
text-overflow: ellipsis;
|
||||
// text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
height: 36px;
|
||||
max-width: calc(100vw - 200px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-bar-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
483
sheep/ui/su-navbar/su-navbar.vue
Normal file
483
sheep/ui/su-navbar/su-navbar.vue
Normal file
@@ -0,0 +1,483 @@
|
||||
<!-- 自定义导航栏 -->
|
||||
<template>
|
||||
<view class="uni-navbar" :class="{ 'uni-dark': dark }">
|
||||
<view
|
||||
:class="{
|
||||
'uni-navbar--fixed': fixed,
|
||||
'uni-navbar--shadow': shadow,
|
||||
'uni-navbar--border': border,
|
||||
}"
|
||||
class="uni-navbar__content"
|
||||
>
|
||||
<view class="fixed-bg" :class="[opacity ? '' : opacityBgUi]"></view>
|
||||
<su-status-bar v-if="statusBar" />
|
||||
<view
|
||||
:style="{
|
||||
color: themeColor,
|
||||
height: navbarHeight,
|
||||
background: backgroundColor,
|
||||
}"
|
||||
class="uni-navbar__header"
|
||||
>
|
||||
<view
|
||||
class="uni-navbar__header-btns uni-navbar__header-btns-left"
|
||||
:style="{ width: leftIconWidth }"
|
||||
>
|
||||
<slot name="left">
|
||||
<view class="uni-navbar__content_view" v-if="leftIcon.length > 0">
|
||||
<view class="icon-box ss-flex">
|
||||
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
|
||||
<text class="sicon-back" v-if="hasHistory" />
|
||||
<text class="sicon-home" v-else />
|
||||
</view>
|
||||
<view class="line"></view>
|
||||
<view
|
||||
class="icon-button icon-button-right ss-flex ss-row-center"
|
||||
@tap="showMenuTools"
|
||||
>
|
||||
<text class="sicon-more" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
:class="{ 'uni-navbar-btn-icon-left': !leftIcon.length > 0 }"
|
||||
class="uni-navbar-btn-text"
|
||||
v-if="
|
||||
titleAlign === 'left' &&
|
||||
title.length &&
|
||||
sheep.$platform.name !== 'WechatOfficialAccount'
|
||||
"
|
||||
>
|
||||
<text :style="{ color: themeColor, fontSize: '18px' }">{{ title }}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-if="tools === 'search'" class="ss-flex-1">
|
||||
<slot name="center">
|
||||
<uni-search-bar
|
||||
class="ss-flex-1 search-box"
|
||||
:radius="20"
|
||||
placeholder="请输入关键词"
|
||||
cancelButton="none"
|
||||
v-model="searchModel"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-else class="uni-navbar__header-container" @tap="onClickTitle">
|
||||
<slot name="center">
|
||||
<view
|
||||
v-if="tools === 'title' && titleAlign === 'center' && title.length"
|
||||
class="uni-navbar__header-container-inner"
|
||||
>
|
||||
<text :style="{ color: themeColor, fontSize: '36rpx' }" class="ss-line-1">{{
|
||||
title
|
||||
}}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="uni-navbar__placeholder" v-if="placeholder">
|
||||
<su-status-bar v-if="statusBar" />
|
||||
<view class="uni-navbar__placeholder-view" :style="{ height: navbarHeight }" />
|
||||
</view>
|
||||
<!-- 头部问题 -->
|
||||
<!-- #ifdef MP -->
|
||||
<!-- <view :style="[capsuleStyle]"></view> -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* NavBar 自定义导航栏
|
||||
* @description 导航栏组件,主要用于头部导航
|
||||
* @property {Boolean} dark 开启黑暗模式
|
||||
* @property {String} title 标题文字
|
||||
* @property {String} rightText 右侧按钮文本
|
||||
* @property {String} leftIcon 左侧按钮图标
|
||||
* @property {String} rightIcon 右侧按钮图标
|
||||
* @property {String} color 图标和文字颜色
|
||||
* @property {String} backgroundColor 导航栏背景颜色
|
||||
* @property {Boolean} fixed = [true|false] 是否固定顶部
|
||||
* @property {Boolean} statusBar = [true|false] 是否包含状态栏
|
||||
* @property {Boolean} shadow = [true|false] 导航栏下是否有阴影
|
||||
* @event {Function} clickLeft 左侧按钮点击时触发
|
||||
* @event {Function} clickRight 右侧按钮点击时触发
|
||||
* @event {Function} clickTitle 中间标题点击时触发
|
||||
*/
|
||||
|
||||
const getVal = (val) => (typeof val === 'number' ? val + 'px' : val);
|
||||
|
||||
const emits = defineEmits(['clickLeft', 'clickRight', 'clickTitle', 'search']);
|
||||
const props = defineProps({
|
||||
dark: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
titleAlign: {
|
||||
type: String,
|
||||
default: 'center', // left | center
|
||||
},
|
||||
rightText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fixed: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
opacity: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
opacityBgUi: {
|
||||
type: String,
|
||||
default: 'bg-white',
|
||||
},
|
||||
statusBar: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
shadow: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
border: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 44,
|
||||
},
|
||||
leftWidth: {
|
||||
type: [Number, String],
|
||||
default: 80,
|
||||
},
|
||||
rightWidth: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
tools: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
defaultSearch: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const capsuleStyle = computed(() => {
|
||||
return {
|
||||
width: sheep.$platform.capsule.width + 'px',
|
||||
height: sheep.$platform.capsule.height + 'px',
|
||||
margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
const searchModel = computed(() => {
|
||||
return props.defaultSearch
|
||||
})
|
||||
|
||||
const themeBgColor = computed(() => {
|
||||
if (props.dark) {
|
||||
// 默认值
|
||||
if (props.backgroundColor) {
|
||||
return props.backgroundColor;
|
||||
} else {
|
||||
return props.dark ? '#333' : '#FFF';
|
||||
}
|
||||
}
|
||||
return props.backgroundColor || '#FFF';
|
||||
});
|
||||
const themeColor = computed(() => {
|
||||
if (props.dark) {
|
||||
// 默认值
|
||||
if (props.color) {
|
||||
return props.color;
|
||||
} else {
|
||||
return props.dark ? '#fff' : '#333';
|
||||
}
|
||||
}
|
||||
return props.color || '#333';
|
||||
});
|
||||
const navbarHeight = computed(() => {
|
||||
return getVal(props.height);
|
||||
});
|
||||
const leftIconWidth = computed(() => {
|
||||
return getVal(props.leftWidth);
|
||||
});
|
||||
const rightIconWidth = computed(() => {
|
||||
return getVal(props.rightWidth);
|
||||
});
|
||||
|
||||
function onSearch(e) {
|
||||
emits('search', e.value);
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
if (uni.report && props.title !== '') {
|
||||
uni.report('title', props.title);
|
||||
}
|
||||
});
|
||||
|
||||
const hasHistory = sheep.$router.hasHistory();
|
||||
|
||||
function onClickLeft() {
|
||||
if (hasHistory) {
|
||||
sheep.$router.back();
|
||||
} else {
|
||||
sheep.$router.go('/pages/tabbar/index');
|
||||
}
|
||||
emits('clickLeft');
|
||||
}
|
||||
function onClickRight() {
|
||||
showMenuTools();
|
||||
}
|
||||
function onClickTitle() {
|
||||
emits('clickTitle');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bg-main {
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient)) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.icon-box {
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 30rpx;
|
||||
width: 134rpx;
|
||||
height: 56rpx;
|
||||
margin-left: 8rpx;
|
||||
.line {
|
||||
width: 2rpx;
|
||||
height: 24rpx;
|
||||
background: #e5e5e7;
|
||||
}
|
||||
.sicon-back {
|
||||
font-size: 32rpx;
|
||||
color: #000;
|
||||
}
|
||||
.sicon-home {
|
||||
font-size: 32rpx;
|
||||
color: #000;
|
||||
}
|
||||
.sicon-more {
|
||||
font-size: 32rpx;
|
||||
color: #000;
|
||||
}
|
||||
.icon-button {
|
||||
width: 67rpx;
|
||||
height: 56rpx;
|
||||
&-left:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 30rpx 0px 0px 30rpx;
|
||||
}
|
||||
&-right:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 0px 30rpx 30rpx 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nav-height: 44px;
|
||||
|
||||
.fixed-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.uni-nav-bar-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 34rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 14px;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-nav-bar-right-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.uni-navbar__content {
|
||||
position: relative;
|
||||
// background-color: #fff;
|
||||
// box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.uni-navbar__content_view {
|
||||
// box-sizing: border-box;
|
||||
}
|
||||
|
||||
.uni-navbar-btn-text {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.uni-navbar__header {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
padding: 0 10px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
height: $nav-height;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.uni-navbar__header-btns {
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
// min-width: 120rpx;
|
||||
min-width: 40rpx;
|
||||
// padding: 0 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* #ifdef H5 */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-navbar__header-btns-left {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
width: 120rpx;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-navbar__header-btns-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
// width: 150rpx;
|
||||
// padding-right: 30rpx;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.uni-navbar__header-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
// display: flex;
|
||||
/* #endif */
|
||||
// flex: 1;
|
||||
// padding: 0 10px;
|
||||
// overflow: hidden;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.uni-navbar__header-container-inner {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
// box-sizing: border-box;
|
||||
}
|
||||
|
||||
.uni-navbar__placeholder-view {
|
||||
height: $nav-height;
|
||||
}
|
||||
|
||||
.uni-navbar--fixed {
|
||||
position: fixed;
|
||||
z-index: 998;
|
||||
/* #ifdef H5 */
|
||||
left: var(--window-left);
|
||||
right: var(--window-right);
|
||||
/* #endif */
|
||||
/* #ifndef H5 */
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-navbar--shadow {
|
||||
box-shadow: 0 1px 6px #ccc;
|
||||
}
|
||||
|
||||
.uni-navbar--border {
|
||||
border-bottom-width: 1rpx;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #eee;
|
||||
}
|
||||
|
||||
.uni-ellipsis-1 {
|
||||
overflow: hidden;
|
||||
/* #ifndef APP-NVUE */
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
// 暗主题配置
|
||||
.uni-dark {
|
||||
}
|
||||
</style>
|
473
sheep/ui/su-notice-bar/su-notice-bar.vue
Normal file
473
sheep/ui/su-notice-bar/su-notice-bar.vue
Normal file
@@ -0,0 +1,473 @@
|
||||
<!-- 公告栏组件 -->
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
class="uni-noticebar"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot name="icon">
|
||||
<uni-icons
|
||||
v-if="showIcon === true || showIcon === 'true'"
|
||||
class="uni-noticebar-icon"
|
||||
type="sound"
|
||||
:color="color"
|
||||
size="22"
|
||||
/>
|
||||
</slot>
|
||||
<view
|
||||
ref="textBox"
|
||||
class="uni-noticebar__content-wrapper"
|
||||
:class="{
|
||||
'uni-noticebar__content-wrapper--scrollable': scrollable,
|
||||
'uni-noticebar__content-wrapper--single': !scrollable && (single || moreText),
|
||||
}"
|
||||
>
|
||||
<view
|
||||
:id="elIdBox"
|
||||
class="uni-noticebar__content"
|
||||
:class="{
|
||||
'uni-noticebar__content--scrollable': scrollable,
|
||||
'uni-noticebar__content--single': !scrollable && (single || moreText),
|
||||
}"
|
||||
>
|
||||
<text
|
||||
:id="elId"
|
||||
ref="animationEle"
|
||||
class="uni-noticebar__content-text"
|
||||
:class="{
|
||||
'uni-noticebar__content-text--scrollable': scrollable,
|
||||
'uni-noticebar__content-text--single': !scrollable && (single || showGetMore),
|
||||
}"
|
||||
:style="{
|
||||
color: color,
|
||||
width: wrapWidth + 'px',
|
||||
animationDuration: animationDuration,
|
||||
'-webkit-animationDuration': animationDuration,
|
||||
animationPlayState: webviewHide ? 'paused' : animationPlayState,
|
||||
'-webkit-animationPlayState': webviewHide ? 'paused' : animationPlayState,
|
||||
animationDelay: animationDelay,
|
||||
'-webkit-animationDelay': animationDelay,
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="showGetMore === true || showGetMore === 'true'"
|
||||
class="uni-noticebar__more uni-cursor-point"
|
||||
@click="clickMore"
|
||||
>
|
||||
<text
|
||||
v-if="moreText.length > 0"
|
||||
:style="{ color: moreColor }"
|
||||
class="uni-noticebar__more-text"
|
||||
>
|
||||
{{ moreText }}
|
||||
</text>
|
||||
<uni-icons v-else type="right" :color="moreColor" size="16" />
|
||||
</view>
|
||||
<view
|
||||
class="uni-noticebar-close uni-cursor-point"
|
||||
v-if="
|
||||
(showClose === true || showClose === 'true') &&
|
||||
(showGetMore === false || showGetMore === 'false')
|
||||
"
|
||||
>
|
||||
<view @click="close">
|
||||
<slot name="close">
|
||||
<uni-icons type="closeempty" :color="color" size="16" />
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sheep from '@/sheep';
|
||||
// #ifdef APP-NVUE
|
||||
const dom = weex.requireModule('dom');
|
||||
const animation = weex.requireModule('animation');
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* NoticeBar 自定义导航栏
|
||||
* @description 通告栏组件
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=30
|
||||
* @property {Number} speed 文字滚动的速度,默认100px/秒
|
||||
* @property {String} text 显示文字
|
||||
* @property {String} backgroundColor 背景颜色
|
||||
* @property {String} color 文字颜色
|
||||
* @property {String} moreColor 查看更多文字的颜色
|
||||
* @property {String} moreText 设置“查看更多”的文本
|
||||
* @property {Boolean} single = [true|false] 是否单行
|
||||
* @property {Boolean} scrollable = [true|false] 是否滚动,为true时,NoticeBar为单行
|
||||
* @property {Boolean} showIcon = [true|false] 是否显示左侧喇叭图标
|
||||
* @property {Boolean} showClose = [true|false] 是否显示左侧关闭按钮
|
||||
* @property {Boolean} showGetMore = [true|false] 是否显示右侧查看更多图标,为true时,NoticeBar为单行
|
||||
* @event {Function} click 点击 NoticeBar 触发事件
|
||||
* @event {Function} close 关闭 NoticeBar 触发事件
|
||||
* @event {Function} getmore 点击”查看更多“时触发事件
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'UniNoticeBar',
|
||||
emits: ['click', 'getmore', 'close'],
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
moreText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
speed: {
|
||||
// 默认1s滚动100px
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--ui-BG-Main)',
|
||||
},
|
||||
moreColor: {
|
||||
type: String,
|
||||
default: '#FF9A43',
|
||||
},
|
||||
single: {
|
||||
// 是否单行
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
scrollable: {
|
||||
// 是否滚动,添加后控制单行效果取消
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
showIcon: {
|
||||
// 是否显示左侧icon
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
showGetMore: {
|
||||
// 是否显示右侧查看更多
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
showClose: {
|
||||
// 是否显示左侧关闭按钮
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const elId = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`;
|
||||
const elIdBox = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`;
|
||||
return {
|
||||
textWidth: 0,
|
||||
boxWidth: 0,
|
||||
wrapWidth: '',
|
||||
webviewHide: false,
|
||||
// #ifdef APP-NVUE
|
||||
stopAnimation: false,
|
||||
// #endif
|
||||
elId: elId,
|
||||
elIdBox: elIdBox,
|
||||
show: true,
|
||||
animationDuration: 'none',
|
||||
animationPlayState: 'paused',
|
||||
animationDelay: '0s',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef APP-PLUS
|
||||
var pages = getCurrentPages();
|
||||
var page = pages[pages.length - 1];
|
||||
var currentWebview = page.$getAppWebview();
|
||||
currentWebview.addEventListener('hide', () => {
|
||||
this.webviewHide = true;
|
||||
});
|
||||
currentWebview.addEventListener('show', () => {
|
||||
this.webviewHide = false;
|
||||
});
|
||||
// #endif
|
||||
this.$nextTick(() => {
|
||||
this.initSize();
|
||||
});
|
||||
},
|
||||
// #ifdef APP-NVUE
|
||||
beforeDestroy() {
|
||||
this.stopAnimation = true;
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
initSize() {
|
||||
if (this.scrollable) {
|
||||
// #ifndef APP-NVUE
|
||||
let query = [],
|
||||
boxWidth = 0,
|
||||
textWidth = 0;
|
||||
let textQuery = new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this)
|
||||
// #endif
|
||||
.select(`#${this.elId}`)
|
||||
.boundingClientRect()
|
||||
.exec((ret) => {
|
||||
this.textWidth = ret[0].width;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
let boxQuery = new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this)
|
||||
// #endif
|
||||
.select(`#${this.elIdBox}`)
|
||||
.boundingClientRect()
|
||||
.exec((ret) => {
|
||||
this.boxWidth = ret[0].width;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
query.push(textQuery);
|
||||
query.push(boxQuery);
|
||||
Promise.all(query).then(() => {
|
||||
this.animationDuration = `${this.textWidth / this.speed}s`;
|
||||
this.animationDelay = `-${this.boxWidth / this.speed}s`;
|
||||
setTimeout(() => {
|
||||
this.animationPlayState = 'running';
|
||||
}, 1000);
|
||||
});
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
dom.getComponentRect(this.$refs['animationEle'], (res) => {
|
||||
let winWidth = sheep.$platform.device.windowWidth;
|
||||
this.textWidth = res.size.width;
|
||||
animation.transition(
|
||||
this.$refs['animationEle'],
|
||||
{
|
||||
styles: {
|
||||
transform: `translateX(-${winWidth}px)`,
|
||||
},
|
||||
duration: 0,
|
||||
timingFunction: 'linear',
|
||||
delay: 0,
|
||||
},
|
||||
() => {
|
||||
if (!this.stopAnimation) {
|
||||
animation.transition(
|
||||
this.$refs['animationEle'],
|
||||
{
|
||||
styles: {
|
||||
transform: `translateX(-${this.textWidth}px)`,
|
||||
},
|
||||
timingFunction: 'linear',
|
||||
duration: ((this.textWidth - winWidth) / this.speed) * 1000,
|
||||
delay: 1000,
|
||||
},
|
||||
() => {
|
||||
if (!this.stopAnimation) {
|
||||
this.loopAnimation();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
// #ifdef APP-NVUE
|
||||
if (!this.scrollable && (this.single || this.moreText)) {
|
||||
dom.getComponentRect(this.$refs['textBox'], (res) => {
|
||||
this.wrapWidth = res.size.width;
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
loopAnimation() {
|
||||
// #ifdef APP-NVUE
|
||||
animation.transition(
|
||||
this.$refs['animationEle'],
|
||||
{
|
||||
styles: {
|
||||
transform: `translateX(0px)`,
|
||||
},
|
||||
duration: 0,
|
||||
},
|
||||
() => {
|
||||
if (!this.stopAnimation) {
|
||||
animation.transition(
|
||||
this.$refs['animationEle'],
|
||||
{
|
||||
styles: {
|
||||
transform: `translateX(-${this.textWidth}px)`,
|
||||
},
|
||||
duration: (this.textWidth / this.speed) * 1000,
|
||||
timingFunction: 'linear',
|
||||
delay: 0,
|
||||
},
|
||||
() => {
|
||||
if (!this.stopAnimation) {
|
||||
this.loopAnimation();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
// #endif
|
||||
},
|
||||
clickMore() {
|
||||
this.$emit('getmore');
|
||||
},
|
||||
close() {
|
||||
this.show = false;
|
||||
this.$emit('close');
|
||||
},
|
||||
onClick() {
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uni-noticebar {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
// margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.uni-cursor-point {
|
||||
/* #ifdef H5 */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar-close {
|
||||
margin-left: 8px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.uni-noticebar-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.uni-noticebar__content-wrapper {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.uni-noticebar__content-wrapper--single {
|
||||
/* #ifndef APP-NVUE */
|
||||
line-height: 18px;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar__content-wrapper--single,
|
||||
.uni-noticebar__content-wrapper--scrollable {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
.uni-noticebar__content-wrapper--scrollable {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.uni-noticebar__content--scrollable {
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-NVUE */
|
||||
flex: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar__content--single {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar__content-text {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar__content-text--single {
|
||||
/* #ifdef APP-NVUE */
|
||||
lines: 1;
|
||||
/* #endif */
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
/* #endif */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.uni-noticebar__content-text--scrollable {
|
||||
/* #ifdef APP-NVUE */
|
||||
lines: 1;
|
||||
padding-left: 750rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
padding-left: 100%;
|
||||
animation: notice 10s 0s linear infinite both;
|
||||
animation-play-state: paused;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-noticebar__more {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.uni-noticebar__more-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes notice {
|
||||
100% {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
225
sheep/ui/su-number-box/su-number-box.vue
Normal file
225
sheep/ui/su-number-box/su-number-box.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<view class="uni-numbox">
|
||||
<!-- <view @click="_calcValue('minus')" class="uni-numbox__minus uni-numbox-btns" :style="{ background }"> -->
|
||||
<!-- <text class="uni-numbox--text" :class="{ 'uni-numbox--disabled': inputValue <= min || disabled }"
|
||||
:style="{ color }">
|
||||
-
|
||||
</text> -->
|
||||
<text
|
||||
class="cicon-move-round"
|
||||
:class="{
|
||||
'uni-numbox--disabled': inputValue <= min || disabled,
|
||||
'groupon-btn': activity === 'groupon',
|
||||
'seckill-btn': activity === 'seckill',
|
||||
}"
|
||||
@click="_calcValue('minus')"
|
||||
></text>
|
||||
<!-- </view> -->
|
||||
<input
|
||||
:disabled="disabled"
|
||||
@focus="_onFocus"
|
||||
@blur="_onBlur"
|
||||
class="uni-numbox__value"
|
||||
type="number"
|
||||
v-model="inputValue"
|
||||
:style="{ color }"
|
||||
/>
|
||||
<!-- <view @click="_calcValue('plus')" class="uni-numbox__plus uni-numbox-btns">
|
||||
<text class="uni-numbox--text" :class="{ 'uni-numbox--disabled': inputValue >= max || disabled }">+</text>
|
||||
</view> -->
|
||||
<text
|
||||
class="cicon-add-round"
|
||||
:class="{
|
||||
'uni-numbox--disabled': inputValue >= max || disabled,
|
||||
'groupon-btn': activity === 'groupon',
|
||||
'seckill-btn': activity === 'seckill',
|
||||
}"
|
||||
@click="_calcValue('plus')"
|
||||
></text>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* NumberBox 数字输入框
|
||||
* @description 带加减按钮的数字输入框
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=31
|
||||
* @property {Number} value 输入框当前值
|
||||
* @property {Number} min 最小值
|
||||
* @property {Number} max 最大值
|
||||
* @property {Number} step 每次点击改变的间隔大小
|
||||
* @property {String} background 背景色
|
||||
* @property {String} color 字体颜色(前景色)
|
||||
* @property {Boolean} disabled = [true|false] 是否为禁用状态
|
||||
* @event {Function} change 输入框值改变时触发的事件,参数为输入框当前的 value
|
||||
* @event {Function} focus 输入框聚焦时触发的事件,参数为 event 对象
|
||||
* @event {Function} blur 输入框失焦时触发的事件,参数为 event 对象
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'UniNumberBox',
|
||||
emits: ['change', 'input', 'update:modelValue', 'blur', 'focus'],
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 1,
|
||||
},
|
||||
modelValue: {
|
||||
type: [Number, String],
|
||||
default: 1,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '#f5f5f5',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#333',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activity: {
|
||||
type: String,
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.inputValue = +val;
|
||||
},
|
||||
modelValue(val) {
|
||||
this.inputValue = +val;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.value === 1) {
|
||||
this.inputValue = +this.modelValue;
|
||||
}
|
||||
if (this.modelValue === 1) {
|
||||
this.inputValue = +this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_calcValue(type) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const scale = this._getDecimalScale();
|
||||
let value = this.inputValue * scale;
|
||||
let step = this.step * scale;
|
||||
if (type === 'minus') {
|
||||
value -= step;
|
||||
if (value < this.min * scale) {
|
||||
return;
|
||||
}
|
||||
if (value > this.max * scale) {
|
||||
value = this.max * scale;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'plus') {
|
||||
value += step;
|
||||
if (value > this.max * scale) {
|
||||
return;
|
||||
}
|
||||
if (value < this.min * scale) {
|
||||
value = this.min * scale;
|
||||
}
|
||||
}
|
||||
|
||||
this.inputValue = (value / scale).toFixed(String(scale).length - 1);
|
||||
this.$emit('change', +this.inputValue);
|
||||
// TODO vue2 兼容
|
||||
this.$emit('input', +this.inputValue);
|
||||
// TODO vue3 兼容
|
||||
this.$emit('update:modelValue', +this.inputValue);
|
||||
},
|
||||
_getDecimalScale() {
|
||||
let scale = 1;
|
||||
// 浮点型
|
||||
if (~~this.step !== this.step) {
|
||||
scale = Math.pow(10, String(this.step).split('.')[1].length);
|
||||
}
|
||||
return scale;
|
||||
},
|
||||
_onBlur(event) {
|
||||
this.$emit('blur', event);
|
||||
let value = event.detail.value;
|
||||
if (!value) {
|
||||
// this.inputValue = 0;
|
||||
return;
|
||||
}
|
||||
value = +value;
|
||||
if (value > this.max) {
|
||||
value = this.max;
|
||||
} else if (value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
const scale = this._getDecimalScale();
|
||||
this.inputValue = value.toFixed(String(scale).length - 1);
|
||||
this.$emit('change', +this.inputValue);
|
||||
this.$emit('input', +this.inputValue);
|
||||
},
|
||||
_onFocus(event) {
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.uni-numbox .uni-numbox--disabled {
|
||||
color: #c0c0c0 !important;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-numbox {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-numbox__value {
|
||||
width: 74rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.cicon-move-round {
|
||||
font-size: 44rpx;
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
|
||||
.cicon-add-round {
|
||||
font-size: 44rpx;
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
|
||||
.groupon-btn {
|
||||
color: #ff6000;
|
||||
}
|
||||
|
||||
.seckill-btn {
|
||||
color: #ff5854;
|
||||
}
|
||||
</style>
|
314
sheep/ui/su-popover/su-popover.vue
Normal file
314
sheep/ui/su-popover/su-popover.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<view class="ui-popover" :class="popover ? 'show' : 'hide'">
|
||||
<view
|
||||
class="ui-popover-button"
|
||||
:class="[ui]"
|
||||
:id="'popover-button-' + elId"
|
||||
:style="{ zIndex: index + zIndexConfig.popover }"
|
||||
@tap="popoverClick"
|
||||
@mouseleave="mouseleave"
|
||||
@mouseover="mouseover"
|
||||
>
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="ui-popover-box" :style="BoxStyle">
|
||||
<view class="ui-popover-content-box" :id="'popover-content-' + elId" :style="contentStyle">
|
||||
<view
|
||||
class="ui-popover-content radius text-a"
|
||||
:class="bg"
|
||||
:style="{ zIndex: index + zIndexConfig.popover + 2 }"
|
||||
>
|
||||
<view class="p-3 text-sm" v-if="tips">{{ tips }}</view>
|
||||
<block v-else><slot name="content" /></block>
|
||||
</view>
|
||||
<view class="ui-popover-arrow" :class="bg" :style="arrowStyle"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="ui-popover-mask"
|
||||
:class="mask ? 'bg-mask-50' : ''"
|
||||
:style="{ zIndex: index + zIndexConfig.popover - 1 }"
|
||||
@tap="popover = false"
|
||||
v-if="(popover && tips == '' && time == 0) || mask"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { guid } from '@/sheep/helper';
|
||||
import zIndexConfig from '@/sheep/config/zIndex.js';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
export default {
|
||||
name: 'suPopover',
|
||||
data() {
|
||||
return {
|
||||
elId: guid(),
|
||||
zIndexConfig,
|
||||
popover: false,
|
||||
BoxStyle: '',
|
||||
contentStyle: '',
|
||||
arrowStyle: '',
|
||||
button: {},
|
||||
content: {},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tips: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'ui-BG',
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
show: {
|
||||
type: [Boolean, String],
|
||||
default: 'change',
|
||||
},
|
||||
hover: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
time: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
bottom: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
popover(val) {
|
||||
this._computedQuery(
|
||||
sheep.$platform.device.windowWidth,
|
||||
sheep.$platform.device.windowHeight,
|
||||
);
|
||||
if (val) {
|
||||
if (this.tips != '' || this.time > 0) {
|
||||
setTimeout(
|
||||
() => {
|
||||
this.popover = false;
|
||||
},
|
||||
this.time == 0 ? 3000 : this.time,
|
||||
);
|
||||
}
|
||||
this.sys_layer = this.sys_layer + 100;
|
||||
} else {
|
||||
this.sys_layer = this.sys_layer - 100;
|
||||
}
|
||||
this.$emit('update:show', val);
|
||||
},
|
||||
show(val) {
|
||||
this.popover = val;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this._computedQuery(
|
||||
sheep.$platform.device.windowWidth,
|
||||
sheep.$platform.device.windowHeight,
|
||||
);
|
||||
// #ifdef H5
|
||||
uni.onWindowResize((res) => {
|
||||
this._computedQuery(res.size.windowWidth, res.size.windowHeight);
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
_onHide() {
|
||||
this.popover = false;
|
||||
},
|
||||
_computedQuery(w, h) {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#popover-button-' + this.elId)
|
||||
.boundingClientRect((button) => {
|
||||
if (button != null) {
|
||||
this.button = button;
|
||||
} else {
|
||||
console.log('popover-button-' + this.elId + ' data error');
|
||||
}
|
||||
})
|
||||
.select('#popover-content-' + this.elId)
|
||||
.boundingClientRect((content) => {
|
||||
if (content != null) {
|
||||
this.content = content;
|
||||
let button = this.button;
|
||||
//contentStyle
|
||||
let contentStyle = '';
|
||||
let arrowStyle = '';
|
||||
this.BoxStyle = `width:${w}px; left:-${button.left}px;z-index: ${
|
||||
this.index + this.sys_layer + 102
|
||||
}`;
|
||||
// 判断气泡在上面还是下面
|
||||
if (button.bottom < h / 2 || this.bottom) {
|
||||
// '下';
|
||||
contentStyle = contentStyle + `top:10px;`;
|
||||
arrowStyle = arrowStyle + `top:${-5}px;`;
|
||||
} else {
|
||||
// '上';
|
||||
contentStyle = contentStyle + `bottom:${button.height + 10}px;`;
|
||||
arrowStyle = arrowStyle + `bottom:${-5}px;`;
|
||||
}
|
||||
|
||||
// 判断气泡箭头在左中右
|
||||
let btnCenter = button.right - button.width / 2;
|
||||
let contentCenter = content.right - content.width / 2;
|
||||
if (
|
||||
(btnCenter < w / 3 && content.width > btnCenter) ||
|
||||
(content.width > w / 2 && btnCenter < w / 2)
|
||||
) {
|
||||
// '左';
|
||||
contentStyle = contentStyle + `left:10px;`;
|
||||
arrowStyle = arrowStyle + `left:${btnCenter - 17}px;`;
|
||||
} else if (
|
||||
(btnCenter > (w / 6) * 4 && content.width > w - btnCenter) ||
|
||||
(content.width > w / 2 && btnCenter > w / 2)
|
||||
) {
|
||||
// '右';
|
||||
contentStyle = contentStyle + `right:10px;`;
|
||||
arrowStyle = arrowStyle + `right:${w - btnCenter - 17}px;`;
|
||||
} else {
|
||||
// '中';
|
||||
contentStyle =
|
||||
contentStyle + `left:${button.left - content.width / 2 + button.width / 2}px;`;
|
||||
arrowStyle = arrowStyle + `left:0px;right:0px;margin:auto;`;
|
||||
}
|
||||
|
||||
this.arrowStyle = arrowStyle + `z-index:${this.index + this.sys_layer + 1};`;
|
||||
this.contentStyle = contentStyle + `z-index:${this.index + this.sys_layer + 2};`;
|
||||
} else {
|
||||
console.log('popover-content-' + this.elId + ' data error');
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
popoverClick() {
|
||||
if (this.isChange) {
|
||||
return false;
|
||||
}
|
||||
if (this.tips == '') {
|
||||
this.popover = !this.popover;
|
||||
} else {
|
||||
this.popover = true;
|
||||
}
|
||||
},
|
||||
mouseover() {
|
||||
if (this.hover && (this.tips != '' || this.content.height != 0)) {
|
||||
this.popover = true;
|
||||
}
|
||||
},
|
||||
mouseleave() {
|
||||
if (this.hover) {
|
||||
this.popover = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-popover {
|
||||
position: relative;
|
||||
|
||||
.ui-popover-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-popover-box {
|
||||
position: absolute;
|
||||
|
||||
.ui-popover-content-box {
|
||||
position: absolute;
|
||||
|
||||
.ui-popover-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-popover-arrow {
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 110%;
|
||||
position: absolute;
|
||||
background-color: #000000;
|
||||
top: 5%;
|
||||
left: 0;
|
||||
filter: blur(15px);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-popover-mask {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.show {
|
||||
.ui-popover-button {
|
||||
}
|
||||
|
||||
.ui-popover-content-box {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ui-popover-arrow {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-popover-mask {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
.ui-popover-button {
|
||||
}
|
||||
|
||||
.ui-popover-content-box {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui-popover-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-popover-mask {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
45
sheep/ui/su-popup/keypress.js
Normal file
45
sheep/ui/su-popup/keypress.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// #ifdef H5
|
||||
export default {
|
||||
name: 'Keypress',
|
||||
props: {
|
||||
disable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const keyNames = {
|
||||
esc: ['Esc', 'Escape'],
|
||||
tab: 'Tab',
|
||||
enter: 'Enter',
|
||||
space: [' ', 'Spacebar'],
|
||||
up: ['Up', 'ArrowUp'],
|
||||
left: ['Left', 'ArrowLeft'],
|
||||
right: ['Right', 'ArrowRight'],
|
||||
down: ['Down', 'ArrowDown'],
|
||||
delete: ['Backspace', 'Delete', 'Del'],
|
||||
};
|
||||
const listener = ($event) => {
|
||||
if (this.disable) {
|
||||
return;
|
||||
}
|
||||
const keyName = Object.keys(keyNames).find((key) => {
|
||||
const keyName = $event.key;
|
||||
const value = keyNames[key];
|
||||
return value === keyName || (Array.isArray(value) && value.includes(keyName));
|
||||
});
|
||||
if (keyName) {
|
||||
// 避免和其他按键事件冲突
|
||||
setTimeout(() => {
|
||||
this.$emit(keyName, {});
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keyup', listener);
|
||||
// this.$once('hook:beforeDestroy', () => {
|
||||
// document.removeEventListener('keyup', listener)
|
||||
// })
|
||||
},
|
||||
render: () => {},
|
||||
};
|
||||
// #endif
|
589
sheep/ui/su-popup/su-popup.vue
Normal file
589
sheep/ui/su-popup/su-popup.vue
Normal file
@@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="showPopup"
|
||||
class="uni-popup"
|
||||
:class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
|
||||
:style="[{ zIndex: zIndex }]"
|
||||
@touchmove.stop.prevent="clear"
|
||||
>
|
||||
<view @touchstart="touchstart">
|
||||
<uni-transition
|
||||
key="1"
|
||||
v-if="maskShow"
|
||||
name="mask"
|
||||
mode-class="fade"
|
||||
:styles="maskClass"
|
||||
:duration="duration"
|
||||
:show="showTrans"
|
||||
@click="onTap"
|
||||
/>
|
||||
<uni-transition
|
||||
key="2"
|
||||
:mode-class="ani"
|
||||
name="content"
|
||||
:styles="{ ...transClass, ...borderRadius }"
|
||||
:duration="duration"
|
||||
:show="showTrans"
|
||||
@click="onTap"
|
||||
>
|
||||
<view
|
||||
v-if="showPopup"
|
||||
class="uni-popup__wrapper"
|
||||
:style="[{ backgroundColor: bg }, borderRadius]"
|
||||
:class="[popupstyle]"
|
||||
@click="clear"
|
||||
>
|
||||
<uni-icons
|
||||
v-if="showClose"
|
||||
class="close-icon"
|
||||
color="#F6F6F6"
|
||||
type="closeempty"
|
||||
size="32"
|
||||
@click="close"
|
||||
></uni-icons>
|
||||
<slot />
|
||||
</view>
|
||||
</uni-transition>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<keypress v-if="maskShow" @esc="onTap" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- #ifdef MP -->
|
||||
<view v-else style="display: none">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef H5
|
||||
import keypress from './keypress.js';
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* PopUp 弹出层
|
||||
* @description 弹出层组件,为了解决遮罩弹层的问题
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
|
||||
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
|
||||
* @value top 顶部弹出
|
||||
* @value center 中间弹出
|
||||
* @value bottom 底部弹出
|
||||
* @value left 左侧弹出
|
||||
* @value right 右侧弹出
|
||||
* @value message 消息提示
|
||||
* @value dialog 对话框
|
||||
* @value share 底部分享示例
|
||||
* @property {Boolean} animation = [true|false] 是否开启动画
|
||||
* @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
|
||||
* @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
|
||||
* @property {String} backgroundColor 主窗口背景色
|
||||
* @property {String} maskBackgroundColor 蒙版颜色
|
||||
* @property {Boolean} safeArea 是否适配底部安全区
|
||||
* @event {Function} change 打开关闭弹窗触发,e={show: false}
|
||||
* @event {Function} maskClick 点击遮罩触发
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
|
||||
export default {
|
||||
name: 'SuPopup',
|
||||
components: {
|
||||
// #ifdef H5
|
||||
keypress,
|
||||
// #endif
|
||||
},
|
||||
emits: ['change', 'maskClick', 'close'],
|
||||
props: {
|
||||
// 开启状态
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 顶部,底部时有效
|
||||
space: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 默认圆角
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
// 是否显示关闭
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 开启动画
|
||||
animation: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
|
||||
// message: 消息提示 ; dialog : 对话框
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bottom',
|
||||
},
|
||||
// maskClick
|
||||
isMaskClick: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
// TODO 2 个版本后废弃属性 ,使用 isMaskClick
|
||||
maskClick: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
// 可设置none
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#ffffff',
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
safeArea: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
maskBackgroundColor: {
|
||||
type: String,
|
||||
default: 'rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 10075,
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
show: {
|
||||
handler: function (newValue, oldValue) {
|
||||
if (typeof oldValue === 'undefined' && !newValue) {
|
||||
return;
|
||||
}
|
||||
if (newValue) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
/**
|
||||
* 监听type类型
|
||||
*/
|
||||
type: {
|
||||
handler: function (type) {
|
||||
if (!this.config[type]) return;
|
||||
this[this.config[type]](true);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
isDesktop: {
|
||||
handler: function (newVal) {
|
||||
if (!this.config[newVal]) return;
|
||||
this[this.config[this.type]](true);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
/**
|
||||
* 监听遮罩是否可点击
|
||||
* @param {Object} val
|
||||
*/
|
||||
maskClick: {
|
||||
handler: function (val) {
|
||||
this.mkclick = val;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
isMaskClick: {
|
||||
handler: function (val) {
|
||||
this.mkclick = val;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
// H5 下禁止底部滚动
|
||||
showPopup(show) {
|
||||
// #ifdef H5
|
||||
// fix by mehaotian 处理 h5 滚动穿透的问题
|
||||
document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible';
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sheep,
|
||||
duration: 300,
|
||||
ani: [],
|
||||
showPopup: false,
|
||||
showTrans: false,
|
||||
popupWidth: 0,
|
||||
popupHeight: 0,
|
||||
config: {
|
||||
top: 'top',
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
message: 'top',
|
||||
dialog: 'center',
|
||||
share: 'bottom',
|
||||
},
|
||||
maskClass: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
transClass: {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
maskShow: true,
|
||||
mkclick: true,
|
||||
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDesktop() {
|
||||
return this.popupWidth >= 500 && this.popupHeight >= 500;
|
||||
},
|
||||
bg() {
|
||||
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
|
||||
return 'transparent';
|
||||
}
|
||||
return this.backgroundColor;
|
||||
},
|
||||
borderRadius() {
|
||||
if (this.round) {
|
||||
if (this.type === 'bottom') {
|
||||
return {
|
||||
'border-top-left-radius': parseFloat(this.round) + 'px',
|
||||
'border-top-right-radius': parseFloat(this.round) + 'px',
|
||||
};
|
||||
}
|
||||
if (this.type === 'center') {
|
||||
return {
|
||||
'border-top-left-radius': parseFloat(this.round) + 'px',
|
||||
'border-top-right-radius': parseFloat(this.round) + 'px',
|
||||
'border-bottom-left-radius': parseFloat(this.round) + 'px',
|
||||
'border-bottom-right-radius': parseFloat(this.round) + 'px',
|
||||
};
|
||||
}
|
||||
if (this.type === 'top') {
|
||||
return {
|
||||
'border-bottom-left-radius': parseFloat(this.round) + 'px',
|
||||
'border-bottom-right-radius': parseFloat(this.round) + 'px',
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const fixSize = () => {
|
||||
const { windowWidth, windowHeight, windowTop, safeArea, screenHeight, safeAreaInsets } =
|
||||
sheep.$platform.device;
|
||||
this.popupWidth = windowWidth;
|
||||
this.popupHeight = windowHeight + (windowTop || 0);
|
||||
// TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
|
||||
if (safeArea && this.safeArea) {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.safeAreaInsets = screenHeight - safeArea.bottom;
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
this.safeAreaInsets = safeAreaInsets.bottom;
|
||||
// #endif
|
||||
} else {
|
||||
this.safeAreaInsets = 0;
|
||||
}
|
||||
};
|
||||
fixSize();
|
||||
// #ifdef H5
|
||||
// window.addEventListener('resize', fixSize)
|
||||
// this.$once('hook:beforeDestroy', () => {
|
||||
// window.removeEventListener('resize', fixSize)
|
||||
// })
|
||||
// #endif
|
||||
},
|
||||
// #ifndef VUE3
|
||||
// TODO vue2
|
||||
destroyed() {
|
||||
this.setH5Visible();
|
||||
},
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
// TODO vue3
|
||||
unmounted() {
|
||||
this.setH5Visible();
|
||||
},
|
||||
// #endif
|
||||
created() {
|
||||
// this.mkclick = this.isMaskClick || this.maskClick
|
||||
if (this.isMaskClick === null && this.maskClick === null) {
|
||||
this.mkclick = true;
|
||||
} else {
|
||||
this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick;
|
||||
}
|
||||
if (this.animation) {
|
||||
this.duration = 300;
|
||||
} else {
|
||||
this.duration = 0;
|
||||
}
|
||||
// TODO 处理 message 组件生命周期异常的问题
|
||||
this.messageChild = null;
|
||||
// TODO 解决头条冒泡的问题
|
||||
this.clearPropagation = false;
|
||||
this.maskClass.backgroundColor = this.maskBackgroundColor;
|
||||
},
|
||||
methods: {
|
||||
setH5Visible() {
|
||||
// #ifdef H5
|
||||
// fix by mehaotian 处理 h5 滚动穿透的问题
|
||||
document.getElementsByTagName('body')[0].style.overflow = 'visible';
|
||||
// #endif
|
||||
},
|
||||
/**
|
||||
* 公用方法,不显示遮罩层
|
||||
*/
|
||||
closeMask() {
|
||||
this.maskShow = false;
|
||||
},
|
||||
/**
|
||||
* 公用方法,遮罩层禁止点击
|
||||
*/
|
||||
disableMask() {
|
||||
this.mkclick = false;
|
||||
},
|
||||
// TODO nvue 取消冒泡
|
||||
clear(e) {
|
||||
// #ifndef APP-NVUE
|
||||
e.stopPropagation();
|
||||
// #endif
|
||||
this.clearPropagation = true;
|
||||
},
|
||||
|
||||
open(direction) {
|
||||
// fix by mehaotian 处理快速打开关闭的情况
|
||||
if (this.showPopup) {
|
||||
clearTimeout(this.timer);
|
||||
this.showPopup = false;
|
||||
}
|
||||
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share'];
|
||||
if (!(direction && innerType.indexOf(direction) !== -1)) {
|
||||
direction = this.type;
|
||||
}
|
||||
if (!this.config[direction]) {
|
||||
console.error('缺少类型:', direction);
|
||||
return;
|
||||
}
|
||||
this[this.config[direction]]();
|
||||
this.$emit('change', {
|
||||
show: true,
|
||||
type: direction,
|
||||
});
|
||||
},
|
||||
close(type) {
|
||||
this.showTrans = false;
|
||||
this.$emit('change', {
|
||||
show: false,
|
||||
type: this.type,
|
||||
});
|
||||
this.$emit('close');
|
||||
clearTimeout(this.timer);
|
||||
// // 自定义关闭事件
|
||||
// this.customOpen && this.customClose()
|
||||
this.timer = setTimeout(() => {
|
||||
this.showPopup = false;
|
||||
}, 300);
|
||||
},
|
||||
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
|
||||
touchstart() {
|
||||
this.clearPropagation = false;
|
||||
},
|
||||
|
||||
onTap() {
|
||||
if (this.clearPropagation) {
|
||||
// fix by mehaotian 兼容 nvue
|
||||
this.clearPropagation = false;
|
||||
return;
|
||||
}
|
||||
this.$emit('maskClick');
|
||||
if (!this.mkclick) return;
|
||||
this.close();
|
||||
},
|
||||
/**
|
||||
* 顶部弹出样式处理
|
||||
*/
|
||||
top(type) {
|
||||
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top';
|
||||
this.ani = ['slide-top'];
|
||||
this.transClass = {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: this.space + 'px',
|
||||
backgroundColor: this.bg,
|
||||
};
|
||||
// TODO 兼容 type 属性 ,后续会废弃
|
||||
if (type) return;
|
||||
this.showPopup = true;
|
||||
this.showTrans = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.messageChild && this.type === 'message') {
|
||||
this.messageChild.timerClose();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 底部弹出样式处理
|
||||
*/
|
||||
bottom(type) {
|
||||
this.popupstyle = 'bottom';
|
||||
this.ani = ['slide-bottom'];
|
||||
this.transClass = {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
paddingBottom: this.safeAreaInsets + this.space + 'px',
|
||||
backgroundColor: this.bg,
|
||||
};
|
||||
// TODO 兼容 type 属性 ,后续会废弃
|
||||
if (type) return;
|
||||
this.showPopup = true;
|
||||
this.showTrans = true;
|
||||
},
|
||||
/**
|
||||
* 中间弹出样式处理
|
||||
*/
|
||||
center(type) {
|
||||
this.popupstyle = 'center';
|
||||
this.ani = ['zoom-out', 'fade'];
|
||||
this.transClass = {
|
||||
position: 'fixed',
|
||||
/* #ifndef APP-NVUE */
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
/* #endif */
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
// TODO 兼容 type 属性 ,后续会废弃
|
||||
if (type) return;
|
||||
this.showPopup = true;
|
||||
this.showTrans = true;
|
||||
},
|
||||
left(type) {
|
||||
this.popupstyle = 'left';
|
||||
this.ani = ['slide-left'];
|
||||
this.transClass = {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
backgroundColor: this.bg,
|
||||
/* #ifndef APP-NVUE */
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
/* #endif */
|
||||
};
|
||||
// TODO 兼容 type 属性 ,后续会废弃
|
||||
if (type) return;
|
||||
this.showPopup = true;
|
||||
this.showTrans = true;
|
||||
},
|
||||
right(type) {
|
||||
this.popupstyle = 'right';
|
||||
this.ani = ['slide-right'];
|
||||
this.transClass = {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
backgroundColor: this.bg,
|
||||
/* #ifndef APP-NVUE */
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
/* #endif */
|
||||
};
|
||||
// TODO 兼容 type 属性 ,后续会废弃
|
||||
if (type) return;
|
||||
this.showPopup = true;
|
||||
this.showTrans = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// 关闭icon
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -80rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.uni-popup {
|
||||
position: fixed;
|
||||
/* #ifndef APP-NVUE */
|
||||
z-index: 99;
|
||||
|
||||
/* #endif */
|
||||
&.top,
|
||||
&.left,
|
||||
&.right {
|
||||
/* #ifdef H5 */
|
||||
top: var(--window-top);
|
||||
/* #endif */
|
||||
/* #ifndef H5 */
|
||||
top: 0;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-popup__wrapper {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
position: relative;
|
||||
background: v-bind(backgroundImage) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
/* iphonex 等安全区设置,底部安全区适配 */
|
||||
/* #ifndef APP-NVUE */
|
||||
// padding-bottom: constant(safe-area-inset-bottom);
|
||||
// padding-bottom: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
&.left,
|
||||
&.right {
|
||||
/* #ifdef H5 */
|
||||
padding-top: var(--window-top);
|
||||
/* #endif */
|
||||
/* #ifndef H5 */
|
||||
padding-top: 0;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixforpc-z-index {
|
||||
/* #ifndef APP-NVUE */
|
||||
z-index: 999;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.fixforpc-top {
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
203
sheep/ui/su-progress/su-progress.vue
Normal file
203
sheep/ui/su-progress/su-progress.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="flex a-center content" v-if="lineData">
|
||||
<view>
|
||||
<slot name="content"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex a-center" style="padding-right: 10rpx">
|
||||
<view
|
||||
class="progress-container"
|
||||
id="container"
|
||||
ref="progressContainer"
|
||||
:style="{ background: inBgColor }"
|
||||
>
|
||||
<view
|
||||
class="progress-content flex j-end"
|
||||
id="content"
|
||||
ref="progressContent"
|
||||
:style="{
|
||||
height: strokeWidth + 'px',
|
||||
background: bgColor,
|
||||
width: contentWidth,
|
||||
transition: `width ${duration / 1000}s ease`,
|
||||
}"
|
||||
v-if="isAnimate"
|
||||
>
|
||||
<view class="textInside flex a-center j-center" v-if="textInside && !noData">
|
||||
<view class="text">{{ percentage }}%</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="!isAnimate"
|
||||
class="progress-content flex j-end"
|
||||
:style="{ width: percentage + '%', height: strokeWidth + 'px', background: bgColor }"
|
||||
>
|
||||
<view class="textInside flex a-center j-center" v-if="textInside && !noData">
|
||||
<view class="text">{{ percentage }}%</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view class="percentage" v-if="!textInside && !lineData && !noData && !isAnimate"
|
||||
>{{ percentage }}%
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiProgress',
|
||||
components: {},
|
||||
props: {
|
||||
// 进度条的值
|
||||
percentage: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
// 是否内联显示数据
|
||||
textInside: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 进度条高度
|
||||
strokeWidth: {
|
||||
type: [Number, String],
|
||||
default: 6,
|
||||
},
|
||||
// 默认动画时长
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 2000,
|
||||
},
|
||||
// 是否有动画
|
||||
isAnimate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%)',
|
||||
},
|
||||
// 是否不显示数据
|
||||
noData: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否自定义显示内容
|
||||
lineData: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 自定义底色
|
||||
inBgColor: {
|
||||
type: String,
|
||||
default: '#ebeef5',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
timer: null,
|
||||
containerWidth: 0,
|
||||
contentWidth: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
start() {
|
||||
if (this.isAnimate) {
|
||||
// #ifdef H5
|
||||
this.$nextTick(() => {
|
||||
let progressContainer = this.$refs.progressContainer.$el;
|
||||
let progressContent = this.$refs.progressContent.$el;
|
||||
let style = window.getComputedStyle(progressContainer, null);
|
||||
let width = style.width.replace('px', '') * ((this.percentage * 1) / 100);
|
||||
progressContent.style.width = width.toFixed(2) + 'px';
|
||||
progressContent.style.transition = `width ${this.duration / 1000}s ease`;
|
||||
});
|
||||
// #endif
|
||||
const container = uni.createSelectorQuery().in(this).selectAll('#container');
|
||||
const content = uni.createSelectorQuery().in(this).selectAll('#content');
|
||||
container.boundingClientRect().exec((res1) => {
|
||||
this.contentWidth =
|
||||
res1[0][0].width * 1 * ((this.percentage * 1) / 100).toFixed(2) + 'px';
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.start();
|
||||
});
|
||||
},
|
||||
created() {},
|
||||
filters: {},
|
||||
computed: {},
|
||||
watch: {},
|
||||
directives: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.content {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.c-per {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
border-radius: 100px;
|
||||
|
||||
.progress-content {
|
||||
border-radius: 100px;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.textInside {
|
||||
color: #fff;
|
||||
margin-right: 10rpx;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10rpx;
|
||||
font-size: 16rpx;
|
||||
width: 100rpx;
|
||||
color: #FFB9B9;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.a-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.j-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.j-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 10px;
|
||||
color: #666;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
301
sheep/ui/su-radio/su-radio.vue
Normal file
301
sheep/ui/su-radio/su-radio.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<view
|
||||
class="ui-radio ss-flex ss-col-center"
|
||||
@tap="onRaido"
|
||||
:class="[{ disabled: disabled }, { img: src }, ui]"
|
||||
:style="[customStyle]"
|
||||
>
|
||||
<slot name="leftLabel"></slot>
|
||||
<view
|
||||
v-if="!none"
|
||||
class="ui-radio-input"
|
||||
:class="[isChecked ? 'cur ' + bg : unbg, src ? 'radius' : 'round']"
|
||||
></view>
|
||||
<image class="ui-radio-img radius" v-if="src" :src="src" mode="aspectFill"></image>
|
||||
<view class="ui-radio-content" v-else>
|
||||
<slot>
|
||||
<view class="ui-label-text" :style="[labelStyle]">{{ label }}</view>
|
||||
</slot>
|
||||
</view>
|
||||
<view
|
||||
v-if="ui.includes('card')"
|
||||
class="ui-radio-bg round"
|
||||
:class="[isChecked ? 'cur ' + bg : '']"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 单选 - radio
|
||||
*
|
||||
*
|
||||
* property {Object} customStyle - 自定义样式
|
||||
* property {String} ui - radio样式Class
|
||||
* property {String} modelValue - 绑定值
|
||||
* property {Boolean} disabled - 是否禁用
|
||||
* property {String} bg - 选中时背景Class
|
||||
* property {String} unbg - 未选中时背景Class
|
||||
* property {String} src - 图片选中radio
|
||||
* property {String} label - label文本
|
||||
* property {Boolean} none - 是否隐藏raido按钮
|
||||
*
|
||||
* @slot default - 自定义label样式
|
||||
* @event {Function} change - change事件
|
||||
*
|
||||
*/
|
||||
import { computed, reactive, watchPostEffect, getCurrentInstance } from 'vue';
|
||||
const vm = getCurrentInstance();
|
||||
|
||||
// 组件数据
|
||||
const state = reactive({
|
||||
currentValue: false,
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emits = defineEmits(['change', 'update:modelValue']);
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: 'check', //check line
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'ui-BG-Main',
|
||||
},
|
||||
unbg: {
|
||||
type: String,
|
||||
default: 'borderss',
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
none: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
watchPostEffect(() => {
|
||||
state.currentValue = props.modelValue;
|
||||
emits('update:modelValue', state.currentValue);
|
||||
});
|
||||
|
||||
// 是否选中
|
||||
const isChecked = computed(() => state.currentValue);
|
||||
|
||||
// 点击
|
||||
const onRaido = () => {
|
||||
if (props.disabled) return;
|
||||
state.currentValue = !state.currentValue;
|
||||
emits('update:modelValue', state.currentValue);
|
||||
emits('change', {
|
||||
label: props.label,
|
||||
value: state.currentValue,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ui-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0.5em 0 0;
|
||||
height: 18px;
|
||||
.ui-radio-input {
|
||||
margin: 0 0.5em 0 0;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
line-height: 18px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: currentColor;
|
||||
border-radius: 18px;
|
||||
@include position-center;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-radio-input.cur {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transition: $transition-base;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin: 0 0.14286em;
|
||||
}
|
||||
|
||||
&.check {
|
||||
.ui-radio-input {
|
||||
&::before {
|
||||
font-family: 'colorui';
|
||||
content: '\e69f';
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-radio-input.cur {
|
||||
&::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 1em;
|
||||
transform: scale(0.8);
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.line {
|
||||
.ui-radio-input.cur {
|
||||
&::before {
|
||||
width: calc(100% - 2px);
|
||||
height: calc(100% - 2px);
|
||||
background-color: var(--ui-BG);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: inherit;
|
||||
border-radius: 50%;
|
||||
@include position-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.lg {
|
||||
.ui-radio-input {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&.img {
|
||||
position: relative;
|
||||
margin: 0 0.28572em 0;
|
||||
|
||||
.ui-radio-input {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 0px;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
|
||||
&::before {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
&.cur {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
border-radius: 7px !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-radio-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.card {
|
||||
display: flex;
|
||||
margin: 30rpx;
|
||||
padding: 30rpx;
|
||||
position: relative;
|
||||
border-radius: $radius !important;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
|
||||
.ui-radio-bg {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
transform: scale(0.5);
|
||||
border-radius: #{$radius * 2} !important;
|
||||
z-index: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: 0 0;
|
||||
background-color: var(--ui-BG);
|
||||
}
|
||||
|
||||
.ui-radio-input {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ui-radio-bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(200% - 16px);
|
||||
height: calc(200% - 16px);
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
background-color: var(--ui-BG) !important;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
border-radius: #{$radius * 2 + 8} !important;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ui-radio-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
247
sheep/ui/su-region-picker/su-region-picker.vue
Normal file
247
sheep/ui/su-region-picker/su-region-picker.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<!-- 省市区选择弹窗 -->
|
||||
<template>
|
||||
<su-popup :show="show" @close="onCancel" round="20">
|
||||
<view class="ui-region-picker">
|
||||
<su-toolbar
|
||||
:cancelColor="cancelColor"
|
||||
:confirmColor="confirmColor"
|
||||
:cancelText="cancelText"
|
||||
:confirmText="confirmText"
|
||||
title="选择区域"
|
||||
@cancel="onCancel"
|
||||
@confirm="onConfirm('confirm')"
|
||||
/>
|
||||
<view class="ui-picker-body">
|
||||
<picker-view
|
||||
:value="state.currentIndex"
|
||||
@change="change"
|
||||
class="ui-picker-view"
|
||||
@pickstart="pickstart"
|
||||
@pickend="pickend"
|
||||
>
|
||||
<picker-view-column>
|
||||
<view class="ui-column-item" v-for="province in provinceList" :key="province.id">
|
||||
<view :style="getSizeByNameLength(province.name)">{{ province.name }}</view>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view class="ui-column-item" v-for="city in cityList" :key="city.id">
|
||||
<view :style="getSizeByNameLength(city.name)">{{ city.name }}</view>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<!-- <picker-view-column>
|
||||
<view class="ui-column-item" v-for="district in districtList" :key="district.id">
|
||||
<view :style="getSizeByNameLength(district.name)">{{ district.name }}</view>
|
||||
</view>
|
||||
</picker-view-column> -->
|
||||
</picker-view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* picker picker弹出选择器
|
||||
* @property {Object} params 需要显示的参
|
||||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
|
||||
* @property {Boolean} show-time-tag 时间模式时,是否显示后面的年月日中文提示
|
||||
* @property {String} cancel-color 取消按钮的颜色
|
||||
* @property {String} confirm-color 确认按钮的颜色
|
||||
* @property {String} confirm-text 确认按钮的文字
|
||||
* @property {String} cancel-text 取消按钮的文字
|
||||
* @property {String} default-region 默认选中的地区,
|
||||
* @property {String} default-code 默认选中的地区
|
||||
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
|
||||
* @property {String Number} z-index 弹出时的z-index值(默认1075)
|
||||
* @property {Array} default-selector 数组形式,其中每一项表示选择了range对应项中的第几个
|
||||
* @property {String} range-key 当range参数的元素为对象时,指定Object中的哪个key的值作为选择器显示内容
|
||||
* @event {Function} confirm 点击确定按钮,返回当前选择的值
|
||||
* @event {Function} cancel 点击取消按钮,返回当前选择的值
|
||||
*/
|
||||
import { computed, reactive } from 'vue';
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// "取消"按钮的颜色
|
||||
cancelColor: {
|
||||
type: String,
|
||||
default: '#6666',
|
||||
},
|
||||
// "确定"按钮的颜色
|
||||
confirmColor: {
|
||||
type: String,
|
||||
default: 'var(--ui-BG-Main)',
|
||||
},
|
||||
// 取消按钮的文字
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
// 确认按钮的文字
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确认',
|
||||
},
|
||||
});
|
||||
const areaData = uni.getStorageSync('areaData');
|
||||
|
||||
const getSizeByNameLength = (name) => {
|
||||
let length = name.length;
|
||||
if (length <= 7) return '';
|
||||
if (length < 9) {
|
||||
return 'font-size:28rpx';
|
||||
} else {
|
||||
return 'font-size: 24rpx';
|
||||
}
|
||||
};
|
||||
const state = reactive({
|
||||
currentIndex: [0, 0, 0],
|
||||
moving: false, // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
|
||||
});
|
||||
const emits = defineEmits(['confirm', 'cancel', 'change']);
|
||||
|
||||
const provinceList = areaData;
|
||||
|
||||
const cityList = computed(() => {
|
||||
return areaData[state.currentIndex[0]].children;
|
||||
});
|
||||
const districtList = computed(() => {
|
||||
return cityList.value[state.currentIndex[1]]?.children;
|
||||
});
|
||||
// 标识滑动开始,只有微信小程序才有这样的事件
|
||||
const pickstart = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
state.moving = true;
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 标识滑动结束
|
||||
const pickend = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
state.moving = false;
|
||||
// #endif
|
||||
};
|
||||
const init = () => {};
|
||||
|
||||
const onCancel = () => {
|
||||
emits('cancel');
|
||||
};
|
||||
|
||||
// 用户更改picker的列选项
|
||||
const change = (e) => {
|
||||
if (
|
||||
state.currentIndex[0] === e.detail.value[0] &&
|
||||
state.currentIndex[1] === e.detail.value[1]
|
||||
) {
|
||||
// 不更改省市区列表
|
||||
state.currentIndex[2] = e.detail.value[2];
|
||||
return;
|
||||
} else {
|
||||
// 更改省市区列表
|
||||
if (state.currentIndex[0] !== e.detail.value[0]) {
|
||||
e.detail.value[1] = 0;
|
||||
}
|
||||
e.detail.value[2] = 0;
|
||||
state.currentIndex = e.detail.value;
|
||||
}
|
||||
emits('change', state.currentIndex);
|
||||
};
|
||||
|
||||
// 用户点击确定按钮
|
||||
const onConfirm = (event = null) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (state.moving) return;
|
||||
// #endif
|
||||
let index = state.currentIndex;
|
||||
let province = provinceList[index[0]];
|
||||
let city = cityList.value[index[1]];
|
||||
let district = districtList.value[index[2]];
|
||||
let result = {
|
||||
province_name: province.name,
|
||||
province_id: province.id,
|
||||
city_name: city.name,
|
||||
city_id: city.id,
|
||||
district_name: district.name,
|
||||
district_id: district.id,
|
||||
};
|
||||
|
||||
if (event) emits(event, result);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ui-region-picker {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.ui-picker-view {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ui-picker-header {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
padding: 0 40rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 30rpx;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-picker-header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-bottom: 1rpx solid #eaeef1;
|
||||
-webkit-transform: scaleY(0.5);
|
||||
transform: scaleY(0.5);
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ui-picker__title {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ui-picker-body {
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ui-column-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.ui-btn-picker {
|
||||
padding: 16rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ui-opacity {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ui-btn-picker--primary {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.ui-btn-picker--tips {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
16
sheep/ui/su-status-bar/su-status-bar.vue
Normal file
16
sheep/ui/su-status-bar/su-status-bar.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- 自定义状态栏 -->
|
||||
<template>
|
||||
<view :style="{ height: statusBarHeight }" class="uni-status-bar"><slot /></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
|
||||
const statusBarHeight = sheep.$platform.device.statusBarHeight + 'px';
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.uni-status-bar {
|
||||
// width: 750rpx;
|
||||
height: var(--status-bar-height);
|
||||
}
|
||||
</style>
|
264
sheep/ui/su-sticky/su-sticky.vue
Normal file
264
sheep/ui/su-sticky/su-sticky.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<view class="u-sticky" :id="elId" :style="[style]">
|
||||
<view :style="[stickyContent]" class="u-sticky__content"><slot /></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepMerge, addStyle, addUnit, sleep, guid, getPx, os, sys } from '@/sheep/helper';
|
||||
import sheep from '@/sheep';
|
||||
/**
|
||||
* sticky 吸顶
|
||||
* @description 该组件与CSS中position: sticky属性实现的效果一致,当组件达到预设的到顶部距离时, 就会固定在指定位置,组件位置大于预设的顶部距离时,会重新按照正常的布局排列。
|
||||
* @property {String | Number} offsetTop 吸顶时与顶部的距离,单位px(默认 0 )
|
||||
* @property {String | Number} customNavHeight 自定义导航栏的高度 (h5 默认44 其他默认 0 )
|
||||
* @property {Boolean} stickyToTop 是否开启吸顶功能 (默认 false )
|
||||
* @property {String} bgColor 组件背景颜色(默认 '#ffffff' )
|
||||
* @property {String | Number} zIndex 吸顶时的z-index值
|
||||
* @property {String | Number} index 自定义标识,用于区分是哪一个组件
|
||||
* @property {Object} customStyle 组件的样式,对象形式
|
||||
* @event {Function} fixed 组件吸顶时触发
|
||||
* @event {Function} unfixed 组件取消吸顶时触发
|
||||
* @example <u-sticky offsetTop="200"><view>塞下秋来风景异,衡阳雁去无留意</view></u-sticky>
|
||||
*/
|
||||
export default {
|
||||
name: 'su-sticky',
|
||||
props: {
|
||||
// 吸顶容器到顶部某个距离的时候,进行吸顶,在H5平台,NavigationBar为44px
|
||||
offsetTop: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
// 自定义导航栏的高度
|
||||
customNavHeight: {
|
||||
type: [String, Number],
|
||||
// #ifdef H5
|
||||
// H5端的导航栏属于“自定义”导航栏的范畴,因为它是非原生的,与普通元素一致
|
||||
default: 44,
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
default: sheep.$platform.navbar,
|
||||
// #endif
|
||||
},
|
||||
// 是否开启吸顶功能
|
||||
stickyToTop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 吸顶区域的背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent',
|
||||
},
|
||||
// z-index值
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
// 列表中的索引值
|
||||
index: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
customStyle: {
|
||||
type: [Object, String],
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cssSticky: false, // 是否使用css的sticky实现
|
||||
stickyTop: 0, // 吸顶的top值,因为可能受自定义导航栏影响,最终的吸顶值非offsetTop值
|
||||
elId: guid(),
|
||||
left: 0, // js模式时,吸顶的内容因为处于postition: fixed模式,为了和原来保持一致的样式,需要记录并重新设置它的left,height,width属性
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
fixed: false, // js模式时,是否处于吸顶模式
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
const style = {};
|
||||
if (!this.stickyToTop) {
|
||||
if (this.cssSticky) {
|
||||
style.position = 'sticky';
|
||||
style.zIndex = this.uZindex;
|
||||
style.top = addUnit(this.stickyTop);
|
||||
} else {
|
||||
style.height = this.fixed ? this.height + 'px' : 'auto';
|
||||
}
|
||||
} else {
|
||||
// 无需吸顶时,设置会默认的relative(nvue)和非nvue的static静态模式即可
|
||||
// #ifdef APP-NVUE
|
||||
style.position = 'relative';
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
style.position = 'static';
|
||||
// #endif
|
||||
}
|
||||
style.backgroundColor = this.bgColor;
|
||||
return deepMerge(addStyle(this.customStyle), style);
|
||||
},
|
||||
// 吸顶内容的样式
|
||||
stickyContent() {
|
||||
const style = {};
|
||||
if (!this.cssSticky) {
|
||||
style.position = this.fixed ? 'fixed' : 'static';
|
||||
style.top = this.stickyTop + 'px';
|
||||
style.left = this.left + 'px';
|
||||
style.width = this.width == 'auto' ? 'auto' : this.width + 'px';
|
||||
style.zIndex = this.uZindex;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
uZindex() {
|
||||
return this.zIndex ? this.zIndex : 970;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.getStickyTop();
|
||||
// 判断使用的模式
|
||||
this.checkSupportCssSticky();
|
||||
// 如果不支持css sticky,则使用js方案,此方案性能比不上css方案
|
||||
if (!this.cssSticky) {
|
||||
!this.stickyToTop && this.initObserveContent();
|
||||
}
|
||||
},
|
||||
$uGetRect(selector, all) {
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
[all ? 'selectAll' : 'select'](selector)
|
||||
.boundingClientRect((rect) => {
|
||||
if (all && Array.isArray(rect) && rect.length) {
|
||||
resolve(rect);
|
||||
}
|
||||
if (!all && rect) {
|
||||
resolve(rect);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
},
|
||||
initObserveContent() {
|
||||
// 获取吸顶内容的高度,用于在js吸顶模式时,给父元素一个填充高度,防止"塌陷"
|
||||
this.$uGetRect('#' + this.elId).then((res) => {
|
||||
this.height = res.height;
|
||||
this.left = res.left;
|
||||
this.width = res.width;
|
||||
this.$nextTick(() => {
|
||||
this.observeContent();
|
||||
});
|
||||
});
|
||||
},
|
||||
observeContent() {
|
||||
// 先断掉之前的观察
|
||||
this.disconnectObserver('contentObserver');
|
||||
const contentObserver = uni.createIntersectionObserver({
|
||||
// 检测的区间范围
|
||||
thresholds: [0.95, 0.98, 1],
|
||||
});
|
||||
// 到屏幕顶部的高度时触发
|
||||
contentObserver.relativeToViewport({
|
||||
top: -this.stickyTop,
|
||||
});
|
||||
// 绑定观察的元素
|
||||
contentObserver.observe(`#${this.elId}`, (res) => {
|
||||
this.setFixed(res.boundingClientRect.top);
|
||||
});
|
||||
this.contentObserver = contentObserver;
|
||||
},
|
||||
setFixed(top) {
|
||||
// 判断是否出于吸顶条件范围
|
||||
const fixed = top <= this.stickyTop;
|
||||
this.fixed = fixed;
|
||||
},
|
||||
disconnectObserver(observerName) {
|
||||
// 断掉观察,释放资源
|
||||
const observer = this[observerName];
|
||||
observer && observer.disconnect();
|
||||
},
|
||||
getStickyTop() {
|
||||
this.stickyTop = getPx(this.offsetTop) + getPx(this.customNavHeight);
|
||||
},
|
||||
async checkSupportCssSticky() {
|
||||
// #ifdef H5
|
||||
// H5,一般都是现代浏览器,是支持css sticky的,这里使用创建元素嗅探的形式判断
|
||||
if (this.checkCssStickyForH5()) {
|
||||
this.cssSticky = true;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 如果安卓版本高于8.0,依然认为是支持css sticky的(因为安卓7在某些机型,可能不支持sticky)
|
||||
if (os() === 'android' && Number(sys().system) > 8) {
|
||||
this.cssSticky = true;
|
||||
}
|
||||
|
||||
// APP-Vue和微信平台,通过computedStyle判断是否支持css sticky
|
||||
// #ifdef APP-VUE || MP-WEIXIN
|
||||
this.cssSticky = await this.checkComputedStyle();
|
||||
// #endif
|
||||
|
||||
// ios上,从ios6开始,都是支持css sticky的
|
||||
if (os() === 'ios') {
|
||||
this.cssSticky = true;
|
||||
}
|
||||
|
||||
// nvue,是支持css sticky的
|
||||
// #ifdef APP-NVUE
|
||||
this.cssSticky = true;
|
||||
// #endif
|
||||
},
|
||||
// 在APP和微信小程序上,通过uni.createSelectorQuery可以判断是否支持css sticky
|
||||
checkComputedStyle() {
|
||||
// 方法内进行判断,避免在其他平台生成无用代码
|
||||
// #ifdef APP-VUE || MP-WEIXIN
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('.u-sticky')
|
||||
.fields({
|
||||
computedStyle: ['position'],
|
||||
})
|
||||
.exec((e) => {
|
||||
resolve('sticky' === e[0].position);
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// H5通过创建元素的形式嗅探是否支持css sticky
|
||||
// 判断浏览器是否支持sticky属性
|
||||
checkCssStickyForH5() {
|
||||
// 方法内进行判断,避免在其他平台生成无用代码
|
||||
// #ifdef H5
|
||||
const vendorList = ['', '-webkit-', '-ms-', '-moz-', '-o-'],
|
||||
vendorListLength = vendorList.length,
|
||||
stickyElement = document.createElement('div');
|
||||
for (let i = 0; i < vendorListLength; i++) {
|
||||
stickyElement.style.position = vendorList[i] + 'sticky';
|
||||
if (stickyElement.style.position !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disconnectObserver('contentObserver');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-sticky {
|
||||
/* #ifdef APP-VUE || MP-WEIXIN */
|
||||
// 此处默认写sticky属性,是为了给微信和APP通过uni.createSelectorQuery查询是否支持css sticky使用
|
||||
position: sticky;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
62
sheep/ui/su-subline/su-subline.vue
Normal file
62
sheep/ui/su-subline/su-subline.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<view class="wrap" :style="{height: `${height}px`}">
|
||||
<view class="divider" :style="[elStyle]"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 分割线
|
||||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 线条颜色
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: '#000',
|
||||
},
|
||||
// 线条样式:'dotted', 'solid', 'double', 'dashed'
|
||||
borderType: {
|
||||
type: String,
|
||||
default: 'dashed',
|
||||
},
|
||||
// 线条宽度
|
||||
lineWidth: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
// 高度
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 'auto'
|
||||
},
|
||||
// 左右边距:none - 无边距,horizontal - 左右留边
|
||||
paddingType: {
|
||||
type: String,
|
||||
default: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
const elStyle = computed(() => {
|
||||
return {
|
||||
'border-top-width': `${props.lineWidth}px`,
|
||||
'border-top-color': props.lineColor,
|
||||
'border-top-style': props.borderType,
|
||||
margin: props.paddingType === 'none' ? '0' : '0px 16px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
502
sheep/ui/su-swiper/su-swiper.vue
Normal file
502
sheep/ui/su-swiper/su-swiper.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="ui-swiper" :class="[props.mode, props.bg, props.ui]">
|
||||
<swiper
|
||||
:circular="props.circular"
|
||||
:current="state.cur"
|
||||
:autoplay="props.autoplay && !state.videoPlaySataus"
|
||||
:interval="props.interval"
|
||||
:duration="props.duration"
|
||||
@transition="transition"
|
||||
@animationfinish="animationfinish"
|
||||
:style="customStyle"
|
||||
@change="swiperChange"
|
||||
>
|
||||
<swiper-item
|
||||
class="swiper-item"
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:class="{ cur: state.cur == index }"
|
||||
@tap="onSwiperItem(item)"
|
||||
>
|
||||
<view class="ui-swiper-main">
|
||||
<image
|
||||
v-if="item.type === 'image'"
|
||||
class="swiper-image"
|
||||
:mode="props.imageMode"
|
||||
:src="item.src"
|
||||
width="100%"
|
||||
height="100%"
|
||||
@load="onImgLoad"
|
||||
></image>
|
||||
<su-video
|
||||
v-else
|
||||
:ref="(el) => (refs.videoRef[`video_${index}`] = el)"
|
||||
:poster="sheep.$url.cdn(item.poster)"
|
||||
:src="sheep.$url.cdn(item.src)"
|
||||
:index="index"
|
||||
:moveX="state.moveX"
|
||||
:initialTime="item.currentTime || 0"
|
||||
:height="seizeHeight"
|
||||
@videoTimeupdate="videoTimeupdate"
|
||||
></su-video>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<template v-if="!state.videoPlaySataus">
|
||||
<view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle != 'tag'">
|
||||
<view
|
||||
class="line-box"
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:class="[state.cur == index ? 'cur' : '', props.dotCur]"
|
||||
></view>
|
||||
</view>
|
||||
<view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle == 'tag'">
|
||||
<view
|
||||
class="ui-tag radius-lg"
|
||||
:class="[props.dotCur]"
|
||||
style="pointer-events: none; padding: 0 10rpx"
|
||||
>
|
||||
<view style="transform: scale(0.7)">{{ state.cur + 1 }} / {{ props.list.length }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 轮播组件
|
||||
*
|
||||
* @property {Boolean} circular = false - 是否采用衔接滑动,即播放到末尾后重新回到开头
|
||||
* @property {Boolean} autoplay = true - 是否自动切换
|
||||
* @property {Number} interval = 5000 - 自动切换时间间隔
|
||||
* @property {Number} duration = 500 - 滑动动画时长,app-nvue不支持
|
||||
* @property {Array} list = [] - 轮播数据
|
||||
* @property {String} ui = '' - 样式class
|
||||
* @property {String} mode - 模式
|
||||
* @property {String} dotStyle - 指示点样式
|
||||
* @property {String} dotCur= 'ui-BG-Main' - 当前指示点样式,默认主题色
|
||||
* @property {String} bg - 背景
|
||||
* @property {String} height = 300 - 组件高度
|
||||
* @property {String} imgHeight = 300 - 图片高度
|
||||
*
|
||||
* @example list = [{url:'跳转路径',urlType:'跳转方式',type:'轮播类型',src:'轮播内容地址',poster:'视频必传'}]
|
||||
*/
|
||||
|
||||
import { reactive, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { clone } from 'lodash-es';
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
imgHeight: 0,
|
||||
cur: 0,
|
||||
moveX: 0,
|
||||
videoPlaySataus: false,
|
||||
heightList: [],
|
||||
});
|
||||
|
||||
const refs = reactive({
|
||||
videoRef: {},
|
||||
});
|
||||
|
||||
// 接收参数
|
||||
|
||||
const props = defineProps({
|
||||
circular: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
interval: {
|
||||
type: Number,
|
||||
default: 3000,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
imageMode: {
|
||||
type: String,
|
||||
default: 'scaleToFill',
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
dotStyle: {
|
||||
type: String,
|
||||
default: 'long', //default long tag
|
||||
},
|
||||
dotCur: {
|
||||
type: String,
|
||||
default: 'ss-bg-opactity-block',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'bg-none',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
imgHeight: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
imgTopRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
imgBottomRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
seizeHeight: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
});
|
||||
|
||||
// current 改变时会触发 change 事件
|
||||
const swiperChange = (e) => {
|
||||
if (e.detail.source !== 'touch' && e.detail.source !== 'autoplay') return;
|
||||
state.cur = e.detail.current;
|
||||
state.videoPlaySataus = false;
|
||||
if (props.list[state.cur].type === 'video') {
|
||||
refs.videoRef[`video_${state.cur}`].pausePlay();
|
||||
}
|
||||
};
|
||||
// 点击轮播组件
|
||||
const onSwiperItem = (item) => {
|
||||
if (item.type === 'video') {
|
||||
state.videoPlaySataus = true;
|
||||
} else {
|
||||
sheep.$router.go(item.url);
|
||||
onPreview();
|
||||
}
|
||||
};
|
||||
|
||||
const onPreview = () => {
|
||||
if (!props.isPreview) return;
|
||||
let previewImage = clone(props.list);
|
||||
previewImage.forEach((item, index) => {
|
||||
if (item.type === 'video') {
|
||||
previewImage.splice(index, 1);
|
||||
}
|
||||
});
|
||||
uni.previewImage({
|
||||
urls:
|
||||
previewImage.length < 1
|
||||
? [props.src]
|
||||
: previewImage.reduce((pre, cur) => {
|
||||
pre.push(cur.src);
|
||||
return pre;
|
||||
}, []),
|
||||
current: state.cur,
|
||||
// longPressActions: {
|
||||
// itemList: ['发送给朋友', '保存图片', '收藏'],
|
||||
// success: function (data) {
|
||||
// console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
|
||||
// },
|
||||
// fail: function (err) {
|
||||
// console.log(err.errMsg);
|
||||
// },
|
||||
// },
|
||||
});
|
||||
};
|
||||
//
|
||||
|
||||
// swiper-item 的位置发生改变时会触发 transition
|
||||
const transition = (e) => {
|
||||
// #ifdef APP-PLUS
|
||||
state.moveX = e.detail.dx;
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 动画结束时会触发 animationfinish
|
||||
const animationfinish = (e) => {
|
||||
state.moveX = 0;
|
||||
};
|
||||
|
||||
const videoTimeupdate = (e) => {
|
||||
props.list[state.cur].currentTime = e.detail.currentTime;
|
||||
};
|
||||
|
||||
// 自动计算高度
|
||||
const customStyle = computed(() => {
|
||||
let height;
|
||||
|
||||
// 固定高度情况
|
||||
if (props.height !== 0) {
|
||||
height = props.height;
|
||||
}
|
||||
|
||||
// 自动高度情况
|
||||
if (props.height === 0) {
|
||||
// 图片预加载占位高度
|
||||
if (state.imgHeight !== 0) {
|
||||
height = state.imgHeight;
|
||||
} else if (props.seizeHeight !== 0) {
|
||||
height = props.seizeHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
height: height + 'rpx',
|
||||
};
|
||||
});
|
||||
|
||||
// 计算轮播图片最大高度
|
||||
function onImgLoad(e) {
|
||||
if (props.height === 0) {
|
||||
let newHeight = (e.detail.height / e.detail.width) * 750;
|
||||
if (state.imgHeight < newHeight) {
|
||||
state.imgHeight = newHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ui-swiper {
|
||||
position: relative;
|
||||
|
||||
.ui-swiper-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui-swiper-main .swiper-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui-swiper-dot {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 20rpx;
|
||||
height: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.default .line-box {
|
||||
display: inline-flex;
|
||||
border-radius: 50rpx;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 2px solid transparent;
|
||||
margin: 0 10rpx;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.cur {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
opacity: 1;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
||||
&.cur::after {
|
||||
content: '';
|
||||
border-radius: 50rpx;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.long .line-box {
|
||||
display: inline-block;
|
||||
border-radius: 100rpx;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 0 10rpx;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
|
||||
&.cur {
|
||||
width: 24rpx;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.cur::after {
|
||||
}
|
||||
}
|
||||
|
||||
&.line {
|
||||
bottom: 20rpx;
|
||||
|
||||
.line-box {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
|
||||
&.cur {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tag {
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.card {
|
||||
.swiper-item {
|
||||
width: 610rpx !important;
|
||||
left: 70rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 0rpx 60rpx;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.swiper-item .ui-swiper-main {
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: 100%;
|
||||
transform: scale(0.9);
|
||||
transition: all 0.2s ease-in 0s;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
|
||||
.swiper-image {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-item .ui-swiper-main::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background: inherit;
|
||||
filter: blur(5px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 10rpx;
|
||||
left: 10rpx;
|
||||
z-index: -1;
|
||||
opacity: 0.3;
|
||||
transform-origin: 0 0;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
.swiper-item.cur .ui-swiper-main {
|
||||
transform: scale(1);
|
||||
transition: all 0.2s ease-in 0s;
|
||||
}
|
||||
|
||||
.ui-swiper-dot.tag {
|
||||
position: absolute;
|
||||
bottom: 85rpx;
|
||||
right: 75rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.hotelCard {
|
||||
.swiper-item {
|
||||
width: 650rpx !important;
|
||||
left: 30rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 0rpx 0rpx 50rpx;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.swiper-item .ui-swiper-main {
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: 100%;
|
||||
transform: scale(0.9);
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s ease-in 0s;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
|
||||
.swiper-image {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-item .ui-swiper-main::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background: inherit;
|
||||
filter: blur(5px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 10rpx;
|
||||
left: 10rpx;
|
||||
z-index: -1;
|
||||
opacity: 0.3;
|
||||
transform-origin: 0 0;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
.swiper-item.cur .ui-swiper-main {
|
||||
transform: scale(1);
|
||||
transition: all 0.2s ease-in 0s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ui-swiper-dot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.hotelDetail {
|
||||
.swiper-item {
|
||||
width: 690rpx !important;
|
||||
left: 30rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 0rpx;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.swiper-item .ui-swiper-main {
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: 100%;
|
||||
transform: scale(0.96);
|
||||
transition: all 0.2s ease-in 0s;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
|
||||
.swiper-image {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-item.cur .ui-swiper-main {
|
||||
transform: scale(0.96);
|
||||
transition: all 0.2s ease-in 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
100
sheep/ui/su-switch/su-switch.vue
Normal file
100
sheep/ui/su-switch/su-switch.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<view class="ui-switch" :class="[{ disabled: props.disabled }, props.ui]">
|
||||
<view class="ui-switch-wrapper" @tap="change">
|
||||
<view
|
||||
class="ui-switch-input"
|
||||
:class="[
|
||||
{ 'ui-switch-input-checked': props.modelValue },
|
||||
props.modelValue ? props.bg : '',
|
||||
props.text,
|
||||
props.size,
|
||||
]"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UiSwitch',
|
||||
};
|
||||
</script>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Boolean, Number],
|
||||
default: false,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'ui-BG-Main',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
||||
const change = () => {
|
||||
emits('update:modelValue', !props.modelValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ui-switch {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
.ui-switch-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ui-switch-input {
|
||||
appearance: none;
|
||||
position: relative;
|
||||
width: 47px;
|
||||
height: 26px;
|
||||
outline: 0;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(119, 119, 119, 0.3);
|
||||
transition: background-color 0.1s, border 0.1s;
|
||||
&:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 200px;
|
||||
transition: transform 0.3s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 3px;
|
||||
background-color: #fff;
|
||||
}
|
||||
&.ui-switch-input-checked {
|
||||
&:after {
|
||||
transform: translateX(21px);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
.ui-switch-input {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
169
sheep/ui/su-tab-item/su-tab-item.vue
Normal file
169
sheep/ui/su-tab-item/su-tab-item.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view>
|
||||
<view :id="'tab-' + props.index" class="ui-tab-item" :class="[{ cur: cur }, tpl]">
|
||||
<view class="ui-tab-icon" :class="props.data.icon" v-if="props.data.icon"></view>
|
||||
<view
|
||||
class="ui-tab-text"
|
||||
:class="[cur ? 'curColor cur' : 'default-color']"
|
||||
:style="[{ color: cur ? titleStyle.activeColor : titleStyle.color }]"
|
||||
>
|
||||
{{ props.data.title }}
|
||||
</view>
|
||||
|
||||
<view class="ui-tag badge ml-2" v-if="props.data.tag != null">{{ props.data.tag }}</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="tpl === 'subtitle'"
|
||||
class="item-subtitle ss-flex ss-col-bottom ss-row-center"
|
||||
:style="[{ color: cur ? subtitleStyle.activeColor : subtitleStyle.color }]"
|
||||
>
|
||||
{{ props.data.subtitle }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UiTabItem',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 基础组件 - uiTabItem
|
||||
*/
|
||||
import { computed, onMounted, getCurrentInstance, inject } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
const vm = getCurrentInstance();
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Object, String, Number],
|
||||
default() {},
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['up']);
|
||||
|
||||
onMounted(() => {
|
||||
computedQuery();
|
||||
uni.onWindowResize((res) => {
|
||||
computedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
function getParent(name) {
|
||||
let parent = vm?.parent;
|
||||
// 无父级返回null
|
||||
if (parent) {
|
||||
let parentName = parent?.type?.name;
|
||||
// 父组件name 为真返回父级,为假循环
|
||||
while (parentName !== name) {
|
||||
parent = parent?.parent;
|
||||
// 存在父级循环,不存在打断循环
|
||||
if (parent) {
|
||||
parentName = parent?.type?.name;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const UiTab = getParent('SuTab');
|
||||
|
||||
// 获取抛出的数据和方法
|
||||
let uiTabProvide;
|
||||
if (UiTab) {
|
||||
uiTabProvide = inject('suTabProvide');
|
||||
}
|
||||
const cur = computed(() => uiTabProvide?.curValue.value === props.index);
|
||||
const tpl = computed(() => uiTabProvide?.props?.tpl);
|
||||
const subtitleStyle = computed(() => uiTabProvide?.props?.subtitleStyle);
|
||||
const titleStyle = computed(() => uiTabProvide?.props?.titleStyle);
|
||||
|
||||
const computedQuery = () => {
|
||||
uni.createSelectorQuery()
|
||||
.in(vm)
|
||||
.select('#tab-' + props.index)
|
||||
.boundingClientRect((data) => {
|
||||
if (data != null) {
|
||||
// 传递到父组件进行计算
|
||||
emits('up', props.index, data);
|
||||
} else {
|
||||
console.log('tab-item data error');
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.default-color {
|
||||
color: $black;
|
||||
}
|
||||
.ui-tab-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 1em;
|
||||
min-height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s;
|
||||
min-width: 60px;
|
||||
|
||||
.ui-tab-text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-tab-icon {
|
||||
margin: 0 0.25em;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
&.cur {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.btn {
|
||||
.ui-tab-text {
|
||||
transform: scale(0.9);
|
||||
transition: color 0.3s;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.subtitle {
|
||||
.ui-tab-text {
|
||||
transform: scale(0.9);
|
||||
transition: color 0.3s;
|
||||
font-weight: bold;
|
||||
height: calc(100% - 2.6em);
|
||||
line-height: calc(100% - 2.6em);
|
||||
margin-top: 1.2em;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
height: 2em;
|
||||
font-size: 22rpx;
|
||||
color: $dark-9;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.cur-subtitle {
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
</style>
|
474
sheep/ui/su-tab/su-tab.vue
Normal file
474
sheep/ui/su-tab/su-tab.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<template>
|
||||
<view
|
||||
class="ui-tab"
|
||||
ref="tabRef"
|
||||
:id="'tab-' + vm.uid"
|
||||
:class="[
|
||||
props.ui,
|
||||
props.tpl,
|
||||
props.bg,
|
||||
props.align,
|
||||
{ 'ui-tab-inline': props.inline },
|
||||
{ 'ui-tab-scrolls': props.scroll },
|
||||
]"
|
||||
>
|
||||
<block v-if="scroll">
|
||||
<view class="ui-tab-scroll-warp">
|
||||
<scroll-view
|
||||
scroll-x="true"
|
||||
class="ui-tab-scroll"
|
||||
:scroll-left="state.curValue > 1 ? state.tabNodeList[state.curValue - 1].left : 0"
|
||||
scroll-with-animation
|
||||
:style="{ width: `${state.content.width}px` }"
|
||||
>
|
||||
<view class="ss-flex ss-col-center">
|
||||
<su-tab-item
|
||||
v-for="(item, index) in props.tab"
|
||||
:data="item"
|
||||
:index="index"
|
||||
:key="index"
|
||||
@up="upitem"
|
||||
@tap.native="click(index, item)"
|
||||
></su-tab-item>
|
||||
<view
|
||||
class="ui-tab-mark-warp"
|
||||
:class="[{ over: state.over }]"
|
||||
:style="[{ left: state.markLeft + 'px' }, { width: state.markWidth + 'px' }]"
|
||||
>
|
||||
<view
|
||||
class="ui-tab-mark"
|
||||
:class="[props.mark, { 'ui-btn': props.tpl == 'btn' || props.tpl == 'subtitle' }]"
|
||||
:style="[
|
||||
{
|
||||
background:
|
||||
props.tpl == 'btn' || props.tpl == 'subtitle' ? titleStyle.activeBg : 'none',
|
||||
},
|
||||
]"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<su-tab-item
|
||||
v-for="(item, index) in props.tab"
|
||||
:data="item"
|
||||
:index="index"
|
||||
:key="index"
|
||||
@up="upitem"
|
||||
@tap.native="click(index, item)"
|
||||
></su-tab-item>
|
||||
<view
|
||||
class="ui-tab-mark-warp"
|
||||
:class="[{ over: state.over }]"
|
||||
:style="[{ left: state.markLeft + 'px' }, { width: state.markWidth + 'px' }]"
|
||||
>
|
||||
<view
|
||||
class="ui-tab-mark"
|
||||
:class="[props.mark, { 'ui-btn': props.tpl == 'btn' || props.tpl == 'subtitle' }]"
|
||||
></view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SuTab',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 基础组件 - suTab
|
||||
*/
|
||||
|
||||
import {
|
||||
toRef,
|
||||
ref,
|
||||
reactive,
|
||||
unref,
|
||||
onMounted,
|
||||
nextTick,
|
||||
getCurrentInstance,
|
||||
provide,
|
||||
} from 'vue';
|
||||
const vm = getCurrentInstance();
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
curValue: 0,
|
||||
tabNodeList: [],
|
||||
scrollLeft: 0,
|
||||
markLeft: 0,
|
||||
markWidth: 0,
|
||||
content: {
|
||||
width: 100,
|
||||
},
|
||||
over: false,
|
||||
});
|
||||
|
||||
const tabRef = ref(null);
|
||||
// 参数
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tab: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
// line dot long,subtitle,trapezoid
|
||||
tpl: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
},
|
||||
mark: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
curColor: {
|
||||
type: String,
|
||||
default: 'ui-TC',
|
||||
},
|
||||
defaultColor: {
|
||||
type: String,
|
||||
default: 'ui-TC',
|
||||
},
|
||||
scroll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
activeBg: '#DA2B10',
|
||||
activeColor: '#FEFEFE',
|
||||
color: '#D70000',
|
||||
}),
|
||||
},
|
||||
subtitleStyle: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
activeColor: '#333',
|
||||
color: '#C42222',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
onMounted(() => {
|
||||
state.curValue = props.modelValue;
|
||||
setCurValue(props.modelValue);
|
||||
nextTick(() => {
|
||||
computedQuery();
|
||||
});
|
||||
uni.onWindowResize((res) => {
|
||||
computedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
const computedQuery = () => {
|
||||
uni.createSelectorQuery()
|
||||
.in(vm)
|
||||
.select('#tab-' + vm.uid)
|
||||
.boundingClientRect((data) => {
|
||||
if (data != null) {
|
||||
if (data.left == 0 && data.right == 0) {
|
||||
// setTimeout(() => {
|
||||
computedQuery();
|
||||
// }, 300);
|
||||
} else {
|
||||
state.content = data;
|
||||
setTimeout(() => {
|
||||
state.over = true;
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
console.log('tab-' + vm.uid + ' data error');
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
};
|
||||
|
||||
const setCurValue = (value) => {
|
||||
if (value == state.curValue) return;
|
||||
state.curValue = value;
|
||||
computedMark();
|
||||
};
|
||||
|
||||
const click = (index, item) => {
|
||||
setCurValue(index);
|
||||
emits('update:modelValue', index);
|
||||
emits('change', {
|
||||
index: index,
|
||||
data: item,
|
||||
});
|
||||
};
|
||||
|
||||
const upitem = (index, e) => {
|
||||
state.tabNodeList[index] = e;
|
||||
if (index == state.curValue) {
|
||||
computedMark();
|
||||
}
|
||||
};
|
||||
|
||||
const computedMark = () => {
|
||||
if (state.tabNodeList.length == 0) return;
|
||||
let left = 0;
|
||||
let list = unref(state.tabNodeList);
|
||||
let cur = state.curValue;
|
||||
state.markLeft = list[cur].left - state.content.left;
|
||||
state.markWidth = list[cur].width;
|
||||
};
|
||||
|
||||
const computedScroll = () => {
|
||||
if (state.curValue == 0 || state.curValue == state.tabNodeList.length - 1) {
|
||||
return false;
|
||||
}
|
||||
let i = 0;
|
||||
let left = 0;
|
||||
let list = state.tabNodeList;
|
||||
for (i in list) {
|
||||
if (i == state.curValue && i != 0) {
|
||||
left = left - list[i - 1].width;
|
||||
break;
|
||||
}
|
||||
left = left + list[i].width;
|
||||
}
|
||||
state.scrollLeft = left;
|
||||
};
|
||||
|
||||
provide('suTabProvide', {
|
||||
props,
|
||||
curValue: toRef(state, 'curValue'),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 4em;
|
||||
align-items: center;
|
||||
|
||||
&.ui-tab-scrolls {
|
||||
width: 100%;
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-bottom: 10px;
|
||||
/* #endif */
|
||||
.ui-tab-scroll-warp {
|
||||
overflow: hidden;
|
||||
height: inherit;
|
||||
width: 100%;
|
||||
.ui-tab-scroll {
|
||||
position: relative;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
min-height: 4em;
|
||||
line-height: 4em;
|
||||
width: 100% !important;
|
||||
.ui-tab-mark-warp {
|
||||
display: flex;
|
||||
align-items: top;
|
||||
justify-content: center;
|
||||
.ui-tab-mark.ui-btn {
|
||||
/* #ifndef MP-WEIXIN */
|
||||
height: 2em;
|
||||
width: calc(100% - 0.6em);
|
||||
margin-top: 4px;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
height: 2em;
|
||||
width: calc(100% - 0.6em);
|
||||
margin-top: 4px;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tab-mark-warp {
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
&.over {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.ui-tab-mark {
|
||||
color: var(--ui-BG-Main);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.line {
|
||||
.ui-tab-mark {
|
||||
border-bottom: 2px solid currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.topline {
|
||||
.ui-tab-mark {
|
||||
border-top: 2px solid currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.dot {
|
||||
.ui-tab-mark::after {
|
||||
content: '';
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
background-color: currentColor;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0.3em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.long {
|
||||
.ui-tab-mark::after {
|
||||
content: '';
|
||||
width: 2em;
|
||||
height: 0.35em;
|
||||
background-color: currentColor;
|
||||
border-radius: 5em;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0.3em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.trapezoid {
|
||||
.ui-tab-mark::after {
|
||||
content: '';
|
||||
width: calc(100% - 2em);
|
||||
height: 0.35em;
|
||||
background-color: currentColor;
|
||||
border-radius: 5em 5em 0 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn {
|
||||
.ui-tab-mark-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.ui-tab-mark.ui-btn {
|
||||
height: calc(100% - 1.6em);
|
||||
width: calc(100% - 0.6em);
|
||||
}
|
||||
}
|
||||
|
||||
&.sm .ui-tab-mark.ui-btn {
|
||||
height: calc(100% - 2px);
|
||||
width: calc(100% - 2px);
|
||||
border-radius: #{$radius - 2};
|
||||
}
|
||||
}
|
||||
|
||||
&.subtitle {
|
||||
.ui-tab-mark-warp {
|
||||
display: flex;
|
||||
align-items: top;
|
||||
justify-content: center;
|
||||
padding-top: 0.6em;
|
||||
|
||||
.ui-tab-mark.ui-btn {
|
||||
height: calc(100% - 2.8em);
|
||||
width: calc(100% - 0.6em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-tab-inline {
|
||||
display: inline-flex;
|
||||
height: 3.5em;
|
||||
|
||||
&.ui-tab-scrolls {
|
||||
.ui-tab-scroll {
|
||||
height: calc(3.5em + 17px);
|
||||
line-height: 3.5em;
|
||||
|
||||
.ui-tab-mark-warp {
|
||||
height: 3.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn {
|
||||
.ui-tab-mark-warp {
|
||||
.ui-tab-mark.ui-btn {
|
||||
height: calc(100% - 10px);
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sm {
|
||||
height: 70rpx !important;
|
||||
|
||||
&.ui-tab-inline {
|
||||
height: 70rpx;
|
||||
|
||||
&.ui-tab-scrolls {
|
||||
.ui-tab-scroll {
|
||||
height: calc(70rpx + 17px);
|
||||
line-height: 70rpx;
|
||||
|
||||
.ui-tab-mark-warp {
|
||||
height: 70rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn .ui-tab-mark.ui-btn {
|
||||
height: calc(100% - 2px);
|
||||
width: calc(100% - 2px);
|
||||
border-radius: #{$radius - 2};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
234
sheep/ui/su-tabbar-item/su-tabbar-item.vue
Normal file
234
sheep/ui/su-tabbar-item/su-tabbar-item.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<!-- 自定义底部导航项 -->
|
||||
<template>
|
||||
<view class="u-tabbar-item" :style="[addStyle(customStyle)]">
|
||||
<view v-if="isCenter" class="tabbar-center-item">
|
||||
<image class="center-image" :src="centerImage" mode="aspectFill"></image>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="u-tabbar-item__icon">
|
||||
<image
|
||||
v-if="icon"
|
||||
:name="icon"
|
||||
:color="isActive ? parentData.activeColor : parentData.color"
|
||||
:size="20"
|
||||
></image>
|
||||
<block v-else>
|
||||
<slot v-if="isActive" name="active-icon" />
|
||||
<slot v-else name="inactive-icon" />
|
||||
</block>
|
||||
<!-- <u-badge
|
||||
absolute
|
||||
:offset="[0, dot ? '34rpx' : badge > 9 ? '14rpx' : '20rpx']"
|
||||
:customStyle="badgeStyle"
|
||||
:isDot="dot"
|
||||
:value="badge || (dot ? 1 : null)"
|
||||
:show="dot || badge > 0"
|
||||
></u-badge> -->
|
||||
</view>
|
||||
|
||||
<slot name="text">
|
||||
<text
|
||||
class="u-tabbar-item__text"
|
||||
:style="{
|
||||
color: isActive ? parentData.activeColor : parentData.color,
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</text>
|
||||
</slot>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* TabbarItem 底部导航栏子组件
|
||||
* @description 此组件提供了自定义tabbar的能力。
|
||||
* @property {String | Number} name item标签的名称,作为与u-tabbar的value参数匹配的标识符
|
||||
* @property {String} icon uView内置图标或者绝对路径的图片
|
||||
* @property {String | Number} badge 右上角的角标提示信息
|
||||
* @property {Boolean} dot 是否显示圆点,将会覆盖badge参数(默认 false )
|
||||
* @property {String} text 描述文本
|
||||
* @property {Object | String} badgeStyle 控制徽标的位置,对象或者字符串形式,可以设置top和right属性(默认 'top: 6px;right:2px;' )
|
||||
* @property {Object} customStyle 定义需要用到的外部样式
|
||||
*
|
||||
*/
|
||||
import { deepMerge, addStyle, sleep, $parent } from '@/sheep/helper';
|
||||
export default {
|
||||
name: 'su-tabbar-item',
|
||||
props: {
|
||||
customStyle: {
|
||||
type: [Object, String],
|
||||
default: () => ({}),
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 跳转的页面路径
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 页面跳转的类型
|
||||
linkType: {
|
||||
type: String,
|
||||
default: 'navigateTo',
|
||||
},
|
||||
// item标签的名称,作为与u-tabbar的value参数匹配的标识符
|
||||
name: {
|
||||
type: [String, Number, null],
|
||||
default: '',
|
||||
},
|
||||
// uView内置图标或者绝对路径的图片
|
||||
icon: {
|
||||
icon: String,
|
||||
default: '',
|
||||
},
|
||||
// 右上角的角标提示信息
|
||||
badge: {
|
||||
type: [String, Number, null],
|
||||
default: '',
|
||||
},
|
||||
// 是否显示圆点,将会覆盖badge参数
|
||||
dot: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 描述文本
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 控制徽标的位置,对象或者字符串形式,可以设置top和right属性
|
||||
badgeStyle: {
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isCenter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
centerImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: false, // 是否处于激活状态
|
||||
addStyle,
|
||||
parentData: {
|
||||
value: null,
|
||||
activeColor: '',
|
||||
color: '',
|
||||
},
|
||||
parent: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
getParentData(parentName = '') {
|
||||
// 避免在created中去定义parent变量
|
||||
if (!this.parent) this.parent = {};
|
||||
// 这里的本质原理是,通过获取父组件实例(也即类似u-radio的父组件u-radio-group的this)
|
||||
// 将父组件this中对应的参数,赋值给本组件(u-radio的this)的parentData对象中对应的属性
|
||||
// 之所以需要这么做,是因为所有端中,头条小程序不支持通过this.parent.xxx去监听父组件参数的变化
|
||||
// 此处并不会自动更新子组件的数据,而是依赖父组件u-radio-group去监听data的变化,手动调用更新子组件的方法去重新获取
|
||||
this.parent = $parent.call(this, parentName);
|
||||
if (this.parent.children) {
|
||||
// 如果父组件的children不存在本组件的实例,才将本实例添加到父组件的children中
|
||||
this.parent.children.indexOf(this) === -1 && this.parent.children.push(this);
|
||||
}
|
||||
if (this.parent && this.parentData) {
|
||||
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData
|
||||
Object.keys(this.parentData).map((key) => {
|
||||
this.parentData[key] = this.parent[key];
|
||||
});
|
||||
}
|
||||
},
|
||||
init() {
|
||||
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
|
||||
this.updateParentData();
|
||||
if (!this.parent) {
|
||||
console.log('u-tabbar-item必须搭配u-tabbar组件使用');
|
||||
}
|
||||
// 本子组件在u-tabbar的children数组中的索引
|
||||
const index = this.parent.children.indexOf(this);
|
||||
// 判断本组件的name(如果没有定义name,就用index索引)是否等于父组件的value参数
|
||||
this.isActive = (this.name.split('?')[0] || index) === this.parentData.value;
|
||||
},
|
||||
updateParentData() {
|
||||
// 此方法在mixin中
|
||||
this.getParentData('su-tabbar');
|
||||
},
|
||||
// 此方法将会被父组件u-tabbar调用
|
||||
updateFromParent() {
|
||||
// 重新初始化
|
||||
this.init();
|
||||
},
|
||||
clickHandler() {
|
||||
this.$nextTick(() => {
|
||||
const index = this.parent.children.indexOf(this);
|
||||
const name = this.name || index;
|
||||
// 点击的item为非激活的item才发出change事件
|
||||
if (name !== this.parent.value) {
|
||||
this.parent.$emit('change', name);
|
||||
}
|
||||
this.$emit('click', name);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabbar-center-item {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: rebeccapurple;
|
||||
transform: scale(1.3) translateY(-6px);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
.center-image {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
.u-tabbar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 150rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--textSize);
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP */
|
||||
// 由于小程序都使用shadow DOM形式实现,需要给影子宿主设置flex: 1才能让其撑开
|
||||
:host {
|
||||
flex: 1;
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
227
sheep/ui/su-tabbar/su-tabbar.vue
Normal file
227
sheep/ui/su-tabbar/su-tabbar.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<!-- 底部导航栏 -->
|
||||
<template>
|
||||
<view class="u-tabbar">
|
||||
<view
|
||||
class="u-tabbar__content"
|
||||
ref="u-tabbar__content"
|
||||
@touchmove.stop.prevent=""
|
||||
:class="[border && 'u-border-top', fixed && 'u-tabbar--fixed', { 'mid-tabbar': midTabBar }]"
|
||||
:style="[tabbarStyle]"
|
||||
>
|
||||
<view class="u-tabbar__content__item-wrapper">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="safeAreaInsetBottom" :style="[{ height: safeBottomHeight + 'px' }]"></view>
|
||||
</view>
|
||||
<view
|
||||
class="u-tabbar__placeholder"
|
||||
v-if="placeholder"
|
||||
:style="{
|
||||
height: placeholderHeight + 'px',
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef APP-NVUE
|
||||
const dom = uni.requireNativePlugin('dom');
|
||||
// #endif
|
||||
/**
|
||||
* Tabbar 底部导航栏
|
||||
* @description 此组件提供了自定义tabbar的能力。
|
||||
* @property {String | Number} value 当前匹配项的name
|
||||
* @property {Boolean} safeAreaInsetBottom 是否为iPhoneX留出底部安全距离(默认 true )
|
||||
* @property {Boolean} border 是否显示上方边框(默认 true )
|
||||
* @property {String | Number} zIndex 元素层级z-index(默认 1 )
|
||||
* @property {String} activeColor 选中标签的颜色(默认 '#1989fa' )
|
||||
* @property {String} inactiveColor 未选中标签的颜色(默认 '#7d7e80' )
|
||||
* @property {Boolean} fixed 是否固定在底部(默认 true )
|
||||
* @property {Boolean} placeholder fixed定位固定在底部时,是否生成一个等高元素防止塌陷(默认 true )
|
||||
* @property {Object} customStyle 定义需要用到的外部样式
|
||||
*
|
||||
*/
|
||||
|
||||
import { deepMerge, addStyle, sleep } from '@/sheep/helper';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
export default {
|
||||
name: 'su-tabbar',
|
||||
props: {
|
||||
customStyle: {
|
||||
type: [Object, String],
|
||||
default: () => ({}),
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 跳转的页面路径
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 页面跳转的类型
|
||||
linkType: {
|
||||
type: String,
|
||||
default: 'navigateTo',
|
||||
},
|
||||
// 当前匹配项的name
|
||||
value: {
|
||||
type: [String, Number, null],
|
||||
default: '',
|
||||
},
|
||||
// 是否为iPhoneX留出底部安全距离
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否显示上方边框
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 元素层级z-index
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 10,
|
||||
},
|
||||
// 选中标签的颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#1989fa',
|
||||
},
|
||||
// 未选中标签的颜色
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#7d7e80',
|
||||
},
|
||||
// 是否固定在底部
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// fixed定位固定在底部时,是否生成一个等高元素防止塌陷
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
midTabBar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placeholderHeight: 0,
|
||||
safeBottomHeight: sheep.$platform.device.safeAreaInsets.bottom,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tabbarStyle() {
|
||||
const style = {
|
||||
zIndex: this.zIndex,
|
||||
};
|
||||
// 合并来自父组件的customStyle样式
|
||||
return deepMerge(style, addStyle(this.customStyle));
|
||||
},
|
||||
// 监听多个参数的变化,通过在computed执行对应的操作
|
||||
updateChild() {
|
||||
return [this.value, this.activeColor, this.inactiveColor];
|
||||
},
|
||||
updatePlaceholder() {
|
||||
return [this.fixed, this.placeholder];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
updateChild() {
|
||||
// 如果updateChildren中的元素发生了变化,则执行子元素初始化操作
|
||||
this.updateChildren();
|
||||
},
|
||||
updatePlaceholder() {
|
||||
// 如果fixed,placeholder等参数发生变化,重新计算占位元素的高度
|
||||
this.setPlaceholderHeight();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.children = [];
|
||||
},
|
||||
mounted() {
|
||||
this.setPlaceholderHeight();
|
||||
},
|
||||
methods: {
|
||||
updateChildren() {
|
||||
// 如果存在子元素,则执行子元素的updateFromParent进行更新数据
|
||||
this.children.length && this.children.map((child) => child.updateFromParent());
|
||||
},
|
||||
getRect(selector, all) {
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
[all ? 'selectAll' : 'select'](selector)
|
||||
.boundingClientRect((rect) => {
|
||||
if (all && Array.isArray(rect) && rect.length) {
|
||||
resolve(rect);
|
||||
}
|
||||
if (!all && rect) {
|
||||
resolve(rect);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
},
|
||||
// 设置用于防止塌陷元素的高度
|
||||
async setPlaceholderHeight() {
|
||||
if (!this.fixed || !this.placeholder) return;
|
||||
// 延时一定时间
|
||||
await sleep(20);
|
||||
// #ifndef APP-NVUE
|
||||
this.getRect('.u-tabbar__content').then(({ height = 50 }) => {
|
||||
// 修复IOS safearea bottom 未填充高度
|
||||
this.placeholderHeight = height;
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
dom.getComponentRect(this.$refs['u-tabbar__content'], (res) => {
|
||||
const { size } = res;
|
||||
this.placeholderHeight = size.height;
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-tabbar {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px -2px 4px 0px rgba(51, 51, 51, 0.06);
|
||||
|
||||
&__item-wrapper {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mid-tabbar {
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
position: fixed;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
3
sheep/ui/su-tabs-item/props.js
Normal file
3
sheep/ui/su-tabs-item/props.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
props: {},
|
||||
};
|
26
sheep/ui/su-tabs-item/su-tabs-item.vue
Normal file
26
sheep/ui/su-tabs-item/su-tabs-item.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<swiper-item>
|
||||
<slot />
|
||||
</swiper-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from './props.js';
|
||||
/**
|
||||
* TabsItem tabs标签组件的自组件
|
||||
* @description tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
|
||||
* @tutorial https://www.uviewui.com/components/tabs.html
|
||||
* @property {type} prop_name
|
||||
* @event {Function()}
|
||||
* @example
|
||||
*/
|
||||
export default {
|
||||
name: 'u-tabs-item',
|
||||
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
434
sheep/ui/su-tabs/su-tabs.vue
Normal file
434
sheep/ui/su-tabs/su-tabs.vue
Normal file
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<view class="u-tabs">
|
||||
<view class="u-tabs__wrapper">
|
||||
<slot name="left" />
|
||||
<view class="u-tabs__wrapper__scroll-view-wrapper">
|
||||
<scroll-view
|
||||
:scroll-x="scrollable"
|
||||
:scroll-left="scrollLeft"
|
||||
scroll-with-animation
|
||||
enable-flex
|
||||
class="u-tabs__wrapper__scroll-view white-space"
|
||||
:show-scrollbar="false"
|
||||
ref="u-tabs__wrapper__scroll-view"
|
||||
>
|
||||
<view class="u-tabs__wrapper__nav" ref="u-tabs__wrapper__nav">
|
||||
<view
|
||||
class="u-tabs__wrapper__nav__item"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
@tap="clickHandler(item, index)"
|
||||
:ref="`u-tabs__wrapper__nav__item-${index}`"
|
||||
:style="[addStyle(itemStyle), { flex: scrollable ? '' : 1 }]"
|
||||
:class="[
|
||||
`u-tabs__wrapper__nav__item-${index}`,
|
||||
item.disabled && 'u-tabs__wrapper__nav__item--disabled',
|
||||
]"
|
||||
>
|
||||
<text
|
||||
:class="[item.disabled && 'u-tabs__wrapper__nav__item__text--disabled']"
|
||||
class="u-tabs__wrapper__nav__item__text"
|
||||
:style="[textStyle(index)]"
|
||||
>{{ item[keyName] }}</text
|
||||
>
|
||||
</view>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<view
|
||||
class="u-tabs__wrapper__nav__line"
|
||||
ref="u-tabs__wrapper__nav__line"
|
||||
:style="[
|
||||
{
|
||||
width: addUnit(lineWidth),
|
||||
height: addUnit(lineHeight),
|
||||
background: lineColor ? lineColor : 'var(--ui-BG-Main)',
|
||||
backgroundSize: lineBgSize,
|
||||
},
|
||||
]"
|
||||
></view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view
|
||||
class="u-tabs__wrapper__nav__line"
|
||||
ref="u-tabs__wrapper__nav__line"
|
||||
:style="[
|
||||
{
|
||||
width: addUnit(lineWidth),
|
||||
transform: `translate(${lineOffsetLeft}px)`,
|
||||
transitionDuration: `${firstTime ? 0 : duration}ms`,
|
||||
height: addUnit(lineHeight),
|
||||
background: lineColor ? lineColor : 'var(--ui-BG-Main)',
|
||||
backgroundSize: lineBgSize,
|
||||
},
|
||||
]"
|
||||
>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepMerge, addStyle, addUnit, sleep, getPx, sys } from '@/sheep/helper';
|
||||
// #ifdef APP-NVUE
|
||||
const animation = uni.requireNativePlugin('animation');
|
||||
const dom = uni.requireNativePlugin('dom');
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Tabs 标签
|
||||
* @description tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
|
||||
* @tutorial https://www.uviewui.com/components/tabs.html
|
||||
* @property {String | Number} duration 滑块移动一次所需的时间,单位秒(默认 200 )
|
||||
* @property {String | Number} swierWidth swiper的宽度(默认 '750rpx' )
|
||||
* @property {String} keyName 从`list`元素对象中读取的键名(默认 'name' )
|
||||
* @event {Function(index)} change 标签改变时触发 index: 点击了第几个tab,索引从0开始
|
||||
* @event {Function(index)} click 点击标签时触发 index: 点击了第几个tab,索引从0开始
|
||||
* @example <u-tabs :list="list" :is-scroll="false" :current="current" @change="change"></u-tabs>
|
||||
*/
|
||||
export default {
|
||||
name: 'su-tabs',
|
||||
data() {
|
||||
return {
|
||||
addStyle,
|
||||
addUnit,
|
||||
firstTime: true,
|
||||
scrollLeft: 0,
|
||||
scrollViewWidth: 0,
|
||||
lineOffsetLeft: 0,
|
||||
tabsRect: {
|
||||
left: 0,
|
||||
},
|
||||
innerCurrent: 0,
|
||||
moving: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 滑块的移动过渡时间,单位ms
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
// tabs标签数组
|
||||
list: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
// 滑块颜色
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 菜单选择中时的样式
|
||||
activeStyle: {
|
||||
type: [String, Object],
|
||||
default() {
|
||||
return {
|
||||
color: '#303133',
|
||||
};
|
||||
},
|
||||
},
|
||||
// 菜单非选中时的样式
|
||||
inactiveStyle: {
|
||||
type: [String, Object],
|
||||
default() {
|
||||
return {
|
||||
color: '#606266',
|
||||
};
|
||||
},
|
||||
},
|
||||
// 滑块长度
|
||||
lineWidth: {
|
||||
type: [String, Number],
|
||||
default: 20,
|
||||
},
|
||||
// 滑块高度
|
||||
lineHeight: {
|
||||
type: [String, Number],
|
||||
default: 3,
|
||||
},
|
||||
// 滑块背景显示大小,当滑块背景设置为图片时使用
|
||||
lineBgSize: {
|
||||
type: String,
|
||||
default: 'cover',
|
||||
},
|
||||
// 菜单item的样式
|
||||
itemStyle: {
|
||||
type: [String, Object],
|
||||
default() {
|
||||
return {
|
||||
height: '44px',
|
||||
};
|
||||
},
|
||||
},
|
||||
// 菜单是否可滚动
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 当前选中标签的索引
|
||||
current: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
// 默认读取的键名
|
||||
keyName: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
current: {
|
||||
immediate: true,
|
||||
handler(newValue, oldValue) {
|
||||
// 内外部值不相等时,才尝试移动滑块
|
||||
if (newValue !== this.innerCurrent) {
|
||||
this.innerCurrent = newValue;
|
||||
this.$nextTick(() => {
|
||||
this.resize();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
// list变化时,重新渲染list各项信息
|
||||
list() {
|
||||
this.$nextTick(() => {
|
||||
this.resize();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textStyle() {
|
||||
return (index) => {
|
||||
const style = {};
|
||||
// 取当期是否激活的样式
|
||||
const customeStyle =
|
||||
index === this.innerCurrent ? addStyle(this.activeStyle) : addStyle(this.inactiveStyle);
|
||||
// 如果当前菜单被禁用,则加上对应颜色,需要在此做处理,是因为nvue下,无法在style样式中通过!import覆盖标签的内联样式
|
||||
if (this.list[index].disabled) {
|
||||
style.color = '#c8c9cc';
|
||||
}
|
||||
return deepMerge(customeStyle, style);
|
||||
};
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
$uGetRect(selector, all) {
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
[all ? 'selectAll' : 'select'](selector)
|
||||
.boundingClientRect((rect) => {
|
||||
if (all && Array.isArray(rect) && rect.length) {
|
||||
resolve(rect);
|
||||
}
|
||||
if (!all && rect) {
|
||||
resolve(rect);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
},
|
||||
setLineLeft() {
|
||||
const tabItem = this.list[this.innerCurrent];
|
||||
if (!tabItem) {
|
||||
return;
|
||||
}
|
||||
// 获取滑块该移动的位置
|
||||
let lineOffsetLeft = this.list
|
||||
.slice(0, this.innerCurrent)
|
||||
.reduce((total, curr) => total + curr.rect.width, 0);
|
||||
// 获取下划线的数值px表示法
|
||||
const lineWidth = getPx(this.lineWidth);
|
||||
this.lineOffsetLeft = lineOffsetLeft + (tabItem.rect.width - lineWidth) / 2;
|
||||
// #ifdef APP-NVUE
|
||||
// 第一次移动滑块,无需过渡时间
|
||||
this.animation(this.lineOffsetLeft, this.firstTime ? 0 : parseInt(this.duration));
|
||||
// #endif
|
||||
|
||||
// 如果是第一次执行此方法,让滑块在初始化时,瞬间滑动到第一个tab item的中间
|
||||
// 这里需要一个定时器,因为在非nvue下,是直接通过style绑定过渡时间,需要等其过渡完成后,再设置为false(非第一次移动滑块)
|
||||
if (this.firstTime) {
|
||||
setTimeout(() => {
|
||||
this.firstTime = false;
|
||||
}, 10);
|
||||
}
|
||||
},
|
||||
// nvue下设置滑块的位置
|
||||
animation(x, duration = 0) {
|
||||
// #ifdef APP-NVUE
|
||||
const ref = this.$refs['u-tabs__wrapper__nav__line'];
|
||||
animation.transition(ref, {
|
||||
styles: {
|
||||
transform: `translateX(${x}px)`,
|
||||
},
|
||||
duration,
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// 点击某一个标签
|
||||
clickHandler(item, index) {
|
||||
// 因为标签可能为disabled状态,所以click是一定会发出的,但是change事件是需要可用的状态才发出
|
||||
this.$emit('click', {
|
||||
...item,
|
||||
index,
|
||||
});
|
||||
// 如果disabled状态,返回
|
||||
if (item.disabled) return;
|
||||
this.innerCurrent = index;
|
||||
this.resize();
|
||||
this.$emit('change', {
|
||||
...item,
|
||||
index,
|
||||
});
|
||||
},
|
||||
init() {
|
||||
sleep().then(() => {
|
||||
this.resize();
|
||||
});
|
||||
},
|
||||
setScrollLeft() {
|
||||
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
|
||||
const tabRect = this.list[this.innerCurrent];
|
||||
// 累加得到当前item到左边的距离
|
||||
const offsetLeft = this.list.slice(0, this.innerCurrent).reduce((total, curr) => {
|
||||
return total + curr.rect.width;
|
||||
}, 0);
|
||||
// 此处为屏幕宽度
|
||||
const windowWidth = sys().windowWidth;
|
||||
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
|
||||
let scrollLeft =
|
||||
offsetLeft -
|
||||
(this.tabsRect.width - tabRect.rect.width) / 2 -
|
||||
(windowWidth - this.tabsRect.right) / 2 +
|
||||
this.tabsRect.left / 2;
|
||||
// 这里做一个限制,限制scrollLeft的最大值为整个scroll-view宽度减去tabs组件的宽度
|
||||
scrollLeft = Math.min(scrollLeft, this.scrollViewWidth - this.tabsRect.width);
|
||||
this.scrollLeft = Math.max(0, scrollLeft);
|
||||
},
|
||||
// 获取所有标签的尺寸
|
||||
resize() {
|
||||
// 如果不存在list,则不处理
|
||||
if (this.list.length === 0) {
|
||||
return;
|
||||
}
|
||||
Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(
|
||||
([tabsRect, itemRect = []]) => {
|
||||
this.tabsRect = tabsRect;
|
||||
this.scrollViewWidth = 0;
|
||||
itemRect.map((item, index) => {
|
||||
// 计算scroll-view的宽度,这里
|
||||
this.scrollViewWidth += item.width;
|
||||
// 另外计算每一个item的中心点X轴坐标
|
||||
this.list[index].rect = item;
|
||||
});
|
||||
// 获取了tabs的尺寸之后,设置滑块的位置
|
||||
this.setLineLeft();
|
||||
this.setScrollLeft();
|
||||
},
|
||||
);
|
||||
},
|
||||
// 获取导航菜单的尺寸
|
||||
getTabsRect() {
|
||||
return new Promise((resolve) => {
|
||||
this.queryRect('u-tabs__wrapper__scroll-view').then((size) => resolve(size));
|
||||
});
|
||||
},
|
||||
// 获取所有标签的尺寸
|
||||
getAllItemRect() {
|
||||
return new Promise((resolve) => {
|
||||
const promiseAllArr = this.list.map((item, index) =>
|
||||
this.queryRect(`u-tabs__wrapper__nav__item-${index}`, true),
|
||||
);
|
||||
Promise.all(promiseAllArr).then((sizes) => resolve(sizes));
|
||||
});
|
||||
},
|
||||
// 获取各个标签的尺寸
|
||||
queryRect(el, item) {
|
||||
// #ifndef APP-NVUE
|
||||
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
|
||||
// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
|
||||
return new Promise((resolve) => {
|
||||
this.$uGetRect(`.${el}`).then((size) => {
|
||||
resolve(size);
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
// nvue下,使用dom模块查询元素高度
|
||||
// 返回一个promise,让调用此方法的主体能使用then回调
|
||||
return new Promise((resolve) => {
|
||||
dom.getComponentRect(item ? this.$refs[el][0] : this.$refs[el], (res) => {
|
||||
resolve(res.size);
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-tabs {
|
||||
background: #fff;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
|
||||
&__wrapper {
|
||||
@include flex;
|
||||
align-items: center;
|
||||
|
||||
&__scroll-view-wrapper {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: auto hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&__nav {
|
||||
@include flex;
|
||||
position: relative;
|
||||
|
||||
&__item {
|
||||
padding: 0 11px;
|
||||
@include flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--disabled {
|
||||
/* #ifndef APP-NVUE */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap !important;
|
||||
|
||||
&--disabled {
|
||||
color: #c8c9cc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__line {
|
||||
height: 3px;
|
||||
background: #3c9cff;
|
||||
width: 30px;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
border-radius: 100px;
|
||||
transition-property: transform;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
37
sheep/ui/su-time-line/su-time-line.vue
Normal file
37
sheep/ui/su-time-line/su-time-line.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="u-time-axis"><slot /></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* timeLine 时间轴
|
||||
* @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。
|
||||
* @tutorial https://www.uviewui.com/components/timeLine.html
|
||||
* @example <u-time-line></u-time-line>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-time-line',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-time-axis {
|
||||
padding-left: 40rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u-time-axis::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 12rpx;
|
||||
width: 1px;
|
||||
bottom: 0;
|
||||
border-left: 1px solid #ddd;
|
||||
transform-origin: 0 0;
|
||||
transform: scaleX(0.5);
|
||||
}
|
||||
</style>
|
76
sheep/ui/su-timeline-item/su-timeline-item.vue
Normal file
76
sheep/ui/su-timeline-item/su-timeline-item.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view class="u-time-axis-item">
|
||||
<slot name="content" />
|
||||
<view class="u-time-axis-node" :style="[nodeStyle]">
|
||||
<slot name="node"><view class="u-dot"></view></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* timeLineItem 时间轴Item
|
||||
* @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。(搭配u-time-line使用)
|
||||
* @tutorial https://www.uviewui.com/components/timeLine.html
|
||||
* @property {String} bg-color 左边节点的背景颜色,一般通过slot内容自定义背景颜色即可(默认#ffffff)
|
||||
* @property {String Number} node-top 节点左边图标绝对定位的top值,单位rpx
|
||||
* @example <u-time-line-item node-top="2">...</u-time-line-item>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-time-line-item',
|
||||
props: {
|
||||
// 节点的背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff',
|
||||
},
|
||||
// 节点左边图标绝对定位的top值
|
||||
nodeTop: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
nodeStyle() {
|
||||
let style = {
|
||||
backgroundColor: this.bgColor,
|
||||
};
|
||||
if (this.nodeTop != '') style.top = this.nodeTop + 'rpx';
|
||||
return style;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-time-axis-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.u-time-axis-node {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
left: -40rpx;
|
||||
transform-origin: 0;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.u-dot {
|
||||
height: 16rpx;
|
||||
width: 16rpx;
|
||||
border-radius: 100rpx;
|
||||
background: #ddd;
|
||||
}
|
||||
</style>
|
129
sheep/ui/su-toolbar/su-toolbar.vue
Normal file
129
sheep/ui/su-toolbar/su-toolbar.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="u-toolbar" @touchmove.stop.prevent="noop" v-if="show">
|
||||
<view class="u-toolbar__cancel__wrapper" hover-class="u-hover-class">
|
||||
<text
|
||||
class="u-toolbar__wrapper__cancel"
|
||||
@tap="cancel"
|
||||
:style="{
|
||||
color: cancelColor,
|
||||
}"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="u-toolbar__title u-line-1" v-if="title">{{ title }}</text>
|
||||
<view class="u-toolbar__confirm__wrapper" hover-class="u-hover-class">
|
||||
<text
|
||||
class="u-toolbar__wrapper__confirm"
|
||||
@tap="confirm"
|
||||
:style="{
|
||||
color: confirmColor,
|
||||
}"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Toolbar 工具条
|
||||
* @description
|
||||
* @tutorial https://www.uviewui.com/components/toolbar.html
|
||||
* @property {Boolean} show 是否展示工具条(默认 true )
|
||||
* @property {String} cancelText 取消按钮的文字(默认 '取消' )
|
||||
* @property {String} confirmText 确认按钮的文字(默认 '确认' )
|
||||
* @property {String} cancelColor 取消按钮的颜色(默认 '#909193' )
|
||||
* @property {String} confirmColor 确认按钮的颜色(默认 '#3c9cff' )
|
||||
* @property {String} title 标题文字
|
||||
* @event {Function}
|
||||
* @example
|
||||
*/
|
||||
export default {
|
||||
name: 'SuToolbar',
|
||||
props: {
|
||||
// 是否展示工具条
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 取消按钮的文字
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
// 确认按钮的文字
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确认',
|
||||
},
|
||||
// 取消按钮的颜色
|
||||
cancelColor: {
|
||||
type: String,
|
||||
default: '#909193',
|
||||
},
|
||||
// 确认按钮的颜色
|
||||
confirmColor: {
|
||||
type: String,
|
||||
default: '#3c9cff',
|
||||
},
|
||||
// 标题文字
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 点击取消按钮
|
||||
cancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
// 点击确定按钮
|
||||
confirm() {
|
||||
this.$emit('confirm');
|
||||
},
|
||||
// 阻止事件冒泡
|
||||
preventEvent(e) {
|
||||
e && typeof e.stopPropagation === 'function' && e.stopPropagation();
|
||||
},
|
||||
// 空操作
|
||||
noop(e) {
|
||||
this.preventEvent(e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-toolbar {
|
||||
height: 42px;
|
||||
@include flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__wrapper {
|
||||
&__cancel {
|
||||
color: #111111;
|
||||
font-size: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #000000;
|
||||
padding: 0 60rpx;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
&__confirm {
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
199
sheep/ui/su-video/su-video.vue
Normal file
199
sheep/ui/su-video/su-video.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<view class="ui-video-wrap">
|
||||
<video
|
||||
:id="`sVideo${uid}`"
|
||||
class="radius"
|
||||
:style="[{ height: height + 'rpx' }]"
|
||||
:src="src"
|
||||
controls
|
||||
object-fit="contain"
|
||||
:enable-progress-gesture="state.enableProgressGesture"
|
||||
:initial-time="initialTime"
|
||||
x5-video-player-type="h5"
|
||||
x-webkit-airplay="allow"
|
||||
webkit-playsinline="true"
|
||||
@error="videoErrorCallback"
|
||||
@timeupdate="timeupdate"
|
||||
@play="play"
|
||||
@pause="pause"
|
||||
@ended="end"
|
||||
:poster="poster"
|
||||
:autoplay="autoplay"
|
||||
>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<cover-view :style="{ transform: 'translateX(' + moveX + 'px)' }" />
|
||||
<!-- #endif -->
|
||||
</video>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
/**
|
||||
* 视频组件
|
||||
*
|
||||
* @property {Number} uid = 0 - 当前轮播下标,还用来标记视频Id
|
||||
* @property {Number} moveX = 0 - app端轮播滑动距离
|
||||
* @property {String} height = 300 - 高度(rpx)
|
||||
* @property {String} width = 750 - 宽度(rpx)
|
||||
* @property {Number} initialTime = 0 - 指定视频播放位置
|
||||
* @property {String} videoSize - 视频大小
|
||||
* @property {String} src - 视频播放地址
|
||||
* @property {String} poster - 视频封面
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import { reactive, nextTick, getCurrentInstance } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
const vm = getCurrentInstance();
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
// #ifdef APP-PLUS
|
||||
enableProgressGesture: true, // 手势滑动
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
enableProgressGesture: false, // 手势滑动
|
||||
// #endif
|
||||
showModal: false, // 弹框
|
||||
});
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
moveX: {
|
||||
type: [Number],
|
||||
default: 0,
|
||||
},
|
||||
// 下标索引
|
||||
uid: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
// 视频高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
// 视频宽度
|
||||
width: {
|
||||
type: Number,
|
||||
default: 750,
|
||||
},
|
||||
// 指定视频初始播放位置,单位为秒(s)
|
||||
initialTime: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto',
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
// 事件
|
||||
const emits = defineEmits(['videoTimeupdate']);
|
||||
|
||||
// 播放进度变化时触发,播放进度传给父组件
|
||||
const timeupdate = (e) => {
|
||||
emits('videoTimeupdate', e);
|
||||
};
|
||||
const videoErrorCallback = (e) => {
|
||||
console.log('视频错误信息:', e.target.errMsg);
|
||||
};
|
||||
// 当开始/继续播放时触发play事件
|
||||
const play = () => {
|
||||
console.log('视频开始');
|
||||
};
|
||||
// 当暂停播放时触发 pause 事件
|
||||
const pause = () => {
|
||||
console.log('视频暂停');
|
||||
};
|
||||
// 视频结束触发end 时间
|
||||
const end = () => {
|
||||
console.log('视频结束');
|
||||
};
|
||||
// 开始播放
|
||||
const startPlay = () => {
|
||||
nextTick(() => {
|
||||
const video = uni.createVideoContext(`sVideo${props.index}`, vm);
|
||||
video.play();
|
||||
});
|
||||
};
|
||||
|
||||
//暂停播放
|
||||
const pausePlay = () => {
|
||||
const video = uni.createVideoContext(`sVideo${props.index}`, vm);
|
||||
video.pause();
|
||||
};
|
||||
|
||||
// 播放前拦截
|
||||
const beforePlay = () => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
const networkType = res.networkType;
|
||||
// if (networkType === 'wifi' || networkType === 'ethernet') {
|
||||
// startPlay();
|
||||
// } else {
|
||||
// uni.showModal({
|
||||
// title: '提示',
|
||||
// content: `当前为移动网络,播放视频需消耗手机流量,是否继续播放?${networkType}`,
|
||||
// success: (res) => {
|
||||
// if (res.confirm) {
|
||||
// startPlay();
|
||||
// } else {
|
||||
// state.isplay = false;
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// sheep.$helper.toast('正在消耗流量播放');
|
||||
// startPlay();
|
||||
// }
|
||||
startPlay();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 抛出方法供父组件调用
|
||||
defineExpose({
|
||||
pausePlay,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.radius {
|
||||
width: 100%;
|
||||
}
|
||||
.ui-video-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.poster-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba($color: #000000, $alpha: 0.1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user