<template>
|
<uni-picker
|
:disabled="disabled"
|
@click="_show"
|
v-on="$listeners"
|
>
|
<div
|
ref="picker"
|
class="uni-picker-container"
|
:class="`uni-${mode}-${selectorTypeComputed}`"
|
@wheel.prevent
|
@touchmove.prevent
|
>
|
<transition name="uni-fade">
|
<div
|
v-show="visible"
|
class="uni-mask uni-picker-mask"
|
@click="_cancel"
|
@mousemove="_fixInputPosition"
|
/>
|
</transition>
|
<div
|
v-if="!system"
|
:class="{ 'uni-picker-toggle': visible }"
|
:style="popupStyle.content"
|
class="uni-picker-custom"
|
>
|
<div
|
class="uni-picker-header"
|
@click.stop
|
>
|
<div
|
class="uni-picker-action uni-picker-action-cancel"
|
@click="_cancel"
|
>
|
{{ $$t("uni.picker.cancel") }}
|
</div>
|
<div
|
class="uni-picker-action uni-picker-action-confirm"
|
@click="_change"
|
>
|
{{ $$t("uni.picker.done") }}
|
</div>
|
</div>
|
<v-uni-picker-view
|
v-if="contentVisible"
|
:value="_l10nColumn(valueArray)"
|
class="uni-picker-content"
|
@change="_pickerViewChange"
|
>
|
<v-uni-picker-view-column
|
v-for="(rangeItem, index0) in _l10nColumn(rangeArray)"
|
:key="index0"
|
>
|
<div
|
v-for="(item, index) in rangeItem"
|
:key="index"
|
class="uni-picker-item"
|
>
|
{{
|
typeof item === "object"
|
? item[rangeKey] || ""
|
: _l10nItem(item, index0)
|
}}
|
</div>
|
</v-uni-picker-view-column>
|
</v-uni-picker-view>
|
<div
|
ref="select"
|
class="uni-picker-select"
|
@wheel.stop
|
@touchmove.stop
|
>
|
<div
|
v-for="(item, index) in rangeArray[0]"
|
:key="index"
|
class="uni-picker-item"
|
:class="{ selected: valueArray[0] === index }"
|
@click="
|
valueArray[0] = index;
|
_change();
|
"
|
>
|
{{ typeof item === "object" ? item[rangeKey] || "" : item }}
|
</div>
|
</div>
|
<div :style="popupStyle.triangle" />
|
</div>
|
</div>
|
<div>
|
<slot />
|
</div>
|
<div
|
v-if="system"
|
class="uni-picker-system"
|
@mousemove="_fixInputPosition"
|
>
|
<input
|
ref="input"
|
:value="valueSync"
|
:type="mode"
|
tabindex="-1"
|
:min="start"
|
:max="end"
|
:class="[system, popupStyle.dock]"
|
@change.stop="_input"
|
>
|
</div>
|
<keypress
|
:disable="!visible"
|
@esc="_cancel"
|
@enter="_change"
|
/>
|
</uni-picker>
|
</template>
|
|
<script>
|
import { emitter } from 'uni-mixins'
|
import { formatDateTime } from 'uni-shared'
|
import popup from '../../../components/app/popup/mixins/popup'
|
import keypress from '../../../helpers/keypress'
|
import {
|
i18nMixin,
|
getLocale
|
} from 'uni-core/helpers/i18n'
|
|
function getDefaultStartValue () {
|
if (this.mode === mode.TIME) {
|
return '00:00'
|
}
|
if (this.mode === mode.DATE) {
|
const year = new Date().getFullYear() - 150
|
switch (this.fields) {
|
case fields.YEAR:
|
return year.toString()
|
case fields.MONTH:
|
return year + '-01'
|
default:
|
return year + '-01-01'
|
}
|
}
|
return ''
|
}
|
|
function getDefaultEndValue () {
|
if (this.mode === mode.TIME) {
|
return '23:59'
|
}
|
if (this.mode === mode.DATE) {
|
const year = new Date().getFullYear() + 150
|
switch (this.fields) {
|
case fields.YEAR:
|
return year.toString()
|
case fields.MONTH:
|
return year + '-12'
|
default:
|
return year + '-12-31'
|
}
|
}
|
return ''
|
}
|
|
function getYearStartEnd (props) {
|
const year = new Date().getFullYear()
|
let start = year - 150
|
let end = year + 150
|
if (props.start) {
|
const _year = new Date(props.start).getFullYear()
|
if (!isNaN(_year) && _year < start) {
|
start = _year
|
}
|
}
|
if (props.end) {
|
const _year = new Date(props.end).getFullYear()
|
if (!isNaN(_year) && _year > end) {
|
end = _year
|
}
|
}
|
|
return {
|
start,
|
end
|
}
|
}
|
|
const mode = {
|
SELECTOR: 'selector',
|
MULTISELECTOR: 'multiSelector',
|
TIME: 'time',
|
DATE: 'date'
|
// 暂不支持城市选择
|
// REGION: 'region'
|
}
|
const fields = {
|
YEAR: 'year',
|
MONTH: 'month',
|
DAY: 'day'
|
}
|
const selectorType = {
|
PICKER: 'picker',
|
SELECT: 'select'
|
}
|
export default {
|
name: 'Picker',
|
components: { keypress },
|
mixins: [i18nMixin, emitter, popup],
|
props: {
|
name: {
|
type: String,
|
default: ''
|
},
|
range: {
|
type: Array,
|
default () {
|
return []
|
}
|
},
|
rangeKey: {
|
type: String,
|
default: ''
|
},
|
value: {
|
type: [Number, String, Array],
|
default: 0
|
},
|
mode: {
|
type: String,
|
default: mode.SELECTOR,
|
validator (val) {
|
return Object.values(mode).includes(val)
|
}
|
},
|
fields: {
|
type: String,
|
default: ''
|
},
|
start: {
|
type: String,
|
default: getDefaultStartValue
|
},
|
end: {
|
type: String,
|
default: getDefaultEndValue
|
},
|
disabled: {
|
type: [Boolean, String],
|
default: false
|
},
|
selectorType: {
|
type: String,
|
default: ''
|
}
|
},
|
data () {
|
return {
|
valueSync: null,
|
visible: false,
|
contentVisible: false,
|
popover: null,
|
valueChangeSource: '',
|
timeArray: [],
|
dateArray: [],
|
valueArray: [],
|
oldValueArray: []
|
}
|
},
|
computed: {
|
rangeArray () {
|
var val = this.range
|
switch (this.mode) {
|
case mode.SELECTOR:
|
return [val]
|
case mode.MULTISELECTOR:
|
return val
|
case mode.TIME:
|
return this.timeArray
|
case mode.DATE:
|
{
|
const dateArray = this.dateArray
|
switch (this.fields) {
|
case fields.YEAR:
|
return [dateArray[0]]
|
case fields.MONTH:
|
return [dateArray[0], dateArray[1]]
|
default:
|
return [dateArray[0], dateArray[1], dateArray[2]]
|
}
|
}
|
}
|
return []
|
},
|
startArray () {
|
return this._getDateValueArray(this.start, getDefaultStartValue.bind(this)())
|
},
|
endArray () {
|
return this._getDateValueArray(this.end, getDefaultEndValue.bind(this)())
|
},
|
selectorTypeComputed () {
|
const type = this.selectorType
|
if (Object.values(selectorType).includes(type)) {
|
return type
|
}
|
return String(navigator.vendor).indexOf('Apple') === 0 && navigator.maxTouchPoints > 0 ? selectorType.PICKER : selectorType.SELECT
|
},
|
system () {
|
if (this.mode === mode.DATE && !Object.values(fields).includes(this.fields) && this.isDesktop && /win|mac/i.test(navigator.platform)) {
|
if (navigator.vendor === 'Google Inc.') {
|
return 'chrome'
|
} else if (/Firefox/.test(navigator.userAgent)) {
|
return 'firefox'
|
}
|
}
|
return ''
|
}
|
},
|
watch: {
|
visible (val) {
|
if (val) {
|
clearTimeout(this.__contentVisibleDelay)
|
this.contentVisible = val
|
this._select()
|
} else {
|
this.__contentVisibleDelay = setTimeout(() => {
|
this.contentVisible = val
|
}, 300)
|
}
|
},
|
value () {
|
this._setValueSync()
|
},
|
mode () {
|
this._setValueSync()
|
},
|
range () {
|
this._setValueSync()
|
},
|
valueSync () {
|
this._setValueArray()
|
},
|
valueArray (val) {
|
if (this.mode === mode.TIME || this.mode === mode.DATE) {
|
const getValue =
|
this.mode === mode.TIME ? this._getTimeValue : this._getDateValue
|
const valueArray = this.valueArray
|
const startArray = this.startArray
|
const endArray = this.endArray
|
if (this.mode === mode.DATE) {
|
const dateArray = this.dateArray
|
const max = dateArray[2].length
|
const day = Number(dateArray[2][valueArray[2]]) || 1
|
const realDay = new Date(
|
`${dateArray[0][valueArray[0]]}/${dateArray[1][valueArray[1]]
|
}/${day}`
|
).getDate()
|
if (realDay < day) {
|
valueArray[2] -= realDay + max - day
|
}
|
}
|
if (getValue(valueArray) < getValue(startArray)) {
|
this._cloneArray(valueArray, startArray)
|
} else if (getValue(valueArray) > getValue(endArray)) {
|
this._cloneArray(valueArray, endArray)
|
}
|
}
|
val.forEach((value, column) => {
|
if (value !== this.oldValueArray[column]) {
|
this.oldValueArray[column] = value
|
if (this.mode === mode.MULTISELECTOR) {
|
this.$trigger('columnchange', {}, {
|
column,
|
value
|
}
|
)
|
}
|
}
|
})
|
}
|
},
|
created () {
|
this.$dispatch('Form', 'uni-form-group-update', {
|
type: 'add',
|
vm: this
|
})
|
this._createTime()
|
this._createDate()
|
this._setValueSync()
|
},
|
beforeDestroy () {
|
this.$refs.picker.remove()
|
this.$dispatch('Form', 'uni-form-group-update', {
|
type: 'remove',
|
vm: this
|
})
|
},
|
methods: {
|
_show (event) {
|
if (this.disabled) {
|
return
|
}
|
this.valueChangeSource = ''
|
var $picker = this.$refs.picker
|
$picker.remove();
|
(document.querySelector('uni-app') || document.body).appendChild($picker)
|
$picker.style.display = 'block'
|
const rect = event.currentTarget.getBoundingClientRect()
|
this.popover = {
|
top: rect.top,
|
left: rect.left,
|
width: rect.width,
|
height: rect.height
|
}
|
setTimeout(() => {
|
this.visible = true
|
}, 20)
|
},
|
_getFormData () {
|
return {
|
value: this.valueSync,
|
key: this.name
|
}
|
},
|
_resetFormData () {
|
switch (this.mode) {
|
case mode.SELECTOR:
|
this.valueSync = 0
|
break
|
case mode.MULTISELECTOR:
|
this.valueSync = this.value.map(val => 0)
|
break
|
case mode.DATE:
|
case mode.TIME:
|
this.valueSync = ''
|
break
|
default:
|
break
|
}
|
},
|
_createTime () {
|
var hours = []
|
var minutes = []
|
hours.splice(0, hours.length)
|
for (let i = 0; i < 24; i++) {
|
hours.push((i < 10 ? '0' : '') + i)
|
}
|
minutes.splice(0, minutes.length)
|
for (let i = 0; i < 60; i++) {
|
minutes.push((i < 10 ? '0' : '') + i)
|
}
|
this.timeArray.push(hours, minutes)
|
},
|
_createDate () {
|
var years = []
|
var year = getYearStartEnd(this)
|
for (let i = year.start, end = year.end; i <= end; i++) {
|
years.push(String(i))
|
}
|
var months = []
|
for (let i = 1; i <= 12; i++) {
|
months.push((i < 10 ? '0' : '') + i)
|
}
|
var days = []
|
for (let i = 1; i <= 31; i++) {
|
days.push((i < 10 ? '0' : '') + i)
|
}
|
this.dateArray.push(years, months, days)
|
},
|
_getTimeValue (val) {
|
return val[0] * 60 + val[1]
|
},
|
_getDateValue (val) {
|
const DAY = 31
|
return val[0] * DAY * 12 + (val[1] || 0) * DAY + (val[2] || 0)
|
},
|
/**
|
* 将右侧数组值同步到左侧(交集部分)
|
*/
|
_cloneArray (val1, val2) {
|
for (let i = 0; i < val1.length && i < val2.length; i++) {
|
val1[i] = val2[i]
|
}
|
},
|
_setValueSync () {
|
let val = this.value
|
switch (this.mode) {
|
case mode.MULTISELECTOR:
|
{
|
if (!Array.isArray(val)) {
|
val = this.valueArray
|
}
|
if (!Array.isArray(this.valueSync)) {
|
this.valueSync = []
|
}
|
const length = this.valueSync.length = Math.max(val.length, this.range.length)
|
for (let index = 0; index < length; index++) {
|
const val0 = Number(val[index])
|
const val1 = Number(this.valueSync[index])
|
const val2 = isNaN(val0) ? (isNaN(val1) ? 0 : val1) : val0
|
const maxVal = this.range[index] ? this.range[index].length - 1 : 0
|
this.valueSync.splice(index, 1, (val2 < 0 || val2 > maxVal) ? 0 : val2)
|
}
|
}
|
break
|
case mode.TIME:
|
case mode.DATE:
|
this.valueSync = String(val)
|
break
|
default: {
|
const valueSync = Number(val)
|
this.valueSync = valueSync < 0 ? 0 : valueSync
|
break
|
}
|
}
|
},
|
_setValueArray () {
|
var val = this.valueSync
|
var valueArray
|
switch (this.mode) {
|
case mode.MULTISELECTOR:
|
valueArray = [...val]
|
break
|
case mode.TIME:
|
valueArray = this._getDateValueArray(val, formatDateTime({
|
mode: mode.TIME
|
}))
|
break
|
case mode.DATE:
|
valueArray = this._getDateValueArray(val, formatDateTime({
|
mode: mode.DATE
|
}))
|
break
|
default:
|
valueArray = [val]
|
break
|
}
|
this.oldValueArray = [...valueArray]
|
this.valueArray = [...valueArray]
|
},
|
_getValue () {
|
var val = this.valueArray
|
switch (this.mode) {
|
case mode.SELECTOR:
|
return val[0]
|
case mode.MULTISELECTOR:
|
return val.map(val => val)
|
case mode.TIME:
|
return this.valueArray
|
.map((val, i) => this.timeArray[i][val])
|
.join(':')
|
case mode.DATE:
|
return this.valueArray
|
.map((val, i) => this.dateArray[i][val])
|
.join('-')
|
}
|
},
|
_getDateValueArray (valueStr, defaultValue) {
|
const splitStr = this.mode === mode.DATE ? '-' : ':'
|
const array = this.mode === mode.DATE ? this.dateArray : this.timeArray
|
let max
|
if (this.mode === mode.TIME) {
|
max = 2
|
} else {
|
switch (this.fields) {
|
case fields.YEAR:
|
max = 1
|
break
|
case fields.MONTH:
|
max = 2
|
break
|
default:
|
max = 3
|
break
|
}
|
}
|
const inputArray = String(valueStr).split(splitStr)
|
let value = []
|
for (let i = 0; i < max; i++) {
|
const val = inputArray[i]
|
value.push(array[i].indexOf(val))
|
}
|
if (value.indexOf(-1) >= 0) {
|
value = defaultValue ? this._getDateValueArray(defaultValue) : value.map(() => 0)
|
}
|
return value
|
},
|
_change () {
|
this._close()
|
this.valueChangeSource = 'click'
|
const value = this._getValue()
|
this.valueSync = Array.isArray(value) ? value.map(val => val) : value
|
this.$trigger('change', {}, {
|
value
|
})
|
},
|
_cancel ($event) {
|
if (this.system === 'firefox') {
|
// Firefox 在 input 同位置区域点击无法隐藏控件
|
const { top, left, width, height } = this.popover
|
const { pageX, pageY } = $event
|
if (pageX > left && pageX < left + width && pageY > top && pageY < top + height) {
|
return
|
}
|
}
|
this._close()
|
this.$trigger('cancel', {}, {})
|
},
|
_close () {
|
this.visible = false
|
setTimeout(() => {
|
var $picker = this.$refs.picker
|
$picker.remove()
|
this.$el.prepend($picker)
|
$picker.style.display = 'none'
|
}, 260)
|
},
|
_select () {
|
if (this.mode === mode.SELECTOR && this.selectorTypeComputed === selectorType.SELECT) {
|
this.$refs.select.scrollTop = this.valueArray[0] * 34
|
}
|
},
|
_input ($event) {
|
this.valueSync = $event.target.value
|
this.$nextTick(() => {
|
this._change()
|
})
|
},
|
_fixInputPosition ($event) {
|
if (this.system === 'chrome') {
|
const rect = this.$el.getBoundingClientRect()
|
const style = this.$refs.input.style
|
const fontSize = 32
|
style.left = `${$event.clientX - rect.left - fontSize * 1.5}px`
|
style.top = `${$event.clientY - rect.top - fontSize * 0.5}px`
|
}
|
},
|
_pickerViewChange (event) {
|
this.valueArray = this._l10nColumn(event.detail.value, true)
|
},
|
_l10nColumn (array, normalize) {
|
if (this.mode === mode.DATE) {
|
const locale = getLocale()
|
if (!locale.startsWith('zh')) {
|
switch (this.fields) {
|
case fields.YEAR:
|
return array
|
case fields.MONTH:
|
return [array[1], array[0]]
|
default:
|
switch (locale) {
|
case 'es':
|
case 'fr':
|
return [array[2], array[1], array[0]]
|
// case 'en':
|
default:
|
return normalize ? [array[2], array[0], array[1]] : [array[1], array[2], array[0]]
|
}
|
}
|
}
|
}
|
return array
|
},
|
_l10nItem (item, index) {
|
if (this.mode === mode.DATE) {
|
const locale = getLocale()
|
if (locale.startsWith('zh')) {
|
const array = ['年', '月', '日']
|
return item + array[index]
|
} else if (this.fields !== fields.YEAR && index === (this.fields !== fields.MONTH && (locale === 'es' || locale === 'fr') ? 1 : 0)) {
|
let array
|
switch (locale) {
|
case 'es':
|
array = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']
|
break
|
case 'fr':
|
array = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre']
|
break
|
// case 'en':
|
default:
|
array = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
break
|
}
|
return array[Number(item) - 1]
|
}
|
}
|
return item
|
}
|
}
|
}
|
</script>
|
|
<style>
|
uni-picker {
|
position: relative;
|
display: block;
|
cursor: pointer;
|
}
|
|
uni-picker[hidden] {
|
display: none;
|
}
|
|
uni-picker[disabled] {
|
cursor: not-allowed;
|
}
|
|
.uni-picker-container {
|
display: none;
|
position: fixed;
|
left: 0;
|
right: 0;
|
top: 0;
|
bottom: 0;
|
box-sizing: border-box;
|
z-index: 999;
|
font-size: 16px;
|
}
|
|
.uni-picker-container .uni-picker-custom * {
|
box-sizing: border-box;
|
}
|
|
.uni-picker-container .uni-picker-custom {
|
position: fixed;
|
left: 0;
|
bottom: 0;
|
transform: translate(0, 100%);
|
backface-visibility: hidden;
|
z-index: 999;
|
width: 100%;
|
background-color: #fff;
|
visibility: hidden;
|
transition: transform 0.3s, visibility 0.3s;
|
}
|
|
.uni-picker-container .uni-picker-custom.uni-picker-toggle {
|
visibility: visible;
|
transform: translate(0, 0);
|
}
|
|
.uni-picker-container .uni-picker-content {
|
position: relative;
|
display: block;
|
width: 100%;
|
height: 238px;
|
background-color: white;
|
}
|
|
.uni-picker-container .uni-picker-item {
|
padding: 0;
|
height: 34px;
|
line-height: 34px;
|
text-align: center;
|
color: #000;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
overflow: hidden;
|
cursor: pointer;
|
}
|
|
.uni-picker-container .uni-picker-header {
|
display: block;
|
position: relative;
|
text-align: center;
|
width: 100%;
|
height: 45px;
|
}
|
|
.uni-picker-container .uni-picker-header:after {
|
content: "";
|
position: absolute;
|
left: 0;
|
bottom: 0;
|
right: 0;
|
height: 1px;
|
clear: both;
|
border-bottom: 1px solid #e5e5e5;
|
color: #e5e5e5;
|
transform-origin: 0 100%;
|
transform: scaleY(0.5);
|
}
|
|
.uni-picker-container .uni-picker-action {
|
display: block;
|
max-width: 50%;
|
top: 0;
|
height: 100%;
|
box-sizing: border-box;
|
padding: 0 14px;
|
font-size: 17px;
|
line-height: 45px;
|
overflow: hidden;
|
cursor: pointer;
|
}
|
|
.uni-picker-container .uni-picker-action.uni-picker-action-cancel {
|
float: left;
|
color: #888;
|
}
|
|
.uni-picker-container .uni-picker-action.uni-picker-action-confirm {
|
float: right;
|
color: #007aff;
|
}
|
|
.uni-picker-container .uni-picker-select {
|
display: none;
|
}
|
|
.uni-picker-system {
|
position: absolute;
|
display: none;
|
display: block;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
}
|
|
.uni-picker-system > input {
|
position: absolute;
|
border: none;
|
height: 100%;
|
opacity: 0;
|
/* Chrome 无效 */
|
cursor: pointer;
|
}
|
|
.uni-picker-system > input.firefox {
|
top: 0;
|
left: 0;
|
width: 100%;
|
}
|
|
.uni-picker-system > input.chrome {
|
/* 日历空白位置宽度 32px */
|
top: 0;
|
left: 0;
|
width: 2em;
|
font-size: 32px;
|
height: 32px;
|
}
|
|
@media screen and (min-width: 500px) and (min-height: 500px) {
|
.uni-mask.uni-picker-mask {
|
background: none;
|
}
|
.uni-picker-container .uni-picker-custom {
|
width: 300px;
|
left: 50%;
|
right: auto;
|
top: 50%;
|
bottom: auto;
|
transform: translate(-50%, -50%);
|
opacity: 0;
|
border-radius: 5px;
|
transition: opacity 0.3s, visibility 0.3s;
|
box-shadow: 0px 0 20px 5px rgba(0, 0, 0, 0.3);
|
}
|
.uni-picker-container .uni-picker-header {
|
border-radius: 5px 5px 0 0;
|
}
|
.uni-picker-container .uni-picker-content {
|
/* transform 用于解决 Safari overflow 失效的问题 */
|
transform: translate(0 0);
|
overflow: hidden;
|
border-radius: 0 0 5px 5px;
|
}
|
.uni-picker-container .uni-picker-custom.uni-picker-toggle {
|
opacity: 1;
|
transform: translate(-50%, -50%);
|
}
|
.uni-selector-select .uni-picker-header,
|
.uni-selector-select .uni-picker-content {
|
display: none;
|
}
|
.uni-selector-select .uni-picker-select {
|
display: block;
|
max-height: 300px;
|
overflow: auto;
|
background-color: white;
|
border-radius: 5px;
|
padding: 6px 0;
|
}
|
.uni-selector-select .uni-picker-item {
|
padding: 0 10px;
|
color: #555555;
|
}
|
.uni-selector-select .uni-picker-item:hover {
|
background-color: #f6f6f6;
|
}
|
.uni-selector-select .uni-picker-item.selected {
|
color: #007aff;
|
}
|
}
|
|
@media (prefers-color-scheme: dark) {
|
.uni-picker-container .uni-picker-content {
|
background-color: var(--UI-BG-2);
|
}
|
.uni-picker-container .uni-picker-action.uni-picker-action-cancel {
|
color: var(--UI-FG-0);
|
}
|
.uni-picker-container .uni-picker-custom {
|
background-color: var(--UI-BG-2);
|
}
|
.uni-selector-select .uni-picker-item {
|
color: var(--UI-FG-0);
|
}
|
.uni-picker-container .uni-picker-header:after {
|
border-bottom-color: var(--UI-FG-3);
|
}
|
}
|
</style>
|