Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-03-24 18:05:22 +08:00
commit af8f951ed8
37 changed files with 1103 additions and 745 deletions

View File

@ -164,5 +164,6 @@ function syncTriggerClass(
.j-ellipsis-line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
word-break: break-all;
}
</style>

View File

@ -45,6 +45,7 @@ initAMapApiLoader({
interface EmitProps {
(e: 'update:point', data: string): void;
(e: 'change', data: string): void;
}
const props = defineProps({
point: { type: [Number, String], default: '' },
@ -59,6 +60,7 @@ const inputPoint = computed({
set: (val: any) => {
mapPoint.value = val;
emit('update:point', val);
emit('change', val);
},
});

View File

@ -1,7 +1,7 @@
<template>
<template v-if="isPermission">
<template v-if="popConfirm">
<j-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<j-popconfirm :overlayStyle='{minWidth: "180px"}' v-bind="popConfirm" :disabled="!isPermission || props.disabled">
<j-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">

View File

@ -46,6 +46,7 @@
<GeoComponent
v-else-if="typeMap.get(itemType) === 'geoPoint'"
v-model:point="myValue"
@change='inputChange'
/>
<j-input
v-else-if="typeMap.get(itemType) === 'file'"
@ -172,6 +173,7 @@ const objectValue = ref<string>('');
const handleItemModalSubmit = () => {
myValue.value = objectValue.value.replace(/[\r\n]\s*/g, '');
modalVis.value = false;
emit('change', objectValue.value)
};
//

View File

@ -120,7 +120,7 @@ export interface SearchItemData {
export const handleParamsToString = (terms:SearchItemData[] = []) => {
const _terms: any[] = [
{ terms: [null,null,null]},
{ terms: [null,null,null]}
{ terms: [null,null,null], type: 'and'}
]
let termsIndex = 0
let termsStar = 0

View File

@ -1,22 +1,22 @@
import axios from 'axios'
import {BASE_API_PATH, TOKEN_KEY} from '@/utils/variable'
import { notification as Notification } from 'ant-design-vue'
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable'
import { notification as Notification } from 'ant-design-vue'
import router from '@/router'
import { LoginPath } from '@/router/menu'
import {LocalStore} from "@/utils/comm";
import { cleanToken, getToken, LocalStore } from '@/utils/comm'
import type { AxiosInstance, AxiosResponse } from 'axios'
interface AxiosResponseRewrite<T = any[]> extends AxiosResponse<T, any> {
result: T
success: boolean
result: T
success: boolean
}
export const SUCCESS_CODE = 200 // 成功代码
export const request = axios.create({
withCredentials: false,
baseURL: BASE_API_PATH,
timeout: 1000 * 60 * 5
withCredentials: false,
baseURL: BASE_API_PATH,
timeout: 1000 * 60 * 5
})
/**
@ -30,15 +30,15 @@ export const request = axios.create({
}
* @returns {AxiosInstance}
*/
export const post = function<T>(url: string, data = {}, params = {}, ext={}) {
ext = typeof ext === 'string' ? { responseType: ext } : ext
return request<any, AxiosResponseRewrite<T>>({
...ext,
params,
method: 'POST',
url,
data
})
export const post = function <T>(url: string, data = {}, params = {}, ext = {}) {
ext = typeof ext === 'string' ? { responseType: ext } : ext
return request<any, AxiosResponseRewrite<T>>({
...ext,
params,
method: 'POST',
url,
data
})
}
/**
@ -47,12 +47,12 @@ export const post = function<T>(url: string, data = {}, params = {}, ext={}) {
* @param {Object} [data]
* @returns {AxiosInstance}
*/
export const put = function<T>(url: string, data = {},) {
return request<any, AxiosResponseRewrite<T>>({
method: 'PUT',
url,
data
})
export const put = function <T>(url: string, data = {}) {
return request<any, AxiosResponseRewrite<T>>({
method: 'PUT',
url,
data
})
}
/**
@ -61,12 +61,12 @@ export const put = function<T>(url: string, data = {},) {
* @param {Object} [data]
* @returns {AxiosInstance}
*/
export const patch = function<T>(url: string, data = {}) {
return request<any, AxiosResponseRewrite<T>>({
method: 'PATCH',
url,
data
})
export const patch = function <T>(url: string, data = {}) {
return request<any, AxiosResponseRewrite<T>>({
method: 'PATCH',
url,
data
})
}
/**
* GET method request
@ -75,13 +75,13 @@ export const patch = function<T>(url: string, data = {}) {
* @param {Object} [ext]
* @returns {AxiosInstance}
*/
export const get = function<T>(url: string, params = {}, ext?: any) {
return request<any, AxiosResponseRewrite<T>>({
method: 'GET',
url,
params,
...ext
})
export const get = function <T>(url: string, params = {}, ext?: any) {
return request<any, AxiosResponseRewrite<T>>({
method: 'GET',
url,
params,
...ext
})
}
/**
@ -91,13 +91,13 @@ export const get = function<T>(url: string, params = {}, ext?: any) {
* @param {Object} [ext]
* @returns {AxiosInstance}
*/
export const remove = function<T>(url: string, params = {}, ext?: any) {
return request<any, AxiosResponseRewrite<T>>({
method: 'DELETE',
url,
params,
...ext
})
export const remove = function <T>(url: string, params = {}, ext?: any) {
return request<any, AxiosResponseRewrite<T>>({
method: 'DELETE',
url,
params,
...ext
})
}
/**
@ -107,25 +107,25 @@ export const remove = function<T>(url: string, params = {}, ext?: any) {
* @return {*}
*/
export const getStream = function(url: string, params = {}) {
return get<any>(url, params, {
responseType: 'arraybuffer' // 设置请求数据类型返回blob可解析类型
})
return get<any>(url, params, {
responseType: 'arraybuffer' // 设置请求数据类型返回blob可解析类型
})
}
export const postStream = function(url: string, data={}, params = {}) {
return post<any>(url, data, params, {
responseType: 'arraybuffer' // 设置请求数据类型返回blob可解析类型
})
export const postStream = function(url: string, data = {}, params = {}) {
return post<any>(url, data, params, {
responseType: 'arraybuffer' // 设置请求数据类型返回blob可解析类型
})
}
const showNotification = ( message: string, description: string, key?: string, show: boolean = true ) => {
if (show) {
Notification.error({
key,
message,
description
})
}
const showNotification = (message: string, description: string, key?: string, show: boolean = true) => {
if (show) {
Notification.error({
key,
message,
description
})
}
}
/**
@ -134,87 +134,79 @@ const showNotification = ( message: string, description: string, key?: string, s
* @returns {Promise<never>}
*/
const errorHandler = (error: any) => {
if (error.response) {
const data = error.response.data
const status = error.response.status
if (status === 403) {
showNotification( 'Forbidden', (data.message + '').substr(0, 90), '403')
} else if (status === 500) {
showNotification( 'Server Side Error', (data.message + '').substr(0, 90), '500')
} else if (status === 400) {
showNotification( 'Request Error', (data.message + '').substr(0, 90), '400')
} else if (status === 401) {
showNotification( 'Unauthorized', '用户未登录', '401')
console.log('showNotification')
setTimeout(() => {
location.href = `/#${LoginPath}`
}, 0)
}
} else if (error.response === undefined) {
showNotification( error.message, (error.stack + '').substr(0, 90), undefined)
if (error.response) {
const data = error.response.data
const status = error.response.status
if (status === 403) {
showNotification('Forbidden', (data.message + '').substr(0, 90), '403')
} else if (status === 500) {
showNotification('Server Side Error', (data.message + '').substr(0, 90), '500')
} else if (status === 400) {
showNotification('Request Error', (data.message + '').substr(0, 90), '400')
} else if (status === 401) {
showNotification('Unauthorized', '用户未登录', '401')
setTimeout(() => {
cleanToken()
router.replace({
path: LoginPath
})
}, 0)
}
return Promise.reject(error)
} else if (error.response === undefined) {
showNotification(error.message, (error.stack + '').substr(0, 90), undefined)
}
return Promise.reject(error)
}
// request interceptor
request.interceptors.request.use(config => {
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
const token = LocalStore.get(TOKEN_KEY)
// const token = store.$state.tokenAlias
if (!token) {
// setTimeout(() => {
// router.replace({
// path: LoginPath
// })
// }, 0)
setTimeout(() => {
location.href = `/#${LoginPath}`
}, 0)
return config
}
config.headers![TOKEN_KEY] = token
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
const token = getToken()
if (!token) {
setTimeout(() => {
cleanToken()
router.replace({
path: LoginPath
})
}, 0)
return config
}
config.headers![TOKEN_KEY] = token
return config
}, errorHandler)
/**
* response interceptor
*/
request.interceptors.response.use(response => {
if (response.data instanceof ArrayBuffer) {
return response
} else {
const { status, message } = response.data
// 增加业务接口处理成功判断方式只需要判断返回参数包含success为true
if (typeof response.data === 'object' && typeof response.data.success === 'undefined') {
response.data.success = status === SUCCESS_CODE
}
// 统一显示异常业务信息
if (status !== SUCCESS_CODE && message) {
Notification.error({
message: 'Server Errors: ' + status,
description: message
})
}
// 如果返回的的是文件流那么return值则为response
if (response.headers['content-type'] === 'application/octet-stream; charset=UTF-8' || response.headers['content-type'] === 'application/vnd.ms-excel;charset=UTF-8') {
return response.data
} else {
return response.data
}
if (response.data instanceof ArrayBuffer) {
return response
} else {
const { status, message } = response.data
// 增加业务接口处理成功判断方式只需要判断返回参数包含success为true
if (typeof response.data === 'object' && typeof response.data.success === 'undefined') {
response.data.success = status === SUCCESS_CODE
}
// 如果返回的的是文件流那么return值则为response
if (response.headers['content-type'] === 'application/octet-stream; charset=UTF-8' || response.headers['content-type'] === 'application/vnd.ms-excel;charset=UTF-8') {
return response.data
} else {
return response.data
}
}
}, errorHandler)
export default {
request: axios,
post,
get,
patch,
put,
remove,
getStream,
postStream
request: axios,
post,
get,
patch,
put,
remove,
getStream,
postStream
}

View File

@ -84,6 +84,7 @@ import TopCard from '@/views/device/DashBoard/components/TopCard.vue';
import { useMenuStore } from '@/store/menu';
import Amap from './components/Amap.vue';
import { useSystem } from '@/store/system';
import dayjs from 'dayjs'
const system = useSystem();
const AmapKey = system.$state.configInfo.amap?.apiKey;
let productTotal = ref(0);
@ -206,6 +207,9 @@ getDeviceData();
* 获取在线数量
*/
const getOnline = () => {
const startTime = dayjs().subtract(0, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().subtract(0, 'days').endOf('day').format('YYYY-MM-DD HH:mm:ss');
dashboard([
{
dashboard: 'device',
@ -215,26 +219,34 @@ const getOnline = () => {
group: 'aggOnline',
params: {
state: 'online',
limit: 15,
from: 'now-15d',
time: '1d',
format: 'yyyy-MM-dd',
limit: 24,
from: startTime,
to: endTime,
time: '1h',
format: 'yyyy-MM-dd HH:mm:ss',
},
},
]).then((res) => {
if (res.status == 200) {
const x = res.result
.map((item: any) => item.data.timeString)
.reverse();
const y = res.result.map((item: any) => item.data.value);
// const x = res.result
// .map((item: any) => item.data.timeString)
// .reverse();
// const y = res.result.map((item: any) => item.data.value);
const x: string[] = [];
const y: number[] = [];
(res.result as any)?.forEach((item: any) => {
x.push(item.data.timeString)
y.push(item.data.value)
})
x.reverse()
const onlineYdata = y;
onlineYdata.reverse();
setOnlineChartOpition(x, onlineYdata);
setOnlineChartOption(x, onlineYdata);
onlineFooter.value[0].value = y?.[1];
}
});
};
const setOnlineChartOpition = (x: Array<any>, y: Array<number>): void => {
const setOnlineChartOption = (x: Array<any>, y: Array<number>): void => {
onlineOptions.value = {
xAxis: {
type: 'category',

View File

@ -61,26 +61,10 @@
"
:itemType="record.type"
:options="
record.type === 'enum'
? (
record?.options
?.elements || []
).map((item:any) => ({
label: item.text,
value: item.value,
}))
: record.type === 'boolean'
? [
{
label: '是',
value: true,
},
{
label: '否',
value: false,
},
]
: undefined
(record?.options || []).map((item:any) => ({
label: item.text,
value: item.value,
}))
"
/>
</j-form-item>
@ -107,7 +91,10 @@
</j-col>
<j-col :span="9">
<h6>执行结果</h6>
<span :ref="`result${func.id}Ref`" class="execute-result">
<span
:ref="`result${func.id}Ref`"
class="execute-result"
>
{{ func.executeResult }}
</span>
</j-col>
@ -192,7 +179,7 @@ const newFunctions = computed(() => {
executeResult: '',
});
});
// console.log('newFunctions: ', result)
// console.log('newFunctions: ', result);
return result;
});

View File

@ -34,8 +34,8 @@
)
: record.type === 'boolean'
? [
{ label: '是', value: true },
{ label: '否', value: false },
{ label: record?.dataType?.trueText, value: record?.dataType?.trueValue },
{ label: record?.dataType?.falseText, value: record?.dataType?.falseValue },
]
: undefined
"

View File

@ -15,7 +15,7 @@
</j-select>
</j-form-item>
<j-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
<j-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></j-select>
<j-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label" showSearch></j-select>
</j-form-item>
<j-form-item label="物模型类型" v-bind="validateInfos.metadata" v-if="type === 'device' || formModel.type === 'import'">
<j-select v-model:value="formModel.metadata">
@ -209,19 +209,20 @@ const handleImport = async () => {
const res = await convertMetadata('from', 'alink', data.import).catch(err => err)
if (res.status === 200) {
const metadata = operateLimits(res.result)
let result;
if (props?.type === 'device') {
await saveMetadata(id as string, metadata).catch(err => err)
// instanceStore.setCurrent(JSON.parse(metadata || '{}'))
result = await saveMetadata(id as string, metadata).catch(err => err)
} else {
await modify(id as string, { metadata: metadata }).catch(err => err)
// productStore.setCurrent(JSON.parse(metadata || '{}'))
result = await modify(id as string, { metadata: metadata }).catch(err => err)
}
if (result.success) {
message.success('导入成功')
}
loading.value = false
// MetadataAction.insert(JSON.parse(metadata || '{}'));
message.success('导入成功')
} else {
loading.value = false
message.error('发生错误!')
// message.error('!')
return
}
if (props?.type === 'device') {
instanceStore.refresh(id as string)

View File

@ -176,7 +176,7 @@
<div class="card-item-content-text">
绑定设备
</div>
<div>{{ slotProps.deviceName }}</div>
<Ellipsis>{{ slotProps.deviceName }}</Ellipsis>
</j-col>
</j-row>
<j-divider style="margin: 12px 0" />

View File

@ -25,7 +25,14 @@
<div class="tool-item" @click.stop="handleRefresh">
刷新
</div>
<div class="tool-item" @click.stop="handleReset">重置</div>
<div class="tool-item">
<j-popconfirm
title="重置将断开直播, 可能会影响其他播放者"
@confirm="() => handleReset"
>
重置
</j-popconfirm>
</div>
</div>
<LivePlayer
ref="player"

View File

@ -156,11 +156,11 @@
{{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span>
</template>
<!-- <template #description="slotProps">
<template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
</template>
<template #action="slotProps">
<j-space :size="16">
<template
@ -194,7 +194,7 @@
<script setup lang="ts">
import ConfigApi from '@/api/notice/config';
import type { ActionsType } from '@/components/Table/index.vue';
import type { ActionsType } from '@/views/device/Instance/typings';
import { message } from 'jetlinks-ui-components';
@ -380,18 +380,6 @@ const getActions = (
currentConfig.value = data;
},
},
{
key: 'log',
text: '通知记录',
tooltip: {
title: '通知记录',
},
icon: 'BarsOutlined',
onClick: () => {
logVis.value = true;
currentConfig.value = data;
},
},
{
key: 'delete',
text: '删除',
@ -439,6 +427,18 @@ const getActions = (
currentConfig.value = data;
},
},
{
key: 'log',
text: '通知记录',
tooltip: {
title: '通知记录',
},
icon: 'BarsOutlined',
onClick: () => {
logVis.value = true;
currentConfig.value = data;
},
},
],
};
@ -447,7 +447,7 @@ const getActions = (
data.provider !== 'dingTalkMessage' &&
data.provider !== 'corpMessage'
)
others.children.splice(1, 1);
others.children?.splice(1, 1);
actions.splice(actions.length - 1, 0, others);
return actions;
} else {
@ -455,7 +455,7 @@ const getActions = (
data.provider !== 'dingTalkMessage' &&
data.provider !== 'corpMessage'
)
others.children.splice(1, 1);
others.children?.splice(1, 1);
actions.splice(actions.length - 1, 0, ...others.children);
return actions;
}

View File

@ -90,24 +90,65 @@
</j-row>
</template>
<template #actions="item">
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'notice/Template:' + item.key"
<j-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<j-dropdown
placement="bottomRight"
v-if="item.key === 'others'"
>
<j-button>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</j-button>
<template #overlay>
<j-menu>
<j-menu-item
v-for="(o, i) in item.children"
:key="i"
>
<PermissionButton
type="link"
@click="o.onClick"
:hasPermission="`notice/Template:${o.key}`"
>
<template #icon>
<AIcon :type="o.icon" />
</template>
<span>{{ o.text }}</span>
</PermissionButton>
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
<j-popconfirm
v-else-if="item.key === 'delete'"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<PermissionButton
:disabled="item.disabled"
:hasPermission="`notice/Template:${item.key}`"
>
<template #icon>
<AIcon type="DeleteOutlined" />
</template>
</PermissionButton>
</j-popconfirm>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
<PermissionButton
:disabled="item.disabled"
@click="item.onClick"
:hasPermission="`notice/Template:${item.key}`"
>
<template #icon>
<AIcon :type="item.icon" />
</template>
<span>{{ item.text }}</span>
</PermissionButton>
</template>
</PermissionButton>
</j-tooltip>
</template>
</CardBox>
</template>
@ -119,11 +160,11 @@
{{ getProviderTxt(slotProps.type, slotProps.provider) }}
</span>
</template>
<!-- <template #description="slotProps">
<template #description="slotProps">
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</template> -->
</template>
<template #action="slotProps">
<j-space :size="16">
<template
@ -340,29 +381,6 @@ const getActions = (
currentConfig.value = data;
},
},
{
key: 'export',
text: '导出',
tooltip: {
title: '导出',
},
icon: 'ArrowDownOutlined',
onClick: () => {
downloadObject(data, `通知配置`);
},
},
{
key: 'log',
text: '通知记录',
tooltip: {
title: '通知记录',
},
icon: 'BarsOutlined',
onClick: () => {
logVis.value = true;
currentConfig.value = data;
},
},
{
key: 'delete',
text: '删除',
@ -381,6 +399,41 @@ const getActions = (
icon: 'DeleteOutlined',
},
];
const others: ActionsType = {
key: 'others',
text: '其他',
icon: 'EllipsisOutlined',
children: [
{
key: 'export',
text: '导出',
tooltip: {
title: '导出',
},
icon: 'ArrowDownOutlined',
onClick: () => {
downloadObject(data, `通知配置`);
},
},
{
key: 'log',
text: '通知记录',
tooltip: {
title: '通知记录',
},
icon: 'BarsOutlined',
onClick: () => {
logVis.value = true;
currentConfig.value = data;
},
},
],
};
type === 'card'
? actions.splice(actions.length - 1, 0, others)
: actions.splice(actions.length - 1, 0, ...others.children);
return actions;
};
</script>

View File

@ -8,8 +8,8 @@
<j-col :span="12">
<j-form-item
name="properties"
label="读取属性"
:rules="[{ required: true, message: '请选择读取属性' }]"
label="设置属性"
:rules="[{ required: true, message: '请选择设置属性' }]"
>
<j-select
showSearch
@ -18,7 +18,7 @@
@change="onChange"
>
<j-select-option
v-for="item in metadata?.properties || []"
v-for="item in (metadata?.properties || [])?.filter(i => i?.expands?.type?.includes('write')) || []"
:value="item?.id"
:key="item?.id"
>{{ item?.name }}</j-select-option
@ -33,7 +33,6 @@
:rules="[{ required: true, message: '请选择' }]"
>
<ParamsDropdown
placeholder="请选择"
:options="handleOptions"
:tabsOptions="tabOptions"
:metricOptions="upperOptions"
@ -42,7 +41,7 @@
@select="onValueChange"
>
<template v-slot="{ label }">
<j-input :value="label" />
<j-input :value="label" placeholder="请选择" />
</template>
</ParamsDropdown>
</j-form-item>
@ -73,7 +72,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['update:value']);
const emit = defineEmits(['update:value', 'change']);
const propertyFormRef = ref();
@ -83,6 +82,7 @@ const propertyModelRef = reactive({
source: 'fixed',
});
const getType = computed(() => {
return props.metadata.properties.find(
(item: any) => item.id === propertyModelRef.properties,
@ -154,7 +154,7 @@ const handleOptions = computed(() => {
const onChange = () => {
propertyModelRef.propertiesValue = undefined;
propertyModelRef.source = 'fixed'
propertyModelRef.source = 'fixed';
emit('update:value', {
[`${propertyModelRef.properties}`]: {
value: propertyModelRef?.propertiesValue,
@ -169,8 +169,9 @@ const onValueChange = () => {
value: propertyModelRef?.propertiesValue,
source: propertyModelRef?.source,
},
}
};
emit('update:value', obj);
emit('change', propertyModelRef?.propertiesValue)
};
watch(
@ -187,4 +188,19 @@ watch(
},
{ deep: true, immediate: true },
);
const onSave = () => {
return new Promise((resolve, reject) => {
propertyFormRef.value
.validate()
.then(() => {
resolve(true);
})
.catch((err: any) => {
reject(err);
});
});
};
defineExpose({ onSave });
</script>

View File

@ -22,7 +22,7 @@
showSearch
placeholder="请选择功能"
v-model:value="modelRef.message.functionId"
@select='functionSelect'
@select="functionSelect"
>
<j-select-option
v-for="item in metadata?.functions || []"
@ -56,7 +56,9 @@
v-model:value="modelRef.message.properties[0]"
>
<j-select-option
v-for="item in metadata?.properties || []"
v-for="item in metadata?.properties.filter((i) =>
i?.expands?.type?.includes('read'),
) || []"
:value="item?.id"
:key="item?.id"
>{{ item?.name }}</j-select-option
@ -66,9 +68,11 @@
</template>
<template v-else-if="deviceMessageType === 'WRITE_PROPERTY'">
<WriteProperty
ref="writeFormRef"
v-model:value="modelRef.message.properties"
:metadata="metadata"
:builtInList="builtInList"
@change="onWriteChange"
/>
</template>
</j-form>
@ -83,7 +87,7 @@ import EditTable from './EditTable.vue';
import WriteProperty from './WriteProperty.vue';
import { useSceneStore } from '@/store/scene';
import { storeToRefs } from 'pinia';
import { getParams } from '../../../util'
import { getParams } from '../../../util';
const sceneStore = useSceneStore();
const { data } = storeToRefs(sceneStore);
@ -137,26 +141,39 @@ const modelRef = reactive({
properties: undefined,
inputs: [],
},
propertiesValue: '',
});
const functionSelect = () => {
modelRef.message.inputs = []
}
const writeFormRef = ref();
const functionRules = [{
validator(_: string, value: any) {
if (!value?.length && functions.value.length) {
return Promise.reject('请输入功能值')
} else {
const hasValue = value.find((item: { name: string, value: any}) => !item.value)
if (hasValue) {
const functionItem = functions.value.find((item: any) => item.id === hasValue.name)
return Promise.reject(functionItem?.name ? `请输入${functionItem.name}` : '请输入功能值')
}
}
return Promise.resolve();
}
}]
const functionSelect = () => {
modelRef.message.inputs = [];
};
const functionRules = [
{
validator(_: string, value: any) {
if (!value?.length && functions.value.length) {
return Promise.reject('请输入功能值');
} else {
const hasValue = value.find(
(item: { name: string; value: any }) => !item.value,
);
if (hasValue) {
const functionItem = functions.value.find(
(item: any) => item.id === hasValue.name,
);
return Promise.reject(
functionItem?.name
? `请输入${functionItem.name}`
: '请输入功能值',
);
}
}
return Promise.resolve();
},
},
];
const metadata = ref<{
functions: any[];
@ -205,15 +222,15 @@ const queryBuiltIn = async () => {
action: props.name - 1,
};
const _data = await getParams(_params, unref(data));
builtInList.value = _data
builtInList.value = _data;
};
const onMessageTypeChange = (val: string) => {
const flag = ['WRITE_PROPERTY', 'INVOKE_FUNCTION'].includes(val)
const flag = ['WRITE_PROPERTY', 'INVOKE_FUNCTION'].includes(val);
modelRef.message = {
messageType: val,
functionId: undefined,
properties:(flag ? undefined : []) as any,
properties: (flag ? undefined : []) as any,
inputs: [],
};
if (flag) {
@ -251,7 +268,11 @@ watch(
(newVal) => {
if (newVal?.messageType) {
modelRef.message = newVal;
if (['WRITE_PROPERTY', 'INVOKE_FUNCTION'].includes(newVal.messageType)) {
if (
['WRITE_PROPERTY', 'INVOKE_FUNCTION'].includes(
newVal.messageType,
)
) {
queryBuiltIn();
}
}
@ -259,11 +280,21 @@ watch(
{ immediate: true },
);
const onWriteChange = (val: string) => {
modelRef.propertiesValue = val;
};
const onFormSave = () => {
return new Promise((resolve, reject) => {
formRef.value
.validate()
.then((_data: any) => {
.then(async (_data: any) => {
if (writeFormRef.value) {
const _val = await writeFormRef.value?.onSave();
if (!_val) {
reject(false);
}
}
//
const obj = {
message: {
@ -273,6 +304,7 @@ const onFormSave = () => {
deviceMessageType.value === 'INVOKE_FUNCTION'
? _function.value?.name
: _property.value?.name,
propertiesValue: modelRef.propertiesValue,
},
};
resolve(obj);

View File

@ -77,12 +77,12 @@ type Emit = {
const actionRef = ref();
const params = ref({
terms: []
terms: [],
});
const props = defineProps({
value: {
type: Array as PropType<any>,
default: []
default: [],
},
detail: {
type: Object,
@ -90,8 +90,8 @@ const props = defineProps({
},
productId: {
type: String,
default: ''
}
default: '',
},
});
const emit = defineEmits<Emit>();
@ -143,9 +143,9 @@ const handleSearch = (p: any) => {
...p,
terms: [
...p.terms,
{terms: [{ column: 'productId', value: props?.productId }]}
]
}
{ terms: [{ column: 'productId', value: props?.productId }] },
],
};
};
const deviceQuery = (p: any) => {
@ -162,19 +162,26 @@ const deviceQuery = (p: any) => {
};
const handleClick = (detail: any) => {
emit('update:value', [{ value: detail.id, name: detail.name }]);
emit('change', detail);
if (props.value?.[0]?.value === detail.id) {
emit('update:value', undefined);
emit('change', {});
} else {
emit('update:value', [{ value: detail.id, name: detail.name }]);
emit('change', detail);
}
};
watchEffect(() => {
params.value = {
...params.value,
terms: params.value?.terms ? [
...(params.value.terms || []),
{terms: [{ column: 'productId', value: props?.productId }]}
] : [{terms: [{ column: 'productId', value: props?.productId }]}]
}
})
terms: params.value?.terms
? [
...(params.value.terms || []),
{ terms: [{ column: 'productId', value: props?.productId }] },
]
: [{ terms: [{ column: 'productId', value: props?.productId }] }],
};
});
</script>
<style scoped lang='less'>

View File

@ -13,10 +13,12 @@
<div class="'way-item-title">
<span class="way-item-label">{{ item.label }}</span>
<j-popover v-if="item.tip">
<AIcon
type="QuestionCircleOutlined"
class="way-item-icon"
/>
<j-tooltip :title="item.tip">
<AIcon
type="QuestionCircleOutlined"
class="way-item-icon"
/>
</j-tooltip>
</j-popover>
</div>
<div class="way-item-image">
@ -54,7 +56,7 @@ const _value = ref(props?.value || '');
watch(
() => props.value,
(newValue) => {
_value.value = newValue || ""
_value.value = newValue || '';
},
{ immediate: true, deep: true },
);
@ -63,7 +65,7 @@ const onSelect = (_type: string) => {
if (!props.disabled) {
_value.value = _type;
emits('update:value', _type);
emits('change', _type)
emits('change', _type);
}
};
</script>

View File

@ -13,17 +13,18 @@
@change="onSelectorChange"
/>
</j-form-item>
<j-form-item
<!-- <j-form-item
v-if="modelRef.selector === 'fixed'"
name="selectorValues"
:rules="[{ required: true, message: '请选择设备' }]"
>
<Device
:productId="values.productDetail.id"
v-model:value="modelRef.selectorValues"
@change="onDeviceChange"
/>
</j-form-item>
> -->
<Device
v-if="modelRef.selector === 'fixed'"
:productId="values.productDetail.id"
v-model:value="modelRef.selectorValues"
@change="onDeviceChange"
/>
<!-- </j-form-item> -->
<j-form-item
v-else-if="modelRef.selector === 'relation'"
label="关系"
@ -78,7 +79,7 @@
import { useSceneStore } from '@/store/scene';
import TopCard from './TopCard.vue';
import { storeToRefs } from 'pinia';
import { getImage } from '@/utils/comm';
import { getImage, onlyMessage } from '@/utils/comm';
import NoticeApi from '@/api/notice/config';
import Device from './Device.vue';
import Tag from './Tag.vue';
@ -318,7 +319,19 @@ const onFormSave = () => {
formRef.value
.validate()
.then(async (_data: any) => {
resolve(_data);
if(modelRef.selector === 'fixed'){
if(!modelRef?.selectorValues?.[0]?.value){
onlyMessage('请选择设备', 'error')
reject(false);
} else {
resolve({
..._data,
selectorValues: modelRef.selectorValues
});
}
} else {
resolve(_data);
}
})
.catch((err: any) => {
reject(err);

View File

@ -24,6 +24,7 @@
v-if="current === 0"
v-model:rowKey="DeviceModel.productId"
v-model:detail="DeviceModel.productDetail"
@change="onProductChange"
/>
<Device
v-else-if="current === 1"
@ -173,9 +174,9 @@ const onSave = (_data: any) => {
_options.type = '设置';
_options.properties = _data.message.propertiesName;
_options.propertiesValue =
typeof DeviceModel.propertiesValue === 'object'
? JSON.stringify(DeviceModel.propertiesValue)
: `${DeviceModel.propertiesValue}`;
typeof _data.message.propertiesValue === 'object'
? JSON.stringify(_data.message.propertiesValue)
: `${_data.message.propertiesValue}`;
_options.columns = DeviceModel.columns;
_options.otherColumns = DeviceModel.columns;
const cur: any = Object.values(_data.message.properties)?.[0];
@ -184,13 +185,6 @@ const onSave = (_data: any) => {
}
}
if (_options.selector === 'tag') {
// const arr = _data.map((item: any) => {
// return {
// column: item.name,
// type: item.type,
// value: item.value,
// };
// });
_options.taglist = DeviceModel.tagList.map((it) => ({
name: it.column || it.name,
type: it.type ? (it.type === 'and' ? '并且' : '或者') : '',
@ -203,6 +197,11 @@ const onSave = (_data: any) => {
emit('save', item, _options);
};
const onProductChange = () => {
DeviceModel.selectorValues = undefined
DeviceModel.message = {}
}
const save = async (step?: number) => {
let _step = step !== undefined ? step : current.value;
if (_step === 0) {
@ -213,8 +212,10 @@ const save = async (step?: number) => {
if (deviceRef.value) {
await deviceRef.value?.onFormSave();
current.value = 2;
} else if (DeviceModel.selectorValues.length) {
} else if (DeviceModel.selectorValues?.length) {
current.value = 2;
} else {
onlyMessage('请选择设备', 'error')
}
} else {
if (actionRef.value) {

View File

@ -18,7 +18,7 @@
},
]"
>
<CardSelect v-model:value="formModel.type" :options="options"/>
<CardSelect v-model:value="formModel.type" :options="options.filter(item => !(item.value === 'delay' && parallel))"/>
</j-form-item>
<ActionTypeComponent
v-bind="props"
@ -69,36 +69,36 @@ const props = defineProps({
const emit = defineEmits(['cancel', 'save']);
const options = [
{
label: '设备输出',
value: 'device',
iconUrl: getImage('/scene/device-type.png'),
subLabel: '配置设备调用功能、读取属性、设置属性规则',
},
{
label: '消息通知',
value: 'notify',
iconUrl: getImage('/scene/message-type.png'),
subLabel: '配置向指定用户发邮件、钉钉、微信、短信等通知',
},
{
label: '延迟执行',
value: 'delay',
iconUrl: getImage('/scene/delay-type.png'),
subLabel: '等待一段时间后,再执行后续动作',
},
{
label: '触发告警',
value: 'trigger',
iconUrl: getImage('/scene/trigger-type.png'),
subLabel: '配置触发告警规则,需配合“告警配置”使用',
},
{
label: '解除告警',
value: 'relieve',
iconUrl: getImage('/scene/cancel-type.png'),
subLabel: '配置解除告警规则,需配合“告警配置”使用',
},
{
label: '设备输出',
value: 'device',
iconUrl: getImage('/scene/device-type.png'),
subLabel: '配置设备调用功能、读取属性、设置属性规则',
},
{
label: '消息通知',
value: 'notify',
iconUrl: getImage('/scene/message-type.png'),
subLabel: '配置向指定用户发邮件、钉钉、微信、短信等通知',
},
{
label: '延迟执行',
value: 'delay',
iconUrl: getImage('/scene/delay-type.png'),
subLabel: '等待一段时间后,再执行后续动作',
},
{
label: '触发告警',
value: 'trigger',
iconUrl: getImage('/scene/trigger-type.png'),
subLabel: '配置触发告警规则,需配合“告警配置”使用',
},
{
label: '解除告警',
value: 'relieve',
iconUrl: getImage('/scene/cancel-type.png'),
subLabel: '配置解除告警规则,需配合“告警配置”使用',
},
];
const actionForm = ref();

View File

@ -14,7 +14,7 @@
:rules="[
{
validator: (_rule, value) => checkValue(_rule, value, item),
trigger: ['change', 'blur']
trigger: ['change', 'blur'],
},
]"
>
@ -22,6 +22,7 @@
:notify="notify"
v-if="getType(item) === 'user'"
v-model:value="modelRef[item.id]"
@change="(val) => onChange(val, 'user')"
/>
<Org
:notify="notify"
@ -78,7 +79,7 @@ const formRef = ref();
const modelRef = reactive({});
watchEffect(() => {
Object.assign(modelRef, props.value);
Object.assign(modelRef, props?.value);
});
const getType = (item: any) => {
@ -102,34 +103,41 @@ const checkValue = (_rule: any, value: any, item: any) => {
return Promise.reject(new Error('请选择' + item.name));
} else {
if (value?.source === 'upper') {
if (!value.upperKey) {
if (!value?.upperKey) {
return Promise.reject(new Error('请选择' + item.name));
} else {
return Promise.resolve();
}
} else {
if (!value.value) {
if (!value?.value) {
return Promise.reject(new Error('请选择' + item.name));
} else {
return Promise.resolve();
}
}
}
} else if (value?.source === 'fixed' && !value.value) {
} else if (value?.source === 'fixed' && !value?.value) {
return Promise.reject(new Error('请输入' + item.name));
} else if (value?.source === 'relation' && !value.value && !value.relation) {
} else if (
value?.source === 'relation' &&
!value?.value &&
!value?.relation
) {
return Promise.reject(new Error('请选择' + item.name));
} else if (value?.source === 'upper' && !value.upperKey) {
return Promise.reject(new Error('请选择' + item.name));
} else if (type === 'user') {
if (props.notify.notifyType === 'email' && value?.source !== 'relation') {
if (Array.isArray(value.value)) {
if (!value.value.length) {
if (
props.notify.notifyType === 'email' &&
value?.source !== 'relation'
) {
if (Array.isArray(value?.value)) {
if (!value?.value.length) {
return Promise.reject(new Error('请输入收件人'));
}
const reg =
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
const flag = value.value.every((it: string) => {
const flag = value?.value.every((it: string) => {
return reg.test(it);
});
if (!flag) {
@ -143,10 +151,11 @@ const checkValue = (_rule: any, value: any, item: any) => {
}
if (
props.notify.notifyType &&
['sms', 'voice'].includes(props?.notify?.notifyType) && value?.source !== 'relation'
['sms', 'voice'].includes(props?.notify?.notifyType) &&
value?.source !== 'relation'
) {
const reg = /^[1][3-9]\d{9}$/;
if (!reg.test(value.value)) {
if (!reg.test(value?.value)) {
return Promise.reject(new Error('请输入正确的手机号码'));
} else {
return Promise.resolve();
@ -161,12 +170,14 @@ const onChange = (val: any, type: any) => {
emit('change', { orgName: val.join(',') });
} else if (type === 'tag') {
emit('change', { tagName: val });
} else if (type === 'user') {
emit('change', { sendTo: val });
}
};
const onSave = () =>
new Promise((resolve) => {
formRef.value.validate().then(async (_data: any) => {
formRef.value?.validate().then(async (_data: any) => {
resolve(_data);
});
});

View File

@ -2,7 +2,7 @@
<j-modal
title="执行动作"
visible
:width="860"
:width="800"
@cancel="onCancel"
@ok="onOk"
:maskClosable="false"

View File

@ -33,21 +33,59 @@
固定号码
</j-select-option>
</j-select>
<j-tree-select
v-if="source === 'relation'"
style="width: calc(100% - 120px)"
placeholder="请选择收信人"
@select="(key, node) => onChange(source, key, false, node?.relation, node.name)"
:tree-data="treeData"
:multiple="['email'].includes(notifyType)"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:value="relationData"
>
<template #title="{ key, username, title }">
<div style="display: flex; justify-content: space-between; margin-right: 10px;" v-if="key !== 'p1' && key !== 'p2'">{{ title }} <span style="color: #cfcfcf;">{{ username }}</span></div>
<span v-else>{{ title }}</span>
</template>
</j-tree-select>
<template v-if="source === 'relation'">
<j-tree-select
v-if="['email'].includes(notifyType)"
style="width: calc(100% - 120px)"
placeholder="请选择收信人"
@change="(key, label) => onChange(source, key, false, label)"
:tree-data="treeData"
:multiple="true"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:value="relationData"
>
<template #title="{ key, username, title }">
<div
style="
display: flex;
justify-content: space-between;
margin-right: 10px;
"
v-if="key !== 'p1' && key !== 'p2'"
>
{{ title }}
<span style="color: #cfcfcf">{{ username }}</span>
</div>
<span v-else>{{ title }}</span>
</template>
</j-tree-select>
<j-tree-select
v-else
style="width: calc(100% - 120px)"
placeholder="请选择收信人"
@select="
(key, node) => onChange(source, key, node?.isRelation, node?.name)
"
:tree-data="treeData"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:value="relationData"
>
<template #title="{ key, username, title }">
<div
style="
display: flex;
justify-content: space-between;
margin-right: 10px;
"
v-if="key !== 'p1' && key !== 'p2'"
>
{{ title }}
<span style="color: #cfcfcf">{{ username }}</span>
</div>
<span v-else>{{ title }}</span>
</template>
</j-tree-select>
</template>
<template v-else>
<j-select
style="width: calc(100% - 120px)"
@ -55,7 +93,15 @@
placeholder="请选择收信人"
:value="value?.value"
showSearch
@change="(val, option) => onChange(source, val, false, option?.label || option?.name)"
@change="
(val, option) =>
onChange(
source,
val,
false,
option?.label || option?.name,
)
"
:options="relationList"
/>
<j-select
@ -64,14 +110,25 @@
placeholder="请输入收件人邮箱,多个收件人用换行分隔"
:value="value?.value"
mode="tags"
@change="(val) => onChange(source, val, false, Array.isArray(val) ? val.join(',') : val)"
@change="
(val) =>
onChange(
source,
val,
false,
Array.isArray(val) ? val.join(',') : val,
)
"
/>
<j-input
style="width: calc(100% - 120px)"
v-else-if="['sms', 'voice'].includes(notifyType)"
placeholder="请输入固定号码"
:value="value?.value"
@change="(e) => onChange(source, e.target.value, false, e.target.value)"
@change="
(e) =>
onChange(source, e.target.value, false, e.target.value)
"
></j-input>
</template>
</j-input-group>
@ -262,7 +319,7 @@ const onChange = (
_name?: string,
) => {
let _values: any = undefined;
const _names: string[] = [_name || ''];
const _names: string[] = Array.isArray(_name) ? _name : [_name || ''];
if (Array.isArray(_value)) {
if (props?.notify?.notifyType === 'email') {
if (isRelation) {
@ -280,7 +337,7 @@ const onChange = (
_values = getObj(_source, _value, isRelation);
}
emit('update:value', _values);
emit('change', { sendTo: _names.filter((item) => !!item).join(',') });
emit('change', _names.filter((item) => !!item).join(','));
};
watch(

View File

@ -1,126 +1,173 @@
<template>
<div class="mangement-container">
<div class="left">
<j-input-search
v-model:value="leftData.searchValue"
placeholder="请输入"
style="margin-bottom: 24px"
/>
<!-- 使用v-if用于解决异步加载数据后不展开的问题 -->
<j-tree
v-if="leftData.treeData.length > 0"
showLine
defaultExpandAll
:tree-data="leftData.treeData"
v-model:selectedKeys="leftData.selectedKeys"
@select="leftData.onSelect"
>
<template #title="{ dataRef }">
<div
v-if="dataRef.root"
:style="`
justify-content: space-between;
display: flex;
align-items: center;
`"
>
<span>
<page-container>
<div class="manager-container">
<div class="left">
<j-input-search
v-model:value="leftData.searchValue"
placeholder="请输入"
style="margin-bottom: 24px"
/>
<!-- 使用v-if用于解决异步加载数据后不展开的问题 -->
<j-tree
v-if="leftData.treeData.length > 0"
showLine
defaultExpandAll
:tree-data="leftData.treeData"
v-model:selectedKeys="leftData.selectedKeys"
@select="onSelect"
>
<template #title="{ dataRef }">
<div
v-if="dataRef.root"
style="
justify-content: space-between;
display: flex;
align-items: center;
width: 200px;
"
>
<span>
{{ dataRef.title }}
</span>
<AIcon
type="PlusOutlined"
style="color: #1d39c4"
@click="addTable"
/>
</div>
<span v-else>
{{ dataRef.title }}
</span>
<AIcon
type="PlusOutlined"
style="color: #1d39c4"
@click="leftData.addTable"
/>
</div>
<span v-else>
{{ dataRef.title }}
</span>
</template>
</j-tree>
</div>
<div class="right">
<div class="btns">
<j-button type="primary" @click="table.clickSave"
>保存</j-button
>
</template>
</j-tree>
</div>
<j-pro-table
ref="tableRef"
:columns="table.columns"
model="TABLE"
:dataSource="table.data"
>
<template #name="slotProps">
<j-input
:disabled="slotProps.scale !== undefined"
v-model:value="slotProps.name"
placeholder="请输入名称"
:maxlength="64"
/>
</template>
<template #type="slotProps">
<j-input
v-model:value="slotProps.type"
placeholder="请输入类型"
:maxlength="64"
/>
</template>
<template #length="slotProps">
<j-input-number
v-model:value="slotProps.length"
:min="0"
:max="99999"
/>
</template>
<template #precision="slotProps">
<j-input-number
v-model:value="slotProps.precision"
:min="0"
:max="99999"
/>
</template>
<template #notnull="slotProps">
<j-radio-group
v-model:value="slotProps.notnull"
button-style="solid"
<div class="right">
<div class="btns">
<j-button type="primary" @click="clickSave">保存</j-button>
</div>
<j-form ref="formRef" :model="table">
<j-table
:columns="columns"
:dataSource="table.data"
:pagination="false"
:scroll="{ y: 500 }"
>
<j-radio-button :value="true"></j-radio-button>
<j-radio-button :value="false"></j-radio-button>
</j-radio-group>
</template>
<template #comment="slotProps">
<j-input
v-model:value="slotProps.comment"
placeholder="请输入说明"
/>
</template>
<template #action="slotProps">
<PermissionButton
:hasPermission="`{permission}:delete`"
type="link"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</template>
</j-pro-table>
<j-botton class="add-row" @click="table.addRow">
<AIcon type="PlusOutlined" /> 新增行
</j-botton>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'name'">
<j-form-item
:name="['data', index, 'name']"
:rules="[
{
max: 64,
message: '最多可输入64个字符',
},
{
required: true,
message: '请输入名称',
},
]"
>
<j-input
:disabled="record.scale !== undefined"
v-model:value="record.name"
placeholder="请输入名称"
/>
</j-form-item>
</template>
<template v-else-if="column.key === 'type'">
<j-form-item
:name="['data', index, 'type']"
:rules="[
{
max: 64,
message: '最多可输入64个字符',
},
{
required: true,
message: '请输入类型',
},
]"
>
<j-input
v-model:value="record.type"
placeholder="请输入类型"
/>
</j-form-item>
</template>
<template v-else-if="column.key === 'length'">
<j-form-item :name="['data', index, 'length']">
<j-input-number
v-model:value="record.length"
:min="0"
:max="99999"
style="width: 100%"
/>
</j-form-item>
</template>
<template v-else-if="column.key === 'precision'">
<j-form-item
:name="['data', index, 'precision']"
>
<j-input-number
v-model:value="record.precision"
:min="0"
:max="99999"
style="width: 100%"
/>
</j-form-item>
</template>
<template v-else-if="column.key === 'notnull'">
<j-form-item
:name="['data', index, 'notnull']"
>
<j-radio-group
v-model:value="record.notnull"
button-style="solid"
>
<j-radio-button :value="true"
></j-radio-button
>
<j-radio-button :value="false"
></j-radio-button
>
</j-radio-group>
</j-form-item>
</template>
<template v-else-if="column.key === 'comment'">
<j-form-item
:name="['data', index, 'notnull']"
>
<j-input
v-model:value="record.comment"
placeholder="请输入说明"
/>
</j-form-item>
</template>
<template v-else-if="column.key === 'action'">
<PermissionButton
hasPermission="system/DataSource:delete"
type="link"
:tooltip="{ title: '删除' }"
:danger="true"
:popConfirm="{
title: `确认删除`,
onConfirm: () => clickDel(record),
}"
:disabled="record.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</template>
</template>
</j-table>
</j-form>
<j-button class="add-row" @click="addRow">
<AIcon type="PlusOutlined" /> 新增行
</j-button>
</div>
</div>
</div>
<div class="dialogs">
<j-modal
v-model:visible="dialog.visible"
title="新增"
@ok="dialog.handleOk"
>
<j-modal v-model:visible="dialog.visible" title="新增" @ok="handleOk">
<j-form :model="dialog.form" ref="addFormRef">
<j-form-item
label="名称"
@ -129,22 +176,22 @@
{
required: true,
message: '请输入名称',
trigger: 'change',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
trigger: 'blur',
},
{
pattern: /^[0-9].*$/,
message: '不能以数字开头',
// pattern: /^[0-9].*$/,
// message: '',
trigger: 'change',
validator: checkName,
},
{
pattern: /^\w+$/,
message: '名称只能由数字、字母、下划线、中划线组成',
trigger: 'change',
trigger: 'blur',
},
]"
>
@ -155,7 +202,7 @@
</j-form-item>
</j-form>
</j-modal>
</div>
</page-container>
</template>
<script setup lang="ts" name="Management">
@ -166,153 +213,166 @@ import {
saveTable_api,
delSaveRow_api,
} from '@/api/system/dataSource';
import { onlyMessage } from '@/utils/comm';
import { FormInstance, message } from 'ant-design-vue';
import { DataNode } from 'ant-design-vue/lib/tree';
import _ from 'lodash';
import { cloneDeep } from 'lodash';
import type { dbColumnType, dictItemType, sourceItemType } from '../typing';
const id = useRoute().query.id as string;
const columns = [
{
title: '列名',
dataIndex: 'name',
key: 'name',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '长度',
dataIndex: 'length',
key: 'length',
},
{
title: '精度',
dataIndex: 'precision',
key: 'precision',
},
{
title: '不能为空',
dataIndex: 'notnull',
key: 'notnull',
},
{
title: '说明',
dataIndex: 'comment',
key: 'comment',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
},
];
const formRef = ref();
const getInfo = (_id: string) => {
getDataSourceInfo_api(_id).then((resp: any) => {
info.data = resp.result;
});
};
const info = reactive({
data: {} as sourceItemType,
init: () => {
id &&
getDataSourceInfo_api(id).then((resp: any) => {
info.data = resp.result;
});
},
});
const leftData = reactive({
searchValue: '',
sourceTree: [] as dictItemType[],
treeData: [] as DataNode[],
treeData: [] as any[],
selectedKeys: [] as string[],
oldKey: '',
init: () => {
leftData.getTree();
watch(
[
() => leftData.searchValue,
() => leftData.sourceTree,
() => info.data,
],
(n) => {
if (leftData.sourceTree.length < 1 || !info.data.shareConfig)
return;
let filterArr = [];
if (leftData.searchValue) {
filterArr = leftData.sourceTree.filter((item) =>
item.name.includes(n[0]),
);
} else filterArr = leftData.sourceTree;
leftData.treeData = [
{
title: info.data.shareConfig.schema,
key: info.data.shareConfig.schema,
root: true,
children: filterArr.map((item) => ({
title: item.name,
key: item.name,
})),
},
];
leftData.selectedKeys = [filterArr[0].name];
leftData.onSelect([filterArr[0].name]);
},
{},
);
},
getTree: () => {
rdbTree_api(id)
.then((resp: any) => {
leftData.sourceTree = resp.result;
})
.catch(() => {});
},
onSelect: (selectedKeys: string[], e?: any) => {
if (e?.node?.root) {
leftData.selectedKeys = [leftData.oldKey];
return;
}
leftData.oldKey = selectedKeys[0];
const key = selectedKeys[0];
table.getTabelData(key);
},
addTable: (e: Event) => {
e.stopPropagation();
},
});
const table = reactive({
columns: [
{
title: '列名',
dataIndex: 'name',
key: 'name',
scopedSlots: true,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
},
{
title: '长度',
dataIndex: 'length',
key: 'length',
scopedSlots: true,
},
{
title: '精度',
dataIndex: 'precision',
key: 'precision',
scopedSlots: true,
},
{
title: '不能为空',
dataIndex: 'notnull',
key: 'notnull',
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'comment',
key: 'comment',
scopedSlots: true,
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
],
data: [] as dbColumnType[],
getTabelData: (key: string) => {
const queryTables = (key: string) => {
if (key) {
rdbTables_api(id, key).then((resp: any) => {
table.data = resp.result.columns.map(
(item: object, index: number) => ({ ...item, index }),
(item: object, index: number) => ({
...item,
index,
}),
);
});
}
};
const handleSearch = (refresh?: boolean) => {
rdbTree_api(id)
.then((resp: any) => {
if (resp.status === 200) {
leftData.sourceTree = resp.result;
if (refresh) {
leftData.selectedKeys = [resp.result[0]?.name];
queryTables(resp.result[0]?.name);
} else {
queryTables(leftData.selectedKeys[0]);
}
}
})
.catch(() => {});
};
const onSelect = (selectedKeys: string[], e?: any) => {
if (e?.node?.root) {
leftData.selectedKeys = [leftData.oldKey];
return;
}
if (!selectedKeys[0]) {
return;
}
leftData.oldKey = selectedKeys[0];
const key = selectedKeys[0];
queryTables(key);
};
const addTable = (e: Event) => {
e?.stopPropagation();
dialog.visible = true;
};
watch(
() => id,
(newId) => {
if (newId) {
getInfo(newId);
handleSearch(true);
}
},
addRow: () => {
const initData: dbColumnType = {
precision: 0,
length: 0,
notnull: false,
type: '',
comment: '',
name: '',
};
table.data.push(initData);
{
immediate: true,
},
clickSave: () => {
);
const table = reactive({
data: [] as dbColumnType[],
});
const addRow = () => {
const initData: dbColumnType = {
precision: 0,
length: 0,
notnull: false,
type: '',
comment: '',
name: '',
};
table.data.push(initData);
};
const clickDel = (row: any) => {
if (row.scale !== undefined) {
delSaveRow_api(id, leftData.selectedKeys[0], [row.name]).then(
(resp: any) => {
if (resp.status === 200) table.data.splice(row.index, 1);
},
);
} else table.data.splice(row.index, 1);
};
const clickSave = () => {
formRef.value.validate().then((_data: any) => {
const columns = cloneDeep(table.data);
columns.forEach((item) => delete item.index);
if (!columns.length) {
onlyMessage('请配置数据源字段', 'error');
return;
}
const params = {
name: leftData.selectedKeys[0],
columns,
@ -320,20 +380,11 @@ const table = reactive({
saveTable_api(id, params).then((resp) => {
if (resp.status === 200) {
message.success('操作成功');
table.getTabelData(params.name);
queryTables(params.name);
}
});
},
clickDel: (row: any) => {
if (row.scale !== undefined) {
delSaveRow_api(id, leftData.selectedKeys[0], [row.name]).then(
(resp: any) => {
if (resp.status === 200) table.data.splice(row.index, 1);
},
);
} else table.data.splice(row.index, 1);
},
});
});
};
const addFormRef = ref<FormInstance>();
const dialog = reactive({
@ -341,73 +392,110 @@ const dialog = reactive({
form: {
name: '',
},
handleOk: () => {
addFormRef.value &&
addFormRef.value.validate().then(() => {
const name = dialog.form.name;
leftData.sourceTree.unshift({
id: name,
name,
});
leftData.oldKey = name;
leftData.selectedKeys = [name];
table.data = [];
});
},
});
init();
function init() {
info.init();
leftData.init();
}
const handleOk = () => {
addFormRef.value &&
addFormRef.value.validate().then(() => {
const name = dialog.form.name;
leftData.sourceTree.unshift({
id: name,
name,
});
leftData.oldKey = name;
leftData.selectedKeys = [name];
table.data = [];
dialog.visible = false;
addFormRef.value?.resetFields();
});
};
watch(
[() => leftData.searchValue, () => leftData.sourceTree],
([m, n]) => {
if (!!m) {
const list = n.filter((item) => {
return item.name.includes(m);
});
leftData.treeData = [
{
title: info.data.shareConfig.schema,
key: info.data.shareConfig.schema,
root: true,
children: list.map((item) => ({
title: item.name,
key: item.name,
})),
},
];
if (!_.map(list, 'name').includes(leftData.selectedKeys[0])) {
leftData.selectedKeys = [list[0]?.name];
queryTables(list[0]?.name);
}
} else {
leftData.treeData = [
{
title: info.data.shareConfig.schema,
key: info.data.shareConfig.schema,
root: true,
children: leftData.sourceTree.map((item) => ({
title: item.name,
key: item.name,
})),
},
];
}
},
{ deep: true },
);
const checkName = (_: any, value: any) =>
new Promise((resolve, reject) => {
if (value) {
const first = value.slice(0, 1);
if (typeof Number(first) === 'number' && !isNaN(Number(first))) {
reject('不能以数字开头');
} else {
resolve('');
}
}
});
</script>
<style lang="less" scoped>
.mangement-container {
margin: 24px;
.manager-container {
padding: 24px;
background-color: #fff;
display: flex;
min-height: 500px;
.left {
flex-basis: 280px;
padding-right: 24px;
padding: 0 24px;
box-sizing: border-box;
:deep(.ant-tree-treenode) {
width: 100%;
.ant-tree-switcher-noop {
display: none;
}
.ant-tree-node-content-wrapper {
width: 100%;
.ant-tree-title {
width: 100%;
}
}
&:first-child .ant-tree-node-selected {
background-color: transparent;
}
}
}
.right {
width: calc(100% - 280px);
box-sizing: border-box;
border-left: 1px solid #f0f0f0;
padding-left: 24px;
.btns {
display: flex;
justify-content: right;
padding: 0px 24px;
}
.add-row {
display: block;
text-align: center;
width: 100%;
margin: 24px 0;
cursor: pointer;
}
.ant-form-item {
margin-bottom: 0;
}
}
}
</style>

View File

@ -6,7 +6,7 @@
:title="dialogTitle"
:confirmLoading="loading"
@ok="confirm"
@cancel="emits('update:visible', false)"
@cancel="emits('cancel')"
>
<j-form ref="formRef" :model="form.data" layout="vertical">
<j-row :gutter="24">
@ -51,7 +51,7 @@
message: '请输入URL',
trigger: 'change',
},
{ validator: form.checkUrl, trigger: 'blur' },
{ validator: checkUrl, trigger: 'blur' },
]"
>
<j-input
@ -193,9 +193,8 @@ import { FormInstance, message } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
import type { dictItemType, optionItemType, sourceItemType } from '../typing';
const emits = defineEmits(['confirm', 'update:visible']);
const emits = defineEmits(['confirm', 'cancel']);
const props = defineProps<{
visible: boolean;
data: sourceItemType;
}>();
//
@ -203,19 +202,25 @@ const dialogTitle = computed(() =>
props.data.id ? '编辑数据源' : '新增数据源',
);
const loading = ref(false);
const confirm = () => {
loading.value = true;
formRef.value
?.validate()
.then(() => form.submit())
.then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功');
emits('confirm');
emits('update:visible', false);
}
})
.finally(() => (loading.value = false));
const checkUrl = (_rule: Rule, value: string): Promise<any> => {
if (!value) return Promise.resolve();
const arr = value.split(':');
if (arr?.[0] === 'jdbc' || arr?.[0] === 'r2dbc') {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的URL');
}
};
const getTypeOption = () => {
getDataTypeDict_api().then((resp: any) => {
const result = resp.result as dictItemType[];
form.typeOptions = result.map((item) => ({
label: item.name,
value: item.id,
}));
});
};
const formRef = ref<FormInstance>();
@ -223,30 +228,38 @@ const form = reactive({
data: {
...props.data,
} as sourceItemType,
typeOptions: [] as optionItemType[],
checkUrl: (_rule: Rule, value: string): Promise<any> => {
if (!value) return Promise.reject('请输入URL');
const arr = value.split(':');
if (arr?.[0] === 'jdbc' || arr?.[0] === 'r2dbc') {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的URL');
}
},
getTypeOption: () => {
getDataTypeDict_api().then((resp: any) => {
const result = resp.result as dictItemType[];
form.typeOptions = result.map((item) => ({
label: item.name,
value: item.id,
}));
});
},
submit: () => {
return saveDataSource_api(form.data);
},
});
form.getTypeOption();
watch(
() => props.data,
(newValue) => {
form.data = {...newValue, shareConfig: { ...newValue?.shareConfig }}
},
{
immediate: true,
deep: true
},
);
onMounted(() => {
getTypeOption();
})
const confirm = () => {
loading.value = true;
formRef.value
?.validate()
.then(async (_data: any) => {
const resp = await saveDataSource_api({ ...props.data, ..._data });
if (resp.status === 200) {
message.success('操作成功');
emits('confirm');
formRef.value?.resetFields()
}
})
.finally(() => {
loading.value = false;
});
};
</script>

View File

@ -14,8 +14,13 @@
model="TABLE"
:params="queryParams"
:defaultParams="{
pageSize: 10,
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:pagination="{
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
}"
>
<template #headerTitle>
<PermissionButton
@ -31,7 +36,7 @@
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:statusNames="{
enabled: 'success',
enabled: 'processing',
disabled: 'error',
}"
>
@ -116,6 +121,7 @@
? '请先禁用,再删除'
: '删除',
}"
:danger="true"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
@ -130,7 +136,7 @@
<EditDialog
v-if="dialog.visible"
v-model:visible="dialog.visible"
@cancel="table.cancel"
:data="dialog.selectItem"
@confirm="table.refresh"
/>
@ -211,7 +217,7 @@ const columns = [
value: 'enabled',
},
{
label: '禁用',
label: '禁用',
value: 'disabled',
},
],
@ -277,7 +283,13 @@ const table = {
//
refresh: () => {
tableRef.value.reload();
dialog.visible = false
dialog.selectItem = {}
},
cancel: () => {
dialog.visible = false
dialog.selectItem = {}
}
};
table.getTypeOption();

View File

@ -38,7 +38,7 @@
:params="queryParams"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys.value,
onChange: selectRow,
onChange: selectChange,
}"
@cancelSelect="table.cancelSelect"
:columns="columns"
@ -55,7 +55,7 @@
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
@ -121,7 +121,7 @@
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
online: 'success',
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
@ -173,10 +173,8 @@ const confirm = () => {
permission: item.selectPermissions,
}));
if (params.length === 1) {
// , ,
departmentStore.setProductId(params[0].assetIdList[0]);
}
// , ,
departmentStore.setProductId(params[0].assetIdList[0]);
loading.value = true;
bindDeviceOrProductList_api(props.assetType, params)
@ -475,15 +473,20 @@ const table: any = {
},
};
table.init();
const selectRow = (keys: string[], rows: any[]) => {
const okRows = rows.filter(
(item) =>
!!item.permissionList.find(
(permiss: any) => permiss.value === 'share',
),
);
table.selectedRows = okRows;
table._selectedRowKeys.value = okRows.map((item) => item.id);
// const selectRow = (rows: any[], check: boolean) => {
// const okRows = rows.filter(
// (item) =>
// !!item.permissionList.find(
// (permiss: any) => permiss.value === 'share',
// ),
// );
// table.selectedRows = okRows;
// table._selectedRowKeys.value = okRows.map((item) => item.id);
// };
// fix: bug#10749
const selectChange = (keys: string[], rows: any[]) => {
table.selectedRows = rows;
table._selectedRowKeys.value = keys;
};
</script>

View File

@ -8,12 +8,13 @@
visible
@cancel="emits('update:visible', false)"
>
<a-alert
message="只能分配有'共享'权限的资产数据"
type="warning"
show-icon
/>
<div style="margin-top: 5px;">
<div class="alert-info">
<j-space>
<AIcon type="ExclamationCircleOutlined" />
<span>只能分配有'共享'权限的资产数据</span>
</j-space>
</div>
<div style="margin-top: 5px">
<span>资产权限</span>
<j-checkbox-group
v-model:value="form.permission"
@ -67,4 +68,11 @@ const options = computed(() => {
});
</script>
<style scoped></style>
<style scoped lang="less">
.alert-info {
background: #f3f3f3;
border-radius: 2px;
padding: 6px;
color: rgba(0, 0, 0, 0.55);
}
</style>

View File

@ -68,8 +68,8 @@
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
1: 'processing',
0: 'error',
online: 'processing',
offline: 'error',
}"
>
<template #img>
@ -171,8 +171,8 @@
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
1: 'processing',
0: 'error',
online: 'processing',
offline: 'error',
}"
></BadgeStatus>
</template>
@ -517,6 +517,7 @@ const table = {
},
];
unBindDeviceOrProduct_api('product', params).then(() => {
tableData._selectedRowKeys = [];
message.success('操作成功');
table.refresh();
});

View File

@ -3,7 +3,7 @@
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
@search="handleParams"
/>
<j-pro-table
ref="tableRef"
@ -119,8 +119,8 @@ const columns = [
{
title: '状态',
dataIndex: 'state',
key: 'state',
dataIndex: 'status',
key: 'status',
ellipsis: true,
fixed: 'left',
search: {
@ -149,6 +149,10 @@ const columns = [
//
const queryParams = ref({});
const handleParams = (params: any) => {
queryParams.value = params
}
//
const tableRef = ref<Record<string, any>>({}); //
const table = reactive({
@ -169,6 +173,7 @@ const table = reactive({
value: props.parentId,
},
],
type: 'and'
},
],
};
@ -213,4 +218,11 @@ const table = reactive({
});
const dialogVisible = ref(false);
watch(
() => props.parentId,
() => {
table.refresh();
},
);
</script>

View File

@ -83,7 +83,7 @@
<div
class="pager"
v-if="
requestBody.params.paramsTable.length &&
requestBody.params.paramsTable.length > 10 &&
requestBody.pageSize
"
>

View File

@ -83,10 +83,27 @@ const rowSelection = {
selectedRowKeys: ref<string[]>([]),
};
const save = async () => {
const keys = props.selectedRowKeys;
// fix: #bug10828
// id
const currenTableKeys = props.tableData.map((m: any) => m.id);
// id
const currentSelectedKeys = rowSelection.selectedRowKeys.value;
// , id
const oldSelectedKeys = currenTableKeys.filter((key) =>
props.sourceKeys.includes(key),
);
const removeKeys = props.sourceKeys.filter((key) => !keys.includes(key));
const addKeys = keys.filter((key) => !props.sourceKeys.includes(key));
// const keys = props.selectedRowKeys;
// const removeKeys = props.sourceKeys.filter((key) => !keys.includes(key));
// const addKeys = keys.filter((key) => !props.sourceKeys.includes(key));
//
const removeKeys = oldSelectedKeys.filter(
(key) => !currentSelectedKeys.includes(key),
);
//
const addKeys = currentSelectedKeys.filter(
(key) => !oldSelectedKeys.includes(key),
);
if (props.mode === 'api') {
// api

View File

@ -71,7 +71,7 @@
v-model:value="form.data.targetType"
:disabled="!!form.data.id"
@change="form.rules.checkUnique"
placeholder="请选择关联方"
placeholder="请选择关联方"
>
<j-select-option
v-for="item in targetList"

View File

@ -45,6 +45,7 @@
</PermissionButton>
<PermissionButton
:danger="true"
:hasPermission="`${permission}:delete`"
type="link"
:tooltip="{ title: '删除' }"
@ -161,6 +162,8 @@ const table = {
if (resp.status === 200) {
tableRef.value?.reload();
message.success('操作成功!');
} else {
message.error(resp.message);
}
});
},

View File

@ -117,9 +117,12 @@
show-search
style="width: 100%"
placeholder="请选择组织"
multiple
:tree-data="form.departmentOptions"
:fieldNames="{ label: 'name', value: 'id' }"
multiple
:filterTreeNode="
(v: string, node: any) => filterSelectNode(v, node, 'name')
"
>
<template #title="{ name }">
{{ name }}
@ -195,6 +198,7 @@ import { Rule } from 'ant-design-vue/es/form';
import { DefaultOptionType } from 'ant-design-vue/es/vc-tree-select/TreeSelect';
import { AxiosResponse } from 'axios';
import { passwordRegEx } from '@/utils/validate';
import { filterSelectNode } from '@/utils/comm';
const deptPermission = 'system/Department';
const rolePermission = 'system/Role';
@ -250,7 +254,8 @@ const form = reactive({
if (!value) return reject('请输入密码');
else if (value.length > 64) return reject('最多可输入64个字符');
else if (value.length < 8) return reject('密码不能少于8位');
else if (!passwordRegEx(value)) return reject('密码必须包含大小写英文和数字');
else if (!passwordRegEx(value))
return reject('密码必须包含大小写英文和数字');
validateField_api('password', value).then((resp: any) => {
resp.result.passed
? resolve('')

View File

@ -3700,8 +3700,8 @@ jetlinks-store@^0.0.3:
jetlinks-ui-components@^1.0.5:
version "1.0.5"
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#8cb5c9e68e46e6e7eebc0d96b1cdaab24828779f"
integrity sha512-yIbmplK+twekevr7n+dGMvO8tvyIqguC60TWeJCmx2mUqpwv8dEnr/cwwpJee4PBLWohvGPywsYgmm7KxVBbcw==
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#6dc396bc8a1b6f5a08accf5f46aec2099c15f481"
integrity sha512-mnVN6MfHfyZf82miEoZV8+ud6RBH29x0A8PfpcraFQUl9Wat6XcjGppr8FOkmFbGw8laCGK5jlbiX3cYJUwaxw==
dependencies:
"@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15"