display:flex布局下设置white-space:nowrap出现bug

本文探讨了在使用display:flex布局时如何处理单行内容溢出的情况,指出在父级元素设置white-space:nowrap;后,添加min-width:0;可能导致的bug,并提供了解决方案。

需求:display:flex布局下实现单行内容溢出隐藏

有bug的效果:
在这里插入图片描述

在设置了white-space: nowrap;的父级元素上添加min-width: 0;属性

在这里插入图片描述

<template> <div class="cascader-select"> <!-- 输入框 --> <div class="input-wrapper" @click="toggleDropdown" > <input v-model="displayValue" :placeholder="placeholder" class="cascader-input" readonly /> <div class="input-icons"> <span v-if="selectedPath.length > 0" class="clear-icon" @click.stop="handleClear" title="清空选择" > × </span> <span class="arrow-icon" :class="{ &#39;arrow-up&#39;: showDropdown }" > ▼ </span> </div> </div> <!-- 联下拉框 --> <div v-show="showDropdown" class="cascader-dropdown" > <div class="cascader-content"> <!-- 联头部标注 --> <div class="cascader-header"> <div v-for="(panel, level) in cascaderPanels" :key="`header-${level}`" class="cascader-header-item" > {{ getHeaderLabel(level) }} </div> </div> <!-- 联面板 --> <div class="cascader-panels"> <div v-for="(panel, level) in cascaderPanels" :key="level" class="cascader-panel" > <div v-for="option in panel" :key="option.name" class="cascader-option" :class="{ &#39;is-active&#39;: isOptionActive(option, level), &#39;is-selected&#39;: isOptionSelected(option, level), &#39;has-children&#39;: option.children && option.children.length > 0 }" @click="handleOptionClick(option, level)" > <el-tooltip class="box-item" effect="dark" :content="option.nameCn" placement="top" > <span class="option-label">{{ option.nameCn }}</span> </el-tooltip> <span v-if="option.children?.length" class="option-arrow" > > </span> </div> </div> </div> </div> <!-- 操作按钮 --> <div class="cascader-footer"> <button class="btn btn-cancel" @click="handleCancel" > 取消 </button> <button class="btn btn-confirm" @click="handleConfirm" :disabled="!tempSelectedPath.length" > 确定 </button> </div> </div> </div> </template> <script setup lang="ts"> import { ref, computed, watch, onUnmounted } from &#39;vue&#39;; import { useI18n } from &#39;vue-i18n&#39;; const { t } = useI18n(); // 类型定义 export interface CascaderOption { name: string nameCn: string nameEn: string children?: CascaderOption[] } export interface CascaderProps { options: CascaderOption[] modelValue?: string[] placeholder?: string headerLabels?: string[] } export interface CascaderEmits { (event: &#39;update:modelValue&#39;, value: string[]): void (event: &#39;change&#39;, value: string[], selectedOptions: CascaderOption[]): void } // 定义props,带默认值 const props = withDefaults(defineProps<CascaderProps>(), { modelValue: () => [], placeholder: &#39;请选择&#39;, headerLabels: () => [&#39;区域&#39;, &#39;大区&#39;, &#39;Region&#39;, &#39;AZ&#39;], }); // 定义emits const emit = defineEmits<CascaderEmits>(); // 响应式数据 const showDropdown = ref<boolean>(false); const tempSelectedPath = ref<string[]>([...props.modelValue]); const selectedPath = ref<string[]>([...props.modelValue]); // 计算属性 - 联面板数据 const cascaderPanels = computed<CascaderOption[][]>(() => { const panels: CascaderOption[][] = [props.options]; let currentOptions: CascaderOption[] = props.options; for (let i = 0; i < tempSelectedPath.value.length; i++) { const selectedValue = tempSelectedPath.value[i]; const selectedOption = currentOptions.find((option: CascaderOption) => option.name === selectedValue); if (selectedOption && selectedOption.children && selectedOption.children.length > 0) { panels.push(selectedOption.children); currentOptions = selectedOption.children; } else { break; } } return panels; }); // 计算属性 - 显示值(只显示最后一的label) const displayValue = computed<string>(() => { if (!selectedPath.value.length){ return &#39;&#39; ; } let currentOptions: CascaderOption[] = props.options; let finalLabel = &#39;&#39;; for (const value of selectedPath.value) { const option = currentOptions.find((opt: CascaderOption) => opt.name === value); if (option) { finalLabel = option.nameCn; currentOptions = option.children || []; } } return finalLabel; }); // 方法 - 切换下拉框显示状态 const toggleDropdown = (): void => { showDropdown.value = !showDropdown.value; if (showDropdown.value) { // 打开时重临时选中路径 tempSelectedPath.value = [...selectedPath.value]; } }; // 方法 - 检查选项是否处于激活状态 const isOptionActive = (option: CascaderOption, level: number): boolean => { return tempSelectedPath.value[level] === option.name; }; // 方法 - 检查选项是否已选中 const isOptionSelected = (option: CascaderOption, level: number): boolean => { return selectedPath.value[level] === option.name; }; // 方法 - 处理选项点击 const handleOptionClick = (option: CascaderOption, level: number): void => { const newPath = tempSelectedPath.value.slice(0, level); newPath.push(option.name); tempSelectedPath.value = newPath; }; // 方法 - 确认选择 const handleConfirm = (): void => { selectedPath.value = [...tempSelectedPath.value]; // 获取选中的完整选项对象 const selectedOptions: CascaderOption[] = []; let currentOptions: CascaderOption[] = props.options; for (const value of selectedPath.value) { const option = currentOptions.find((opt: CascaderOption) => opt.name === value); if (option) { selectedOptions.push(option); currentOptions = option.children || []; } } // 触发事件 emit(&#39;update:modelValue&#39;, selectedPath.value); emit(&#39;change&#39;, selectedPath.value, selectedOptions); // 关闭下拉框 showDropdown.value = false; }; // 方法 - 取消选择 const handleCancel = (): void => { tempSelectedPath.value = [...selectedPath.value]; showDropdown.value = false; }; // 方法 - 清空选择 const handleClear = (): void => { selectedPath.value = []; tempSelectedPath.value = []; emit(&#39;update:modelValue&#39;, []); emit(&#39;change&#39;, [], []); showDropdown.value = false; }; // 方法 - 获取头部标签 const getHeaderLabel = (level: number): string => { return props.headerLabels[level] || `别 ${level + 1}`; }; // 监听外部值变化 watch(() => props.modelValue, (newValue: string[]) => { selectedPath.value = [...newValue]; tempSelectedPath.value = [...newValue]; }, { deep: true }); // 点击外部关闭下拉框 const handleClickOutside = (event: Event): void => { const target = event.target as HTMLElement; const cascaderElement = document.querySelector(&#39;.cascader-select&#39;) as HTMLElement; if (cascaderElement && !cascaderElement.contains(target)) { showDropdown.value = false; } }; // 添加/移除全局点击监听 watch(showDropdown, (isShow: boolean) => { if (isShow) { document.addEventListener(&#39;click&#39;, handleClickOutside); } else { document.removeEventListener(&#39;click&#39;, handleClickOutside); } }); onUnmounted(() => { document.removeEventListener(&#39;click&#39;, handleClickOutside); }); </script> <style scoped> .cascader-select { position: relative; display: inline-block; width: 100%; } .input-wrapper { position: relative; cursor: pointer; } .cascader-input { width: 100%; padding: 6px 40px 4px 16px; border: 1px solid #e9ecef; border-radius: 4px; background-color: #fff; cursor: pointer; transition: all 0.3s ease; font-size: 14px; box-sizing: border-box; color:#606266; } .cascader-input:hover { border-color: #667eea; } .cascader-input:focus { outline: none; border-color: #667eea; } .input-icons { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 4px; } .clear-icon { color: #9ca3af; font-size: 14px; cursor: pointer; padding: 2px; border-radius: 3px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; } .clear-icon:hover { color: #6b7280; background-color: #f3f4f6; } .arrow-icon { color: #adb5bd; font-size: 12px; transition: transform 0.3s ease; pointer-events: none; margin-left: 4px; } .arrow-icon.arrow-up { transform: rotate(180deg); } .cascader-dropdown { position: absolute; top: 100%; left: 0; right: 0; background: white; border: 2px solid #e9ecef; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); z-index: 99; height: 300px; width: 700px; display: flex; flex-direction: column; overflow: hidden; } .cascader-content { flex: 1; overflow: hidden; } .cascader-header { display: flex; border-bottom: 2px solid #e9ecef; background: #fff; } .cascader-header-item { flex: 1; min-width: 100px; width: 100px; padding: 12px; font-weight: 600; font-size: 13px; text-align: left; background: #fff; } .cascader-header-item:last-child { border-right: none; } .cascader-panels { display: flex; height: 450px; overflow: auto; } .cascader-panel { flex: 1; min-width: 100px; width: 100px; height: 200px; border-right: 1px solid #e9ecef; background: #fff; overflow: scroll; } .cascader-panel:last-child { border-right: none; } .cascader-option { padding: 8px 12px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: #495057; transition: all 0.2s ease; position: relative; } .cascader-option:hover { background-color: #f8f9fa; } .cascader-option.is-active { color: #667eea; background-color: #f2f5fc; font-weight: 600; } .cascader-option.is-selected { //color: #667eea; //font-weight: 600; } .cascader-option.has-children { padding-right: 10px; } .option-label { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .option-arrow { color: #adb5bd; font-size: 12px; margin-left: 8px; font-weight: bold; } .cascader-footer { border-top: 1px solid #e9ecef; padding: 16px 20px; display: flex; justify-content: flex-end; gap: 12px; background: #fff; } .btn { width:58px; height: 26px; line-height: 24px; text-align: center; border-radius: 2px; font-size: 14px; font-weight: 500; cursor: pointer; border: 2px solid; transition: all 0.3s ease; } .btn-cancel { background: #fff; color: #6c757d; border-color: #dee2e6; } .btn-cancel:hover { color: #495057; border-color: #adb5bd; background-color: #f8f9fa; } .btn-confirm { background: #5E7CE0; color: #fff; border-color: transparent; } .btn-confirm:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } .btn-confirm:disabled { background: #adb5bd; border-color: #adb5bd; cursor: not-allowed; transform: none; } @media (max-width: 768px) { .cascader-panel { min-width: 100px; width: 100px; } .cascader-header-item { min-width: 100px; width: 100px; font-size: 12px; padding: 6px 8px; } .cascader-option { padding: 6px 8px; font-size: 12px; } } </style> 这是一个组件,然后它使用在一个页面中,bug就是滑动组件的面板,里面的选项内容的 <el-tooltip>会随着列表的滚轮滚动,而展示在外面,但是我希望它的 <el-tooltip>只在组件里面展示
09-17
<template> <div class="schedule-management"> <!-- 日历区域 --> <div id="calendar-container" class="calendar-container"> <!-- 打印按钮 --> <div class="calendar-toolbar"> <el-button type="primary" size="small" @click="openPrintPreview">打印预览</el-button> </div> <el-calendar v-model="selectedDate" @select="handleCalendar-select"> <template #date-cell="{ data }"> <div class="calendar-date-cell" @click="handleDateCellClick(data.day)"> <span class="date-number">{{ data.day.split(&#39;-&#39;)[2] }}</span> <!-- 展示当天的日程列表 --> <div class="schedule-list-on-date"> <div v-for="schedule in getDaySchedules(data.day)" :key="schedule.id" class="schedule-item-on-date" :class="`schedule-item-on-date priority-${schedule.priority}`" @click.stop="viewScheduleDetails(schedule)" > <span class="schedule-title">{{ schedule.title }}</span> </div> <!-- 如果日程太多,显示省略号 --> <div v-if="getDaySchedules(data.day).length > maxSchedulesToShow" class="more-schedules" > +{{ getDaySchedules(data.day).length - maxSchedulesToShow }} 更多 </div> </div> </div> </template> </el-calendar> </div> <!-- 日程详情侧边栏 --> <div class="schedule-sidebar" v-if="selectedSchedule"> <div class="sidebar-header"> <h3>{{ formatDateTitle(selectedDate) }} 日程详情</h3> <el-button type="text" @click="selectedSchedule = null" icon="Close">关闭</el-button> </div> <div class="schedule-detail"> <h4>{{ selectedSchedule.title }}</h4> <p><strong>时间:</strong> {{ selectedSchedule.startTime }} - {{ selectedSchedule.endTime }}</p> <p><strong>优先:</strong> <el-tag :type="selectedSchedule.priority === &#39;high&#39; ? &#39;danger&#39; : selectedSchedule.priority === &#39;medium&#39; ? &#39;warning&#39; : &#39;success&#39;" size="small" > {{ selectedSchedule.priority === &#39;high&#39; ? &#39;高&#39; : selectedSchedule.priority === &#39;medium&#39; ? &#39;中&#39; : &#39;低&#39; }} </el-tag> </p> <p v-if="selectedSchedule.description"><strong>描述:</strong> {{ selectedSchedule.description }}</p> <div class="detail-actions"> <el-button size="small" @click="editSchedule(selectedSchedule)">编辑</el-button> <el-button size="small" type="danger" @click="deleteSchedule(selectedSchedule.id)">删除</el-button> </div> </div> </div> <!-- 添加/编辑日程对话框 --> <el-dialog v-model="dialogVisible" :title="isEditing ? &#39;编辑日程&#39; : &#39;添加日程&#39;" width="500px" :before-close="handleDialogClose" > <el-form :model="scheduleForm" :rules="formRules" ref="formRef" label-width="80px"> <el-form-item label="标题" prop="title"> <el-input v-model="scheduleForm.title" placeholder="请输入日程标题" /> </el-form-item> <el-form-item label="日期" prop="date"> <el-date-picker v-model="scheduleForm.date" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 100%" /> </el-form-item> <el-form-item label="时间" prop="time"> <el-time-picker v-model="scheduleForm.startTime" placeholder="开始时间" format="HH:mm" value-format="HH:mm" style="width: 48%" /> <span style="margin: 0 4px">-</span> <el-time-picker v-model="scheduleForm.endTime" placeholder="结束时间" format="HH:mm" value-format="HH:mm" style="width: 48%" /> </el-form-item> <el-form-item label="优先" prop="priority"> <el-select v-model="scheduleForm.priority" placeholder="选择优先" style="width: 100%"> <el-option label="低" value="low"></el-option> <el-option label="中" value="medium"></el-option> <el-option label="高" value="high"></el-option> </el-select> </el-form-item> <el-form-item label="描述" prop="description"> <el-input v-model="scheduleForm.description" type="textarea" :rows="3" placeholder="请输入日程描述" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="saveSchedule">确定</el-button> </span> </template> </el-dialog> </div> </template> <script setup> import { ref, computed, watch, onMounted } from &#39;vue&#39; import { ElMessage, ElMessageBox } from &#39;element-plus&#39; import { add, del, update, list } from &#39;@/api/calendar&#39; import print from "print-js"; // 响应式数据 const selectedDate = ref(new Date()) const dialogVisible = ref(false) const isEditing = ref(false) const formRef = ref() const selectedSchedule = ref(null) const maxSchedulesToShow = ref(3) // 日程数据 const schedules = ref([]) // 表单数据 const scheduleForm = ref({ id: null, title: &#39;&#39;, date: &#39;&#39;, startTime: &#39;&#39;, endTime: &#39;&#39;, priority: &#39;medium&#39;, description: &#39;&#39; }) // 表单验证规则 const formRules = { title: [{ required: true, message: &#39;请输入日程标题&#39;, trigger: &#39;blur&#39; }], date: [{ required: true, message: &#39;请选择日期&#39;, trigger: &#39;change&#39; }], startTime: [{ required: true, message: &#39;请选择开始时间&#39;, trigger: &#39;change&#39; }], endTime: [{ required: true, message: &#39;请选择结束时间&#39;, trigger: &#39;change&#39; }] } // 获取指定日期的日程(最多显示 maxSchedulesToShow 条) const getDaySchedules = (date) => { return schedules.value.filter(schedule => schedule.date === date).slice(0, maxSchedulesToShow.value) } // 格式化日期标题 const formatDateTitle = (date) => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() return `${year}年${month}月${day}日` } // 日历选择事件 const handleCalendarSelect = (date) => { selectedDate.value = new Date(date) } // 点击日期单元格 const handleDateCellClick = (dateString) => { isEditing.value = false scheduleForm.value = { id: null, title: &#39;&#39;, date: dateString, startTime: &#39;09:00&#39;, endTime: &#39;10:00&#39;, priority: &#39;medium&#39;, description: &#39;&#39; } dialogVisible.value = true } // 查看日程详情 const viewScheduleDetails = (schedule) => { selectedSchedule.value = schedule } // 编辑日程 const editSchedule = (schedule) => { isEditing.value = true scheduleForm.value = { ...schedule } dialogVisible.value = true } // 删除日程 const deleteSchedule = async (id) => { try { await ElMessageBox.confirm(&#39;确认删除该日程吗?&#39;, &#39;提示&#39;, { confirmButtonText: &#39;确定&#39;, cancelButtonText: &#39;取消&#39;, type: &#39;warning&#39; }) del(id).then(() => { ElMessage.success(&#39;删除成功&#39;) selectedSchedule.value = null refreshCalendar() }) } catch { // 取消操作 } } // 保存日程 const saveSchedule = () => { formRef.value?.validate((valid) => { if (valid) { if (isEditing.value) { update(scheduleForm.value).then(() => { refreshCalendar() ElMessage.success(&#39;日程更新成功&#39;) dialogVisible.value = false }) } else { add({ title: scheduleForm.value.title, date: scheduleForm.value.date, startTime: scheduleForm.value.startTime, endTime: scheduleForm.value.endTime, priority: scheduleForm.value.priority, description: scheduleForm.value.description }).then(() => { ElMessage.success(&#39;日程添加成功&#39;) refreshCalendar() dialogVisible.value = false }) } } }) } // 关闭对话框 const handleDialogClose = (done) => { formRef.value?.clearValidate() done() } // 刷新日历数据 function refreshCalendar(year, month) { const y = year ?? selectedDate.value.getFullYear() const m = month ?? selectedDate.value.getMonth() + 1 const strStartDate = `${y}-${String(m).padStart(2, &#39;0&#39;)}-01` const lastDay = new Date(y, m, 0).getDate() const strEndDate = `${y}-${String(m).padStart(2, &#39;0&#39;)}-${lastDay}` list({ from: strStartDate, to: strEndDate }) .then(res => { if (res.data?.result) { schedules.value = res.data.result } }) .catch(err => { console.error(&#39;加载日程失败:&#39;, err) ElMessage.error(&#39;加载日程失败&#39;) }) } // 打印预览 const openPrintPreview = () => { printJS({ printable: &#39;calendar-container&#39;, // 给日历容器加 id="calendar-container" type: &#39;html&#39;, targetStyles: [&#39;*&#39;], documentTitle: &#39;日程表 - &#39; + formatDateTitle(selectedDate.value), // 可选:添加额外打印样式 style: ` .el-calendar__header { display: none !important; } .calendar-toolbar { display: none !important; } ` }) } // 监听月份变化 let lastMonthKey = &#39;&#39; watch(selectedDate, (newDate) => { const key = `${newDate.getFullYear()}-${newDate.getMonth()}` if (key !== lastMonthKey) { lastMonthKey = key refreshCalendar(newDate.getFullYear(), newDate.getMonth() + 1) } }) // 初始化 onMounted(() => { refreshCalendar(selectedDate.value.getFullYear(), selectedDate.value.getMonth() + 1) }) </script> <style scoped> .schedule-management { padding: 10px; background-color: #f5f7fa; min-height: calc(100vh - 260px); display: flex; gap: 20px; } .calendar-container { flex: 1; background: white; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); overflow: hidden; } :deep(.el-calendar) { width: 100%; } :deep(.el-calendar__header) { padding: 15px 20px; } :deep(.el-calendar-day) { height: 120px; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; padding: 5px; position: relative; } .calendar-date-cell { width: 100%; height: 100%; display: flex; flex-direction: column; cursor: pointer; } .date-number { font-weight: bold; font-size: 14px; margin-bottom: 4px; align-self: flex-end; } .schedule-list-on-date { flex: 1; width: 100%; overflow-y: auto; padding-right: 2px; } .schedule-item-on-date { display: flex; align-items: center; padding: 2px 4px; margin-bottom: 2px; border-radius: 3px; cursor: pointer; font-size: 12px; transition: background-color 0.2s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .schedule-item-on-date:hover { background-color: #f0f9ff; } .schedule-title { flex: 1; overflow: hidden; text-overflow: ellipsis; } .more-schedules { font-size: 10px; color: #909399; padding: 2px 4px; cursor: pointer; } .schedule-sidebar { width: 350px; background: white; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 20px; height: fit-content; max-height: calc(100vh - 120px); overflow-y: auto; } .sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed; } .schedule-detail h4 { margin: 0 0 15px 0; color: #303133; } .schedule-detail p { margin: 8px 0; color: #606266; line-height: 1.5; } .detail-actions { margin-top: 20px; display: flex; gap: 10px; } .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; } /* 优先样式 */ .schedule-item-on-date.priority-high { background-color: #fef0f0; color: #e54d42; border-left: 2px solid #f56c6c; font-weight: 500; } .schedule-item-on-date.priority-medium { background-color: #fdf6ec; color: #e6a23c; border-left: 2px solid #e6a23c; font-weight: 500; } .schedule-item-on-date.priority-low { background-color: #f0f9ff; color: #409eff; border-left: 2px solid #409eff; font-weight: 500; } /* 打印专用样式 */ @media print { /* 隐藏所有元素 */ body * { visibility: hidden; } /* 仅显示日历容器及其内容 */ .calendar-container, .calendar-container * { visibility: visible; } /* 定位到顶部,占满页面 */ .calendar-container { position: absolute; top: 0; left: 0; width: 100%; margin: 0; padding: 0; background: white !important; box-shadow: none; border: none; } /* 隐藏日历头部(月份切换区域) */ :deep(.el-calendar__header) { display: none !important; } /* 隐藏工具栏(打印按钮) */ .calendar-toolbar { display: none !important; } /* 隐藏侧边栏、弹窗等 */ .schedule-sidebar, .el-dialog__wrapper, .el-message, .el-message-box__wrapper { display: none !important; } /* 隐藏所有按钮 */ button, .el-button { display: none !important; } /* 打印优化:字体、间距、颜色 */ :deep(.el-calendar-day) { height: 80px !important; padding: 4px !important; font-size: 10px !important; border: 1px solid #eee !important; box-sizing: border-box; } .date-number { font-size: 12px !important; font-weight: bold; color: black !important; margin-bottom: 2px; } .schedule-item-on-date { font-size: 9px !important; padding: 1px 2px !important; margin-bottom: 1px !important; white-space: normal !important; line-height: 1.2 !important; overflow: hidden; text-overflow: ellipsis; background: #f9f9f9 !important; border-left: 2px solid #999 !important; color: black !important; font-weight: normal !important; } .more-schedules { font-size: 8px !important; color: #666 !important; } } </style>
12-03
<template> <div> <el-dialog title="设备导入" :visible.sync="dialogVisible" width="600px" custom-class="ota-dialog" :close-on-click-modal="false" > <div class="dialog-content"> <div class="action-section"> <div class="upload-section"> <el-upload action="#" :auto-upload="false" :before-upload="beforeUpload" :on-change="handleFileChange" :limit="1" :on-exceed="handleExceed" :file-list="fileList" drag > <i class="el-icon-upload"></i> <div class="el-upload__text"> <div>点击或拖拽文件到此处上传</div> <div class="el-upload__tip">支持.xlsx、.xls格式文件</div> </div> </el-upload> <div v-if="fileList.length" class="file-card"> <div class="file-info"> <i class="el-icon-document"></i> <div class="file-details"> <div class="file-name">{{ fileList[0].name }}</div> <div class="file-size">{{ formatFileSize(fileList[0].size) }}</div> <div v-if="firstRowString" class="file-data"> <el-tooltip effect="dark" :content="firstRowString" placement="top"> <span>首行数据: {{ truncateString(firstRowString) }}</span> </el-tooltip> </div> </div> </div> <div class="file-actions"> <i v-if="loading" class="el-icon-loading"></i> <el-button v-else type="danger" icon="el-icon-delete" circle @click="clearFile" ></el-button> </div> </div> </div> </div> </div> <div slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="handleConfirm" :disabled="!fileList.length || loading" > {{ loading ? &#39;处理中...&#39; : &#39;确 定&#39; }} </el-button> </div> </el-dialog> </div> </template> <script> import * as XLSX from &#39;xlsx&#39;; export default { data() { return { dialogVisible: false, fileList: [], firstRowString: &#39;&#39;, loading: false }; }, methods: { init() { this.dialogVisible = true; this.fileList = []; this.firstRowString = &#39;&#39;; }, beforeUpload(file) { const isValidType = [ &#39;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#39;, &#39;application/vnd.ms-excel&#39; ].includes(file.type); if (!isValidType) { this.$message.error(&#39;请上传Excel格式的文件 (.xlsx 或 .xls)&#39;); } return isValidType; }, // 处理文件选择变化 handleFileChange(file) { if (!file) return; const validTypes = [ &#39;application/vnd.ms-excel&#39;, // .xls &#39;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#39; // .xlsx ]; if (!validTypes.includes(file.raw.type)) { this.$message.error(&#39;请上传Excel格式的文件 (.xlsx 或 .xls)&#39;); this.clearFile(); return; } this.fileList = [file]; this.readExcelFirstRow(file.raw); }, // 读取Excel文件第一行 readExcelFirstRow(file) { this.loading = true; const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: &#39;array&#39; }); // 获取第一个工作表 const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; // 获取第一行数据 const range = XLSX.utils.decode_range(worksheet[&#39;!ref&#39;]); const firstRow = []; // 遍历第一行的所有列 for (let col = range.s.c; col <= range.e.c; col++) { const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col }); const cell = worksheet[cellAddress]; firstRow.push(cell ? cell.v : &#39;&#39;); } // 用分号拼接第一行数据 this.firstRowString = firstRow.join(&#39;;&#39;); this.$message.success(&#39;Excel文件解析成功&#39;); } catch (error) { console.error(&#39;Excel解析错误:&#39;, error); this.$message.error(&#39;Excel文件解析失败,请检查文件格式&#39;); this.firstRowString = &#39;&#39;; } finally { this.loading = false; } }; reader.onerror = () => { this.$message.error(&#39;文件读取失败&#39;); this.loading = false; }; reader.readAsArrayBuffer(file); }, // 处理确认操作 handleConfirm() { if (!this.firstRowString) { this.$message.warning(&#39;未解析到有效数据&#39;); return; } this.$message.success(`已获取首行数据: ${this.firstRowString}`); // 这里可以添加将数据发送到服务器的逻辑 // this.otaBatchUpgradeConfirm(this.firstRowString); // 关闭对话框 this.dialogVisible = false; }, // 清空文件 clearFile() { this.fileList = []; this.firstRowString = &#39;&#39;; }, handleExceed() { this.$message.warning(&#39;每次只能上传一个文件&#39;); }, formatFileSize(size) { if (size < 1024) return size + &#39; B&#39;; if (size < 1024 * 1024) return (size / 1024).toFixed(1) + &#39; KB&#39;; return (size / (1024 * 1024)).toFixed(1) + &#39; MB&#39;; }, // 截断长字符串 truncateString(str, maxLength = 30) { if (str.length <= maxLength) return str; return str.substring(0, maxLength) + &#39;...&#39;; } } }; </script> <style scoped> .action-section { display: flex; flex-direction: column; gap: 20px; } .upload-section { position: relative; display: flex; justify-content: center; } .file-card { margin-top: 15px; padding: 15px; border-radius: 8px; background-color: #f5f7fa; display: flex; align-items: center; justify-content: space-between; border: 1px solid #ebeef5; } .file-info { display: flex; align-items: center; gap: 12px; flex: 1; } .file-info i { font-size: 28px; color: #409EFF; } .file-details { line-height: 1.5; flex: 1; } .file-name { font-weight: 500; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-size { font-size: 12px; color: #909399; } .file-data { font-size: 12px; color: #67C23A; margin-top: 5px; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-actions { margin-left: 10px; } .dialog-footer { display: flex; justify-content: flex-end; padding-top: 15px; border-top: 1px solid #ebeef5; } .has-file >>> .el-upload-dragger { border: 1px dashed #67C23A; background-color: rgba(103, 194, 58, 0.05); } .el-icon-loading { font-size: 20px; color: #409EFF; margin-right: 10px; } </style> 上传文件后布局混乱
07-12
<template> <view class="upgrade-popup"> <!-- <image class="header-bg" src="../../static/upgrade_bg.png" mode="widthFix"></image> --> <view class="main"> <view class="version">发现新版本{{ versionName }}</view> <view class="content"> <text class="title">更新内容</text> <view class="desc" v-html="versionDesc"></view> </view> <!--下载状态-进度条显示 --> <view class="footer" v-if="isStartDownload"> <view class="progress-view" :class="{ &#39;active&#39;:!hasProgress }" @click="handleInstallApp"> <!-- 进度条 --> <view v-if="hasProgress" style="height: 100%;"> <view class="txt">{{ percentText }}</view> <view class="progress" :style="setProStyle"></view> </view> <view v-else> <view class="btn upgrade force">{{ isDownloadFinish ? &#39;立即安装&#39; : &#39;下载中...&#39; }}</view> </view> </view> </view> <!-- 强制更新 --> <view class="footer" v-else-if="isForceUpdate"> <view class="btn upgrade force" @click="handleUpgrade">立即更新</view> </view> <!-- 可选择更新 --> <view class="footer" v-else> <view class="btn close" @click="handleClose">以后再说</view> <view class="btn upgrade" @click="handleUpgrade">立即更新</view> </view> </view> </view> </template> <script setup> import { ref, computed } from &#39;vue&#39;; import { downloadApp, installApp } from &#39;./upgrade.js&#39;; import { onLoad, onBackPress } from "@dcloudio/uni-app"; // 响应式数据 const isForceUpdate = ref(false); // 是否强制更新 const versionName = ref(&#39;&#39;); // 版本名称 const versionDesc = ref(&#39;&#39;); // 更新说明 const downloadUrl = ref(&#39;&#39;); // APP 下载链接 const isDownloadFinish = ref(false); // 是否下载完成 const hasProgress = ref(false); // 是否能显示进度条 const currentPercent = ref(0); // 当前下载百分比 const isStartDownload = ref(false); // 是否开始下载 const fileName = ref(&#39;&#39;); // 下载后 app 本地路径名称 // 计算属性 const setProStyle = computed(() => { return { width: (510 * currentPercent.value / 100) + &#39;rpx&#39; // 510:按钮进度条宽度 }; }); const percentText = computed(() => { let percent = currentPercent.value; if (typeof percent !== &#39;number&#39; || isNaN(percent)) return &#39;下载中...&#39;; if (percent < 100) return `下载中${percent}%`; return &#39;立即安装&#39;; }); // 生命周期钩 onLoad((option) => { const obj = JSON.parse(decodeURIComponent(option.obj)); versionName.value = obj.contents; // 版本名称 versionDesc.value = "修复若干bug"; // 更新说明 downloadUrl.value = obj.url; // 下载链接 isForceUpdate.value = true; }); onBackPress((options) => { // 禁用返回 if (options.from === &#39;backbutton&#39;) { return true; } }); // 方法 const handleUpgrade = () => { if (downloadUrl.value) { isStartDownload.value = true; // 开始下载 App downloadApp(downloadUrl.value, (current) => { // 下载进度监听 hasProgress.value = true; currentPercent.value = current; }).then((newFileName) => { // 下载完成 isDownloadFinish.value = true; fileName.value = newFileName; if (newFileName) { // 自动安装 App handleInstallApp(); } }).catch((e) => { console.log(e, &#39;e&#39;); }); } else { uni.showToast({ title: &#39;下载链接不存在&#39;, icon: &#39;none&#39; }); } }; const handleInstallApp = () => { // 下载完成才能安装,防止下载过程中点击 if (isDownloadFinish.value && fileName.value) { installApp(fileName.value, () => { // 安装成功,关闭升弹窗 uni.navigateBack(); }); } }; const handleClose = () => { uni.navigateBack(); }; </script> <style> page { background: rgba(0, 0, 0, 0.5); /**设窗口背景半透明*/ } </style> <style lang="scss" scoped> .upgrade-popup { width: 580rpx; height: auto; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 20rpx; box-sizing: border-box; border: 1px solid #eee; } .header-bg { width: 100%; margin-top: -112rpx; } .main { padding: 10rpx 30rpx 30rpx; box-sizing: border-box; .version { font-size: 36rpx; color: #026DF7; font-weight: 700; width: 100%; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: 1px; } .content { margin-top: 60rpx; .title { font-size: 28rpx; font-weight: 700; color: #000000; } .desc { box-sizing: border-box; margin-top: 20rpx; font-size: 28rpx; color: #6A6A6A; max-height: 80vh; overflow-y: auto; } } .footer { width: 100%; display: flex; justify-content: center; align-items: center; position: relative; flex-shrink: 0; margin-top: 100rpx; .btn { width: 246rpx; display: flex; justify-content: center; align-items: center; position: relative; z-index: 999; height: 96rpx; box-sizing: border-box; font-size: 32rpx; border-radius: 10rpx; letter-spacing: 2rpx; &.force { width: 500rpx; } &.close { border: 1px solid #E0E0E0; margin-right: 25rpx; color: #000; } &.upgrade { background-color: #026DF7; color: white; } } .progress-view { width: 510rpx; height: 90rpx; display: flex; position: relative; align-items: center; border-radius: 6rpx; background-color: #dcdcdc; display: flex; justify-content: flex-start; padding: 0px; box-sizing: border-box; border: none; overflow: hidden; &.active { background-color: #026DF7; } .progress { height: 100%; background-color: #026DF7; padding: 0px; box-sizing: border-box; border: none; border-top-left-radius: 10rpx; border-bottom-left-radius: 10rpx; } .txt { font-size: 28rpx; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; } } } } </style> 代码转换uniapp VUE2
最新发布
12-18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值