微信截圖_20211130113621.png
手拉手 cv教學(xué) 復(fù)制就能學(xué)會
date.vue 父組件
datepicker.vue 兒子組件
picker.vue 孫子組件
item.vue 太孫子組件
微信圖片_20211130113945.png
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") {
//只在月份改變時做聯(lián)動
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;
//無起始和結(jié)束時間蔬芥,顯示前后10年
if (isNaN(min)) {
yearMin = cur.getFullYear() - 10;
} else {
yearMin = min.getFullYear();
}
if (isNaN(max)) {
yearMax = cur.getFullYear() + 10;
} else {
yearMax = max.getFullYear();
}
//如果沒有初始值仅炊,則設(shè)置為當(dāng)前時間
if (value == "Invalid Date") {
value = cur;
}
//取當(dāng)月天數(shù)
//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;
}
//當(dāng)選擇的時候超出最大或最小值時鼻吮,做限制
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;
}
//這里也要做格式化轉(zhuǎn)換,為簡化代碼先跳過
}
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(),
];
}
//按顯示格式裁剪數(shù)組
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: "結(jié)束時間",
},
],
startTime: [],
endTime: [],
};
},
watch: {
visible(v) {
//初始時數(shù)據(jù)為空筹裕,在顯示時再計算位置
if (v && this.liHeight == 0) {
this._getDisplayHeight();
}
},
},
created() {
console.log(this.data);
},
props: {
visible: {
//顯示或隱藏醋闭,通過sync實現(xiàn)雙向綁定
type: Boolean,
default: false,
},
maskClose: {
//點閉遮罩層是否關(guān)閉
type: Boolean,
default: true,
},
cancelText: {
//取消按鈕文本
type: String,
default: "取消",
},
cancelEvent: Function,
confirmText: {
//確定按鈕文本
type: String,
default: "確認",
},
confirmEvent: Function,
change: Function,
title: {
type: String,
default: "自定義日期",
},
visibleCount: {
//顯示的個數(shù)
type: Number,
default: 5,
},
data: Array,
value: [String, Array],
},
components: { pickerItem },
methods: {
clickItem(index) {
this.tabsind = index;
},
_maskClick(e) {
//點閉遮罩層是否關(guān)閉
this.maskClose ? this._cancelClick(e) : "";
},
_cancelClick(e) {
//點擊取消,關(guān)閉退出
//恢復(fù)狀態(tài)
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監(jiān)聽的是this.value证逻,
//沒有監(jiān)聽this.value的子項,所以直接改變子項不會觸發(fā)更新
//newValue.splice(index, 1, value);//先移除再添加
//this.$emit('input', newValue);
this.newValue = newValue;
}
//bool=false時是初始時設(shè)置的
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;//取到的是整數(shù)
this.liHeight = parseFloat(window.getComputedStyle(li, null).height); //取到的精確到小數(shù)
}
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設(shè)定顯示的高度*/
}
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, //當(dāng)前移動的距離
};
},
watch: {
height() {
//父組件mounted后更新了height的高度囚企,這里將數(shù)據(jù)移動到指定位置
this._moveTo();
},
data() {
//在聯(lián)動時,數(shù)據(jù)變化了瑞眼,下級還會保持在上一次的移動位置
this._moveTo();
},
},
props: {
height: Number, //移動單位的高度
data: Array,
change: Function,
value: String, //選中的值
index: Number, //當(dāng)前索引龙宏,多個選擇時如聯(lián)動時,指向的是第幾個選擇伤疙,在change時返回去區(qū)別哪項改變了
},
components: {},
methods: {
_getTouch(event) {
return event.changedTouches[0] || event.touches[0];
},
_getVisibleCount() {
//取顯示條數(shù)的一半烦衣,因為選中的在中間,顯示條數(shù)為奇數(shù)
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
//如果當(dāng)前選中的為disabled狀態(tài)衅澈,則往下選擇,僅在滑動選擇時判斷谬墙,默認填值時不作判斷
//存在數(shù)據(jù)加載問題今布,有可能初始時數(shù)據(jù)是空的
if (this.data.length > 0) {
bool ? (index = this._isDisabled(index, index)) : "";
this.offset = (this._getVisibleCount() - index) * this.height;
//回調(diào)
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; //到最后一條時经备,再從第一條開始找
}
//防止死循環(huán),全都是disabled時部默,原路返回
if (index + 1 == index2) {
return index2;
}
return this._isDisabled(index + 1);
}
return index;
},
_moveTo() {
//根據(jù)value移動動相對應(yīng)的位置侵蒙,這個是組件加載完引用
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不存在于數(shù)據(jù)數(shù)組中時index=0
},
},
computed: {
pickerMask() {
return {
//設(shè)定過度遮罩的顯示高度,即總顯示個數(shù)減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設(shè)定顯示的高度*/
}
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>