fix: 第一版错误修改前的存档。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { Upload } from "@icon-park/vue-next";
|
||||
import { Upload, Download } from "@icon-park/vue-next";
|
||||
import BaseForm from "../base-form/BaseForm.vue";
|
||||
import { ElMessage, type UploadInstance, type UploadUserFile } from "element-plus";
|
||||
import * as XLSX from "xlsx";
|
||||
@@ -15,6 +15,7 @@ const props = defineProps({
|
||||
uploadDesc: String,
|
||||
itemArrayName: { type: String, required: true },
|
||||
mappingConfig: { type: Object, required: true },
|
||||
templateFileName: { type: String, default: "模板" },
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
const form = defineModel<any>("form");
|
||||
@@ -104,16 +105,13 @@ const handleFileChange = (file: any) => {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
ElMessage.error("文件大小不能超过 10MB");
|
||||
clearAllData();
|
||||
return;
|
||||
}
|
||||
|
||||
clearAllData();
|
||||
|
||||
// 更新当前文件显示
|
||||
currentFile.value = file;
|
||||
|
||||
// 解析 Excel
|
||||
// 解析 Excel(追加模式,不清空原有数据)
|
||||
parseExcel(file.raw);
|
||||
};
|
||||
const parseExcel = (file: any) => {
|
||||
@@ -149,11 +147,15 @@ const parseExcel = (file: any) => {
|
||||
form.value[props.itemArrayName] = [];
|
||||
}
|
||||
|
||||
// 使用 splice 替换所有内容,这是最安全的触发数组更新的方式
|
||||
form.value[props.itemArrayName].splice(0, form.value[props.itemArrayName].length, ...mappedData);
|
||||
// 追加数据到现有数组
|
||||
form.value[props.itemArrayName].push(...mappedData);
|
||||
}
|
||||
|
||||
ElMessage.success(`解析成功,共 ${mappedData.length} 条物料数据`);
|
||||
ElMessage.success(`解析成功,共追加 ${mappedData.length} 条数据`);
|
||||
|
||||
// 清空上传组件的文件列表,允许用户再次选择文件进行追加
|
||||
uploadRef.value?.clearFiles();
|
||||
currentFile.value = null;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("文件解析失败,请检查文件格式");
|
||||
@@ -165,32 +167,36 @@ const parseExcel = (file: any) => {
|
||||
// 【新增】存储当前上传的文件信息,用于显示和移除
|
||||
const currentFile = ref<UploadUserFile | null>(null);
|
||||
|
||||
// 【新增】深度清理函数
|
||||
const resetFormData = () => {
|
||||
// 1. 清空表单验证
|
||||
// 【新增】只清理表格数据(保留基本表单数据)
|
||||
const clearTableData = () => {
|
||||
// 1. 清空表格数据
|
||||
if (form.value && Array.isArray(form.value[props.itemArrayName])) {
|
||||
form.value[props.itemArrayName].splice(0, form.value[props.itemArrayName].length);
|
||||
}
|
||||
|
||||
// 2. 清空表单验证(仅表格相关)
|
||||
if (baseFormComponentRef.value) {
|
||||
// 方式 A: 调用暴露的 clearValidate 方法
|
||||
baseFormComponentRef.value.clearValidate();
|
||||
// 方式 B: 或者访问内部 formRef (如果暴露了)
|
||||
// baseFormComponentRef.value.formRef?.clearValidate();
|
||||
}
|
||||
|
||||
// 2. 重置 form 对象 (保留非表格字段?通常重置是全部清空或设为初始值,这里设为空对象)
|
||||
if (form.value) {
|
||||
// 只清空表格数据,保留其他表单字段?还是全部重置?
|
||||
// 根据需求“数据清理不干净”,通常指表格和文件。
|
||||
// 这里我们清空表格数组和文件,其他字段由 BaseForm 的 reset 逻辑处理或手动置空
|
||||
if (Array.isArray(form.value[props.itemArrayName])) {
|
||||
form.value[props.itemArrayName].splice(0, form.value[props.itemArrayName].length);
|
||||
}
|
||||
ElMessage.success("表格数据已清空");
|
||||
};
|
||||
|
||||
// 【新增】深度清理函数(清理表格+文件,保留基本表单数据)
|
||||
const resetFormData = () => {
|
||||
// 1. 清空表格数据
|
||||
if (form.value && Array.isArray(form.value[props.itemArrayName])) {
|
||||
form.value[props.itemArrayName].splice(0, form.value[props.itemArrayName].length);
|
||||
}
|
||||
|
||||
// 3. 清空文件状态
|
||||
// 2. 清空文件状态
|
||||
currentFile.value = null;
|
||||
uploadRef.value?.clearFiles();
|
||||
|
||||
// 4. 如果需要完全重置 form 为初始空对象,取消下面注释
|
||||
form.value = {};
|
||||
// 3. 清空表单验证
|
||||
if (baseFormComponentRef.value) {
|
||||
baseFormComponentRef.value.clearValidate();
|
||||
}
|
||||
};
|
||||
|
||||
// 【新增】增加一行
|
||||
@@ -220,6 +226,43 @@ const removeFile = () => {
|
||||
clearAllData();
|
||||
ElMessage.success("文件已移除,表格数据已清空");
|
||||
};
|
||||
|
||||
// 【新增】下载模板文件
|
||||
const downloadTemplate = () => {
|
||||
// 1. 根据 mappingConfig 生成表头
|
||||
const headers: string[] = [];
|
||||
for (const [targetKey, config] of Object.entries(props.mappingConfig)) {
|
||||
const fieldConfig = config as ExcelDataMapping;
|
||||
// 使用 sourceKey 作为表头,如果是数组则使用第一个
|
||||
let header: string;
|
||||
if (Array.isArray(fieldConfig.sourceKey)) {
|
||||
header = fieldConfig.sourceKey[0];
|
||||
} else {
|
||||
header = fieldConfig.sourceKey;
|
||||
}
|
||||
headers.push(header);
|
||||
}
|
||||
|
||||
// 2. 创建工作簿
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// 3. 创建工作表(只有表头,没有数据行)
|
||||
const ws = XLSX.utils.aoa_to_sheet([headers]);
|
||||
|
||||
// 4. 设置列宽
|
||||
const colWidths = headers.map(() => ({ wch: 20 }));
|
||||
ws["!cols"] = colWidths;
|
||||
|
||||
// 5. 将工作表添加到工作簿
|
||||
XLSX.utils.book_append_sheet(wb, ws, "模板");
|
||||
|
||||
// 6. 生成文件并下载
|
||||
const fileName = `${props.templateFileName}.xlsx`;
|
||||
XLSX.writeFile(wb, fileName);
|
||||
|
||||
ElMessage.success("模板文件下载成功");
|
||||
};
|
||||
|
||||
// 监听 BaseForm 发出的 reset 事件
|
||||
const handleBaseFormReset = () => {
|
||||
resetFormData();
|
||||
@@ -229,6 +272,8 @@ defineExpose({
|
||||
resetFormData,
|
||||
addRow,
|
||||
deleteRow,
|
||||
clearTableData,
|
||||
downloadTemplate,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
@@ -266,6 +311,10 @@ defineExpose({
|
||||
<span class="tip-desc" v-if="props.uploadDesc">{{ uploadDesc }}</span>
|
||||
</span>
|
||||
</el-upload>
|
||||
<el-button type="success" @click="downloadTemplate">
|
||||
<el-icon><Download /></el-icon>
|
||||
下载模板文件
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
@@ -288,7 +337,7 @@ defineExpose({
|
||||
<el-icon><Plus /></el-icon>
|
||||
增加一行
|
||||
</el-button>
|
||||
<el-button type="warning" link @click="resetFormData">
|
||||
<el-button type="warning" link @click="clearTableData">
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空表格
|
||||
</el-button>
|
||||
|
||||
155
src/components/base/base-tree-select/BaseTreeSelect.vue
Normal file
155
src/components/base/base-tree-select/BaseTreeSelect.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import { get } from "@/common/http/request";
|
||||
|
||||
interface TreeNode {
|
||||
id: number | string;
|
||||
label: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
url: String,
|
||||
data: Array as () => TreeNode[],
|
||||
placeholder: String,
|
||||
nodeKey: { type: String, default: "id" },
|
||||
labelKey: { type: String, default: "label" },
|
||||
childrenKey: { type: String, default: "children" },
|
||||
clearable: { type: Boolean, default: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const modelValue = defineModel<number | string | undefined>({ required: true });
|
||||
const treeData = ref<TreeNode[]>([]);
|
||||
const treeSelectRef = ref();
|
||||
const popoverVisible = ref(false);
|
||||
const selectedLabel = ref("");
|
||||
|
||||
const defaultProps = {
|
||||
children: props.childrenKey,
|
||||
label: props.labelKey,
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
if (props.url) {
|
||||
const rawData = await get(props.url).then(res => res.data);
|
||||
treeData.value = rawData || [];
|
||||
} else if (props.data) {
|
||||
treeData.value = props.data;
|
||||
}
|
||||
};
|
||||
|
||||
const findNodeLabel = (nodes: TreeNode[], targetId: number | string): string => {
|
||||
for (const node of nodes) {
|
||||
if (node[props.nodeKey as keyof TreeNode] === targetId) {
|
||||
return node[props.labelKey as keyof TreeNode] as string;
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const label = findNodeLabel(node.children, targetId);
|
||||
if (label) return label;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleNodeClick = (node: TreeNode) => {
|
||||
modelValue.value = node[props.nodeKey as keyof TreeNode] as number | string;
|
||||
selectedLabel.value = node[props.labelKey as keyof TreeNode] as string;
|
||||
popoverVisible.value = false;
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
modelValue.value = undefined;
|
||||
selectedLabel.value = "";
|
||||
};
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
newVal => {
|
||||
if (newVal !== undefined && newVal !== null) {
|
||||
selectedLabel.value = findNodeLabel(treeData.value, newVal);
|
||||
} else {
|
||||
selectedLabel.value = "";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => treeData.value,
|
||||
() => {
|
||||
if (modelValue.value !== undefined && modelValue.value !== null) {
|
||||
selectedLabel.value = findNodeLabel(treeData.value, modelValue.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadData);
|
||||
|
||||
defineExpose({
|
||||
reload: loadData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
ref="treeSelectRef"
|
||||
v-model:visible="popoverVisible"
|
||||
placement="bottom-start"
|
||||
:width="200"
|
||||
trigger="click"
|
||||
:disabled="disabled"
|
||||
popper-class="tree-select-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="selectedLabel"
|
||||
readonly
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable"
|
||||
@clear="handleClear"
|
||||
class="tree-select-input"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="el-input__icon">
|
||||
<arrow-down :class="{ 'is-reverse': popoverVisible }" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<div class="tree-select-dropdown">
|
||||
<el-tree
|
||||
:data="treeData"
|
||||
:props="defaultProps"
|
||||
:node-key="nodeKey"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:current-node-key="modelValue"
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-select-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tree-select-input :deep(.el-input__inner) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-select-input :deep(.el-input__icon) {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.tree-select-input :deep(.el-input__icon.is-reverse) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.tree-select-dropdown {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,13 @@ const loadData = async () => {
|
||||
if (props.data !== undefined && treeData.value === undefined) {
|
||||
treeData.value = props.data;
|
||||
}
|
||||
// 树数据加载完成后,如果有选中的值,使用 setCheckedKeys 设置选中状态
|
||||
// 因为 default-checked-keys 只在组件初始化时生效
|
||||
nextTick(() => {
|
||||
if (treeRef.value && treeCheck.value !== undefined && treeCheck.value.length > 0) {
|
||||
treeRef.value.setCheckedKeys(treeCheck.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearSelect = () => {
|
||||
@@ -35,7 +42,8 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const checkHandle = (checkedNode: any, treeStatus: any) => {
|
||||
treeCheck.value = treeStatus.checkedKeys;
|
||||
// 将字符串类型的 id 转换为数字类型
|
||||
treeCheck.value = treeStatus.checkedKeys.map((id: string | number) => (typeof id === "string" ? Number(id) : id));
|
||||
};
|
||||
|
||||
const clickHandle = (clickNode: any, nodeAttr: any, treeNode: any, event: any) => {
|
||||
@@ -43,11 +51,13 @@ const clickHandle = (clickNode: any, nodeAttr: any, treeNode: any, event: any) =
|
||||
};
|
||||
|
||||
const initSelect = (newV: number[]) => {
|
||||
treeRef.value?.setCheckedKeys(newV);
|
||||
if (treeRef.value && newV !== undefined && newV.length > 0) {
|
||||
treeRef.value.setCheckedKeys(newV);
|
||||
}
|
||||
};
|
||||
|
||||
watch(treeCheck, (newV: number[] | undefined) => {
|
||||
if (newV === undefined) return;
|
||||
if (newV === undefined || newV.length === 0) return;
|
||||
initSelect(newV);
|
||||
});
|
||||
|
||||
@@ -64,11 +74,12 @@ defineExpose({
|
||||
ref="treeRef"
|
||||
:props="defaultProps"
|
||||
:data="treeData"
|
||||
v-model="treeCheck"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:show-checkbox="showCheckbox"
|
||||
:default-checked-keys="treeCheck"
|
||||
check-strictly
|
||||
@check="checkHandle"
|
||||
@node-click="clickHandle"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { $t } from "@/common/languages";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
authShowFunc: Function,
|
||||
@@ -9,6 +10,16 @@ const emit = defineEmits<{ "operate-button-click": [eventName: string, row: any]
|
||||
|
||||
const buttonList = useRoute().meta.toolButtonAuth ?? [];
|
||||
|
||||
// 根据按钮数量计算列宽
|
||||
// 中文:2字按钮约40px,4字按钮约60px
|
||||
// 英文:1词按钮约40px,3词按钮约80px
|
||||
// 间隙:每个按钮间8px
|
||||
const columnWidth = computed(() => {
|
||||
const count = buttonList.length;
|
||||
// 基础边距20px + 每个按钮平均45px + 间隙
|
||||
return 20 + count * 45 + (count - 1) * 8;
|
||||
});
|
||||
|
||||
const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
|
||||
if (props.authShowFunc !== undefined) {
|
||||
return props.authShowFunc(row, button);
|
||||
@@ -17,18 +28,49 @@ const authShowFunc = (row: any, button: globalThis.ButtonProp) => {
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<el-table-column :label="$t('_prop.common.operate')" v-if="buttonList?.length !== 0">
|
||||
<el-table-column
|
||||
:label="$t('_prop.common.operate')"
|
||||
v-if="buttonList?.length !== 0"
|
||||
:min-width="columnWidth"
|
||||
align="center"
|
||||
fixed="right"
|
||||
class-name="operate-column"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template v-for="button of buttonList" :key="button.buttonName">
|
||||
<el-button
|
||||
v-if="authShowFunc(row, button)"
|
||||
:type="button.colorType || 'primary'"
|
||||
@click="emit('operate-button-click', button.eventName, row)"
|
||||
link
|
||||
>
|
||||
{{ $t("_button." + button.eventName) }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="operate-buttons">
|
||||
<template v-for="button of buttonList" :key="button.buttonName">
|
||||
<el-button
|
||||
v-if="authShowFunc(row, button)"
|
||||
:type="button.colorType || 'primary'"
|
||||
@click="emit('operate-button-click', button.eventName, row)"
|
||||
link
|
||||
size="small"
|
||||
class="operate-btn"
|
||||
>
|
||||
{{ $t("_button." + button.eventName) }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<style scoped>
|
||||
.operate-buttons {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.operate-btn {
|
||||
padding: 2px 4px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.operate-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,7 @@ const getTagType = (code: number | null): string => {
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<el-table-column :label="$t('_prop.common.status')" :prop="statusParamName">
|
||||
<el-table-column :label="$t('_prop.common.status')" :prop="statusParamName" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<!-- 没有权限按钮时,显示带框标签 -->
|
||||
<span v-if="buttonList.length === 0">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, nextTick } from "vue";
|
||||
import { ref, reactive, nextTick, computed } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import BasePageableTable from "../base-pageable-table/BasePageableTable.vue";
|
||||
import type { SearcherProp } from "../search-bar/SearchBarType";
|
||||
@@ -19,6 +19,14 @@ const props = defineProps({
|
||||
itemIdName: String,
|
||||
itemFieldName: String,
|
||||
rowKey: { type: String, default: "id" },
|
||||
useAuth: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const buttonList = useRoute().meta.toolButtonAuth ?? [];
|
||||
|
||||
const hasShowItemAuth = computed(() => {
|
||||
if (!props.useAuth) return true;
|
||||
return buttonList.some(item => item.eventName === "showItem");
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
@@ -209,7 +217,7 @@ defineExpose({
|
||||
</template>
|
||||
|
||||
<template #columns>
|
||||
<el-table-column v-if="itemUrl || itemFieldName" type="expand" fixed="left" width="50">
|
||||
<el-table-column v-if="(itemUrl || itemFieldName) && hasShowItemAuth" type="expand" fixed="left" width="50">
|
||||
<template #default="{ row, expanded }">
|
||||
<div :class="['item-container', { 'is-expanded': expanded }]">
|
||||
<div v-if="loadingItems[getRowKey(row)]" class="loading-state">
|
||||
|
||||
@@ -19,6 +19,7 @@ const props = defineProps({
|
||||
treeSideNodeName: String,
|
||||
treeSideTitle: String,
|
||||
});
|
||||
const emit = defineEmits(["update:current-node"]);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
@@ -32,6 +33,10 @@ const resetClick = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
watch(treeSelect, newVal => {
|
||||
emit("update:current-node", newVal);
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
if (props.data !== undefined) {
|
||||
total.value = props.data.length;
|
||||
|
||||
Reference in New Issue
Block a user