299 lines
7.7 KiB
Vue
299 lines
7.7 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 通用单选列表弹层(底部抽屉):标题 + 可滚动选项行 + 确认按钮。
|
||
* 「共享家人」选目标群组目录、「发布」选专辑都吃它——数据由页面拉好喂进来,组件不发请求。
|
||
* 行为:展开时重置为「未选中」(防上次残留误提交);点行仅选中,点确认才 emit confirm(key);
|
||
* loading 显示转圈占位,items 空且不在 loading 时显示 emptyText。
|
||
*/
|
||
import { useLocale } from "uview-pro";
|
||
import { computed, ref, watch } from "vue";
|
||
import { useThemeTokens } from "@/composables";
|
||
|
||
export interface PickerOption {
|
||
key: string;
|
||
/** 主文案(如群组目录名 / 专辑名) */
|
||
label: string;
|
||
/** 次要说明(如共享路径 / 专辑描述),缺省不占行 */
|
||
sub?: string;
|
||
/** 行首图标(如相册=photo),缺省不占位 */
|
||
icon?: 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: "",
|
||
},
|
||
/** 确认键文案:发布场景传「发布相册」等,缺省仍用「创建」 */
|
||
confirmText: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
/** 附加「新建」行文案(如 新建相册):非空即渲染,点按 emit create */
|
||
createText: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits<{
|
||
/** 遮罩/取消关闭 */
|
||
(e: "close"): void;
|
||
/** 确认并带上选中项 key */
|
||
(e: "confirm", key: string): void;
|
||
/** 点了附加的「新建」行 */
|
||
(e: "create"): void;
|
||
}>();
|
||
|
||
const { t } = useLocale();
|
||
|
||
// 勾选色与确认钮随主题;确认钮用纯色(渐变浅档在白字下发糊),未选择/提交中用透明度做置灰态
|
||
const { primary } = useThemeTokens();
|
||
|
||
const picked = ref("");
|
||
|
||
const confirmStyle = computed(() => ({
|
||
background: primary.value,
|
||
opacity: picked.value && !props.loading ? 1 : 0.45,
|
||
}));
|
||
|
||
// 每次展开清掉上次选择
|
||
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>
|
||
<!-- 列表白卡(底部抽屉统一风格:灰底上的白色圆角分块) -->
|
||
<view class="option-picker__card">
|
||
<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">
|
||
<u-icon v-if="item.icon" :name="item.icon" size="40" :color="primary" />
|
||
<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-circle-fill" size="40" :color="primary" />
|
||
<view v-else class="option-picker__radio" />
|
||
</view>
|
||
</template>
|
||
</scroll-view>
|
||
</view>
|
||
<!-- 附加「新建」行(如 新建相册):点按跳创建页 -->
|
||
<view v-if="props.createText" class="option-picker__create" @tap="emit('create')">
|
||
<u-icon name="plus-circle-fill" size="40" :color="primary" />
|
||
<text class="option-picker__create-text">
|
||
{{ props.createText }}
|
||
</text>
|
||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||
</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" :style="confirmStyle" @tap="onConfirm">
|
||
<text class="option-picker__btn-text option-picker__btn-text--primary">
|
||
{{ props.confirmText || t("FilePage.folderConfirm") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="option-picker__safe" />
|
||
</view>
|
||
</u-popup>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||
.option-picker {
|
||
padding: 32rpx 24rpx 0;
|
||
background-color: #f2f3f5;
|
||
|
||
&__title {
|
||
display: block;
|
||
padding: 8rpx 8rpx 28rpx;
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
text-align: center;
|
||
border-bottom: 2rpx solid #e8eaed;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
&__card {
|
||
padding: 8rpx;
|
||
border-radius: 20rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
&__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;
|
||
gap: 20rpx;
|
||
padding: 24rpx 20rpx;
|
||
border-radius: 16rpx;
|
||
|
||
&--picked {
|
||
background-color: #f0f6ff;
|
||
}
|
||
}
|
||
|
||
&__row-main {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
&__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%;
|
||
}
|
||
|
||
/* 附加「新建」行:与列表卡同款白卡(图标 + 文案 + 右尖角) */
|
||
&__create {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
margin-top: 20rpx;
|
||
padding: 28rpx 32rpx;
|
||
border-radius: 20rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
&__create-text {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #1f2329;
|
||
}
|
||
|
||
&__footer {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-top: 28rpx;
|
||
}
|
||
|
||
/* 按键版式对齐设计图:等高圆角矩形 + 细描边,非胶囊 */
|
||
&__btn {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 96rpx;
|
||
border: 2rpx solid #e5e6eb;
|
||
border-radius: 24rpx;
|
||
background-color: #ffffff;
|
||
|
||
&--primary {
|
||
border-color: transparent;
|
||
background-color: #2b8fff;
|
||
}
|
||
}
|
||
|
||
&__btn-text {
|
||
font-size: 32rpx;
|
||
color: #5c626b;
|
||
|
||
&--primary {
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
&__safe {
|
||
height: calc(32rpx + env(safe-area-inset-bottom));
|
||
}
|
||
}
|
||
</style>
|