This commit is contained in:
xuziqiang
2024-05-15 17:29:42 +08:00
commit d0155dbe3c
7296 changed files with 1832517 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# port
VITE_PORT = 4200
# spa-title
VITE_GLOB_APP_TITLE = nervui-resource-manage
# spa shortname
VITE_GLOB_APP_SHORT_NAME = nervui-resource-manage

View File

@@ -0,0 +1,22 @@
# Whether to open mock
VITE_USE_MOCK = true
# public path
VITE_PUBLIC_PATH = /nervui-resource-manage/
# Cross-domain proxy, you can configure multiple
# Please note that no line breaks
VITE_PROXY = {"/api": { "target": "http://portal.cloud.dev2.dingcloud.com:30080", "changeOrigin": true }}
# VITE_PROXY=[["/api","https://vvbin.cn/test"]]
# Delete console
VITE_DROP_CONSOLE = false
# Basic interface address SPA
VITE_GLOB_API_URL=/basic-api
# File upload address optional
VITE_GLOB_UPLOAD_URL=/upload
# Interface prefix
VITE_GLOB_API_URL_PREFIX=

View File

@@ -0,0 +1,35 @@
# Whether to open mock
VITE_USE_MOCK = true
# public path
VITE_PUBLIC_PATH = /nervui-resource-manage/
# Delete console
VITE_DROP_CONSOLE = true
# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none
# If you need multiple forms, you can use `,` to separate
VITE_BUILD_COMPRESS = 'none'
# Whether to delete origin files when using compress, default false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# Basic interface address SPA
VITE_GLOB_API_URL=/basic-api
# File upload address optional
# It can be forwarded by nginx or write the actual address directly
VITE_GLOB_UPLOAD_URL=/upload
# Interface prefix
VITE_GLOB_API_URL_PREFIX=
# Whether to enable image compression
VITE_USE_IMAGEMIN= true
# use pwa
VITE_USE_PWA = false
# Is it compatible with older browsers
VITE_LEGACY = false

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,74 @@
#!/bin/bash
SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
if [ -z $WORKSPACE ];then
echo "WORKSPACE not exists"
else
set DIR=$WORKSPACE
fi
echo "current dir"
echo "$DIR"
cd "$DIR"
projectname=$(basename `pwd`)
npm run rm-build
if [ -d "$DIR/dist" ];then
cd "$DIR/dist"
# copy module.json
cp ../module.json ./
# package
VERSION=$(cat ../.version)
lastdir=../release/
if [ -d ${lastdir} ];then
echo "删除旧release文件夹"
rm -rf ${lastdir}
else
echo "文件夹不存在!"
fi
mkdir -p ${lastdir}
dir=../release/nerv/$projectname/$VERSION
mkdir -p ${dir}
tar -zcvf "${dir}/$projectname-$VERSION.tgz" ./*
templatedir=../release/resources/templates/nerv/$projectname/$VERSION/$projectname
mkdir -p ${templatedir}
cp -r ../resources/templates/* ${templatedir}
cd ../
releasefile=nerv-$projectname-$VERSION.tgz
if [ -f ${releasefile} ];then
echo "删除旧包!"
rm -rf ${releasefile}
fi
tar -zcvf ${releasefile} ./release/* release.yaml
mkdir -p ./release/nervui
cp -r ./release/nerv/* ./release/nervui
if [ -f ${releasefile} ];then
echo "编译成功!"
mv ${releasefile} ./release
else
echo "编译失败!!!"
exit 1
fi
else
echo "编译失败!!!"
exit 1
fi

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="/favicon.ico" rel="icon" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>资源管理</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
[
{
"catalog": "计算",
"icon": "&#xe787;",
"name": "resourceManage",
"label": "资源管理",
"menus": [
{
"name": "resource",
"url": "/rm/resourceManage",
"label": "资源管理",
"operation": {
"resource": "resource",
"method": "list"
},
"submenus": []
}
]
}
]

View File

@@ -0,0 +1,11 @@
{
"release": [
{
"src": "nervui-resource-manage/release",
"dest": "/upload/pkg",
"include": [
"nervui-resource-manage-(\\d+).(\\d+).(\\d+)(|-\\w+).tar.gz"
]
}
]
}

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
cd "$DIR" || exit
VERSION=$(cat "$(pwd)"/.version)
sed "s|\${VERSION}|${VERSION}|g" "$(pwd)"/offline.tpl.yaml > "$(pwd)"/offline.yaml || exit 1
releaseDir="./release/nervui/nerv/nervui-resource-manage-offline"
rm -rf $releaseDir
version="${VERSION}-$(cat "$(pwd)"/offline.version)"
set -x
docker run --rm -i \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)"/offline.yaml:/app/build.yaml \
-v "$(pwd)/$releaseDir":/app/output \
-e name=nervui-resource-manage \
-e version="$version" \
--pull=always \
registry.nervhub.nervstack.io/nerv3/deploy:latest
rm -rf $releaseDir/output

View File

@@ -0,0 +1,5 @@
images:
helm:
- name: nervui-resource-manage
version: "${VERSION}"
repository: "https://registry.nervhub.nervstack.io/chartrepo/nerv3-ui"

View File

@@ -0,0 +1 @@
alpha1

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,12 @@
# 上传release目录信息到nerv-file仓库
release:
- {src: release, dest: /upload/pkg, include: [".*(.tgz)$"]}
- {src: release/resources/templates, dest: /upload/templates}
register:
name: nervui-resource-manage
version: 1.0.0
components:
- type: nervui-resource-manage
resources:
- {type: template, relativePath: /nervui-resource-manage/deploy.json}

View File

@@ -0,0 +1,69 @@
<template>
<Skeleton
active
:loading="loading">
<ns-application/>
</Skeleton>
</template>
<script lang="ts" type="module">
import {defineComponent, provide, ref, onUnmounted, onBeforeUnmount} from 'vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import {useRoute} from 'vue-router';
import { Skeleton } from 'ant-design-vue';
export default defineComponent({
name: 'App',
components: {Skeleton},
setup() {
let loading = ref(false);
const route = useRoute();
let showLayout = ref(true);
let event = ref(null);
let type = ref('');
provide('showLayout', showLayout);
provide('event', event);
let eventData = ref({});
const onEventData = () => { //动态改变值
return event.value
};
provide('onEventData', onEventData);
const onHandle = function(e){
loading.value = false;
event.value = e;
const data = e?.data;
if (data && data.type ) {
eventData.value = data;
type.value = data.type;
if(data.type === 'style') {
showLayout.value = data?.showLayout;
}
}
}
window.addEventListener('message', onHandle );
onBeforeUnmount(()=>{
window.removeEventListener('message', onHandle)
})
return {
locale: zhCN,
route,
showLayout, event,
type,
eventData,
loading
};
},
unmounted(){
}
});
</script>
<style>
#app {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,50 @@
/***
*配置接口 格式 module:Array<resource>
*/
export const apiModule = {
pension: [
'User',
'CurrentUser',
'Gaffer',
// 'Organizational',
// 'Device',
// 'Region',
// 'Owner',
// 'AllList',
// 'VisitorRecord',
// 'Room',
// 'Device',
// 'CommunityVisitor',
// 'AccessControlGroup',
// 'AccessControlLeft',
// 'AccessControlRight',
// 'AccessControlRightIdentity',
// 'Visitor',
// 'DeviceListByDeviceCategory',
// 'Perimeter',
// 'AccessControlRightVisitorIdentity',
// 'BedChoose',
// 'Gateway',
// 'FileUpload',
// 'User',
// 'CurrentUser',
// 'Organizational',
// 'Device',
// 'Region',
// 'Owner',
// 'AllList',
// 'VisitorRecord',
// 'Room',
// 'Device',
// 'CommunityVisitor',
// 'AccessControlGroup',
// 'AccessControlLeft',
// 'AccessControlRight',
// 'AccessControlRightIdentity',
// 'Visitor',
// 'DeviceListByDeviceCategory',
// 'BedChoose',
// 'Perimeter',
// 'OrganizationConfig',
],
};

