Files
Docker/uniapp/youjiala/src/components/option-picker/option-picker.vue
T
2026-10-16 11:57:29 +08:00

299 lines
7.7 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.
<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>