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-repository
# spa shortname
VITE_GLOB_APP_SHORT_NAME = nervui-resource-repository

View File

@@ -0,0 +1,22 @@
# Whether to open mock
VITE_USE_MOCK = true
# public path
VITE_PUBLIC_PATH = /nervui-resource-repository/
# 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-repository/
# 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 rr-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,36 @@
[
{
"catalog": "计算",
"icon": "&#xe787;",
"name": "resourceRepo",
"label": "资源仓库",
"menuSort": [
"resourceType"
],
"menus": [
{
"name": "resourceType",
"url": "/rr/resourceRepository",
"label": "资源仓库",
"operation": {
"resource": "resourceType",
"method": "list"
},
"submenus": [
{
"name": "resourceTypeClass",
"url": "/rr/resourceRepository",
"label": "资源类型分类",
"operation": {
"resource": "resourceTypeClass",
"method": "list"
},
"submenus": []
}
]
}
]
}
]

View File

@@ -0,0 +1,11 @@
{
"release": [
{
"src": "nervui-resource-repository/release",
"dest": "/upload/pkg",
"include": [
"nervui-resource-repository-(\\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-repository-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-repository \
-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-repository
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-repository
version: 1.0.0
components:
- type: nervui-resource-repository
resources:
- {type: template, relativePath: /nervui-resource-repository/deploy.json}

View File

@@ -0,0 +1,27 @@
<template>
<ns-application/>
</template>
<script lang="ts" type="module">
import {defineComponent} from 'vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import {useRoute} from 'vue-router';
export default defineComponent({
name: 'App',
setup() {
const route = useRoute();
return {
locale: zhCN,
route
};
}
});
</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,106 @@
<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) => {
text.value = val;
};
return {
getBindValue,
text,
change,
};
},
watch: {
text: {
handler(val) {
this.$emit('change', val);
},
deep: true,
},
value: {
handler(val) {
this.text = 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,531 @@
<template>
<div>
<div class="container clearfix">
<!-- 图片显示框 -->
<template v-if="currentImg.length">
<div class="imgList" v-for="(item, index) in fileList" :key="item">
<div class="imgContainer" @mouseenter="mouseEnter(index)" @mouseleave="mouseLeave(index)">
<img :src="currentImg[index]" class="imgCover" />
<span v-if="maskShow[index]" class="mask">
<EyeOutlined v-if="false" @click="handlePreview(item, index)" />
<DeleteOutlined @click="deleteImg(index)" />
</span>
</div>
</div>
</template>
<!-- 图片显示框 /-->
<!-- 上传图片框/内置了input -->
<a-upload
v-if="count === 1 ? 'true' : fileList.length < count"
list-type="picture-card"
:before-upload="beforeUpload"
@change="handleChange"
:multiple="count != 1"
:customRequest="selfUpload"
:showUploadList="false">
<!-- :disabled="count == 1 && fileUuid ? true : false" -->
<div>
<template v-if="isLt5M && isJpgOrPngOrJpeg">
<UploadOutlined :style="{ fontSize: '14px' }" />
<div class="ant-upload-text">上传图片</div>
</template>
<template v-else>
<LoadingOutlined />
<div class="ant-upload-text">校验失败</div>
</template>
</div>
</a-upload>
<!-- 上传图片框/内置input /-->
</div>
<!-- 预览图片弹窗 -->
<ns-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img v-if="previewVisible" alt="example" style="width: 100%" :src="previewImage" />
</ns-modal>
<!-- 预览图片弹窗 /-->
<!-- 错误消息提示 -->
<div class="err-msg" v-if="!isJpgOrPngOrJpeg">
<p>文件上传失败请选择{{ fileType.join(',') }}类型大小不超过{{maxSizeLabel}}的图片</p>
<!-- <p :style="{ color: 'red' }">请选择{{ fileType.join(',') }}的图片</p> -->
</div>
<div class="err-msg" v-if="isJpgOrPngOrJpeg ? !isLt5M : ''">
<p>{{ fileName }} 文件上传失败 请选择 {{ fileType.join(',') }}类型大小不超过{{maxSizeLabel}}的图片</p>
<!-- <p :style="{ color: 'red' }">请选择{{ maxSize / 1024 / 1024 }}M内的图片</p> -->
</div>
<!-- 错误消息提示 /-->
</div>
</template>
<script lang="ts">
import {
UploadOutlined,
LoadingOutlined,
EyeOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import { defineComponent, ref, computed, reactive, watch, toRaw } from 'vue';
import { http } from '/nerv-lib/util/http';
import { NsMessage } from '/nerv-lib/component/message';
import {basicSetup} from "codemirror";
interface FileItem {
uid: string;
type: string;
size: number;
name?: string;
status?: string;
response?: string;
percent?: number;
url?: string;
preview?: string;
originFileObj?: any;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
// 转base64
function getBase64(file: File) {
return new Promise((resolve, reject) => {
// console.log(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}
export default defineComponent({
name: 'NsUploadCustom',
components: {
UploadOutlined,
LoadingOutlined,
DeleteOutlined,
EyeOutlined,
},
props: {
value: {},
// 上传的地址
url: {
type: String,
// require: true,
},
// 上传的图片大小
maxSize: {
type: Number,
default: 5242880,
},
maxSizeLabel: {
type: String,
default: '5M',
},
// 上传的图片类型
fileType: {
type: Array,
default: () => {
return ['jpg', 'png', 'jpeg'];
},
},
// 展示图片数量
count: {
type: Number,
default: 1,
},
// 上传文件类型0-证书1-图片2-身份证件
uploadType: {
type: Number,
default: 1,
},
fileName: {
type: String,
default: 'file',
},
baseImageUrl: {
type: [String, Object],
},
compatibilityUuid: {
type: [String, Object],
},
// 图片归类所需
params: {
type: [Object],
},
// 是否两个都emit
whetherTwo: {
type: Boolean,
default: false,
},
saveType: {
type: String, // 'base64', 'formData'
},
// baseImageUrlParam: {
// // type: String
// }
},
emits: ['change'],
setup(props, { emit }) {
// console.log(props);
const previewVisible = ref<boolean>(false);
const isLt5M = ref<boolean>(true);
const isJpgOrPngOrJpeg = ref<boolean>(true);
const previewImage = ref<string | undefined>('');
const fileList = ref<FileItem[]>([]);
const fileUuid = ref<string>('');
const uuidList = ref([]);
const uuidString = ref('');
const currentImg = ref<string[]>([]);
const isIntImg = ref(false);
const filterValue = (value) => {
if (typeof value == 'object') {
let arr = [];
value.forEach((item) => {
if (item.includes('ParkPic/')) {
arr.push(item.slice(item.lastIndexOf('/') + 1));
} else {
arr.push(item);
}
});
return arr;
} else {
let str = '';
if (value) {
if (value.includes('ParkPic/')) {
str = value.slice(value.lastIndexOf('/') + 1);
} else {
str = value;
}
}
return str;
}
};
let baseImageUrl = ref();
baseImageUrl.value = props.baseImageUrl;
if (baseImageUrl.value) {
isIntImg.value = true;
if (typeof baseImageUrl.value == 'object') {
uuidList.value = props.compatibilityUuid;
// currentImg.value.concat(props.baseImageUrl);
baseImageUrl.value.map((item, index) => {
console.log(item);
fileList.value.push(item);
currentImg.value.push(item);
// previewImage.value = props.baseImageUrl[index];
});
// currentImg.value.unshift(...props.baseImageUrl);
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
} else {
fileList.value.push({});
uuidString.value = props.compatibilityUuid;
fileUuid.value = baseImageUrl.value;
currentImg.value.unshift(baseImageUrl.value);
previewImage.value = baseImageUrl.value;
if(props.saveType === 'base64') {
toDataUrl(baseImageUrl.value, function(myBase64) {
currentImg.value = [myBase64];
emit('change', currentImg.value[0]);
});
} else {
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
}
}
}
const fileName = ref<string | undefined>('');
const maskShow = ref<boolean[]>([]);
const acceptType = computed(() =>
props.fileType.map((item: String) => {
return 'image/' + item;
}),
);
watch(
() => baseImageUrl,
(e) => {
if (e) {
isIntImg.value = true;
if (typeof e.value == 'object') {
e.value.map((item, index) => {
fileList.value.push(item);
currentImg.value.push(item);
});
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
} else {
fileList.value.push({});
uuidString.value = props.compatibilityUuid;
fileUuid.value = baseImageUrl.value;
currentImg.value.unshift(baseImageUrl.value);
previewImage.value = baseImageUrl.value;
if(props.saveType === 'base64') {
http.get(baseImageUrl.value).then(res=>{
const transfer = async () => {
currentImg.value = [await getBase64(res)];
emit('change', currentImg.value[0]);
}
transfer();
})
} else {
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
}
}
}
},
{deep: true}
);
watch(
() => props.value,
(val) => {
if( typeof val === 'number') {
baseImageUrl.value = baseImageUrl.value+ JSON.stringify(val);
}
}
)
function toDataUrl(url, callback) {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
let reader = new FileReader();
reader.onloadend = function() {
callback(reader.result);
}
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
}
const beforeUpload = (file: FileItem) => {
// 上传出错后下次上传图片前重置为true让图片可以上传
isLt5M.value = true;
isJpgOrPngOrJpeg.value = true;
// 限制图片格式,服务器不支持gif图片
if (file.type === 'image/gif') {
NsMessage.warn('不支持gif图片');
return false;
}
isJpgOrPngOrJpeg.value = acceptType.value.includes(file.type);
// 如果大于指定的大小,显示错误信息
if (file.size > props.maxSize) {
isLt5M.value = false;
if(currentImg.value?.length>0) {
deleteImg(0);
}
}
fileName.value = file.name;
return isLt5M.value && isJpgOrPngOrJpeg.value;
};
const handleChange = ({ fileList: newFileList }: FileInfo) => {
// 单图上传
if (props.count === 1) {
// 删除图片时newFileList.length = 0
// 图片大小不符合规范时,!isLt5M.value为true
if (!isLt5M.value || !isJpgOrPngOrJpeg.value || newFileList.length - 1 < 0) {
// 让图片不显示,也不上传
fileList.value = [];
} else {
// 添加\更换图片
// newFileList[newFileList.length - 1]的目的是为了只显示最新一张图片
fileList.value = [newFileList[newFileList.length - 1]];
}
}
};
const selfUpload = async ({ file }) => {
if (props.count !== 1) {
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
currentImg.value.push(await getBase64(file));
} else {
currentImg.value = [await getBase64(file)];
if(props.saveType === 'base64') {
emit('change', currentImg.value[0]);
return;
}
}
const params = {
uploadType: props.uploadType,
};
const formData = new FormData();
formData.append(props.fileName, file);
// formData.append('uploadType', props.uploadType);
Object.keys(props.params).map((item) => {
formData.append(item, props.params[item]);
});
//formData.append('uploadType', 1);
const config = {
headers: {
'Content-Type': 'multipart/form-data',
},
// params: params,
};
http.post(props.url, formData, config).then((res) => {
emit('change', res.logoImage);
});
};
const handlePreview = async (item, index: number) => {
if (props.count > 1) {
previewImage.value = fileList.value[index];
} else {
previewImage.value = fileUuid.value;
}
previewVisible.value = true;
};
const deleteImg = (index: number) => {
currentImg.value.splice(index, 1);
fileList.value.splice(index, 1);
uuidList.value && uuidList.value.splice(index, 1);
uuidString.value = '';
fileUuid.value = '';
isIntImg.value = false;
if (props.count == 1) {
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
} else {
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
}
};
const handleCancel = () => {
previewVisible.value = false;
};
const mouseEnter = (index: number) => {
maskShow.value[index] = true;
};
const mouseLeave = (index: number) => {
maskShow.value[index] = false;
};
return {
previewVisible,
previewImage,
fileList,
isLt5M,
isJpgOrPngOrJpeg,
fileName,
currentImg,
maskShow,
fileUuid,
baseImageUrl,
selfUpload,
handleCancel,
handlePreview,
handleChange,
beforeUpload,
mouseEnter,
mouseLeave,
deleteImg,
};
},
});
</script>
<style lang="less" scoped>
:deep(.ant-upload-picture-card-wrapper) {
width: unset !important;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select-picture-card) {
margin: 0;
width: 64px !important;
height: 64px !important;
border: 1px solid #d9d9d9;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select-picture-card:hover) {
border-color: #d9d9d9;
}
:deep(.ant-upload-select-picture-card i) {
font-size: 32px;
color: #999;
}
:deep(.ant-upload-select-picture-card .ant-upload-text) {
color: #666;
font-size: 12px;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload-list-picture-card-container) {
width: 88px;
height: 64px !important;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload-list-picture-card .ant-upload-list-item) {
margin: 0;
padding: 0;
width: 64px !important;
height: 64px !important;
}
.title,
.err-msg {
text-align: left;
}
.err-msg p {
margin: 0;
}
.container {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.imgList {
display: flex;
}
.imgContainer {
margin-right: 16px;
border: 1px solid #d9d9d9;
position: relative;
margin-bottom: 8px;
}
.imgCover {
width: 64px;
height: 64px;
object-fit: contain;
}
.imgContainer .mask {
position: absolute;
top: 0;
left: 0;
width: 64px !important;
height: 64px !important;
background: rgba(0, 0, 0, 0.5);
text-align: center;
line-height: 64px !important;
}
.mask .anticon-eye {
color: white;
margin-right: 18px;
}
.mask .anticon-eye:hover {
color: #00acff;
margin-right: 18px;
}
.mask .anticon-delete {
color: white;
}
.mask .anticon-delete:hover {
color: #00acff;
}
</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-repository",
"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-repository",
"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-repository",
"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-repository/${version}/nervui-resource-repository-${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,360 @@
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 resourceTypeAddForm = () => import('../view/resource-type-add.vue');
const resourceTypeDetailForm = () => import('../view/resource-type-detail.vue');
const resourceTypeReleaseForm = () => import('../view/resource-type-release.vue');
const resourceTypeEditForm = () => import('../view/resource-type-edit.vue');
export const APP = 'nerv-rm-server'
const rrRoute = {
path: '/rr',
name: 'resourceRepo',
meta: {
sideMenus: {
title: '资源仓库',
name: 'resourceRepo',
root: true,
menus: [
{
name: 'resourceType',
label: '资源仓库',
url: 'resourceType',
module: '',
}
],
},
},
children: [
/****** 资源仓库 start *********** */
{
path: 'resourceRepository',
name: 'resourceTypeModule',
redirect: '/rr/resourceRepository/list',
component: Base,
children: [
// 资源仓库-列表
{
path: 'list',
name: 'resourceType',
component: treeAndTableList,
props: {
title: '资源仓库',
enableTableSession: true,
api: `/api/${APP}/objs/ResourceTypes`,
formConfig: {
schemas: [],
showAction: false,
},
rowSelection: null,
resultField: 'data',
columns: [],
columnActions: {
title: '操作',
width: 100,
actions: [],
},
headerActions: [
{
label: '注册资源类型',
name: 'resourceTypeAdd',
type: 'primary',
route: 'add',
}
],
rowKey: 'ID',
},
},
// 资源仓库-添加
{
name: 'resourceTypeAdd',
path: 'add',
component: resourceTypeAddForm,
props: {
title: '配置资源类型',
formLayout: 'ns-vertical-form',
api: `/api/${APP}/objs/ResourceTypes`,
schemas: [
{
label: '名称',
field: 'alias',
component: 'NsInput',
rules: [
{
required: true,
trigger: 'change',
validator: async (rule: any, value: any) => {
if (!value) {
return Promise.reject('名称不能为空');
}
if (!/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9-_\u4e00-\u9fa5]{0,63}$/.test(value)) {
return Promise.reject(
'只能包括中文、大小写字母、数字和短横线(-)、下划线(_)必须以字母、数字、或中文开头长度必须在164字节之间。',
);
}
}
}
]
},
{
label: '资源代码',
field: 'code',
component: 'NsInput',
componentProps: {},
rules: [
{
required: true,
message: '资源代码不能为空',
}
],
},
{
label: '类别',
field: 'typeClassCode',
component: 'NsSelectTreeApi',
componentProps: {
showCheckedStrategy: 'TreeSelect.SHOW_ALL',
selectType: 'tree',
api: `/api/${APP}/objs/ResourceTypeClasses`,
labelField: 'alias',
valueField: 'code',
resultField: '',
immediate: true,
placeholder: '请选择类型',
unSelectable: {
level: -1,
},
},
rules: [
{
required: true,
message: '请选择类型',
},
],
},
{
label: '介绍',
field: 'desc',
component: 'NsTextarea',
componentProps: {
showCount: true,
maxlength: 300,
},
rules: [
{
required: true,
message: '介绍内容不能为空',
},
]
},
{
field: 'logo',
component: 'NsUploadCustom',
label: 'icon',
componentProps: {
saveType: 'base64',
// 上传的图中片大小
maxSize: 15360,
maxSizeLabel: "15KB",
// 上传的图片类型
fileType: ['jpg', 'png', 'jpeg'],
// 展示图片数量
count: 1,
imagePreview: true
},
rules: [
{
required: true,
message: '请选择图片',
trigger: 'blur',
},
],
},
{
label: '详情',
field: 'detail',
component: 'NsMarkDown',
class: 'ns-form-item-label',
componentProps: {
// previewOnly: true
},
rules: [
{
required: true,
message: '详情不能为空',
trigger: 'blur',
},
],
}
]
}
},
{
name: 'resourceTypeDetail',
path: ':id/detail',
component: resourceTypeDetailForm,
},
{
name: 'resourceTypeEdit',
path: ':id/edit',
component: resourceTypeEditForm,
props: {
title: '编辑资源类型',
formLayout: 'ns-vertical-form',
getApi: `/api/${APP}/objs/ResourceTypes`,
api: `/api/${APP}/objs/ResourceTypes`,
schemas: [
{
label: '名称',
field: 'alias',
component: 'NsInput',
rules: [
{
required: true,
trigger: 'change',
validator: async (rule: any, value: any) => {
if (!value) {
return Promise.reject('名称不能为空');
}
if (!/^[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9-_\u4e00-\u9fa5]{0,63}$/.test(value)) {
return Promise.reject(
'只能包括中文、大小写字母、数字和短横线(-)、下划线(_)必须以字母、数字、或中文开头长度必须在164字节之间。',
);
}
}
}
]
},
{
label: '资源代码',
field: 'code',
component: 'NsInputText',
componentProps: {},
},
{
label: '类型',
field: 'typeClassCode',
component: 'NsSelectTreeApi',
componentProps: {
showCheckedStrategy: 'TreeSelect.SHOW_ALL',
selectType: 'tree',
api: `/api/${APP}/objs/ResourceTypeClasses`,
labelField: 'alias',
valueField: 'code',
resultField: '',
immediate: true,
placeholder: '请选择类型',
unSelectable: {
level: -1,
},
},
rules: [
{
required: true,
message: '请选择类型',
},
],
},
{
label: '介绍',
field: 'desc',
component: 'NsTextarea',
componentProps: {
showCount: true,
maxlength: 300,
},
rules: [
{
required: true,
message: '介绍内容不能为空',
},
]
},
{
field: 'logo',
component: 'NsUploadCustom',
label: 'icon',
componentProps: {
saveType: 'base64',
// 上传的图中片大小
maxSize: 15360,
maxSizeLabel: "15KB",
// 上传的图片类型
fileType: ['jpg', 'png', 'jpeg'],
// 展示图片数量
count: 1,
imagePreview: true,
baseImageUrl: '',
},
rules: [
{
required: true,
message: '请选择图片',
trigger: 'blur',
}
],
},
{
label: '详情',
field: 'detail',
component: 'NsMarkDown',
class: 'ns-form-item-label',
componentProps: {
// previewOnly: true
},
rules: [
{
required: true,
message: '详情不能为空',
trigger: 'blur',
},
]
}
],
}
},
{
name: 'resourceTypePutAway',
path: ':id/release',
component: resourceTypeReleaseForm,
props: {
title: '配置资源类型',
formLayout: 'ns-vertical-form',
api: `/api/${APP}/objs/ResourceTypes`,
schemas: [
{
label: '资源服务地址',
field: 'resourceProviderAddr',
component: 'NsInput',
componentProps: {
placeholder: '请输入域名或IP地址(http://dingcloud.com:30080)'
},
rules: [
{
required: true,
trigger: 'change',
validator: async (rule: any, value: any) => {
if (!value) {
return Promise.reject('资源服务地址不能为空');
}
if (!/^(((ht|f)tps?):\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/.test(value)) {
return Promise.reject(
'请输入正常的域名和IP地址',
);
}
}
}
]
}
]
}
}
]
}
/****** 资源管理 end *********** */
]
};
export default rrRoute;

View File

@@ -0,0 +1,9 @@
const RootRoute = {
path: '/root',
name: 'root',
redirect: '/rr/resourceRepository',
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,40 @@
<!-- @format -->
<template>
<NsViewAddForm v-bind="getBindValue"/>
</template>
<script lang="ts">
import { defineComponent, computed, provide } from 'vue';
import {formProps} from "/nerv-lib/component/form/form/props";
import {PropTypes} from "/nerv-lib/util";
import NsMarkDown from '../component/markdown.vue';
import NsUploadCustom from '../component/upload-custom.vue';
export default defineComponent({
name: 'ResourceTypeAdd',
props: {
...formProps,
api: PropTypes.string,
title: PropTypes.string,
},
components: { NsMarkDown, NsUploadCustom },
setup(props, {attrs}) {
provide('components', () => {
return {
NsMarkDown,
NsUploadCustom
};
});
const getBindValue = computed(() => ({
...attrs,
...props,
}));
return {
getBindValue,
};
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,129 @@
<template>
<Skeleton
active
:loading="loading">
<page-title :title="data?.alias"/>
<a-page-header class="ns-page-header">
<template #extra>
<ns-button @click="onBack">返回</ns-button>
<ns-button @click="onEdit" type="primary" v-if="authMap['edit']">编辑</ns-button>
<ns-button @click="onRelease" type="primary" v-if="!data?.shelfState && authMap['putAway']">上架</ns-button>
<ns-button @click="onUnRelease" type="primary" v-if="data?.shelfState && authMap['putAway']">下架</ns-button>
<ns-button @click="onRemove" type="primary" v-if="authMap['remove']">删除</ns-button>
</template>
</a-page-header>
<div class="content-wrapper">
<NsMarkDown
:value="data?.detail"
:previewOnly="true"
:htmlPreview="true"/>
</div>
</Skeleton>
</template>
<script lang="ts">
import { defineComponent, ref} from 'vue';
import NsMarkDown from '../component/markdown.vue';
import {http} from "/nerv-lib/util";
import {useRoute} from "vue-router";
import {useNavigate} from "/nerv-lib/use/use-navigate";
import { NsModal} from "/nerv-lib/component";
import {Skeleton} from "ant-design-vue";
import {APP} from "/@/router/index.ts";
import {authorizationService} from "/nerv-lib/paas";
export default defineComponent({
name: 'ResourceTypeDetail',
props: {},
components: {NsMarkDown, Skeleton},
setup(props, {attrs}) {
let loading = ref(true);
let data = ref({});
const route = useRoute();
const params = route.params;
const code = params?.id;
const { navigateBack } = useNavigate();
const unReleaseData = ref({
code: code,
shelfState: false
})
const removeData = ref({
code: code,
})
http.get(`/api/${APP}/objs/ResourceTypes/` + params.id).then(res => {
loading.value = false;
data.value = res;
})
//权限部分
let authMap = ref({});
const resourceTypeClassOp= ['edit', 'putAway', 'remove'];
resourceTypeClassOp.forEach( key => {
authMap.value[key]= authorizationService().checkPermission('resourceRepo', 'resourceType', key);
})
return {
data,
navigateBack,
code,
removeData,
unReleaseData,
loading,
authMap
};
},
methods: {
onEdit() {
this.$router.push('edit');
},
onRelease() {
this.$router.push('release');
},
onUnRelease() {
const thisObj = this;
NsModal.confirm({
title: '警告',
content: `确定要下架该资源类型吗?\n下架后不可恢复请谨慎操作`,
okText: '确认',
cancelText: '取消',
onOk() {
http.post(`/api/${APP}/objs/ResourceTypes/PutAway`, thisObj.unReleaseData).then(res=>{
// navigateBack()
thisObj.$router.push({name:'resourceType', path: '/rr/resourceRepository/list'});
})
},
});
},
onRemove() {
const thisObj = this;
NsModal.confirm({
title: '警告',
content: `确定要删除该资源类型吗?\n删除后不可恢复请谨慎操作`,
okText: '确认',
cancelText: '取消',
onOk() {
http.delete(`/api/${APP}/objs/ResourceTypes`, thisObj.removeData).then(res=>{
thisObj.$router.push({name:'resourceType', path: '/rr/resourceRepository/list'});
})
},
});
},
onBack() {
this.navigateBack();
}
}
})
</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
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div class="editResourceType">
<page-title title="编辑" />
<a-page-header>
<template #extra>
<ns-button @click="navigateBack">返回</ns-button>
<ns-button type="primary" :disabled="!mainRef?.validateResult" @click="submit">保存</ns-button>
</template>
</a-page-header>
<Skeleton
active
:loading="loading">
<ns-form
style="margin-left: 24px"
ref="mainRef"
formLayout="修改"
v-bind="getBindValue"
:schemas="schemas"
:model ="data"
/>
</Skeleton>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, provide, ref } from 'vue';
import {formProps} from "/nerv-lib/component/form/form/props";
import {http, PropTypes} from "/nerv-lib/util";
import NsMarkDown from '../component/markdown.vue';
import NsUploadCustom from "/@/component/upload-custom.vue";
import {useNavigate} from "/nerv-lib/use/use-navigate";
import {NsMessage} from "/nerv-lib/component";
import {useRouter} from "vue-router";
import {Skeleton} from "ant-design-vue";
import {APP} from "/@/router/index.ts";
export default defineComponent({
name: 'ResourceTypeEdit',
props: {
...formProps,
title: PropTypes.string,
getApi: Object | String
},
components: { NsMarkDown, Skeleton },
setup(props, {attrs}) {
provide('components', () => {
return {
NsMarkDown,
NsUploadCustom
};
});
const { navigateBack } = useNavigate();
let loading = ref(false);
const router = useRouter();
const { params } = router.currentRoute.value;
let data = ref({});
const baseImageUrl = ref();
let schemas = ref([])
const initData = async () => {
http.get(props.getApi + '/' + params.id).then(res=>{
data.value = res;
baseImageUrl.value = `/api/${APP}/objs/Images/${res?.iconID}`;
schemas.value = props.schemas;
schemas.value?.forEach(fc=>{
if( fc['field']=='logo') {
fc.componentProps.baseImageUrl = baseImageUrl.value;
}
})
loading.value = false;
})
};
initData();
const mainRef = ref();
function submit() {
mainRef.value
.triggerSubmit()
.then((formData) => {
http.put(`/api/${APP}/objs/ResourceTypes`, formData).then((res) => {
NsMessage.success('操作成功', 1, () => {
router.push({name:'resourceType', path: '/rr/resourceRepository/list'});
});
});
})
.catch(() => ({}));
}
const getBindValue = computed(() => ({
...attrs,
...props,
}));
return {
getBindValue,
navigateBack,
schemas,
data,
mainRef,
submit
};
}
});
</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;
}
.customConfig {
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<page-title :title="title"/>
<a-page-header class="ns-page-header">
<template #extra>
<ns-button @click="navigateBack">返回</ns-button>
<ns-button @click="submit" type="primary" :disabled="!testFlag">保存</ns-button>
<ns-button @click="testConnect" type="primary" :disabled="mainRef?.validateResult ? !mainRef?.validateResult : true"
>测试连接</ns-button>
</template>
</a-page-header>
<ns-form
style="margin-top: 30px; margin-left: 24px"
ref="mainRef"
formLayout="配置"
v-bind="$attrs"
/>
<div class="ant-row ant-form-item" v-if="testFlag">
<div class="ant-form-item-label item-label">
<label title="可执行操作">可执行操作</label>
</div>
<div class="ant-col ant-col-20 ant-form-item-control">
<div class="item-result">
{{testResult}}
</div>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent, ref} from 'vue';
import NsMarkDown from '../component/markdown.vue';
import {http} from "/nerv-lib/util";
import {useRoute} from "vue-router";
import {useNavigate} from "/nerv-lib/use/use-navigate";
import {APP} from "/@/router/index.ts";
export default defineComponent({
name: 'ResourceTypeRelease',
props: {},
components: {NsMarkDown},
setup(props, {attrs}) {
const mainRef = ref('');
const title = ref('配置资源服务地址');
const route = useRoute();
const code = route.params.id
const testFlag = ref(false);
const testResult = ref();
const { navigateBack } = useNavigate();
let releaseData = ref({
code: code,
shelfState: true
});
return {
title,
navigateBack,
testFlag,
mainRef,
testResult,
releaseData,
};
},
methods: {
submit() {
const thisObj = this;
this.mainRef.triggerSubmit().then((data:any)=>{
http.post(`/api/${APP}/objs/ResourceTypes/PutAway`, this.releaseData).then(res=>{
thisObj.$router.push({name:'resourceType', path: '/rr/resourceRepository/list'})
})
})
},
testConnect() {
this.testFlag = false;
this.mainRef.triggerSubmit().then((data:any)=>{
const resourceProviderAddr = data['resourceProviderAddr']
http.get(`/api/${APP}/objs/ResourceTypes/TestConnect?resourceProviderAddr=` + resourceProviderAddr ).then(res=>{
this.testResult = res;
this.testFlag = true;
this.releaseData['resourceProviderAddr']= resourceProviderAddr;
this.releaseData['configs']= this.testResult;
})
})
}
}
})
</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
}
.ant-form-item {
margin-left: 24px;
.item-label {
text-align: left;
flex: 0 0 100px;
}
.item-result {
white-space: break-spaces;
height: 500px;
overflow: auto;
background-color: #fbfbfb;
padding: 4px
}
}
</style>

View File

@@ -0,0 +1,942 @@
<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>

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,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,
};