项目初始化

This commit is contained in:
jerry
2025-01-21 01:46:34 +08:00
parent 364021b042
commit 48153e7761
962 changed files with 172070 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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模式为了和原来保持一致的样式需要记录并重新设置它的leftheightwidth属性
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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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() {
// 如果fixedplaceholder等参数发生变化重新计算占位元素的高度
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>

View File

@@ -0,0 +1,3 @@
export default {
props: {},
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>