Files
SaaS-lib/nervui-resource-repository/src/view/tree-and-table-list.vue
xuziqiang d0155dbe3c push
2024-05-15 17:29:42 +08:00

943 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>