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

This commit is contained in:
leiqiaochu 2023-03-23 10:33:28 +08:00
commit 49ed583525
68 changed files with 803 additions and 357 deletions

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -1,5 +1,10 @@
<template>
<ConfigProvider :locale='zhCN'>
<!-- <router-view v-slot="{ Component }">-->
<!-- <keep-alive>-->
<!-- <component :is="Component" />-->
<!-- </keep-alive>-->
<!-- </router-view>-->
<router-view />
</ConfigProvider>
</template>

View File

@ -107,7 +107,7 @@ export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boo
* @param type
* @returns
*/
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`
export const deviceExport = (productId: string, type: string, params?: any) => server.get(`/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`, params, {responseType: 'blob'})
/**
* ID是否重复

View File

@ -97,7 +97,7 @@ export const _import = (configId: any, params: any) => server.get(`/network/card
* @param format xlsxcsv
* @param params
*/
export const _export = (format: string, data: any) => server.post(`/network/card/download.${format}/_query`, data, { responseType: 'blob' });
export const _export = (format: string, data: any) => server.postStream(`/network/card/download.${format}/_query`, data);
/**
*

View File

@ -18,6 +18,8 @@ export const getDeviceOrProductList_api = (data: object) => server.post(`/device
export const getDeviceList_api = (data: object) => server.post(`/device/instance/_query`, data);
// 根据产品的id获取产品的权限
export const getPermission_api = (type: 'device' | 'product', ids: object, id: string) => server.post(`/assets/bindings/${type}/org/${id}/_query`, ids);
// 获取绑定的权限
export const getBindingsPermission = (type: 'device' | 'product', ids: string[]) => server.post(`/assets/bindings/${type}`, ids);
// 获取产品的权限字典
export const getPermissionDict_api = () => server.get(`/assets/bindings/product/permissions`);

View File

@ -6,6 +6,7 @@
v-model:selectedKeys="state.selectedKeys"
:pure="state.pure"
:breadcrumb="{ routes: breadcrumb }"
@back='routerBack'
>
<template #breadcrumbRender="slotProps">
<a
@ -70,6 +71,10 @@ const state = reactive<StateType>({
selectedKeys: [],
});
const routerBack = () => {
router.go(-1)
}
const findRouteMeta = (code: string) => {
let meta = []
let menuItem: any = menu.menus[code]

View File

@ -194,7 +194,7 @@ const timeChange = (e: any) => {
}
const inputChange = (e: any) => {
emit('change', e.target ? e.target.value : e)
emit('change', e && e.target ? e.target.value : e)
}
const dateChange = (e: any) => {

View File

@ -51,7 +51,7 @@ export const AccountMenu = {
export default [
{ path: '/*', redirect: '/'},
{
path: '/login',
path: LoginPath,
component: () => import('@/views/user/Login/index.vue')
},
{

View File

@ -112,6 +112,12 @@ export const getStream = function(url: string, params = {}) {
})
}
export const postStream = function(url: string, data={}, params = {}) {
return post<any>(url, data, params, {
responseType: 'arraybuffer' // 设置请求数据类型返回blob可解析类型
})
}
/**
*
* @param {Object} error
@ -149,10 +155,11 @@ const errorHandler = (error: any) => {
Notification.error({
key: '401',
message: 'Unauthorized',
description: 'Authorization verification failed'
description: '用户未登录'
})
setTimeout(() => {
router.replace({
debugger
router.push({
path: LoginPath
})
}, 0)
@ -208,7 +215,7 @@ request.interceptors.response.use(response => {
}
// 如果返回的的是文件流那么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
return response.data
} else {
return response.data
}
@ -222,5 +229,6 @@ export default {
patch,
put,
remove,
getStream
getStream,
postStream
}

View File

@ -1,7 +1,7 @@
import moment from "moment";
import { LocalStore } from "./comm";
import { TOKEN_KEY } from "./variable";
import {SystemConst} from './consts';
import { SystemConst } from './consts';
/**
* JSON
@ -30,7 +30,7 @@ export const downloadObject = (record: Record<string, any>, fileName: string, fo
* @param url
* @param params
*/
export const downloadFile = (url: string, params?: Record<string, any>) => {
export const downloadFile = (url: string, params?: Record<string, any>) => {
const formElement = document.createElement('form');
formElement.style.display = 'display:none;';
formElement.method = 'GET';
@ -91,29 +91,29 @@ export const randomString = (length?: number) => {
* @returns
*/
export const timestampFormat = (time: number) => {
let hour = 0;
let minute = 0;
let second = 0;
const timeStr = 'hh小时mm分钟ss秒';
let hour = 0;
let minute = 0;
let second = 0;
const timeStr = 'hh小时mm分钟ss秒';
if (time) {
if (time >= 60 * 60 * 1000) {
hour = Math.trunc(time / (60 * 60 * 1000));
}
if (time >= 60 * 1000) {
minute = Math.trunc((time - hour * 60 * 60 * 1000) / (60 * 1000));
}
second = Math.trunc(
(time - hour * (60 * 60 * 1000) - minute * 60 * 1000) / 1000,
);
if (time) {
if (time >= 60 * 60 * 1000) {
hour = Math.trunc(time / (60 * 60 * 1000));
}
return timeStr
.replace('hh', hour.toString())
.replace('mm', minute.toString())
.replace('ss', second.toString());
if (time >= 60 * 1000) {
minute = Math.trunc((time - hour * 60 * 60 * 1000) / (60 * 1000));
}
second = Math.trunc(
(time - hour * (60 * 60 * 1000) - minute * 60 * 1000) / 1000,
);
}
return timeStr
.replace('hh', hour.toString())
.replace('mm', minute.toString())
.replace('ss', second.toString());
};
export const ArrayToTree = (list: any[]): any[] => {

View File

@ -16,6 +16,9 @@
moment(slotProps.requestTime).format('YYYY-MM-DD HH:mm:ss')
}}
</template>
<template #description="slotProps">
{{ slotProps.action }}
</template>
<template #responseTime="slotProps">
<j-tag color="purple">
{{ slotProps.responseTime - slotProps.requestTime }} ms
@ -59,12 +62,7 @@
</template>
</j-pro-table>
</div>
<j-modal
:width="1100"
v-model:visible="visible"
title="详情"
@ok="handleOk"
>
<j-modal :width="1100" v-model:visible="visible" title="详情">
<j-descriptions :data="descriptionsData" title="" bordered :column="2">
<j-descriptions-item label="URL">
{{ descriptionsData?.url }}
@ -112,6 +110,9 @@
/>
</j-descriptions-item>
</j-descriptions>
<template #footer>
<j-button type="primary" @click="handleOk">关闭</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup name="AccessLog">
@ -145,6 +146,16 @@ const columns = [
},
ellipsis: true,
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: true,
search: {
type: 'string',
},
ellipsis: true,
},
{
title: '请求方法',
dataIndex: 'httpMethod',
@ -198,9 +209,9 @@ const columns = [
title: '请求用户',
dataIndex: 'username',
key: 'username',
search: {
type: 'string',
},
// search: {
// type: 'string',
// },
width: 150,
scopedSlots: true,
},

View File

@ -66,12 +66,7 @@
</template>
</j-pro-table>
</div>
<j-modal
:width="1100"
v-model:visible="visible"
title="详情"
@ok="handleOk"
>
<j-modal :width="1100" v-model:visible="visible" title="详情">
<div>
<span class="mr-10">[{{ descriptionsData?.threadName }}]</span>
<span class="mr-10">{{
@ -102,6 +97,9 @@
placeholder="暂无数据"
:auto-size="{ minRows: 24, maxRows: 28 }"
/>
<template #footer>
<j-button type="primary" @click="handleOk">关闭</j-button>
</template>
</j-modal>
</template>
<script lang="ts" setup name="SystemLog">

View File

@ -19,6 +19,7 @@
format="YYYY-MM-DD HH:mm:ss"
valueFormat="YYYY-MM-DD HH:mm:ss"
style="margin-left: 12px"
:show-time="{ format: 'HH:mm:ss' }"
@change="rangeChange"
v-model:value="rangeVal"
:allowClear="false"
@ -110,5 +111,5 @@ const handleBtnChange = (val: string) => {
});
};
handleBtnChange(radioValue.value);
watch(() => radioValue.value, { deep: true, immediate: true });
// watch(() => radioValue.value, { deep: true, immediate: true });
</script>

View File

@ -237,6 +237,8 @@ const emit = defineEmits(['change']);
const id = props.data.id;
const VersionOrder = props.data.versionOrder;
const VersionSign = props.data.sign;
const VersionUrl = props.data.url;
const formData: any = ref({
name: '',
@ -254,12 +256,14 @@ const extraValue: any = ref({});
const validatorSign = async (_: Record<string, any>, value: string) => {
const { signMethod, url } = formData.value;
if (value && !!signMethod && !!url && !extraValue.value) {
return extraValue.value[signMethod] !== value
? Promise.reject('签名不一致,请检查文件是否上传正确')
: Promise.resolve();
} else {
if (id && VersionSign === value && VersionUrl === url) {
return Promise.resolve();
} else {
if (value && !!signMethod && !!url && !!extraValue.value) {
return extraValue.value[signMethod] !== value
? Promise.reject('签名不一致,请检查文件是否上传正确')
: Promise.resolve();
}
}
};
const validatorVersionOrder = async (_: Record<string, any>, value: string) => {
@ -270,7 +274,9 @@ const validatorVersionOrder = async (_: Record<string, any>, value: string) => {
if (value && !!signMethod && productId) {
const res = await validateVersion(productId, value);
if (res.status === 200) {
return Promise.reject(res.result ? '版本序号已存在' : '');
return res.result
? Promise.reject('版本序号已存在')
: Promise.resolve();
}
}
}

View File

@ -280,7 +280,7 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) {
return [];
}
const Actions = [
const Actions: any = [
{
key: 'view',
text: '查看',
@ -292,7 +292,11 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
handlEye(data.errorReason);
},
},
{
];
const { state, mode } = data;
if (mode.value === 'push' && state.value === 'failed') {
Actions.push({
key: 'update',
text: '重试',
tooltip: {
@ -305,8 +309,9 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
handlTry(data.id);
},
},
},
];
});
}
return Actions;
};

View File

@ -208,10 +208,4 @@ watch(
);
</script>
<style lang="less" scoped>
.form {
.form-submit {
background-color: @primary-color !important;
}
}
</style>
<style lang="less" scoped></style>

View File

@ -143,7 +143,7 @@ const columns = [
key: 'createTime',
dataIndex: 'createTime',
search: {
type: 'time',
type: 'date',
},
width: 200,
scopedSlots: true,
@ -197,6 +197,9 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
{
key: 'delete',
text: '删除',
tooltip: {
title: '删除',
},
popConfirm: {
title: '确认删除?',
okText: ' 确定',

View File

@ -49,7 +49,7 @@
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product';
import { downloadFile } from '@/utils/utils';
import { downloadFileByUrl } from '@/utils/utils';
import encodeQuery from '@/utils/encodeQuery';
import { deviceExport } from '@/api/device/instance';
@ -83,13 +83,23 @@ watch(
{ immediate: true, deep: true },
);
const handleOk = () => {
const handleOk = async () => {
const params = encodeQuery(props.data);
downloadFile(
deviceExport(modelRef.product || '', modelRef.fileType),
params,
// downloadFile(
// deviceExport(modelRef.product || '', modelRef.fileType),
// params,
// );
const res: any = await deviceExport(
modelRef.product || '',
modelRef.fileType,
params
);
emit('close');
if (res) {
const blob = new Blob([res], { type: modelRef.fileType });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备实例`, modelRef.fileType);
emit('close');
}
};
const handleCancel = () => {

View File

@ -3,16 +3,22 @@
:maskClosable="false"
width="800px"
:visible="true"
title="当前进度"
@ok="handleCancel"
@cancel="handleCancel"
:title="type === 'active' ? '启用' : '同步'"
:closable="false"
>
<div>
<j-badge v-if="flag" status="processing" text="进行中" />
<j-badge v-else status="success" text="已完成" />
<div style="margin: 10px 0px 20px 0px">
<div v-if="flag">
<div>{{ type === 'active' ? '正在启用全部设备' : '正在同步设备状态' }}</div>
<j-progress :percent="50" />
</div>
<div v-else>
<p>{{ type === 'active' ? '启用' : '同步' }}成功{{ count }}</p>
<p v-if="type === 'active'">启用失败:{{ errCount }}条<j-tooltip title="实例信息页面中的配置项未完善"><AIcon type="QuestionCircleOutlined" /></j-tooltip></p>
</div>
</div>
<p>总数量{{ count }}</p>
<a style="color: red">{{ errMessage }}</a>
<template #footer>
<j-button v-if="!flag" type="primary" @click="handleCancel">完成</j-button>
</template>
</j-modal>
</template>
@ -32,8 +38,8 @@ const props = defineProps({
});
// const eventSource = ref<Record<string, any>>({})
const count = ref<number>(0);
const flag = ref<boolean>(false);
const errMessage = ref<string>('');
const flag = ref<boolean>(true);
const errCount = ref<number>(0);
const isSource = ref<boolean>(false);
const id = ref<string>('');
const source = ref<Record<string, any>>({});
@ -43,30 +49,27 @@ const handleCancel = () => {
emit('save');
};
// const handleOk = () => {
// emit('close');
// emit('save');
// };
const getData = (api: string) => {
flag.value = true;
let dt = 0;
const _source = new EventSourcePolyfill(api);
source.value = _source;
_source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
// console.log(res)
switch (props.type) {
case 'active':
if (res.success) {
_source.close();
dt += res.total;
count.value = dt;
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg;
errCount.value = 1
id.value = res.source.id;
isSource.value = true;
} else {
errMessage.value = res.message;
errCount.value = 1
}
}
break;
@ -74,22 +77,13 @@ const getData = (api: string) => {
dt += res;
count.value = dt;
break;
case 'import':
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message;
}
break;
default:
break;
}
};
_source.onerror = () => {
flag.value = false;
_source.close();
flag.value = false;
};
_source.onopen = () => {};
};

View File

@ -237,6 +237,11 @@ const getData = (
return new Promise((resolve) => {
queryFlow(start, end, {
orderBy: 'date',
terms: [{
column : "cardId",
termType: "eq",
value: route.params.id
}]
}).then((resp: any) => {
if (resp.status === 200) {
const sortArray = resp.result.sort(

View File

@ -43,11 +43,9 @@ const type = ref<string>('xlsx');
const handleOk = () => {
_export(type.value, props.data).then((res: any) => {
console.log(res)
if (res) {
const blob = new Blob([res], { type: type.value });
const blob = new Blob([res.data], { type: type.value });
const url = URL.createObjectURL(blob);
console.log(url, 123);
downloadFileByUrl(
url,
`物联卡管理-${moment(new Date()).format(

View File

@ -232,8 +232,8 @@ const handleOk = () => {
btnLoading.value = true;
const resp =
props.type === 'add'
? await add(toRaw(modelRef))
: await edit(toRaw(modelRef));
? await add(toRaw(modelRef)).catch(err => err)
: await edit(toRaw(modelRef)).catch(err => err);
btnLoading.value = false;
if (resp.status === 200) {
message.success('操作成功')

View File

@ -457,6 +457,7 @@ const columns = [
width: 200,
search: {
type: 'string',
defaultTermType: 'eq'
},
},
{
@ -466,6 +467,9 @@ const columns = [
ellipsis: true,
scopedSlots: true,
width: 200,
search: {
type: 'string'
}
},
{
title: '平台对接',
@ -571,7 +575,8 @@ const columns = [
{ label: '正常', value: 'using' },
{ label: '未激活', value: 'toBeActivated' },
{ label: '停机', value: 'deactivate' },
],
{ label: '其它', value: 'using,toBeActivated,deactivate' },
]
},
},
{
@ -737,7 +742,16 @@ const getActions = (
};
const handleSearch = (e: any) => {
params.value = e;
const newParams = (e?.terms as any[])?.map(item1 => {
item1.terms = item1.terms.map((item2: any) => {
if (['cardStateType'].includes(item2.column)) {
item2.termType = 'nin'
}
return item2
})
return item1
})
params.value = { terms: newParams || [] };
};
const onSelectChange = (keys: string[], rows: []) => {

View File

@ -87,7 +87,7 @@
</page-container>
</template>
<script setup lang="ts">
<script setup lang="ts" name='IotCardHome'>
import { getImage } from '@/utils/comm';
import Guide from '../components/Guide.vue';
import moment from 'moment';
@ -113,14 +113,14 @@ const menuHasPermission = useMenuStore().hasPermission;
const btnHasPermission = usePermissionStore().hasPermission;
//
const dashBoardUrl = menuHasPermission('/iot-card/Dashboard');
const platformUrl = menuHasPermission('/iot-card/Platform/Detail');
const recordUrl = menuHasPermission('/iot-card/Record');
const cardUrl = menuHasPermission('/iot-card/CardManagement');
const dashBoardUrl = menuHasPermission('iot-card/Dashboard');
const platformUrl = menuHasPermission('iot-card/Platform/Detail');
const recordUrl = menuHasPermission('iot-card/Record');
const cardUrl = menuHasPermission('iot-card/CardManagement');
//
const paltformPermission = btnHasPermission(`/iot-card/Platform:add`);
const cardPermission = btnHasPermission(`/iot-card/CardManagement:add`);
const paltformPermission = btnHasPermission(`iot-card/Platform:add`);
const cardPermission = btnHasPermission(`iot-card/CardManagement:add`);
const Image = {
1: getImage('/home/1.png'),
@ -179,23 +179,19 @@ const pieChartData = ref<any[]>([
]);
const jumpPage = (data: GuideItemProps) => {
console.log(data.auth)
if (!data.auth){
message.warning('暂无权限,请联系管理员');
return
}
if (data.key === 'EQUIPMENT') {
menuStory.jumpPage(data.url, { id: 'add' });
menuStory.jumpPage(data.url, { id: ':id'});
} else {
menuStory.jumpPage(data.url);
}
};
const jumpDashboard = () => {
// if (dashBoardUrl) {
// router.push(`${dashBoardUrl}`);
// } else {
// message.warning('');
// }
menuStory.jumpPage('iot-card/Dashboard');
};

View File

@ -204,6 +204,7 @@ const rules = {
};
const getDetail = async () => {
console.log(route.params)
if (route.params.id === ':id') return;
const resp: any = await queryById(route.params.id);
if (resp.status === 200) {

View File

@ -238,7 +238,7 @@ const handleSearch = (e: any) => {
* 新增
*/
const handleAdd = () => {
menuStory.jumpPage('iot-card/Platform/Detail', { id: 'add' })
menuStory.jumpPage('iot-card/Platform/Detail', { id: ':id' })
};
</script>

View File

@ -16,8 +16,9 @@
</j-radio-button>
</j-radio-group>
<j-range-picker
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
format="YYYY-MM-DD HH:mm:ss"
valueFormat="YYYY-MM-DD HH:mm:ss"
:show-time="{ format: 'HH:mm:ss' }"
style="margin-left: 12px"
@change="rangeChange"
v-model:value="rangeVal"
@ -28,7 +29,7 @@
</template>
<script setup lang="ts">
import moment from 'moment';
import dayjs from 'dayjs';
import { PropType } from 'vue';
interface BtnOptions {
@ -70,38 +71,37 @@ const rangeVal = ref<[string, string]>();
const rangeChange = (val: any) => {
radioValue.value = undefined;
emit('change', {
start: moment(val[0]).valueOf(),
end: moment(val[1]).valueOf(),
start: dayjs(val[0]).valueOf(),
end: dayjs(val[1]).valueOf(),
type: undefined,
});
};
const getTimeByType = (type: string) => {
const getTimeByType = (type?: string) => {
switch (type) {
case 'hour':
return moment().subtract(1, 'hours').valueOf();
return dayjs().subtract(1, 'hours').valueOf();
case 'week':
return moment().subtract(6, 'days').valueOf();
return dayjs().subtract(6, 'days').valueOf();
case 'month':
return moment().subtract(29, 'days').valueOf();
return dayjs().subtract(29, 'days').valueOf();
case 'year':
return moment().subtract(365, 'days').valueOf();
return dayjs().subtract(365, 'days').valueOf();
default:
return moment().startOf('day').valueOf();
return dayjs().startOf('day').valueOf();
}
};
const handleBtnChange = (val: string) => {
radioValue.value = val;
let endTime = moment(new Date()).valueOf();
const handleBtnChange = (val?: string) => {
let endTime = dayjs(new Date()).valueOf();
let startTime = getTimeByType(val);
if (val === 'yesterday') {
startTime = moment().subtract(1, 'days').startOf('day').valueOf();
endTime = moment().subtract(1, 'days').endOf('day').valueOf();
startTime = dayjs().subtract(1, 'days').startOf('day').valueOf();
endTime = dayjs().subtract(1, 'days').endOf('day').valueOf();
}
rangeVal.value = [
moment(startTime).format('YYYY-MM-DD'),
moment(endTime).format('YYYY-MM-DD'),
dayjs(startTime).format('YYYY-MM-DD HH:mm:ss'),
dayjs(endTime).format('YYYY-MM-DD HH:mm:ss'),
];
emit('change', {
start: startTime,
@ -110,11 +110,8 @@ const handleBtnChange = (val: string) => {
});
};
watch(
() => radioValue.value,
(val) => {
handleBtnChange(val);
},
{ deep: true, immediate: true },
);
nextTick(() => {
handleBtnChange(radioValue.value)
})
</script>

View File

@ -3,7 +3,7 @@
hoverable
:class="[
'card-render',
'container',
`access-${data.type || 'network'}`,
checked === data.id ? 'checked' : '',
]"
@click="checkedChange(data.id)"
@ -106,8 +106,16 @@ const checkedChange = (id: string) => {
}
}
}
.container {
background: url('/public/images/access-icon.png') no-repeat;
.access-media {
background: url('/public/images/access-media.png') no-repeat;
background-position: bottom right;
}
.access-network {
background: url('/public/images/access-network.png') no-repeat;
background-position: bottom right;
}
.access-protocol {
background: url('/public/images/access-protocol.png') no-repeat;
background-position: bottom right;
}
</style>

View File

@ -20,7 +20,11 @@
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input
@ -119,7 +123,7 @@ const onFinish = async (values: any) => {
};
onMounted(() => {
if (id === ':id') {
if (id !== ':id') {
formState.value = {
name: props.data.name,
description: props.data?.description || '',

View File

@ -1,7 +1,7 @@
<template>
<div>
<j-steps class="steps-steps" :current="stepCurrent">
<j-step v-for="item in steps" :key="item" :title="item" />
<j-step disabled v-for="item in steps" :key="item" :title="item" />
</j-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
@ -51,6 +51,7 @@
max: 64,
message:
'最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -75,6 +76,7 @@
max: 64,
message:
'最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -184,7 +186,11 @@
</PermissionButton>
</div>
<j-scrollbar height="480">
<j-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<j-row
:gutter="[24, 24]"
style="width: 100%"
v-if="procotolList.length > 0"
>
<j-col
:span="8"
v-for="item in procotolList"
@ -193,12 +199,16 @@
<AccessCard
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
:data="{ ...item, type: 'protocol' }"
>
</AccessCard>
</j-col>
</j-row>
<j-empty v-else description="暂无数据" />
<j-empty
style="margin-top: 10%"
v-else
description="暂无数据"
/>
</j-scrollbar>
</div>
</div>
@ -222,7 +232,11 @@
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input

View File

@ -1,7 +1,7 @@
<template>
<div>
<j-steps class="steps-steps" :current="stepCurrent">
<j-step v-for="item in steps" :key="item" :title="item" />
<j-step disabled v-for="item in steps" :key="item" :title="item" />
</j-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
@ -63,6 +63,7 @@
max: 64,
message:
'最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -86,13 +87,14 @@
max: 64,
message:
'最多可输入64个字符',
trigger: 'blur',
},
]"
>
<template #label>
通知Token
<j-tooltip
title="接收OneNet推送的Token地址"
title="自定义token,可用于验证请求是否来自OneNet"
>
<AIcon
type="QuestionCircleOutlined"
@ -116,6 +118,7 @@
max: 64,
message:
'最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -266,7 +269,11 @@
</PermissionButton>
</div>
<j-scrollbar height="480">
<j-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<j-row
:gutter="[24, 24]"
style="width: 100%"
v-if="procotolList.length > 0"
>
<j-col
:span="8"
v-for="item in procotolList"
@ -275,12 +282,16 @@
<AccessCard
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
:data="{ ...item, type: 'protocol' }"
>
</AccessCard>
</j-col>
</j-row>
<j-empty v-else description="暂无数据" />
<j-empty
style="margin-top: 10%"
v-else
description="暂无数据"
/>
</j-scrollbar>
</div>
</div>
@ -305,7 +316,11 @@
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input

View File

@ -5,7 +5,7 @@
class="steps-steps"
:current="stepCurrent"
>
<j-step v-for="item in steps" :key="item" :title="item" />
<j-step disabled v-for="item in steps" :key="item" :title="item" />
</j-steps>
<div v-if="channel !== 'edge-child-device'" class="steps-content">
<div class="steps-box" v-if="current === 0">
@ -30,7 +30,11 @@
</PermissionButton>
</div>
<j-scrollbar height="480">
<j-row :gutter="[24, 24]" v-if="networkList.length > 0">
<j-row
:gutter="[24, 24]"
style="width: 100%"
v-if="networkList.length > 0"
>
<j-col
:span="8"
v-for="item in networkList"
@ -91,7 +95,11 @@
</AccessCard>
</j-col>
</j-row>
<j-empty v-else description="暂无数据" />
<j-empty
style="margin-top: 10%"
v-else
description="暂无数据"
/>
</j-scrollbar>
</div>
</div>
@ -119,7 +127,11 @@
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input

View File

@ -1,7 +1,7 @@
<template>
<div style="margin-top: 10px">
<j-steps :current="stepCurrent">
<j-step v-for="item in steps" :key="item" :title="item" />
<j-step disabled v-for="item in steps" :key="item" :title="item" />
</j-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
@ -29,7 +29,8 @@
},
{
max: 64,
message: '最大可输入64个字符',
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -50,7 +51,8 @@
},
{
max: 64,
message: '最大可输入64个字符',
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
@ -202,7 +204,11 @@
<j-collapse v-model:activeKey="activeKey">
<j-collapse-panel
:key="cluster.id"
:header="`#${index + 1}.节点`"
:header="
cluster.clusterNodeId
? cluster.clusterNodeId
: `#${index + 1}.配置信息`
"
>
<template #extra>
<span
@ -504,7 +510,7 @@ interface Form2 {
}
interface FormState {
domain: string | undefined;
sipId: string | undefined;
sipId: string | number | undefined;
shareCluster: boolean;
hostPort: {
port: string | undefined;
@ -576,7 +582,7 @@ const removeCluster = (item: Form2) => {
};
const addCluster = () => {
const id = Date.now();
const id: any = Date.now();
dynamicValidateForm.cluster.push({
clusterNodeId: undefined,
port: undefined,
@ -613,7 +619,11 @@ const { resetFields, validate, validateInfos } = useForm(
reactive({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),
@ -628,7 +638,7 @@ const saveData = () => {
transport: 'SIP',
channel: 'gb28181',
};
params.configuration.sipId = Number(params.configuration?.sipId);
const resp =
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
@ -645,20 +655,32 @@ const next = async () => {
data1.hostPort.port = port;
}
if (!data1?.shareCluster) {
let data2 = await formRef2.value?.validate();
if (data2 && data2?.cluster) {
data2.cluster.forEach((i: any) => {
i.enabled = true;
i.port = JSON.parse(i.port).port;
await formRef2.value
?.validate()
.then((data2) => {
if (data2 && data2?.cluster) {
data2.cluster.forEach((i: any) => {
i.enabled = true;
i.port = JSON.parse(i.port).port;
});
data1 = {
...data1,
...data2,
};
}
current.value = current.value + 1;
params.configuration = data1;
})
.catch((err) => {
err.errorFields.forEach((item: any) => {
const activeId: any =
dynamicValidateForm.cluster[item.name[1]].id;
if (!activeKey.value.includes(activeId)) {
activeKey.value.push(activeId); //
}
});
});
data1 = {
...data1,
...data2,
};
}
}
current.value = current.value + 1;
params.configuration = data1;
};
const prev = () => {
current.value = current.value - 1;
@ -698,11 +720,25 @@ onMounted(() => {
});
if (id !== ':id') {
formState.value = props.data.configuration;
formData.value = {
name: props.data.name,
description: props.data?.description || '',
};
const { configuration, name, description = '' } = props.data;
formData.value = { name, description };
if (configuration?.shareCluster) {
formState.value = {
...formState.value,
...props.data.configuration,
};
} else {
formState.value = {
...formState.value,
...props.data.configuration,
};
dynamicValidateForm.cluster = configuration.cluster;
if (dynamicValidateForm.cluster.length === 1) {
activeKey.value = ['1'];
dynamicValidateForm.cluster[0].id = 1;
}
}
}
});

View File

@ -21,7 +21,11 @@
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input

View File

@ -1,7 +1,7 @@
<template>
<div>
<j-steps :current="stepCurrent">
<j-step v-for="item in steps" :key="item" :title="item" />
<j-step disabled v-for="item in steps" :key="item" :title="item" />
</j-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
@ -26,7 +26,11 @@
</PermissionButton>
</div>
<j-scrollbar height="480">
<j-row :gutter="[24, 24]" v-if="networkList.length > 0">
<j-row
:gutter="[24, 24]"
style="width: 100%"
v-if="networkList.length > 0"
>
<j-col
:span="8"
v-for="item in networkList"
@ -40,6 +44,7 @@
description: item.description
? item.description
: descriptionList[provider.id],
type: 'network',
}"
>
<template #other>
@ -87,7 +92,11 @@
</AccessCard>
</j-col>
</j-row>
<j-empty v-else description="暂无数据" />
<j-empty
style="margin-top: 10%"
v-else
description="暂无数据"
/>
</j-scrollbar>
</div>
<div class="steps-box" v-else-if="current === 1">
@ -112,21 +121,29 @@
</PermissionButton>
</div>
<j-scrollbar height="480">
<j-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<j-row
:gutter="[24, 24]"
style="width: 100%"
v-if="procotolList.length > 0"
>
<j-col
:span="8"
v-for="item in procotolList"
:key="item?.id"
>
<access-card
<AccessCard
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
:data="{ ...item, type: 'protocol' }"
>
</access-card>
</AccessCard>
</j-col>
</j-row>
<j-empty v-else description="暂无数据" />
<j-empty
style="margin-top: 10%"
v-else
description="暂无数据"
/>
</j-scrollbar>
</div>
<div class="steps-box" v-else>
@ -366,7 +383,11 @@ const { resetFields, validate, validateInfos } = useForm(
reactive({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),

View File

@ -186,6 +186,7 @@ const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
let providersList = ref<Record<string, any>>([]);
const providersOptions = ref<Record<string, any>>([]);
const statusMap = new Map();
statusMap.set('enabled', 'success');
@ -207,13 +208,7 @@ const columns = [
key: 'provider',
search: {
type: 'select',
options: async () => {
const res: any = await getProviders();
return (res?.result || [])?.map((item: any) => ({
lable: item.name,
value: item.id,
}));
},
options: providersOptions,
},
},
{
@ -315,6 +310,10 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
const getProvidersList = async () => {
const res: any = await getProviders();
providersList.value = res.result;
providersOptions.value = (res?.result || [])?.map((item: any) => ({
label: item.name,
value: item.id,
}));
};
getProvidersList();

View File

@ -54,12 +54,17 @@ const loading = ref(false);
const handleChange = (info: UploadChangeParam) => {
loading.value = true;
if (info.file.status === 'done') {
onlyMessage('上传成功!', 'success');
const result = info.file.response?.result;
keystoreBase64.value = result;
const reg = new RegExp(/\.pem$/i);
if (reg.test(info.file.name)) {
keystoreBase64.value = result;
emit('change', result);
emit('update:modelValue', result);
onlyMessage('上传成功!', 'success');
} else {
onlyMessage('请上传.pem格式的文件', 'error');
}
loading.value = false;
emit('change', result);
emit('update:modelValue', result);
}
};
const textChange = (val: any) => {

View File

@ -9,7 +9,7 @@
:model="formData"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
:wrapper-col="{ span: 24 }"
autocomplete="off"
>
<j-form-item
@ -86,7 +86,7 @@
<h1>2. 配置说明</h1>
<h2>1证书文件</h2>
<div>
您可以使用文本编辑工具打开PEM或者CRT格式的证书文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书文件将文件内容上传到文本框
您可以使用文本编辑工具打开PEM格式的证书文件复制其中的内容并粘贴到该文本框或者单击该文本框下的上传并选择存储在本地计算机的证书文件将文件内容上传到文本框
</div>
<h2>2证书私钥</h2>
<div>
@ -208,31 +208,4 @@ detail(id);
}
}
}
.doc {
height: 100%;
padding: 0 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fff;
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 10px;
color: rgba(0, 0, 0, 0.8);
font-weight: 400;
font-size: 14px;
}
}
</style>

View File

@ -76,14 +76,17 @@ const columns = [
width: 200,
ellipsis: true,
search: {
type: 'select',
options: [
{
label: '证书标准',
value: 'common',
},
],
type: 'string',
},
// search: {
// type: 'select',
// options: [
// {
// label: '',
// value: 'common',
// },
// ],
// },
scopedSlots: true,
},
{
@ -118,17 +121,17 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
return [];
}
return [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: async () => {
handlEye(data.id);
},
},
// {
// key: 'view',
// text: '',
// tooltip: {
// title: '',
// },
// icon: 'EyeOutlined',
// onClick: async () => {
// handlEye(data.id);
// },
// },
{
key: 'update',
text: '编辑',
@ -143,6 +146,9 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
{
key: 'delete',
text: '删除',
tooltip: {
title: '删除',
},
popConfirm: {
title: '确认删除?',
okText: ' 确定',

View File

@ -91,17 +91,41 @@
>
<j-collapse
v-model:activeKey="activeKey"
class="collapse"
:class="[
!formData.shareCluster
? 'collapse'
: 'collapse-panel',
]"
:ghost="formData.shareCluster"
collapsible="header"
>
<j-collapse-panel
:key="cluster.id"
:show-arrow="!formData.shareCluster"
>
<!-- <j-collapse-panel
:key="cluster.id"
:header="
cluster.serverId
? cluster.serverId
: `#${index + 1}.配置信息`
: !formData.shareCluster
? `#${index + 1}.配置信息`
: ''
"
collapsible="header"
>
:show-arrow="!formData.shareCluster"
> -->
<template #header v-if="!shareCluster">
<div class="collapse-header">
{{
cluster.serverId
? cluster.serverId
: !formData.shareCluster
? `#${index + 1}.配置信息`
: ''
}}
</div>
</template>
<template #extra v-if="!shareCluster">
<j-popconfirm
@confirm.prevent="
@ -1112,6 +1136,10 @@ const filterConfigByType = (data: any, type: string) => {
});
};
const changeheader = (value: string) => {
console.log(22, value);
};
const getPortOptions = (portOptions: object, index = 0) => {
if (!portOptions) return;
const type = formData.value.type;
@ -1358,7 +1386,7 @@ watch(
top: -10px;
left: 10px;
right: 10px;
width: calc(100%-10px);
width: calc(100% - 10px);
height: 100%;
background-color: #f4f4f4;
content: ' ';
@ -1369,7 +1397,20 @@ watch(
.collapse {
margin-bottom: 20px;
background: #f4f4f4;
:deep(.ant-collapse-header-text) {
flex: 1;
}
}
.collapse-panel {
margin-bottom: 20px;
border: #d9d9d9 1px solid;
background: #f4f4f4;
border-radius: 2px;
:deep(.ant-collapse-header) {
padding: 0;
}
}
.delete-btn {
display: inline-block;
color: #e50012;

View File

@ -20,8 +20,8 @@ export const Configuration = {
script: '',
size: '',
length: '4',
offset: '0',
little: 'false',
offset: undefined,
little: undefined,
},
};
@ -103,11 +103,12 @@ export const isVisible = (
) => VisibleData[LastName].includes(dependencies);
export const Validator = {
regIp: new RegExp(
/((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/,
regIpv4: new RegExp(
/^((([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))\.){3}(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$/,
),
regIPv6: new RegExp(/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/),
regDomain: new RegExp(
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/,
/^https?:\/\/(([a-zA-Z0-9_-])+(\.)?)*(:\d+)?(\/((\.)?(\?)?=?&?[a-zA-Z0-9_-](\?)?)*)*$/i,
),
regOnlyNumber: new RegExp(/^\d+$/),
};
@ -159,7 +160,8 @@ export const Rules = {
message: '请输入公网地址',
},
{
pattern: Validator.regIp || Validator.regDomain,
pattern:
Validator.regIpv4 || Validator.regIPv6 || Validator.regDomain,
message: '请输入正确格式的域名或ip',
},
],
@ -179,7 +181,9 @@ export const Rules = {
message: '请输入远程地址',
},
{
pattern: Validator.regIp || Validator.regDomain,
pattern:
Validator.regIpv4 || Validator.regIPv6 || Validator.regDomain,
message: '请输入正确格式的域名或ip',
},
],

View File

@ -76,7 +76,10 @@
message: '请输入API Host',
},
{
pattern: regDomain,
pattern:
Validator.regIpv4 ||
Validator.regIPv6 ||
Validator.regDomain,
message: '请输入正确的IP地址或者域名',
},
]"
@ -130,7 +133,10 @@
message: '请输入RTP IP',
},
{
pattern: regDomain,
pattern:
Validator.regIpv4 ||
Validator.regIPv6 ||
Validator.regDomain,
message: '请输入正确的IP地址或者域名',
},
]"
@ -191,7 +197,15 @@
style="width: 100%"
:min="1"
:max="
formData.configuration.dynamicRtpPortRange1
Number(
formData.configuration
.dynamicRtpPortRange1,
) < 65535
? Number(
formData.configuration
.dynamicRtpPortRange1,
)
: 65535
"
:precision="0"
placeholder="起始端口"
@ -279,8 +293,17 @@ const formRef = ref<FormInstance>();
const loading = ref(false);
const options = ref([]);
const checked = ref(false);
const regDomain =
/[j-zA-Z0-9][-j-zA-Z0-9]{0,62}(\.[j-zA-Z0-9][-j-zA-Z0-9]{0,62})+\.?/;
const Validator = {
regIpv4: new RegExp(
/^((([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))\.){3}(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$/,
),
regIPv6: new RegExp(/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/),
regDomain: new RegExp(
/^https?:\/\/(([a-zA-Z0-9_-])+(\.)?)*(:\d+)?(\/((\.)?(\?)?=?&?[a-zA-Z0-9_-](\?)?)*)*$/i,
),
regOnlyNumber: new RegExp(/^\d+$/),
};
const formData = ref<FormDataType>({
name: '',

View File

@ -3,7 +3,7 @@
title='触发规则'
visible
:width='820'
@click='save'
@ok='save'
@cancel='cancel'
:maskClosable="false"
>

View File

@ -3,7 +3,7 @@
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
class="scene-search"
target="scene-triggrt-device-device"
/>
<j-divider style='margin: 0' />

View File

@ -22,11 +22,16 @@
<j-form-item>定时调用所选功能</j-form-item>
</j-col>
<j-col :span='24'>
<j-form-item
name='functionData'
:rules="rules"
>
<FunctionCall
:value='_value'
v-model:value='formModel.functionData'
:data='functionData'
@change='callDataChange'
/>
</j-form-item>
</j-col>
</j-row>
</j-form>
@ -66,9 +71,9 @@ const props = defineProps({
const emit = defineEmits<Emit>()
const invokeForm = ref()
const formModel = reactive({
functionId: props.functionId
functionId: props.functionId,
functionData: props.functionParameters
})
const _value = ref<any[]>(props.functionParameters)
/**
* 获取当前选择功能属性
@ -94,13 +99,29 @@ const functionData = computed(() => {
return arrCache
})
const rules = [{
validator(_: string, value: any) {
if (!value?.length && functionData.value.length) {
return Promise.reject('请输入功能值')
} else {
let hasValue = value.find((item: { name: string, value: any}) => !item.value)
if (hasValue) {
const functionItem = functionData.value.find((item: any) => item.id === hasValue.name)
return Promise.reject(functionItem?.name ? `请输入${functionItem?.name}` : '请输入功能值')
}
}
return Promise.resolve();
}
}]
const onSelect = (v: string, item: any) => {
formModel.functionData = []
emit('update:action', `执行${item.name}`)
emit('update:functionId', v)
emit('update:functionParameters', [])
}
const callDataChange = (v: any[]) => {
_value.value = v
emit('update:functionParameters', v)
}

View File

@ -3,7 +3,7 @@
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
class="scene-search"
target="scene-triggrt-device-device"
/>
<j-divider style='margin: 0' />

View File

@ -1,5 +1,5 @@
<template>
<div class='actions-branches-item'>
<div class='manual actions-branches-item'>
<j-form-item
:rules="actionRules"
:name="['branches', 0, 'then']"
@ -32,6 +32,21 @@ const actionRules = [{
</script>
<style scoped>
<style scoped lang='less'>
@minWidth: 75%;
.manual {
&.actions-branches-item {
width: 100%;
}
}
@media (min-width: 1600px) {
.manual {
&.actions-branches-item {
width: @minWidth;
}
}
}
</style>

View File

@ -3,7 +3,7 @@
title='触发规则'
visible
:width='820'
@click='save'
@ok='save'
@cancel='cancel'
>
<Timer
@ -15,17 +15,16 @@
<script setup lang="ts" name="timerAddModel">
import Timer from '../components/Timer'
import type { OperationTimer } from '@/views/rule-engine/Scene/typings'
import {nextTick, PropType} from "vue";
import {TriggerDevice} from "@/views/rule-engine/Scene/typings";
import { PropType} from "vue";
import {handleTimerOptions} from "@/views/rule-engine/Scene/Save/components/Timer/util";
type Emit = {
(e: 'cancel'): void
(e: 'save', data: TriggerDevice, options: Record<string, any>): void
(e: 'save', data: OperationTimer, options: Record<string, any>): void
}
const props = defineProps({
timer: {
value: {
type: Object as PropType<OperationTimer>,
default: () => ({})
}
@ -39,14 +38,14 @@ interface AddModelType {
}
const addModel = reactive<AddModelType>({
timer: props.timer
timer: props.value
})
const save = async () => {
const timerData = await timerRef.value?.validateFields()
if (timerData) {
const options = handleTimerOptions(timerData)
emit("save", timerData, options)
const options = handleTimerOptions(addModel.timer)
emit("save", addModel.timer, options)
}
}
@ -54,9 +53,9 @@ const cancel = () => {
emit("cancel")
}
nextTick(() => {
Object.assign(addModel, props.timer)
})
// watchEffect(() => {
// addModel.timer = props.value
// })
</script>

View File

@ -0,0 +1,58 @@
<template>
<div :class='["trigger-options-content", isAdd ? "is-add" : ""]'>
<span v-if='!isAdd'> 点击配置定时触发 </span>
<template v-else>
<div class='center-item'>
<AIcon v-if='options.selectorIcon' :type='options.selectorIcon' class='icon-padding-right' />
<span class='trigger-options-name'>
<Ellipsis style='max-width: 310px'>
{{ options.name }}
</Ellipsis>
</span>
<span v-if='options.extraName'>{{ options.extraName }}</span>
</div>
<div v-if='options.when'>
<span className='trigger-options-when'>{{ options.when }}</span>
</div>
<div v-if='options.time'>
<span className='trigger-options-time'>{{ options.time }}</span>
</div>
<div v-if='options.extraTime'>
<span className='trigger-options-extraTime'>{{ options.extraTime }}</span>
</div>
</template>
</div>
</template>
<script setup lang='ts' name='TimerTitle'>
const props = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const isAdd = computed(() => {
console.log(props.options, Object.keys(props.options).length)
return !!Object.keys(props.options).length
})
</script>
<style scoped lang='less'>
.trigger-options-content {
display: inline-flex;
gap: 16px;
.center-item {
display: flex;
align-items: center;
}
.icon-padding-right {
padding-right: 4px;
}
}
</style>

View File

@ -2,7 +2,7 @@
<div class='timer'>
<j-form-item
:rules="rules"
name="timer"
:name="['trigger', 'timer']"
>
<template #label>
<TitleComponent data='触发规则' style='font-size: 14px;' />
@ -29,8 +29,7 @@
v-if="visible"
@cancel='visible = false'
@save="save"
:value="data.trigger.device"
:options="data.options.trigger"
:value="data.trigger.timer"
/>
</div>
</template>
@ -41,6 +40,7 @@ import { storeToRefs } from 'pinia';
import Action from '../action/index.vue';
import AddModel from './AddModal.vue'
import AddButton from '../components/AddButton.vue'
import Title from './Title.vue'
import type { OperationTimer, BranchesThen } from '@/views/rule-engine/Scene/typings'
const sceneStore = useSceneStore();
@ -49,6 +49,7 @@ const visible = ref(false)
const rules = [{
validator(_: any, v: any) {
console.log(v)
if (!v) {
return Promise.reject(new Error('请配置定时触发规则'));
}
@ -85,5 +86,21 @@ const save = (_data: OperationTimer, options: Record<string, any>) => {
}
</script>
<style scoped>
<style scoped lang='less'>
@minWidth: 75%;
.timer {
.actions-branches-item {
width: 100%;
}
}
@media (min-width: 1600px) {
.timer {
.actions-branches-item {
width: @minWidth;
}
}
}
</style>

View File

@ -3,7 +3,7 @@
:columns="columns"
type="simple"
@search="handleSearch"
class="search"
class="scene-search"
target="scene-trigger-device-product"
/>
<j-divider style="margin: 0" />

View File

@ -22,6 +22,7 @@
showSearch
placeholder="请选择功能"
v-model:value="modelRef.message.functionId"
@select='functionSelect'
>
<j-select-option
v-for="item in metadata?.functions || []"
@ -34,7 +35,7 @@
<j-form-item
v-if="modelRef.message.functionId"
:name="['message', 'inputs']"
:rules="[{ required: true, message: '请输入功能值' }]"
:rules="functionRules"
>
<EditTable
:functions="functions"
@ -138,6 +139,25 @@ const modelRef = reactive({
},
});
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[];
properties: any[];

View File

@ -3,7 +3,7 @@
:columns="columns"
type="simple"
@search="handleSearch"
class="search"
class="scene-search"
target="scene-trigger-device-device"
/>
<j-divider style="margin: 0" />

View File

@ -101,6 +101,7 @@ const valueChange = debounce(() => {
const _value = dataSource.value.map(item => ({
name: item.id, value: item.value
}))
console.log(_value)
emit('change', _value)
emit('update:value', _value)
}, 500)

View File

@ -17,6 +17,7 @@ import { numberToString } from './util'
type Emit = {
(e: 'update:value', data: Array<number>):void
(e: 'change', data: Array<number>):void
}
const props = defineProps({
@ -48,6 +49,7 @@ const change = (number: number) => {
}
rowKeys.value = [..._keys.values()]
emit('update:value', rowKeys.value)
emit('change', rowKeys.value)
}
const allActive = computed(() => {
@ -57,7 +59,9 @@ const allActive = computed(() => {
watch(() => props.type, () => {
const isMonth = props.type === 'month'
const day = isMonth ? 31 : 7
change(0)
if (!props.value.length) {
change(0)
}
timeOptions.value = new Array(day)
.fill(1)
.map((_, index) => {

View File

@ -15,6 +15,7 @@
]'
option-type='button'
button-style='solid'
@change='updateValue'
/>
</j-form-item>
<j-form-item v-if='showCron' name='cron' :rules="[
@ -32,11 +33,11 @@
}
}
]">
<j-input placeholder='corn表达式' v-model:value='formModel.cron' />
<j-input placeholder='corn表达式' v-model:value='formModel.cron' @change='updateValue' />
</j-form-item>
<template v-else>
<j-form-item name='when'>
<WhenOption v-model:value='formModel.when' :type='formModel.trigger' />
<WhenOption v-model:value='formModel.when' :type='formModel.trigger' @change='updateValue' />
</j-form-item>
<j-form-item name='mod'>
<j-radio-group
@ -47,13 +48,19 @@
]'
option-type='button'
button-style='solid'
@change='updateValue'
/>
</j-form-item>
</template>
<j-space v-if='showOnce' style='display: flex;gap: 24px'>
<j-form-item :name="['once', 'time']">
<j-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%'
format='HH:mm:ss' />
<j-time-picker
valueFormat='HH:mm:ss'
v-model:value='formModel.once.time'
style='width: 100%'
format='HH:mm:ss'
@change='updateValue'
/>
</j-form-item>
<j-form-item> 执行一次</j-form-item>
</j-space>
@ -68,6 +75,7 @@
@change='(v) => {
formModel.period.from = v[0]
formModel.period.to = v[1]
updateValue()
}'
/>
</j-form-item>
@ -83,6 +91,7 @@
:min='1'
:max='59'
v-model:value='formModel.period.every'
@change='updateValue'
>
<template #addonAfter>
<j-select
@ -92,6 +101,7 @@
{ label: "分", value: "minutes" },
{ label: "小时", value: "hours" },
]'
@select='updateValue'
/>
</template>
</j-input-number>
@ -103,7 +113,7 @@
<script setup lang='ts' name='Timer'>
import type { PropType } from 'vue'
import moment from 'moment'
import dayjs from 'dayjs'
import WhenOption from './WhenOption.vue'
import { cloneDeep } from 'lodash-es'
import type { OperationTimer } from '../../../typings'
@ -131,23 +141,21 @@ const emit = defineEmits<Emit>()
const formModel = reactive<OperationTimer>({
trigger: 'week',
when: [],
when: props.value.when || [],
mod: 'period',
cron: undefined,
once: {
time: moment(new Date()).format('HH:mm:ss')
time: dayjs(new Date()).format('HH:mm:ss')
},
period: {
from: moment(new Date()).startOf('day').format('HH:mm:ss'),
to: moment(new Date()).endOf('day').format('HH:mm:ss'),
from: dayjs(new Date()).startOf('day').format('HH:mm:ss'),
to: dayjs(new Date()).endOf('day').format('HH:mm:ss'),
every: 1,
unit: 'seconds'
}
})
const timerForm = ref()
Object.assign(formModel, props.value)
const showCron = computed(() => {
return formModel.trigger === 'cron'
})
@ -160,7 +168,7 @@ const showPeriod = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'period'
})
watch(() => formModel, () => {
const updateValue = () => {
const cloneValue = cloneDeep(formModel)
if (cloneValue.trigger === 'cron') {
delete cloneValue.when
@ -174,7 +182,7 @@ watch(() => formModel, () => {
delete cloneValue.period
}
emit('update:value', cloneValue)
}, { deep: true })
}
defineExpose({
validateFields: () => new Promise(async (resolve) => {
@ -182,6 +190,8 @@ defineExpose({
resolve(data)
})
})
Object.assign(formModel, props.value)
formModel.when = props.value.when || []
</script>

View File

@ -57,15 +57,17 @@ const classNames = computed(() => {
})
const handleClick = (type: string) => {
emit('update:modelValue', type)
if (!props.disabled) {
emit('update:modelValue', type)
}
}
</script>
<style scoped lang='less'>
// @import 'ant-design-vue/es/style/themes/default.less';
.scene-trigger-way-warp {display: flex;
.scene-trigger-way-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
@ -115,5 +117,16 @@ const handleClick = (type: string) => {
}
}
}
&.disabled {
color: rgba(#000, .8);
.way-item-image {
opacity: 0.6;
}
.trigger-way-item {
cursor: not-allowed;
}
}
}
</style>

View File

@ -115,3 +115,6 @@ onUnmounted(() => {
}
}
</style>
<style lang='less'>
@import "./style.less";
</style>

View File

@ -0,0 +1,3 @@
.scene-search {
padding: 24px 0 0 0;
}

View File

@ -300,7 +300,7 @@ const getActions = (
];
if (data.triggerType === 'manual') {
const _item: ActionsType = {
key: 'trigger',
key: 'tigger',
text: '手动触发',
disabled: data.state?.value === 'disable',
tooltip: {

View File

@ -139,6 +139,7 @@ import {
getDeviceList_api,
getPermission_api,
bindDeviceOrProductList_api,
getBindingsPermission,
} from '@/api/system/department';
import { message } from 'jetlinks-ui-components';
import { dictType } from '../typing';
@ -295,22 +296,17 @@ const table: any = {
const { pageIndex, pageSize, total, data } =
resp.result as resultType;
const ids = data.map((item) => item.id);
getPermission_api(props.assetType, ids, parentId).then(
// fix: bug#10706
getBindingsPermission(props.assetType, ids).then(
(perResp: any) => {
const permissionObj = {};
perResp.result.forEach((item: any) => {
permissionObj[item.assetId] = props.allPermission
.filter((permission) =>
item.allPermissions.includes(permission.id),
)
.map((item) => ({
label: item.name,
value: item.id,
data.forEach((item) => {
item.permissionList = perResp.result
.find((f: any) => f.assetId === item.id)
.permissionInfoList?.map((m: any) => ({
label: m.name,
value: m.id,
disabled: true,
}));
});
data.forEach((item) => {
item.permissionList = permissionObj[item.id];
item.selectPermissions = ['read'];
//
@ -331,11 +327,10 @@ const table: any = {
};
}
});
resolve({
code: 200,
result: {
data: data,
data: data.sort((a, b) => a.createTime - b.createTime),
pageIndex,
pageSize,
total,
@ -344,6 +339,57 @@ const table: any = {
});
},
);
// getPermission_api(props.assetType, ids, parentId).then(
// (perResp: any) => {
// console.log('perResp: ', perResp);
// console.log('props.allPermission: ', props.allPermission);
// const permissionObj = {};
// perResp.result.forEach((item: any) => {
// permissionObj[item.assetId] = props.allPermission
// .filter((permission) =>
// item.allPermissions.includes(permission.id),
// )
// .map((item) => ({
// label: item.name,
// value: item.id,
// disabled: true,
// }));
// });
// data.forEach((item) => {
// item.permissionList = permissionObj[item.id];
// item.selectPermissions = ['read'];
// //
// if (props.assetType === 'product') {
// item.state = {
// value:
// item.state === 1
// ? 'online'
// : item.state === 0
// ? 'offline'
// : '',
// text:
// item.state === 1
// ? ''
// : item.state === 0
// ? ''
// : '',
// };
// }
// });
// resolve({
// code: 200,
// result: {
// data: data,
// pageIndex,
// pageSize,
// total,
// },
// status: 200,
// });
// },
// );
});
}),
//
@ -398,10 +444,15 @@ const table: any = {
},
};
table.init();
const selectRow = (keys:string[], rows:any[]) => {
const okRows = rows.filter(item=>!!item.permissionList.find((permiss:any)=>permiss.value === 'share'));
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)
table._selectedRowKeys.value = okRows.map((item) => item.id);
};
</script>

View File

@ -70,7 +70,7 @@
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
@ -147,7 +147,7 @@
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
online: 'success',
online: 'processing',
offline: 'error',
notActive: 'warning',
}"
@ -247,6 +247,9 @@ const columns = [
search: {
rename: 'productId$product-info',
type: 'select',
handleValue(value: string) {
return `id is ${value}`
},
options: () =>
new Promise((resolve) => {
const params = {
@ -288,14 +291,9 @@ const columns = [
search: {
type: 'select',
options: [
{
label: '正常',
value: 1,
},
{
label: '禁用',
value: 0,
},
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
scopedSlots: true,

View File

@ -68,9 +68,8 @@
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
1: 'processing',
0: 'error',
}"
>
<template #img>
@ -172,9 +171,8 @@
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
1: 'processing',
0: 'error',
}"
></BadgeStatus>
</template>

View File

@ -4,7 +4,7 @@
<pro-search
:columns="columns"
target="category"
@search="(params:any)=>queryParams = {...params}"
@search="handleParams"
/>
<j-pro-table
@ -262,6 +262,23 @@ type dictType = {
name: string;
};
type modalType = '' | 'add' | 'edit' | 'reset';
const handleParams = (params: any)=> {
const newParams = (params?.terms as any[])?.map(item1 => {
item1.terms = item1.terms.map((item2: any) => {
if (['telephone', 'email'].includes(item2.column)) {
return {
column: 'id$user-detail',
value: [item2]
}
}
return item2
})
return item1
})
queryParams.value = { terms: newParams || [] }
}
</script>
<style lang="less" scoped>

View File

@ -92,8 +92,8 @@ export default defineConfig(({ mode}) => {
[env.VITE_APP_BASE_API]: {
// target: 'http://192.168.33.22:8800',
// target: 'http://192.168.32.244:8881',
// target: 'http://120.77.179.54:8844', // 120测试
target: 'http://192.168.33.46:8844', // 本地开发环境
target: 'http://120.77.179.54:8844', // 120测试
// target: 'http://192.168.33.46:8844', // 本地开发环境
ws: 'ws://192.168.33.46:8844',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')

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#682711e0f69c141fff2c256db61a060c82539611"
integrity sha512-rQxD/YlE+XSAG7BWIcFTtKrCQJXk5o+TUgejyuUT/baBThJB6xYt1k2dQEdXyiwpukYen5FzaoLpelSD9SUegw==
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#77bd58156212d793fdebe41fc8864eeed5db7182"
integrity sha512-VXuCMJlMV/bbmBhPtBzY/BUBIvGebQbguLPE06xps79i5Pjq46+HBW8VPsJ9MwvyMhYkhzPlQJu9Ft7v5uo+yA==
dependencies:
"@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15"