Vue2.6之后才有这种写法v-slot:header="scope" 2.6之前的写法 slot="header" slot-scope="scope"
父组件 调用
<template>
<div class="recommend">
<el-tabs v-model="reviewTabs" @tab-click="handleTabsClick">
<el-tab-pane label="待审核" name="audit" lazy>
<search-form
v-model="searchAudit"
:search-option="searchOptionInner"
:title-fun="titleFunOption"
:customClear="true"
@clear="onClearSearch"
@search="onSearchClick"
@table-click="handleClick">
</search-form>
<table-list
ref="tableAudit"
:dialogLoading="dialogLoading"
:columns="tableColOptionInner"
:data="listData"
:total="total"
:page.sync="searchAudit.curpage"
:page-size.sync="searchAudit.pagesize"
@page-change="listPageChange"
@table-click="handleClick">
<template v-slot:data1header>
<span>选择</span>
</template>
<template v-slot:data="{row,index}">
<el-checkbox :value="isChecked(row)"
@change="onCheckedChange(row)"></el-checkbox>
</template>
</table-list>
</el-tab-pane>
<el-tab-pane label="全部审核" name="complete" lazy>
<search-form
v-model="searchComplete"
:search-option="searchOptionInner"
:title-fun="titleFunOption"
:customClear="true"
@clear="onClearSearch"
@search="onSearchClick"
@table-click="handleClick">
</search-form>
<table-list
ref="tableComplete"
:dialogLoading="dialogLoading"
:columns="tableColOptionInner"
:data="listData"
:total="total"
:page.sync="searchComplete.curpage"
:page-size.sync="searchComplete.pagesize"
@page-change="listPageChange"
@table-click="handleClick">
<template v-slot:data1header>
<span>选择</span>
</template>
<template v-slot:data="{row,index}">
<el-checkbox :value="isChecked(row)"
@change="onCheckedChange(row)"></el-checkbox>
</template>
<template v-slot:status="{row,index}">
<span v-if="row.status === '10'">待审核</span>
<span v-else-if="row.status === '20'">通过</span>
<span v-else-if="row.status === '30'">驳回</span>
</template>
</table-list>
</el-tab-pane>
</el-tabs>
<div class="multiple-selection-operation">
<el-button type="primary" size="small" @click="onSelectAllChange">全选</el-button>
<el-button type="primary" size="small" @click="onSelectInvertChange">反选</el-button>
<el-button v-show="reviewTabs !== 'complete'"
type="primary"
size="small"
@click="toExamineSelected">批量通过
</el-button>
<el-button v-show="reviewTabs === 'complete'"
type="primary"
size="small"
@click="onDeleteSelected">一键删除
</el-button>
</div>
<el-dialog
title="评论驳回"
width="34%"
:visible.sync="rejectDialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
@open="openRejectDialog">
<el-form
ref="rejectFrom"
:model="rejectFrom"
label-width="115px"
:rules="rejectFromRules">
<el-form-item label="拒绝理由" prop="classA">
<el-radio-group v-model="rejectFrom.classA" @change="onRejectChange">
<el-radio v-for="item in rejectList" :key="item.id" :label="item.id">{{ item.content }}</el-radio>
</el-radio-group>
<div class="sub-reject-container">
<div v-for="(label) in rejectChildList"
:key="label.id"
class="sub-reject-item"
:class="{'reject-active': rejectFrom.classB.includes(label.id)}"
@click="onSelectRejectItem(label.id)">
{{ label.id }}.{{ label.content }}
</div>
</div>
</el-form-item>
<el-form-item label="审核备注">
<el-input type="textarea"
:rows="3"
maxlength="30"
placeholder="请输入拒绝理由"
v-model="rejectFrom.reason"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="rejectDialog = false">取 消</el-button>
<el-button type="primary" @click="onSubmitDialog">确 定</el-button>
</div>
</el-dialog>
<el-dialog
title="社区评论详情"
width="34%"
:visible.sync="detailDialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
custom-class="custom-detail-log">
<div class="community-details">
<el-descriptions :column="1"
:contentStyle="{fontSize: '16px',marginBottom: '12px',color:'#333333'}"
:labelStyle="{fontSize: '16px',marginBottom: '12px',color:'#333333'}">
<el-descriptions-item label="评论内容">{{detailInfo.content}}</el-descriptions-item>
<el-descriptions-item label="评论图片">
<template v-if="detailInfo.img">
<el-image
style="width: 100px; height: 100px"
:src="detailInfo.img"
:preview-src-list="[detailInfo.img]">
</el-image>
</template>
</el-descriptions-item>
<el-descriptions-item label="目标作品">{{detailInfo.sourseid}}</el-descriptions-item>
<el-descriptions-item label="审核结果">
<span class="descriptions-result" v-if="detailInfo.status === '20'">通过</span>
<span class="descriptions-result" v-else-if="detailInfo.status === '30'">拒绝</span>
</el-descriptions-item>
<el-descriptions-item v-if="detailInfo.status === '30'" label="驳回理由">{{detailInfo.reason}}</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
<checked-log :father-dialog-visible.sync="checkedLogVisible" :log-type="6"></checked-log>
</div>
</template>
<script>
import SearchForm from "@/components/list/SearchFrom.vue";
import TableList from "@/components/list/TableList.vue";
import C from "@/util/constants";
import audit from "@/api/audit-reply";
import moment from 'moment'
import community from "@/api/feedback";
import CheckedLog from "@/components/checkedLog.vue";
export default {
name: "BaseUserInfo",
components: { CheckedLog, TableList, SearchForm },
data() {
return {
reviewTabs: "audit",
searchAudit: {
content: '',
member_name: '',
status: '10',
member_id: '',
sourseid: '',
start_time: undefined,
end_time: undefined,
curpage: 1,
// 每页条数
pagesize: C.LIST_PAGE_SIZE
},
searchComplete: {
content: '',
member_name: '',
sourseid: '',
status: '',
starttime: undefined,
endtime: undefined,
// 当前页
curpage: 1,
// 每页条数
pagesize: C.LIST_PAGE_SIZE
},
titleFunOption: [{
label: '审核日志',
class: '',
fun: 'log',
btnType: 'primary',
// limitsMenu: '拉新活动',
// limitsDown: '新增',
}],
listData: [],
total: 0,
selectsList: [],
dialogLoading: false,
// 拒绝弹框
rejectDialog: false,
rejectFrom: {
classA: '',
classB: [],
reason: '',
},
rejectFromRules: {
classA: [
{ required: true, message: '请选择拒绝理由', trigger: 'blur' }
]
},
rejectList: [],
rejectChildList: [],
rowDetail: null,
checkedLogVisible: false,
detailDialogVisible: false,
detailInfo:{}
}
},
computed: {
searchOptionInner() {
if (this.reviewTabs === 'complete') {
return [
{ label: '评论内容关键词', prop: 'content', clearable: true },
{ label: '发布者昵称/ID', prop: 'member_name', clearable: true },
{ label: '目标作品ID', prop: 'sourseid', clearable: true },
{
label: '审核状态',
prop: 'status',
type: 'select',
default: null,
clearable: true,
option: [
{ label: "通过", value: 20 },
{ label: "驳回", value: 30 }]
},
{ prop: 'time', label: '发布时间', type: 'dateRange', start: "starttime", end: "endtime", clearable: true },
]
} else {
return [
{ label: '评论内容关键词', prop: 'content', clearable: true },
{ label: '发布者昵称', prop: 'member_name', clearable: true },
{ label: '发布者ID', prop: 'member_id', clearable: true },
{ label: '目标作品ID', prop: 'sourseid', clearable: true },
{ prop: 'time', label: '发布时间', type: 'dateRange', start: "start_time", end: "end_time", clearable: true },
]
}
},
tableColOptionInner() {
if (this.reviewTabs === 'complete') {
return [
{ prop: 'data', slot: true, headerSlot: "data1header", width: 50 },
{ label: '序号', type: 'index' },
{ label: '评论内容', prop: 'content' },
{ label: '评论图片', prop: 'img', propType: 'image' },
{ label: '目标作品ID', prop: 'sourseid' },
{ label: '发布者昵称', prop: 'member_name' },
{ label: '发布者ID', prop: 'member_id' },
{ label: '点赞量', prop: 'hot_num', },
{ label: '审核状态', prop: 'status', slot: true },
{ label: '审核人', prop: 'ename' },
{ label: '发布时间', prop: 'itime', propType: "time" },
{ label: '审核时间', prop: 'etime', propType: "time" },
{
label: '操作', type: 'action', actions: [
{
label: '详情',
// limitsMenu: '拉新活动',
// limitsDown: '启用/停用',
fun: 'detail',
showWithProp: 'status',
showWithPropValue: ["20"]
},
{
label: '删除',
// limitsMenu: '拉新活动',
// limitsDown: '启用/停用',
fun: 'delete',
customClass: 'del',
showWithProp: 'status',
showWithPropValue: ["20"]
}
]
}
]
} else {
return [
{ prop: 'data', slot: true, headerSlot: "data1header", width: 50 },
{ label: '序号', type: 'index' },
{ label: '评论内容', prop: 'member_id' },
{ label: '评论图片', prop: 'img', propType: 'image' },
{ label: '目标作品ID', prop: 'sourseid' },
{ label: '发布者昵称', prop: 'member_name' },
{ label: '发布者ID', prop: 'member_id' },
{ label: '发布时间', prop: 'itime', propType: "time" },
{
label: '操作', type: 'action', actions: [
{
label: '驳回',
// limitsMenu: '拉新活动',
// limitsDo wn: '启用/停用',
customClass: 'del',
fun: 'reject',
showWithProp: 'exam_status',
showWithPropValue: ["10"]
},
{
label: '通过',
// limitsMenu: '拉新活动',
// limitsDown: '启用/停用',
fun: 'pass',
showWithProp: 'exam_status',
showWithPropValue: ["10"]
}]
}
]
}
},
},
mounted() {
this.getList()
},
methods: {
onSelectRejectItem(id) {
const { classB } = this.rejectFrom
if (classB.includes(id)) {
classB.splice(classB.indexOf(id), 1)
} else {
classB.push(id)
}
},
onRejectChange(value) {
this.rejectFrom.classB = []
this.rejectChildList = this.rejectList.find(item => item.id === value).childs
},
async onSubmitDialog() {
await this.$refs.rejectFrom.validate()
const { classA, classB } = this.rejectFrom
if (classB.length <= 0) {
this.$message.error('请至少选择一个驳回理由')
return
}
await this.auditProcessing(this.rowDetail, '30', {
reason: this.rejectFrom.reason,
reject_id: { id: classA, childs: classB }
})
this.$common.notifySuccess('审核驳回', '已更终端数据')
this.rejectDialog = false
},
async openRejectDialog() {
const { data, message, code } = await community.RejectConfigAPI()
if (code === 1) {
this.rejectList = data.list
this.rejectFrom.classA = data.list[0].id
this.rejectChildList = data.list[0].childs
} else {
this.$message.error(message)
}
},
isChecked(item) {
return this.selectsList.findIndex((it) => it.id === item.id) >= 0;
},
onCheckedChange(item) {
const index = this.selectsList.findIndex((it) => it.id === item.id);
if (index >= 0) {
this.selectsList.splice(index, 1);
} else {
this.selectsList.push(item);
}
},
// 全选
onSelectAllChange() {
const selects = [...(this.selectsList), ...(this.listData)]
this.selectsList = [...(new Set(selects))]
},
onSelectInvertChange() {
const { listData, selectsList } = this;
this.selectsList = listData.filter(item => selectsList.findIndex(it => it.id === item.id) < 0)
},
// 一键删除
onDeleteSelected() {
if (this.selectsList.length <= 0) {
this.$common.notifyError('请至少选择一条数据')
return false
}
const ids = this.selectsList.map(item => {
return item.id
}).join(',')
this.$common.openConfirm(
`是否确定<span style="color: red">删除</span>全部选中评论吗?<br/><p>删除后对应终端将无法展示</p>`,
'确认提示',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmCallBack: ({ confirmButtonLoadingClose }) => {
audit.delCommunitySelected(ids).then(() => {
this.$common.notifySuccess('删除成功', '已更终端数据')
this.getList()
this.selectsList = []
}).catch(() => {
this.$common.notifyError('删除失败')
}).finally(() => {
confirmButtonLoadingClose()
})
}
}
)
},
// 一键审核
toExamineSelected() {
if (this.selectsList.length <= 0) {
this.$common.notifyError('请至少选择一条数据')
return false
}
let flag
if (this.reviewTabs === 'audit') {
flag = this.selectsList.every(it => it.exam_status === '10')
} else {
flag = this.selectsList.every(it => it.status === '10')
}
if (!flag) {
this.$common.notifyError('只能选择待审核状态数据')
return false
}
const ids = this.selectsList.map(item => {
return item.id
}).join(',')
this.$confirm(
`是否通过选择的全部数据?<br/><p>通过后将会显示在终端,全部用户可见</p>`,
'确认提示',
{
type: 'warning',
confirmButtonText: '通过',
cancelButtonText: '不通过',
dangerouslyUseHTMLString: true,
closeOnClickModal: false,
distinguishCancelAndClose: true,
}).then(async () => {
const { code, message } = await audit.auditProcessingApi({
exam_status: '20',
ids,
k_type: 'comment'
})
if (code === 1) {
this.$common.notifySuccess('审核已通过', '已更终端数据')
await this.getList()
this.selectsList = []
} else {
this.$message.error(message)
}
}).catch(async (action) => {
if (action == 'cancel') {
const { code, message } = await audit.auditProcessingApi({
exam_status: '30',
ids,
reason: this.rejectFrom.reason,
k_type: 'comment'
})
if (code === 1) {
this.$common.notifySuccess('审核已驳回', '已更终端数据')
await this.getList()
this.selectsList = []
} else {
this.$message.error(message)
}
}
});
},
handleTabsClick() {
this.$nextTick(() => {
switch (this.reviewTabs) {
case "audit": {
this.$refs.tableAudit.calculateTableListHeight();
break
}
case "complete": {
this.$refs.tableComplete.calculateTableListHeight();
break
}
}
})
this.selectsList = []
this.onClearSearch()
this.getList()
},
onClearSearch() {
if (this.reviewTabs !== 'complete') {
this.searchAudit = {
content: '',
member_name: '',
member_id: '',
status: '10',
sourseid: '',
start_time: undefined,
end_time: undefined,
curpage: 1,
// 每页条数
pagesize: C.LIST_PAGE_SIZE
}
} else {
this.searchComplete = {
content: '',
member_name: '',
sourseid: '',
status: '',
starttime: undefined,
endtime: undefined,
// 当前页
curpage: 1,
// 每页条数
pagesize: C.LIST_PAGE_SIZE
}
}
},
onSearchClick() {
this.getList();
},
// 分页变化事件
listPageChange() {
this.selectsList = []
this.getList()
},
async auditProcessing(row = {}, mark, params = {}) {
const { code, message } = await audit.auditProcessingApi({
exam_status: mark,
ids: row.id,
k_type: 'comment',
...params
})
if (code === 1) {
await this.getList()
} else {
this.$message.error(message)
}
},
//表格点击事件
async handleClick(fun, row) {
switch (fun) {
case "log":
this.checkedLogVisible = true
break;
case 'detail':
this.detailInfo = {}
this.detailDialogVisible = true
const { data, message, code } = await audit.getCommunityDetail({ id: row.id})
if (code === 1) {
this.detailInfo = data
} else {
this.$message.error(message)
}
break;
case 'delete':
await this.$common.openConfirm(
`是否确定<span style="color: red">删除</span>选中评论吗?<br/><p>删除后对应终端将无法展示</p>`,
'确认提示',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmCallBack: async ({ confirmButtonLoadingClose }) => {
audit.delCommunityCurrent({ id: row.id }).then(() => {
this.$common.notifySuccess('删除成功', '已更终端数据')
this.getList()
}).catch(() => {
this.$common.notifyError('删除失败')
}).finally(() => {
confirmButtonLoadingClose()
})
}
})
break;
case "pass":
await this.$common.openConfirm(
`是否确定通过该条数据的审核?<br/><p>通过后所有用户可见</p>`,
'确认提示',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmCallBack: async ({ confirmButtonLoadingClose }) => {
await this.auditProcessing(row, '20')
this.$common.notifySuccess('审核通过', '已更终端数据')
this.rejectDialog = false
confirmButtonLoadingClose()
}
}
)
break;
case "reject":
this.rejectDialog = true
this.rowDetail = row
break;
case "offShelf":
await this.$common.openConfirm(
`是否确定下架该推荐?<br/><p>下架后该推荐将会在用户端全部隐藏,请确认是否继续该操作?</p>`,
'确认提示',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmCallBack: async ({ confirmButtonLoadingClose }) => {
// 下架Api
confirmButtonLoadingClose()
}
})
break;
}
},
//获取列表
async getList() {
try {
this.dialogLoading = true
let params = {}
if (this.reviewTabs === 'audit') {
const { content, member_name, member_id, sourseid, start_time, end_time } = this.searchAudit
params = {
content,
member_name,
member_id,
status: '10',
sourseid,
start_time: start_time ? moment(start_time).unix() : undefined,
end_time: end_time ? moment(end_time).unix() : undefined,
}
} else {
const { content, member_name, sourseid, status, starttime, endtime } = this.searchComplete
params = {
content,
member_name,
sourseid,
status,
start_time: starttime ? moment(starttime).unix() : undefined,
end_time: endtime ? moment(endtime).unix() : undefined,
}
}
const {
code,
data,
message
} = await (this.reviewTabs === 'audit' ? audit.getAuditListApi : audit.getCompleteListApi)(params)
if (code === 1) {
this.listData = data.list
this.total = Number(data.list.length)
} else {
this.$message.error(message)
}
} finally {
this.dialogLoading = false
}
},
}
}
</script>
<style scoped lang="scss">
.recommend {
.el-tabs {
height: 60px !important;
}
.table-list {
height: 100% !important;
}
.list__table {
flex: 1;
}
::v-deep {
.el-radio-group {
display: grid;
grid-template-columns: repeat(auto-fill, 100px);
grid-row-gap: 20px;
grid-column-gap: 20px;
}
}
}
.multiple-selection-operation {
position: absolute;
left: 39px;
bottom: 33px;
display: flex;
}
.sub-reject-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-row-gap: 20px;
grid-column-gap: 10px;
margin-top: 20px;
}
.sub-reject-item {
padding: 4px 8px;
border: 1px solid #7a7a7a;
&:hover {
background-color: #d3d3d3;
border-color: #bdbdbd;
cursor: pointer;
}
}
.reject-active {
background-color: #d3d3d3;
border-color: #bdbdbd;
cursor: pointer;
}
::v-deep {
.el-tabs__item {
font-size: 16px;
}
.el-tabs {
height: 100% !important;
}
.el-table {
.cell {
padding: 4px 5px 4px !important;
}
}
}
</style>
<style lang="scss">
.custom-detail-log {
.el-dialog__body {
padding-top: 8px !important;
}
}
</style>
SearchFrom.vue 搜索组件
<!--搜索表单组件-->
<template>
<el-form
class="search-box"
ref="searchForm"
:model="search"
:size="size"
:inline="true"
:label-width="width"
label-position="right">
<template v-for="form in searchOption">
<!-- :label="form.label" -->
<el-form-item
:label-width="form.width || width">
<!--选择框-->
<template v-if="form.type === 'select'">
<el-select v-model="search[form.prop]"
:multiple="form.multiple || false"
:filterable="form.filterable || true"
:placeholder="form.placeholder || `请选择${form.label}`"
:clearable="form.clearable">
<template v-if="form['optionSlot']">
<slot :name="form['optionSlot']" :option="form"></slot>
</template>
<template v-else>
<el-option v-for="(opt, odx) in form.option"
:key="'search_option_' + odx"
:label="opt[form.selectionLabel] || opt.label"
:value="opt[form.selectionId] || opt.value"
/>
</template>
</el-select>
</template>
<!--时间-->
<template v-else-if="form.type === 'time'">
<el-time-picker
is-range
v-model="search[form.prop]"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
:value-format="form.format || 'HH:mm:ss'"
:placeholder="form.placeholder || `请选择${form.label}`"
:clearable="form.clearable">
</el-time-picker>
</template>
<!--日期-->
<template v-else-if="form.type === 'date'">
<el-date-picker v-model="search[form.prop]"
type="date"
:placeholder="form.placeholder || `请选择${form.label}`"
:value-format="form.format || 'yyyy-MM-dd'"
:clearable="form.clearable"></el-date-picker>
</template>
<!--级联-->
<template v-else-if="form.type === 'cascader'">
<el-cascader v-model="search[form.prop]"
:placeholder="form.placeholder || `请选择${form.label}`"
:props="form.groupingProps"
:options="form.option"
:clearable="form.clearable"></el-cascader>
</template>
<!--范围日期-->
<template v-else-if="form.type === 'dateRange'">
<el-date-picker v-model="search[form.prop]"
type="daterange"
range-separator="-"
:value-format="form.format || 'yyyy-MM-dd'"
start-placeholder="开始日期"
end-placeholder="结束日期"
:clearable="form.clearable"></el-date-picker>
</template>
<!--搜索框-->
<template v-else>
<el-input class="search-input"
autocomplete="off"
v-model="search[form.prop]"
:placeholder="form.placeholder || `请输入${form.label}`"
:clearable="form.clearable" />
</template>
</el-form-item>
</template>
<!--扩展插槽-->
<slot name="form"></slot>
<div class="search-btn-box" v-if="searchOption.length > 0">
<el-button type="primary"
icon="el-icon-search"
@click="handleSearch">查询
</el-button>
<el-button
icon="el-icon-refresh"
@click="handleClear">重置
</el-button>
<div class="list-header__fun">
<template v-for="(fun, funIdx) in titleFun">
<template v-if="fun.slot">
<slot :name="fun.fun" :fun="fun" :funIdx="funIdx"></slot>
</template>
<template v-else>
<template
v-if="fun.limitsMenu == null && fun.limitsMenu == null || $common.findAuthFun(fun.limitsMenu, fun.limitsDown)">
<el-button
:key="`list_fun_${funIdx}`"
:class="['fun-btn hover', fun.class || '']"
:icon="fun.icon"
:type="fun.btnType"
@click="headerFunClick(fun.fun)">
{{ fun.label }}
</el-button>
</template>
</template>
</template>
</div>
</div>
</el-form>
</template>
<script>
import C from '@/util/constants'
import _ from 'lodash'
export default {
name: 'SearchForm',
model: {
prop: 'form',
event: 'update'
},
props: {
// 配置项
searchOption: {
default() {
return [];
},
type: Array
},
// 表单参数 (支持v-model)
form: {
type: Object,
default() {
return {};
}
},
// 头部按钮
titleFun: {
type: Array,
default() {
return []
}
},
// 尺寸
size: {
default: C.COMPONENT_SIZE,
type: String
},
// 标题宽度
width: {
default: '70px',
type: String
},
// 是否自定义清空
customClear: {
type: Boolean,
default: false
},
// 清空时是否触发搜索
searchOnClear: {
type: Boolean,
default: true
}
},
data() {
return {
search: {}
};
},
computed: {
searchOptionMap() {
const searchOptionMap = {}
const searchOption = this.searchOption
for (const option of searchOption) {
searchOptionMap[option.prop] = option
}
return searchOptionMap
}
},
watch: {
// 监听值的变化
form: {
handler(form) {
if (!_.isEqual(form, this.search)) {
this.search = { ...form }
}
},
deep: true,
immediate: true
},
search: {
handler(search) {
this.$emit('update', search)
},
deep: true
}
},
methods: {
/** 点击搜索 */
handleSearch() {
// 格式化搜索参数
const searchOptionMap = this.searchOptionMap;
const search = this.search;
for (const key in searchOptionMap) {
const option = searchOptionMap[key];
const value = search[key];
if (option.type === 'dateRange' || option.type === 'time') {
const [start = '', end = ''] = value ?? [];
search[option.start] = start;
search[option.end] = end;
} else {
search[key] = value ?? '';
}
}
this.search = { ...search }
this.$emit('search', this.search);
},
/** 头部点击事件 */
headerFunClick(funName) {
this.$emit('table-click', funName, {}, -1)
},
/** 清空搜索条件 */
handleClear() {
if (!this.customClear) {
const searchOptionMap = this.searchOptionMap
const search = this.search
for (const key in searchOptionMap) {
const option = searchOptionMap[key]
const def = option.default
const defaults = def === undefined ? '' : def
search[key] = defaults
if (option.type === 'dateRange') {
search[option.start] = defaults
search[option.end] = defaults
}
}
this.search = { ...search }
}
this.$emit('clear', this.search)
if (this.searchOnClear) {
this.$emit('search', this.search)
}
}
}
}
</script>
<style lang="scss" scoped>
.search-box {
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
$select-input-width: 170px;
$search-btn-width: 90px;
::v-deep {
.el-input--medium {
.el-input__inner {
height: 40px !important;
line-height: 40px !important;
}
}
}
.el-form-item {
margin-bottom: 10px;
.el-form-item__label {
font-size: 14px;
}
}
.search-btn-box {
display: flex;
flex-direction: row;
margin-bottom: 10px;
.el-button {
//border-radius: 20px;
//font-size: 13px;
}
}
.list-header__fun {
margin-left: 10px;
}
// 设置圆角
//.el-input__inner {
// border-radius: 15px;
//}
// 普通输入框
.search-input {
.el-input__inner {
width: $select-input-width;
}
}
// 单选日期
.el-date-editor--date {
width: $select-input-width;
.el-input__inner {
padding-right: 20px;
}
}
// 范围日期
.el-date-editor--daterange {
width: 240px;
height: 40px !important;
line-height: 40px !important;
}
// 选择框
//.el-select {
// width: $select-input-width;
//}
// 搜索按钮
.el-button {
width: $search-btn-width;
}
}
</style>
TableList.vue 表格组件
<template>
<div ref="table" :class="['table-list',{ 'box-base': box }]">
<!--表格列表-->
<el-table
ref="multipleTable"
v-loading="dialogLoading"
class="list__table"
:data="data"
:height="tableHeight"
:stripe="stripe"
border
v-bind="$attrs"
@selection-change="handleSelectionChange">
<!--自定义空行-->
<el-empty slot="empty-text" description="暂无数据"></el-empty>
<!--判断是否开启多选-->
<el-table-column
v-if="isSelection"
type="selection"
align="left"
width="70px"
:selectable="checkIsSelectable"></el-table-column>
<template v-for="column in columns">
<!--序号列-->
<el-table-column
v-if="column.type === 'index'"
type="index"
:label="column.label || '序号'"
:align="column.align || 'left'"
:width="column.width || 100"
:index="column['tableIndex'] || getTableIndex"
/>
<!--自定义展开列-->
<el-table-column v-else-if="column.type === 'expand'" type="expand">
<template slot-scope="scope">
<slot :name="column.prop" :row="scope.row" :index="scope.$index"></slot>
</template>
</el-table-column>
<!--操作列-->
<el-table-column
v-else-if="column.type === 'action'"
:label="column.label"
:align="column.align || 'left'"
:width="column.width || 150">
<template slot-scope="scope">
<template v-for="(action, actIdx) in column.actions">
<template v-if="actIdx < 5">
<template
v-if="action.showWithProp == null || ((action.showWithPropValue || []).indexOf(scope.row[action.showWithProp]) >= 0)">
<template
v-if="action.limitsMenu == null && action.limitsMenu == null || $common.findAuthFun(action.limitsMenu, action.limitsDown)">
<!-- getActionOptionValue(action, scope.row,'class') :class="`${action.class}`"-->
<el-button type="text" v-if="getIsBtnShow(scope.row[`${action.fun}BtnShow`])"
:key="`action_btn_${actIdx}`"
size="small"
:icon="`el-icon-${action.icon}`"
:class="action.customClass"
@click="actionClick(scope, action.fun, getActionOptionValue(action, scope.row, 'option'))">
{{ getActionOptionValue(action, scope.row, 'label') }}
</el-button>
</template>
</template>
</template>
</template>
<!-- 没有写权限判断哈-->
<template v-if="column.actions.length > 5">
<el-dropdown
trigger="click"
class="action-more-line"
@command="command => {actionClick(scope, command)}">
<span class="el-dropdown-link">更多<i
class="el-icon-arrow-down el-icon--right"></i></span>
<el-dropdown-menu slot="dropdown">
<template v-for="(action, actIdx) in column.actions">
<template v-if="actIdx >= 2">
<el-dropdown-item :command="action.fun">{{ action.label }}
</el-dropdown-item>
</template>
</template>
</el-dropdown-menu>
</el-dropdown>
</template>
</template>
</el-table-column>
<!--自定义列、常规列-->
<el-table-column
v-else
:label="column.label"
:align="column.align || 'left'"
:fixed="column.fixed || false"
:resizable="column.resize || false"
:min-width="column.width">
<template v-if="column.headerSlot" v-slot:header="scope">
<slot :name="column.headerSlot" :column="scope.column" :index="scope.$index"></slot>
</template>
<template slot-scope="scope">
<!--需自定义处理-->
<slot v-if="column.slot" :name="column.prop" :row="scope.row" :index="scope.$index"></slot>
<template v-else>
<!--开关列-->
<el-switch
v-if="column.propType === 'state'"
:value="getValueByProp(scope.row, column.prop)"
:active-value="column['open'] == null ? true : column['open']"
:inactive-value="column['close'] == null ? false : column['close']"
:inactive-color="column['closeColor'] || ''"
:active-color="column['openColor'] || ''"
:inactive-text="column['closeText'] || ''"
:active-text="column['openText'] || ''"
@change="(value)=>{switchChange(value, column, scope)}"
></el-switch>
<!--图片列-->
<template v-else-if="column.propType === 'image'">
<app-image-uploader
v-if="getValueByProp(scope.row, column.prop)"
:imageList="getValueByProp(scope.row, column.prop)"
:trashDisabled="false"></app-image-uploader>
<span v-else>--</span>
</template>
<!--状态文字列-->
<span v-else-if="column.propType === 'text'"
:style="{ color: getOptionValue(column.option, getValueByProp(scope.row, column.prop), 'color') }">{{
getOptionValue(column.option, getValueByProp(scope.row, column.prop), 'label', '/')
}}</span>
<!--格式化时间-->
<span v-else-if="column.propType === 'time'" :style="{ color: '' }">
{{ getValueByProp(scope.row, column.prop) | dateFormat }}</span>
<!--范围时间双字段-->
<!-- <span v-else-if="column.propType === 'time-range'" :style="{ color: '' }">{{-->
<!-- getValueByProp(scope.row, column.prop[0]) | dateTime(column.format)-->
<!-- }} ~ {{ getValueByProp(scope.row, column.prop[1]) | dateTime(column.format) }}</span>-->
<!--链接列-->
<span v-else-if="column.propType === 'link'">
<a
class="table-link hover-line-center"
target="_blank"
:href="getValueByProp(scope.row, column.prop)">{{ getValueByProp(scope.row, column.prop) }}</a>
</span>
<!--常规类-->
<span v-else :style="{ color: '' }">{{ getValueByProp(scope.row, column.prop) | defaults }}</span>
</template>
</template>
</el-table-column>
</template>
</el-table>
<!--分页组件-->
<pagination-box
v-if="isPagination"
:page="page"
:page-size="pageSize"
:page-sizes="pageSizes"
v-bind="$attrs"
@update:page="$emit('update:page', $event)"
@update:page-size="$emit('update:page-size', $event)"
v-on="$listeners" />
</div>
</template>
<script>
import PaginationBox from './PaginationBox.vue'
import appImageUploader from "@/components/list/AppImageUploader.vue";
import Sortable from 'sortablejs';
import C from '@/util/constants'
import _ from 'lodash'
export default {
name: 'TableList',
components: { PaginationBox, appImageUploader },
props: {
// 列表数据源
data: {
default() {
return []
},
type: Array
},
dialogLoading: {
type: [Boolean, Object],
default: false
},
// 表格列配置
columns: {
default() {
return []
},
type: Array
},
// 是否显示标题栏
showTitle: {
type: Boolean,
default: true
},
// 是否开启多选
// 是否开启多选
isSelection: {
default: false,
type: Boolean
},
// checkbox是否可用prop (仅isSelection为true时生效)
selectableProp: {
type: String,
default: null
},
draggableDOM: {
type: String,
default: ''
},
// 是否对selectableProp的值取反 (仅isSelection为true时生效)
selectableAnti: {
type: Boolean,
default: false
},
// 是否开启斑马线风格
stripe: {
default: true,
type: Boolean
},
// 是否显示阴影
box: {
default: true,
type: Boolean
},
// 头部标题
title: {
default: '',
type: String
},
// 是否显示分页
isPagination: {
default: true,
type: Boolean
},
// 页码
page: {
type: Number,
default: 1
},
// 每页条数
pageSize: {
type: Number,
default: C.LIST_PAGE_SIZE
},
pageSizes: {
default() {
return [C.LIST_PAGE_SIZE, 30, 50, 100, 200, 500];
},
type: Array
},
// 表格高度
height: {
type: [String, Number, Boolean],
default: true
},
// 是否可拖拽
isDraggable: {
default: false,
type: Boolean
}
},
data() {
return {
// 当前选择的列
selectList: [],
// 表格高度
tableHeight: undefined
}
},
watch: {
height(val) {
if (typeof val !== 'boolean') {
this.tableHeight = val
}
}
},
created() {
this.tableHeight = typeof this.height !== 'boolean' ? this.height : undefined
},
mounted() {
// 如果为bool值且为true时才自动计算高度
if (typeof this.height === 'boolean' && this.height) {
this.calculateTableListHeight()
window.addEventListener("resize", this.calculateTableListHeight);
}
if (this.isDraggable) {
this.rowDrop();
}
},
beforeDestroy() {
this.destorySortable();
window.removeEventListener("resize", this.calculateTableListHeight);
},
methods: {
/** 行拖拽 */
rowDrop() {
this.destorySortable();
setTimeout(() => {
const tbody = document.querySelector(`${this.draggableDOM} .el-table__body-wrapper>.el-table__body tbody`);
this.sortable = Sortable.create(tbody, {
animation: 180,
delay: 0,
onEnd: ({ newIndex, oldIndex }) => {
this.$refs.multipleTable.doLayout();
this.$emit("onDragEnd", newIndex, oldIndex);
},
});
}, 800);
},
destorySortable() {
if (this.sortable != null) {
this.sortable.destroy();
this.sortable = null;
}
},
getValueByProp(row, prop) {
return _.get(row, prop)
},
setValueByProp(row, prop, value) {
_.set(row, prop, value)
},
checkIsSelectable(row) {
const { selectableProp, selectableAnti } = this
if (selectableProp == null) {
return true
}
const selectable = this.getValueByProp(row, selectableProp)
return selectableAnti ? !selectable : selectable
},
/**
* @param option 外部传入的配置数组
* @param propValue 原始值
* @param key 需要获取的key
* @param def 默认值
* @returns {*|string}
*/
getOptionValue(option, propValue, key, def = '') {
// 判断值
propValue = typeof propValue === 'boolean' ? Number(propValue) : propValue
return option && option[propValue] ? option[propValue][key] : def
},
/** 获取操作按钮配置 */
getActionOptionValue(option, rowData, key) {
let actionOption = option
if (option.prop && option.option) {
const value = Number(rowData[option.prop])
actionOption = option.option[value]
}
return key === 'option' ? actionOption : (actionOption[key] || '')
},
/** 获取按钮状态 */
getIsBtnShow(btnShow) {
return (typeof btnShow !== 'undefined') ? btnShow : true
},
/** 序号 */
getTableIndex(index) {
return index + 1 + (this.page - 1) * this.pageSize
},
/** 多选事件 */
handleSelectionChange(selectionList) {
this.selectList = selectionList
this.$emit('select-change', selectionList)
},
/** 按钮点击事件 */
actionClick(scope, funName, option = {}) {
// if (option.confirmText) {
// this.$message({
// message: option.confirmText,
// type: 'warning'
// }).then(res => {
// if (res === 'confirm') {
// this.$emit('table-click', funName, JSON.parse(JSON.stringify(scope.row)), scope.$index)
// }
// }).catch(() => {
// })
// } else {
// }
this.$emit('table-click', funName, JSON.parse(JSON.stringify(scope.row)), scope.$index)
},
/** 开关点击事件 */
switchChange(value, option, scope) {
this.setValueByProp(scope.row, option.prop, value)
this.actionClick(scope, option.prop, option)
},
// 计算表格高度
calculateTableListHeight() {
// 当前视窗高度
const viewH = window.innerHeight
this.$nextTick(() => {
// setTimeout(() => {
const tableListDom = this.$refs.table
if (tableListDom == null) {
return
}
// 获取tableDom
const tableDom = tableListDom.querySelectorAll('.el-table')[0]
const pageDomAll = tableListDom.querySelectorAll('.pagination-box')
if (tableDom == null || pageDomAll == null) {
return
}
let pageH = 0
if (pageDomAll.length > 0) {
pageH = pageDomAll[0].offsetHeight
}
const rect = tableDom.getBoundingClientRect()
// 表格高度 = 视窗高度 - 表格top - 分页 - 底边距
this.tableHeight = viewH - rect.top - pageH - 15
// }, 100)
})
}
}
}
</script>
<style lang="scss" scoped>
.table-list {
margin-top: 8px;
overflow: hidden;
::v-deep {
.el-table {
transition: height 0.3s ease-out;
.el-table__cell {
padding: 20px 0 !important;
}
}
.pagination-box {
text-align: right !important;
}
}
.list__header {
.list-header__title {
span::before {
content: "";
position: absolute;
left: -10px;
width: 2px;
border-radius: 4px;
height: 20px;
top: 50%;
transform: translate(0, -50%);
}
}
}
.list__table {
width: 100%;
// 处理出现滚动条时的样式错误
.el-table__fixed-right-patch {
background-color: #2196f3;
border-bottom: 1px solid #2196f3;
}
thead {
th {
color: #333;
background-color: #fafafa;
font-weight: 400;
font-size: 14px;
border-right: none;
}
}
.el-table-column--selection {
.el-checkbox {
&.is-disabled {
.el-checkbox__inner::after {
display: none;
}
}
.el-checkbox__inner {
width: 22px;
height: 22px;
&::after {
left: 7px;
height: 12px;
width: 4px;
border-width: 2px;
}
}
}
}
tbody {
td {
color: var(--color-3);
font-size: 13px;
.table-link {
//color: var(--app-theme);
text-decoration: none;
padding-bottom: 2px;
}
}
}
// 按钮样式
.action-more-line {
.el-dropdown-link {
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
outline: 0;
cursor: pointer;
}
}
::v-deep {
.el-button--text {
& > span {
margin-left: 0 !important;
}
}
}
}
}
// 新增
.add {
color: #2196f3;
}
// 详情
.detail {
color: #7b68ee;
}
// 编辑
.edit {
color: #1e6abc;
}
// 删除
.del {
color: #ff301f;
}
// 重置
.reset {
color: #ff7f24;
}
// 审核
.audit {
color: #ee4000;
}
// 导出
.export {
color: #911656;
}
</style>
constants.js
const APP = {
// 每页默认条数
LIST_PAGE_SIZE: 10,
// 组件默认尺寸
COMPONENT_SIZE: 'medium',
// 上传图片大小默认值
UPLOAD_IMAGE_SIZE: 5,
// 附件/文件大小限制
UPLOAD_FILE_SIZE: 50,
// 视频大小
UPLOAD_VIDEO_SIZE: 200,
// 图片上传格式
UPLOAD_IMAGE_TYPE: 'jpg,jpeg,png',
// 附件上传格式
UPLOAD_FILE_TYPE: 'pdf,docx,xlsx,ppt,txt',
// 视频上传格式
UPLOAD_VIDEO_TYPE: 'mp4,mp3'
}
const C = {...APP}
export default C
PaginationBox.vue 分页组件
<template>
<div class="pagination-box" :style="{textAlign: align}">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
:hide-on-single-page="singHide"
:size="size"
:current-page="page"
:page-sizes="pageSizes"
:page-size="pageSize"
:layout="layout"
:total="total">
</el-pagination>
</div>
</template>
<script>
import C from '@/util/constants';
export default {
name: 'PaginationBox',
props: {
// 当前页码
page: {
default: 1,
type: Number
},
// 总记录数
total: {
default: 0,
type: Number
},
// 每页条数
pageSize: {
default: C.LIST_PAGE_SIZE,
type: Number
},
//
pageSizes: {
default() {
return [C.LIST_PAGE_SIZE, 30, 50, 100, 200, 500];
},
type: Array
},
// 底部菜单
layout: {
default() {
return 'total, sizes, prev, pager, next, jumper';
},
type: String
},
// 只有一页的时候是否隐藏 默认隐藏
singHide: {
default() {
return false;
},
type: Boolean
},
// 尺寸
size: {
default() {
return 'small'
},
type: String
},
// 位置
align: {
default: 'center',
type: String
}
},
methods: {
// 每页条数变化
handleSizeChange(size) {
this.$emit('update:page-size', size)
this.$emit('page-change', this.page, size);
},
// 页面发生变化
handleCurrentChange(page) {
this.$emit('update:page', page)
this.$emit('page-change', page, this.pageSize);
}
}
}
</script>
<style lang="scss" scoped>
.pagination-box {
padding: 20px 30px;
}
</style>
appImageUploader.vue 图片查看组件
<template>
<div class="app-image">
<div class="app-image__inner"
:style="cssVars">
<div v-for="(item,index) in innerImages"
:key="index"
class="app-image__item">
<el-image
:src="item"
fit="cover"
:preview-src-list="innerImages">
</el-image>
</div>
</div>
</div>
</template>
<script>
export default {
name: "AppImage",
model: {
prop: 'imageList',
event: 'input'
},
props: {
imageList: {
type: [Array, String],
default() {
return []
}
},
// 上传图标颜色
iconColor: {
type: String,
default: "#999999"
},
iconHoverColor: {
type: String,
default: "#409eff"
},
//装图标的盒子大小
itemWidth: {
type: String,
default: "90px"
},
itemHeight: {
type: String,
default: "90px"
},
// 第二条备注
prompt2: {
type: String,
default: ''
},
// 上传盒子之间间隔
uploaderMargin: {
type: String,
default: "14px"
},
uploadType: {
type: Array,
default() {
return []
}
},
uploadSize: {
type: Number,
default: 0
}
},
computed: {
cssVars() {
return {
'--uploader-width': this.itemWidth,
'--uploader-height': this.itemHeight,
'--uploader-icon': this.iconFontSize,
'--uploader-icon-color': this.iconColor,
'--uploader-icon--hover-color': this.iconHoverColor,
'--uploader-margin-left': this.uploaderMargin
}
},
innerImages: {
get() {
if (typeof this.imageList === "string") {
return this.imageList.length <= 0 ? [] : [this.imageList];
}
return this.imageList;
},
set(value) {
if (typeof this.imageList === "string") {
this.$emit("input", value[0] ? value[0]: "");
}
this.$emit("input", value);
}
}
},
data() {
return {
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.app-image {
display: flex;
align-items: center;
}
.app-image__inner {
display: flex;
flex-direction: row;
}
.app-image__item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: var(--uploader-width);
height: var(--uploader-height);
::v-deep {
.el-image {
width: 100%;
height: 100%;
border-radius: 10px;
> image {
width: 100%;
height: 100%;
}
}
}
& + .app-image__item {
margin-left: var(--uploader-margin-left);
}
}
.item-trash {
position: absolute;
right: -13px;
top: -9px;
cursor: pointer;
font-size: 20px;
&:hover {
color: var(--uploader-icon--hover-color);
}
}
.add {
border: 1px dashed var(--uploader-icon-color);
border-radius: 10px;
&:hover {
border-color: var(--uploader-icon--hover-color);
.item-upload {
color: var(--uploader-icon--hover-color);
}
}
}
.item-upload {
font-size: var(--uploader-icon);
color: var(--uploader-icon-color);
}
.app-image__icon__upload {
font-size: 65px;
}
.app-image__file {
position: absolute;
top: 0;
opacity: 0;
width: var(--uploader-width);
height: var(--uploader-height);
cursor: pointer;
&:hover {
.item-upload {
color: var(--uploader-icon--hover-color);
}
}
}
.app-image__prompt {
margin-left: 20px;
color: var(--color-8);;
}
.app-image__prompt__content {
display: flex;
flex-direction: column;
}
</style>