手拉手 cv教学 复制就能学会
date.vue 父组件
datepicker.vue 儿子组件
picker.vue 孙子组件
item.vue 太孙子组件
date.vue 父组件
<template>
<div>
<button @click.prevent="opendate">open</button>
<p>日期:{{ datetime }}</p>
<Date
v-model="datetime"
:fill0="true"
placeholder="请选择"
ref="datepicker"
:showUnit="true"
></Date>
</div>
</template>
<script>
import Date from "./datecomponents/datepicker.vue";
export default {
data() {
return {
datetime: "2021-11-11",
};
},
components: {
Date,
},
methods: {
opendate() {
this.$refs.datepicker._init();
},
},
};
</script>
<style>
</style>
datepicker.vue 儿子组件
<template>
<div @click="_init">
<Picker
:visible.sync="visible"
:data="data"
v-model="valueNew"
:change="_change"
:cancelEvent="_cancelEvent"
:confirmEvent="_confirmEvent"
:cancelText="cancelText"
:confirmText="confirmText"
:title="title"
:visibleCount="visibleCount"
@input="_input"
></Picker>
</div>
</template>
<script>
import Picker from "./picker.vue";
export default {
name: "datePicker",
data() {
return {
visible: false,
valueCache: [], //改变后临时保存的值
data: [],
};
},
props: {
visibles: Boolean,
min: String,
max: String,
type: {
type: String,
default: "ymd",
},
value: String,
placeholder: String, //模拟input效果
cancelText: String,
cancelEvent: Function,
confirmText: String,
confirmEvent: Function,
change: Function,
title: String,
visibleCount: Number,
showUnit: {
type: Boolean,
default: false,
},
fill0: {
//小于10前面补0
type: Boolean,
default: false,
},
},
components: { Picker },
methods: {
_init() {
this.visible = true;
},
_input(start, end) {
this.$emit("timer", start, end);
// console.log(staet, end)
},
_change(value, index) {
const valueNum = parseInt(value); //带单位时,这个value会是01月
if (index == 0) {
this.valueCache[0] = valueNum;
} else if (index == 1 && this.type.substr(2, 1) == "d") {
//只在月份改变时做联动
let day = new Date(this.valueCache[0], valueNum, 0);
let array = this._forArray(1, day.getDate(), "日");
this.data.splice(2, 1, array);
}
this.cancelEvent ? this.cancelEvent(valueNum) : "";
},
_cancelEvent(value) {
this.cancelEvent ? this.cancelEvent(this._format(value)) : "";
},
_confirmEvent(value) {
this.$emit("input", this._format(value));
this.confirmEvent ? this.confirmEvent(this._format(value)) : "";
},
_setDate() {
this.data.splice(0, this.data.length);
let min = new Date(this.min);
let max = new Date(this.max);
let value = new Date(this.value);
let cur = new Date();
let yearMin, yearMax;
//无起始和结束时间,显示前后10年
if (isNaN(min)) {
yearMin = cur.getFullYear() - 10;
} else {
yearMin = min.getFullYear();
}
if (isNaN(max)) {
yearMax = cur.getFullYear() + 10;
} else {
yearMax = max.getFullYear();
}
//如果没有初始值,则设置为当前时间
if (value == "Invalid Date") {
value = cur;
}
//取当月天数
//new Date(2018,4,1)输出2018-5-1,月份从0开始
//new Date(2018,4,0)输出2018-4-30,0表示前一天,即上月最后一天
let day = new Date(
value.getFullYear(),
value.getMonth() + 1,
0
).getDate();
this.data.push(this._forArray(yearMin, yearMax, "年"));
let type = this.type;
//type第2位为m时显示月份
if (type.substr(1, 1) == "m") {
this.data.push(this._forArray(1, 12, "月"));
}
if (type.substr(2, 1) == "d") {
this.data.push(this._forArray(1, day, "日"));
}
if (type.substr(3, 1) == "h") {
this.data.push(this._forArray(0, 23, "时"));
}
if (type.substr(4, 1) == "m") {
this.data.push(this._forArray(0, 59, "分"));
}
if (type.substr(5, 1) == "s") {
this.data.push(this._forArray(0, 59, "秒"));
}
},
_forArray(min, max, unit) {
let array = [];
let v;
for (let i = min; i <= max; i++) {
//前面补0
v = i.toString();
if (this.fill0 && i < 10) {
v = "0" + i;
}
if (this.showUnit) {
v = v + unit;
}
array.push(v.toString());
}
return { value: array };
},
_format(value) {
//格式化时间
let day, day2;
if (this.showUnit) {
day = value.toString().replace(/,/g, "");
day2 = value.toString().replace("年,", "-");
day2 = day2.toString().replace("月,", "-");
day2 = day2.toString().replace("日,", " ");
day2 = day2.toString().replace("时,", ":");
day2 = day2.toString().replace("分,", ":");
day2 = day2.toString().replace("秒", "");
} else {
day = value.toString().replace(",", "-");
day = day.toString().replace(",", "-");
day = day.toString().replace(",", " ");
day = day.toString().replace(",", ":");
day = day.toString().replace(",", ":");
day2 = day;
}
//当选择的时候超出最大或最小值时,做限制
if (this.min != "" || this.max != "") {
const minMax = new Date(day2);
const min = new Date(this.min);
const max = new Date(this.max);
if (min > minMax) {
day = this.min;
}
if (max < minMax) {
day = this.max;
}
//这里也要做格式化转换,为简化代码先跳过
}
return day;
},
},
computed: {
valueNew: {
get() {
let v = new Date(this.value);
let array = [];
if (this.value == "") {
v = new Date();
}
this.valueCache[0] = v.getFullYear();
this.valueCache[1] = v.getMonth();
if (this.showUnit) {
array = [
v.getFullYear().toString() + "年",
(v.getMonth() + 1).toString() + "月",
v.getDate().toString() + "日",
v.getHours().toString() + "时",
v.getMinutes().toString() + "分",
v.getSeconds().toString() + "秒",
];
} else {
array = [
v.getFullYear().toString(),
(v.getMonth() + 1).toString(),
v.getDate().toString(),
v.getHours().toString(),
v.getMinutes().toString(),
v.getSeconds().toString(),
];
}
//按显示格式裁剪数组
return array.splice(0, this.type.length);
},
set() {},
},
},
mounted() {
this._setDate();
},
filters: {},
};
</script>
picker.vue 孙子组件
<template>
<div class="picker">
<transition name="fade">
<div class="mask" v-show="visible" @click="_maskClick"></div>
</transition>
<transition name="slide">
<div class="picker-content" v-show="visible" ref="content">
<div class="picker-control">
<a
href="javascript:;"
class="picker-cancel"
v-text="cancelText"
@click="_cancelClick"
></a>
<span v-text="title" v-if="title" class="picker-title"></span>
<a
href="javascript:;"
class="picker-confirm"
v-text="confirmText"
@click="_confirmClick"
>确定</a
>
</div>
<!-- <div class="picker-tabs">
<div
class="tabs-item"
v-for="(item, index) in tabsList"
:key="item.id"
>
<span
:class="tabsind == index ? 'active-span' : 'item-span'"
@click="clickItem(index)"
>{{ item.text }}</span
>
<div :class="tabsind == index ? 'active-div' : 'item-div'"></div>
</div>
</div> -->
<div
class="picker-group"
:style="{ height: visibleCount * liHeight + 'px' }"
>
<div class="picker-border"></div>
<pickerItem
v-for="(item, index) in data"
:data="item.value"
:key="index"
:index="index"
:height="liHeight"
:change="_change"
:value="typeof value == 'string' ? value : value[index]"
ref="item"
></pickerItem>
</div>
</div>
</transition>
</div>
</template>
<script>
import pickerItem from "./item.vue";
export default {
name: "picker",
data() {
return {
liHeight: 0,
newValue: this.value,
tabsind: 0,
tabsList: [
{
id: 0,
text: "开始时间",
},
{
id: 1,
text: "结束时间",
},
],
startTime: [],
endTime: [],
};
},
watch: {
visible(v) {
//初始时数据为空,在显示时再计算位置
if (v && this.liHeight == 0) {
this._getDisplayHeight();
}
},
},
created() {
console.log(this.data);
},
props: {
visible: {
//显示或隐藏,通过sync实现双向绑定
type: Boolean,
default: false,
},
maskClose: {
//点闭遮罩层是否关闭
type: Boolean,
default: true,
},
cancelText: {
//取消按钮文本
type: String,
default: "取消",
},
cancelEvent: Function,
confirmText: {
//确定按钮文本
type: String,
default: "确认",
},
confirmEvent: Function,
change: Function,
title: {
type: String,
default: "自定义日期",
},
visibleCount: {
//显示的个数
type: Number,
default: 5,
},
data: Array,
value: [String, Array],
},
components: { pickerItem },
methods: {
clickItem(index) {
this.tabsind = index;
},
_maskClick(e) {
//点闭遮罩层是否关闭
this.maskClose ? this._cancelClick(e) : "";
},
_cancelClick(e) {
//点击取消,关闭退出
//恢复状态
let item = this.$refs.item;
for (let i in item) {
item[i]._moveTo();
}
this.$emit("update:visible", false);
this.cancelEvent ? this.cancelEvent(this.value) : "";
e.stopPropagation();
},
_confirmClick(e) {
//this._cancelClick();
this.$emit("update:visible", false);
this.confirmEvent ? this.confirmEvent(this.newValue) : "";
if (this.tabsind == 1) {
this.endTime = this.newValue;
}
this.$emit("input", this.startTime, this.endTime);
e.stopPropagation();
},
_change(value, index, bool) {
//这里修改为点击确认才更新选中值
if (typeof this.value == "string") {
//this.$emit('input', value);
this.newValue = value;
} else {
let newValue = this.newValue.slice(0);
newValue[index] = value;
if (this.tabsind == 0) {
this.startTime = newValue;
}
if (this.tabsind == 1) {
this.endTime = newValue;
}
//采用上面方法是不会同步更新的,因为vue监听的是this.value,
//没有监听this.value的子项,所以直接改变子项不会触发更新
//newValue.splice(index, 1, value);//先移除再添加
//this.$emit('input', newValue);
this.newValue = newValue;
}
//bool=false时是初始时设置的
if (bool) {
this.change ? this.change(value, index) : "";
}
},
_getDisplayHeight() {
//取隐藏标签的高
const obj = this.$refs.content;
const clone = obj.cloneNode(true);
clone.style.display = "block";
clone.style.position = "absolute";
clone.style.opacity = 0;
clone.style.top = "-10000px";
obj.parentNode.appendChild(clone);
const li = clone.querySelector("li");
if (li) {
//this.liHeight = li.offsetHeight;//取到的是整数
this.liHeight = parseFloat(window.getComputedStyle(li, null).height); //取到的精确到小数
}
obj.parentNode.removeChild(clone);
},
},
computed: {},
mounted() {
this._getDisplayHeight();
this.tabsind = 0;
},
filters: {},
};
</script>
<style scoped lang='less'>
.picker {
touch-action: none;
.mask {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.picker-content {
position: fixed;
left: 0;
bottom: 0;
right: 0;
background: #fff;
z-index: 101;
border-radius: 20px 20px 0px 0px;
}
.active-span {
font-family: PingFangSC-Medium;
font-size: 30px;
color: #6522e6;
font-weight: 500;
margin-top: 15px;
}
.active-div {
width: 52px;
height: 6px;
background: #6522e6;
border-radius: 3px;
margin-top: 5px;
}
.picker-tabs {
width: 650px;
margin: 0 auto;
height: 80px;
display: flex;
justify-content: center;
border-bottom: 2px solid #e6e8ed;
.tabs-item {
width: 40%;
display: flex;
flex-direction: column;
align-items: center;
.item-span {
font-family: PingFangSC-Regular;
font-size: 28px;
color: #130038;
font-weight: 400;
margin-top: 15px;
}
}
}
/*取消确定按钮*/
.picker-control {
height: 100px;
background: #f8f8f8;
border-radius: 20px 20px 0px 0px;
padding: 0 20px;
display: flex;
justify-content: center;
align-items: center;
.picker-title {
display: block;
flex: 2;
font-family: PingFangSC-Medium;
font-size: 32px;
color: #130038;
text-align: center;
font-weight: 500;
}
a {
font-family: PingFangSC-Regular;
display: block;
color: #818181;
font-size: 32px;
font-weight: 400;
&:last-child {
font-family: PingFangSC-Medium;
text-align: right;
color: #130038;
}
}
}
.picker-group {
display: flex;
position: relative;
overflow: hidden;
.picker-border {
left: 50%;
top: 50%;
margin-left: -326px;
position: absolute;
height: 100px;
border-bottom: 2px solid #e5e5e5;
border-top: 2px solid #e5e5e5;
width: 650px;
box-sizing: border-box;
transform: translateY(-50%);
}
}
}
.picker-item {
width: 100%;
position: relative;
overflow: hidden;
.picker-mask {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 3;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.95),
rgba(255, 255, 255, 0.6)
),
linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));
background-repeat: no-repeat;
background-position: top, bottom;
/*background-size: 100% 102px;*/
/*两个线性过度叠在一起,通过size设定显示的高度*/
}
li {
height: 100px;
line-height: 100px;
text-align: center;
overflow: hidden;
box-sizing: border-box;
font-family: PingFangSC-Medium;
font-size: 32px;
color: #130038;
font-weight: 500;
&.disabled {
font-style: italic;
}
}
}
// .fade-enter-active {
// animation: fadeIn 0.5s;
// }
// .fade-leave-active {
// animation: fadeOut 0.5s;
// }
// @keyframes fadeIn {
// 0% {
// opacity: 0;
// }
// 100% {
// opacity: 1;
// }
// }
// @keyframes fadeOut {
// 0% {
// opacity: 1;
// }
// 100% {
// opacity: 0;
// }
// }
// .slide-enter-active {
// animation: fadeUp 0.5s;
// }
// .slide-leave-active {
// animation: fadeDown 0.5s;
// }
// @keyframes fadeUp {
// 0% {
// opacity: 0.6;
// transform: translateY(100%);
// }
// 100% {
// opacity: 1;
// transform: translateY(0);
// }
// }
// @keyframes fadeDown {
// 0% {
// opacity: 1;
// transform: translateY(0);
// }
// 100% {
// opacity: 0.6;
// transform: translateY(100%);
// }
// }
</style>
item.vue 太孙子组件
<template>
<div
class="picker-item"
@touchstart="_onTouchStart"
@touchmove.prevent="_onTouchMove"
@touchend="_onTouchEnd"
@touchcancel="_onTouchEnd"
>
<div class="picker-mask" :style="pickerMask"></div>
<ul class="picker-li" :style="transformStyle">
<li
v-for="(item, index) in data"
v-text="item.name || item"
:key="index"
:class="{ disabled: item.disabled }"
></li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: "picker-item",
data() {
return {
startY: 0, //touch时鼠标所有位置
startOffset: 0, //touch前已移动的距离
offset: 0, //当前移动的距离
};
},
watch: {
height() {
//父组件mounted后更新了height的高度,这里将数据移动到指定位置
this._moveTo();
},
data() {
//在联动时,数据变化了,下级还会保持在上一次的移动位置
this._moveTo();
},
},
props: {
height: Number, //移动单位的高度
data: Array,
change: Function,
value: String, //选中的值
index: Number, //当前索引,多个选择时如联动时,指向的是第几个选择,在change时返回去区别哪项改变了
},
components: {},
methods: {
_getTouch(event) {
return event.changedTouches[0] || event.touches[0];
},
_getVisibleCount() {
//取显示条数的一半,因为选中的在中间,显示条数为奇数
return Math.floor(this.$parent.visibleCount / 2);
},
_onTouchStart(event) {
const touch = this._getTouch(event);
this.startOffset = this.offset;
this.startY = touch.clientY;
},
_onTouchMove(event) {
const touch = this._getTouch(event);
const currentY = touch.clientY;
const distance = currentY - this.startY;
this.offset = this.startOffset + distance;
},
_onTouchEnd() {
let index = Math.round(this.offset / this.$parent.liHeight);
const vc = this._getVisibleCount();
// console.log("liHeight:" + this.$parent.liHeight);
// console.log("this.offset:" + this.offset);
// console.log("index:" + index);
// index的有效范围
const indexMax = vc - this.data.length;
if (index >= vc) {
index = 0; // 选择第一个
} else if (index < indexMax) {
// 选择最后一个
index = this.data.length - 1; //最后一个
} else {
index = vc - index;
}
this._setIndex(index, true);
},
_setIndex(index, bool) {
//按显示5条计算,选择第3条时,偏移为0,选择第1条时,偏移为li的高*2
//即偏移距离为(5/2取整-index)*liHeight
//如果当前选中的为disabled状态,则往下选择,仅在滑动选择时判断,默认填值时不作判断
//存在数据加载问题,有可能初始时数据是空的
if (this.data.length > 0) {
bool ? (index = this._isDisabled(index, index)) : "";
this.offset = (this._getVisibleCount() - index) * this.height;
//回调
const value = this.data[index].value || this.data[index];
this.change ? this.change(value, this.index, bool) : "";
}
},
_isDisabled(index, index2) {
if (this.data[index].disabled) {
if (index == this.data.length - 1) {
index = -1; //到最后一条时,再从第一条开始找
}
//防止死循环,全都是disabled时,原路返回
if (index + 1 == index2) {
return index2;
}
return this._isDisabled(index + 1);
}
return index;
},
_moveTo() {
//根据value移动动相对应的位置,这个是组件加载完引用
let index = 0;
for (let i = 0; i < this.data.length; i++) {
let v = this.data[i].value || this.data[i];
if (this.value === v) {
index = i;
break;
}
}
this._setIndex(index, false);
//没有默认时或是value不存在于数据数组中时index=0
},
},
computed: {
pickerMask() {
return {
//设定过度遮罩的显示高度,即总显示个数减1(高亮)的一半
backgroundSize: "100% " + this._getVisibleCount() * this.height + "px",
};
},
transformStyle() {
return {
transition: "all 150ms ease",
transform: `translate3d(0, ${this.offset}px, 0)`,
};
},
},
mounted() {},
filters: {},
};
</script>
<style scoped lang='less'>
.picker {
touch-action: none;
.mask {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.picker-content {
position: fixed;
left: 0;
bottom: 0;
right: 0;
background: #fff;
z-index: 101;
border-radius: 20px 20px 0px 0px;
}
.active-span {
font-family: PingFangSC-Medium;
font-size: 30px;
color: #6522e6;
font-weight: 500;
margin-top: 15px;
}
.active-div {
width: 52px;
height: 6px;
background: #6522e6;
border-radius: 3px;
margin-top: 5px;
}
.picker-tabs {
width: 650px;
margin: 0 auto;
height: 80px;
display: flex;
justify-content: center;
border-bottom: 2px solid #e6e8ed;
.tabs-item {
width: 40%;
display: flex;
flex-direction: column;
align-items: center;
.item-span {
font-family: PingFangSC-Regular;
font-size: 28px;
color: #130038;
font-weight: 400;
margin-top: 15px;
}
}
}
/*取消确定按钮*/
.picker-control {
height: 100px;
background: #f8f8f8;
border-radius: 20px 20px 0px 0px;
padding: 0 20px;
display: flex;
justify-content: center;
align-items: center;
.picker-title {
display: block;
flex: 2;
font-family: PingFangSC-Medium;
font-size: 32px;
color: #130038;
text-align: center;
font-weight: 500;
}
a {
font-family: PingFangSC-Regular;
display: block;
color: #818181;
font-size: 32px;
font-weight: 400;
&:last-child {
font-family: PingFangSC-Medium;
text-align: right;
color: #130038;
}
}
}
.picker-group {
display: flex;
position: relative;
overflow: hidden;
.picker-border {
left: 50%;
top: 50%;
margin-left: -326px;
position: absolute;
height: 100px;
border-bottom: 2px solid #e5e5e5;
border-top: 2px solid #e5e5e5;
width: 650px;
box-sizing: border-box;
transform: translateY(-50%);
}
}
}
.picker-item {
width: 100%;
position: relative;
overflow: hidden;
.picker-mask {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 3;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.95),
rgba(255, 255, 255, 0.6)
),
linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));
background-repeat: no-repeat;
background-position: top, bottom;
/*background-size: 100% 102px;*/
/*两个线性过度叠在一起,通过size设定显示的高度*/
}
li {
height: 100px;
line-height: 100px;
text-align: center;
overflow: hidden;
box-sizing: border-box;
font-family: PingFangSC-Medium;
font-size: 32px;
color: #130038;
font-weight: 500;
&.disabled {
font-style: italic;
}
}
}
// .fade-enter-active {
// animation: fadeIn 0.5s;
// }
// .fade-leave-active {
// animation: fadeOut 0.5s;
// }
// @keyframes fadeIn {
// 0% {
// opacity: 0;
// }
// 100% {
// opacity: 1;
// }
// }
// @keyframes fadeOut {
// 0% {
// opacity: 1;
// }
// 100% {
// opacity: 0;
// }
// }
// .slide-enter-active {
// animation: fadeUp 0.5s;
// }
// .slide-leave-active {
// animation: fadeDown 0.5s;
// }
// @keyframes fadeUp {
// 0% {
// opacity: 0.6;
// transform: translateY(100%);
// }
// 100% {
// opacity: 1;
// transform: translateY(0);
// }
// }
// @keyframes fadeDown {
// 0% {
// opacity: 1;
// transform: translateY(0);
// }
// 100% {
// opacity: 0.6;
// transform: translateY(100%);
// }
// }
</style>