Files
SaaS-lib/nervui-resource-repository/src/view/tree-and-table-list.vue

943 lines
29 KiB
Vue
Raw Normal View History

2024-05-15 17:29:42 +08:00
<template>
<page-title :title="getTitle"/>
<div class="ns-table-wrapper">
<div class="ns-table" :class="{ 'ns-table-no-search': !(formConfig?.schemas.length > 0) }">
<a-spin :spinning="tableState.loading">
<a-row type="flex" class="ns-table-main">
<a-col flex="180px">
<ns-table-header
:headerActions="headerActions"
:searchData="formModel"
:tableTitle="tableTitle"
:data="tableState.selectedRows">
<template #header="data">
<slot name="header" v-bind="data || {}"></slot>
</template>
</ns-table-header>
<Skeleton :loading="loadingTree" active>
<div class="type-tree-wrapper">
<div class="tree-top-header">
<div class="tree-node-content-wrapper" :class="{'selected': allSelected}" @click="onAllSelect">全部类别</div>
<ns-button type="link" @click="onContextMenuClick(null, 'add')" v-if="authMap['add']">
<PlusCircleOutlined/>
添加类别
</ns-button>
</div>
<a-tree v-model:expandedKeys="expandedKeys" :tree-data="treeData" v-model:selectedKeys="selectedKeys"
@expand="onExpand" @select="onTreeSelect">
<template #title="{ key: treeKey, title, dataRef }">
<a-dropdown :trigger="['contextmenu']">
<span style="min-width: 80px; display: block">{{ title }}</span>
<template #overlay>
<a-menu @click="({ key: menuKey }) => onContextMenuClick(treeKey, menuKey, dataRef)">
<a-menu-item key="edit" v-if="authMap['edit']">
<EditOutlined/>
编辑
</a-menu-item>
<a-menu-item key="add" v-if="authMap['add']">
<PlusCircleOutlined/>
添加
</a-menu-item>
<a-menu-item key="remove" v-if="authMap['remove']">
<DeleteOutlined/>
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tree>
</div>
</Skeleton>
</a-col>
<a-col flex="auto">
<!-- {{ formConfig }} -->
<div class="ns-table-search">
<ns-form
ref="formElRef"
class="ns-table-form"
:showAction="true"
v-bind="formConfig"
formLayout="flex"
:expand="expand"
:showExpand="showExpand"
:model="formModel"
@finish="formFinish"/>
</div>
<Skeleton :loading="loadingTable" active>
<div class="section0">
<div class="section-content" v-if="tableData && tableData.length>0">
<template v-for="(item) in tableData">
<div class="item card-item" @click="onCLickCard(item)">
<header class="item-header">
<div class="info-card-header">
<div class="card-title">{{ item['alias'] }}</div>
</div>
</header>
<div class="info-item-block">
<div class="card-icon"><img :src="imgSrc + item['iconID']" alt=""></div>
<div class="card-description-wrapper">
<div class="card-description"><span>{{ item['desc'] }}</span>
</div>
</div>
</div>
<footer class="item-footer">
<div class="card-item-footer">
<div class="card-footer-left">
{{ item['typeClassAlias'] }}
</div>
<div class="card-footer-right">
<div class="card-status">
<span v-if="item['shelfState']"> <i class="iconfont iconGreen font-12">&#xe658;</i> 已上架
</span>
<span v-if="!item['shelfState']"><i class="iconfont iconRed font-12">&#xe658;</i> 待上架
</span>
</div>
</div>
</div>
</footer>
</div>
</template>
</div>
<div class="section-content" v-if="tableData && tableData.length === 0 || !tableData">
<div class="ns-table-content"> <a-empty /> </div>
</div>
</div>
<div class="pagination" v-if="tableData && tableData.length === 0 || tableData!=null">
<a-pagination
v-bind="getPagination"
show-quick-jumper
show-size-changer
@change=" (current,pageSize) => onChange({current,pageSize})"
size="small"
:show-total="(total, range) => `显示 ${range[0]} 到 ${range[1]} 条数据,共计${total}条`"
/>
</div>
</Skeleton>
</a-col>
</a-row>
</a-spin>
</div>
</div>
<a-modal v-model:visible="modalVisible" :title="modalOperation.title" @ok="handleOk" @cancel="cancel" v-if="modalVisible">
<div class="add-form" id="app" v-if="typeSchema.length">
<ns-form
ref="configRef"
formLayout="ns-vertical-form"
:schemas="typeSchema"
:model="typeData"
/>
</div>
</a-modal>
</template>
<script lang="ts">
import {computed, defineComponent, reactive, ref, unref, watch} from "vue";
import {Skeleton, Pagination} from "ant-design-vue";
import {debounce, cloneDeep, isArray, get} from "lodash-es";
import {tableProps} from "../../../lib/component/table/props";
import {http, PropTypes, stringUtil} from "../../../lib/util";
import {useRoute} from "vue-router";
import {PlusCircleOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined} from "@ant-design/icons-vue";
import {NsMessage, NsModal} from "../../../lib/component";
import {RequestParams} from "/nerv-lib/component/table/table";
import {AxiosRequestConfig} from "axios";
import {useApi, useParams} from "/nerv-lib/use";
import {useTableRefresh} from "/nerv-lib/component/table/use-table-refresh";
import {useTableSession} from "/nerv-lib/component/table/use-table-session";
import {APP} from "/@/router/index.ts";
import {authorizationService} from "/nerv-lib/paas";
export default defineComponent({
name: 'TreeAndTableList',
components: {
Skeleton,
PlusCircleOutlined,
EditOutlined,
DeleteOutlined,
'a-pagination': Pagination,
},
props: {
...tableProps,
title: PropTypes.string,
tableTitle: PropTypes.func,
showTitle: {
type: Boolean,
default: true,
},
resultField: PropTypes.string
},
setup: (props, {attrs, emit}) => {
const nsTableRef = ref();
const route = useRoute();
const formModel = ref({});
// 搜索form参数
const formParamsRef = ref({});
// table参数
const tableState = reactive({
selectedRowKeys: [],
selectedRows: [],
loading: false,
loadError: false,
loadErrorMessage: '',
});
const expandedKeys = ref()
const { delayFetch } = useTableRefresh({ props, reload });
const defaultPageRef = ref(0);
const { getParams } = useParams();
const treeParamsRef = ref({});
const dataRef = ref([]);
const orderRef = ref({});
const { setTableSession } = useTableSession(formModel.value, formParamsRef, defaultPageRef);
const tableData = ref<Recordable[]>([]);
let allSelected = ref(true);
const selectedKeys = ref([]);
let loadingTable = ref<boolean>(false);
let loadingTree = ref<boolean>(false);
const imgSrc = ref(`/api/${APP}/objs/Images/`)
let typeFieldErrors = ref({});
// 缓存
function initTableSession() {
const { fullPath } = route;
const tableSession = JSON.parse(sessionStorage[fullPath] || '{}');
if (!props.enableTableSession) return;
if (tableSession['formModel']) {
Object.assign(formModel.value, tableSession['formModel']);
let code = formModel.value['typeClassCode'];
if(code){
treeParamsRef.value['typeClassCode'] = code;
selectedKeys.value =[ code];
allSelected.value = false;
}
}
}
initTableSession();
//权限部分
let authMap = ref({});
const resourceTypeClassOp= ['add', 'edit', 'remove'];
resourceTypeClassOp.forEach( key => {
authMap.value[key]= authorizationService().checkPermission('resourceRepo', 'resourceTypeClass', key);
})
// 页面标题
const getTitle = computed(() => {
const {title} = props;
if (title) return title;
const {
params: {pageTitle},
} = route;
if (pageTitle) return pageTitle;
});
// 搜索参数
const formFinish = debounce((data) => {
formParamsRef.value = data;
fetch({
page: 1,
});
}, 300);
const tableChangeEvent = (pagination: Props, filters: [], sorter?: any) => {
// console.log('params', pagination, filters, sorter);
if (sorter?.field) {
if (sorter.order) {
orderRef.value = {
[props.paramsOrderField]: stringUtil.toLine(
`${sorter.field} ${sorter.order.replace('end', '')}`,
),
};
} else {
orderRef.value = { [props.paramsOrderField]: '' }; //覆盖默认params
}
fetch({
page: pagination?.current || getPagination.value?.current || 1,
pageSize: pagination?.pageSize,
});
} else if (pagination?.current) {
fetch({
page: pagination?.current,
pageSize: pagination.pageSize,
});
}
};
// pagination
const getPagination: Recordable | Boolean = computed(() => {
const { pagination } = props;
if (pagination) {
const current = get(dataRef.value, props.pageField);
return {
showQuickJumper: true,
showLessItems: true,
showSizeChanger: true,
showTotal: (total: number, range: Array<number>) =>
`显示第${range[0]}${range[1]}条记录 ,共 ${total} 条记录`,
...(pagination as Props),
total: get(dataRef.value, props.totalField),
current: (current >= 0 ? current : 0) + props.pageFieldOffset, // 后端0 开始
pageSize: get(dataRef.value, props.sizeField),
}
}
return false;
});
const getTableBindValues = computed(() => {
const { params, dynamicParams } = props;
return {
...attrs,
...props,
params: dynamicParams
? getParams({ ...route.params, ...route.query }, dynamicParams, params)
: params || {},
pagination: getPagination.value,
onChange: tableChangeEvent,
};
});
watch(
() => getTableBindValues.value.api,
() => {
fetch();
},
{
immediate: true,
},
);
function fetch(requestParams: RequestParams = {}, clearDelay = true) {
clearDelay && delayFetch();
const { api, pagination } = props;
const { page, pageSize } = requestParams;
if (api) {
let pageParams: Recordable = {};
if (pagination !== false) {
pageParams = {
[props.paramsPageField]: page ? page - props.pageFieldOffset : defaultPageRef.value, // 后端0 开始
[props.paramsPageSizeField]:
pageSize || getPagination.value?.pageSize || props.defaultPageSize,
};
} else {
pageParams = {
[props.paramsPageField]: defaultPageRef.value, // 后端0 开始
[props.paramsPageSizeField]:
pageSize || getPagination.value?.pageSize || props.defaultPageSize,
};
}
const httpParams = {
...getTableBindValues.value.params,
...pageParams,
...formParamsRef.value,
...treeParamsRef.value,
...orderRef.value,
};
setTableSession(pageParams[props.paramsPageField]);
clearDelay && setLoading(true);
const requestConfig: AxiosRequestConfig = { method: 'get' };
const { httpRequest } = useApi();
httpRequest({
api,
params: httpParams,
pathParams: { ...route.params, ...route.query },
requestConfig,
}).then((res: any) => {
tableState.loadError = false;
tableState.loadErrorMessage = '';
dataRef.value = res;
tableData.value = get(unref(dataRef), props.listField);
emit('update:dataSource', tableData.value);
clearDelay && setLoading(false);
loadingTable.value = false;
})
.catch((error: any) => {
const { response, code, message } = error || {};
let errMessage = response?.data?.msg;
const err: string = error?.toString?.() ?? '';
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = '接口请求超时,请刷新页面重试!';
}
if (err?.includes('Network Error')) {
errMessage = '网络异常,请检查您的网络连接是否正常!';
}
tableState.loadError = true;
tableState.loadErrorMessage = errMessage;
clearDelay && setLoading(false);
loadingTable.value = false;
});
}
}
function setLoading(loading: boolean) {
tableState.loading = loading;
}
// 右侧树形结构
const treeData = ref( []);
// 树形结构添加
let typeSchema = ref([]);
const getTypeSchema = (mode) => {
let schema = [
{
field: 'alias',
component: 'NsInput',
label: '类别名称',
rules: [
{
required: true,
trigger: 'change',
validator: async (rule, value) => {
if (!value) {
return Promise.reject('类别名称不能为空');
}
if (!/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9-_\u4e00-\u9fa5]{0,50}$/.test(value)) {
return Promise.reject(
'只能包括大小写字母、中文、数字和短横线(-)、下划线(_)。必须以字母、中文、数字开头。长度必须在150字符之间。'
);
}
},
},
{
trigger: 'change',
validator: async (rule, value) => {
if (Object.keys(typeFieldErrors.value).indexOf('alias') !== -1) {
const errorInfo = typeFieldErrors.value['alias'];
delete typeFieldErrors.value['alias'];
return Promise.reject(errorInfo);
}
},
},
],
},
];
if (mode === 'add') {
schema.push(
{
field: 'code',
component: 'NsInput',
label: '类别代码',
rules: [
{
required: true,
trigger: 'change',
validator: async (rule, value) => {
if (!value) {
return Promise.reject('类别代码不能为空');
}
if (!/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9-_\u4e00-\u9fa5]{0,50}$/.test(value)) {
return Promise.reject(
'只能包括大小写字母、中文、数字和短横线(-)、下划线(_)。必须以字母、中文、数字开头。长度必须在150字符之间。'
);
}
},
},
{
trigger: 'change',
validator: async (rule, value) => {
if (Object.keys(typeFieldErrors.value).indexOf('code') !== -1) {
const errorInfo = typeFieldErrors.value['code'];
delete typeFieldErrors.value['code'];
return Promise.reject(errorInfo);
}
},
},
],
})
} else if (mode === 'edit') {
schema.push(
{
field: 'code',
component: 'NsInputText',
label: '类别代码',
})
}
return schema || [];
}
const typeData = ref({});
let modalVisible = ref(false);
let modalOperation = ref({
title: '添加类别',
key: 'add'
});
// 搜索
const formElRef = ref(null);
const getFormConfig = computed(() => {
const formConfig = cloneDeep(props.formConfig);
if (formConfig) {
formConfig.showAction = false;
if (!isArray(formConfig.schemas)) {
formConfig.schemas = [];
}
if (formConfig.keySearch !== false) {
formConfig.schemas.push({
field: 'alias',
label: '关键字',
component: 'NsInputSearch',
rules: [
{
message: '请输入关键字',
trigger: 'change',
},
],
componentProps: {
maxlength: 50,
onSearch: () => {
unref(formElRef)?.triggerSubmit()
// unref(nsTableRef)?.formElRef?.triggerSubmit();
},
onKeydown: (event) => {
//fix 单个input回车会提交表单 造成重复提交
if (event.key === 'Enter' || event.code === 'Enter') {
event.preventDefault();
}
},
placeholder: '请输入关键字',
},
});
}
}
return formConfig;
});
// 卡片列表分页信息
function onTreeSelect(selectedKeys: never[],
e: {
selected: boolean;
selectedNodes: { props: { dataRef: any } }[];
node: any;
event: any;
})
{
allSelected.value = false;
const dataRef = e.selectedNodes[0];
treeParamsRef.value = getParams(dataRef, {typeClassCode: 'code'});
unref(formElRef)?.triggerSubmit();
let key = 'typeClassCode';
if (e.selected) {
formModel.value[key] = treeParamsRef.value[key];
} else {
updateFormModel();
}
}
function reload(clearDelay = true) {
const pagination = unref(getPagination);
fetch(
{
page: pagination === false ? 1 : pagination.current,
},
clearDelay,
);
}
const updateFormModel = function (value?:any) {
const key = 'typeClassCode';
const model: object = formModel.value;
if (value) {
if (Object.keys(model).includes(key) && model[key] === value) {
delete model[key];
}
} else if (Object.keys(model).includes(key)){
delete model[key];
}
}
return {
tableState,
getTitle,
treeData,
expandedKeys,
formModel,
formFinish,
tableData,
typeSchema,
typeData,
modalVisible,
modalOperation,
typeFieldErrors,
getTypeSchema,
formConfig: getFormConfig.value,
nsTableRef,
formElRef,
getParams,
onTreeSelect,
onChange: tableChangeEvent,
allSelected,
selectedKeys,
treeParamsRef,
loadingTable,
loadingTree,
imgSrc,
getTableBindValues,
getPagination,
authMap,
updateFormModel,
setTableSession
}
},
mounted() {
this.getDataTree()
},
methods: {
onCLickCard(item) {
this.$router.push(item['code'] + '/detail' );
},
// 转换树结构数据
transferTree(data) {
if( isArray(data) && data.length > 0) {
data.forEach(it=>{
it['title'] = it['alias'];
it['key']=it['code'];
if(it['children'] ) {
this.transferTree(it['children']);
}
})
} else {
return;
}
},
getDataTree() {
http.get(`/api/${APP}/objs/ResourceTypeClasses`).then(res => {
this.transferTree(res)
this.treeData = res;
this.loadingTree = false;
}).catch( err => {
this.loadingTree = false;
})
},
onAllSelect() {
this.allSelected = !this.allSelected;
this.selectedKeys = [];
this.treeParamsRef = null;
this.updateFormModel();
this.formElRef.triggerSubmit();
},
// onExpand(keys) {
// this.expandedKeys = keys;
// },
onContextMenuClick(treeKey, menuKey, data) {
this.modalOperation.key = menuKey
if (menuKey === 'add') {
this.modalOperation.title = '添加类别';
this.modalVisible = true;
this.typeData['parentID'] = data?.id || null;
this.typeSchema = this.getTypeSchema('add')
}
if (menuKey === 'edit') {
this.modalOperation.title = '编辑类别';
this.modalVisible = true;
this.typeData = {
alias: data['title'],
code: data['code'],
id: data['id'],
parentID: data['parentID']
}
this.typeSchema = [
{
field: 'alias',
component: 'NsInput',
label: '类别名称',
rules: [
{
required: true,
trigger: 'change',
validator: async (rule, value) => {
if (!value) {
return Promise.reject('类别名称不能为空');
}
if (!/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9-_\u4e00-\u9fa5]{0,50}$/.test(value)) {
return Promise.reject(
'只能包括大小写字母、中文、数字和短横线(-)、下划线(_)。必须以字母、中文、数字开头。长度必须在150字符之间。'
);
}
},
},
],
},
{
field: 'code',
component: 'NsInputText',
label: '类别代码'
},
{
field: 'parentID',
label: '类别',
component: 'NsSelectTreeApi',
componentProps: {
showCheckedStrategy: 'TreeSelect.SHOW_ALL',
selectType: 'tree',
api: `/api/${APP}/objs/ResourceTypeClasses`,
labelField: 'alias',
valueField: 'id',
resultField: '',
immediate: true,
placeholder: '请选择类型',
allowClear: true,
unSelectable: {
level: -1,
}
}
}
];
}
if (menuKey === 'remove') {
const thisObj = this;
NsModal.confirm(
{
title: '警告',
content: `确定要删除该项吗?\n删除后不可恢复请谨慎操作`,
okText: '确认',
cancelText: '取消',
onOk() {
http.delete(`/api/${APP}/objs/ResourceTypeClasses`, {id: data['id']}).then(() => {
NsMessage.success('删除成功');
if (data['code']) {
thisObj.updateFormModel( data['code']);
thisObj.setTableSession(thisObj.getPagination.current);
}
thisObj.getDataTree();
})
}
});
}
},
handleArgumentError(data) {
const error = data?.response?.data;
if(error && error['errType'] === 'ArgumentError') {
this.typeFieldErrors = error['fieldErrors'];
this.$refs.configRef.triggerSubmit();
}
},
addType() {
http.post(`/api/${APP}/objs/ResourceTypeClasses`, this.typeData).then((res) => {
NsMessage.success('操作成功');
this.getDataTree();
this.typeData = {};
this.modalVisible = false;
}).catch((e)=>{
this.handleArgumentError(e)
})
},
editType() {
if (Object.keys(this.typeData).includes('parentID')) {
this.typeData['parentID'] = 0;
}
http.put(`/api/${APP}/objs/ResourceTypeClasses`, this.typeData).then((res) => {
NsMessage.success('操作成功');
this.getDataTree();
this.typeData = {};
this.modalVisible = false;
}).catch((e)=>{
this.handleArgumentError(e)
})
},
removeType() {
console.log(this.typeData)
},
handleOk() {
if (['add'].includes(this.modalOperation.key)) {
this.$refs.configRef.triggerSubmit().then(() => {
this.addType();
});
}
if (['edit'].includes(this.modalOperation.key)) {
this.$refs.configRef.triggerSubmit().then(() => {
this.editType();
});
}
if (['remove'].includes(this.modalOperation.key)) {
this.removeType();
}
},
cancel() {
this.typeData = {};
this.modalVisible = false;
this.typeFieldErrors = {};
}
},
});
</script>
<style lang="less" scoped>
.ns-table-main {
flex-wrap: nowrap;
}
:deep(.ns-table-header) {
min-width: unset;
.ant-btn {
margin-left: 0 !important;
}
}
.ns-table-wrapper {
margin: 0 24px;
}
:deep(.ns-table-search .ns-form-item label) {
display: none;
}
.type-tree-wrapper {
color: rgba(0, 0, 0, 0.65);
border-right: 1px solid rgba(0, 0, 0, 0.08);
height: 800px;
overflow-y: auto;
.tree-top-header {
display: flex;
justify-content: space-between;
cursor: pointer;
margin-bottom: 4px;
.tree-node-content-wrapper {
padding: 0 4px;
line-height: 30px;
&.selected {
background-color: rgba(0, 172, 255, 0.1);
color: #00acff;
}
}
}
}
:deep(.ant-tree ) {
color: rgba(0, 0, 0, 0.65);
.ant-tree-node-content-wrapper.ant-tree-node-selected {
background-color: rgba(0, 172, 255, 0.1);
color: #00acff
}
}
// 卡片列表
.section0 {
height: 100%;
max-height: 600px;
overflow-y: auto;
.iconRed {
color: #EB5757;
}
.iconGreen {
color: #0DCB70;
}
.font-12 {
font-size: 12px;
}
margin: 0 6px 0 24px;
.section-content {
.item {
width: 24%;
//border: 1px solid #dedede;
margin-right: 8px;
display: inline-block;
margin-bottom: 10px;
cursor: pointer;
//transition: box-shadow 400ms;
//box-shadow: none;s
transition-property: box-shadow, border;
transition-duration: 400ms, 400ms;
border: 1px solid #F1F3F5;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.09);
border-radius: 6px;
&:hover {
//box-shadow: 0 2px 12px rgba(0, 172, 255, .7);
border: 1px solid #6DCFFF;
box-shadow: 0 0 16px rgba(0, 172, 255, 0.3);
}
.item-header {
background-color: inherit;
display: flex;
height: auto;
padding: 10px 14px 0 14px;
justify-content: space-between;
align-items: center;
//border-bottom: .05rem solid #dedede;
.info-card-header {
flex: 1;
height: 32px;
max-height: 32px;
overflow: hidden;
.card-title {
font-size: 16px;
font-weight: 500;
overflow-x: hidden;
overflow-y: hidden;
text-overflow: ellipsis
}
}
}
.info-item-block {
height: 90px;
overflow: hidden;
align-items: center;
//border-bottom: .05rem solid #dedede;
padding: 4px 14px;
display: flex;
.card-icon {
display: flex;
align-items: center;
text-align: center;
//width: 54px;
max-height: 88px;
img {
width: 64px;
max-height: 88px;
}
}
.card-description-wrapper {
display: grid;
height: 88px;
width: 100%;
align-items: center;
overflow: hidden;
.card-description {
max-height: 88px;
padding-left: 6px;
span {
line-height: 20px;
}
}
}
}
.item-footer {
padding: 10px 14px;
background-color: #fafbfe;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
.card-item-footer {
display: flex;
justify-content: space-between;
width: 100%;
overflow-x: auto;
.card-footer-left {
line-height: 22px;
}
}
}
}
}
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 6px;
}
// 无数据
.ant-empty-description {
color: #bfbfbf;
}
</style>