<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue + ElementUI 文件与印章 Demo</title>
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
<style>
body {
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
background: #f5f7fa;
}
.page {
max-width: 1200px;
margin: 0 auto;
}
.card {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
min-height: 560px;
}
.title {
margin: 0 0 14px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.left-list {
border: 1px solid #ebeef5;
border-radius: 6px;
max-height: 500px;
overflow: auto;
}
.left-item {
padding: 10px 12px;
border-bottom: 1px solid #f2f6fc;
}
.left-item:last-child {
border-bottom: none;
}
.file-content {
margin-top: 4px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
.stamp-tag {
margin-right: 6px;
margin-bottom: 6px;
}
.hint {
margin-top: 12px;
color: #909399;
font-size: 12px;
line-height: 1.6;
}
</style>
</head>
<body>
<div id="app" class="page">
<el-row :gutter="16">
<el-col :span="8">
<div class="card">
<h3 class="title">文件列表</h3>
<div class="left-list">
<div class="left-item" v-for="file in files" :key="file.id">
<el-checkbox
:value="isFileChecked(file.id)"
@change="onFileCheckChange(file, $event)"
>
{{ file.name }}(ID: {{ file.id }})
</el-checkbox>
<div class="file-content">内容:{{ file.content }}</div>
</div>
</div>
<div class="hint">
说明:勾选文件会进入右侧表格。<br />
每个文件可选择多个印章;每个印章会在表格中占一行。<br />
只有某个文件在右侧所有行都删除后,左侧才会取消勾选。
</div>
</div>
</el-col>
<!-- 右侧:选中文件表格 -->
<el-col :span="16">
<div class="card">
<h3 class="title">已选文件(按印章拆行展示)</h3>
<el-table
:data="tableRows"
:row-key="getRowKey"
border
style="width: 100%"
empty-text="暂无数据,先在左侧勾选文件"
>
<el-table-column label="文件ID" width="100">
<template slot-scope="scope">{{ scope.row.fileId }}</template>
</el-table-column>
<el-table-column label="文件名称" width="180">
<template slot-scope="scope">{{ scope.row.fileName }}</template>
</el-table-column>
<el-table-column label="选择的印章" min-width="280">
<template slot-scope="scope">
<el-tag
v-if="scope.row.stampId"
size="small"
type="success"
class="stamp-tag"
>
{{ stampNameById(scope.row.stampId) }}
</el-tag>
<span v-else style="color: #c0c4cc">未选择印章</span>
<el-button
size="mini"
type="primary"
plain
@click="openStampDialog(scope.row.fileId)"
>
选择印章
</el-button>
</template>
</el-table-column>
<el-table-column label="文件内容" min-width="240">
<template slot-scope="scope">{{ scope.row.content }}</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="removeRow(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
title="选择印章"
:visible.sync="stampDialogVisible"
width="360px"
:close-on-click-modal="false"
@close="onStampDialogClose"
>
<el-checkbox-group v-model="currentDraftStamps">
<el-checkbox
v-for="stamp in stampOptions"
:key="stamp.id"
:label="stamp.id"
style="display: block; margin: 6px 0"
>
{{ stamp.name }}
</el-checkbox>
</el-checkbox-group>
<span slot="footer" class="dialog-footer">
<el-button @click="closeStampDialog">取消</el-button>
<el-button type="primary" @click="confirmStampDialog">确定</el-button>
</span>
</el-dialog>
</div>
</el-col>
</el-row>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
// 创建 Vue 根实例,挂载整个页面交互逻辑
new Vue({
// 指定挂载点
el: "#app",
// 组件状态(响应式数据)
data: function () {
// 返回 data 对象
return {
// 左侧文件源数据
files: [
// 文件1
{ id: 1, name: "文件1", content: "这是文件1的内容" },
// 文件2
{ id: 2, name: "文件2", content: "这是文件2的内容" },
// 文件3
{ id: 3, name: "文件3", content: "这是文件3的内容" },
// 文件4
{ id: 4, name: "文件4", content: "这是文件4的内容" }
],
// 可选印章数据源
stampOptions: [
// 印章 A
{ id: "A", name: "合同章" },
// 印章 B
{ id: "B", name: "财务章" },
// 印章 C
{ id: "C", name: "人事章" },
// 印章 D
{ id: "D", name: "公章" }
],
// 左侧当前勾选的文件 ID 列表
selectedFileIds: [],
// 文件 -> 印章列表映射,如:{ 1: ["A","C"] }
selectedStampsMap: {},
// 印章弹窗是否可见
stampDialogVisible: false,
// 当前正在编辑的文件 ID
editingFileId: null,
// 弹窗中的临时选择数组(点确定才提交)
currentDraftStamps: []
};
},
// 派生数据
computed: {
// 右侧表格数据:按文件与印章关系展开成多行
tableRows: function () {
// 最终返回给表格的数据行
var rows = [];
// 保存 this 引用给回调使用
var _this = this;
// 遍历所有已勾选文件
this.selectedFileIds.forEach(function (fileId) {
// 按 fileId 找到文件详情
var file = _this.files.find(function (f) {
// 匹配当前 fileId
return f.id === fileId;
});
// 找不到文件则跳过
if (!file) return;
// 读取该文件已选印章列表
var stamps = _this.selectedStampsMap[fileId] || [];
// 没有印章时展示占位行
if (!stamps.length) {
// 推入占位行
rows.push({
// 占位行唯一键
rowKey: fileId + "-empty",
// 文件 ID
fileId: file.id,
// 文件名称
fileName: file.name,
// 文件内容
content: file.content,
// 无印章
stampId: null
});
// 当前文件处理结束
return;
}
// 有印章时,一个印章生成一行
stamps.forEach(function (stampId) {
// 推入印章行
rows.push({
// 行唯一键
rowKey: fileId + "-" + stampId,
// 文件 ID
fileId: file.id,
// 文件名称
fileName: file.name,
// 文件内容
content: file.content,
// 当前行印章 ID
stampId: stampId
});
});
});
// 返回最终表格行
return rows;
}
},
// 事件与业务方法
methods: {
// 返回表格行唯一 key
getRowKey: function (row) {
// 直接返回 rowKey
return row.rowKey;
},
// 打开印章弹窗并加载当前文件草稿
openStampDialog: function (fileId) {
// 取当前文件已选印章(不存在则空数组)
var current = this.selectedStampsMap[fileId] || [];
// 记录当前编辑文件
this.editingFileId = fileId;
// 复制一份数组作为草稿,避免直接改已提交数据
this.currentDraftStamps = current.slice();
// 打开弹窗
this.stampDialogVisible = true;
},
// 关闭印章弹窗
closeStampDialog: function () {
// 仅修改可见状态
this.stampDialogVisible = false;
},
// 弹窗关闭后的清理动作
onStampDialogClose: function () {
// 清空草稿
this.currentDraftStamps = [];
// 清空正在编辑的文件标记
this.editingFileId = null;
},
// 点击“确定”提交弹窗草稿
confirmStampDialog: function () {
// 没有编辑目标时直接关闭并退出
if (this.editingFileId === null || this.editingFileId === undefined) {
// 关闭弹窗
this.closeStampDialog();
// 结束方法
return;
}
// 用 $set 确保对象属性更新可响应
this.$set(
// 目标对象
this.selectedStampsMap,
// 目标键(文件ID)
this.editingFileId,
// 提交草稿副本
this.currentDraftStamps.slice()
);
// 关闭弹窗
this.closeStampDialog();
},
// 根据印章 ID 获取印章名称
stampNameById: function (stampId) {
// 在印章列表中查找对应项
var stamp = this.stampOptions.find(function (s) {
// 比较 id
return s.id === stampId;
});
// 找到返回名称,找不到返回空字符串
return stamp ? stamp.name : "";
},
// 判断某文件是否已勾选
isFileChecked: function (fileId) {
// indexOf 大于 -1 代表存在
return this.selectedFileIds.indexOf(fileId) > -1;
},
// 左侧勾选/取消勾选逻辑
onFileCheckChange: function (file, checked) {
// 先判断当前是否已存在
var exists = this.isFileChecked(file.id);
// 新勾选且原先不存在
if (checked && !exists) {
// 加入选中文件列表
this.selectedFileIds.push(file.id);
// 初始化该文件印章数组
if (!this.selectedStampsMap[file.id]) {
// 使用 $set 保证响应式
this.$set(this.selectedStampsMap, file.id, []);
}
}
// 取消勾选且原先存在
if (!checked && exists) {
// 从选中文件列表中移除该文件
this.selectedFileIds = this.selectedFileIds.filter(function (id) {
// 保留不是当前文件的项
return id !== file.id;
});
// 删除该文件的印章映射
this.$delete(this.selectedStampsMap, file.id);
// 若当前弹窗正在编辑该文件,则关闭弹窗
if (this.editingFileId === file.id) {
// 关闭弹窗
this.closeStampDialog();
}
}
},
// 删除右侧某行数据
removeRow: function (row) {
// 当前行对应文件 ID
var fileId = row.fileId;
// 取当前文件所有印章
var stamps = this.selectedStampsMap[fileId] || [];
// 分支1:删除的是具体印章行
if (row.stampId) {
// 过滤掉当前行印章,得到删除后的数组
var nextStamps = stamps.filter(function (id) {
// 保留不是当前印章的项
return id !== row.stampId;
});
// 删除后没有印章了
if (!nextStamps.length) {
// 从左侧选中文件列表移除该文件
this.selectedFileIds = this.selectedFileIds.filter(function (id) {
// 保留不是当前文件的项
return id !== fileId;
});
// 删除该文件的印章映射
this.$delete(this.selectedStampsMap, fileId);
// 如果弹窗正在编辑该文件,顺便关闭
if (this.editingFileId === fileId) {
// 关闭弹窗
this.closeStampDialog();
}
} else {
// 仍有印章时只更新印章数组
this.$set(this.selectedStampsMap, fileId, nextStamps);
}
} else {
// 分支2:删除的是“未选择印章”的占位行
this.selectedFileIds = this.selectedFileIds.filter(function (id) {
// 保留不是当前文件的项
return id !== fileId;
});
// 删除该文件印章映射
this.$delete(this.selectedStampsMap, fileId);
// 如果弹窗正在编辑该文件,顺便关闭
if (this.editingFileId === fileId) {
// 关闭弹窗
this.closeStampDialog();
}
}
}
}
});
</script>
</body>
</html>