Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
leiqiaochu 2023-03-14 17:28:18 +08:00
commit 2559138a4a
72 changed files with 7836 additions and 8725 deletions

View File

@ -1 +1,2 @@
ENV=develop
VITE_APP_BASE_API=/api

View File

@ -1 +1,2 @@
ENV=production
VITE_APP_BASE_API=/api

2977
package-lock.json generated

File diff suppressed because it is too large Load Diff

49
plugin/optimize.ts Normal file
View File

@ -0,0 +1,49 @@
import fs from 'fs'
import path from 'path'
const rootPath = path.resolve(__dirname, '../')
function optimizeAntdComponents(moduleName: string): string[] {
const moduleESPath = `${moduleName}/es`
const nodeModulePath = `./node_modules/${moduleESPath}`
const includes: string[] = [moduleESPath]
const folders = fs.readdirSync(
path.resolve(rootPath, nodeModulePath)
)
folders.map(name => {
const folderName = path.resolve(
rootPath,
nodeModulePath,
name
)
let stat = fs.lstatSync(folderName)
if (stat.isDirectory()) {
let styleFolder = path.resolve(folderName, 'style')
if (fs.existsSync((styleFolder))) {
let _stat = fs.lstatSync(styleFolder)
if (_stat.isDirectory()) {
includes.push(`${moduleESPath}/${name}/style`)
}
}
}
})
return includes
}
export function optimizeDeps() {
return {
name: "optimizeDeps",
configResolved: async (config) => {
const components = [
...optimizeAntdComponents('ant-design-vue'),
...optimizeAntdComponents('jetlinks-ui-components')
]
let concat = config.optimizeDeps.include.concat(components)
config.optimizeDeps.include = Array.from(new Set(concat))
console.log(config.optimizeDeps.include)
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 285 KiB

View File

@ -1,8 +1,13 @@
<template>
<router-view />
<ConfigProvider :locale='zhCN'>
<router-view />
</ConfigProvider>
</template>
<script setup lang="ts">
import { ConfigProvider } from 'jetlinks-ui-components'
import zhCN from 'jetlinks-ui-components/es/locale/zh_CN';
</script>
<style scoped>

View File

@ -33,6 +33,13 @@ export const detail = (id: string) => server.get<DeviceInstance>(`/device-instan
*/
export const query = (data?: Record<string, any>) => server.post('/device-instance/_query', data)
/**
*
* @param data
* @returns
*/
export const queryNoPagingPost = (data?: Record<string, any>) => server.post('/device-instance/_query/no-paging?paging=false', data)
/**
*
* @param id ID

9
src/api/edge/device.ts Normal file
View File

@ -0,0 +1,9 @@
import server from '@/utils/request'
export const restPassword = (id: string) => server.post(`/edge/operations/${id}/auth-user-password-reset/invoke`)
export const _control = (deviceId: string) => server.get(`/edge/remote/${deviceId}/url`)
export const _stopControl = (deviceId: string) => server.get(`/edge/remote/${deviceId}/stop`, {})

14
src/api/edge/resource.ts Normal file
View File

@ -0,0 +1,14 @@
import server from '@/utils/request'
export const query = (data: Record<string, any>) => server.post(`/entity/template/_query`, data)
export const modify = (id: string, data: Record<string, any>) => server.put(`/entity/template/${id}`, data)
export const _delete = (id: string) => server.remove(`/entity/template/${id}`)
export const _start = (data: Record<string, any>) => server.post(`/entity/template/start/_batch`, data)
export const _stop = (data: Record<string, any>) => server.post(`/entity/template/stop/_batch`, data)
export const queryDeviceList = (data: Record<string, any>) => server.post(`/device-instance/detail/_query`, data)

View File

@ -24,3 +24,12 @@ export const addOperations_api = (data:object) => server.patch(`/application/ope
* ID
*/
export const delOperations_api = (data:object) => server.remove(`/application/operations/_batch`,{},{data});
/**
* -/api
* @param id
* @param type
* @param data
* @returns
*/
export const updateOperations_api = (code:string,type:'_add'| '_delete', data: object) => server.post(`/application/${code}/grant/${type}`, data);

View File

@ -0,0 +1,17 @@
const color = {
'processing': '64, 169, 255',
'error': '247, 79, 70',
'success': '74, 234, 220',
'warning': '250, 178, 71',
'default': '63, 73, 96'
}
export const getHexColor = (code: string, pe: number = 0.3) => {
const _color = color[code] || color.default
if (code === 'default') {
pe = 0.1
}
return `rgba(${_color}, ${pe})`
}
export default color

View File

@ -1,12 +1,13 @@
<template>
<j-badge
:status="statusNames ? statusNames[status] : 'default'"
:color="_color"
:text="text"
></j-badge>
</template>
<script setup lang="ts">
// import { StatusColorEnum } from '@/utils/consts.ts';
import { getHexColor } from './color'
const props = defineProps({
text: {
type: String,
@ -26,6 +27,18 @@ const props = defineProps({
* 0: 'error'
* }
*/
statusNames: { type: Object },
statusNames: {
type: Object,
default: () => ({
'success': 'success',
'warning': 'warning',
'error': 'error',
'default': 'default',
})
},
});
const _color = computed(() => {
return getHexColor(props.statusNames[props.status], 1)
})
</script>

View File

@ -29,7 +29,9 @@
<div
v-if="showStatus"
class="card-state"
:class="statusNames ? statusNames[status] : ''"
:style='{
backgroundColor: getHexColor(statusNames[status])
}'
>
<div class="card-state-content">
<BadgeStatus
@ -68,9 +70,10 @@
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" name='CardBox'>
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import type { ActionsType } from '@/components/Table/index.vue';
import { getHexColor } from '../BadgeStatus/color'
import type { ActionsType } from '@/components/Table';
import { PropType } from 'vue';
type EmitProps = {

View File

@ -5,6 +5,7 @@
:request='saveSearchHistory'
:historyRequest='getSearchHistory'
:columns='columns'
:class='props.class'
@search='searchSubmit'
/>
</template>

View File

@ -54,7 +54,7 @@
</div>
</template>
<script lang="ts" setup>
<script lang="ts" setup name='JProUpload'>
import { message, UploadChangeParam, UploadProps } from 'ant-design-vue';
import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';

View File

@ -1,6 +1,6 @@
<!-- 参数类型输入组件 -->
<template>
<div class="wrapper">
<div class="value-item-warp">
<j-select
v-if="typeMap.get(itemType) === 'select'"
v-model:value="myValue"
@ -55,7 +55,7 @@
allowClear
>
<template #addonAfter>
<j-upload
<a-upload
name="file"
:action="FILE_UPLOAD"
:headers="headers"
@ -63,7 +63,7 @@
@change="handleFileChange"
>
<AIcon type="UploadOutlined" />
</j-upload>
</a-upload>
</template>
</j-input>
<j-input
@ -92,7 +92,7 @@
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" name='ValueItem'>
import { PropType } from 'vue';
import { UploadChangeParam, UploadFile } from 'ant-design-vue';
import { DefaultOptionType } from 'ant-design-vue/lib/select';
@ -102,6 +102,7 @@ import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { ItemData, ITypes } from './types';
import { FILE_UPLOAD } from '@/api/comm';
import { Upload } from 'jetlinks-ui-components'
type Emits = {
(e: 'update:modelValue', data: string | number | boolean): void;

View File

@ -8,7 +8,7 @@ import CardBox from './CardBox/index.vue';
import Search from './Search'
import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JProUpload from './JUpload/index.vue'
import JProUpload from './Upload/index.vue'
import { BasicLayoutPage, BlankLayoutPage } from './Layout'
import { PageContainer, AIcon } from 'jetlinks-ui-components'
import Ellipsis from './Ellipsis/index.vue'

View File

@ -4,13 +4,14 @@ import store from './store'
import components from './components'
import router from './router'
import './style.less'
// import jComponents from 'jetlinks-ui-components'
// import 'jetlinks-ui-components/es/style.js'
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const app = createApp(App)
app.use(store)
.use(router)
.use(components)
// .use(jComponents)
.mount('#app')

View File

@ -145,6 +145,9 @@ const extraRouteObj = {
{ code: 'Save', name: '详情' },
],
},
'edge/Device': {
children: [{ code: 'Remote', name: '远程控制' }],
},
};

View File

@ -6,7 +6,7 @@
<!-- 已登录-绑定三方账号 -->
<template v-if="!token">
<div class="info">
<a-card style="width: 280px">
<j-card style="width: 280px">
<template #title>
<div class="info-head">
<img :src="getImage('/bind/Rectangle.png')" />
@ -18,9 +18,9 @@
<p>账号admin</p>
<p>用户名超级管理员</p>
</div>
</a-card>
</j-card>
<img :src="getImage('/bind/Vector.png')" />
<a-card style="width: 280px">
<j-card style="width: 280px">
<template #title>
<div class="info-head">
<img :src="getImage('/bind/Rectangle.png')" />
@ -37,11 +37,11 @@
<p>用户名-</p>
<p>名称{{ accountInfo?.name || '-' }}</p>
</div>
</a-card>
</j-card>
</div>
<div class="btn">
<a-button type="primary" @click="handleBind"
>立即绑定</a-button
<j-button type="primary" @click="handleBind"
>立即绑定</j-button
>
</div>
</template>
@ -60,30 +60,30 @@
你已通过微信授权,完善以下登录信息即可以完成绑定
</div>
<div class="login-form">
<a-form layout="vertical">
<a-form-item
<j-form layout="vertical">
<j-form-item
label="账户"
v-bind="validateInfos.username"
>
<a-input
<j-input
v-model:value="formData.username"
placeholder="请输入账户"
/>
</a-form-item>
<a-form-item
</j-form-item>
<j-form-item
label="密码"
v-bind="validateInfos.password"
>
<a-input-password
<j-input-password
v-model:value="formData.password"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item
</j-form-item>
<j-form-item
label="验证码"
v-bind="validateInfos.verifyCode"
>
<a-input
<j-input
v-model:value="formData.verifyCode"
placeholder="请输入验证码"
>
@ -94,18 +94,18 @@
style="cursor: pointer"
/>
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
</j-input>
</j-form-item>
<j-form-item>
<j-button
type="primary"
@click="handleLoginBind"
style="width: 100%"
>
登录并绑定账户
</a-button>
</a-form-item>
</a-form>
</j-button>
</j-form-item>
</j-form>
</div>
</div>
</template>

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="notification-record-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="notification-subscription-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table

View File

@ -1,12 +1,29 @@
<template>
<j-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleSave" @cancel="handleCancel">
<j-modal
:maskClosable="false"
width="800px"
:visible="true"
title="导入"
@ok="handleSave"
@cancel="handleCancel"
>
<div style="margin-top: 10px">
<j-form :layout="'vertical'">
<j-row>
<j-col span="24">
<j-form-item label="产品" required>
<j-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
<j-select-option :value="item.id" v-for="item in productList" :key="item.id" :label="item.name">{{ item.name }}</j-select-option>
<j-select
showSearch
v-model:value="modelRef.product"
placeholder="请选择产品"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
@ -17,7 +34,11 @@
</j-col>
<j-col span="12">
<j-form-item label="文件上传" v-if="modelRef.product">
<NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" />
<NormalUpload
:product="modelRef.product"
v-model="modelRef.upload"
:file="modelRef.file"
/>
</j-form-item>
</j-col>
</j-row>
@ -27,16 +48,17 @@
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product'
import { queryNoPagingPost } from '@/api/device/product';
const emit = defineEmits(['close', 'save'])
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object,
default: undefined
}
})
const productList = ref<Record<string, any>[]>([])
default: undefined,
},
type: String,
});
const productList = ref<Record<string, any>[]>([]);
const modelRef = reactive({
product: undefined,
@ -44,26 +66,40 @@ const modelRef = reactive({
file: {
fileType: 'xlsx',
autoDeploy: false,
}
},
});
watch(
() => props.data,
() => {
queryNoPagingPost({paging: false}).then(resp => {
if(resp.status === 200){
productList.value = resp.result as Record<string, any>[]
queryNoPagingPost({
paging: false,
terms: [
{
column: 'state',
value: '1',
type: 'and'
},
{
column: 'accessProvider',
value: props?.type
}
],
sorts: [{ name: 'createTime', order: 'desc' }]
}).then((resp) => {
if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[];
}
})
});
},
{immediate: true, deep: true}
)
{ immediate: true, deep: true },
);
const handleCancel = () => {
emit('close')
}
emit('close');
};
const handleSave = () => {
emit('save')
}
emit('save');
};
</script>

