This commit is contained in:
owenchen committed 2026-10-14 21:23:40 +08:00
1 parent e740ec77ec
commit ceb3e7beec
20 files changed
+2280 -138

No files matched your search

@@ -0,0 +1,239 @@
<script setup lang="ts">
/**
* 通用单选列表弹层(底部抽屉):标题 + 可滚动选项行 + 确认按钮。
* 「共享家人」选目标群组目录、「发布」选专辑都吃它——数据由页面拉好喂进来,组件不发请求。
* 行为:展开时重置为「未选中」(防上次残留误提交);点行仅选中,点确认才 emit confirm(key);
* loading 显示转圈占位,items 空且不在 loading 时显示 emptyText。
*/
import { useLocale } from "uview-pro";
import { ref, watch } from "vue";
export interface PickerOption {
key: string;
/** 主文案(如群组目录名 / 专辑名) */
label: string;
/** 次要说明(如共享路径 / 专辑描述),缺省不占行 */
sub?: string;
}
const props = defineProps({
/** 弹层是否展开 */
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
},
/** 候选项;key 必须非空且唯一 */
items: {
type: Array as () => PickerOption[],
default: () => [],
},
/** 数据在途:列表区显示加载占位、确认按钮禁用 */
loading: {
type: Boolean,
default: false,
},
/** 空列表文案 */
emptyText: {
type: String,
default: "",
},
});
const emit = defineEmits<{
/** 遮罩/取消关闭 */
(e: "close"): void;
/** 确认并带上选中项 key */
(e: "confirm", key: string): void;
}>();
const { t } = useLocale();
const picked = ref("");
// 每次展开清掉上次选择
watch(
() => props.visible,
(on) => {
if (on) {
picked.value = "";
}
},
);
function onConfirm(): void {
if (picked.value && !props.loading) {
emit("confirm", picked.value);
}
}
</script>
<template>
<!-- u-popup 只认 modelValue(v-model);loading 中禁点遮罩防误关 -->
<u-popup :model-value="props.visible" mode="bottom" round="24" :close-on-click-overlay="!props.loading"
@close="emit('close')">
<view class="option-picker">
<text class="option-picker__title">
{{ props.title }}
</text>
<scroll-view scroll-y class="option-picker__list">
<view v-if="props.loading" class="option-picker__empty">
<u-loading mode="circle" />
</view>
<template v-else>
<view v-if="!props.items.length" class="option-picker__empty">
<text class="option-picker__empty-text">
{{ props.emptyText || t("FilePage.pickerEmpty") }}
</text>
</view>
<view v-for="item in props.items" :key="item.key" class="option-picker__row"
:class="{ 'option-picker__row--picked': picked === item.key }" @tap="picked = item.key">
<view class="option-picker__row-main">
<text class="option-picker__row-label">
{{ item.label }}
</text>
<text v-if="item.sub" class="option-picker__row-sub">
{{ item.sub }}
</text>
</view>
<u-icon v-if="picked === item.key" name="checkmark" size="36" color="#2b8fff" />
<view v-else class="option-picker__radio" />
</view>
</template>
</scroll-view>
<view class="option-picker__footer">
<view class="option-picker__btn" @tap="emit('close')">
<text class="option-picker__btn-text">
{{ t("FilePage.folderCancel") }}
</text>
</view>
<view class="option-picker__btn option-picker__btn--primary"
:class="{ 'option-picker__btn--muted': !picked || props.loading }" @tap="onConfirm">
<text class="option-picker__btn-text option-picker__btn-text--primary">
{{ t("FilePage.folderConfirm") }}
</text>
</view>
</view>
<view class="option-picker__safe" />
</view>
</u-popup>
</template>
<style lang="scss" scoped>
.option-picker {
padding: 40rpx 32rpx 0;
background-color: #ffffff;
&__title {
display: block;
padding-bottom: 24rpx;
font-size: 32rpx;
font-weight: 600;
color: #1f2329;
text-align: center;
}
&__list {
max-height: 560rpx;
}
&__empty {
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
}
&__empty-text {
font-size: 26rpx;
color: #8f959e;
}
&__row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
border-radius: 16rpx;
&--picked {
background-color: #f0f6ff;
}
}
&__row-main {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 4rpx;
margin-right: 20rpx;
}
&__row-label {
overflow: hidden;
font-size: 28rpx;
color: #1f2329;
text-overflow: ellipsis;
white-space: nowrap;
}
&__row-sub {
overflow: hidden;
font-size: 22rpx;
color: #8f959e;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 未选中态的空心圆点 */
&__radio {
flex-shrink: 0;
width: 36rpx;
height: 36rpx;
border: 3rpx solid #d4d7dd;
border-radius: 50%;
}
&__footer {
display: flex;
gap: 20rpx;
margin-top: 28rpx;
}
&__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 76rpx;
border-radius: 999rpx;
background-color: #f5f6f8;
&--primary {
background-color: #2b8fff;
}
/* 未选择或提交中:主按钮置灰(仍可点,由 onConfirm 拦截) */
&--muted {
background-color: #b9d6ff;
}
}
&__btn-text {
font-size: 28rpx;
color: #5c626b;
&--primary {
color: #ffffff;
}
}
&__safe {
height: calc(32rpx + env(safe-area-inset-bottom));
}
}
</style>