View File

@@ -0,0 +1,15 @@
import {http} from '/nerv-lib/paas';
enum Api {
USER_LOGIN = '/api/passport/objs/login', //用户登录
USER_INFO = '/api/webui/webui/objs/PassportUserInfo', //获取用户信息
}
export const userLogin = (data: RoomListModel) => http.post(Api.USER_LOGIN, data);
export const userInfo = () => http.get(Api.USER_INFO);
/**
* @description 用户登录
* @property `[fatherRegionUuid]` 父级区域唯一标识
*/
interface RoomListModel {
data: string;
}

View File

@@ -0,0 +1,105 @@
<template>
<MdEditor v-bind="getBindValue" v-model="text" @change="change" />
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
export default defineComponent({
name: 'NsMarkDown',
components: { MdEditor },
props: {
value: {
type: String,
default: () => {
return '';
},
},
toolbars: {
type: Array,
default: () => {
return [
'bold',
'underline',
'italic',
'-',
'title',
'strikeThrough',
'sub',
'sup',
'quote',
'unorderedList',
'orderedList',
'-',
'codeRow',
'code',
'link',
// 'image',
'table',
'mermaid',
'katex',
'-',
'revoke',
'next',
'save',
'=',
'pageFullscreen',
'fullscreen',
'preview',
'htmlPreview',
'catalog',
// 'github',
];
},
},
// readonly: {
// type: Boolean,
// default: () => false,
// },
},
emits: ['change'],
setup(props, { attrs, emit }) {
const getBindValue = computed(() => ({
...attrs,
...props,
// model: model.value || 'test',
}));
const text = ref(props.value || '');
const change = (val) => {
console.log(val);
text.value = val;
};
// watchEffect(() => {
// dataSource.value = props.initData;
// });
return {
getBindValue,
text,
change,
};
},
watch: {
text: {
handler(val) {
this.$emit('change', val);
},
deep: true,
},
},
});
</script>
<style lang="less" scoped>
#md-editor-v3 {
width: 100% !important;
}
:deep(.ant-form-item-explain.ant-form-item-explain-error) {
display: flex;
min-width: 130px !important;
width: 140px !important;
}
</style>

View File

@@ -0,0 +1,7 @@
export const appConfig = {
projectType: 'web',
baseApi: '/api',
baseHeader: '/home',
baseRouter: '/home/index',
timeout: 15 * 1000
};

View File

@@ -0,0 +1,3 @@
import {appConfig} from '/@/config/app.config';
export {appConfig};

View File

@@ -0,0 +1,18 @@
/** @format */
import {createApp} from 'vue';
import App from '/@/App.vue';
import {paasInit} from '/nerv-lib/paas';
import {apiModule} from '/@/api';
import {appConfig} from '/@/config';
import '/@/theme/theme.scss';
const app = createApp(App);
app.config.isCustomElement = (tag) => tag === 'plastic-button';
paasInit({
app,
apiModule,
appConfig,
});
app.mount('#app');

View File

