未验证 提交 2b777a43 编写于 作者: A Amy 提交者: GitHub

[reconstruction] time-picker (#185)

* [reconstruction] time-picker

* [update] time-picker: use cascade-picker to keep value as same as the one before change

* [fix] minuteIndex

* [update] example

* [add] test

* [update] picker: merge the watch handlers of data and selectedIndex into one

* [update] spell correct

* [feat] support config the text of now

* [optimize]

* [doc]
上级 089d7cec
......@@ -145,7 +145,7 @@ __Notice:__ Cause this component used create-api, so you should read [create-api
| - | - | - | - |
| delay | minutes that postponed backwards from now, which determines the minimal optional time| Number | 15 |
| day | date configuration | Object | { len: 3, filter: ['今日'], format: 'M月D日' } |
| showNow | whether to display current time | Boolean | true |
| showNow | whether to display now; configure the text of option now<sup>1.9.0</sup> | Boolean, Object<sup>1.9.0</sup> | true |
| minuteStep | step of the minute | Number | 10 |
| title | title | String | '选择时间' |
| subtitle<sup>1.8.1</sup> | subtitle | String | '' |
......@@ -162,6 +162,12 @@ __Notice:__ Cause this component used create-api, so you should read [create-api
| filter | date column, map time to the text in filter | Array | ['今日'] |
| format | format time | String | 'M月D日' |
* `showNow` sub configuration
| Attribute | Description | Type | Default |
| - | - | - | - |
| text<sup>1.9.0</sup> | the text of option now | String | '现在' |
### Events
| Event Name | Description | Parameters 1 | Parameters 2 |
......
......@@ -135,7 +135,7 @@ __注:__ 由于此组件基于 create-api 实现,所以在使用之前,请
| - | - | - | - |
| delay | 将当前时间向后推算的分钟数,决定了最小可选时间 | Number | 15 |
| day | 日期配置 | Object | { len: 3, filter: ['今日'], format: 'M月D日' } |
| showNow | 是否显示当前时间 | Boolean | true |
| showNow | 是否显示现在;以及现在选项的文案<sup>1.9.0</sup> | Boolean, Object<sup>1.9.0</sup> | true |
| minuteStep | 分钟数的步长 | Number | 10 |
| title | 标题 | String | '选择时间' |
| subtitle<sup>1.8.1</sup> | 副标题 | String | '' |
......@@ -144,7 +144,7 @@ __注:__ 由于此组件基于 create-api 实现,所以在使用之前,请
| swipeTime | 快速滑动选择器滚轮时,惯性滚动动画的时长,单位:ms | Number | 2500 |
| visible<sup>1.8.1</sup> | 显示状态,是否可见。`v-model`绑定值 | Boolean | false |
* `day`子配置项
* `day` 子配置项
| 参数 | 说明 | 类型 | 默认值 |
| - | - | - | - |
......@@ -152,6 +152,12 @@ __注:__ 由于此组件基于 create-api 实现,所以在使用之前,请
| filter | 日期列,将时间映射为filter中的文案内容 | Array | ['今日'] |
| format | 时间格式化 | String | 'M月D日' |
* `showNow` 子配置项
| 参数 | 说明 | 类型 | 默认值 |
| - | - | - | - |
| text<sup>1.9.0</sup> | 现在选项的文案 | String | '现在' |
### 事件
| 事件名 | 说明 | 参数1 | 参数2 |
......
......@@ -2,9 +2,9 @@
<cube-page type="time-picker-view" title="TimePicker(时间选择器)">
<div slot="content">
<cube-button-group>
<cube-button @click="selectTime">TimePicker</cube-button>
<cube-button @click="selectTimeDay">TimePicker - day options</cube-button>
<cube-button @click="selectTimeSetTime">TimePicker - setTime(next hour)</cube-button>
<cube-button @click="showTimePicker">TimePicker</cube-button>
<cube-button @click="showConfigDayPicker">Config day options</cube-button>
<cube-button @click="showSetTimePiker">Use setTime</cube-button>
</cube-button-group>
</div>
</cube-page>
......@@ -14,93 +14,63 @@
import CubePage from '../components/cube-page.vue'
import CubeButtonGroup from '../components/cube-button-group.vue'
export default {
components: {
CubePage,
CubeButtonGroup
},
methods: {
selectTime() {
this.timePicker = this.$createTimePicker({
showNow: true,
minuteStep: 5,
delay: 15,
onSelect: (selectedTime, selectedText) => {
this.$createDialog({
type: 'warn',
title: `选中的时间戳是 ${selectedTime}`,
content: `选中的内容是 ${selectedText}`,
icon: 'cubeic-alert'
}).show()
},
onCancel: () => {
this.$createToast({
type: 'correct',
txt: 'Picker canceled',
time: 1000
}).show()
}
})
showTimePicker() {
if (!this.timePicker) {
this.timePicker = this.$createTimePicker({
onSelect: this.selectHandler,
onCancel: this.cancelHandler
})
}
this.timePicker.show()
},
selectTimeDay() {
this.timePicker = this.$createTimePicker({
showNow: true,
minuteStep: 10,
delay: 10,
day: {
len: 5,
filter: ['今天', '明天'],
format: 'M月d日'
},
onSelect: (selectedTime, selectedText) => {
this.$createDialog({
type: 'warn',
title: `选中的时间戳是 ${selectedTime}`,
content: `选中的内容是 ${selectedText}`,
icon: 'cubeic-alert'
}).show()
},
onCancel: () => {
this.$createToast({
type: 'correct',
txt: 'Picker canceled',
time: 1000
}).show()
}
})
this.timePicker.show()
showConfigDayPicker() {
if (!this.configDayPicker) {
this.configDayPicker = this.$createTimePicker({
minuteStep: 5,
delay: 10,
day: {
len: 5,
filter: ['今天', '明天'],
format: 'M月D日'
},
onSelect: this.selectHandler,
onCancel: this.cancelHandler
})
}
this.configDayPicker.show()
},
selectTimeSetTime() {
const time = new Date().valueOf() + 1 * 60 * 60 * 1000
this.timePicker = this.$createTimePicker({
showNow: true,
minuteStep: 10,
delay: 15,
day: {
len: 5,
filter: ['今天', '明天', '后天'],
format: 'M月d日'
},
onSelect: (selectedTime, selectedText) => {
this.$createDialog({
type: 'warn',
title: `选中的时间戳是 ${selectedTime}`,
content: `选中的内容是 ${selectedText}`,
icon: 'cubeic-alert'
}).show()
},
onCancel: () => {
this.$createToast({
type: 'correct',
txt: 'Picker canceled',
time: 1000
}).show()
}
})
showSetTimePiker() {
if (!this.setTimePiker) {
this.setTimePiker = this.$createTimePicker({
onSelect: this.selectHandler,
onCancel: this.cancelHandler
})
}
this.timePicker.setTime(time)
this.timePicker.show()
const time = new Date().valueOf() + 1 * 60 * 60 * 1000 * 24
this.setTimePiker.setTime(time)
this.setTimePiker.show()
},
selectHandler(selectedTime, selectedText) {
this.$createDialog({
type: 'warn',
title: `选中的时间戳是 ${selectedTime}`,
content: `选中的内容是 ${selectedText}`,
icon: 'cubeic-alert'
}).show()
},
cancelHandler() {
this.$createToast({
type: 'correct',
txt: 'Picker canceled',
time: 1000
}).show()
}
},
components: {
CubePage,
CubeButtonGroup
}
}
</script>
......@@ -47,11 +47,15 @@ function formatDate(date, format, regExpAttributes) {
return format
}
function getZeroDate(date) {
function getZeroStamp(date) {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return new Date(year + '/' + month + '/' + day + ' 00:00:00')
return +new Date(year + '/' + month + '/' + day + ' 00:00:00')
}
function getDayDiff(date1, date2) {
return Math.floor((getZeroStamp(date1) - getZeroStamp(date2)) / DAY_TIMESTAMP)
}
function getNow() {
......@@ -78,7 +82,8 @@ export {
pad,
formatType,
formatDate,
getZeroDate,
getZeroStamp,
getDayDiff,
getNow,
computeNatureMaxDay
}
......@@ -30,14 +30,15 @@ export default {
},
textKey() {
return this.alias.text || DEFAULT_KEYS.text
},
merge() {
return [this.data, this.selectedIndex]
}
},
watch: {
data(newVal) {
this.setData(newVal, this.selectedIndex)
},
selectedIndex(newVal) {
this.setData(this.data, newVal)
// Merge the watch handlers of data and selectedIndex into one.
merge(newVal) {
this.setData(newVal[0], newVal[1])
}
}
}
<template>
<cube-picker
<cube-cascade-picker
ref="picker"
v-model="isVisible"
:data="data"
:data="cascadeData"
:selected-index="selectedIndex"
:title="title"
:subtitle="subtitle"
......@@ -12,13 +12,16 @@
:z-index="zIndex"
@select="_pickerSelect"
@cancel="_pickerCancel"
@change="_pickerChange"></cube-picker>
@change="_pickerChange">
</cube-cascade-picker>
</template>
<script type="text/ecmascript-6">
import {
getZeroDate,
pad,
formatDate,
getZeroStamp,
getDayDiff,
DAY_TIMESTAMP,
HOUR_TIMESTAMP,
MINUTE_TIMESTAMP
......@@ -26,37 +29,29 @@
import visibilityMixin from '../../common/mixins/visibility'
import popupMixin from '../../common/mixins/popup'
import pickerMixin from '../../common/mixins/picker'
import CubePicker from '../picker/picker.vue'
const DAY_STEP = 1
const MAX_HOUR = 23
const MAX_MINUTE = 60
const HOUR_STEP = 1
const MINUTE_STEP = 10
import CubeCascadePicker from '../cascade-picker/cascade-picker.vue'
const COMPONENT_NAME = 'cube-time-picker'
const EVENT_SELECT = 'select'
const EVENT_CANCEL = 'cancel'
const EVENT_CHANGE = 'change'
function formatNum(num) {
return ('' + num).length > 1 ? num : ('0' + num)
}
function roundMinute(minute, step) {
return Math.ceil(minute / step) * step
const NOW = {
value: 'now',
defaultText: '现在'
}
export default {
name: COMPONENT_NAME,
mixins: [visibilityMixin, popupMixin, pickerMixin],
components: {
CubeCascadePicker
},
props: {
title: {
type: String,
default: '选择时间'
},
// delay is valid when less than (the minute left in today + 1440).
// So, it will be security when less than 1440.
delay: {
type: Number,
default: 15
......@@ -72,283 +67,154 @@
}
},
showNow: {
type: Boolean,
type: [Boolean, Object],
default: true
},
minuteStep: {
type: Number,
default: MINUTE_STEP
default: 10
}
},
data() {
return {
selectedDayIndex: 0,
selectedHourIndex: 0,
selectedMinuteIndex: 0,
days: [],
hours: [],
minutes: [],
minTime: 0
now: new Date(),
selectedIndex: [0, 0, 0],
value: 0
}
},
computed: {
data() {
return [this.days, this.hours, this.minutes]
},
selectedIndex() {
return [this.selectedDayIndex, this.selectedHourIndex, this.selectedMinuteIndex]
}
},
created() {
this.selectedTimeStamp = null
},
methods: {
show() {
if (this.isVisible) {
return
}
this.isVisible = true
this._updateMinTime()
this._initDays()
this.today = this.days[0].value
// make sure picker render before call refillColumn
this.$nextTick(() => {
this.selectedDayIndex = this.$refs.picker.refillColumn(0, this.days)
this._handleHourAndMinute(true)
this._resetTime()
})
},
setTime(timeStamp) {
this.selectedTimeStamp = parseInt(timeStamp)
nowText() {
return (this.showNow && this.showNow.text) || NOW.defaultText
},
_resetTime() {
if (!this.selectedTimeStamp) {
return
}
const now = new Date()
const currentTimestamp = now.getTime()
let resetToCurrent = false
if (this.selectedTimeStamp < currentTimestamp + this.delay * MINUTE_TIMESTAMP) {
resetToCurrent = true
}
const date = new Date(this.selectedTimeStamp)
this.$nextTick(() => {
this._updateMinTime()
const zeroTimestamp = +getZeroDate(this.minTime)
let dayDiff = resetToCurrent ? 0 : Math.floor((this.selectedTimeStamp - zeroTimestamp) / DAY_TIMESTAMP)
if (dayDiff < this.days.length) {
if (dayDiff !== this.selectedDayIndex) {
this.$refs.picker.scrollTo(0, dayDiff)
this._pickerChange(0, dayDiff)
}
this.$nextTick(() => {
let hourDiff = 0
if (!resetToCurrent) {
if (this.hours[0].value === 'now') {
hourDiff = Math.floor((date.getHours() - this.hours[1].value) / HOUR_STEP) + 1
} else {
hourDiff = Math.floor((date.getHours() - this.hours[0].value) / HOUR_STEP)
}
}
if (hourDiff !== this.selectedHourIndex) {
this.$refs.picker.scrollTo(1, hourDiff)
this._pickerChange(1, hourDiff)
}
if (!resetToCurrent) {
this.$nextTick(() => {
let minuteDiff = 0
if (this.minutes.length) {
minuteDiff = Math.floor((date.getMinutes() - this.minutes[0].value) / this.minuteStep)
}
if (minuteDiff !== this.selectedMinuteIndex) {
this.$refs.picker.scrollTo(2, minuteDiff)
this._pickerChange(2, minuteDiff)
}
})
}
})
}
this.selectedTimeStamp = null
})
minTime() {
return new Date(+this.now + this.delay * MINUTE_TIMESTAMP)
},
_updateMinTime() {
this.minTime = new Date(+new Date() + this.delay * MINUTE_TIMESTAMP)
},
_initDays() {
days() {
const days = []
const dayConf = this.day
const zeroTimestamp = +getZeroDate(new Date())
for (let i = 0; i < dayConf.len; i += DAY_STEP) {
const timestamp = zeroTimestamp + i * DAY_TIMESTAMP
const dayDiff = getDayDiff(this.minTime, this.now)
if (dayConf.filter && i < dayConf.filter.length) {
days.push({
value: timestamp,
text: dayConf.filter[i]
})
} else {
days.push({
value: timestamp,
text: formatDate(new Date(timestamp), dayConf.format, 'i')
})
}
for (let i = dayDiff; i < this.day.len; i++) {
const timestamp = +this.minTime + i * DAY_TIMESTAMP
days.push({
value: timestamp,
text: (this.day.filter && this.day.filter[dayDiff + i]) || formatDate(new Date(timestamp), this.day.format, 'i')
})
}
this.days = days
return days
},
_initHours(begin) {
hours() {
const hours = []
if (this.showNow && this.selectedDayIndex === 0) {
for (let i = 0; i < 24; i++) {
hours.push({
value: 'now',
text: '现在'
value: i,
text: i + '',
children: this.minutes
})
}
for (let i = begin; i <= MAX_HOUR; i += HOUR_STEP) {
hours.push({
value: i,
text: i + ''
return hours
},
partHours() {
const partHours = this.hours.slice(this.minTime.getHours())
partHours[0] = Object.assign({}, partHours[0], {children: this.partMinutes})
if (this.showNow) {
partHours.unshift({
value: NOW.value,
text: this.nowText,
children: []
})
}
this.hours = hours
return partHours
},
_initMinutes(begin) {
if (begin === false) {
this.minutes = []
} else {
const minutes = []
const step = this.minuteStep
const max = 60 - step
begin = begin % 60
for (let i = begin; i <= max; i += step) {
minutes.push({
value: i,
text: formatNum(i) + ''
})
}
this.minutes = minutes
minutes() {
const minutes = []
for (let i = 0; i < 60; i += this.minuteStep) {
minutes.push({
value: i,
text: pad(i) + ''
})
}
return minutes
},
_handleHourAndMinute(inited) {
let beginHour = 0
let beginMinute = 0
let moreThanOne = false
if (this.today + DAY_TIMESTAMP < this.minTime) {
moreThanOne = true
if (this.showNow && this.selectedDayIndex === 0) {
beginHour = 24
}
} else {
beginHour = this.minTime.getHours()
if (this.minTime.getMinutes() > MAX_MINUTE - this.minuteStep) {
beginHour += 1
if (beginHour === 24) {
moreThanOne = true
}
}
partMinutes() {
const begin = Math.floor(this.minTime.getMinutes() / this.minuteStep)
return this.minutes.slice(begin)
},
cascadeData() {
const data = this.days.slice()
data.forEach((item, index) => {
item.children = index ? this.hours : this.partHours
})
return data
}
},
methods: {
show() {
if (this.isVisible) {
return
}
this.isVisible = true
// smaller than min time
if (this.days[this.selectedDayIndex].value < this.minTime) {
if (!this.showNow && moreThanOne && inited) {
beginHour = 0
this.days.shift()
this.selectedDayIndex = this.$refs.picker.refillColumn(0, this.days)
}
this._updateNow()
this._updateSelectedIndex()
},
setTime(value) {
this.value = value
this._initHours(beginHour)
this.selectedHourIndex = this.$refs.picker.refillColumn(1, this.hours)
this.isVisible && this._updateSelectedIndex()
},
_updateSelectedIndex() {
const value = this.value
const minTime = this.minTime
let distHour = this.hours[this.selectedHourIndex].value
if (distHour === beginHour) {
beginMinute = roundMinute(this.minTime.getMinutes() + 1, this.minuteStep)
}
// today now
if (this.selectedDayIndex === 0 && this.selectedHourIndex === 0 && this.showNow) {
beginMinute = false
}
this._initMinutes(beginMinute)
this.selectedMinuteIndex = this.$refs.picker.refillColumn(2, this.minutes)
if (value <= +minTime) {
this.selectedIndex = [0, 0, 0]
} else {
if (!this.showNow && moreThanOne && inited) {
this.days.shift()
this.selectedDayIndex = this.$refs.picker.refillColumn(0, this.days)
// calculate dayIndex
const valueDate = new Date(value)
const dayIndex = getDayDiff(valueDate, minTime)
if (dayIndex >= this.days.length) {
// TODO: add cube warn
return
}
beginHour = 0
this._initHours(beginHour)
this._initMinutes(beginMinute)
const refillRet = this.$refs.picker.refill([this.days, this.hours, this.minutes])
this.selectedHourIndex = refillRet[1]
this.selectedMinuteIndex = refillRet[2]
// calculate hourIndex
const hour = valueDate.getHours()
const beginHour = dayIndex === 0
? this.showNow ? this.minTime.getHours() - 1 : this.minTime.getHours()
: 0
const hourIndex = hour - beginHour
// calculate minuteIndex
const minute = Math.floor(valueDate.getMinutes() / this.minuteStep)
const beginMinute = !dayIndex && (this.showNow ? hourIndex === 1 : !hourIndex)
? Math.floor(this.minTime.getMinutes() / this.minuteStep)
: 0
const minuteIndex = minute - beginMinute
this.selectedIndex = [dayIndex, hourIndex, minuteIndex]
}
},
_handleMinute() {
if (this.days[this.selectedDayIndex].value - +this.minTime < 0) {
let beginMinute = 0
let beginHour = this.minTime.getHours()
if (this.hours[this.selectedHourIndex].value === beginHour) {
beginMinute = roundMinute(this.minTime.getMinutes() + 1, this.minuteStep)
}
// today now
if (this.selectedDayIndex === 0 && this.selectedHourIndex === 0 && this.showNow) {
beginMinute = false
}
this._initMinutes(beginMinute)
this.selectedMinuteIndex = this.$refs.picker.refillColumn(2, this.minutes)
}
_updateNow() {
this.now = new Date()
},
_getSelect() {
let selectedTime
let selectedText
if (this.selectedDayIndex === 0 && this.selectedHourIndex === 0 && this.showNow) {
selectedTime = +new Date()
selectedText = this.hours[0].text
_pickerChange(i, newIndex) {
this.$emit(EVENT_CHANGE, i, newIndex)
},
_pickerSelect(selectedVal, selectedIndex, selectedText) {
if (selectedVal[1] === NOW.value) {
this.$emit(EVENT_SELECT, +new Date(), this.nowText)
} else {
selectedTime = this.days[this.selectedDayIndex].value +
this.hours[this.selectedHourIndex].value * HOUR_TIMESTAMP +
this.minutes[this.selectedMinuteIndex].value * MINUTE_TIMESTAMP
selectedText = this.days[this.selectedDayIndex].text + ' ' +
this.hours[this.selectedHourIndex].text + ':' +
this.minutes[this.selectedMinuteIndex].text
}
return {
selectedTime,
selectedText
const timestamp = getZeroStamp(new Date(selectedVal[0])) + selectedVal[1] * HOUR_TIMESTAMP + selectedVal[2] * MINUTE_TIMESTAMP
const text = selectedText[0] + ' ' + selectedText[1] + ':' + selectedText[2]
this.value = timestamp
this.$emit(EVENT_SELECT, timestamp, text)
}
},
_pickerSelect(selectedVal, selectedIndex) {
this.selectedDayIndex = selectedIndex[0]
this.selectedHourIndex = selectedIndex[1]
this.selectedMinuteIndex = selectedIndex[2]
let {selectedTime, selectedText} = this._getSelect()
this.$emit(EVENT_SELECT, selectedTime, selectedText)
},
_pickerCancel() {
this.$emit(EVENT_CANCEL)
},
_pickerChange(index, selectedIndex) {
this._updateMinTime()
if (index === 0) {
this.selectedDayIndex = selectedIndex
this._handleHourAndMinute(false)
} else if (index === 1) {
this.selectedHourIndex = selectedIndex
this._handleMinute()
} else {
this.selectedMinuteIndex = selectedIndex
}
let {selectedTime, selectedText} = this._getSelect()
this.$emit(EVENT_CHANGE, selectedTime, selectedText)
}
},
components: {
CubePicker
}
}
</script>
import Vue from 'vue2'
import TimePicker from '@/modules/time-picker'
import instantiateComponent from '@/common/helpers/instantiate-component'
import { dispatchSwipe } from '../utils/event'
describe('TimePicker', () => {
let vm
......@@ -96,7 +97,26 @@ describe('TimePicker', () => {
}, 100)
})
it('should trigger events', function () {
it('should render correct contents - showNow text', function (done) {
vm = createPicker({
showNow: {
text: 'now text'
}
})
vm.show()
setTimeout(() => {
const wheels = vm.$el.querySelectorAll('.cube-picker-wheel-wrapper > div')
const secondWheelItems = wheels[1].querySelectorAll('li')
expect(secondWheelItems[0].textContent.trim())
.to.equal('now text')
done()
}, 100)
})
it('should trigger events', function (done) {
this.timeout(10000)
const selectHandle = sinon.spy()
......@@ -109,27 +129,71 @@ describe('TimePicker', () => {
}
vm = createPicker({}, events)
return new Promise((resolve) => {
vm.show()
// cancel
vm.show()
const cancelBtn = vm.$el.querySelector('.cube-picker-cancel')
cancelBtn.click()
expect(cancelHandle)
.to.be.callCount(1)
// change
vm.show()
setTimeout(() => {
const wheels = vm.$el.querySelectorAll('.cube-picker-wheel-wrapper > div')
const firstWheelItems = wheels[0].querySelectorAll('li')
dispatchSwipe(firstWheelItems[1], [
{
pageX: firstWheelItems[1].offsetLeft + 10,
pageY: firstWheelItems[1].offsetTop + 10
},
{
pageX: 300,
pageY: 380
}
], 100)
setTimeout(() => {
const cancelBtn = vm.$el.querySelector('.cube-picker-cancel')
cancelBtn.click()
expect(cancelHandle)
expect(changeHandle)
.to.be.callCount(1)
vm.show()
setTimeout(() => {
const confirmBtn = vm.$el.querySelector('.cube-picker-confirm')
confirmBtn.click()
const now = +new Date()
expect(selectHandle)
.to.be.callCount(1)
expect(selectHandle.args[0][0])
.to.be.closeTo(now, 2)
resolve()
}, 100)
}, 100)
// select: now
const confirmBtn = vm.$el.querySelector('.cube-picker-confirm')
confirmBtn.click()
const now = +new Date()
expect(selectHandle)
.to.be.callCount(1)
expect(selectHandle.args[0][0])
.to.be.closeTo(now, 2)
done()
}, 1000)
}, 100)
})
it('should have correct args when select normal time', function (done) {
const selectHandle = sinon.spy()
vm = createPicker({
showNow: false,
delay: 0,
minuteStep: 1
}, {
select: selectHandle
})
// select: now
vm.show()
setTimeout(() => {
const confirmBtn = vm.$el.querySelector('.cube-picker-confirm')
confirmBtn.click()
const now = +new Date()
expect(selectHandle)
.to.be.callCount(1)
expect(selectHandle.args[0][0])
.to.be.closeTo(now, 60 * 1000)
done()
}, 100)
})
it('should add warn log when sigle is true', () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册