当前位置: 首页>前端>正文

vue 移动端 日期滑动选择器

vue 移动端 日期滑动选择器,第1张
微信截图_20211130113621.png

手拉手 cv教学 复制就能学会

date.vue 父组件
datepicker.vue 儿子组件
picker.vue 孙子组件
item.vue 太孙子组件


vue 移动端 日期滑动选择器,第2张
微信图片_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") {
        //只在月份改变时做联动
        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>

引用原文大佬代码
原文地址 https://github.com/337547038/Vue-Wap


https://www.xamrdz.com/web/2vv1996552.html

相关文章: