chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
.popup-root {
width: 0; // 解决 H5 关闭抽屉存在黑色背景 BUG
}
.drawer-wrapper {
background-color: rgba(0, 0, 0, 0.4);
width: 100vw;
height: 100vh;
height: 100dvh; /* 使用动态视口高度,排除浏览器 UI */
position: fixed;
left: 0;
top: 0;
display: flex;
&.direction-left {
flex-direction: row;
justify-content: flex-end;
}
&.direction-bottom {
flex-direction: column;
justify-content: flex-start;
}
}
.drawer-content {
display: flex;
flex-direction: column;
background: #fff;
transition: all 0.2s ease;
will-change: transform;
&.direction-left {
position: absolute;
left: 0;
box-shadow: 4rpx 0 20rpx rgba(0, 0, 0, 0.2);
border-radius: 0 30rpx 30rpx 0;
transform: translateX(-100%);
}
&.direction-bottom {
position: absolute;
bottom: 0;
left: 0;
border-radius: 20rpx 20rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.2);
transform: translateY(100%);
max-height: 100vh;
max-height: 100dvh; /* 限制最大高度,确保不超出视口 */
overflow: hidden;
// 自适应高度模式的额外样式
&.auto-height {
overflow: visible;
}
}
.head-box {
box-sizing: border-box;
width: 100%;
position: sticky;
top: 0;
z-index: 10;
background: #fff;
display: flex;
flex-direction: row;
padding: 30rpx 30rpx 20rpx;
align-items: center;
justify-content: flex-end;
flex-shrink: 0; /* 防止被压缩 */
border-radius: 20rpx 20rpx 0 0; /* 与父容器保持一致 */
min-height: 94rpx; /* 保证不包含关闭按钮时高度撑开一致 */
border-bottom: 2rpx solid #f0f0f0; /* 调深浅灰色下边框 */
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: calc(100% - 200rpx);
font-weight: 600;
color: rgba(21, 23, 31, 1);
padding-top: 10rpx;
}
.close-btn {
z-index: 1;
cursor: pointer;
padding: 10rpx; /* 增加点击区域 */
margin: -10rpx; /* 抵消padding保持视觉位置不变 */
.iconfont {
font-size: 30rpx;
color: #333;
}
}
}
.content-box {
flex: 1;
margin: 20rpx 0;
overflow: auto;
min-height: 0;
}
}
.drawer-mask {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
}
.drawer-show {
&.direction-left {
transform: translateX(0);
}
&.direction-bottom {
transform: translateY(0);
}
}

View File