View File

@ -289,7 +289,6 @@ import { queryTree } from '@/api/device/category';
import { useMenuStore } from '@/store/menu';
import type { ActionsType } from './typings';
import dayjs from 'dayjs';
import { throttle } from 'lodash-es';
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});

View File

@ -0,0 +1,49 @@
<template>
<page-container>
<div class="box">
<iframe :src="url" class="box-iframe"></iframe>
</div>
</page-container>
</template>
<script setup lang="ts">
import { _control, _stopControl } from '@/api/edge/device';
const url = ref<string>('');
const deviceId = ref<string>('');
watch(
() => history.state?.params?.id,
(newId) => {
if (newId) {
deviceId.value = newId as string;
_control(newId).then((resp: any) => {
if (resp.status === 200) {
const item = `http://${resp.result?.url}/#/login?token=${resp.result.token}`;
url.value = item;
}
});
}
},
{ immediate: true },
);
onUnmounted(() => {
if (deviceId.value) {
_stopControl(unref(deviceId));
}
});
</script>
<style lang="less" scoped>
.box {
width: 100%;
height: 85vh;
background-color: #fff;
}
.box-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>

View File

@ -0,0 +1,265 @@
<template>
<j-modal
:maskClosable="false"
width="650px"
:visible="true"
:title="!!data?.id ? '编辑' : '新增'"
@ok="handleSave"
@cancel="handleCancel"
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-row type="flex">
<j-col flex="180px">
<j-form-item name="photoUrl">
<JProUpload v-model="modelRef.photoUrl" />
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item
name="id"
:rules="[
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
max: 64,
message: '最多输入64个字符',
},
{
validator: vailId,
trigger: 'blur',
},
]"
>
<template #label>
<span>
ID
<j-tooltip
title="若不填写系统将自动生成唯一ID"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-input
v-model:value="modelRef.id"
placeholder="请输入ID"
:disabled="!!data?.id"
/>
</j-form-item>
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]"
>
<j-input
v-model:value="modelRef.name"
placeholder="请输入名称"
/>
</j-form-item>
</j-col>
</j-row>
<j-row>
<j-col :span="22">
<j-form-item
name="productId"
:rules="[
{
required: true,
message: '请选择所属产品',
},
]"
>
<template #label>
<span
>所属产品
<j-tooltip title="只能选择“正常”状态的产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-select
showSearch
v-model:value="modelRef.productId"
:disabled="!!data?.id"
placeholder="请选择所属产品"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="2" style="margin-top: 30px">
<PermissionButton
type="link"
:disabled="data.id"
@click="visible = true"
hasPermission="device/Product:add"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-col>
</j-row>
<j-form-item
label="说明"
name="describe"
:rules="[
{
max: 200,
message: '最多输入200个字符',
},
]"
>
<j-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
showCount
:maxlength="200"
/>
</j-form-item>
</j-form>
</div>
</j-modal>
<SaveProduct
v-model:visible="visible"
v-model:productId="modelRef.productId"
:channel="'official-edge-gateway'"
@close="onClose"
:deviceType="'gateway'"
@save="onSave"
/>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product';
import { isExists, update } from '@/api/device/instance';
import { getImage } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import SaveProduct from '@/views/media/Device/Save/SaveProduct.vue';
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object,
default: undefined,
},
});
const productList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const visible = ref<boolean>(false);
const formRef = ref();
const modelRef = reactive({
productId: undefined,
id: undefined,
name: '',
describe: '',
photoUrl: getImage('/device/instance/device-card.png'),
});
const vailId = async (_: Record<string, any>, value: string) => {
if (!props?.data?.id && value) {
const resp = await isExists(value);
if (resp.status === 200 && resp.result) {
return Promise.reject('ID重复');
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
};
watch(
() => props.data,
(newValue) => {
queryNoPagingPost({
paging: false,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
termType: 'eq',
column: 'state',
value: 1,
type: 'and',
},
{
termType: 'eq',
column: 'accessProvider',
value: 'official-edge-gateway',
},
],
},
],
}).then((resp) => {
if (resp.status === 200) {
productList.value = resp.result as Record<string, any>[];
}
});
Object.assign(modelRef, newValue);
},
{ immediate: true, deep: true },
);
const handleCancel = () => {
emit('close');
formRef.value.resetFields();
};
const onClose = () => {
visible.value = false;
};
const onSave = (_data: any) => {
productList.value.push(_data)
}
const handleSave = () => {
formRef.value
.validate()
.then(async (_data: any) => {
loading.value = true;
const obj = { ..._data };
if (!obj.id) {
delete obj.id;
}
const resp = await update(obj).finally(() => {
loading.value = false;
});
if (resp.status === 200) {
message.success('操作成功!');
emit('save');
formRef.value.resetFields();
}
})
.catch((err: any) => {
console.log('error', err);
});
};
</script>

