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

This commit is contained in:
wangshuaiswim 2023-03-27 11:08:38 +08:00
commit 1afe27ca91
59 changed files with 1159 additions and 408 deletions

View File

@ -293,8 +293,8 @@ function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _
}
}
const filterName = ['message', 'Notification', 'AIcon']
const primitiveNames = ['Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker']
const filterName = ['message', 'Notification']
const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker']
const prefix = 'J'
let jetlinksNames: Set<string>

View File

@ -11,7 +11,7 @@ export const delApply_api = (id: string) => server.remove(`/application/${id}`)
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);
export const getDepartmentList_api = (params: any) => server.get(`/organization/_all/tree`, params);
// 获取应用详情
export const getAppInfo_api = (id: string) => server.get(`/application/${id}`);
// 新增应用

View File

@ -1,88 +0,0 @@
import { createFromIconfontCN } from '@ant-design/icons-vue';
import * as $Icon from '@ant-design/icons-vue';
import { createVNode } from 'vue';
const AliIcon = createFromIconfontCN({
scriptUrl: '/icons/iconfont.js', // 在 iconfont.cn 上生成
});
const AntdIcon = (props: {type: string}) => {
const {type} = props;
let antIcon: {[key: string]: any} = $Icon
return createVNode(antIcon[type])
}
const iconKeys = [
'EyeOutlined',
'EditOutlined',
'PlusOutlined',
'DeleteOutlined',
'CheckCircleOutlined',
'StopOutlined',
'CheckOutlined',
'CloseOutlined',
'DownOutlined',
'ImportOutlined',
'ExportOutlined',
'SyncOutlined',
'ExclamationCircleOutlined',
'UploadOutlined',
'LoadingOutlined',
'PlusCircleOutlined',
'QuestionCircleOutlined',
'DisconnectOutlined',
'LinkOutlined',
'PoweroffOutlined',
'SwapOutlined',
'BugOutlined',
'BarsOutlined',
'ArrowDownOutlined',
'SmallDashOutlined',
'TeamOutlined',
'MenuUnfoldOutlined',
'MenuFoldOutlined',
'QuestionCircleOutlined',
'InfoCircleOutlined',
'SearchOutlined',
'EllipsisOutlined',
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'PlayCircleOutlined',
'RightOutlined',
'FileTextOutlined',
'UploadOutlined',
'LikeOutlined',
'ArrowLeftOutlined',
'DownloadOutlined',
'PauseOutlined',
'ControlOutlined',
'RedoOutlined',
'ExpandOutlined',
'VideoCameraOutlined',
'HistoryOutlined',
'ToolOutlined',
'FileOutlined',
'LikeOutlined',
'CaretUpOutlined',
'CaretRightOutlined',
'CaretLeftOutlined',
'CaretDownOutlined',
'MinusOutlined',
'AudioOutlined',
'BellOutlined',
'UserOutlined',
'LogoutOutlined',
'ReadIconOutlined',
'CloudDownloadOutlined',
'PauseCircleOutlined',,
'FormOutlined',
'EyeInvisibleOutlined',
]
const Icon = (props: {type: string}) => {
if(iconKeys.includes(props.type)) return <AntdIcon {...props} />
return <AliIcon {...props} />
}
export default Icon

View File