@@ -0,0 +1,238 @@
<template>
<uni-popup ref="popupRef" :is-mask-click="maskClick">
<view
class="drawer-wrapper page-container"
:class="`direction-${direction}`"
:style="wrapperStyle"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view
class="drawer-content"
:class="[
`direction-${direction}`,
{
'drawer-show': visible,
'auto-height': autoHeight && direction === 'bottom',
},
]"
:style="contentStyle"
>
<!-- 微信安全区 -->
<safety-zone v-if="direction === 'left'" :add-height="0"></safety-zone>
<view class="head-box" v-if="showHeader">
<view class="title">
<text class="text-ellipsis">{{ displayTitle }}</text>
</view>
<view class="close-btn" @tap="close" v-if="showCloseBtn">
<text class="iconfont icon-X"></text>
</view>
</view>
<view class="content-box">
<slot></slot>
</view>
</view>
<view
class="drawer-mask"
:class="`direction-${direction}`"
@tap="close"
></view>
</view>
</uni-popup>
</template>
<script setup lang="uts">
import { ref, computed, nextTick } from "vue";
import { translateText } from "@/utils/i18n";
interface UniPopupInstance {
open(type?: string): void;
close(): void;
}
const props = defineProps({
title: {
type: String,
default: "",
},
width: {
type: String,
default: "75vw",
},
height: {
type: String,
default: "50vh",
},
// 默认高度仅对bottom方向有效
defaultHeight: {
type: String,
default: "20vh",
},
// 最大高度仅对bottom方向有效
maxHeight: {
type: String,
default: "50vh",
},
// 是否启用自适应高度仅对bottom方向有效
autoHeight: {
type: Boolean,
default: false,
},
maskClick: {
type: Boolean,
default: true,
},
// 弹出方向:'left' | 'bottom'
direction: {
type: String,
default: "left",
},
// 是否显示头部
showHeader: {
type: Boolean,
default: true,
},
// 是否显示关闭按钮
showCloseBtn: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(["update-visible"]);
const popupRef = ref<UniPopupInstance | null>(null);
const visible = ref(false);
const displayTitle = computed(() => translateText(props.title));
// 滑动关闭相关状态
const touchStartX = ref<number>(0);
const touchStartY = ref<number>(0);
const isSwiping = ref<boolean>(false);
const swipeOffset = ref<number>(0);
const swipeThreshold = 50; // 降低最小滑动距离阈值,提升灵敏度
const handleTouchStart = (e: any) => {
if (!e.touches || e.touches.length === 0) return;
touchStartX.value = e.touches[0].clientX;
touchStartY.value = e.touches[0].clientY;
isSwiping.value = false;
swipeOffset.value = 0;
};
const handleTouchMove = (e: any) => {
if (!e.touches || e.touches.length === 0) return;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const diffX = Math.abs(currentX - touchStartX.value);
const diffY = Math.abs(currentY - touchStartY.value);
// 降低判定门槛,只要水平意图明显且超过 5px 就算滑动
if (diffX > diffY && diffX > 5) {
isSwiping.value = true;
swipeOffset.value = currentX - touchStartX.value;
// 阻止默认行为以防止页面滚动干扰手势
e.preventDefault();
}
};
const handleTouchEnd = (e: any) => {
if (!isSwiping.value) return;
if (Math.abs(swipeOffset.value) > swipeThreshold) {
close();
}
isSwiping.value = false;
swipeOffset.value = 0;
};
// 根据方向和props动态计算wrapper样式
const wrapperStyle = computed(() => {
const style = {};
if (props.direction === "bottom") {
// 为底部弹出设置CSS变量
if (props.autoHeight) {
style["--content-default-height"] = props.defaultHeight;
style["--content-max-height"] = props.maxHeight;
} else {
style["--content-height"] = props.height;
}
}
return style;
});
// 根据方向和props动态计算内容样式
const contentStyle = computed(() => {
const style = {} as Record<string, any>;
if (props.direction === "left") {
// 左侧弹出时使用width属性
style.width = props.width;
style.height = "100%";
} else if (props.direction === "bottom") {
// 底部弹出时使用height属性
if (props.autoHeight) {
// 自适应高度模式
style.minHeight = props.defaultHeight;
style.maxHeight = props.maxHeight;
style.height = "auto";
} else {
// 固定高度模式
style.height = props.height;
}
style.width = "100%";
}
return style;
});
// 打开抽屉
const open = () => {
if (!popupRef.value) {
console.warn("popupRef is null");
return;
}
popupRef.value.open();
emit("update-visible", true);
// 在微信小程序中使用 nextTick 替代 setTimeout
// #ifdef MP-WEIXIN
nextTick(() => {
visible.value = true;
});
// #endif
// 在其他平台使用 setTimeout
// #ifndef MP-WEIXIN
setTimeout(() => {
visible.value = true;
}, 20);
// #endif
};
// 关闭抽屉
const close = () => {
visible.value = false;
// 等待动画结束再关闭 popup
setTimeout(() => {
popupRef.value?.close();
}, 200);
emit("update-visible", false);
};
defineExpose({
open,
close,
popupRef,
visible,
});
</script>
<style scoped lang="scss">
@import "./drawer-popup.scss";
</style>