<script setup lang="ts"> import { onMounted, reactive, ref, watch } from "vue"; import { $t } from "@/locales"; import { UploadFileInfo, UploadInst } from "naive-ui"; import { exportXlsx, SheetData } from "@/utils/export-excel"; import { fetchAddInterlock, fetchUpdateInterlock, fetchUploadInterlockLedger} from "@/service/api"; import { useEquipment } from "@/hooks/common/useEquipment"; defineOptions({ name: "LedgerUploadDrawer", }); interface Props { /** the type of operation */ } // 引入设备钩子 const { loading: equipLoading, filteredequipOptions, handleequipSearch, loadEquipMap, getequip, nameToequipMap, resetEquipSearch } = useEquipment(); onMounted(() => { loadEquipMap(); }); const props = defineProps<Props>(); const fileListLengthRef = ref(0); const uploadRef = ref<UploadInst | null>(null); // 定义符合实际接口返回的数据结构 interface UploadResult { total_files: number; // 总文件数(数字类型) success_files: number; // 成功文件数(数字类型) total_records: number; // 导入记录数(数字类型) details: FileDetail[]; // 文件详情 } interface FileDetail { filename: string; // 文件名 status: "success" | "partial" | "fail"; // 状态 imported: number; // 导入记录数(数字类型) errors: string[]; // 错误信息 } const uploadResult = ref<UploadResult | null>(null); const loading = ref(false); const fileList = ref<UploadFileInfo[]>([]); // 使用单独的ref管理文件列表 const fileDetail = ref<FileDetail | null>(null); interface Emits { (e: "submitted"): void; } const emit = defineEmits<Emits>(); const visible = defineModel<boolean>("visible", { default: false, }); //const model: any = reactive(createDefaultModel()); const model = reactive({ // 不再需要file属性 }); function createDefaultModel(): any { return { file: null, }; } function handleInitModel() { Object.assign(model, createDefaultModel()); } function closeDrawer() { visible.value = false; } async function handleSubmit() { // request if (fileList.value.length === 0) { window.$message?.error("没有待上传文件"); return; } loading.value = true; uploadResult.value = null; // 重置结果 try { const formData = new FormData(); // 添加所有文件到FormData fileList.value.forEach((fileInfo) => { if (fileInfo.file) { formData.append("files", fileInfo.file); } }); const { data, error } = await fetchUploadInterlockLedger(formData); console.log(data) if (!error && data) { // 使用实际接口返回的数据结构 uploadResult.value = { total_files: data.total_files || fileList.value.length, success_files: data.success_files || 0, total_records: data.total_records || 0, details: data.details }; // 显示详细上传结果 // if (uploadResult.value?.success_files <= uploadResult.value?.total_files){ // window.$message?.warning( // `${uploadResult.value?.success_files}/${uploadResult.value?.total_files}个文件上传成功` // ); // } let allErrors: any[] = []; // 存储所有错误信息 if (uploadResult.value.details.length > 0) { for (var val in uploadResult.value.details) { if (uploadResult.value.details[val].errors.length > 0) { // 将当前元素的errors数组拼接到总错误列表 allErrors = allErrors.concat(uploadResult.value.details[val].errors); window.$message?.error(`${uploadResult.value.details[val].filename}:${allErrors.join('\n')}`); } } // 统一显示所有错误(用换行符分隔) // if (allErrors.length > 0) { // window.$message?.warning(allErrors.join('\n')); // } } console.log(allErrors) if (allErrors.length == 0) { window.$message?.success("上传成功"); } } } catch (err) { window.$message?.error("网络错误"); } finally { loading.value = false; } closeDrawer(); emit("submitted"); } function handleChange(data: { fileList: UploadFileInfo[] }) { fileList.value = data.fileList; // 更新文件列表 uploadResult.value = null; // 重置结果 } //sheet={} function downloadTemplate() { const equipList = Object.keys(nameToequipMap.value); // 创建主工作表 const mainSheet: SheetData = { sheetName: "互锁台账列表", data: [ // 表头行 [ "报警名称", "触发器", "满足条件", "渠道号", "报警级别", "报警描述", "触发器渠道", "报警动作", "报警动作Data通道", "报警动作描述", "互锁类型", "所属模块", "模块类型", "所属产品", // 需要设置下拉选择的列 "所属部门", "备注" ], ], // 优化数据验证:使用动态范围引用 dataValidations: [{ type: 'list', formula1: `设备选项说明!$A$2:$A$${equipList.length + 1}`, // 引用另一工作表的数据 ref: "N2:N1000" // 应用范围:N列第2行到1000行 }] }; // 创建设备选项说明表 const helpSheet: SheetData = { sheetName: "设备选项说明", data: [ ["设备选项列表"], ...equipList.map(item => [item]) ], dataValidations:[] }; exportXlsx([mainSheet, helpSheet], "互锁台账导入模板"); } // watch(visible, () => { // if (visible.value) { // handleInitModel(); // } // }); function clearFiles() { fileList.value = []; if (uploadRef.value) { uploadRef.value.clear(); } uploadResult.value = null; } watch(visible, (newVal) => { if (newVal) { clearFiles(); // 每次打开时清空文件 } }); </script> <template> <NModal v-model:show="visible" preset="dialog" :title="$t('common.upload')"> <NPopover trigger="hover"> <template #trigger> <NButton @click="downloadTemplate" text type="primary">{{ $t("common.templateDownload") }}</NButton> </template> <span>点此下载导入模板</span> </NPopover> <NUpload accept=".csv,.xls,.xlsx" ref="uploadRef" :default-upload="false" :max="10" multiple :file-list="fileList" @change="handleChange" > <NUploadDragger @click="clearFiles"> <div style="margin-bottom: 12px"> <icon-mdi-tray-upload class="text-icon" /> </div> <NText style="font-size: 16px"> 点击或者拖动文件到该区域来上传 </NText> <NP depth="3" style="margin: 8px 0 0 0"> 请参考上传模板来提供数据,支持csv,xls,xlsx格式,单次最多上传10个文件 </NP> </NUploadDragger> </NUpload> <template #action> <NSpace :size="16"> <NButton @click="closeDrawer">{{ $t("common.cancel") }}</NButton> <NButton type="primary" @click="handleSubmit" :loading="loading" :disabled="fileList.length === 0" > {{ $t("common.confirm") }}</NButton> </NSpace> </template> </NModal> </template> <style scoped></style> 用户打开模板后,"所属产品"列会有下拉箭头 点击下拉箭头会显示设备选项列表 用户只能选择列表中的设备,不能手动输入
时间: 2025-07-26 14:21:18 浏览: 16
<think>根据用户描述,当前代码已经实现了在Excel模板的“所属产品”列设置下拉选择框,并且用户只能选择列表中的设备,不能手动输入。这通过数据验证(dataValidation)中的列表(list)类型实现,且引用另一个工作表(设备选项说明)中的设备列表。
但是,用户可能希望进一步优化或了解相关实现细节。下面提供一些关键点说明:
1. **数据验证设置**:
- 在`mainSheet`的`dataValidations`中,我们设置了类型为`list`的验证,并引用了另一个工作表(设备选项说明)中的单元格范围(A2到A列最后一个设备名)。
- `ref: "N2:N1000"`表示这个验证应用于主工作表的N列(第2行到第1000行)。
2. **设备列表来源**:
- 设备列表来自`nameToequipMap.value`的键(即设备名称),通过`Object.keys`获取。
- 在帮助表(设备选项说明)中,我们将设备列表放在A列,从第2行开始(第1行是标题)。
3. **禁止手动输入**:
- 设置数据验证为列表(list)后,Excel默认只允许从下拉列表中选择,不允许手动输入不在列表中的值。
4. **模板使用提示**:
- 用户下载模板后,在“所属产品”列(N列)的单元格中会有一个下拉箭头,点击即可选择设备。
- 如果用户尝试输入不在列表中的值,Excel会弹出错误提示。
### 注意事项
- 确保`nameToequipMap.value`已经正确加载了设备数据,否则设备列表可能为空。
- 设备列表长度变化时,动态计算引用范围(`设备选项说明!$A$2:$A$${equipList.length + 1}`)可以确保所有设备都被包含。
### 可能的问题及优化
- **性能问题**:如果设备数量非常大(比如超过1万),在Excel中打开模板可能会变慢。可以考虑在模板中只放置常用设备,或者提供搜索功能(但Excel本身对大量下拉列表支持有限)。
- **模板更新**:当设备列表更新后,用户需要重新下载模板才能获取最新的设备列表。
### 代码确认
当前`downloadTemplate`函数已经按照上述逻辑实现,用户反馈的功能已经满足。
§§相关问题§§
1. 如果设备列表过长(例如超过1万条),如何优化Excel模板的性能?
2. 如何实现设备列表的分页显示或搜索功能(在Excel的下拉列表中)?
3. 当设备列表发生变化时,如何让已下载的模板自动更新设备选项?
4. 除了设备列表,如何为其他列设置类似的下拉选择(如报警级别、互锁类型等)?
5. 如何防止用户修改“设备选项说明”工作表中的设备列表?</think>### 实现Excel下拉选择功能的关键代码分析
在您提供的代码中,"所属产品"列的下拉选择功能是通过以下关键部分实现的:
```typescript
function downloadTemplate() {
const equipList = Object.keys(nameToequipMap.value);
const mainSheet: SheetData = {
// ...其他配置
dataValidations: [{
type: 'list',
formula1: `设备选项说明!$A$2:$A$${equipList.length + 1}`,
ref: "N2:N1000" // N列对应"所属产品"
}]
};
const helpSheet: SheetData = {
sheetName: "设备选项说明",
data: [
["设备选项列表"],
...equipList.map(item => [item])
]
};
exportXlsx([mainSheet, helpSheet], "互锁台账导入模板");
}
```
### 实现原理说明
1. **数据验证设置**:
- `type: 'list'`:指定为下拉列表类型
- `formula1`: 指向"设备选项说明"工作表中的设备列表范围
- `ref: "N2:N1000"`:将验证规则应用到N列("所属产品"列)的第2-1000行
2. **设备选项来源**:
- 创建单独的"设备选项说明"工作表
- 将设备名称列表填充到该工作表的A列
- 主表通过跨工作表引用获取选项数据
3. **禁止手动输入**:
- Excel默认会强制用户只能从下拉列表中选择
- 如果用户尝试手动输入非列表值,Excel会拒绝并提示错误
### 使用效果验证
当用户打开模板后:
1. "所属产品"列(N列)每个单元格右侧会显示下拉箭头
2. 点击箭头会显示所有设备名称列表
3. 用户只能从列表中选择,无法手动输入其他值
### 可能的问题排查
如果下拉功能未生效,请检查:
1. `equipList`是否包含有效的设备名称
2. Excel文件是否包含两个工作表(主表和设备选项说明表)
3. 在Excel中查看N列的数据验证设置是否正确
4. 导出的XLSX文件是否被正确生成(无报错)
阅读全文
相关推荐

