@ -13,7 +13,7 @@ const props = defineProps({
type: String,
},
status: {
type: String || Number,
type: [String, Number],
default: 'default',
// validator: (value) => {
// //

View File

@ -121,11 +121,11 @@ const props = defineProps({
default: '正常',
},
status: {
type: [String, Number],
type: [String, Number] as PropType<string | number>,
default: 'default',
},
statusNames: {
type: Object,
type: Object as PropType<Record<any, any>>,
default:()=>({'default':'default'})
},
actions: {
@ -142,7 +142,7 @@ const props = defineProps({
},
});
const getBackgroundColor = (code: string) => {
const getBackgroundColor = (code: string | number) => {
const _color = color[code] || color.default;
return `linear-gradient(
188.4deg,

View File

@ -165,5 +165,6 @@ function syncTriggerClass(
display: -webkit-box;
-webkit-box-orient: vertical;
word-break: break-all;
white-space: normal;
}
</style>

View File

@ -11,7 +11,7 @@
<template #breadcrumbRender="slotProps">
<a
v-if="slotProps.route.index !== 0 && !slotProps.route.isLast"
@click='() => jumpPage(slotProps.route.path)'
@click='jump(slotProps.route)'
>
{{ slotProps.route.breadcrumbName }}
</a>
@ -89,14 +89,31 @@ const findRouteMeta = (code: string) => {
return meta
}
const jumpPage = (path: string) => {
const jump = (item: any) => {
let path = history.state.back
if (path) {
// query,
if (path.includes('?')) {
const _path = path.split('?')[0]
path = _path === item.path ? path : item.path
} else if (path !== item.path) {
path = item.path
}
} else {
path = item.path
}
console.log(item, history.state)
console.log(path)
// jumpPage(slotProps.route.path)
router.push(path)
}
const breadcrumb = computed(() =>
{
const paths = router.currentRoute.value.name as string
console.log(router.currentRoute)
console.log(route)
const metas = findRouteMeta(paths)
return metas.map((item, index) => {
return {

View File

@ -0,0 +1,57 @@
<!-- 表格行新增 - 简单分页组件 -->
<template>
<div class="pager">
<j-select v-model:value="myCurrent" style="width: 60px">
<j-select-option v-for="(val, i) in pageArr" :value="i + 1">
{{ i + 1 }}
</j-select-option>
</j-select>
<j-pagination
:pageSize="pageSize"
v-model:current="myCurrent"
:total="total"
style="text-align: center"
/>
</div>
</template>
<script setup lang="ts" name="RowPagination">
type PageEmits = {
(e: 'update:pageNum', data: number | string): void;
(e: 'update:pageSize', data: number | string): void;
};
type PageProps = {
pageNum: number;
pageSize: number;
total: number;
};
const emit = defineEmits<PageEmits>();
const props = defineProps<PageProps>();
const myCurrent = computed({
get: () => props.pageNum,
set: (val: number) => {
emit('update:pageNum', val);
},
});
const pageArr = computed(() => {
const maxPageNum = Math.ceil(props.total / props.pageSize);
return new Array(maxPageNum).fill(1);
});
</script>
<style lang="less" scoped>
.pager {
display: flex;
justify-content: center;
margin: 8px 0;
.ant-pagination {
margin-left: 8px;
:deep(.ant-pagination-item) {
display: none;
}
}
}
</style>

View File

@ -1,5 +1,4 @@
import type { App } from 'vue'
// import AIcon from './AIcon'
import PermissionButton from './PermissionButton/index.vue'
import JTable from './Table/index'
import TitleComponent from "./TitleComponent/index.vue";
@ -16,6 +15,7 @@ import JEmpty from './Empty/index.vue'
import AMapComponent from './AMapComponent/index.vue'
import PathSimplifier from './AMapComponent/PathSimplifier.vue'
import ValueItem from './ValueItem/index.vue'
import RowPagination from './RowPagination/index.vue'
export default {
install(app: App) {
@ -37,5 +37,6 @@ export default {
.component('AMapComponent', AMapComponent)
.component('PathSimplifier', PathSimplifier)
.component('ValueItem', ValueItem)
.component('RowPagination', RowPagination)
}
}

View File

@ -39,10 +39,7 @@ type MenuStateType = {
}
}
siderMenus: MenuItem[]
params: {
key: string
params: Record<string, any>
}
params: Record<string, any>
}
@ -50,10 +47,7 @@ export const useMenuStore = defineStore({
id: 'menu',
state: (): MenuStateType => ({
menus: {},
params: {
key: '',
params: {}
},
params: {},
siderMenus: []
}),
getters: {
@ -85,6 +79,7 @@ export const useMenuStore = defineStore({
jumpPage(name: string, params?: Record<string, any>, query?: Record<string, any>) {
const path = this.hasMenu(name)
if (path) {
this.params = { [name]: params || {}}
router.push({
name, params, query, state: { params }
})

View File

@ -1,6 +1,7 @@
import type { Slots } from 'vue'
import { TOKEN_KEY } from '@/utils/variable'
import { message } from 'jetlinks-ui-components';
import {cloneDeep, isArray} from "lodash-es";
/**
*
@ -133,4 +134,44 @@ export const handleParamsToString = (terms:SearchItemData[] = []) => {
})
return JSON.stringify({ terms: _terms})
}
export const treeFilter = (data: any[], value: any, key: string = 'name'): any[] => {
if (!data) return []
return data.filter(item => {
if (item.children && item.children.length) {
item.children = treeFilter(item.children || [], value, key)
return !!item.children.length
} else {
return item[key] === value
}
})
}
/**
*
* @param data
* @param search
* @param searchKey key
* @param returnKey key
*/
export const openKeysByTree = (data: any[], search: any, searchKey: string = 'id', returnKey: string = 'id'): any[] => {
if (!data || (data && !isArray(data))) return []
const cloneData = cloneDeep(data)
const filterTree = treeFilter(cloneData, search, searchKey)
const openKeys: any[] = []
const findKey = (treeData: any[]) => {
for (let i = 0; i < treeData.length; i++) {
const item = treeData[i]
openKeys.push(item[returnKey])
if (item.children && item.children.length) {
findKey(item.children)
}
}
}
findKey(filterTree)
return openKeys
}

View File

@ -0,0 +1,22 @@
import {useMenuStore} from "store/menu";
import { onBeforeUnmount } from 'vue'
export const useRouterParams = () => {
const params = ref<Record<string, any>>({})
const menu = useMenuStore();
const router = useRouter();
const routeName = router.currentRoute.value.name as string
params.value = routeName && menu.params[routeName] ? menu.params[routeName] : {}
onBeforeUnmount(() => {
if (routeName && menu.params[routeName]) { // 如果当前路由params参数离开页面清除掉
menu.params = {}
}
})
return {
params
}
}

View File

@ -96,9 +96,9 @@ const handleBtnChange = (val: string) => {
radioValue.value = val;
let endTime = dayjs(new Date()).valueOf();
let startTime = getTimeByType(val);
if (val === 'yesterday') {
startTime = dayjs().subtract(1, 'days').startOf('day').valueOf();
endTime = dayjs().subtract(1, 'days').endOf('day').valueOf();
if (val === 'today') {
startTime = dayjs().subtract(0, 'days').startOf('day').valueOf();
endTime = dayjs().subtract(0, 'days').endOf('day').valueOf();
}
rangeVal.value = [
dayjs(startTime).format('YYYY-MM-DD HH:mm:ss'),

View File

@ -43,7 +43,6 @@
<TimeSelect
key="flow-static"
:type="'week'"
:quickBtnList="quickBtnList"
@change="getEcharts"
/>
</template>
@ -134,12 +133,12 @@ let onlineOptions = ref<any>({});
let TodayDevOptions = ref<any>({});
let devMegOptions = ref<any>({});
const menuStore = useMenuStore();
const quickBtnList = [
{ label: '昨日', value: 'yesterday' },
{ label: '近一周', value: 'week' },
{ label: '近一月', value: 'month' },
{ label: '近一年', value: 'year' },
];
// const quickBtnList = [
// { label: '', value: 'yesterday' },
// { label: '', value: 'week' },
// { label: '', value: 'month' },
// { label: '', value: 'year' },
// ];
/**
* 获取产品数量
*/
@ -242,10 +241,40 @@ const getOnline = () => {
const onlineYdata = y;
onlineYdata.reverse();
setOnlineChartOption(x, onlineYdata);
onlineFooter.value[0].value = y?.[1];
}
});
};
/**
* 昨日在线
*/
const getYesterdayOnline = () => {
const startTime = dayjs().subtract(1, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs().subtract(1, 'days').endOf('day').format('YYYY-MM-DD HH:mm:ss');
dashboard([
{
dashboard: 'device',
object: 'session',
measurement: 'online',
dimension: 'agg',
group: 'aggOnline',
params: {
state: 'online',
limit: 24,
from: startTime,
to: endTime,
time: '1d',
format: 'yyyy-MM-dd HH:mm:ss',
},
},
]).then((res) => {
if (res.status == 200) {
onlineFooter.value[0].value = res.result?.[0]?.data.value || 0
}
});
}
const setOnlineChartOption = (x: Array<any>, y: Array<number>): void => {
onlineOptions.value = {
xAxis: {
@ -441,9 +470,12 @@ const setDevMesChartOption = (
],
};
};
getOnline();
//
const getDevice = () => {
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',
@ -455,7 +487,8 @@ const getDevice = () => {
time: '1h',
format: 'yyyy-MM-dd HH:mm:ss',
limit: 24,
from: 'now-1d',
from: startTime,
to: endTime
},
},
{
@ -502,7 +535,7 @@ const getDevice = () => {
}
});
};
getDevice();
const getEcharts = (data: any) => {
let _time = '1h';
let format = 'HH';
@ -557,6 +590,11 @@ const getEcharts = (data: any) => {
}
});
};
getOnline();
getYesterdayOnline()
getDevice();
</script>
<style lang="less" scoped>
.message-card,

View File

@ -237,7 +237,7 @@ const columns = [
scopedSlots: true,
width: 200,
search: {
type: 'string',
type: 'number',
},
},
{

View File

@ -90,7 +90,6 @@
</template>
<script setup lang='ts' name="Parsing">
import AIcon from '@/components/AIcon'
import PermissionButton from '@/components/PermissionButton/index.vue'
// import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { useFullscreen } from '@vueuse/core'

View File

@ -119,10 +119,12 @@ import { message } from 'jetlinks-ui-components';
import { getImage } from '@/utils/comm';
import { getWebSocket } from '@/utils/websocket';
import { useMenuStore } from '@/store/menu';
import {useRouterParams} from "@/utils/hooks/useParams";
const menuStory = useMenuStore();
const route = useRoute();
const routerParams = useRouterParams()
const instanceStore = useInstanceStore();
const statusMap = new Map();
@ -246,22 +248,35 @@ const getDetail = () => {
}
};
watch(
() => route.params?.id,
async (newId) => {
if (newId) {
await instanceStore.refresh(String(newId));
getStatus(String(newId));
list.value = [...initList];
getDetail();
instanceStore.tabActiveKey = 'Info';
}
},
{ immediate: true, deep: true },
);
// watch(
// () => route.params?.id,
// async (newId) => {
// if (newId) {
// await instanceStore.refresh(String(newId));
// getStatus(String(newId));
// list.value = [...initList];
// console.log('watch', route.params?.id)
// getDetail();
// instanceStore.tabActiveKey = 'Info';
// }
// },
// { immediate: true, deep: true },
// );
const getDetailFn = async () => {
const _id = route.params?.id
if (_id) {
await instanceStore.refresh(String(_id));
getStatus(String(_id));
list.value = [...initList];
getDetail();
instanceStore.tabActiveKey = routerParams.params.value.tab || 'Info';
}
}
onMounted(() => {
instanceStore.tabActiveKey = history.state?.params?.tab || 'Info';
getDetailFn()
instanceStore.tabActiveKey = routerParams.params.value.tab || 'Info';
});
const onBack = () => {

View File

@ -308,6 +308,7 @@ import dayjs from 'dayjs';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import BatchDropdown from '@/components/BatchDropdown/index.vue';
import { BatchActionsType } from '@/components/BatchDropdown/types';
import {useRouterParams} from "@/utils/hooks/useParams";
const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
@ -320,7 +321,7 @@ const operationVisible = ref<boolean>(false);
const api = ref<string>('');
const type = ref<string>('');
const isCheck = ref<boolean>(false);
const routerParams = useRouterParams()
const menuStory = useMenuStore();
const columns = [
@ -546,7 +547,7 @@ const paramsFormat = (
};
onMounted(() => {
if (history.state?.params?.type === 'add') {
if (routerParams.params.value.type === 'add') {
handleAdd();
}
});

View File

@ -61,7 +61,6 @@
</template>
<script setup lang='ts' name="DataAnalysis">
import AIcon from '@/components/AIcon'
import PermissionButton from '@/components/PermissionButton/index.vue'
// import MonacoEditor from '@/components/MonacoEditor/index.vue';
import { useFullscreen } from '@vueuse/core'

View File

@ -114,11 +114,13 @@ import { message } from 'jetlinks-ui-components';
import { getImage, handleParamsToString } from '@/utils/comm'
import encodeQuery from '@/utils/encodeQuery';
import { useMenuStore } from '@/store/menu';
import {useRouterParams} from "@/utils/hooks/useParams";
const menuStory = useMenuStore();
const route = useRoute();
const checked = ref<boolean>(true);
const productStore = useProductStore();
const routerParams = useRouterParams()
const searchParams = ref({
terms1: [
{
@ -291,8 +293,8 @@ const jumpDevice = () => {
);
};
onMounted(() => {
if (history.state?.params?.tab) {
productStore.tabActiveKey = history.state?.params?.tab;
if (routerParams.params?.value.tab) {
productStore.tabActiveKey = routerParams.params?.value.tab;
}
});
</script>

View File

@ -181,6 +181,7 @@ import { typeOptions } from '@/components/Search/util';
import Save from './Save/index.vue';
import { useMenuStore } from 'store/menu';
import { useRoute } from 'vue-router';
import {useRouterParams} from "@/utils/hooks/useParams";
/**
* 表格数据
*/
@ -616,9 +617,9 @@ const saveRef = ref();
const handleSearch = (e: any) => {
params.value = e;
};
const route = useRoute();
const routerParams = useRouterParams()
onMounted(() => {
if(history.state?.params?.save){
if(routerParams.params?.value.save){
add();
}
});

View File

@ -8,12 +8,13 @@
<script setup lang="ts">
import { _control, _stopControl } from '@/api/edge/device';
import {useRouterParams} from "@/utils/hooks/useParams";
const url = ref<string>('');
const deviceId = ref<string>('');
const { params } = useRouterParams()
watch(
() => history.state?.params?.id,
() => params.value.id,
(newId) => {
if (newId) {
deviceId.value = newId as string;

View File

@ -76,18 +76,15 @@ const jumpPage = (item: bootConfig) => {
.box-item {
cursor: pointer;
position: relative;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: rgb(238, 238, 238) rgb(238, 238, 238)
rgb(238, 238, 238) rgb(133, 165, 255);
//border-width: 1px 1px 1px 2px;
//border-style: solid;
//border-color: rgb(238, 238, 238) rgb(238, 238, 238)
// rgb(238, 238, 238) rgb(133, 165, 255);
border: 1px solid #e6e6e6;
padding: 11px;
background: linear-gradient(
135.62deg,
#f6f7fd 22.27%,
rgba(255, 255, 255, 0.86) 91.82%
);
background: linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(135.62deg, rgba(47, 84, 235, 0.07) 22.27%, rgba(47, 84, 235, 0.01) 91.82%);
border-radius: 2px;
box-shadow: 0 4px 18px #efefef;
box-shadow: -2px 0 #85A5FF;
&:not(:first-child) {
margin-top: 12px;
}

View File

@ -12,11 +12,13 @@
<div class="box-list">
<div class="list-item" v-for="item in props.dataList">
<div class="item-content">
<div class="box-top" @click="jumpPage(item)">
<span class="top-title">{{ item.title }}</span>
<img :src="item.iconUrl" alt="" />
</div>
<div class="box-details">{{ item.details }}</div>
</div>
</div>
</div>
</div>
@ -82,6 +84,14 @@ const jumpPage = (row: recommendList) => {
.list-item {
flex: 1;
position: relative;
.item-content {
display: flex;
flex-direction: column;
height: 100%;
&:hover {
box-shadow: @shadow-1-down;
}
}
.box-top {
position: relative;
padding: 16px 24px;
@ -103,6 +113,7 @@ const jumpPage = (row: recommendList) => {
padding: 24px;
border: 1px solid #e5edf4;
border-top: none;
flex: 1 1 auto;
}
&:not(:last-child)::after {

View File

@ -6,12 +6,12 @@
title="导入"
okText="确定"
cancelText="取消"
@ok="handleCancel"
@ok="handleOk"
@cancel="handleCancel"
>
<div style="margin-top: 10px">
<j-form :layout="'vertical'">
<j-form-item label="平台对接" required>
<j-form :layout="'vertical'" :model="modelRef" ref="formRef" :rules="rules">
<j-form-item label="平台对接" required name="configId">
<j-select
showSearch
v-model:value="modelRef.configId"
@ -86,19 +86,24 @@ import { queryPlatformNoPage, _import ,exportCard} from '@/api/iot-card/cardMana
import { message } from 'jetlinks-ui-components';
const emit = defineEmits(['close']);
const emit = defineEmits(['close', 'save']);
const configList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const totalCount = ref<number>(0);
const errCount = ref<number>(0);
const formRef = ref(null)
const importStatus = ref(false)
const modelRef = reactive({
configId: undefined,
upload: [],
fileType: 'xlsx',
});
const rules = {
configId: [{ required: true, message: '请选择平台对接'}]
}
const getConfig = async () => {
const resp: any = await queryPlatformNoPage({
paging: false,
@ -127,6 +132,7 @@ const fileChange = (info: any) => {
_import(modelRef.configId, { fileUrl: r.result })
.then((resp: any) => {
totalCount.value = resp.result.total;
importStatus.value = true
message.success('导入成功')
})
.catch((err) => {
@ -159,9 +165,20 @@ const handleCancel = () => {
totalCount.value = 0;
errCount.value = 0;
modelRef.configId = undefined;
emit('close', true);
if (importStatus.value) {
emit('save', true)
}
importStatus.value = false
};
const handleOk = () => {
formRef.value.validate().then(res => {
handleCancel()
})
}
getConfig();
</script>

View File

@ -370,7 +370,7 @@
</template>
</j-pro-table>
<!-- 批量导入 -->
<Import v-if="importVisible" @close="importVisible = false" />
<Import v-if="importVisible" @close="importVisible = false" @save="importSave"/>
<!-- 批量导出 -->
<Export
v-if="exportVisible"
@ -421,13 +421,15 @@ import { useMenuStore } from 'store/menu';
import BadgeStatus from '@/components/BadgeStatus/index.vue';
import BatchDropdown from '@/components/BatchDropdown/index.vue';
import { BatchActionsType } from '@/components/BatchDropdown/types';
import {usePermissionStore} from "store/permission";
import {useRouterParams} from "@/utils/hooks/useParams";
const router = useRouter();
const menuStory = useMenuStore();
const cardManageRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const _selectedRow = ref<any[]>([]);
// const _selectedRow = ref<any[]>([]);
const bindDeviceVisible = ref<boolean>(false);
const visible = ref<boolean>(false);
const exportVisible = ref<boolean>(false);
@ -468,7 +470,8 @@ const columns = [
scopedSlots: true,
width: 200,
search: {
type: 'string'
type: 'string',
rename: 'deviceName'
}
},
{
@ -587,6 +590,14 @@ const columns = [
scopedSlots: true,
},
];
const btnHasPermission = usePermissionStore().hasPermission;
const paltformPermission = btnHasPermission(`iot-card/Platform:add`);
const importSave = () => {
cardManageRef.value?.reload()
importVisible.value = false
}
const routerParams = useRouterParams()
const getActions = (
data: Partial<Record<string, any>>,
@ -756,7 +767,7 @@ const handleSearch = (e: any) => {
const onSelectChange = (keys: string[], rows: []) => {
_selectedRowKeys.value = [...keys];
_selectedRow.value = [...rows];
// _selectedRow.value = [...rows];
};
const cancelSelect = () => {
@ -819,6 +830,9 @@ const bindDevice = (val: boolean) => {
* 批量激活
*/
const handleActive = () => {
if (!_selectedRowKeys.value.length) {
return message.warn('请选择数据');
}
if (
_selectedRowKeys.value.length >= 10 &&
_selectedRowKeys.value.length <= 100
@ -885,15 +899,15 @@ const handleSync = () => {
* 批量删除
*/
const handelRemove = async () => {
if (!_selectedRow.value.length) {
if (!_selectedRowKeys.value.length) {
message.error('请选择数据');
return;
}
const resp = await removeCards(_selectedRow.value);
const resp = await removeCards(_selectedRowKeys.value);
if (resp.status === 200) {
message.success('操作成功');
_selectedRowKeys.value = [];
_selectedRow.value = [];
// _selectedRow.value = [];
cardManageRef.value?.reload();
}
};
@ -980,6 +994,12 @@ const batchActions: BatchActionsType[] = [
},
},
];
onMounted(() => {
if (routerParams.params.value.type === 'add' && paltformPermission) {
handleAdd()
}
})
</script>
<style scoped lang="less">

View File

@ -148,7 +148,7 @@
<script setup lang="ts">
import Guide from '../components/Guide.vue';
import LineChart from '../components/LineChart.vue';
import moment from 'moment';
import dayjs from 'dayjs';
import { queryFlow } from '@/api/iot-card/home';
import TimeSelect from '@/views/iot-card/components/TimeSelect.vue';
import { Empty } from 'ant-design-vue';
@ -200,16 +200,16 @@ const getData = (
*/
const getDataTotal = () => {
const dTime = [
moment(new Date()).startOf('day').valueOf(),
moment(new Date()).endOf('day').valueOf(),
dayjs(new Date()).startOf('day').valueOf(),
dayjs(new Date()).endOf('day').valueOf(),
];
const mTime = [
moment().startOf('month').valueOf(),
moment().endOf('month').valueOf(),
dayjs().startOf('month').valueOf(),
dayjs().endOf('month').valueOf(),
];
const yTime = [
moment().startOf('year').valueOf(),
moment().endOf('year').valueOf(),
dayjs().startOf('year').valueOf(),
dayjs().endOf('year').valueOf(),
];
getData(dTime[0], dTime[1]).then((resp) => {
dayTotal.value = resp.data
@ -238,9 +238,10 @@ const getDataTotal = () => {
const getEcharts = (data: any) => {
let startTime = data.start;
let endTime = data.end;
if (data.type === 'week' || data.type === 'month') {
startTime = moment(data.start).startOf('days').valueOf();
endTime = moment(data.end).startOf('days').valueOf();
if (data.type !== 'day') {
startTime = dayjs(data.start).startOf('days').valueOf();
endTime = dayjs(data.end).startOf('days').valueOf();
}
getData(startTime, endTime).then((resp) => {
flowData.value = resp.sortArray;

View File

@ -179,7 +179,6 @@ const pieChartData = ref<any[]>([
]);
const jumpPage = (data: GuideItemProps) => {
console.log(data.auth)
if (!data.auth){
message.warning('暂无权限,请联系管理员');
return
@ -187,7 +186,11 @@ const jumpPage = (data: GuideItemProps) => {
if (data.key === 'EQUIPMENT') {
menuStory.jumpPage(data.url, { id: ':id'});
} else {
menuStory.jumpPage(data.url);
let params: any = undefined
if (data.key === 'SCREEN') {
params = { type: 'add'}
}
menuStory.jumpPage(data.url, params);
}
};

View File

@ -70,10 +70,11 @@ const rangeVal = ref<[string, string]>();
const rangeChange = (val: any) => {
radioValue.value = undefined;
const differenceTime = dayjs(val[1]).valueOf() - dayjs(val[0]).valueOf()
emit('change', {
start: dayjs(val[0]).valueOf(),
end: dayjs(val[1]).valueOf(),
type: undefined,
type: differenceTime > (24 * 60 * 60 * 1000) ? undefined : 'day',
});
};
@ -103,6 +104,7 @@ const handleBtnChange = (val?: string) => {
dayjs(startTime).format('YYYY-MM-DD HH:mm:ss'),
dayjs(endTime).format('YYYY-MM-DD HH:mm:ss'),
];
console.log(startTime, endTime)
emit('change', {
start: startTime,
end: endTime,

View File

@ -53,8 +53,10 @@ import { message } from 'jetlinks-ui-components';
import { useAlarmStore } from '@/store/alarm';
import Info from './info.vue';
import { storeToRefs } from 'pinia';
import {useRouterParams} from "@/utils/hooks/useParams";
const route = useRoute();
const id = route.params?.id;
const { params: routerParams } = useRouterParams()
let visiable = ref(false);
const columns = [
{
@ -181,7 +183,7 @@ const close = () => {
watchEffect(()=>{
current.value = details.value;
if(history.state?.params.detail && details.value){
if(routerParams.value.detail && details.value){
visiable.value = true;
}
})

View File

@ -151,9 +151,11 @@ import { getImage } from '@/utils/comm';
import { message } from 'jetlinks-ui-components';
import Save from './Save/index.vue';
import { SystemConst } from '@/utils/consts';
import {useRouterParams} from "@/utils/hooks/useParams";
const params = ref<Record<string, any>>({});
let visiable = ref(false);
const tableRef = ref<Record<string, any>>({});
const { params: routeParams } = useRouterParams()
const query = {
columns: [
{

View File

@ -23,11 +23,16 @@
</span>
</j-col>
<j-col :span='24' v-if='showTable'>
<FunctionCall
:value='_value'
:data='callDataOptions'
@change='callDataChange'
/>
<j-form-item
name='data'
:rules="rules"
>
<FunctionCall
v-model:value='formModel.data'
:data='callDataOptions'
@change='callDataChange'
/>
</j-form-item>
</j-col>
</j-row>
</j-form>
@ -61,8 +66,9 @@ const props = defineProps({
const emit = defineEmits<Emit>()
const formModel = reactive<{ reportKey: string | undefined }>({
reportKey: undefined
const formModel = reactive<{ reportKey: string | undefined, data: any[] }>({
reportKey: undefined,
data: Object.keys(props.value).map(key => ({ name: key, value: props.value[key] })) || []
})
const callData = ref<Array<{ id: string, value: string | undefined }>>()
@ -97,6 +103,10 @@ const callDataOptions = computed(() => {
return []
})
nextTick(() => {
formModel.reportKey = Object.keys(props.value)[0]
})
const showTable = computed(() => {
return !!formModel.reportKey
})
@ -116,6 +126,23 @@ const callDataChange = (v: any[]) => {
})
}
const rules = [{
validator(_: string, value: any) {
console.log(value, callDataOptions.value)
if (!value?.length && callDataOptions.value.length) {
return Promise.reject('请选择属性值')
} else {
let hasValue = value.find((item: { name: string, value: any}) => !item.value)
if (hasValue) {
const item = callDataOptions.value.find((item: any) => item.id === hasValue.name)
console.log()
return Promise.reject(item?.name ? `请输入${item?.name}` : '请输入属性值')
}
}
return Promise.resolve();
}
}]
const initRowKey = () => {
if (props.value.length) {
const keys = Object.keys(props.value)

View File

@ -23,7 +23,8 @@ const { data } = storeToRefs(sceneStore);
const actionRules = [{
validator(_: any, v?: BranchesThen[]) {
if (!v || (v && !v.length) || (v && v.length && !v[0].actions.length)) {
if (!v || (v && !v.length) || !v.some(item => item.actions && item.actions.length)) {
return Promise.reject('至少配置一个执行动作');
}
return Promise.resolve();

View File

@ -59,7 +59,7 @@ const rules = [{
const actionRules = [{
validator(_: any, v?: BranchesThen[]) {
if (!v || (v && !v.length)) {
if (!v || (v && !v.length) || !v.some(item => item.actions && item.actions.length)) {
return Promise.reject('至少配置一个执行动作');
}
return Promise.resolve();

View File

@ -0,0 +1,102 @@
<template>
<slot />
</template>
<script setup lang='ts' name='ActionCheckItem'>
import { ActionsType } from '@/views/rule-engine/Scene/typings'
import { useSceneStore } from '@/store/scene';
import { storeToRefs } from 'pinia';
import { queryProductList } from '@/api/device/product'
import { query as deviceQuery } from '@/api/device/instance'
import noticeConfig from '@/api/notice/config'
import noticeTemplate from '@/api/notice/template'
import { Form } from 'jetlinks-ui-components'
const sceneStore = useSceneStore();
const { data: _data } = storeToRefs(sceneStore);
const formItemContext = Form.useInjectFormItemContext()
const props = defineProps({
branchesName: {
type: Number,
default: 0,
},
thenName: {
type: Number,
default: 0,
},
name: {
type: Number,
default: 0,
},
});
const rules = [{
validator(_: any, v?: ActionsType) {
console.log('validator',v)
if (v?.executor === 'device') {
if(!v.device?.productId || !v.device?.selectorValues) {
return Promise.reject(new Error('该数据已发生变更,请重新配置'))
}
}
return Promise.resolve()
}
}]
const formTouchOff = () => {
formItemContext.onFieldChange()
}
/**
* 校验当前执行动作的设备或者产品是否删除
*/
const checkDeviceDelete = async () => {
const item = _data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device
const proResp = await queryProductList({ terms: [{ terms: [{ column: 'id', termType: 'eq', value: item!.productId }]}]})
if (proResp.success && (proResp.result as any)?.total === 0) { //
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device!.productId = undefined
formTouchOff()
return
}
const deviceList = item!.selectorValues?.map(item => item.value) || []
const deviceResp = await deviceQuery({ terms: [{ terms: [{ column: 'id', termType: 'in', value: deviceList.toString() }]}]})
if (deviceResp.success && (deviceResp.result as any)?.total < (item!.selectorValues?.length || 0)) { //
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device!.selectorValues = undefined
formTouchOff()
return
}
}
/**
* 校验当前执行动作的通知配置消息模板是否删除
*/
const checkNoticeDelete = async () => {
const item = _data.value.branches![props.branchesName].then[props.thenName].actions[props.name].notify
const configResp = await noticeConfig.list({ terms: [{ terms: [{ column: 'id', termType: 'eq', value: item!.notifierId }]}]})
if (configResp.success && (configResp.result as any)?.total === 0) {
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].notify!.notifierId = ''
formTouchOff()
return
}
const templateResp = await noticeTemplate.list({ terms: [{ terms: [{ column: 'id', termType: 'eq', value: item!.templateId }]}]})
if (templateResp.success && (templateResp.result as any)?.total === 0) {
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].notify!.templateId = ''
formTouchOff()
return
}
}
nextTick(() => {
const _executor = _data.value.branches![props.branchesName].then[props.thenName].actions[props.name]?.executor
if (_executor === 'device' && _data.value.branches![props.branchesName].then[props.thenName].actions[props.name]?.device) {
checkDeviceDelete()
} else if (_executor === 'notify' && _data.value.branches![props.branchesName].then[props.thenName].actions[props.name]?.notify) {
checkNoticeDelete
}
})
</script>
<style scoped>
</style>

View File

@ -43,7 +43,7 @@
value-name='id'
label-name='name'
:options='valueOptions'
:metricOptions='columnOptions'
:metricOptions='valueColumnOptions'
:tabsOptions='tabsOptions'
v-model:value='paramsValue.value.value'
v-model:source='paramsValue.value.source'
@ -56,7 +56,7 @@
value-name='id'
label-name='name'
:options='valueOptions'
:metricOptions='columnOptions'
:metricOptions='valueColumnOptions'
:tabsOptions='tabsOptions'
v-model:value='paramsValue.value.value'
v-model:source='paramsValue.value.source'
@ -83,8 +83,9 @@ import ParamsDropdown, { DoubleParamsDropdown } from '../../components/ParamsDro
import { inject } from 'vue'
import { useSceneStore } from 'store/scene'
import { storeToRefs } from 'pinia';
import { flattenDeep, set } from 'lodash-es'
import {cloneDeep, flattenDeep, isArray, set} from 'lodash-es'
import { Form } from 'jetlinks-ui-components'
import {treeFilter} from "@/utils/comm";
const sceneStore = useSceneStore()
const { data: formModel } = storeToRefs(sceneStore)
@ -160,6 +161,7 @@ const columnOptions: any = inject('filter-params') //
const termTypeOptions = ref<Array<{ id: string, name: string}>>([]) //
const valueOptions = ref<any[]>([]) //
const arrayParamsKey = ['nbtw', 'btw', 'in', 'nin']
const valueColumnOptions = ref<any[]>([])
const tabsOptions = ref<Array<TabsOption>>(
[
@ -182,9 +184,11 @@ const handOptionByColumn = (option: any) => {
} else{
valueOptions.value = option.options || []
}
valueColumnOptions.value = treeFilter(cloneDeep(columnOptions.value), option.type, 'type')
} else {
termTypeOptions.value = []
valueOptions.value = []
valueColumnOptions.value = []
}
}
@ -259,7 +263,8 @@ const columnSelect = (e: any) => {
}
const termsTypeSelect = (e: { key: string, name: string }) => {
const value = arrayParamsKey.includes(e.key) ? [ undefined, undefined ] : undefined
const oldValue = isArray(paramsValue.value!.value) ? paramsValue.value!.value[0] : paramsValue.value!.value
const value = arrayParamsKey.includes(e.key) ? [ oldValue, undefined ] : oldValue
paramsValue.value = {
source: tabsOptions.value[0].key,
value: value
@ -276,6 +281,8 @@ const valueSelect = (_: any, label: string, labelObj: Record<number, any>) => {
}
const typeChange = (e: any) => {
paramsValue.type = e.value
emit('update:value', { ...paramsValue })
formModel.value.branches![props.branchName].then[props.thenName].actions[props.actionName].options!.terms[props.termsName].terms[props.name][3] = e.label
}

View File

@ -63,6 +63,7 @@ import { flattenDeep, isArray } from 'lodash-es'
import { provide } from 'vue'
import { randomString } from '@/utils/utils'
import { getParams, EventEmitter, EventSubscribeKeys } from '@/views/rule-engine/Scene/Save/util'
import { handleParamsData } from '@/views/rule-engine/Scene/Save/components/Terms/util'
const sceneStore = useSceneStore()
const { data: formModel } = storeToRefs(sceneStore)
@ -119,7 +120,7 @@ const columnRequest = () => {
action: props.actionName
}
getParams(param, formModel.value).then(res => {
columnOptions.value = res
columnOptions.value = handleParamsData(res, 'id')
})
}
@ -226,6 +227,9 @@ const rules = [
}
}
} else {
if (v?.error) { //
return Promise.reject(new Error('该数据已发生变更,请重新配置'))
}
return Promise.reject(new Error('请选择参数'))
}
return Promise.resolve()

View File

@ -1,6 +1,11 @@
<template>
<div class="actions-item-warp">
<j-form-item
:name='["branches", branchesName, "then", thenName, "actions", name]'
:rules='rules'
>
<div class="actions-item">
<CheckItem v-bind='props'>
<div class="item-options-warp">
<div class="item-options-type" @click="onAdd">
<img
@ -120,7 +125,7 @@
</div>
</template>
<template v-else-if="data?.notify?.notifyType === 'email'">
<div>
<div style="display: flex;">
通过
<span class="notify-text-highlight">
<img
@ -133,9 +138,13 @@
/>
邮件
</span>
<span class="notify-text-highlight">{{
options?.sendTo || ''
}}</span>
<span class="notify-text-highlight">
<Ellipsis style='max-width: 400px;'>
{{
options?.sendTo || ''
}}
</Ellipsis>
</span>
发送
<span class="notify-text-highlight">
{{
@ -256,19 +265,23 @@
<Ellipsis style='max-width: 400px;'>
{{data?.options?.properties}}
</Ellipsis>
<Ellipsis style='max-width: 200px;'>
{{
`${
(
isBoolean(
data?.options?.propertiesValue,
)
? true
: data?.options?.propertiesValue
)
? `${data?.options?.propertiesValue}`
: ''
}`
`${
(
isBoolean(
data?.options?.propertiesValue,
)
? true
: data?.options?.propertiesValue
)
? `${data?.options?.propertiesValue}`
: ''
}`
}}
</Ellipsis>
</div>
</template>
<template v-else-if="data?.device?.selector === 'tag'">
@ -322,12 +335,14 @@
<j-button v-else @click="onAdd">点击配置执行动作</j-button>
</div>
<div class="item-number">{{ name + 1 }}</div>
<j-popconfirm title="确认删除?" @confirm="onDelete">
<j-popconfirm title="确认删除?" @confirm="onDelete" :overlayStyle='{minWidth: "180px"}'>
<div class="item-delete">
<AIcon type="DeleteOutlined" />
</div>
</j-popconfirm>
</CheckItem>
</div>
</j-form-item>
<template v-if="!isLast && type === 'serial'">
<div
:class="[
@ -399,6 +414,8 @@ import { iconMap, itemNotifyIconMap, typeIconMap } from './util';
import FilterGroup from './FilterGroup.vue';
import { randomString } from '@/utils/utils'
import { EventEmitter, EventEmitterKeys } from '@/views/rule-engine/Scene/Save/util'
import CheckItem from './CheckItem.vue'
import { Form } from 'jetlinks-ui-components'
const sceneStore = useSceneStore();
const { data: _data } = storeToRefs(sceneStore);
@ -443,7 +460,7 @@ const eventEmitterKey = EventEmitterKeys({
branchGroup: props.thenName,
action: props.name
})
const formItemContext = Form.useInjectFormItemContext()
const termsOptions = computed(() => {
if (!props.parallel) {
//
@ -543,6 +560,56 @@ const onPropsCancel = () => {
actionType.value = '';
};
const rules = [{
validator(_: any, v?: ActionsType) {
console.log('validator',v)
if (v?.executor === 'device') {
if(!v.device?.productId || !v.device?.selectorValues) {
return Promise.reject(new Error('该数据已发生变更,请重新配置'))
}
}
return Promise.resolve()
}
}]
const formTouchOff = () => {
console.log('formTouchOff')
formItemContext.onFieldChange()
}
/**
* 校验当前执行动作的设备或者产品是否删除
*/
const checkDeviceDelete = async () => {
const item = _data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device
const proResp = await queryProductList({ terms: [{ terms: [{ column: 'id', termType: 'eq', value: item!.productId }]}]})
if (proResp.success && (proResp.result as any)?.total === 0) { //
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device!.productId = undefined
formTouchOff()
return
}
const deviceList = item!.selectorValues?.map(item => item.value) || []
const deviceResp = await deviceQuery({ terms: [{ terms: [{ column: 'id', termType: 'in', value: deviceList.toString() }]}]})
if (deviceResp.success && (deviceResp.result as any)?.total < (item!.selectorValues?.length || 0)) { //
_data.value.branches![props.branchesName].then[props.thenName].actions[props.name].device!.selectorValues = undefined
formTouchOff()
return
}
}
/**
* 校验当前执行动作的通知配置消息模板是否删除
*/
const checkNoticeDelete = () => {
}
nextTick(() => {
if (_data.value.branches![props.branchesName].then[props.thenName].actions[props.name]?.executor === 'device') {
checkDeviceDelete()
}
})
</script>
<style lang="less" scoped>
@ -571,7 +638,7 @@ const onPropsCancel = () => {
.actions-item {
position: relative;
margin-bottom: 24px;
//margin-bottom: 24px;
padding: 16px;
border: 1px dashed #999;
border-radius: 2px;

View File

@ -73,6 +73,19 @@ const thenName = computed(() => {
return _data.value.branches![props.branchesName].then.findIndex(item => item.parallel === props.parallel)
})
const rules = [{
validator(_: any, v?: ActionsType) {
console.log('validator',v)
if (v?.executor === 'device') {
if(!v.device?.productId || !v.device?.selectorValues) {
return Promise.reject(new Error('该数据已发生变更,请重新配置'))
}
}
return Promise.resolve()
}
}]
const onAdd = () => {
visible.value = true;
};

View File

@ -15,10 +15,12 @@
:treeData="builtInList"
placeholder="请选择参数"
style="width: calc(100% - 120px)"
:fieldNames="{ label: 'name', value: 'id' }"
@change="(val) => itemOnChange(undefined, val)"
>
<template #title="{ name, description }">
<template #title="{ fullName, description }">
<j-space>
{{ name }}
{{ fullName }}
<span style="color: grey; margin-left: 5px">{{ description }}</span>
</j-space>
</template>
@ -95,14 +97,15 @@ const sourceChange = (val: any) => {
emit('update:value', {
...props.value,
source: val,
value: undefined,
value: undefined
});
};
const itemOnChange = (val: any) => {
const itemOnChange = (val: any, _upperKey?: string) => {
emit('update:value', {
...props.value,
value: val,
upperKey: _upperKey
});
};

View File

@ -151,6 +151,7 @@ const onAdd = (actionItem: any, _parallel: boolean) => {
}
formItemContext.onFieldChange()
}
</script>
<style lang='less' scoped>

View File

@ -27,15 +27,17 @@
v-if='component === "select"'
:value='selectValue'
:options='options'
:valueName='valueName'
@click='menuSelect'
/>
<div style='min-width: 400px' v-else>
<j-tree
v-model:expandedKeys="treeOpenKeys"
:selectedKeys='selectValue ? [selectValue] : []'
:treeData='options'
@select='treeSelect'
:height='450'
:virtual='true'
@select='treeSelect'
>
<template #title="{ name, description }">
<j-space>
@ -60,6 +62,7 @@ import DropMenus from './Menus.vue'
import DropdownTimePicker from './Time.vue'
import { getOption } from './util'
import type { DropdownButtonOptions } from './util'
import {openKeysByTree} from "@/utils/comm";
type LabelType = string | number | boolean | undefined
@ -108,7 +111,7 @@ const emit = defineEmits<Emit>()
const label = ref<LabelType>(props.placeholder)
const selectValue = ref(props.value)
const visible = ref(false)
const treeOpenKeys = ref<(string|number)[]>([])
const visibleChange = (v: boolean) => {
visible.value = v
}
@ -147,10 +150,11 @@ const menuSelect = (v: string, option: any) => {
watchEffect(() => {
const option = getOption(props.options, props.value, props.valueName)
selectValue.value = props.value
if (option) {
if (option) { //
label.value = option[props.labelName] || option.name
treeOpenKeys.value = openKeysByTree(props.options, props.value, props.valueName)
} else {
label.value = props.value || props.placeholder
label.value = props.value !== undefined ? props.value : props.placeholder
}
})

View File

@ -1,7 +1,9 @@
<template>
<j-menu class='scene-dropdown-menus' @click='click' :selectedKeys='[myValue]'>
<j-menu-item v-for='item in myOptions' :key='item.value' :title='item.label'>
{{ item.label }}
<Ellipsis >
{{ item.label }}
</Ellipsis>
</j-menu-item>
</j-menu>
</template>
@ -24,6 +26,10 @@ const props = defineProps({
options: {
type: Array,
default: () => []
},
valueName: {
type: String,
default: 'value'
}
})
@ -53,7 +59,7 @@ const handleBoolean = (key: string) => {
const click = (e: any) => {
const _key = ['true', 'false'].includes(e.key) ? handleBoolean(e.key) : e.key
const option = getOption(myOptions.value, _key)
const option = getOption(myOptions.value, _key, props.valueName)
myValue.value = _key
emit('update:value', _key)
emit('click', _key, {

View File

@ -6,6 +6,7 @@
v-model:value='myValue'
class='manual-time-picker'
:format='myFormat'
:valueFormat='myFormat'
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ -16,6 +17,7 @@
class='manual-time-picker'
v-model:value='myValue'
:format='myFormat'
:valueFormat='myFormat'
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
@ -48,15 +50,17 @@ const props = defineProps({
const emit = defineEmits<Emit>()
const myFormat = props.format || ( props.type === 'time' ? 'HH:mm:ss' : 'YYYY-MM-DD HH:mm:ss')
const myValue = ref<Dayjs>(dayjs(props.value || new Date(), myFormat))
// const myValue = ref<Dayjs>(dayjs(props.value || new Date(), myFormat))
const myValue = ref<string>(props.value || dayjs(new Date()).format(myFormat))
const getPopupContainer = (trigger: HTMLElement) => {
return trigger?.parentNode || document.body
}
const change = (e: Dayjs) => {
emit('update:value', e.format(myFormat))
emit('change', e.format(myFormat))
const change = (e: string) => {
myValue.value = e
emit('update:value', e)
emit('change', e)
}
</script>

View File

@ -1,3 +1,5 @@
import { isEqual } from 'lodash-es'
export type DropdownButtonOptions = {
label: string;
value: string;
@ -34,7 +36,7 @@ export const getOption = (data: any[], value?: string | number | boolean, key: s
if (value === undefined && value === null) return option
for (let i = 0; i < data.length; i++) {
const item = data[i]
if (item[key] === value) {
if (isEqual(item[key], value)) {
option = data[i]
break
} else if (item.children && item.children.length) {

View File

@ -3,6 +3,9 @@
class='scene-select-value'
trigger='click'
v-model:visible='visible'
:overlayStyle='{
maxWidth: "300px"
}'
@visibleChange='visibleChange'
>
<div @click.prevent='visible = true'>
@ -35,6 +38,7 @@
<DropdownMenus
v-if='(["metric", "upper"].includes(item.key) ? metricOptions : options).length'
:options='["metric", "upper"].includes(item.key) ? metricOptions : options'
:valueName='valueName'
@click='onSelect'
/>
<div class='scene-select-empty' v-else>
@ -44,11 +48,12 @@
<template v-else-if='item.component === "tree"'>
<div style='min-width: 400px' v-if='(item.key === "upper" ? metricOptions : options).length'>
<j-tree
v-model:expandedKeys="treeOpenKeys"
:selectedKeys='myValue ? [myValue] : []'
:treeData='item.key === "upper" ? metricOptions : options'
@select='treeSelect'
:height='450'
:virtual='true'
@select='treeSelect'
>
<template #title="{ name, description }">
<j-space>
@ -84,6 +89,8 @@ import type { ValueType } from './typings'
import { defaultSetting } from './typings'
import { DropdownMenus, DropdownTimePicker} from '../DropdownButton'
import { getOption } from '../DropdownButton/util'
import { isArray } from 'lodash-es'
import {openKeysByTree} from "@/utils/comm";
type Emit = {
(e: 'update:value', data: ValueType): void
@ -101,6 +108,7 @@ const emit = defineEmits<Emit>()
const myValue = ref<ValueType>(props.value)
const mySource = ref<string>(props.source)
const label = ref<any>(props.placeholder)
const treeOpenKeys = ref<(string|number)[]>([])
const visible = ref(false)
nextTick(() => {
@ -120,7 +128,7 @@ const tabsChange = (e: string) => {
const treeSelect = (v: any, option: any) => {
const node = option.node
visible.value = false
label.value = node.fullname || node.name
label.value = node[props.labelName] || node.name
emit('update:value', node[props.valueName])
emit('select', node, label.value, { 0: label.value })
}
@ -133,7 +141,7 @@ const valueItemChange = (e: string) => {
const onSelect = (e: string, option: any) => {
visible.value = false
label.value = option.label
label.value = option[props.labelName]
emit('update:value', e)
emit('select', e, label.value, { 0: label.value })
}
@ -150,14 +158,19 @@ const visibleChange = (v: boolean) => {
}
watchEffect(() => {
const _options = props.source === 'upper' ? props.metricOptions : props.options
const _options = ['metric', 'upper'].includes(props.source) ? props.metricOptions : props.options
const option = getOption(_options, props.value as string, props.valueName) // label
myValue.value = props.value
mySource.value = props.source
if (option) {
label.value = option[props.labelName] || option.name
treeOpenKeys.value = openKeysByTree(_options, props.value, props.valueName)
} else {
label.value = props.value || props.placeholder
let doubleNull = false
if (isArray(props.value)) {
doubleNull = !!props.value.filter(item => !!item).length
}
label.value = props.value !== undefined && !doubleNull ? props.value : props.placeholder
}
})

View File

@ -7,6 +7,7 @@
<j-popconfirm
title='确认删除?'
@confirm='onDelete'
:overlayStyle='{minWidth: "180px"}'
>
<div v-if='!isFirst' class='terms-params-delete danger show'>
<AIcon type='DeleteOutlined' />

View File

@ -58,7 +58,7 @@
v-model:source='paramsValue.value.source'
@select='valueSelect'
/>
<j-popconfirm title='确认删除?' @confirm='onDelete'>
<j-popconfirm title='确认删除?' @confirm='onDelete' :overlayStyle='{minWidth: "180px"}'>
<div v-show='showDelete' class='button-delete'> <AIcon type='CloseOutlined' /></div>
</j-popconfirm>
</div>
@ -81,7 +81,7 @@ import { ContextKey } from './util'
import { useSceneStore } from 'store/scene'
import { storeToRefs } from 'pinia';
import { Form } from 'jetlinks-ui-components'
import { pick } from 'lodash-es'
import {isArray, pick} from 'lodash-es'
const sceneStore = useSceneStore()
const { data: formModel } = storeToRefs(sceneStore)
@ -154,14 +154,15 @@ const columnOptions: any = inject(ContextKey) //
const termTypeOptions = ref<Array<{ id: string, name: string}>>([]) //
const valueOptions = ref<any[]>([]) //
const metricOption = ref<any[]>([]) // termType
const isMetric = ref<boolean>(false) //
const tabsOptions = ref<Array<TabsOption>>([{ label: '手动输入', key: 'manual', component: 'string' }])
const arrayParamsKey = ['nbtw', 'btw', 'in', 'nin']
let metricsCacheOption: any[] = [] //
const metricsCacheOption = ref<any[]>([]) //
const handOptionByColumn = (option: any) => {
if (option) {
termTypeOptions.value = option.termTypes || []
metricsCacheOption = option.metrics || []
metricsCacheOption.value = option.metrics?.map((item: any) => ({...item, label: item.name})) || []
tabsOptions.value.length = 1
tabsOptions.value[0].component = option.dataType
@ -169,6 +170,9 @@ const handOptionByColumn = (option: any) => {
tabsOptions.value.push(
{ label: '指标值', key: 'metric', component: 'select' }
)
isMetric.value = true
} else {
isMetric.value = false
}
if (option.dataType === 'boolean') {
@ -183,7 +187,7 @@ const handOptionByColumn = (option: any) => {
}
} else {
termTypeOptions.value = []
metricsCacheOption = []
metricsCacheOption.value = []
valueOptions.value = []
}
}
@ -212,12 +216,12 @@ watch(() => [columnOptions.value, paramsValue.column], () => {
const showDouble = computed(() => {
const isRange = paramsValue.termType ? arrayParamsKey.includes(paramsValue.termType) : false
if (metricsCacheOption.length) {
metricOption.value = metricsCacheOption.filter(item => isRange ? item.range : !item.range)
if (metricsCacheOption.value.length) {
metricOption.value = metricsCacheOption.value.filter(item => isRange ? item.range : !item.range)
} else {
metricOption.value = []
}
return isRange
return isRange && !isMetric
})
const mouseover = () => {
@ -244,11 +248,11 @@ const columnSelect = (option: any) => {
formItemContext.onFieldChange()
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][0] = option.name
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][1] = paramsValue.termType
}
const termsTypeSelect = (e: { key: string, name: string }) => {
const value = arrayParamsKey.includes(e.key) ? [ undefined, undefined ] : undefined
const oldValue = isArray(paramsValue.value!.value) ? paramsValue.value!.value[0] : paramsValue.value!.value
const value = arrayParamsKey.includes(e.key) ? [ oldValue, undefined ] : oldValue
paramsValue.value = {
source: tabsOptions.value[0].key,
value: value
@ -266,6 +270,7 @@ const valueSelect = (_: any, label: string, labelObj: Record<number, any>) => {
}
const typeSelect = (e: any) => {
emit('update:value', { ...paramsValue })
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[props.name][3] = e.label
}

View File

@ -6,6 +6,7 @@
>
<j-popconfirm
title='确认删除?'
:overlayStyle='{minWidth: "180px"}'
@confirm='onDelete'
>
<div v-show='showDelete' class='terms-params-delete'>
@ -97,7 +98,7 @@ const rules = [
if (!v.termType) {
return Promise.reject(new Error('请选择操作符'));
}
if (!v.value?.value) {
if (v.value?.value === undefined) {
return Promise.reject(new Error('请选择或输入参数值'));
}
if (
@ -107,6 +108,9 @@ const rules = [
return Promise.reject(new Error('请选择或输入参数值'));
}
} else {
if (v?.error) { //
return Promise.reject(new Error('该数据已发生变更,请重新配置'))
}
return Promise.reject(new Error('请选择参数'));
}
return Promise.resolve();

View File

@ -13,11 +13,8 @@ export const handleParamsData = (data: any[], key: string = 'column'): any[] =>
export const thenRules = [{
validator(_: string, value: any) {
if (!value || (value && !value.length)) {
if (!value || (value && !value.length) || !value.some(item => item.actions && item.actions.length)) {
return Promise.reject('至少配置一个执行动作')
} else {
const isActions = value.some((item: any) => item.actions && item.actions.length)
return isActions ? Promise.resolve() : Promise.reject('至少配置一个执行动作');
}
return Promise.resolve();
}

View File

@ -3,7 +3,6 @@ import { handleParamsData } from './components/Terms/util'
import { useSceneStore } from 'store/scene'
import { storeToRefs } from 'pinia'
import type { FormModelType } from '@/views/rule-engine/Scene/typings'
import { isArray } from 'lodash-es'
interface Params {
branch: number

View File

@ -52,14 +52,14 @@
{{ slotProps.name }}
</span>
</Ellipsis>
<div class="subTitle">
<Ellipsis :lineClamp="2">
<div class="subTitle">
说明{{
slotProps?.description ||
typeMap.get(slotProps.triggerType)?.tip
}}
</div>
</Ellipsis>
</div>
</template>
<template #actions="item">
<PermissionButton

View File

@ -103,11 +103,6 @@
<j-checkbox-group
v-model:value="form.data.integrationModes"
:options="joinOptions"
@change="
form.integrationModesISO = [
...form.data.integrationModes,
]
"
/>
</j-form-item>
@ -333,6 +328,7 @@
.clientId
"
placeholder="请输入appId"
:disabled="!!form.data.id"
/>
</j-form-item>
<j-form-item
@ -408,7 +404,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入授权地址',
},
]"
>
@ -439,7 +435,7 @@
:rules="[
{
required: true,
message: '请选择认证方式',
message: '请选择请求方式',
},
]"
>
@ -448,6 +444,7 @@
form.data.apiClient.authConfig.oauth2
.tokenRequestType
"
placeholder="请选择请求方式"
>
<j-select-option value="POST_BODY">
请求体
@ -469,7 +466,11 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入client_id',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
@ -499,7 +500,11 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入client_secret',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
@ -537,6 +542,10 @@
required: true,
message: '该字段是必填字段',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
@ -560,6 +569,10 @@
required: true,
message: '该字段是必填字段',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
@ -576,7 +589,12 @@
form.data.apiClient.authConfig.type === 'bearer'
"
label="token"
:name="['apiClient', 'authConfig', 'token']"
:name="[
'apiClient',
'authConfig',
'bearer',
'token',
]"
:rules="[
{
required: true,
@ -586,7 +604,7 @@
>
<j-input
v-model:value="
form.data.apiClient.authConfig.token
form.data.apiClient.authConfig.bearer.token
"
placeholder="请输入token"
/>
@ -594,7 +612,18 @@
</div>
<div v-if="form.data.provider !== 'internal-integrated'">
<j-form-item>
<j-form-item
:name="['apiClient', 'headers']"
:rules="[
{
required: !headerValid,
message: '请输入请求头',
},
{
validator: headerValidator,
},
]"
>
<template #label>
<FormLabel
text="请求头"
@ -604,11 +633,25 @@
<RequestTable
v-model:value="form.data.apiClient.headers"
v-model:valid="headerValid"
/>
</j-form-item>
<j-form-item label="参数">
<j-form-item
label="参数"
:name="['apiClient', 'parameters']"
:rules="[
{
required: !paramsValid,
message: '请输入参数',
},
{
validator: paramsValidator,
},
]"
>
<RequestTable
v-model:value="form.data.apiClient.parameters"
v-model:valid="paramsValid"
/>
</j-form-item>
</div>
@ -657,10 +700,6 @@
required: true,
message: '请输入secureKey',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
@ -696,7 +735,7 @@
:rules="[
{
required: true,
message: '请选角色',
message: '请选角色',
},
]"
>
@ -711,7 +750,7 @@
v-model:value="form.data.apiServer.roleIdList"
:options="form.roleIdList"
mode="multiple"
placeholder="请选角色"
placeholder="请选角色"
></j-select>
<PermissionButton
:hasPermission="`${rolePermission}:update`"
@ -749,6 +788,9 @@
multiple
:tree-data="form.orgIdList"
placeholder="请选择组织"
:filterTreeNode="
(v: string, node: any) => filterSelectNode(v, node, 'name')
"
>
<template #title="{ name }">
{{ name }}
@ -760,7 +802,7 @@
@click="
clickAddItem(
form.data.apiServer.orgIdList,
'Role',
'Department',
)
"
class="add-item"
@ -821,7 +863,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请选择认证方式',
},
]"
>
@ -948,7 +990,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入授权地址',
},
]"
>
@ -981,7 +1023,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入token地址',
},
]"
>
@ -1002,7 +1044,7 @@
<j-form-item label="logo">
<j-upload
v-model:file-list="form.fileList"
accept=".jpg,.png,.jfif,.pjp,.pjpeg,.jpeg"
accept=".jpg,.png"
:maxCount="1"
list-type="picture-card"
:show-upload-list="false"
@ -1010,6 +1052,7 @@
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
:beforeUpload="beforeLogoUpload"
@change="changeBackUpload"
>
<img
@ -1050,7 +1093,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入用户信息地址',
},
]"
>
@ -1074,7 +1117,7 @@
:rules="[
{
required: true,
message: '请输入该字段是必填字段用户ID',
message: '请输入用户ID',
},
]"
>
@ -1105,7 +1148,7 @@
:rules="[
{
required: true,
message: '该字段是必填字段',
message: '请输入用户名',
},
]"
>
@ -1165,6 +1208,10 @@
required: true,
message: '请输入appId',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
@ -1196,6 +1243,10 @@
required: true,
message: '请输入appKey',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
@ -1227,6 +1278,10 @@
required: true,
message: '请输入appSecret',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
@ -1281,6 +1336,10 @@
required: true,
message: '请输入默认密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
@ -1294,7 +1353,7 @@
v-model:value="form.data.sso.roleIdList"
mode="multiple"
:options="form.roleIdList"
placeholder="请选角色"
placeholder="请选角色"
></j-select>
<PermissionButton
:hasPermission="`${rolePermission}:update`"
@ -1383,7 +1442,7 @@
<script setup lang="ts">
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { LocalStore, filterSelectNode } from '@/utils/comm';
import {
getDepartmentList_api,
@ -1436,7 +1495,7 @@ const initForm: formType = {
type: 'oauth2', // , none, bearer, oauth2, basic, other
bearer: { token: '' }, //
basic: { username: '', password: '' }, //
token: '',
// token: '',
oauth2: {
// OAuth2
authorizationUrl: '', //
@ -1446,7 +1505,7 @@ const initForm: formType = {
clientSecret: '', //
grantType: '', //
accessTokenProperty: '', // token
tokenRequestType: '', // token, POST_URIPOST_BODY
tokenRequestType: undefined, // token, POST_URIPOST_BODY
},
},
},
@ -1515,6 +1574,21 @@ const form = reactive({
fileList: [] as any[],
uploadLoading: false,
});
//
const headerValid = ref(true);
const paramsValid = ref(true);
const headerValidator = () => {
return new Promise((resolve, reject) => {
headerValid.value ? resolve('') : reject('请输入完整的请求头');
});
};
const paramsValidator = () => {
return new Promise((resolve, reject) => {
paramsValid.value ? resolve('') : reject('请输入完整的请求参数');
});
};
//
const joinOptions = computed(() => {
if (form.data.provider === 'internal-standalone')
@ -1614,12 +1688,27 @@ function init() {
o.forEach((key) => {
if (!n.includes(key)) form.errorNumInfo[key].clear();
});
form.integrationModesISO = [...n];
},
);
}
function getInfo(id: string) {
getAppInfo_api(id).then((resp: any) => {
// headersparameters, keylabel
resp.result.apiClient.headers = resp.result.apiClient.headers.map(
(m: any) => ({
...m,
label: m.key,
}),
);
resp.result.apiClient.parameters = resp.result.apiClient.parameters.map(
(m: any) => ({
...m,
label: m.key,
}),
);
form.data = {
...resp.result,
integrationModes: resp.result.integrationModes.map(
@ -1643,7 +1732,7 @@ function getRoleIdList() {
}
//
function getOrgIdList() {
getDepartmentList_api().then((resp) => {
getDepartmentList_api({ paging: false }).then((resp) => {
if (resp.status === 200) {
form.orgIdList = resp.result as dictType;
}
@ -1660,6 +1749,7 @@ function clickAddItem(data: string[], target: string) {
}
//
function clickSave() {
console.log('headers: ', form.data.apiClient.headers);
formRef.value?.validate().then(() => {
const params = cloneDeep(form.data);
//
@ -1699,6 +1789,23 @@ function clickSave() {
params.id = params.apiServer.appId;
}
}
// headersparamslabelkey
if (params.integrationModes.includes('apiClient')) {
params.apiClient.headers = params.apiClient.headers.map(
(m: any) => ({
...m,
key: m.label,
}),
);
params.apiClient.parameters = params.apiClient.parameters.map(
(m: any) => ({
...m,
key: m.label,
}),
);
}
const request = routeQuery.id
? updateApp_api(routeQuery.id as string, params)
: addApp_api(params);
@ -1734,11 +1841,24 @@ function getErrorNum(
} else if (!set.has(key)) set.add(key);
}
}
const imageTypes = ref(['image/jpg', 'image/png']);
const beforeLogoUpload = (file: any) => {
const isType: any = imageTypes.value.includes(file.type);
if (!isType) {
message.error(`请上传.jpg.png格式的图片`);
return false;
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
message.error(`图片大小必须小于${4}M`);
}
return isType && isSize;
};
function changeBackUpload(info: UploadChangeParam<UploadFile<any>>) {
if (info.file.status === 'uploading') {
form.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
form.uploadLoading = false;
form.data.sso.configuration.oauth2.logoUrl = info.file.response?.result;
@ -1798,7 +1918,7 @@ function clearNullProp(obj: object) {
color: #000;
&.ant-radio-button-wrapper-disabled {
opacity: .5;
opacity: 0.5;
}
&.ant-radio-button-wrapper-checked {

View File

@ -1,49 +1,89 @@
<template>
<div class="request-table-container">
<j-table
:columns="columns"
:data-source="tableData"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'key'">
<j-input v-model:value="record.label" />
</template>
<template v-else-if="column.dataIndex === 'value'">
<j-input
v-model:value="record.value"
v-if="props.valueType === 'input'"
/>
<j-select
v-else-if="props.valueType === 'select'"
v-model:value="record.value"
>
<j-select-option
v-for="item in props.valueOptions"
:value="item.value"
>{{ item.label }}</j-select-option
<j-form ref="formRef" :model="formData" layout="vertical">
<j-table
:columns="columns"
:data-source="formData.tableData"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'label'">
<j-form-item
:name="[
'tableData',
index + (current - 1) * 10,
'label',
]"
:rules="[
{
required: !!record.label && !!record.value,
message: '该字段为必填字段',
trigger: 'change',
},
]"
>
</j-select>
<j-input v-model:value="record.label" />
</j-form-item>
</template>
<template v-else-if="column.dataIndex === 'value'">
<j-form-item
:name="[
'tableData',
index + (current - 1) * 10,
'value',
]"
:rules="[
{
required: !!record.value && !!record.label,
message: '该字段为必填字段',
trigger: 'change',
},
]"
>
<j-input
v-model:value="record.value"
v-if="props.valueType === 'input'"
/>
<j-select
v-else-if="props.valueType === 'select'"
v-model:value="record.value"
>
<j-select-option
v-for="item in props.valueOptions"
:value="item.value"
>
{{ item.label }}
</j-select-option>
</j-select>
</j-form-item>
</template>
<template v-else-if="column.dataIndex === 'action'">
<j-button
type="link"
@click="removeRow((current - 1) * 10 + index)"
>
<AIcon type="DeleteOutlined" />
</j-button>
</template>
</template>
<template v-else-if="column.dataIndex === 'action'">
<j-button
type="link"
@click="removeRow((current - 1) * 10 + index)"
>
<AIcon type="DeleteOutlined" />
</j-button>
</template>
</template>
</j-table>
<j-pagination
</j-table>
</j-form>
<!-- <j-pagination
v-show="props.value.length > 10"
v-model:current="current"
:page-size="10"
:total="props.value.length"
show-less-items
/> -->
<RowPagination
v-if="props.value.length > 10"
v-model:pageNum="current"
:pageSize="10"
:total="props.value.length"
/>
<j-button type="dashed" @click="addRow" class="add-btn">
<AIcon type="PlusOutlined" />新增
</j-button>
@ -53,12 +93,13 @@
<script setup lang="ts">
import type { optionsType } from '../typing';
const emits = defineEmits(['update:value']);
const emits = defineEmits(['update:value', 'update:valid']);
const props = withDefaults(
defineProps<{
value: optionsType;
valueType?: 'input' | 'select';
valueOptions?: optionsType;
valid?: boolean;
}>(),
{
valueType: 'input',
@ -67,28 +108,50 @@ const props = withDefaults(
const columns = [
{
title: 'KEY',
dataIndex: 'key',
width: '40%'
dataIndex: 'label', // , key, label
key: 'label',
width: '40%',
},
{
title: 'VALUE',
dataIndex: 'value',
width: '40%'
key: 'value',
width: '40%',
},
{
title: ' ',
dataIndex: 'action',
width: '20%'
key: 'action',
width: '20%',
},
];
const current = ref<number>(1);
const tableData = computed(() => {
return props.value.slice((current.value - 1) * 10, current.value * 10);
// const tableData = computed(() => {
// return props.value.slice((current.value - 1) * 10, current.value * 10);
// });
const formData = ref({
tableData: computed(() => {
return props.value.slice((current.value - 1) * 10, current.value * 10);
}),
});
if(props.value.length < 1) addRow()
const formRef = ref();
watch(
() => formData.value,
(val) => {
formRef.value?.validate();
// , , ,
const valid = formData.value.tableData.every(
(e: any) => (e.label && e.value) || (!e.label && !e.value),
);
emits('update:valid', valid);
},
{ deep: true },
);
if (props.value.length < 1) addRow();
watch(
() => props.value,
(n, o) => {
@ -140,5 +203,8 @@ function addRow() {
display: block;
margin-top: 10px;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
}
</style>

View File

@ -10,9 +10,11 @@ export type dictType = {
}[];
export type optionsType = {
label: string,
label?: string;
key?: string;
value: string;
disabled?: boolean
disabled?: boolean;
required?: boolean;
}[]
export type formType = {
id?:string,
@ -34,7 +36,7 @@ export type formType = {
type: 'none' | 'bearer' | 'oauth2' | 'basic' | 'other', // 类型, 可选值none, bearer, oauth2, basic, other
bearer: { token: string }, // 授权信息
basic: { username: string, password: string }, // 基本信息
token: string,
token?: string,
oauth2: { // OAuth2信息
authorizationUrl: string, // 授权地址
tokenUrl: string, // token地址
@ -43,7 +45,7 @@ export type formType = {
clientSecret: string, // 客户端密钥
grantType: 'authorization_code' | 'client_credentials' | '', // 类型
accessTokenProperty: string, // token属性名
tokenRequestType: 'POST_URI' | 'POST_BODY' | '' // token请求方式, 可选值POST_URIPOST_BODY
tokenRequestType: 'POST_URI' | 'POST_BODY' | '' | undefined // token请求方式, 可选值POST_URIPOST_BODY
}
}
},

View File

@ -46,7 +46,6 @@
enabled: 'success',
disabled: 'error',
}"
hasMark
>
<template #img>
<slot name="img">
@ -55,7 +54,9 @@
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
<Ellipsis>
{{ slotProps.name }}
</Ellipsis>
</h3>
<j-row>
<j-col :span="12">
@ -74,7 +75,9 @@
<div class="card-item-content-text">
说明
</div>
<div>{{ slotProps.description }}</div>
<Ellipsis>
{{ slotProps.description }}
</Ellipsis>
</j-col>
</j-row>
</template>
@ -242,7 +245,7 @@ const columns = [
key: 'status',
ellipsis: true,
search: {
rename: 'status',
rename: 'state',
type: 'select',
options: [
{
@ -271,8 +274,8 @@ const columns = [
dataIndex: 'action',
key: 'action',
scopedSlots: true,
width:'200px',
fixed:'right'
width: '200px',
fixed: 'right',
},
];
const queryParams = ref({});
@ -361,7 +364,7 @@ const table = {
//
if (otherServers.includes('page'))
others.children?.push({
permission: [`${permission}:add`,`${permission}:update`],
permission: [`${permission}:add`, `${permission}:update`],
key: 'page',
text: '集成菜单',
tooltip: {
@ -378,7 +381,7 @@ const table = {
if (otherServers.includes('apiServer'))
others.children?.push(
{
permission: [`${permission}:add`,`${permission}:update`],
permission: [`${permission}:add`, `${permission}:update`],
key: 'empowerment',
text: '赋权',
tooltip: {
@ -394,7 +397,7 @@ const table = {
},
},
{
permission: [`${permission}:add`,`${permission}:update`],
permission: [`${permission}:add`, `${permission}:update`],
key: 'viewApi',
text: '查看API',
tooltip: {

View File

@ -23,12 +23,16 @@
style="margin-bottom: 12px"
/>
<j-tree
v-if="treeData.length !== 0"
show-line
defaultExpandAll
multiple
draggable
:tree-data="treeData"
:height="500"
@select="onSelect"
:selectedKeys="selectedKeys"
@drop="onDrop"
>
<template #title="row">
<div class="tree-content">
@ -38,24 +42,6 @@
{{ row.name }}
</div>
</div>
<div class="tree-content-action">
<span @click="(e) => e.stopPropagation()">
<PermissionButton
type="text"
:tooltip="{
title: '删除',
}"
hasPermission="DataCollect/Collector:delete"
:popConfirm="{
title: `确定删除?`,
onConfirm: () =>
handlDelete(row.id),
}"
>
<AIcon type="CloseOutlined" />
</PermissionButton>
</span>
</div>
</div>
</template>
</j-tree>
@ -69,28 +55,32 @@
<script setup lang="ts" name="MenuSetting">
import { getMenuTree_api } from '@/api/system/menu';
import { getSystemPermission as getSystemPermission_api } from '@/api/initHome';
import { filterMenu, getKeys, loop } from './utils';
import {
filterMenu,
mergeMapToArr,
developArrToMap,
drop,
select,
} from './utils';
import BaseMenu from './baseMenu';
import type {
AntTreeNodeDropEvent,
TreeDataItem,
TreeProps,
} from 'ant-design-vue/es/tree';
import type { AntTreeNodeDropEvent } from 'ant-design-vue/es/tree';
import { treeFilter } from '@/utils/tree';
import { cloneDeep } from 'lodash';
const treeData = ref<any>();
const selectedKeys: any = ref([]);
const treeData = ref<any>([]);
const filterText = ref('');
treeData.value = [...BaseMenu];
let systemMenu: any = reactive([]);
const baseMenu = cloneDeep(BaseMenu);
const systemMenu: any = ref([]);
const baseMenu: any = ref([]);
const AllMenu = ref([]);
const BaseMenuMap = new Map();
BaseMenu.forEach((item) => {
BaseMenuMap.set(item.code, item);
});
const onSelect = (selecteds: Array<string>, e: any) => {
selectedKeys.value = select(selecteds, e);
};
console.log(11, BaseMenuMap);
const onDrop = (info: AntTreeNodeDropEvent) => {
treeData.value = drop(info, treeData.value);
};
const params = {
paging: false,
@ -114,20 +104,25 @@ const params = {
};
const change = (val: any) => {
treeData.value = treeFilter(baseMenu, val.target.value, 'name');
};
const handlDelete = (value) => {
console.log(22, value);
treeData.value = treeFilter(AllMenu.value, val.target.value, 'name');
};
onMounted(() => {
getMenuTree_api(params).then((resp: any) => {
if (resp.status == 200) {
systemMenu = resp.result;
console.log(2, systemMenu);
}
// transfer.data.rightTreeData = resp.result;
getSystemPermission_api().then((resp: any) => {
baseMenu.value = filterMenu(
resp.result.map((item: any) => JSON.parse(item).id),
BaseMenu,
);
getMenuTree_api(params).then((resp: any) => {
if (resp.status == 200) {
systemMenu.value = resp.result;
const baseMenuData = developArrToMap(baseMenu.value);
const systemMenuData = developArrToMap(systemMenu.value, true);
selectedKeys.value = systemMenuData.checkedKeys;
AllMenu.value = mergeMapToArr(baseMenuData, systemMenuData);
treeData.value = cloneDeep(AllMenu.value);
}
});
});
});
</script>
@ -183,15 +178,13 @@ onMounted(() => {
&-content {
display: flex;
justify-content: space-between;
width: 100%;
&-title {
flex: 1;
font-weight: 800;
font-size: 12px;
line-height: 17px;
line-height: 24px;
display: flex;
align-items: center;
color: #333333;
}
&-action {

View File

@ -1,4 +1,8 @@
import { TreeProps } from "ant-design-vue";
import type {
AntTreeNodeDropEvent,
TreeProps,
TreeDataItem,
} from 'ant-design-vue/es/tree';
/**
*
@ -18,21 +22,167 @@ export const filterMenu = (permissions: string[], menus: any[]) => {
});
};
// 在树形结构中对id进行匹配通过callback对匹配id的对象进行操作
export const loop= (data: TreeProps['treeData'], id: string | number, callback: any) => {
data?.forEach((item, index) => {
if (item.id === id) {
return callback(item, index, data);
}
if (item.children) {
return loop(item.children, id, callback);
/**
* Map菜单转成Arr菜单
* @param baseMenuData baseMenu developArrToMap平铺后的数据
* @param systemMenuData systemMenu developArrToMap平铺后的数据
* @returns
*/
export const mergeMapToArr = (baseMenuData: any, systemMenuData: any) => {
const updataArr = (r: any) => {
for (let i = 0; i < r.length; i++) {
const child = r[i].children;
if (child) {
updataArr(child);
}
r[i] = newMap.get(r[i].code);
delete r[i].parentCode;
}
};
const root: any = [];
const newMap = new Map([...baseMenuData?.arrMap, ...systemMenuData.arrMap]);
const newRootArr = [
...new Set([...baseMenuData?.rootSet, ...systemMenuData.rootSet]),
];
newRootArr.forEach((item: any) => {
root.push(newMap.get(item));
});
}
//
export const getKeys = (data: any[]): (string | number)[] => {
return data.reduce((pre: (string | number)[], next: any) => {
const childrenKeys = next.children ? getKeys(next.children) : [];
return [...pre, next.code, ...childrenKeys];
}, []);
}
updataArr(root);
return root;
};
/**
*
* @param value baseMenu systemMenu
* @param checked true
* @returns Mapkeys
*/
export const developArrToMap = (Menu: any, checked = false) => {
const rootSet = new Set();
const arrMap = new Map();
const checkedKeys: any = [];
const getMap = (arr: any, parentCode = 'root', preKey = '0') => {
arr.forEach((item: any, index: number) => {
const key = preKey + `-${index}`; //初始化key
item.title = item.code;
item.key = key;
if (checked) {
checkedKeys.push(key);
}
arrMap.set(item.code, item);
if (parentCode === 'root') {
rootSet.add(item.code); //处理根菜单
}
if (item?.children) {
getMap(item?.children, item.code, key);
}
});
};
getMap(Menu);
return { arrMap, rootSet, checkedKeys };
};
/**
*
* @param selecteds onSelect事件默认参数
* @param e onSelect事件默认参数
* @returns keys
*/
export const select = (selecteds: Array<string>, e: any) => {
const { node } = e;
const childKeys: Array<string> = [];
const getChildKeys = (data: any, preKey = '0') => {
data.forEach((item: any, index: number) => {
const checkedKey = preKey + `-${index}`;
childKeys.push(checkedKey);
if (item?.children) {
getChildKeys(item?.children, checkedKey);
}
});
};
if (node?.children) {
getChildKeys(node.children, node.key);
}
const Keys = new Set(selecteds);
const selectedAllKeys = [...[node.key, ...childKeys]];
selectedAllKeys.forEach((item: string) => {
Keys[e.selected ? 'add' : 'delete'](item);
});
return [...Keys];
};
/**
*
* @param info drop事件默认参数
* @param treeData treeData值
* @returns treeData值
*/
export const drop = (info: AntTreeNodeDropEvent, treeData: any) => {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition =
info.dropPosition - Number(dropPos[dropPos.length - 1]);
const loop = (
data: TreeProps['treeData'],
key: string | number,
callback: any,
) => {
data.forEach((item, index) => {
if (item.key === key) {
return callback(item, index, data);
}
if (item.children) {
return loop(item.children, key, callback);
}
});
};
const data = [...treeData];
let dragObj: TreeDataItem;
loop(
data,
dragKey,
(item: TreeDataItem, index: number, arr: TreeProps['treeData']) => {
arr.splice(index, 1);
dragObj = item;
},
);
if (!info.dropToGap) {
loop(data, dropKey, (item: TreeDataItem) => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else if (
(info.node.children || []).length > 0 && // Has children
info.node.expanded && // Is expanded
dropPosition === 1 // On the bottom gap
) {
loop(data, dropKey, (item: TreeDataItem) => {
item.children = item.children || [];
item.children.unshift(dragObj);
});
} else {
let ar: TreeProps['treeData'] = [];
let i = 0;
loop(
data,
dropKey,
(
_item: TreeDataItem,
index: number,
arr: TreeProps['treeData'],
) => {
ar = arr;
i = index;
},
);
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
} else {
ar.splice(i + 1, 0, dragObj);
}
}
return data;
};

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#6dc396bc8a1b6f5a08accf5f46aec2099c15f481"
integrity sha512-mnVN6MfHfyZf82miEoZV8+ud6RBH29x0A8PfpcraFQUl9Wat6XcjGppr8FOkmFbGw8laCGK5jlbiX3cYJUwaxw==
resolved "http://47.108.170.157:9013/jetlinks-ui-components/-/jetlinks-ui-components-1.0.5.tgz#593185f6313895485b59e9a79bc920c63374d84d"
integrity sha512-dkSOmatSPLHlV91YdTcHWO2wfwriUIZKEuLd5bJF2GsO9SvDMyJ2YJ4n/3fkklOoL5albhY37iX2Ot3A+7QYwA==
dependencies:
"@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15"