@@ -0,0 +1,35 @@
echo "=====================================================create====================================================="
#!/usr/bin/env bash
function create() {
if [ -d "$nervui_app_home" ];then
echo "$nervui_app_home exists!"
else
echo "start mkdir $nervui_app_home"
mkdir -p "$nervui_app_home"
fi
pkg_file_name=${pkg_url##*/}
pkg_file_path="$nervui_app_home$pkg_file_name"
echo "start download $pkg_url"
curl -L -o $pkg_file_path $pkg_url
echo "start install $pkg_file_path"
tar -xf $pkg_file_path -C $nervui_app_home
}
if [ "$pkg_url" == "" ]; then
echo {\"error\":\"pkg_url is empty\"}
exit 1
elif [ "$nervui_app_home" == "" ]; then
echo {\"error\":\"nervui_app_home is empty\"}
exit 1
else
create
fi

View File

@@ -0,0 +1 @@
echo "=====================================================delete====================================================="

View File

@@ -0,0 +1 @@
echo "=====================================================setup====================================================="

View File

@@ -0,0 +1 @@
echo "=====================================================start====================================================="

View File

@@ -0,0 +1 @@
echo "=====================================================stop====================================================="

View File

@@ -0,0 +1,30 @@
{
"name": "/nervui/nervui-resource-manage",
"operations": [
{
"name": "Create",
"type": "shell",
"implementor": "create.sh"
},
{
"name": "Delete",
"type": "shell",
"implementor": "delete.sh"
},
{
"name": "Setup",
"type": "shell",
"implementor": "setup.sh"
},
{
"name": "Start",
"type": "shell",
"implementor": "start.sh"
},
{
"name": "Stop",
"type": "shell",
"implementor": "stop.sh"
}
]
}

View File

@@ -0,0 +1,64 @@
{
"name": "/nervui/nervui-resource-manage",
"version": 1,
"inputs": [
{
"name": "server_ip",
"type": "string",
"required": true,
"description": "应用安装IP地址",
"inputType": "ipSelectType"
},
{
"name": "version",
"type": "string",
"required": true,
"description": "软件版本",
"inputType": "versionSelectType"
},
{
"name": "install_dir",
"type": "string",
"required": true,
"defaultValue": "/data",
"inputType": "textInputType",
"description": "安装目录"
}
],
"nodes": [
{
"name": "nervui-resource-manage",
"type": "/nerv/nerv-orchestrator/cluster/Nervui",
"parameters": [
{
"name": "file_repository",
"value": "${nerv_file_repository}"
},
{
"name": "install_dir",
"value": "${install_dir}"
},
{
"name": "pkg_url",
"value": "/api/pkg/nerv/nervui-resource-manage/${version}/nervui-resource-manage-${version}.tgz"
}
],
"dependencies": [
{
"type": "contained",
"target": "host"
}
]
},
{
"name": "host",
"type": "/nerv/nerv-orchestrator/compute/Host",
"parameters": [
{
"name": "address",
"value": "${server_ip}"
}
]
}
]
}

View File

@@ -0,0 +1,85 @@
const SideNav = () => import('/nerv-lib/paas/view/service/side-nav.vue');
const Base = () => import('/nerv-lib/paas/view/system/layout/content.vue');
const ListTable = () => import('/nerv-lib/paas/view/service/list-table.vue');
const Detail = () => import('/nerv-lib/paas/view/service/detail.vue');
const treeAndTableList = () => import('/view/tree-and-table-list.vue');
const resourceTypeList = () => import('/view/resource-type-list.vue');
const resourceTypeDetailForm = () => import('/view/resource-type-detail.vue');
const mockAddForm = () => import('/view/mock-add.vue');
export const APP = 'nerv-rm-server'
const rmRoute = {
path: '/rm',
name: 'resourceManage',
meta: {
sideMenus: {
title: '资源管理',
name: 'resourceManage',
root: true,
menus: [
{
name: 'resource',
label: '资源管理',
url: 'resourceManage',
module: '',
}
],
},
},
children: [
/****** 资源管理 start *********** */
{
path: 'resourceManage',
name: 'resourceModule',
redirect: '/rm/resourceManage/list',
component: Base,
children: [
// 资源管理-列表
{
path: 'list',
name: 'resource',
component: treeAndTableList,
props: {
enableTableSession: true,
refreshTime: 10,
}
},
// 资源管理-添加
{
name: 'resourceAdd',
path: 'add',
component: resourceTypeList,
props: {
title: '选择资源类型',
enableTableSession: true,
api: `/api/${APP}/objs/ManageResourceTypes`,
formConfig: {
schemas: [],
showAction: false,
},
rowSelection: null,
resultField: 'data',
columns: [],
columnActions: {},
headerActions: [],
rowKey: 'ID',
},
},
{
name: 'resourceDetail',
path: ':id/detail',
component: resourceTypeDetailForm,
},
{
name: 'resourceMock',
path: 'mock',
component: mockAddForm,
},
]
}
/****** 资源管理 end *********** */
]
};
export default rmRoute;

View File

@@ -0,0 +1,9 @@
const RootRoute = {
path: '/root',
name: 'root',
redirect: '/rm/resourceManage',
meta: {
title: 'Root',
},
};
export default RootRoute;

View File

@@ -0,0 +1,3 @@
.application .contentMenu .content {
background-color: #fff;
}

View File

@@ -0,0 +1,2 @@
@import "variable";
@import "global";

View File

@@ -0,0 +1,48 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"lib": ["esnext", "dom"],
"module": "esnext",
"moduleResolution": "node",
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"strictFunctionTypes": false,
"target": "esnext",
"types": ["vite/client"],
"typeRoots": [
"../node_modules/@types",
"../node_modules/@vue",
"../type"
],
"paths": {
"/@/*": [
"src/*"
],
"/nerv-lib/*": [
"../lib/*"
],
"/nerv-base/*": [
"../lib/paas/*"
]
}
},
"include": [
"src/**/*",
"type/**/*",
"mock/**/*",
"vite.config.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.js"
]
}

View File

@@ -0,0 +1,739 @@
<!-- @format -->
<template>
<div class="ns-table">
<a-spin :spinning="tableState.loading">
<!-- {{ formConfig }} -->
<div class="ns-table-search" v-if="!isEmpty(formConfig)">
<ns-form
ref="formElRef"
class="ns-table-form"
:showAction="true"
v-bind="formConfig"
formLayout="flex"
:expand="expand"
:showExpand="showExpand"
:model="formModel"
@finish="formFinish" />
</div>
<a-row type="flex" class="ns-table-main">
<a-col :flex="getTreeWidth" v-if="!isEmpty(treeConfig)">
<ns-tree v-if="getTreeData.length" v-bind="getTreeBindValue" @select="treeSelect" />
</a-col>
<a-col flex="auto">
<ns-table-header
v-if="!isEmpty(headerActions) || tableTitle"
:headerActions="headerActions"
:searchData="formModel"
:tableTitle="tableTitle"
:data="tableState.selectedRows">
<template #header="data">
<slot name="header" v-bind="data || {}"></slot>
</template>
</ns-table-header>
<ns-basic-table ref="tableElRef" v-bind="getTableBindValues" :dataSource="tableData">
<template #emptyText>
<template v-if="tableState.loadError">
<div class="ns-table-content">
<div class="fetch-error">
<p>{{ tableState.loadErrorMessage }}</p>
<ns-button type="primary" ghost @click="reload">重新加载</ns-button></div
></div
>
</template>
<template v-else-if="tableState.loading"
><div class="ns-table-content"></div
></template>
<template v-else>
<div class="ns-table-content"> <a-empty /> </div>
</template>
</template>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
<template v-if="item === 'bodyCell'">
<template v-if="data?.column?.textEllipsis">
<span class="tool-tips" :style="{ width: `${data.column.width}px` }">
<ns-tooltip
placement="top"
v-if="
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex]
">
<template #title>
<span>{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</template>
<span class="text-ellipsis">{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</ns-tooltip>
<span class="text-ellipsis" v-else> - </span>
</span>
</template>
<!-- 操作 -->
<template v-if="data.column.dataIndex === 'tableAction'">
<ns-table-action
:data="data.record"
:searchData="formModel"
:columnActions="getColumnActions" />
</template>
<template v-if="data.column.edit">
<ns-table-cell
:value="data.text"
:record="data.record"
:column="data.column"
:index="data.index" />
</template>
</template>
<template v-if="item === 'footer'">
<ns-table-footer :footerActions="footerActions" :data="ediRowData" />
</template>
</template>
<template #bodyCell="data" v-if="!Object.keys($slots).includes('bodyCell')">
<template v-if="data.column.textEllipsis">
<span class="tool-tips" :style="{ width: `${data.column.width}px` }">
<ns-tooltip
placement="top"
v-if="
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex]
">
<template #title>
<span>{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</template>
<span class="text-ellipsis">{{
data.column.customRender
? data.column.customRender(data)
: data.record[data.column.dataIndex] || '-'
}}</span>
</ns-tooltip>
<span class="text-ellipsis" v-else> - </span>
</span>
</template>
<template v-if="data.column.dataIndex === 'tableAction'">
<ns-table-action
:data="data.record"
:searchData="formModel"
:columnActions="getColumnActions" />
</template>
<template v-if="data.column.edit">
<ns-table-cell
:value="data.text"
:record="data.record"
:column="data.column"
:index="data.index" />
</template>
</template>
<template
#footer
v-if="!Object.keys($slots).includes('footer') && !isEmpty(footerActions)">
<ns-table-footer :footerActions="footerActions" :data="ediRowData" />
</template>
</ns-basic-table>
</a-col>
</a-row>
</a-spin>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, provide, reactive, ref, unref, watch } from 'vue';
import { RequestParams } from '/nerv-lib/component/table/table';
import {
cloneDeep,
debounce,
get,
isArray,
isEmpty,
isEqual,
isFunction,
isObject,
isString,
isUndefined,
} from 'lodash-es';
import { useParams } from '/nerv-lib/use/use-params';
import { transformColumns } from '/nerv-lib/component/table/table-columns';
import NsBasicTable from '/nerv-lib/component/table/basic-table.vue';
import { tableProps } from '/nerv-lib/component/table/props';
import { AxiosRequestConfig } from 'axios';
import { useApi } from '/nerv-lib/use/use-api';
import { useRoute } from 'vue-router';
import { useTableEdit } from '/nerv-lib/component/table/use-table-edit';
import { Form } from 'ant-design-vue';
import { stringUtil } from '/nerv-lib/util/string-util';
import { useTableRefresh } from '/nerv-lib/component/table/use-table-refresh';
import { tableConfig } from '/nerv-base/config/table.config';
import { useTableSession } from '/nerv-lib/component/table/use-table-session';
import { useTableColumn } from '/nerv-lib/component/table/use-table-column';
import NsTableAction from '/nerv-lib/component/table/table-action.vue';
import NsTableHeader from '/nerv-lib/component/table/table-header.vue';
import NsTableFooter from '/nerv-lib/component/table/table-footer.vue';
import NsTableCell from '/nerv-lib/component/table/edit/table-cell.vue';
export default defineComponent({
name: 'NsCustomListTable',
components: {
NsBasicTable,
NsTableAction,
NsTableHeader,
NsTableFooter,
NsTableCell,
},
props: tableProps,
emits: ['cellChange', 'update:value', 'dataSourceChange', 'update:dataSource'],
setup(props, { attrs, emit }) {
const tableElRef = ref(null);
const formElRef = ref(null);
const dataRef = ref([]);
const treeParamsRef = ref({});
const formParamsRef = ref({});
const orderRef = ref({});
const formModel = reactive<Recordable>({});
const tableData = ref<Recordable[]>([]);
const tableState = reactive({
selectedRowKeys: [],
selectedRows: [],
loading: false,
loadError: false,
loadErrorMessage: '',
});
const route = useRoute();
const { getColumnActionWidth } = useTableColumn({
columnActions: Object.assign({}, tableConfig.columnActions, props.columnActions),
});
const defaultPageRef = ref(1 - props.pageFieldOffset);
const { setTableSession } = useTableSession(formModel, formParamsRef, defaultPageRef);
const { delayFetch } = useTableRefresh({ props, reload });
watch(
[() => props.value, () => props.dataSource],
() => {
tableData.value = props.value || props.dataSource || [];
},
{
immediate: true,
},
);
const formItemContext = Form.useInjectFormItemContext();
const getColumnActions = computed(() => {
const { actions } = props.columnActions as any;
const _tableConfig = cloneDeep(tableConfig);
if (actions) {
_tableConfig.columnActions.width = getColumnActionWidth(actions);
}
return Object.assign(_tableConfig.columnActions, props.columnActions);
});
const getColumns = computed(() => {
const columns = transformColumns(cloneDeep(props.columns || []));
const { title, width, dataIndex, fixed } = getColumnActions.value;
if (props.columnActions) {
columns.push({
title,
width,
dataIndex,
fixed,
});
}
return columns;
});
watch(
() => tableData.value,
(val) => {
// console.log(val, tableData.value);
if (isEqual(val, tableData.value)) return;
const data = cloneDeep(tableData.value);
if (props.editable) {
Object.keys(data).forEach((key) => {
delete data[key][props.rowKey];
});
}
// emit('update:value', data);
emit('dataSourceChange', data);
formItemContext.onFieldChange();
},
{
deep: true,
},
);
const tableEdit = useTableEdit({
dataSource: tableData,
columns: getColumns,
rowKey: props.rowKey,
editable: ref(props.editable),
});
provide('tableEdit', tableEdit);
const { getParams } = useParams();
const rowSelection = computed(() => {
const { rowSelection } = props;
if (rowSelection === false || rowSelection === null) {
return null;
}
return Object.assign(
{
fixed: true,
columnWidth: 48,
preserveSelectedRowKeys: true, // 跨页选中默认不清除选中key
selectedRowKeys: tableState.selectedRowKeys,
onChange: (selectedRowKeys: never[], selectedRows: never[]) => {
tableState.selectedRowKeys = selectedRowKeys;
tableState.selectedRows = selectedRows;
},
},
rowSelection,
);
});
const customizeRenderEmpty = computed(() => {
return () => '暂无数据';
});
const formFinish = debounce((data: object) => {
formParamsRef.value = data;
fetch({
page: 1,
});
}, 300);
function setLoading(loading: boolean) {
tableState.loading = loading;
}
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,
rowSelection: rowSelection.value,
params: dynamicParams
? getParams({ ...route.params, ...route.query }, dynamicParams, params)
: params || {},
columns: getColumns.value,
pagination: getPagination.value,
onChange: tableChangeEvent,
};
});
watch(
() => getTableBindValues.value.api,
() => {
fetch();
},
{
immediate: true,
},
);
/**
* 请求函数
* @param requestParams 主要是传入页面,部分变量闭包处理
* @param clearDelay 是否需要清除刷新时间(页面操作之后,自动刷新重新计算)
*/
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,
};
if (!checkrequiredParams(httpParams)) {
console.log('check fail');
return;
}
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) => {
console.log(res)
res = {
"page": 1, // 页码
"pageSize": 20, // 条目数
"pageCount": 3,
"totalCount": 3, // 满足搜索条件的总数
"data": [
{
"alias": "111", // 资源实例别名
"RM_UUID": "xxxx", // 资源实例的RM_UUID
"typeAlias": "xxxx", // 资源类型别名
"typeCode": "xxxx", // 资源类型代码
"projectName": "xxx", // 所属项目名称
"projectID": 1, // 所属项目id
"status": "running", // 状态
"actions": [
{
"name": "edit",
"label": "编辑",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
},
{
"name": "remove",
"label": "卸载",
"mode": "api",
"pageAddr": "",
"actionApi": "/api/objs/RA/Volume/Detach",
"authRequired": false
},
{
"name": "extend",
"label": "磁盘扩容",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
}
]
},
{
"alias": "222", // 资源实例别名
"RM_UUID": "xxxx", // 资源实例的RM_UUID
"typeAlias": "xxxx", // 资源类型别名
"typeCode": "xxxx", // 资源类型代码
"projectName": "xxx", // 所属项目名称
"projectID": 1, // 所属项目id
"status": "running", // 状态
"actions": [
{
"name": "edit",
"label": "编辑",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
},
{
"name": "remove",
"label": "卸载",
"mode": "api",
"pageAddr": "",
"actionApi": "/api/objs/RA/Volume/Detach",
"authRequired": false
},
{
"name": "extend",
"label": "磁盘扩容",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
}
]
},
{
"alias": "333", // 资源实例别名
"RM_UUID": "xxxx",
"typeAlias": "xxxx", // 资源类型别名
"typeCode": "xxxx", // 资源类型代码
"projectName": "xxx", // 所属项目名称
"projectID": 1, // 所属项目id
"status": "running", // 状态
"actions": [
{
"name": "edit",
"label": "编辑",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
},
{
"name": "remove",
"label": "卸载",
"mode": "api",
"pageAddr": "",
"actionApi": "/api/objs/RA/Volume/Detach",
"authRequired": false
},
{
"name": "extend",
"label": "磁盘扩容",
"mode": "page",
"pageAddr": "前端提供",
"actionApi": "",
"authRequired": false
}
]
}
]
}
tableState.loadError = false;
tableState.loadErrorMessage = '';
dataRef.value = res;
tableData.value = get(unref(dataRef), props.listField);
emit('update:dataSource', tableData.value);
clearDelay && setLoading(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);
});
}
}
/**
* 检测requiredParams是否全部获得数据
* @param params
*/
function checkrequiredParams(params: Recordable) {
const { params: dynamicParams } = getTableBindValues.value as any;
let { requiredParams } = props;
if (requiredParams) {
if (requiredParams === true) requiredParams = dynamicParams as any;
if (isFunction(requiredParams)) {
console.error(
'Property dynamicParams of props cannot set to Function when using requiredParams',
);
return false;
} else {
if (isString(requiredParams)) {
if (isUndefined(params[requiredParams])) return false;
} else if (isArray(requiredParams)) {
for (let i = 0, l = requiredParams.length; i < l; i++) {
if (isUndefined(params[requiredParams[i]])) return false;
}
} else if (isObject(requiredParams)) {
const keys = Object.keys(requiredParams);
for (let i = 0, l = keys.length; i < l; i++) {
if (isUndefined(params[keys[i]])) return false;
}
}
}
return true;
}
return true;
}
function treeSelect(
selectedKeys: never[],
e: {
selected: boolean;
selectedNodes: { props: { dataRef: any } }[];
node: any;
event: any;
},
) {
console.log(selectedKeys, e);
const { dataRef } = e.selectedNodes[0].props;
treeParamsRef.value = getParams(dataRef, props.params);
fetch({
page: 1,
});
}
const getTreeData = computed(() => {
return props?.treeConfig?.treeData || [];
});
const getTreeWidth = computed(() => {
return props?.treeConfig?.width || '300px';
});
const getTreeBindValue = computed(() => ({
...props?.treeConfig,
}));
//todo 异步加载|| 树形接口
function reload(clearDelay = true) {
const pagination = unref(getPagination);
fetch(
{
page: pagination === false ? 1 : pagination.current,
},
clearDelay,
);
}
provide('reload', reload); //提供刷新功能
return {
reload,
formElRef,
tableElRef,
getColumnActions,
getTableBindValues,
formModel,
tableState,
isEmpty,
formFinish,
tableChangeEvent,
treeSelect,
getTreeBindValue,
getTreeWidth,
getTreeData,
customizeRenderEmpty,
tableData,
treeParamsRef
};
},
});
</script>
<style lang="less" scoped>
:deep(.ant-spin-nested-loading > div > .ant-spin) {
max-height: none;
}
.ns-table-search {
padding-top: 24px;
}
.ns-table {
position: relative;
min-height: 400px;
.ant-spin-nested-loading {
height: 100%;
min-height: 400px;
}
.ns-table-content {
min-height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.fetch-error {
p {
line-height: 40px;
padding: 0;
margin: 0;
font-size: 16px;
}
.ant-btn {
width: 88px;
}
}
}
.text-ellipsis {
display: inline-block;
vertical-align: top;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.tool-tips {
display: inline-block;
vertical-align: top;
padding: 0;
word-wrap: break-word;
word-break: break-word;
width: 100%;
}
</style>

View File

@@ -0,0 +1,218 @@
<!-- @format -->
<template>
<div>
<page-title :title="editId ? '编辑磁盘' : '创建磁盘'" />
<a-page-header>
<template #extra>
<ns-button @click="navigateBack()">返回</ns-button>
<ns-button type="primary" @click="submit" :disabled="!mainRef?.validateResult">保存</ns-button>
</template>
</a-page-header>
<ns-form
style="margin-top: 30px; margin-left: 24px"
ref="mainRef"
formLayout="修改"
:schemas="formSchema"
:model="data"
v-bind="$attrs" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch, computed, inject } from 'vue';
import { authorizationService } from '/nerv-base/store/modules/authorization-service';
import { NsMessage } from '/nerv-lib/component/message';
import { http } from '/nerv-lib/util/http';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'AppAdd',
setup() {
const mainRef = ref();
const formLayout = 'ns-vertical-form';
const router = useRouter();
const data = ref({});
const event = inject('event');
const submitData = ref();
const navigateBack = (type?: string) => {
const msg = {
type: type || 'back'
}
event.value.source.postMessage(msg, event.value.origin);
};
const onEventData = inject('onEventData')
const eventData = computed(function () {
return onEventData();
})
const params = router.currentRoute.value.params;
if (params.id) {
http.get(`/api/iaas/iaas/objs/RA/Volume/${params.id}`).then((res) => {
initRes.value = res;
data.value.ID = res.ID;
data.value.description = res.description;
data.value.name = res.name;
data.value.projectName = res.projectName;
});
}
const initRes = ref({});
const errorItem = ref({});
function submit() {
mainRef.value
.triggerSubmit()
.then((data) => {
submitData.value = data
let objData = {
alias: data['name'],
projectID: data['projectID'],
projectName: data['projectName'],
}
// saveData(submitData.value)
event.value.source.postMessage({type:'getId', data: objData}, event.value.origin);
})
.catch(() => ({}));
}
function saveData(data){
initRes.value.alias = data.name;
initRes.value.RM_UUID = data.RM_UUID;
initRes.value.projectID = data.projectID;
initRes.value.projectName = data.projectName;
http.post('/api/nerv-mock-ra/objs/Resources/Create', initRes.value).then(
(res) => {
NsMessage.success('操作成功', 1, () => {
navigateBack('successBack')
});
},
(error) => {
if (error?.response?.data?.fieldErrors) {
errorItem.value = error?.response?.data?.fieldErrors;
mainRef.value.triggerSubmit(['name']);
}
// 删除RM_UUID
let objData = {
RM_UUID: data['RM_UUID'],
}
event.value.source.postMessage({type:'removeId', data: objData}, event.value.origin);
},
);
}
let formSchema = ref([
{
label: '项目',
field: 'projectID',
addModel: {
projectName: 'projectname',
},
ifShow: params.id ? false : true,
viewOnly: params.id ? true : false,
component: 'NsSelectApi',
componentProps: {
api: {
url: '/api/passport/passport/objs/Authorization/CheckAuthorization',
method: 'POST',
},
autoSelectFirst: params.id ? false : true,
showSearch: true,
filterOption: (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
filterData: (item) => {
return authorizationService().checkPermission(
'vdisk',
'Volume',
'add',
item.projectname,
);
},
resultField: 'projects',
labelField: 'projectname',
valueField: 'projectid',
immediate: true,
placeholder: '请选择项目',
},
rules: params.id
? []
: [
{
type: 'number',
required: true,
message: '项目必填',
trigger: 'blur',
},
],
},
{
field: 'name',
label: '磁盘名称',
component: 'NsInput',
componentProps: {
placeholder: '请输入',
},
rules: [
{
required: true,
message: '磁盘名称必填',
trigger: 'blur',
},
{
trigger: 'blur',
validator: async (rule, value) => {
if (Object.keys(errorItem.value).indexOf('name') !== -1) {
const errorInfo = errorItem.value['name'];
delete errorItem.value['name'];
return Promise.reject(errorInfo);
}
},
},
{
pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]{5,31}$',
message:
'只能包含字母,数字,短横线(-)和下划线(_)且必须由大写字母、小写字母或数字开头。长度在6-32字符之间。',
trigger: 'blur',
},
],
},
]);
return {
errorItem,
navigateBack,
editId: null,
data,
mainRef,
submit,
formSchema,
formLayout,
event,
submitData,
saveData,
eventData,
};
},
watch: {
eventData: function (eventData) {
const data = this.event?.data;
if (data.type === 'returnId' && data.data) {
this.submitData['RM_UUID'] = data.data['RM_UUID'];
this.saveData(this.submitData);
}
}
},
beforeCreate() {},
});
</script>
<style lang="less" scoped>
.ant-col {
flex: 0 0 120px !important;
}
.ant-page-header {
padding: 0 24px;
margin-bottom: 15px;
}
:deep(.ant-page-header-heading-extra) {
margin-right: auto !important;
margin-left: 0;
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<Skeleton
active
:loading="loading">
<div v-if="!show" >
<page-title :title="data?.alias"/>
<a-page-header class="ns-page-header">
<template #extra>
<ns-button @click="navigateBack">返回</ns-button>
<ns-button @click="onAdd" type="primary">创建</ns-button>
</template>
</a-page-header>
<div class="content-wrapper">
<NsMarkDown
:value="data?.detail"
:previewOnly="true"
:htmlPreview="true"/>
</div>
</div>
<div v-if="show" class="frame-wrapper" style="height: 100%">
<iframe :src="src" id="frame" height="100%" width="100%" ></iframe>
</div>
</Skeleton>
</template>
<script lang="ts">
import {defineComponent, nextTick, onUnmounted, ref} from 'vue';
import NsMarkDown from '/@/component/markdown.vue';
import {http} from "/nerv-lib/util";
import {useRoute, useRouter} from "vue-router";
import {useNavigate} from "/nerv-lib/use/use-navigate";
import {NsMessage} from "/nerv-lib/component";
import {Skeleton} from "ant-design-vue";
import {APP} from "/@/router/index.ts";
import Cookie from "js-cookie";
export default defineComponent({
name: 'ResourceTypeDetail',
props: {},
components: {NsMarkDown, Skeleton},
setup(props, {attrs}) {
let loading = ref(true);
let data = ref({});
const route = useRoute();
const router = useRouter();
const params = route.params;
const code = params?.id;
const { navigateBack } = useNavigate();
let src = ref('');
let show = ref(false);
let frameRef = ref(null);
let createPageAddr = '';
http.get(`/api/${APP}/objs/ManageResourceTypes/` + params.id).then(res => {
loading.value = false;
data.value = res;
createPageAddr = res['createPageAddr'];
src.value = createPageAddr + '?nervsid=' + Cookie.get('nervsid');
// src.value = 'http://100.68.2.97:4200/nervui-mock-ra/mr/instance/add?nervsid='+Cookie.get('nervsid');
// src.value = 'http://localhost:4200/nervui-mock-ra/mr/instance/add?nervsid='+Cookie.get('nervsid');
})
function onAdd() {
if (createPageAddr) {
let flag = !/^(((ht|f)tps?):\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/.test(createPageAddr);
if (flag) {
NsMessage.error('不合法地址');
} else {
show.value = true;
}
} else {
NsMessage.error('无对应资源页面');
return;
}
// loading.value = true;
nextTick(() => {
load();
});
}
function handleMsg(e) {
const frame = frameRef.value;
const data = e?.data;
if(data) {
if(data.type === 'back') {
window.removeEventListener('message', handleMsg);
show.value = false;
// router.push({name:'resourceDetail', path: '/rm/resourceManage/list/'+params.id + '/detail'});
}
if(data.type === 'successBack') {
window.removeEventListener('message', handleMsg);
router.push({name:'resource', path: '/rm/resourceManage/list'});
}
}
}
function load() {
frameRef.value = document.getElementById('frame');
const frame = frameRef.value;
const msg = {
type: 'style',
showLayout: false,
data: {
typeCode: params.id
}
}
if(frame) {
loading.value = false;
frame.onload = function (e) {
frame.contentWindow?.postMessage(msg, src.value );
}
window.addEventListener('message', handleMsg);
}
}
onUnmounted(()=>{
window.removeEventListener('message', handleMsg);
})
return {
data,
navigateBack,
code,
loading,
onAdd,
show,
src
};
},
methods: {
}
})
</script>
<style lang="less" scoped>
.ant-page-header {
padding: 0 24px;
margin-bottom: 15px;
}
:deep(.ant-page-header-heading-extra) {
margin-right: auto !important;
margin-left: 0;
}
.content-wrapper {
margin: 0 24px
}
#frame {
border: 0;
}
</style>

View File

@@ -0,0 +1,660 @@
<template>
<div class="origin-wrapper">
<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">
<div class="ns-table-header">
<a-button @click="navigateBack" type="secondary">返回</a-button>
</div>
<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>
</div>
<a-tree v-model:expandedKeys="expandedKeys" :tree-data="treeData" v-model:selectedKeys="selectedKeys"
@expand="onExpand" @select="onTreeSelect">
</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>
</div>
</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, useRouter} 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 {useNavigate} from "../../lib/use/use-navigate";
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 router = useRouter();
const formModel = reactive({});
// 搜索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, 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/`)
const { navigateBack } = useNavigate();
// 缓存
function initTableSession() {
const { fullPath } = route;
const tableSession = JSON.parse(sessionStorage[fullPath] || '{}');
if (!props.enableTableSession) return;
if (tableSession['formModel']) {
Object.assign(formModel, tableSession['formModel']);
let code = formModel['typeClassCode'];
if(code){
treeParamsRef.value['typeClassCode'] = code;
selectedKeys.value =[ code];
allSelected.value = false;
}
}
}
initTableSession();
// 页面标题
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( []);
// 树形结构添加
const typeData = ref({});
// 搜索
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[key] = treeParamsRef.value[key];
} else if(Object.keys(formModel).includes(key)) {
delete formModel[key];
}
}
function reload(clearDelay = true) {
const pagination = unref(getPagination);
fetch(
{
page: pagination === false ? 1 : pagination.current,
},
clearDelay,
);
}
return {
tableState,
getTitle,
treeData,
expandedKeys,
formModel,
formFinish,
tableData,
typeData,
formConfig: getFormConfig.value,
nsTableRef,
formElRef,
getParams,
onTreeSelect,
onChange: tableChangeEvent,
allSelected,
selectedKeys,
treeParamsRef,
loadingTable,
loadingTree,
imgSrc,
getPagination,
navigateBack
}
},
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/ManageResourceTypeClasses`).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;
if(Object.keys(this.formModel).includes('typeClassCode')) {
delete this.formModel['typeClassCode'];
}
this.formElRef.triggerSubmit();
},
},
});
</script>
<style lang="less" scoped>
.ns-table-main {
flex-wrap: nowrap;
}
:deep(.ns-table-header) {
min-width: unset;
margin-bottom: 16px;
.ant-btn {
margin-left: 0 !important;
}
}
.ns-table-wrapper {
margin: 16px 24px 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>

View File

@@ -0,0 +1,380 @@
<template>
<div claas="origin-wrapper" v-if="!show">
<page-title title="资源管理"/>
<div class="ns-table-wrapper">
<div class="ns-table">
<a-row type="flex" class="ns-table-main">
<a-col flex="180px">
<ns-table-header
:headerActions="headerActions"
:tableTitle="tableTitle">
<template #header="data">
<slot name="header" v-bind="data || {}"></slot>
</template>
</ns-table-header>
<div class="type-tree-wrapper">
<div class="tree-top-header">
<div class="tree-node-content-wrapper" :class="{'selected': allSelected}" @click="onAllSelect">全部资源</div>
</div>
<a-tree v-model:expandedKeys="expandedKeys" :tree-data="treeData" v-model:selectedKeys="selectedKeys"
@expand="onExpand" @select="onTreeSelect">
</a-tree>
</div>
</a-col>
<a-col flex="auto">
<!-- {{ formConfig }} -->
<ns-view-list-table v-bind="getTableBindValues" :model="data" rowKey="code" ref="tableRef">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'tableAction'">
<ns-table-action
:data="record"
:searchData="{}"
:columnActions="record && getColumnActions(record)" />
</template>
</template>
</ns-view-list-table>
</a-col>
</a-row>
</div>
</div>
</div>
<div v-if="show" class="frame-wrapper" style="height: 100%">
<iframe :src="src" id="frame" height="100%" width="100%" ></iframe>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, nextTick, ref, unref} from "vue";
import {Skeleton} from "ant-design-vue";
import {tableProps} from "../../lib/component/table/props";
import {http} from "../../lib/util";
import {PlusCircleOutlined, EditOutlined, DeleteOutlined} from "@ant-design/icons-vue";
import {useParams} from "../../lib/use";
import {RequestParams} from "../../lib/component/table/table";
import { isArray} from "lodash-es";
import {NsModal} from "../../lib/component";
import {APP} from "/@/router/index.ts";
import NsCustomListTable from './custom-list-table.vue'
import NsTableAction from '/nerv-lib/component/table/table-action.vue';
import { useRouter } from "vue-router";
import Cookie from "js-cookie";
export default defineComponent({
name: 'TreeAndTableList',
components: {
Skeleton,
PlusCircleOutlined,
EditOutlined,
DeleteOutlined,
NsCustomListTable,
'ns-table-action': NsTableAction
},
props: {
...tableProps,
title: String,
tableTitle: Function,
showTitle: {
type: Boolean,
default: true,
},
resultField: String
},
setup: (props, {attrs, emit}) => {
let allSelected = ref(true);
const expandedKeys = ref(['1', '2'])
const { getParams } = useParams();
const treeParamsRef = ref({});
const selectedKeys = ref([]);
const tableRef = ref();
// let loadingTable = ref<boolean>(false);
let loadingTree = ref<boolean>(false);
const router = useRouter();
let show = ref(false);
let src = ref('');
let frameRef = ref(null);
const headerActions = [
{
label: '创建资源',
name: 'resourceAdd',
type: 'primary',
route: 'add',
}
]
const handleAction = (action, record, reload)=>{
const mode = action?.mode;
if(mode === 'api') {
NsModal.confirm({
title: '警告',
content: '确定执行该操作吗',
okText: '确认',
cancelText: '取消',
onOk: function () {
http.post(`/api/${APP}/objs/Resources/Operator`, {
RM_UUID: record.RM_UUID,
typeCode: record.typeCode,
actionApi: action['actionApi']
}).then(res=>{
reload();
})
},
});
}
if(mode === 'page') {
// action['pageAddr'] = 'http://100.68.2.97:4200/nervui-mock-ra/mr/instance/manage';
const template = action['pageAddr'];
if(template.indexOf('$') !== -1) { // 模版参数值替换
action['pageAddr'] = template.replace(/\$(\w+)\b/g, function (match, key) {
return record[key.trim()] ? record[key.trim()] : match;
});
}
src.value = `${action['pageAddr']}?nervsid=${Cookie.get('nervsid')}`;
show.value = true;
nextTick(() => {
load(record, action['name']);
})
}
}
function handleMsg(e) {
const frame = frameRef.value;
const data = e?.data;
if(data) {
if(data.type === 'back') {
window.removeEventListener('message', handleMsg);
show.value = false;
}
if(data.type === 'successBack') {
window.removeEventListener('message', handleMsg);
router.push({name:'resource', path: '/rm/resourceManage/list'});
}
}
}
function load(record, actionName) {
frameRef.value = document.getElementById('frame');
console.log(frameRef.value)
const frame = frameRef.value;
let msg = {
type: 'style',
mode: 'page',
routerPath: actionName,
showLayout: false,
data: { pageParams: {}}
}
if(['manage', 'edit'].includes(actionName)) {
msg['data'] = {
pageParams: {
ID: record['RM_UUID'],
pageTitle: record['alias']
}
}
}
if(frame) {
frame.onload = function (e) {
frame.contentWindow?.postMessage(msg, src.value );
}
window.addEventListener('message', handleMsg);
}
}
const getColumnActions =(record)=>{
if(isArray(record?.actions)) {
record?.actions.forEach(action => {
action['openPermission'] = true;
action['handle'] = (record, name, {reload, action}) =>{
handleAction(action, record, reload );
}
})
} else {
record['actions'] = [];
}
return record;
}
const tableConfig = ref(
{
// title: '资源管理',
showTitle: false,
api: `/api/${APP}/objs/Resources`,
formConfig: {},
rowSelection: null,
resultField: 'data',
columns: [
{
title: '名称',
dataIndex: 'alias',
},
{
title: '类型',
dataIndex: 'typeAlias',
},
{
title: '项目',
dataIndex: 'projectName',
},
{
title: '状态',
dataIndex: 'status',
transform: {
pipe: 'state',
statusField: (record: object) => {
return record.status;
},
// textField: (record: object) => {
// return record.statusInCN;
// },
messageField: 'reason',
},
},
],
columnActions: {
title: '操作',
width: 100,
actions: [],
},
rowKey: 'id'
}
)
// 右侧树形结构
const treeData = ref([])
const data = []
// 树形结构添加
let typeSchema = ref([]);
const typeData = ref({});
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'});
console.log(treeParamsRef.value)
unref(tableRef).nsTableRef.treeParamsRef = {
...treeParamsRef.value
}
unref(tableRef).nsTableRef.formElRef.triggerSubmit();
}
const getTableBindValues = computed(() => {
const { params, dynamicParams } = props;
return {
...props,
...tableConfig.value
};
});
return {
treeData,
expandedKeys,
typeSchema,
typeData,
onTreeSelect,
getTableBindValues,
headerActions,
data,
allSelected,
selectedKeys,
tableRef,
loadingTree,
getColumnActions,
show,
src
};
},
mounted() {
this.getDataTree();
},
methods: {
// 转换树结构数据
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/ManageResourceTypeClasses`).then(res => {
this.transferTree(res)
this.treeData = res;
this.loadingTree = false;
}).catch( ()=>{
this.loadingTree = false;
})
},
onAllSelect() {
this.allSelected = !this.allSelected;
this.selectedKeys = [];
this.tableRef.nsTableRef.treeParamsRef = {};
this.tableRef.nsTableRef.formElRef.triggerSubmit();
}
},
});
</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;
}
.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;
&.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
}
}
// iframe 样式
#frame {
border: 0;
}
</style>

View File

@@ -0,0 +1,10 @@
/** @format */
import configFun from '../build/vite-default.config';
const dirname = __dirname;
const proxy = {
'/api': { target: 'http://portal.cloud.sh.dingcloud.com:30080/', changeOrigin: true },
'/assets': { target: 'http://portal.cloud.sh.dingcloud.com:30080/', changeOrigin: true },
};
export default configFun({ dirname, serviceMode: 'paas', baseDir: '../', proxy });

View File

@@ -0,0 +1,3 @@
module.exports = {
runtimeCompiler: true,
};