hldy_app_mini/component/public/superpicker.vue

532 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view v-if="visible" class="overlay">
<view class="box" :style="boxStyle" >
<view class="header" ref="headerRef">
<view class="title">{{ title }}</view>
<view class="actions">
<button class="btn" @click="cancel">取消</button>
<button class="btn" @click="confirm">确定</button>
</view>
</view>
<picker-view class="picker-view" :style="{
height: pickerHeight + 'px',
'--item-h': ITEM_H + 'px',
'--cols': displayColumns.length || 1
}" :value="normalizedSelectedIndexes" @change="onPickerChange">
<picker-view-column class="picker-view-column" v-for="(col, ci) in displayColumns" :key="ci">
<view v-for="(item, i) in col" :key="i" class="picker-item">{{ item }}</view>
<!-- :style="{ height: ITEM_H + 'px', lineHeight: ITEM_H + 'px' }" -->
</picker-view-column>
</picker-view>
<view class="resize-handle" @touchstart.stop.prevent="onResizeStartTouch"
@mousedown.stop.prevent="onResizeStartMouse">
<view class="grip"></view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick
} from 'vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
title: {
type: String,
default: '请选择'
},
initLeft: {
type: Number,
default: 50
},
initTop: {
type: Number,
default: 100
},
initWidth: {
type: Number,
default: 320
},
initHeight: {
type: Number,
default: 320
},
minWidth: {
type: Number,
default: 180
},
minHeight: {
type: Number,
default: 200
},
maxWidth: {
type: Number,
default: 1000
},
maxHeight: {
type: Number,
default: 1200
},
columns: {
type: Array,
default: () => [
[]
]
},
nameKey: {
type: [String, Array],
default: 'name'
},
value: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'confirm', 'change', 'update:position', 'update:size','close']);
/* ========== 基本可见性 / 同步 ========== */
const visible = ref(props.modelValue);
watch(() => props.modelValue, v => visible.value = v);
watch(visible, v => emit('update:modelValue', v));
/* ========== 位置 / 尺寸 ========== */
const left = ref(props.initLeft);
const top = ref(props.initTop);
const width = ref(props.initWidth);
const height = ref(props.initHeight);
/* ========== 固定行高(关键:整数像素) ========== */
const ITEM_H = 44; // 行高px保持整数像素
const HEADER_H = 44; // header 高度px与你样式一致
/* ========== selectedIndexes 初始值 & 正规化 ========== */
const selectedIndexes = ref(
(props.value && props.value.length) ? props.value.map(v => Number(v || 0)) : (Array.isArray(props.columns) ?
props.columns.map(() => 0) : [])
);
watch(() => props.value, v => {
if (v && v.length) {
selectedIndexes.value = v.map(x => Number(x || 0));
clampSelectedIndexes();
}
});
const normalizedSelectedIndexes = computed(() => selectedIndexes.value.map(v => Number(v || 0)));
/* ========== columns -> displayColumns显示文本 ========== */
function getByPath(obj, path) {
if (obj == null) return undefined;
if (!path) return obj;
const parts = path.split('.');
let cur = obj;
for (let p of parts) {
if (cur == null) return undefined;
cur = cur[p];
}
return cur;
}
const displayColumns = computed(() => {
const raw = props.columns || [];
const nk = props.nameKey;
return raw.map((col, ci) => {
if (!Array.isArray(col)) return [];
return col.map(item => {
if (item == null) return '';
if (typeof item === 'object') {
let keyToUse = Array.isArray(nk) ? (nk[ci] !== undefined ? nk[ci] : nk[0]) :
nk;
if (!keyToUse) {
return item.name ?? item.label ?? item.title ?? String(item);
}
const val = getByPath(item, keyToUse);
return (val === undefined || val === null) ? (item.name ?? item.label ?? item
.title ?? String(item)) : String(val);
} else {
return String(item);
}
});
});
});
const rawColumns = computed(() => (props.columns || []).map(col => Array.isArray(col) ? col : []));
/* ========== 屏幕信息(只用 uni.getSystemInfoSyncApp/H5/小程序 均可用) ========== */
let screenW = 800,
screenH = 600; // 安全默认值(不会访问 window
onMounted(() => {
try {
const info = uni.getSystemInfoSync();
// uni.getSystemInfoSync 在 App/H5/小程序 都可用,优先使用
screenW = info.windowWidth || info.screenWidth || screenW;
screenH = info.windowHeight || info.screenHeight || screenH;
} catch (e) {
// 若异常,不尝试访问 window保留默认值
screenW = screenW;
screenH = screenH;
}
});
/* ========== 拖拽 / 缩放(保持原逻辑) ========== */
let dragging = false;
let dragStart = {
x: 0,
y: 0,
left: 0,
top: 0
};
let resizing = false;
let resizeStart = {
x: 0,
y: 0,
w: 0,
h: 0
};
function onDragStartTouch(e) {
const t = e.touches && e.touches[0];
if (t) startDrag(t.clientX, t.clientY);
}
function onDragStartMouse(e) {
startDrag(e.clientX, e.clientY);
}
function startDrag(cx, cy) {
dragging = true;
dragStart.x = cx;
dragStart.y = cy;
dragStart.left = left.value;
dragStart.top = top.value;
}
function onResizeStartTouch(e) {
const t = e.touches && e.touches[0];
if (t) startResize(t.clientX, t.clientY);
}
function onResizeStartMouse(e) {
startResize(e.clientX, e.clientY);
}
function startResize(cx, cy) {
resizing = true;
resizeStart.x = cx;
resizeStart.y = cy;
resizeStart.w = width.value;
resizeStart.h = height.value;
}
function onTouchMove(e) {
if (!dragging && !resizing) return;
const t = e.touches && e.touches[0];
if (t) handleMove(t.clientX, t.clientY, e);
}
function onMouseMove(e) {
if (!dragging && !resizing) return;
handleMove(e.clientX, e.clientY, e);
}
function handleMove(cx, cy, e) {
if (dragging) {
const dx = cx - dragStart.x;
const dy = cy - dragStart.y;
left.value = Math.min(Math.max(0, dragStart.left + dx), Math.max(0, screenW - width.value));
top.value = Math.min(Math.max(0, dragStart.top + dy), Math.max(0, screenH - height.value));
emit('update:position', {
left: left.value,
top: top.value
});
} else if (resizing) {
const dx = cx - resizeStart.x;
const dy = cy - resizeStart.y;
let nw = Math.min(props.maxWidth, Math.max(props.minWidth, Math.round(resizeStart.w + dx)));
let nh = Math.min(props.maxHeight, Math.max(props.minHeight, Math.round(resizeStart.h + dy)));
if (left.value + nw > screenW) nw = screenW - left.value;
if (top.value + nh > screenH) nh = screenH - top.value;
width.value = nw;
height.value = nh;
emit('update:size', {
width: width.value,
height: height.value
});
}
if (e && e.preventDefault) e.preventDefault();
}
function onMouseUp() {
if (dragging) dragging = false;
if (resizing) resizing = false;
}
function onTouchEnd() {
if (dragging) dragging = false;
if (resizing) resizing = false;
}
/* ========== picker 行数 / 高度 计算(关键) ========== */
const pickerHeight = computed(() => {
const avail = Math.max(0, Math.round(height.value) - HEADER_H);
let rows = Math.floor(avail / ITEM_H);
if (rows < 1) rows = 1;
if (rows % 2 === 0) rows = rows - 1 > 0 ? rows - 1 : 1;
return rows * ITEM_H;
});
/* ========== clamp helper ========== */
function clampSelectedIndexes() {
const colsArr = rawColumns.value || [];
if (selectedIndexes.value.length !== colsArr.length) {
selectedIndexes.value = Array.from({
length: colsArr.length
}, (_, i) => selectedIndexes.value[i] ?? 0);
}
selectedIndexes.value = selectedIndexes.value.map((idx, ci) => {
const col = Array.isArray(colsArr[ci]) ? colsArr[ci] : [];
const maxIdx = Math.max(0, col.length - 1);
return col.length ? Math.min(Math.max(0, Number(idx) || 0), maxIdx) : 0;
});
}
/* ========== 对齐重置逻辑(核心) ========== */
let resetTimer = null;
function resetPickerAlign(delay = 40) {
if (resetTimer) clearTimeout(resetTimer);
resetTimer = setTimeout(() => {
nextTick(() => {
clampSelectedIndexes();
selectedIndexes.value = selectedIndexes.value.map(v => Number(v || 0));
});
resetTimer = null;
}, delay);
}
/* 在关键变化上调用重置columns / visible / pickerHeight */
watch(() => props.columns, () => {
clampSelectedIndexes();
resetPickerAlign(30);
}, {
deep: true,
immediate: true
});
watch(() => visible.value, (v) => {
if (v) {
resetPickerAlign(60);
setTimeout(() => resetPickerAlign(40), 120);
}
});
watch(() => pickerHeight.value, () => {
resetPickerAlign(30);
});
/* ========== picker change / confirm / cancel ========== */
function onPickerChange(e) {
const val = (e && e.detail && e.detail.value) ? e.detail.value : e;
if (Array.isArray(val)) {
selectedIndexes.value = val.map((v, ci) => {
const col = rawColumns.value[ci] || [];
const max = Math.max(0, col.length - 1);
const num = Number(v) || 0;
return col.length ? Math.min(Math.max(0, num), max) : 0;
});
}
emit('change', selectedIndexes.value.slice());
resetPickerAlign(80);
}
function confirm() {
console.log("111")
const result = selectedIndexes.value.map((idx, ci) => {
const col = rawColumns.value[ci] || [];
const display = (displayColumns.value[ci] && displayColumns.value[ci][idx] !== undefined) ?
displayColumns.value[ci][idx] : (col[idx] !== undefined ? String(col[idx]) : '');
const val = col[idx] !== undefined ? col[idx] : (display || null);
return {
index: idx,
value: val,
display
};
});
emit('confirm', result);
visible.value = false;
}
function cancel() {
visible.value = false;
emit('close');
}
/* ========== boxStyle ========== */
const boxStyle = computed(() => ({
position: 'fixed',
left: `${Math.round(left.value)}px`,
top: `${Math.round(top.value)}px`,
width: `${Math.round(width.value)}px`,
height: `${Math.round(height.value)}px`,
zIndex: 1000,
background: '#fff',
borderRadius: '8px',
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
overflow: 'hidden',
transform: 'translateZ(0)'
}));
/* ========== 全局事件监听(更稳健的平台检测) ========== */
/* 我们优先使用 window/documentH5 环境),如果都不可用(如原生 App则不添加全局监听
因为原生 App 的触摸事件通常在组件内部就能处理touchstart/touchmove/touchend */
let globalEventTarget = null;
if (typeof window !== 'undefined' && window && typeof window.addEventListener === 'function') {
globalEventTarget = window;
} else if (typeof document !== 'undefined' && document && typeof document.addEventListener === 'function') {
globalEventTarget = document;
} else {
globalEventTarget = null; // 在原生 App非 H5中通常为 null
}
onMounted(() => {
if (globalEventTarget) {
try {
globalEventTarget.addEventListener('mousemove', onMouseMove);
globalEventTarget.addEventListener('mouseup', onMouseUp);
// touchmove 需要 passive:false 以阻止默认行为
globalEventTarget.addEventListener('touchmove', onTouchMove, {
passive: false
});
globalEventTarget.addEventListener('touchend', onTouchEnd);
} catch (e) {
// 如果某些环境不支持 options 参数,用 fallback
try {
globalEventTarget.addEventListener('touchmove', onTouchMove);
globalEventTarget.addEventListener('touchend', onTouchEnd);
} catch (err) {
// 忽略
}
}
}
});
onBeforeUnmount(() => {
if (globalEventTarget) {
try {
globalEventTarget.removeEventListener('mousemove', onMouseMove);
globalEventTarget.removeEventListener('mouseup', onMouseUp);
globalEventTarget.removeEventListener('touchmove', onTouchMove);
globalEventTarget.removeEventListener('touchend', onTouchEnd);
} catch (e) {
// 忽略
}
}
if (resetTimer) {
clearTimeout(resetTimer);
resetTimer = null;
}
});
</script>
<style scoped>
.overlay {
position: fixed;
inset: 0;
z-index: 900;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.box {
background: #fff;
display: flex;
flex-direction: column;
user-select: none;
-webkit-user-select: none;
transform: translateZ(0);
}
.header {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
border-bottom: 1px solid #eee;
background: linear-gradient(90deg, #fff, #fafafa);
cursor: move;
}
.title {
font-size: 16px;
font-weight: 600;
}
.actions {
display: flex;
gap: 8px;
}
.btn {
padding: 6px 8px;
border-radius: 6px;
background: #f5f5f5;
}
.picker-view {
width: 100%;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overflow: hidden;
}
.picker-view-column {
display: inline-block;
vertical-align: top;
width: calc(100% / var(--cols, 1));
box-sizing: border-box;
text-align: center;
}
.picker-item {
display: block;
/* height: var(--item-h);
line-height: var(--item-h); */
text-align: center;
font-size: 14px;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.resize-handle {
position: absolute;
right: 6px;
bottom: 6px;
width: 28px;
height: 28px;
touch-action: none;
}
.grip {
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px dashed #bbb;
box-sizing: border-box;
}
</style>