一、直接上代码:样式如下
# 安装拖拽依赖vuedraggable
npm install vuedraggable
二、示例代码
<template>
<div class="app-container">
<!--条件搜索-->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="所属部门" prop="deptId">
<treeselect v-model="queryParams.deptId" :options="deptOptions" :show-count="true"
placeholder="所属部门" style="width: 240px"/>
</el-form-item>
<el-form-item label="考核类型" prop="judgeType">
<el-select
v-model="queryParams.judgeType"
placeholder="考核类型"
clearable
style="width: 240px">
<el-option
v-for="dict in dict.type.judge_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!--按钮-->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['evaluate:standard:add']">新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-document-copy"
size="mini"
:disabled="multiple"
@click="handleCopy"
v-hasPermi="['evaluate:standard:copy']">复制
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['evaluate:standard:remove']">批量删除
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!--列表-->
<el-table ref="tables" v-loading="loading" :data="list" @selection-change="handleSelectionChange"
:default-sort="defaultSort" @sort-change="handleSortChange"
row-key="id"
:tree-props="{children: 'copyData', hasChildren: 'hasChildren'}">
<el-table-column type="selection" width="50" align="center" :selectable="isParentNode"/>
<el-table-column label="编号" align="center" prop="id"/>
<el-table-column label="所属部门" align="center" prop="deptId">
<template slot-scope="scope">
{{ deptNames[scope.row.deptId] || '未知部门' }}
</template>
</el-table-column>
<el-table-column label="考核类型" align="center" prop="judgeType">
<template slot-scope="scope">
<dict-tag :options="dict.type.judge_type" :value="scope.row.judgeType"/>
</template>
</el-table-column>
<el-table-column label="考核指标" align="center" prop="judgeIndex"/>
<el-table-column label="对应加减分" align="center" prop="judgeSocre"/>
<el-table-column label="创建人" align="center" prop="createBy"/>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" sortable="custom"
:sort-orders="['descending', 'ascending']"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<!-- 判断是否为父节点(parentId 为空)-->
<template v-if="!scope.row.parentId">
<!-- parentId 为空时,显示 详细、修改 和 删除 按钮 -->
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['evaluate:standard:query']"
>详细
</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['evaluate:standard:edit']"
>修改
</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['evaluate:standard:remove']"
>删除
</el-button>
</template>
<!-- parentId 不为空时,只显示 删除 按钮 -->
<template v-else>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['evaluate:standard:remove']"
>删除
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!--弹窗 -->
<MyDialog ref="dialog" @refresh="getList"></MyDialog>
</div>
</template>
<script>
import MyDialog from './myDialog.vue'
import {deptTreeSelect} from "@/api/system/user";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {listJudge, getJudge, delJudge, copyJudge} from "@/api/coppms/judge";
export default {
// 字典类型
dicts: ['judge_type'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 是否显示弹出层
open: false,
// 日期范围
dateRange: [],
// 默认排序
defaultSort: {prop: 'createTime', order: 'descending'},
// 表单参数
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
deptId: undefined,
judgeType: undefined
},
// 部门树选项
deptOptions: [],
// 用于存储部门名称的对象
deptNames: {}
};
},
created() {
this.getDeptTree();
this.getList();
},
components: {
Treeselect,
MyDialog
},
methods: {
// 判断是否为父节点(parentId 为空)
isParentNode(row) {
return !row.parentId;
},
// 查询部门下拉树结构
async getDeptTree() {
const response = await deptTreeSelect();
this.deptOptions = response.data;
this.extractDeptNames(this.deptOptions);
},
// 获取部门信息
extractDeptNames(departments) {
const deptNames = {};
// 递归处理部门信息并保存
function traverse(depts) {
depts.forEach(dept => {
deptNames[dept.id] = dept.label;
if (dept.children) {
traverse(dept.children);
}
});
}
traverse(departments);
this.deptNames = deptNames;
},
// 查询评判标准列表
getList() {
this.loading = true;
listJudge(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.list = response.rows;
this.total = response.total;
this.loading = false;
}
);
this.loading = false;
},
// 搜索按钮操作
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
// 重置按钮操作
resetQuery() {
this.queryParams = {}
this.dateRange = []
this.getList()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id);
this.single = selection.length != 1;
this.multiple = !selection.length;
},
// 排序触发事件
handleSortChange(column, prop, order) {
this.queryParams.orderByColumn = column.prop;
this.queryParams.isAsc = column.order;
this.getList();
},
// 详细
handleView(row) {
getJudge(row.id).then(response => {
let title = '查看';
let formData = response.data;
let isViewMode = true
this.$refs.dialog.open(title, formData, isViewMode);
});
},
// 新增
handleAdd() {
let title = '新增';
let formData = {};
let isViewMode = false
this.$refs.dialog.open(title, formData, isViewMode);
},
// 修改
handleUpdate(row) {
const id = row.id;
getJudge(id).then(response => {
let title = '修改';
let formData = response.data;
let isViewMode = false
this.$refs.dialog.open(title, formData, isViewMode);
});
},
// 复制
handleCopy() {
const ids = this.ids;
this.$modal.confirm('是否确认复制编号为"' + ids + '"的数据项?').then(function () {
return copyJudge(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("复制成功");
}).catch(() => {
});
},
// 删除按钮操作
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除编号为"' + ids + '"的数据项?').then(function () {
return delJudge(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
});
},
}
};
</script>
<template>
<div>
<el-dialog custom-class="dialog-title" :visible.sync="isVisible" :title="dialogTitle+'评判标准'"
:close-on-click-modal="false"
@close="close"
width="80%">
<el-form ref="elForm" :model="formData" :rules="rules" size="medium" label-width="170px">
<el-form-item label="评判标准所属部门" prop="deptId">
<treeselect v-model="formData.deptId" :options="deptOptions" :show-count="true"
placeholder="请选择评判标准所属部门" :style="{width: '50%'}" :disabled="isViewMode"/>
</el-form-item>
<el-form-item label="考核类型" prop="judgeType">
<el-select v-model="formData.judgeType" placeholder="请选择考核类型" clearable :style="{width: '50%'}"
:disabled="isViewMode">
<el-option
v-for="dict in dict.type.judge_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="评判标准对应考核指标" prop="judgeIndex">
<el-input v-model="formData.judgeIndex" placeholder="请输入评判标准对应考核指标" clearable
:style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="评判标准对应加减分值" prop="judgeSocre">
<el-input v-model="formData.judgeSocre"
placeholder="说明:分值必须为 X~X 格式,或者单独一个 ~ 字符,其中 X 可以为数字(正数、负数、小数,正数不能以 0 开头);后面的值必须大于前面的值"
clearable
:style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="评判标准描述">
<el-input v-model="formData.judgeDesc" type="textarea" placeholder="请输入评判标准描述"
:autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<!--具体选项----------------------------------------------------------------------------------------->
<el-form-item label="具体选项">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="addOptions" plain :disabled="isViewMode">
添加
</el-button>
</el-form-item>
<!-- 增加一个固定高度的容器用于滚动 -->
<!--选项-->
<div class="options-container">
<draggable v-model="options" handle=".drag-handle" @end="updateOrder">
<transition-group>
<el-form-item v-for="(option, index) in options" :key="option.optionKey">
<!-- 父选项头部 -->
<el-form-item>
<label>选项 {{ index + 1 }}:</label>
<el-button class="remove-button" type="danger" icon="el-icon-delete" size="mini"
@click="removeOption(index)" plain :disabled="isViewMode">删除
</el-button>
<el-tag class="drag-handle" style="cursor: move;margin-left: 20px;" type="info">点击此处拖动排序
</el-tag>
</el-form-item>
<div class="option-block">
<el-form-item label="是否有下级选项">
<el-radio-group v-model="option.optionIsHaveChild" :disabled="isViewMode">
<el-radio :label=1>是</el-radio>
<el-radio :label=0>否</el-radio>
</el-radio-group>
<el-button v-if="option.optionIsHaveChild === 1" class="add-button" type="primary"
icon="el-icon-plus"
size="mini" @click="addSubOption(index)"
plain :disabled="isViewMode">添加
</el-button>
</el-form-item>
<!--父选项内容-->
<el-form-item label="选项介绍" class="form-item-with-spacing">
<el-input v-model="option.optionIntroduce" type="textarea" placeholder="请输入选项介绍"
:readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item v-if="option.optionIsHaveChild === 0" label="选项分值" class="form-item-with-spacing">
<el-input-number v-model="option.optionScore" placeholder="请输入选项分值"
:style="{width: '25%'}" :disabled="isViewMode"
:precision="2" :step="0.1" :max="99"></el-input-number>
</el-form-item>
<!--子选项-->
<draggable v-if="option.optionIsHaveChild===1" v-model="option.subOptions" handle=".drag-handle"
@end="updateSubOrder(index)">
<transition-group>
<div class="sub-option-block" v-for="(subOption, subIndex) in option.subOptions"
:key="subOption.optionKey">
<!--子选项头部-->
<el-form-item>
<label>子选项 {{ subIndex + 1 }}:</label>
<el-button class="remove-button" type="danger" icon="el-icon-delete" size="mini"
@click="removeSubOption(index, subIndex)" plain :disabled="isViewMode">删除
</el-button>
<el-tag class="drag-handle" style="cursor: move;margin-left: 20px;" type="info">点击此处拖动排序
</el-tag>
</el-form-item>
<!--子选项内容-->
<el-form-item label="选项介绍" class="form-item-with-spacing">
<el-input v-model="subOption.optionIntroduce" type="textarea" placeholder="请输入选项介绍"
:readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="选项分值" class="form-item-with-spacing">
<el-input-number v-model="subOption.optionScore" placeholder="请输入选项分值"
:style="{width: '25%'}" :disabled="isViewMode"
:precision="2" :step="0.1" :max="99"></el-input-number>
</el-form-item>
</div>
</transition-group>
</draggable>
</div>
</el-form-item>
</transition-group>
</draggable>
</div>
<!--以上是具体选项内容---------------------------------------------------------------------------------->
<el-form-item label="特殊评分" prop="specialSocre">
<el-radio-group v-model="formData.specialSocre" size="medium" :disabled="isViewMode">
<el-radio v-for="(item, index) in specialSocreOptions" :key="index" :label="item.value">{{ item.label }}
</el-radio>
</el-radio-group>
<span class="green"
style="margin-left: 100px;color: red;font-size: 14px;">解释:特殊评分过程请根据评分标准手动输入分值</span>
</el-form-item>
<el-form-item label="评判标准解释">
<el-input v-model="formData.judgeExplain" type="textarea" placeholder="请输入评判标准解释"
:autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="指标完成情况" prop="completionStatus">
<el-input v-model="formData.completionStatus" type="textarea" placeholder="请输入指标完成情况"
:autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="部门自评" prop="deptComment">
<el-input v-model="formData.deptComment" type="textarea" placeholder="请输入部门自评"
:autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注"
:autosize="{minRows: 4, maxRows: 4}" :style="{width: '100%'}" :readonly="isViewMode"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button v-if="dialogTitle === '新增'" type="primary" @click="save" :disabled="isViewMode">保存</el-button>
<el-button v-if="dialogTitle === '修改'" type="primary" @click="update" :disabled="isViewMode">修改</el-button>
<el-button @click="close">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {addJudge, updateJudge} from "@/api/coppms/judge";
import {deptTreeSelect} from "@/api/system/user";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import draggable from 'vuedraggable';
export default {
// 字典类型
dicts: ['judge_type'],
inheritAttrs: false,
components: {Treeselect, draggable},
props: [],
data() {
return {
// 是否允许修改,默认不是查看模式
isViewMode: false,
// 弹窗显示
isVisible: false,
// 弹窗标题
dialogTitle: '',
// 选项数组
options: [],
// 数据对象
formData: {
id: '',
deptId: '',
judgeType: '',
judgeIndex: '',
judgeSocre: '',
judgeDesc: '',
specialSocre: 0,
judgeExplain: '',
completionStatus: '',
deptComment: '',
remark: '',
children: []
},
// 部门树选项
deptOptions: undefined,
rules: {
deptId: [{
required: true,
message: '请选择评判标准所属部门',
trigger: 'change'
}],
judgeType: [{
required: true,
message: '请选择考核类型',
trigger: 'change'
}],
judgeIndex: [{
required: true,
message: '请输入评判标准对应考核指标',
trigger: 'blur'
}],
judgeSocre: [{
required: true,
message: '请输入评判标准对应加减分值',
trigger: 'blur'
}, {
validator: (rule, value, callback) => {
// 正则表达式:匹配负数、正数、小数;正数的两位数不能以 0 开头
const regex = /^(-?(\d|[1-9]\d*)(\.\d+)?~(-?(\d|[1-9]\d*)(\.\d+)?)|~)$/;
if (!regex.test(value)) {
callback(new Error('分值必须为 X~X 格式,或者单独一个 ~ 字符。X 可以是数字(正数、负数、小数),且正数不能以 0 开头'));
} else if (value !== '~') {
// 如果不是单独的 '~',继续检查数值关系
const [firstX, secondX] = value.split('~').map(Number);
if (firstX >= secondX) {
callback(new Error('后面的值必须大于前面的值'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'blur'
}],
specialSocre: [{
required: true,
message: '特殊评分不能为空',
trigger: 'change'
}]
},
specialSocreOptions: [{
"label": "是",
"value": 1
}, {
"label": "否",
"value": 0
}],
}
},
computed: {},
watch: {},
created() {
},
mounted() {
// 获取组织树
this.getDeptTree();
},
methods: {
// 查询部门下拉树结构
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data;
});
},
// 添加选项
addOptions() {
this.options.push({
optionKey: this.randomUUID(),
optionIsHaveChild: 0,
optionIntroduce: '',
optionScore: '',
subOptions: [],
optionOrder: this.options.length + 1
});
},
// 移除选项
removeOption(index) {
this.options.splice(index, 1);
this.updateOrder();
},
// 添加子选项
addSubOption(index) {
this.options[index].subOptions.push({
optionKey: this.randomUUID(),
optionIntroduce: '',
optionScore: '',
optionOrder: this.options[index].subOptions.length + 1
});
},
randomUUID() {
const hexDigits = '0123456789abcdef';
let uuid = '';
for (let i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) {
uuid += '-';
} else if (i === 14) {
uuid += '4';
} else if (i === 19) {
uuid += hexDigits[(Math.floor(Math.random() * 4) + 8)];
} else {
uuid += hexDigits[Math.floor(Math.random() * 16)];
}
}
return uuid;
},
// 移除子选项
removeSubOption(index, subIndex) {
this.options[index].subOptions.splice(subIndex, 1);
this.updateSubOrder(index);
},
// 打开
open(title, formData, isViewMode) {
this.dialogTitle = title;
this.isViewMode = isViewMode;
this.formData = {
...formData,
judgeType: String(formData.judgeType || ''),// 转换为字符串
specialSocre: formData.specialSocre || 0
};
// 具体选项赋值
this.options = Array.isArray(formData.children) ? formData.children : [];
this.isVisible = true;
// 获取组织树
this.getDeptTree();
},
// 关闭
close() {
this.formData = {}
this.options = []
this.deptOptions = []
this.$refs.elForm.resetFields()
this.isVisible = false;
// 刷新表格
this.$emit('refresh');
},
// 提交
save() {
// 手动校验嵌套选项
if (!this.validateNestedOptions()) {
return;
}
this.$refs['elForm'].validate(valid => {
if (valid) {
let judgeExplain = this.formData.judgeExplain
let judgeDesc = this.formData.judgeDesc
// 判断两个值是否至少有一个不为空
if (!judgeExplain && !judgeDesc) {
this.$modal.msgError("评判标准描述和评判标准解释必须有一个不为空!");
return;
}
// 不是特殊评分的时候才有具体选项
let specialSocre = this.formData.specialSocre;
// 特殊评分时必须有具体选项
this.formData.children = this.options
let children = this.formData.children
if (specialSocre === 0 && (children === undefined || children.length === 0)) {
this.$modal.msgError("不是特殊评分,必须有具体选项!");
return;
}
// 不是特殊评分赋值
if (specialSocre === 1) {
this.formData.children = []
}
addJudge(this.formData).then(response => {
this.$modal.msgSuccess("新增成功");
this.close()
});
}
})
},
// 修改
update() {
// 手动校验嵌套选项
if (!this.validateNestedOptions()) {
return;
}
this.$refs['elForm'].validate(valid => {
if (valid) {
let judgeExplain = this.formData.judgeExplain
let judgeDesc = this.formData.judgeDesc
// 判断两个值是否至少有一个不为空
if (!judgeExplain && !judgeDesc) {
this.$modal.msgError("评判标准描述和评判标准解释必须有一个不为空!");
return;
}
// 不是特殊评分的时候才有具体选项
let specialSocre = this.formData.specialSocre;
// 特殊评分时必须有具体选项
this.formData.children = this.options
let children = this.formData.children
if (specialSocre === 0 && (children === undefined || children.length === 0)) {
this.$modal.msgError("不是特殊评分,必须有具体选项!");
return;
}
// 不是特殊评分赋值
if (specialSocre === 1) {
this.formData.children = []
}
updateJudge(this.formData).then(response => {
this.$modal.msgSuccess("修改成功");
this.close()
});
}
})
},
// 更新选择项顺序
updateOrder() {
this.options.forEach((option, index) => {
option.optionOrder = index + 1;
});
},
// 更新子选择项顺序
updateSubOrder(index) {
this.options[index].subOptions.forEach((subOption, subIndex) => {
subOption.optionOrder = subIndex + 1;
});
},
// 校验选项介绍是否为空
validateNestedOptions() {
for (let option of this.options) {
if (!option.optionIntroduce) {
this.$modal.msgError("选项介绍不能为空");
return false;
}
if (option.optionIsHaveChild === 1) {
for (let subOption of option.subOptions) {
if (!subOption.optionIntroduce) {
this.$modal.msgError("子选项介绍不能为空");
return false;
}
}
}
}
return true;
}
}
}
</script>
<style>
.option-block {
border: 1px solid #dcdfe6;
padding: 15px;
margin-bottom: 10px;
border-radius: 4px;
}
.sub-option-block {
border: 1px dashed #dcdfe6;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
}
.input-with-button {
display: flex;
align-items: center;
}
.input-with-button .el-input {
width: 80%;
margin-right: 10px;
}
.add-button, .remove-button {
margin-left: 20px;
}
.form-item-with-spacing {
margin-bottom: 20px !important;
}
.options-container {
max-height: 650px; /* 根据你的需要调整高度 */
overflow-y: auto;
border: 0px solid #dcdfe6; /* 可选:给滚动区域添加边框 */
padding: 10px; /* 可选:调整内边距 */
}
</style>