View File

@ -0,0 +1,438 @@
<template>
<page-container>
<pro-search
:columns="columns"
target="edge-device"
@search="handleSearch"
/>
<JProTable
ref="edgeDeviceRef"
:columns="columns"
:request="query"
:defaultParams="defaultParams"
:params="params"
:gridColumn="3"
>
<template #headerTitle>
<j-space>
<PermissionButton
type="primary"
@click="handleAdd"
hasPermission="edge/Device:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
<PermissionButton
@click="importVisible = true"
hasPermission="edge/Device:import"
>
<template #icon
><AIcon type="ImportOutlined"
/></template>
导入
</PermissionButton>
</j-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
>
<template #img>
<img
:src="getImage('/device/instance/device-card.png')"
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 20px">
<j-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>{{ slotProps.deviceType?.text }}</div>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<Ellipsis style="width: 100%">
{{ slotProps.productName }}
</Ellipsis>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'edge/Device:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</CardBox>
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #createTime="slotProps">
<span>{{
dayjs(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="slotProps">
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0 5px"
:hasPermission="'edge/Device:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
<Save
v-if="visible"
:data="current"
@close="visible = false"
@save="saveBtn"
/>
<Import @save="onRefresh" @close="importVisible = false" v-if="importVisible" type="official-edge-gateway" />
</page-container>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product';
import { queryTree } from '@/api/device/category';
import { message } from 'jetlinks-ui-components';
import { ActionsType } from '@/views/device/Instance/typings';
import { useMenuStore } from '@/store/menu';
import { getImage } from '@/utils/comm';
import dayjs from 'dayjs';
import { query, _delete, _deploy, _undeploy } from '@/api/device/instance';
import { restPassword } from '@/api/edge/device';
import Save from './Save/index.vue';
import Import from '@/views/device/Instance/Import/index.vue';
const menuStory = useMenuStore();
const defaultParams = {
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
};
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const params = ref<Record<string, any>>({});
const edgeDeviceRef = ref<Record<string, any>>({});
const importVisible = ref<boolean>(false);
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
defaultTermType: 'eq',
},
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
first: true,
},
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
queryNoPagingPost({ paging: false }).then((resp: any) => {
resolve(
resp.result.map((item: any) => ({
label: item.name,
value: item.id,
})),
);
});
}),
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
{
key: 'classifiedId',
dataIndex: 'classifiedId',
title: '产品分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: () =>
new Promise((resolve) => {
queryTree({ paging: false }).then((resp: any) => {
resolve(resp.result);
});
}),
},
},
{
dataIndex: 'deviceType',
title: '设备类型',
valueType: 'select',
hideInTable: true,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
],
},
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
search: {
type: 'string',
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
handleView(data.id);
},
},
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'setting',
text: '远程控制',
tooltip: {
title: '远程控制',
},
icon: 'ControlOutlined',
onClick: () => {
menuStory.jumpPage('edge/Device/Remote', { id: data.id });
},
},
{
key: 'password',
text: '重置密码',
tooltip: {
title: '重置密码',
},
icon: 'RedoOutlined',
popConfirm: {
title: '确认重置密码为P@ssw0rd',
onConfirm: async () => {
restPassword(data.id).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
}
});
},
},
},
{
key: 'action',
text: data.state?.value !== 'notActive' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'notActive' ? '禁用' : '启用',
},
icon:
data.state.value !== 'notActive'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'notActive' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'notActive') {
response = await _undeploy(data.id);
} else {
response = await _deploy(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'notActive',
tooltip: {
title:
data.state.value !== 'notActive'
? '已启用的设备不能删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
edgeDeviceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleView = (id: string) => {
menuStory.jumpPage('device/Instance/Detail', { id });
};
const handleAdd = () => {
visible.value = true;
current.value = {};
};
const saveBtn = () => {
visible.value = false;
edgeDeviceRef.value?.reload();
};
const onRefresh = () => {
importVisible.value = false
edgeDeviceRef.value?.reload();
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,132 @@
<template>
<j-modal
visible
title="下发结果"
:width="900"
@ok="emit('close')"
@cancel="emit('close')"
>
<j-row>
<j-col :span="8">
<div>成功{{ count }}</div>
<div>
失败{{ countErr }}
<j-button @click="_download(errMessage || '', '下发失败原因')" v-if="errMessage.length" type="link"
>下载</j-button
>
</div>
</j-col>
<j-col :span="8">下发设备数量{{ list.length || 0 }}</j-col>
<j-col :span="8">已下发数量{{ countErr + count }}</j-col>
</j-row>
<div v-if="!flag">
<j-textarea :rows="20" :value="JSON.stringify(errMessage)" />
</div>
</j-modal>
</template>
<script setup lang="ts">
import { LocalStore } from '@/utils/comm';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import dayjs from 'dayjs';
import { EventSourcePolyfill } from 'event-source-polyfill';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
list: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['close']);
const count = ref<number>(0);
const countErr = ref<number>(0);
const flag = ref<boolean>(true);
const errMessage = ref<any[]>([]);
const getData = () => {
let dt = 0;
let et = 0;
const errMessages: any[] = [];
const _terms = {
deviceId: (props.list || []).map((item: any) => item?.id),
params: JSON.stringify({
name: props.data.name,
targetId: props.data.targetId,
targetType: props.data.targetType,
category: props.data.category,
metadata: props.data?.metadata,
}),
};
const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => {
if (Array.isArray(_terms[key]) && _terms[key].length) {
_terms[key].map((item: string) => {
url.append(key, item);
});
} else {
url.append(key, _terms[key]);
}
});
const source = new EventSourcePolyfill(
`${BASE_API_PATH}/edge/operations/entity-template-save/invoke/_batch?:X_Access_Token=${LocalStore.get(
TOKEN_KEY,
)}&${url}`,
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.successful) {
dt += 1;
count.value = dt;
} else {
et += 1;
countErr.value = et;
flag.value = false;
if (errMessages.length <= 5) {
errMessages.push({ ...res });
errMessage.value = [...errMessages];
}
}
};
source.onerror = () => {
source.close();
};
source.onopen = () => {};
};
const _download = (record: Record<string, any>, fileName: string, format?: string) => {
//
const ghostLink = document.createElement('a');
ghostLink.download = `${fileName ? '' : record?.name}${fileName}_${dayjs(new Date()).format(
format || 'YYYY_MM_DD',
)}.txt`;
ghostLink.style.display = 'none';
//Blob
const blob = new Blob([JSON.stringify(record)]);
ghostLink.href = URL.createObjectURL(blob);
//
document.body.appendChild(ghostLink);
ghostLink.click();
//
document.body.removeChild(ghostLink);
}
watch(
() => props.data.id,
(newId) => {
if(newId){
getData()
}
},
{
immediate: true,
},
);
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,181 @@
<template>
<j-modal
visible
title="下发设备"
:width="1000"
@ok="onSave"
@cancel="onCancel"
>
<div class="alert">
<AIcon
type="InfoCircleOutlined"
style="margin-right: 10px"
/>线
</div>
<pro-search
:columns="columns"
target="edge-resource-issue"
@search="handleSearch"
type="simple"
class="search"
/>
<JProTable
ref="edgeResourceIssueRef"
:columns="columns"
:request="queryDeviceList"
:defaultParams="defaultParams"
:params="params"
model="TABLE"
:bodyStyle="{ padding: 0 }"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #sourceId="slotProps">
{{ slotProps.sourceName }}
</template>
<template #registerTime="slotProps">
<span>{{
dayjs(slotProps.registerTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
</JProTable>
<Result v-if="visible" :data="props.data" :list="_data" @close="onCancel" />
</j-modal>
</template>
<script setup lang="ts">
import { onlyMessage } from '@/utils/comm';
import { queryDeviceList } from '@/api/edge/resource';
import dayjs from 'dayjs';
import Result from './Result.vue';
const defaultParams = {
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [
{
terms: [
{
termType: 'eq',
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
};
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['close']);
const params = ref({});
const edgeResourceIssueRef = ref();
const _selectedRowKeys = ref<string[]>([]);
const _data = ref<any[]>([]);
const visible = ref<boolean>(false);
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
width: 200,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
ellipsis: true,
search: {
type: 'select',
},
},
{
title: '设备名称',
ellipsis: true,
dataIndex: 'name',
key: 'name',
},
{
title: '注册时间',
dataIndex: 'registerTime',
key: 'registerTime',
width: 200,
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
];
const onSelectChange = (keys: string[], _options: any[]) => {
_selectedRowKeys.value = [...keys];
_data.value = _options;
};
const handleSearch = (v: any) => {
params.value = v;
};
const onSave = () => {
if(_data.value.length){
visible.value = true
} else {
onlyMessage('请选择设备', 'error')
}
};
const onCancel = () => {
emit('close');
};
</script>
<style lang="less" scoped>
.search {
padding: 0 0 0 24px;
}
.alert {
height: 40px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<j-modal visible title="编辑" :width="700" @ok="onSave" @cancel="onCancel">
<MonacoEditor
style="width: 100%; height: 370px"
theme="vs"
v-model="monacoValue"
language="json"
/>
</j-modal>
</template>
<script setup lang="ts">
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { modify } from '@/api/edge/resource';
import { onlyMessage } from '@/utils/comm';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['close', 'save']);
const monacoValue = ref<string>('{}');
watchEffect(() => {
monacoValue.value = props.data?.metadata || '{}';
});
const onSave = async () => {
const resp = await modify(props.data.id, { metadata: unref(monacoValue) });
if (resp.status === 200) {
emit('save');
onlyMessage('操作成功', 'success');
}
};
const onCancel = () => {
emit('close');
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,383 @@
<template>
<page-container>
<pro-search
:columns="columns"
target="edge-resource"
@search="handleSearch"
/>
<JProTable
ref="edgeResourceRef"
:columns="columns"
:request="query"
:defaultParams="defaultParams"
:params="params"
>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<img
:src="getImage('/device/instance/device-card.png')"
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span
style="font-size: 16px; font-weight: 600"
@click.stop="handleView(slotProps.id)"
>
{{ slotProps.name }}
</span>
</Ellipsis>
<j-row style="margin-top: 20px">
<j-col :span="12">
<div class="card-item-content-text">
通讯协议
</div>
<Ellipsis>{{
options.find(
(i) => i.value === slotProps.category,
)?.label || slotProps.category
}}</Ellipsis>
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
所属边缘网关
</div>
<Ellipsis style="width: 100%">
{{ slotProps.sourceName }}
</Ellipsis>
</j-col>
</j-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'edge/Resource:' + item.key"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</PermissionButton>
</template>
</CardBox>
</template>
<template #state="slotProps">
<j-badge
:text="slotProps.state?.text"
:status="statusMap.get(slotProps.state?.value)"
/>
</template>
<template #sourceId="slotProps">
{{ slotProps.sourceName }}
</template>
<template #category="slotProps">
{{
options.find((i) => i.value === slotProps.category)
?.label || slotProps.category
}}
</template>
<template #createTime="slotProps">
<span>{{
dayjs(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="slotProps">
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
:disabled="i.disabled"
:popConfirm="i.popConfirm"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
type="link"
style="padding: 0 5px"
:hasPermission="'edge/Resource:' + i.key"
>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
<Save
v-if="visible"
:data="current"
@close="visible = false"
@save="saveBtn"
/>
<Issue
v-if="settingVisible"
:data="current"
@close="settingVisible = false"
/>
</page-container>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/instance';
import { message } from 'jetlinks-ui-components';
import { ActionsType } from '@/views/device/Instance/typings';
import { useMenuStore } from '@/store/menu';
import { getImage } from '@/utils/comm';
import dayjs from 'dayjs';
import { query, _delete, _start, _stop } from '@/api/edge/resource';
import Save from './Save/index.vue';
import Issue from './Issue/index.vue';
const menuStory = useMenuStore();
const defaultParams = { sorts: [{ name: 'createTime', order: 'desc' }] };
const statusMap = new Map();
statusMap.set('enabled', 'success');
statusMap.set('disabled', 'error');
const options = [
{ label: 'UA接入', value: 'OPC_UA' },
{ label: 'Modbus TCP接入', value: 'MODBUS_TCP' },
{ label: 'S7-200接入', value: 'snap7' },
{ label: 'BACnet接入', value: 'BACNetIp' },
{ label: 'MODBUS_RTU接入', value: 'MODBUS_RTU' },
];
const params = ref<Record<string, any>>({});
const edgeResourceRef = ref<Record<string, any>>({});
const settingVisible = ref<boolean>(false);
const visible = ref<boolean>(false);
const current = ref<Record<string, any>>({});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
dataIndex: 'category',
title: '通信协议',
valueType: 'select',
scopedSlots: true,
key: 'category',
search: {
type: 'select',
options: options,
},
},
{
title: '所属边缘网关',
dataIndex: 'sourceId',
key: 'sourceId',
scopedSlots: true,
search: {
type: 'select',
options: () =>
new Promise((resolve) => {
queryNoPagingPost({
paging: false,
terms: [
{
terms: [
{
column: 'productId$product-info',
value: 'accessProvider is official-edge-gateway',
},
],
type: 'and',
},
],
sorts: [
{
name: 'createTime',
order: 'desc',
},
],
}).then((resp: any) => {
resolve(
resp.result.map((item: any) => ({
label: item.name,
value: item.id,
})),
);
});
}),
},
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'disabled' },
{ label: '正常', value: 'enabled' },
],
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 250,
scopedSlots: true,
},
];
const getActions = (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
): ActionsType[] => {
if (!data) return [];
const actions = [
{
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
visible.value = true;
current.value = data;
},
},
{
key: 'setting',
text: '下发',
disabled: data.state?.value === 'disabled',
tooltip: {
title:
data.state.value === 'disabled'
? '请先启用,再下发'
: '下发',
},
icon: 'DownSquareOutlined',
onClick: () => {
settingVisible.value = true;
current.value = data;
},
},
{
key: 'action',
text: data.state?.value !== 'disabled' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disabled' ? '禁用' : '启用',
},
icon:
data.state.value !== 'disabled'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `确认${
data.state.value !== 'disabled' ? '禁用' : '启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state.value !== 'disabled') {
response = await _stop([data.id]);
} else {
response = await _start([data.id]);
}
if (response && response.status === 200) {
message.success('操作成功!');
edgeResourceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data.state?.value !== 'disabled',
tooltip: {
title:
data.state.value !== 'disabled'
? '请先禁用,再删除。'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await _delete(data.id);
if (resp.status === 200) {
message.success('操作成功!');
edgeResourceRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
return actions.filter((i: ActionsType) => i.key !== 'view');
return actions;
};
const handleSearch = (_params: any) => {
params.value = _params;
};
const handleView = (id: string) => {
menuStory.jumpPage('device/Instance/Detail', { id });
};
const saveBtn = () => {
visible.value = false;
edgeResourceRef.value?.reload();
};
const onRefresh = () => {
settingVisible.value = false;
edgeResourceRef.value?.reload();
};
</script>
<style lang="less" scoped>
</style>

View File

@ -1,8 +1,6 @@
<template>
<a-card class="boot-card-container" :bordered="false">
<template #title>
<h5 class="title">{{ cardTitle }}</h5>
</template>
<div class="boot-card-container">
<h5 class="title">{{ cardTitle }}</h5>
<div class="box">
<div
class="box-item"
@ -23,7 +21,7 @@
</div>
</div>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
@ -46,9 +44,8 @@ const jumpPage = (item: bootConfig) => {
<style lang="less" scoped>
.boot-card-container {
:deep(.ant-card-body) {
padding-top: 0;
}
background-color: #fff;
padding: 24px 12px;
.title {
position: relative;
z-index: 2;

View File

@ -13,8 +13,8 @@
<j-row :gutter="24">
<j-col :span="12"><DeviceCountCard /></j-col>
<j-col :span="12"><BasicCountCard /></j-col>
<j-col :span="24" style="margin-top: 24px">
<PlatformPicCard image="/images/home/content1.png" />
<j-col :span="24" style="margin-top: 24px;">
<PlatformPicCard image="/images/home/content1.svg" />
</j-col>
</j-row>
</j-col>
@ -41,7 +41,7 @@
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id })"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id, tab: 'Diagnose' })"
/>
</div>
</div>
@ -157,7 +157,7 @@ const deviceStepDetails: recommendList[] = [
linkUrl: 'device/Instance',
auth: devicePermission('import'),
params: {
import: true,
type: 'import'
},
},
];
@ -175,7 +175,7 @@ const opsBootConfig: bootConfig[] = [
label: '日志排查',
link: 'Log',
params: {
key: 'system',
tab: 'system',
},
image: '/images/home/guide-home5.png',
},
@ -220,7 +220,7 @@ const opsStepDetails: recommendList[] = [
iconUrl: '/images/home/bottom-5.png',
linkUrl: 'Log',
params: {
key: 'system',
tab: 'system',
},
},
];

View File

@ -39,7 +39,7 @@ const opsBootConfig: bootConfig[] = [
label: '日志排查',
link: 'Log',
params: {
key: 'system',
tab: 'system',
},
},
{
@ -83,7 +83,7 @@ const opsStepDetails: recommendList[] = [
iconUrl: '/images/home/bottom-5.png',
linkUrl: 'Log',
params: {
key: 'system',
tab: 'system',
},
},
];

View File

@ -28,7 +28,7 @@
<DeviceChooseDialog
v-if="deviceDialogVisible"
v-model:visible="deviceDialogVisible"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id })"
@confirm="(id:string)=>jumpPage('device/Instance/Detail', { id, tab: 'Diagnose' })"
/>
</div>
</div>

View File

@ -25,10 +25,11 @@ const props = defineProps({
overflow: hidden;
background-color: #fff;
border-bottom: 1px solid #2f54eb;
height: 458px;
.bj {
display: block;
width: 100%;
height: 100%;
}
.title {

View File

@ -20,33 +20,35 @@
placeholder="请输入名称"
/>
</j-form-item>
<template v-for="(item, index) in extendFormItem" :key="index">
<j-form-item
:name="item.name"
:label="item.label"
:rules="{
required: item.required,
message: item.message,
trigger: 'change',
}"
>
<j-select
v-if="item.type === 'enum'"
v-model:value="formData[item.name[0]][item.name[1]]"
:options="item.options"
:placeholder="item.message"
/>
<j-input-password
v-else-if="item.type === 'password'"
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/>
<j-input
v-else
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/>
</j-form-item>
<template v-if="deviceType !== 'gateway'">
<template v-for="(item, index) in extendFormItem" :key="index">
<j-form-item
:name="item.name"
:label="item.label"
:rules="{
required: item.required,
message: item.message,
trigger: 'change',
}"
>
<j-select
v-if="item.type === 'enum'"
v-model:value="formData[item.name[0]][item.name[1]]"
:options="item.options"
:placeholder="item.message"
/>
<j-input-password
v-else-if="item.type === 'password'"
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/>
<j-input
v-else
v-model:value="formData[item.name[0]][item.name[1]]"
:placeholder="item.message"
/>
</j-form-item>
</template>
</template>
<j-form-item
label="接入网关"
@ -147,6 +149,7 @@ type Emits = {
(e: 'update:visible', data: boolean): void;
(e: 'update:productId', data: string): void;
(e: 'close'): void;
(e: 'save', data: Record<string, any>): void;
};
const emit = defineEmits<Emits>();
@ -154,6 +157,7 @@ const props = defineProps({
visible: { type: Boolean, default: false },
productId: { type: String, default: '' },
channel: { type: String, default: '' },
deviceType: { type: String, default: 'device' },
});
const _vis = computed({
@ -186,12 +190,12 @@ const handleClick = async (e: any) => {
formData.value.accessId = e.id;
formData.value.accessName = e.name;
formData.value.accessProvider = e.provider;
formData.value.messageProtocol = e.provider;
formData.value.messageProtocol = e.protocolDetail.id;
formData.value.protocolName = e.protocolDetail.name;
formData.value.transportProtocol = e.transport;
const { result } = await DeviceApi.getConfiguration(
props.channel,
e.protocol,
e.transport,
);
@ -233,7 +237,7 @@ const formData = ref({
access_pwd: '',
stream_mode: 'UDP',
},
deviceType: 'device',
deviceType: props.deviceType,
messageProtocol: '',
name: '',
protocolName: '',
@ -256,6 +260,7 @@ const handleOk = () => {
res.result.id,
);
if (deployResp.success) {
emit('save', {...res.result})
message.success('操作成功');
handleCancel();
}

View File

@ -392,53 +392,53 @@ const formRules = ref({
provider: [{ required: true, message: '请选择类型' }],
//
'configuration.appKey': [
{ required: true, message: '请输入AppKey' },
{ max: 64, message: '最多可输入64个字符' },
{ required: true, message: '请输入AppKey', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
'configuration.appSecret': [
{ required: true, message: '请输入AppSecret' },
{ max: 64, message: '最多可输入64个字符' },
{ required: true, message: '请输入AppSecret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
// 'configuration.url': [{ required: true, message: 'WebHook' }],
//
'configuration.corpId': [
{ required: true, message: '请输入corpId' },
{ required: true, message: '请输入corpId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.corpSecret': [
{ required: true, message: '请输入corpSecret' },
{ required: true, message: '请输入corpSecret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
// /
'configuration.regionId': [
{ required: true, message: '请输入RegionId' },
{ required: true, message: '请输入RegionId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.accessKeyId': [
{ required: true, message: '请输入AccessKeyId' },
{ required: true, message: '请输入AccessKeyId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.secret': [
{ required: true, message: '请输入Secret' },
{ required: true, message: '请输入Secret', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
//
'configuration.host': [{ required: true, message: '请输入服务器地址' }],
'configuration.host': [{ required: true, message: '请输入服务器地址', trigger: 'blur' }],
'configuration.sender': [
{ required: true, message: '请输入发件人' },
{ required: true, message: '请输入发件人', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.username': [
{ required: true, message: '请输入用户名' },
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.password': [
{ required: true, message: '请输入密码' },
{ required: true, message: '请输入密码', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
// webhook
'configuration.url': [
{ required: true, message: '请输入Webhook' },
{ required: true, message: '请输入Webhook', trigger: 'blur' },
// {
// pattern:
// /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[j-z]{2,6}\/?/,
@ -458,8 +458,6 @@ const getDetail = async () => {
const res = await configApi.detail(route.params.id as string);
// formData.value = res.result;
Object.assign(formData.value, res.result);
// console.log('res.result: ', res.result);
// console.log('formData.value: ', formData.value);
};
getDetail();
@ -537,7 +535,6 @@ const btnLoading = ref<boolean>(false);
const handleSubmit = () => {
validate()
.then(async () => {
// console.log('formData.value: ', formData.value);
btnLoading.value = true;
let res;
if (!formData.value.id) {
@ -545,7 +542,6 @@ const handleSubmit = () => {
} else {
res = await configApi.update(formData.value);
}
// console.log('res: ', res);
if (res?.success) {
message.success('保存成功');
router.back();

View File

@ -147,6 +147,19 @@
</template>
</CardBox>
</template>
<template #type="slotProps">
<span> {{ getMethodTxt(slotProps.type) }}</span>
</template>
<template #provider="slotProps">
<span>
{{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span>
</template>
<!-- <template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
<template #action="slotProps">
<j-space :size="16">
<template
@ -205,6 +218,7 @@ const columns = [
title: '配置名称',
dataIndex: 'name',
key: 'name',
width: 100,
search: {
type: 'string',
},
@ -214,6 +228,7 @@ const columns = [
dataIndex: 'type',
key: 'type',
scopedSlots: true,
width: 100,
search: {
type: 'select',
options: NOTICE_METHOD,
@ -227,6 +242,7 @@ const columns = [
dataIndex: 'provider',
key: 'provider',
scopedSlots: true,
width: 200,
search: {
type: 'select',
options: providerList,
@ -239,6 +255,8 @@ const columns = [
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: true,
ellipsis: true,
search: {
type: 'string',
},
@ -272,6 +290,14 @@ const getLogo = (type: string, provider: string) => {
const getMethodTxt = (type: string) => {
return NOTICE_METHOD.find((f) => f.value === type)?.label;
};
/**
* 根据类型展示对应文案
* @param type
* @param provider
*/
const getProviderTxt = (type: string, provider: string) => {
return MSG_TYPE[type].find((f: any) => f.value === provider)?.label;
};
/**
* 新增

View File

@ -172,11 +172,11 @@ const getTemplateDetail = async () => {
formData.value.templateDetailTable = result.variableDefinitions.map(
(m: any) => ({
...m,
type: m.expands ? m.expands.businessType : m.type,
type: m.expands?.businessType ? m.expands.businessType : m.type,
value: undefined,
//
otherRules:
m.id === 'calledNumber'
m.id === 'calledNumber' || m.id === 'phoneNumber'
? [
{
max: 64,

View File

@ -895,23 +895,25 @@ watch(
const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }],
name: [
{ required: true, message: '请输入名称' },
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
provider: [{ required: true, message: '请选择类型' }],
configId: [{ required: true, message: '请选择绑定配置' }],
configId: [{ required: true, message: '请选择绑定配置', trigger: 'blur' }],
//
'template.agentId': [
{ required: true, message: '请输入AgentId' },
{ required: true, message: '请输入AgentId', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
'template.messageType': [{ required: true, message: '请选择消息类型' }],
'template.messageType': [
{ required: true, message: '请选择消息类型', trigger: 'blur' },
],
'template.markdown.title': [
{ required: true, message: '请输入标题', trigger: 'change' },
{ required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
'template.link.title': [
{ required: true, message: '请输入标题', trigger: 'change' },
{ required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
// 'template.url': [{ required: true, message: 'WebHook' }],
@ -919,7 +921,7 @@ const formRules = ref({
// 'template.agentId': [{ required: true, message: 'AgentId' }],
//
'template.subject': [
{ required: true, message: '请输入标题' },
{ required: true, message: '请输入标题', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
],
'template.sendTo': [
@ -946,7 +948,9 @@ const formRules = ref({
],
//
'template.templateType': [{ required: true, message: '请选择类型' }],
'template.templateCode': [{ required: true, message: '请输入模板ID' }],
'template.templateCode': [
{ required: true, message: '请输入模板ID', trigger: 'blur' },
],
'template.calledNumber': [
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
{
@ -980,14 +984,19 @@ const formRules = ref({
},
],
//
'template.code': [{ required: true, message: '请选择模板' }],
'template.signName': [{ required: true, message: '请输入签名' }],
'template.code': [
{ required: true, message: '请选择模板', trigger: 'blur' },
],
'template.signName': [
{ required: true, message: '请输入签名', trigger: 'blur' },
],
// webhook
description: [{ max: 200, message: '最多可输入200个字符' }],
'template.message': [
{
required: true,
message: '请输入模板内容',
trigger: 'blur',
},
{ max: 500, message: '最多可输入500个字符', trigger: 'change' },
],
@ -1073,7 +1082,7 @@ const spliceStr = () => {
variableFieldsStr += formData.value.template.body as string;
if (formData.value.provider === 'aliyun')
variableFieldsStr += formData.value.template.ttsmessage as string;
// console.log('variableFieldsStr: ', variableFieldsStr);
return variableFieldsStr || '';
};
@ -1130,7 +1139,6 @@ const handleMessageTypeChange = () => {
};
}
formData.value.variableDefinitions = [];
// formData.value.template.message = '';
};
/**
@ -1141,7 +1149,6 @@ const getDetail = async () => {
const res = await templateApi.detail(route.params.id as string);
// formData.value = res.result;
Object.assign(formData.value, res.result);
// console.log('formData.value: ', formData.value);
}
};
getDetail();
@ -1176,8 +1183,7 @@ const handleTypeChange = () => {
const handleProviderChange = () => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value: ', formData.value);
// console.log('formData.value.template: ', formData.value.template);
getConfigList();
resetPublicFiles();
};
@ -1245,7 +1251,6 @@ const handleSubmit = () => {
delete formData.value.template.link;
if (formData.value.template.messageType === 'link')
delete formData.value.template.markdown;
// console.log('formData.value: ', formData.value);
// , , , :
setTimeout(() => {
validate()
@ -1261,13 +1266,11 @@ const handleSubmit = () => {
}
btnLoading.value = true;
let res;
if (!formData.value.id) {
res = await templateApi.save(formData.value);
} else {
res = await templateApi.update(formData.value);
}
// console.log('res: ', res);
const res = formData.value.id
? await templateApi.update(formData.value)
: await templateApi.save(formData.value);
if (res?.success) {
message.success('保存成功');
router.back();
@ -1281,14 +1284,4 @@ const handleSubmit = () => {
});
}, 200);
};
// test
// watch(
// () => formData.value,
// (val) => {
// console.log('formData.value: ', val);
// },
// { deep: true },
// );
// test
</script>

View File

@ -110,14 +110,19 @@
</template>
</CardBox>
</template>
<template #bodyCell="{ column, text, record }">
<span v-if="column.dataIndex === 'type'">
{{ getMethodTxt(record.type) }}
</span>
<span v-if="column.dataIndex === 'provider'">
{{ getProviderTxt(record.type, record.provider) }}
<template #type="slotProps">
<span> {{ getMethodTxt(slotProps.type) }}</span>
</template>
<template #provider="slotProps">
<span>
{{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span>
</template>
<!-- <template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
<template #action="slotProps">
<j-space :size="16">
<template
@ -150,12 +155,8 @@
<script setup lang="ts">
import TemplateApi from '@/api/notice/template';
import type { ActionsType } from '@/components/Table/index.vue';
// import { getImage, LocalStore } from '@/utils/comm';
import { message } from 'ant-design-vue';
// import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { NOTICE_METHOD, MSG_TYPE } from '@/views/notice/const';
import Debug from './Debug/index.vue';
import Log from './Log/index.vue';
import { downloadObject } from '@/utils/utils';
@ -210,6 +211,8 @@ const columns = [
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: true,
ellipsis: true,
search: {
type: 'string',
},

View File

@ -82,7 +82,7 @@ export const MSG_TYPE = {
],
email: [
{
label: 'email',
label: '邮件',
value: 'embedded',
logo: getImage('/notice/email.png'),
},

View File

@ -1,12 +1,11 @@
<template>
<div class='dropdown-time-picker'>
<j-time-picker
v-if='type === "time"'
v-if='!_type'
open
class='manual-time-picker'
v-model:value='myValue'
class='manual-time-picker'
:format='myFormat'
:valueFormat='myFormat'
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ -17,7 +16,6 @@
class='manual-time-picker'
v-model:value='myValue'
:format='myFormat'
:valueFormat='myFormat'
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ -26,7 +24,7 @@
</template>
<script setup lang='ts' name='DropdownTime'>
import dayjs from 'dayjs'
import dayjs, { Dayjs } from 'dayjs'
type Emit = {
(e: 'update:value', value: string) : void
@ -44,23 +42,26 @@ const props = defineProps({
},
format: {
type: String,
default: ''
default: undefined
}
})
const emit = defineEmits<Emit>()
const myFormat = props.format || ( props.type === 'time' ? 'HH:mm:ss' : 'YYYY-MM-DD HH:mm:ss')
const myValue = ref(props.value || dayjs(new Date()).format(myFormat))
const myValue = ref<Dayjs>(dayjs(props.value || new Date(), myFormat))
const getPopupContainer = (trigger: HTMLElement) => {
return trigger?.parentNode || document.body
}
const change = (e: string) => {
myValue.value = e
emit('update:value', e)
emit('change', e)
const change = (e: Dayjs) => {
emit('update:value', e.format(myFormat))
emit('change', e.format(myFormat))
}
const _type = computed(() => {
return props.value?.includes('-')
})
</script>
<style lang='less'>

View File

@ -11,7 +11,7 @@ export const getComponent = (type: string): string => {
case 'long':
case 'float':
case 'double':
return 'number'
return type
case 'metric':
case 'enum':
case 'boolean':

View File

@ -28,7 +28,7 @@
@change='timeChange'
/>
<DropdownMenus
v-if='["select","enum", "boolean"].includes(item.component)'
v-else-if='["select","enum", "boolean"].includes(item.component)'
:options='["metric", "upper"].includes(item.key) ? metricOption : options'
@click='onSelect'
/>
@ -54,7 +54,7 @@
<ValueItem
v-else
v-model:modelValue='myValue'
:itemType='getComponent(item.component)'
:itemType='item.component'
:options='item.key === "upper" ? metricOption : options'
@change='valueItemChange'
/>

View File

@ -1738,7 +1738,6 @@ function changeBackUpload(info: UploadChangeParam<UploadFile<any>>) {
if (info.file.status === 'uploading') {
form.uploadLoading = true;
} else if (info.file.status === 'done') {
console.log(info);
info.file.url = info.file.response?.result;
form.uploadLoading = false;
@ -1749,9 +1748,6 @@ function changeBackUpload(info: UploadChangeParam<UploadFile<any>>) {
message.error('logo上传失败请稍后再试');
}
}
function test(...args: any[]) {
console.log('test:', args);
}
function clearNullProp(obj: object) {
if (typeof obj !== 'object') return;
for (const prop in obj) {
@ -1799,6 +1795,17 @@ function clearNullProp(obj: object) {
padding: 0 15px;
box-sizing: content-box;
margin-right: 20px;
color: #000;
&.ant-radio-button-wrapper-disabled {
opacity: .5;
}
&.ant-radio-button-wrapper-checked {
background-color: #fff;
border: 1px solid #1d39c4;
opacity: 1;
}
> :last-child {
width: 100%;

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="apply-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
@ -270,6 +271,8 @@ const columns = [
dataIndex: 'action',
key: 'action',
scopedSlots: true,
width:'200px',
fixed:'right'
},
];
const queryParams = ref({});

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="data-source-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

View File

@ -1,5 +1,5 @@
<template>
<a-modal
<j-modal
class="add-device-or-product-dialog-container"
title="绑定"
width="1440px"
@ -15,7 +15,7 @@
<div class="row">
<span style="margin-right: 8px">批量配置</span>
<a-switch
<j-switch
v-model:checked="bulkBool"
checked-children="开"
un-checked-children="关"
@ -23,16 +23,19 @@
/>
</div>
<div v-show="bulkBool">
<a-checkbox-group v-model:value="bulkList" :options="options" />
<j-checkbox-group v-model:value="bulkList" :options="options" />
</div>
<Search :columns="props.queryColumns" @search="query.search" />
<pro-search
:columns="props.queryColumns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
:request="table.requestFun"
:gridColumn="2"
:params="query.params.value"
:params="queryParams"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys.value,
onChange: selectRow,
@ -69,8 +72,8 @@
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<j-row>
<j-col :span="12">
<div class="card-item-content-text">ID</div>
<div
style="cursor: pointer"
@ -78,8 +81,8 @@
>
{{ slotProps.id }}
</div>
</a-col>
<a-col :span="12">
</j-col>
<j-col :span="12">
<div class="card-item-content-text">
资产权限
</div>
@ -88,15 +91,15 @@
class="card-item-content-value"
@click="(e) => e.stopPropagation()"
>
<a-checkbox-group
<j-checkbox-group
v-model:value="
slotProps.selectPermissions
"
:options="slotProps.permissionList"
/>
</div>
</a-col>
</a-row>
</j-col>
</j-row>
</template>
</CardBox>
</template>
@ -107,7 +110,7 @@
class="card-item-content-value"
@click="(e) => e.stopPropagation()"
>
<a-checkbox-group
<j-checkbox-group
v-model:value="slotProps.selectPermissions"
:options="slotProps.permissionList"
/>
@ -125,7 +128,7 @@
></BadgeStatus>
</template>
</j-pro-table>
</a-modal>
</j-modal>
</template>
<script setup lang="ts">
@ -189,58 +192,8 @@ const options = computed(() =>
const columns = props.queryColumns.filter(
(item) => item.dataIndex !== 'action',
);
const query = {
columns: [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: [
{
label: '在线',
value: 'online',
},
{
label: '离线',
value: 'offline',
},
{
label: '禁用',
value: 'notActive',
},
],
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
},
};
const queryParams = ref({});
const table: any = {
_selectedRowKeys: ref<string[]>([]), // id
backRowKeys: [] as string[], // id

View File

@ -12,7 +12,7 @@
>
<j-form ref="formRef" :model="form.data" layout="vertical">
<j-form-item name="parentId" label="上级组织">
<a-tree-select
<j-tree-select
v-model:value="form.data.parentId"
style="width: 100%"
placeholder="请选择上级组织"
@ -20,7 +20,7 @@
:field-names="{ value: 'id' }"
>
<template #title="{ name }"> {{ name }} </template>
</a-tree-select>
</j-tree-select>
</j-form-item>
<j-form-item
name="name"

View File

@ -21,7 +21,7 @@
</PermissionButton>
</div>
<a-tree
<jTree
:tree-data="treeData"
v-model:selected-keys="selectedKeys"
:fieldNames="{ key: 'id' }"
@ -69,7 +69,7 @@
</PermissionButton>
</span>
</template>
</a-tree>
</jTree>
<!-- 编辑弹窗 -->
<EditDepartmentDialog

View File

@ -1,8 +1,9 @@
<template>
<div class="product-container">
<j-advanced-search
<pro-search
:columns="columns"
@search="(params:any) => (queryParams = params)"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
@ -153,7 +154,7 @@
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<j-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
@ -165,7 +166,7 @@
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</j-space>
</template>
</j-pro-table>

View File

@ -1,7 +1,8 @@
<template>
<div class="product-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
@ -113,35 +114,35 @@
</j-row>
</template>
<template #actions="item">
<a-tooltip
<j-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-dropdown
<j-dropdown
placement="bottomRight"
v-if="item.key === 'others'"
>
<a-button>
<j-button>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
</j-button>
<template #overlay>
<a-menu>
<a-menu-item
<j-menu>
<j-menu-item
v-for="(o, i) in item.children"
:key="i"
>
<a-button
<j-button
type="link"
@click="o.onClick"
>
<AIcon :type="o.icon" />
<span>{{ o.text }}</span>
</a-button>
</a-menu-item>
</a-menu>
</j-button>
</j-menu-item>
</j-menu>
</template>
</a-dropdown>
</j-dropdown>
<PermissionButton
v-else
:hasPermission="item.permission"
@ -155,7 +156,7 @@
item.text
}}</span>
</PermissionButton>
</a-tooltip>
</j-tooltip>
</template>
</CardBox>
</template>
@ -178,7 +179,7 @@
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<j-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:hasPermission="i.permission"
@ -190,7 +191,7 @@
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</j-space>
</template>
</j-pro-table>

View File

@ -9,13 +9,17 @@
@ok="confirm"
@cancel="emits('update:visible', false)"
>
<Search :columns="query.columns" @search="query.search" />
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<div class="table">
<j-pro-table
ref="tableRef"
:columns="table.columns"
:columns="columns"
:request="table.requestFun"
:params="query.params"
:params="queryParams"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys,
onChange: table.onSelectChange,
@ -57,47 +61,28 @@ const confirm = () => {
}
};
const query = {
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
],
params: ref({}),
search: (params: any) => {
query.params.value = params;
},
};
{
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
search: {
type: 'string',
},
},
];
const queryParams = ref({});
const table = reactive({
columns: [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
],
_selectedRowKeys: [] as string[],
requestFun: async (oParams: any) => {

View File

@ -1,12 +1,15 @@
<template>
<div>
<j-advanced-search :columns="columns" @search="(p:any)=>params = p" />
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
:columns="columns"
:request="table.requestFun"
:params="params"
:params="queryParams"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys,
onChange: table.onSelectChange,
@ -137,7 +140,7 @@ const columns = [
},
];
//
const params = ref({});
const queryParams = ref({});
//
const tableRef = ref<Record<string, any>>({}); //

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="menu-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

View File

@ -1,9 +1,10 @@
<template>
<page-container>
<div class="permission-container">
<j-advanced-search
<pro-search
:columns="columns"
@search="(params:any) => (queryParams = params)"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table

View File

@ -148,7 +148,9 @@ const requestCard = reactive<tableCardType>({
return (requestCard.tableData = props.selectApi.parameters);
const schema =
props.selectApi.requestBody.content['application/json'].schema;
const schemaName = (schema.$ref || schema.items.$ref)?.split('/').pop();
const _ref = schema.$ref || schema?.items?.$ref;
if(!_ref) return; // schemaJava
const schemaName = _ref?.split('/').pop();
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {

View File

@ -95,6 +95,7 @@
</j-button>
</div>
<MonacoEditor
v-if="refStr"
v-model:modelValue="requestBody.code"
style="height: 300px; width: 100%"
theme="vs"
@ -125,6 +126,8 @@ const props = defineProps<{
selectApi: apiDetailsType;
schemas: any;
}>();
const responsesContent = ref({});
const editorRef = ref();
const formRef = ref<FormInstance>();
const requestBody = reactive({
tableColumns: [
@ -178,8 +181,16 @@ const paramsTable = computed(() => {
return requestBody.params.paramsTable.slice(startIndex, endIndex);
});
const responsesContent = ref({});
const editorRef = ref()
let schema: any = {};
const refStr = ref('');
const init = () => {
if (!props.selectApi.requestBody) return;
schema = props.selectApi.requestBody.content['application/json'].schema;
refStr.value = schema.$ref || schema?.items?.$ref;
};
init();
const send = () => {
if (paramsTable.value.length)
formRef.value &&
@ -210,10 +221,10 @@ const _send = () => {
...urlParams,
};
server[methodObj[methodName]](url, params).then((resp: any) => {
if (Object.keys(params).length === 0){
// body
if (Object.keys(params).length === 0 && refStr.value) {
requestBody.code = JSON.stringify(getDefaultParams());
editorRef.value?.editorFormat()
editorRef.value?.editorFormat();
}
responsesContent.value = resp;
});
@ -224,9 +235,8 @@ const _send = () => {
*/
function getDefaultParams() {
if (!props.selectApi.requestBody) return {};
const schema =
props.selectApi.requestBody.content['application/json'].schema;
const schemaName = (schema.$ref || schema.items.$ref)?.split('/').pop();
if (!refStr.value) return ''; // schemaJava
const schemaName = refStr.value?.split('/').pop() as string;
const type = schema.type || '';
const tableData = findData(schemaName);
if (type === 'array') {

View File

@ -11,7 +11,7 @@
<template #url="slotProps">
<span
style="color: #1d39c4; cursor: pointer"
@click="jump(slotProps)"
@click="emits('update:clickApi', slotProps)"
>{{ slotProps.url }}</span
>
</template>
@ -25,18 +25,28 @@
</template>
<script setup lang="ts">
import { addOperations_api, delOperations_api } from '@/api/system/apiPage';
import {
addOperations_api,
delOperations_api,
updateOperations_api,
} from '@/api/system/apiPage';
import { message } from 'ant-design-vue';
import { modeType } from '../typing';
const emits = defineEmits(['update:clickApi', 'update:selectedRowKeys']);
const emits = defineEmits([
'refresh',
'update:clickApi',
'update:selectedRowKeys',
'update:changedApis',
]);
const props = defineProps<{
tableData: any[];
clickApi: any;
selectedRowKeys: string[];
sourceKeys: string[];
mode: modeType;
changedApis: any; // api
}>();
const code = useRoute().query.code as string;
const columns = [
{
title: 'API',
@ -52,13 +62,20 @@ const columns = [
];
const rowSelection = {
onSelect: (record: any) => {
const targetId = record.id;
let newKeys = [...props.selectedRowKeys];
if (props.selectedRowKeys.includes(record.id)) {
newKeys = newKeys.filter((id) => id !== record.id);
} else newKeys.push(record.id);
if (props.selectedRowKeys.includes(targetId)) {
newKeys = newKeys.filter((id) => id !== targetId);
} else newKeys.push(targetId);
emits('update:selectedRowKeys', newKeys);
if (props.mode === 'appManger') {
emits('update:changedApis', {
...props.changedApis,
[record.id]: record,
});
}
},
selectedRowKeys: ref<string[]>([]),
};
@ -73,13 +90,30 @@ const save = () => {
removeKeys.length &&
delOperations_api(removeKeys)
.finally(() => addOperations_api(addKeys))
.then(() => message.success('操作成功'));
.then(() => {
message.success('操作成功');
emits('refresh')
});
} else if (props.mode === 'appManger') {
const removeItems = removeKeys.map((key) => ({
id: key,
permissions: props.changedApis[key]?.security,
}));
const addItems = addKeys.map((key) => ({
id: key,
permissions: props.changedApis[key]?.security,
}));
Promise.all([
updateOperations_api(code, '_delete', { operations: removeItems }),
updateOperations_api(code, '_add', { operations: addItems }),
]).then((resps) => {
if (resps[0].status === 200 && resps[1].status === 200) {
message.success('操作成功');
emits('refresh');
}
});
}
};
const jump = (row: any) => {
emits('update:clickApi', row);
};
watch(
() => props.selectedRowKeys,
(n) => {

View File

@ -16,7 +16,6 @@
@select="treeSelect"
:mode="props.mode"
:has-home="props.hasHome"
:filter-array="treeFilter"
:code="props.code"
/>
</j-col>
@ -26,10 +25,12 @@
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
:table-data="tableData"
v-model:selectedRowKeys="selectedKeys"
v-model:changedApis="changedApis"
:table-data="tableData"
:source-keys="selectSourceKeys"
:mode="props.mode"
@refresh="getSelectKeys"
/>
<div
@ -82,9 +83,8 @@ const props = defineProps<{
hasHome?: boolean;
code?: string;
}>();
const showHome = ref<boolean>(Boolean(props.hasHome));
const showHome = ref<boolean>(Boolean(props.hasHome)); // home
const tableData = ref([]);
const treeFilter = ref([]);
const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
if (node.key === 'home') return (showHome.value = true);
schemas.value = nodeSchemas;
@ -110,7 +110,7 @@ const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
};
const activeKey = ref<'does' | 'test'>('does');
const schemas = ref({});
const schemas = ref({}); // api
const initSelectedApi: apiDetailsType = {
url: '',
method: '',
@ -121,23 +121,14 @@ const initSelectedApi: apiDetailsType = {
};
const selectedApi = ref<apiDetailsType>(initSelectedApi);
const selectedKeys = ref<string[]>([]); //
let selectSourceKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]); //
const selectSourceKeys = ref<string[]>([]); //
const changedApis = ref({}); // idkey
init();
function init() {
//
if (props.mode === 'appManger') {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
}
getSelectKeys();
watch(tableData, () => {
activeKey.value = 'does';
selectedApi.value = initSelectedApi;
@ -147,6 +138,24 @@ function init() {
() => (activeKey.value = 'does'),
);
}
/**
* 右侧api选中项
*/
function getSelectKeys() {
if (props.mode === 'appManger') {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
changedApis.value = {};
});
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
}
}
</script>
<style lang="less" scoped>

View File

@ -24,5 +24,9 @@ export type apiDetailsType = {
responses:object;
description?:string;
}
/**
* api: api配置
* appManger -
* home-
*/
export type modeType = 'api'| 'appManger' | 'home'

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="relationship-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

View File

@ -1,7 +1,8 @@
<template>
<div class="role-user-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

View File

@ -6,10 +6,15 @@
@ok="confirm"
@cancel="emits('update:visible', false)"
>
<j-advanced-search
<!-- <j-advanced-search
:columns="columns"
type="simple"
@search="(params:any)=>queryParams = {...params}"
/> -->
<pro-search
:columns="columns"
target="simple"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
@ -35,7 +40,7 @@ import { message } from 'ant-design-vue';
const emits = defineEmits(['refresh', 'update:visible']);
const props = defineProps<{
visible: boolean;
roleId: string
roleId: string;
}>();
const columns = [
@ -85,15 +90,13 @@ const confirm = () => {
if (selectedRowKeys.value.length < 1) {
message.error('请至少选择一项');
} else {
bindUser_api(props.roleId, selectedRowKeys.value).then(
(resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('refresh');
emits('update:visible', false);
}
},
);
bindUser_api(props.roleId, selectedRowKeys.value).then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
emits('refresh');
emits('update:visible', false);
}
});
}
};
</script>

View File

@ -1,9 +1,10 @@
<template>
<page-container>
<div class="role-container">
<j-advanced-search
<pro-search
:columns="columns"
@search="(params:any)=>queryParams = params"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table

View File

@ -1,8 +1,9 @@
<template>
<page-container>
<div class="user-container">
<j-advanced-search
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
/>

1
src/vite-env.d.ts vendored
View File

@ -1,6 +1,7 @@
interface ImportMetaEnv {
readonly VITE_APP_BASE_API: string;
readonly VITE_APP_WS_URL: string;
readonly MODE: string;
}
interface ImportMeta {

View File

@ -21,6 +21,7 @@
"layouts/*": ["./src/layouts/*"],
"store/*": ["./src/store/*"],
"style/*": ["./src/style/*"],
"jetlinks-ui-components/es": ["./node_modules/jetlinks-ui-components/es/*"]
},
"types": ["ant-design-vue/typings/global", "vite/client"],
"suppressImplicitAnyIndexErrors": true

View File

@ -12,12 +12,13 @@ import * as path from 'path'
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
// import { JetlinksVueResolver } from 'jetlinks-ui-components/lib/plugin/resolve'
import { JetlinksVueResolver } from './plugin/jetlinks'
import { optimizeDeps } from './plugin/optimize'
import copy from 'rollup-plugin-copy';
// https://vitejs.dev/config/
export default defineConfig(({ mode}) => {
const env: Partial<ImportMetaEnv> = loadEnv(mode, process.cwd());
return {
base: './',
resolve: {
@ -53,6 +54,7 @@ export default defineConfig(({ mode}) => {
vue(),
monacoEditorPlugin({}),
vueJsx(),
optimizeDeps(),
Components({
resolvers: [JetlinksVueResolver({ importStyle: 'less' }), VueAmapResolver()],
directoryAsNamespace: true
@ -110,6 +112,9 @@ export default defineConfig(({ mode}) => {
javascriptEnabled: true,
}
}
},
optimizeDeps: {
include: ['pinia', 'vue-router', 'axios', 'lodash-es', '@vueuse/core', 'echarts', 'dayjs'],
}
}
})

11055
yarn.lock

File diff suppressed because it is too large Load Diff