这是一套基于 Vue 3 (Composition API) + Vue Router + Element Plus 实现的路由与页面设计方案。它完全满足了你在草图和原型图中所提出的业务逻辑(如:上传文件夹、正小数+字数限制校验、权限隐藏标签页、按钮联动禁用等)。
核心路由配置 (router/index.js)
这里采用嵌套路由(children)来实现三层页面的跳转:
-
首页(Dashboard/Home)
-
报表自动处理主页(功能中心)
-
子功能页(Excel在线操作 / 自有人员工时汇总)
TypeScript
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
// 假设你的首页组件
component: () => import('@/views/Home.vue'),
},
{
path: '/report-process',
name: 'ReportProcess',
// 第二张图:报表自动处理的功能中心主页
component: () => import('@/views/report/Index.vue'),
children: [
{
path: 'excel-online',
name: 'ExcelOnline',
component: () => import('@/views/report/ExcelOnline.vue'),
},
{
path: 'staff-hours',
name: 'StaffHoursSummary',
// 第三张图:自有人员工时汇总页面
component: () => import('@/views/report/StaffHoursSummary.vue'),
}
]
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
页面代码实现
1. 报表自动处理功能中心 (views/report/Index.vue)
对应第二张图片:包含"Excel在线操作"和"自有人员工时汇总"两个卡片入口。
html<template> <!-- 如果在子路由页面,则渲染子路由内容;否则渲染功能中心主页 --> <router-view v-if="$route.path !== '/report-process'"></router-view> <div v-else class="report-container"> <div class="header"> <h2>⭐ BBZDCL</h2> </div> <div class="content-panel"> <h3>功能中心</h3> <div class="card-grid"> <!-- 卡片 1: Excel在线操作 --> <div class="menu-card" @click="$router.push('/report-process/excel-online')"> <div class="icon-wrapper excel-icon"> <el-icon><Document /></el-icon> </div> <h4>Excel在线操作</h4> <p>多人协作编辑 Excel 表格</p> </div> <!-- 卡片 2: ZYRYGSHZ --> <div class="menu-card" @click="$router.push('/report-process/staff-hours')"> <div class="icon-wrapper staff-icon"> <el-icon><DocumentCopy /></el-icon> </div> <h4>ZYRYGSHZ</h4> <p>XXXXXX</p> </div> </div> </div> </div> </template> <script setup> import { Document, DocumentCopy } from "@element-plus/icons-vue"; </script> <style scoped> .report-container { padding: 24px; background: #f5f7fa; min-height: 100vh; } .header h2 { margin-bottom: 30px; } .content-panel h3 { margin-bottom: 20px; color: #333; } .card-grid { display: flex; gap: 24px; } .menu-card { background: #fff; border-radius: 12px; padding: 30px; width: 280px; text-align: center; cursor: pointer; transition: all 0.3s; border: 1px solid #e4e7ed; } .menu-card:hover { transform: translateY(-5px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .icon-wrapper { width: 70px; height: 70px; border-radius: 16px; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #fff; } .excel-icon { background-color: #67c23a; } .staff-icon { background-color: #409eff; } .menu-card h4 { margin: 10px 0; font-size: 18px; } .menu-card p { color: #909399; font-size: 14px; } </style>
2. 重点:自有人员工时汇总页 (views/report/StaffHoursSummary.vue)
对应第三张图片(及草图说明):
逻辑1:部门汇总 / 企划汇总标签页切换(企划汇总增加模拟角色权限控制)。
逻辑2 :点击上传文件可选择两个文件夹。
逻辑3 :总合工作量占比输入框限制:最长50字 + 必须为正小数。
逻辑4 :导出按钮联动------只有成功上传文件 且输入占比正确时才可点击。
html<template> <div class="summary-container"> <!-- 面包屑页头 --> <div class="page-header"> <div class="left"> <el-icon class="page-icon"><Document /></el-icon> <span class="title">BBZDCL</span> </div> <div class="right help-link"> <el-icon><QuestionFilled /></el-icon> 帮助 </div> </div> <!-- 主体卡片区域 --> <div class="main-card"> <!-- 标签页切换 --> <el-tabs v-model="activeTab" class="custom-tabs"> <el-tab-pane label="BMHZ" name="department"></el-tab-pane> <!-- 权限控制:模拟只有当前用户为特定用户/角色时才显示 --> <el-tab-pane v-if="currentUser.username === 'admin'" label="QHHZ" name="plan"> </el-tab-pane> </el-tabs> <!-- 工具栏操作区 --> <div class="toolbar"> <div class="left-actions"> <!-- 上传文件夹(directory 允许选择文件夹) --> <el-upload action="#" multiple directory :auto-upload="false" :show-file-list="false" :on-change="handleFolderUpload" > <el-button type="primary" :icon="Upload">上传文件</el-button> </el-upload> <!-- 导出按钮:受上传状态和输入框校验联合控制 --> <el-button type="success" :icon="Download" :disabled="!isExportAvailable" class="export-btn" > 导出 Excel 打包下载 </el-button> </div> <!-- 占比输入框及校验逻辑 --> <div class="right-input"> <span class="label-required">ZHGZLZB:</span> <el-input v-model="ratioInput" placeholder="输入框(字数限制50,要求正小数)" maxlength="50" show-word-limit :class="{ 'input-error': ratioError }" @input="validateRatio" style="width: 320px" /> <div v-if="ratioError" class="error-msg">请输入有效的正小数(例如:0.25)</div> </div> </div> <!-- 数据表格 --> <el-table :data="tableData" border style="width: 100%; margin-top: 20px" empty-text="请点击「上传文件」选择文件夹" > <el-table-column prop="fileName" label="名称" min-width="250" /> <el-table-column prop="status" label="状态" width="120" align="center"> <template #default="scope"> <el-tag :type="scope.row.status === '成功' ? 'success' : 'info'">{{ scope.row.status }}</el-tag> </template> </el-table-column> </el-table> </div> </div> </template> <script setup> import { ref, computed } from "vue"; import { Document, QuestionFilled, Upload, Download } from "@element-plus/icons-vue"; import { ElMessage } from "element-plus"; // 模拟当前登录用户数据(改为非 'admin' 即可测试隐藏"QHHZ") const currentUser = ref({ username: "admin" }); const activeTab = ref("department"); const ratioInput = ref(""); const ratioError = ref(false); const tableData = ref([]); // 上传的文件列表数据 // 校验正小数的正则 const validateRatio = (val) => { if (!val) { ratioError.value = false; return; } // 正小数正则(不包括0和负数) const reg = /^(0|[1-9]\d*)(\.\d+)?$/; if (reg.test(val) && parseFloat(val) > 0) { ratioError.value = false; } else { ratioError.value = true; } }; // 处理文件夹上传 const handleFolderUpload = (uploadFile) => { // 模拟读取文件夹下的文件并展示到表格中 const rawFile = uploadFile.raw; const path = rawFile.webkitRelativePath || rawFile.name; // 避免重复添加 if (tableData.value.some((item) => item.fileName === path)) return; tableData.value.push({ fileName: path, status: "成功", recordCount: Math.floor(Math.random() * 100) + 10, reportCount: Math.floor(Math.random() * 10) + 1, total: (Math.random() * 200).toFixed(2), }); ElMessage.success(`读取文件成功: ${rawFile.name}`); }; // 核心联动逻辑:是否允许导出 const isExportAvailable = computed(() => { // 1. 必须有上传成功的文件 const hasFiles = tableData.value.length > 0; // 2. 输入框不能为空且必须校验通过(无错误) const hasValidRatio = ratioInput.value && !ratioError.value; return hasFiles && hasValidRatio; }); </script> <style scoped> .summary-container { background: #f5f7fa; min-height: 100vh; padding-bottom: 30px; } .page-header { background: #fff; padding: 14px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e4e7ed; } .page-header .left { display: flex; align-items: center; gap: 8px; font-weight: bold; } .page-icon { color: #409eff; font-size: 20px; } .help-link { color: #409eff; cursor: pointer; font-size: 14px; display: flex; align-items: center; gap: 4px; } .main-card { background: #fff; margin: 20px; padding: 20px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); } .toolbar { display: flex; justify-content: space-between; align-items: flex-start; margin-top: 20px; flex-wrap: wrap; gap: 16px; } .left-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .right-input { position: relative; display: flex; align-items: center; gap: 8px; } .label-required { font-weight: bold; font-size: 15px; white-space: nowrap; } /* 输入框标红框提示 */ :deep(.input-error .el-input__wrapper) { box-shadow: 0 0 0 1px #f56c6c inset !important; } .error-msg { position: absolute; bottom: -20px; right: 0; color: #f56c6c; font-size: 12px; } </style>
代码设计亮点说明
-
文件夹上传功能 (
directory) :在el-upload上添加了原生的directory属性,点击"上传文件"按钮时,浏览器会自动弹出文件夹选择框而非单个文件选择框。 -
正小数与字数动态校验 :利用正则响应式监听输入,配合 Element Plus 的
maxlength自动在右侧展示0/50字数限制。如果输入不合法(如输入负数、字母、或留空),输入框会立刻呈现高亮红框提示。 -
按钮状态联合联动 (
computed) :采用计算属性isExportAvailable完美收拢业务逻辑。只有当tableData数组不为空(表示上传成功)且ratioInput输入合法时,导出打包下载按钮才会激活,逻辑清晰。
问题:
上传文件夹可以同时上传两个文件夹吗?或者可以上传文件夹套文件夹这样的吗?
在前端开发中,使用 Element Plus 的 directory(或原生 webkitdirectory)属性时,关于你提到的这两种复杂上传场景,实际的浏览器行为和底层逻辑是这样的:
1. 可以同时选择并上传"两个独立的文件夹"吗?
结论:原生文件选择弹窗不支持,但可以通过"拖拽"或"分批上传"来实现。
-
常规点击上传(不支持多选文件夹) : 当你点击按钮触发系统的文件选择器时,即使你配置了
multiple,在绝大多数操作系统(Windows / macOS)的标准弹窗里,你一次只能双击选中并"打开/上传"一个文件夹 。你无法按住Ctrl或Shift同时选中两个平级的文件夹点击确定。 -
完美的替代解决方案:
-
方案 A(拖拽上传) :如果你使用 Element Plus 的拖拽上传区域(
drag模式),用户可以在桌面同时选中两个文件夹,直接拖进上传区域,前端是可以同时接收到这两个文件夹内所有文件的。 -
方案 B(分批多次点击):用户可以点击第一次,选择"文件夹 A"上传;然后再次点击,选择"文件夹 B"上传。我们在前端代码中通过一个数组把它们累加起来即可。
-
2. 可以上传"文件夹套文件夹(层级嵌套)"吗?
结论:完全可以!但正如你截图中的官方文档所说,数据会被"扁平化处理"。
当你选择了一个最外层的母文件夹,而这个文件夹里面还嵌套了子文件夹、孙文件夹时:
-
浏览器行为 :浏览器会递归地把这个母文件夹下所有层级里的文件全部读取出来。
-
数据的"扁平化":正如文档所述,Element 会把所有文件抽出来放进一个一维数组里传给你的后端或前端回调。
-
如何保留层级关系? 虽然数组是扁平的,但每一个文件对象上都包含一个关键属性:
file.raw.webkitRelativePath(文件的相对路径)。 例如:-
母文件夹/文件1.xlsx -
母文件夹/子文件夹A/文件2.xlsx -
母文件夹/子文件夹A/孙文件夹B/文件3.xlsx
你的后端(或前端)完全可以通过解析这个
webkitRelativePath路径字符串,在服务器端或者页面上完美还原出原本的"嵌套树状结构"! -
💡 针对你业务的落地建议
在写代码时,为了完美支持你原型图上的需求,建议这样处理:
TypeScript
// 前端数组,用来接收多次上传或拖拽的多个文件夹文件
const allUploadedFiles = ref([]);
const handleFolderUpload = (uploadFile) => {
const rawFile = uploadFile.raw;
// 核心:通过 webkitRelativePath 拿到完整的层级路径
console.log("当前文件相对路径:", rawFile.webkitRelativePath);
// 将文件追加到列表中,这样用户分两次上传两个文件夹也能完美兼容
allUploadedFiles.value.push(rawFile);
// 可以在这里通过解析路径,判断用户一共上传了多少个不同的顶级文件夹
};
-
如果业务一定要用户一次性 交齐两个文件夹,建议页面上直接提示用户:"请将两个文件夹打包拖拽至此处,或分两次点击上传"。
-
如果需要区分不同的文件夹,在前端通过对
webkitRelativePath字符串进行截取(取第一个/前的名字)就可以轻松将它们归类。
