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

This commit is contained in:
wangshuaiswim 2023-03-13 15:12:18 +08:00
commit 58ba145a0e
70 changed files with 2492 additions and 1756 deletions

View File

@ -25,7 +25,7 @@
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.4",
"jetlinks-ui-components": "^1.0.5",
"js-cookie": "^3.0.1",
"less": "^4.1.3",
"less-loader": "^11.1.0",

View File

@ -15,7 +15,7 @@ export const unBind_api = (appId: string) => server.post(`/application/sso/${app
* @param type
* @param name
*/
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`,name,{
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`,name,{},{
headers: {
'Content-Type': 'text/plain'
}
@ -24,7 +24,7 @@ export const validateField_api = (type: 'username' | 'password', name: string)
*
* @param password
*/
export const checkOldPassword_api = (password:string) => server.post(`/user/me/password/_validate`,password,{
export const checkOldPassword_api = (password:string) => server.post(`/user/me/password/_validate`,password,{},{
headers: {
'Content-Type': 'text/plain'
}

View File

@ -21,3 +21,34 @@ export const _validateField = (id: string, data?: any) =>
server.get(`/data-collect/point/${id}/_validate`, data);
export const queryCodecProvider = () => server.get(`/things/collector/codecs`);
export const updatePoint = (id: string, data: any) =>
server.put(`/data-collect/point/${id}`, data);
export const savePointBatch = (data: any) =>
server.patch(`/data-collect/point`, data);
export const savePoint = (data: any) =>
server.post(`/data-collect/point`, data);
export const batchDeletePoint = (data: any) =>
server.post(`/data-collect/point/batch/_delete`, data);
export const removePoint = (id: string) =>
server.remove(`/data-collect/point/${id}`);
export const readPoint = (collectorId: string, data: string[]) =>
server.post(`/data-collect/collector/${collectorId}/points/_read`, data);
export const writePoint = (collectorId: string, data: string[]) =>
server.post(`/data-collect/collector/${collectorId}/points/_write`, data);
export const queryPointNoPaging = () =>
server.post(`/data-collect/point/_query/no-paging`, { paging: false });
export const scanOpcUAList = (data: any) =>
server.get(
`/data-collect/opc/channel/${data.id}/nodes?nodeId=${
data?.nodeId || ''
}`,
);

View File

@ -100,7 +100,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) => `${BASE_API_PATH}/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`
/**
* ID是否重复

View File

@ -7,7 +7,7 @@ export const getUserType_api = () => server.get(`/user/detail/types`);
export const getUserList_api = (data: object) => server.post(`/user/detail/_query`, data);
// 校验字段合法性
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name, {
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name,{}, {
headers: {
'Content-Type': 'text/plain'
}
@ -25,7 +25,7 @@ export const addUser_api = (data: object) => server.post(`/user/detail/_create`,
// 更新用户
export const updateUser_api = (data: any) => server.put(`/user/detail/${data.id}/_update`, data);
// 更新密码
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password, {
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password,{}, {
headers: {
'Content-Type': 'text/plain'
}

View File

@ -5,7 +5,7 @@
name="file"
:action="FILE_UPLOAD"
:headers="{
'X-Access-Token': LocalStore.get(TOKEN_KEY)
'X-Access-Token': LocalStore.get(TOKEN_KEY),
}"
accept=".xlsx,.csv"
:maxCount="1"
@ -26,19 +26,23 @@
</a-space>
<div style="margin-top: 20px" v-if="importLoading">
<a-badge v-if="flag" status="processing" text="进行中" />
<a-badge v-else status="success" text="已完成" />
<span>总数量{{count}}</span>
<p style="color: red">{{errMessage}}</p>
<a-badge v-else status="success" text="已完成" />
<span>总数量{{ count }}</span>
<p style="color: red">{{ errMessage }}</p>
</div>
</template>
<script lang="ts" setup>
import { FILE_UPLOAD } from '@/api/comm'
import { TOKEN_KEY } from '@/utils/variable';
import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { downloadFile, downloadFileByUrl } from '@/utils/utils';
import { deviceImport, deviceTemplateDownload ,templateDownload} from '@/api/device/instance'
import { EventSourcePolyfill } from 'event-source-polyfill'
import {
deviceImport,
deviceTemplateDownload,
templateDownload,
} from '@/api/device/instance';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { message } from 'ant-design-vue';
type Emits = {
@ -50,11 +54,11 @@ const props = defineProps({
//
modelValue: {
type: Array,
default: () => []
default: () => [],
},
product: {
type: String,
default: ''
default: '',
},
file: {
type: Object,
@ -62,67 +66,62 @@ const props = defineProps({
return {
fileType: 'xlsx',
autoDeploy: false,
}
}
}
})
};
},
},
});
const importLoading = ref<boolean>(false)
const flag = ref<boolean>(false)
const count = ref<number>(0)
const errMessage = ref<string>('')
const importLoading = ref<boolean>(false);
const flag = ref<boolean>(false);
const count = ref<number>(0);
const errMessage = ref<string>('');
const downFile =async (type: string) => {
const downFile = async (type: string) => {
// downloadFile(deviceTemplateDownload(props.product, type));
const res:any =await templateDownload(props.product, type)
if(res){
const res: any = await templateDownload(props.product, type);
if (res) {
const blob = new Blob([res], { type: type });
const url = URL.createObjectURL(blob);
console.log(url);
downloadFileByUrl(
url,
`设备导入模版`,
type,
);
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `设备导入模版`, type);
}
}
};
const submitData = async (fileUrl: string) => {
if (!!fileUrl) {
count.value = 0
errMessage.value = ''
flag.value = true
const autoDeploy = !!props?.file?.autoDeploy || false;
importLoading.value = true
let dt = 0;
const source = new EventSourcePolyfill(deviceImport(props.product, fileUrl, autoDeploy));
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt
} else {
errMessage.value = res.message || '失败'
}
};
source.onerror = (e: { status: number; }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员'
flag.value = false
source.close();
};
source.onopen = () => {};
count.value = 0;
errMessage.value = '';
flag.value = true;
const autoDeploy = !!props?.file?.autoDeploy || false;
importLoading.value = true;
let dt = 0;
const source = new EventSourcePolyfill(
deviceImport(props.product, fileUrl, autoDeploy),
);
source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
if (res.success) {
const temp = res.result.total;
dt += temp;
count.value = dt;
} else {
errMessage.value = res.message || '失败';
}
};
source.onerror = (e: { status: number }) => {
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员';
flag.value = false;
source.close();
};
source.onopen = () => {};
} else {
message.error('请先上传文件')
message.error('请先上传文件');
}
}
};
const uploadChange = async (info: Record<string, any>) => {
if (info.file.status === 'done') {
const resp: any = info.file.response || { result: '' };
await submitData(resp?.result || '');
}
}
};
</script>

View File

@ -1,125 +0,0 @@
<template>
<a-dropdown-button
type='primary'
@click='click'
placement='bottomLeft'
:visible='historyVisible'
@visibleChange='visibleChange'
>
搜索
<template #overlay>
<a-menu>
<template v-if='!showEmpty'>
<a-menu-item v-for='item in historyList' :key='item.id'>
<div class='history-item'>
<span @click.stop='itemClick(item.content)'>{{ item.name }}</span>
<a-popconfirm
title='确认删除吗?'
placement='top'
@confirm.stop='deleteHistory(item.id)'
:okButtonProps='{
loading: deleteLoading
}'
>
<span class='delete'>
<DeleteOutlined />
</span>
</a-popconfirm>
</div>
</a-menu-item>
</template>
<template v-else>
<div class='history-empty'>
<a-empty />
</div>
</template>
</a-menu>
</template>
<template #icon>
<SearchOutlined />
</template>
</a-dropdown-button>
</template>
<script setup lang='ts' name='SearchHistory'>
import { SearchOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { deleteSearchHistory, getSearchHistory } from '@/api/comm'
import type { SearchHistoryList } from 'components/Search/types'
type Emit = {
(event: 'click'): void
(event: 'itemClick', data: string): void
}
const emit = defineEmits<Emit>()
const props = defineProps({
target: {
type: String,
default: '',
required: true
}
})
const historyList = ref<SearchHistoryList[]>([])
const historyVisible = ref(false)
const deleteLoading = ref(false)
const showEmpty = ref(false)
const visibleChange = async (visible: boolean) => {
historyVisible.value = visible
if (visible) {
const resp = await getSearchHistory(props.target)
if (resp.success && resp.result.length) {
historyList.value = resp.result.filter(item => item.content)
showEmpty.value = false
} else {
showEmpty.value = true
}
}
}
const click = () => {
emit('click')
}
const itemClick = (content: string) => {
historyVisible.value = false
emit('itemClick', content)
}
const deleteHistory = async (id: string) => {
deleteLoading.value = true
const resp = await deleteSearchHistory(props.target, id)
deleteLoading.value = false
if (resp.success) {
historyVisible.value = false
}
}
</script>
<style scoped lang='less'>
.history-empty {
width: 200px;
background-color: #fff;
box-shadow: @box-shadow-base;
border-radius: 2px;
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
}
.history-item {
width: 200px;
display: flex;
> span {
flex: 1 1 auto;
}
.delete {
padding: 0 6px;
flex: 0 0 28px;
}
}
</style>

View File

@ -1,349 +0,0 @@
<template>
<div class='JSearch-item'>
<div class='JSearch-item--type' v-if='expand'>
<a-select
v-if='index !== 1 && index !== 4'
:options='typeOptions'
v-model:value='termsModel.type'
style='width: 100%;'
@change='valueChange'
/>
<span v-else>
{{
index === 1 ? '第一组' : '第二组'
}}
</span>
</div>
<a-select
class='JSearch-item--column'
:options='columnOptions'
v-model:value='termsModel.column'
@change='columnChange'
/>
<a-select
class='JSearch-item--termType'
:options='termTypeOptions.option'
v-model:value='termsModel.termType'
@change='termTypeChange'
/>
<div class='JSearch-item--value'>
<a-input
v-if='component === componentType.input'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-select
v-else-if='component === componentType.select'
showSearch
:loading='optionLoading'
v-model:value='termsModel.value'
:options='options'
style='width: 100%'
:filterOption='(v, option) => filterTreeSelectNode(v, option, "label")'
@change='valueChange'
/>
<a-input-number
v-else-if='component === componentType.inputNumber'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-input-password
v-else-if='component === componentType.password'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-switch
v-else-if='component === componentType.switch'
v-model:checked='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-radio-group
v-else-if='component === componentType.radio'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-checkbox-group
v-else-if='component === componentType.checkbox'
v-model:value='termsModel.value'
:options='options'
style='width: 100%'
@change='valueChange'
/>
<a-time-picker
v-else-if='component === componentType.time'
valueFormat='HH:mm:ss'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-date-picker
v-else-if='component === componentType.date'
showTime
v-model:value='termsModel.value'
valueFormat='YYYY-MM-DD HH:mm:ss'
style='width: 100%'
@change='valueChange'
/>
<a-tree-select
v-else-if='component === componentType.treeSelect'
showSearch
v-model:value='termsModel.value'
:tree-data='options'
style='width: 100%'
:fieldNames='{ label: "name", value: "id" }'
@change='valueChange'
:filterTreeNode='(v, option) => filterSelectNode(v, option)'
/>
</div>
</div>
</template>
<script setup lang='ts' name='SearchItem'>
import { componentType } from 'components/Form'
import { typeOptions, termType } from './util'
import { PropType } from 'vue'
import type { SearchItemData, SearchProps, Terms } from './types'
import { cloneDeep, get, isArray, isFunction } from 'lodash-es'
import { filterTreeSelectNode, filterSelectNode } from '@/utils/comm'
import { useUrlSearchParams } from '@vueuse/core'
type ItemType = SearchProps['type']
interface Emit {
(e: 'change', data: SearchItemData): void
}
type UrlParam = {
q: string | null
target: string | null
}
const urlParams = useUrlSearchParams<UrlParam>('hash')
const props = defineProps({
columns: {
type: Array as PropType<SearchProps[]>,
default: () => [],
required: true
},
index: {
type: Number,
default: 1
},
expand: {
type: Boolean,
default: false
},
termsItem: {
type: Object as PropType<Terms>,
default: {}
},
reset: {
type: Number,
default: 1
}
})
const emit = defineEmits<Emit>()
const termsModel = reactive<SearchItemData>({
type: 'or',
value: '',
termType: 'like',
column: ''
})
const component = ref(componentType.input)
const options = ref<any[]>([])
const columnOptions = ref<({ label: string, value: string})[]>([])
const columnOptionMap = new Map()
const termTypeOptions = reactive({option: termType})
const optionLoading = ref(false)
/**
* 根据类型切换默termType值
* @param type
*/
const getTermType = (type?: ItemType) => {
termTypeOptions.option = termType
switch (type) {
case 'select':
case 'treeSelect':
case 'number':
return 'eq'
case 'date':
case 'time':
//
termTypeOptions.option = termType.filter(item => ['gt','lt'].includes(item.value))
return 'gt'
default:
return 'like'
}
}
/**
* 根据类型返回组件
* @param type
*/
const getComponent = (type?: ItemType) => {
switch (type) {
case 'select':
component.value = componentType.select
break;
case 'treeSelect':
component.value = componentType.treeSelect
break;
case 'date':
component.value = componentType.date
break;
case 'time':
component.value = componentType.time
break;
case 'number':
component.value = componentType.inputNumber
break;
default:
component.value = componentType.input
break;
}
}
const handleItemOptions = (option?: any[] | Function) => {
options.value = []
if (isArray(option)) {
options.value = option
} else if (isFunction(option)) {
optionLoading.value = true
option().then((res: any[]) => {
optionLoading.value = false
options.value = res
}).catch((_: any) => {
optionLoading.value = false
})
}
}
const columnChange = (value: string, isChange: boolean) => {
const item = columnOptionMap.get(value)
optionLoading.value = false
// valueundefined
termsModel.column = value
termsModel.termType = item.defaultTermType || getTermType(item.type)
getComponent(item.type) // Item
// options request
if ('options' in item) {
handleItemOptions(item.options)
}
termsModel.value = undefined
if (isChange) {
valueChange()
}
}
const handleItem = () => {
columnOptionMap.clear()
columnOptions.value = []
if (!props.columns.length) return
columnOptions.value = props.columns.map(item => { // columnsMap
columnOptionMap.set(item.column, item)
return {
label: item.title,
value: item.column
}
})
//
const sortColumn = cloneDeep(props.columns)
sortColumn?.sort((a, b) => a.sortIndex! - b.sortIndex!)
const _index = props.index > sortColumn.length ? sortColumn.length - 1 : props.index
const _itemColumn = sortColumn[_index - 1]
columnChange(_itemColumn.column, false)
}
const termTypeChange = () => {
valueChange()
}
const valueChange = () => {
emit('change', {
type: termsModel.type,
value: termsModel.value,
termType: termsModel.termType,
column: termsModel.column,
})
}
const handleQuery = (_params: UrlParam) => {
if (_params.q) {
const path = props.index < 4 ? [0, 'terms', props.index - 1] : [1, 'terms', props.index - 4]
const itemData: SearchItemData = get(props.termsItem.terms, path)
if (itemData) {
termsModel.type = itemData.type
termsModel.column = itemData.column
termsModel.termType = itemData.termType
termsModel.value = itemData.value
const item = columnOptionMap.get(itemData.column)
getComponent(item.type) // Item
// options request
if ('options' in item) {
handleItemOptions(item.options)
}
}
}
}
handleItem()
nextTick(() => {
handleQuery(urlParams)
})
watch(() => props.reset, () => {
handleItem()
})
</script>
<style scoped lang='less'>
.JSearch-item {
display: flex;
gap: 16px;
.JSearch-item--type {
min-width: 80px;
> span {
line-height: 34px;
font-weight: bold;
}
}
.JSearch-item--column {
min-width: 100px;
}
.JSearch-item--termType {
min-width: 100px;
}
.JSearch-item--value {
flex: 1 1 auto;
}
}
</style>

View File

@ -1,100 +0,0 @@
<template>
<a-popover
title='搜索名称'
trigger='click'
v-model:visible='visible'
@visibleChange='visibleChange'
>
<template #content>
<div style='width: 240px'>
<a-form ref='formRef' :model='modelRef'>
<a-form-item
name='name'
:rules='[
{ required: true, message: "请输入名称"}
]'
>
<a-textarea
v-model:value='modelRef.name'
:rows='3'
:maxlength='200'
/>
</a-form-item>
</a-form>
<a-button
:loading='saveHistoryLoading'
type='primary'
class='save-btn'
@click='saveHistory'
>
保存
</a-button>
</div>
</template>
<a-button>
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
</a-popover>
</template>
<script setup lang='ts' name='SaveHistory'>
import type { Terms } from './types'
import { PropType } from 'vue'
import { saveSearchHistory } from '@/api/comm'
import { SaveOutlined } from '@ant-design/icons-vue';
const props = defineProps({
terms: {
type: Object as PropType<Terms>,
default: () => ({})
},
target: {
type: String,
default: '',
required: true
}
})
const searchName = ref('')
const saveHistoryLoading = ref(false)
const visible = ref(false)
const formRef = ref()
const modelRef = reactive({
name: undefined
})
/**
* 保存当前查询条件
*/
const saveHistory = async () => {
//
const formData = await formRef.value.validate()
if (formData) {
formData.content = JSON.stringify(props.terms)
saveHistoryLoading.value = true
const resp = await saveSearchHistory(formData, props.target)
saveHistoryLoading.value = false
if (resp.success) {
visible.value = false
}
}
}
const visibleChange = (e: boolean) => {
visible.value = e
}
</script>
<style scoped lang='less'>
.save-btn {
width: 100%
}
</style>

View File

@ -1,83 +1,21 @@
<template>
<div :class="['JSearch-warp', props.class]" ref='searchRef'>
<!-- 高级模式 -->
<div v-if='props.type === "advanced"' :class='["JSearch-content senior", expand ? "senior-expand" : "", screenSize ? "big" : "small"]'>
<div :class='["JSearch-items", expand ? "items-expand" : "", layout]'>
<div class='left'>
<SearchItem :expand='expand' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms' :reset='resetNumber'/>
<SearchItem v-if='expand' :expand='expand' :index='2' :columns='searchItems' @change='(v) => itemValueChange(v, 2)' :termsItem='terms' :reset='resetNumber'/>
<SearchItem v-if='expand' :expand='expand' :index='3' :columns='searchItems' @change='(v) => itemValueChange(v, 3)' :termsItem='terms' :reset='resetNumber'/>
</div>
<div class='center' v-if='expand'>
<a-select
v-model:value='termType'
class='center-select'
:options='typeOptions'
/>
</div>
<div class='right' v-if='expand'>
<SearchItem :expand='expand' :index='4' :columns='searchItems' @change='(v) => itemValueChange(v, 4)' :termsItem='terms' :reset='resetNumber'/>
<SearchItem :expand='expand' :index='5' :columns='searchItems' @change='(v) => itemValueChange(v, 5)' :termsItem='terms' :reset='resetNumber'/>
<SearchItem :expand='expand' :index='6' :columns='searchItems' @change='(v) => itemValueChange(v, 6)' :termsItem='terms' :reset='resetNumber'/>
</div>
</div>
<div :class='["JSearch-footer", expand ? "expand" : ""]'>
<div class='JSearch-footer--btns'>
<History :target='target' @click='searchSubmit' @itemClick='historyItemClick' />
<SaveHistory :terms='terms' :target='target'/>
<a-button @click='reset'>
<template #icon><RedoOutlined /></template>
重置
</a-button>
</div>
<a-button type='link' class='more-btn' @click='expandChange'>
更多筛选
<DownOutlined :class='["more-icon",expand ? "more-up" : "more-down"]' />
</a-button>
</div>
</div>
<!-- 简单模式 -->
<div v-else class='JSearch-content simple big'>
<div class='JSearch-items'>
<div class='left'>
<SearchItem :expand='false' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms' :reset='resetNumber'/>
</div>
</div>
<div class='JSearch-footer'>
<div class='JSearch-footer--btns'>
<a-button type="primary" @click='searchSubmit'>
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button @click='reset'>
<template #icon><RedoOutlined /></template>
重置
</a-button>
</div>
</div>
</div>
</div>
<j-advanced-search
:target='target'
:type='type'
:request='saveSearchHistory'
:historyRequest='getSearchHistory'
:columns='columns'
@search='searchSubmit'
/>
</template>
<script setup lang='ts' name='Search'>
import SearchItem from './Item.vue'
import { typeOptions } from './util'
import { useElementSize, useUrlSearchParams } from '@vueuse/core'
import { cloneDeep, isFunction, isString, set } from 'lodash-es'
import { SearchOutlined, DownOutlined, RedoOutlined } from '@ant-design/icons-vue';
<script setup lang='ts' name='ProSearch'>
import { PropType } from 'vue'
import { JColumnsProps } from 'components/Table/types'
import SaveHistory from './SaveHistory.vue'
import History from './History.vue'
import type { SearchItemData, SearchProps, Terms } from './types'
type UrlParam = {
q: string | null
target: string | null
}
import { saveSearchHistory, getSearchHistory } from '@/api/comm'
interface Emit {
(e: 'search', data: Terms): void
(e: 'search', data: any): void
}
const props = defineProps({
@ -98,299 +36,20 @@ const props = defineProps({
class: {
type: String,
default: ''
},
// defaultTerms: {
// type: Object,
// default: () => ({})
// }
}
})
const searchRef = ref(null)
const { width } = useElementSize(searchRef)
const urlParams = useUrlSearchParams<UrlParam>('hash')
//
const expand = ref(false)
//
const termType = ref('and')
//
const historyList = ref([])
//
const layout = ref('horizontal')
// true 1000
const screenSize = ref(true)
const resetNumber = ref(1)
const searchItems = ref<SearchProps[]>([])
//
const terms = reactive<Terms>({ terms: [] })
const columnOptionMap = new Map()
const emit = defineEmits<Emit>()
const expandChange = () => {
expand.value = !expand.value
}
const searchParams = reactive({
data: {}
})
const handleItems = () => {
searchItems.value = []
columnOptionMap.clear()
const cloneColumns = cloneDeep(props.columns)
cloneColumns!.forEach((item, index) => {
if (item.search && Object.keys(item.search).length) {
columnOptionMap.set(item.dataIndex, item.search)
searchItems.value.push({
...item.search,
sortIndex: item.search.first ? 0 : index + 1,
title: item.title,
column: item.dataIndex,
})
}
})
}
const itemValueChange = (value: SearchItemData, index: number) => {
if (index < 4) { //
set(terms.terms, [0, 'terms', index - 1], value)
} else { //
set(terms.terms, [1, 'terms', index - 4], value)
}
}
const addUrlParams = () => {
urlParams.q = JSON.stringify(terms)
urlParams.target = props.target
}
/**
* 处理termType为likenlike的值
* @param v
*/
const handleLikeValue = (v: string) => {
if (isString(v)) {
return v.split('').reduce((pre: string, next: string) => {
let _next = next
if (next === '\\') {
_next = '\\\\'
} else if (next === '%') {
_next = '\\%'
}
return pre + _next
}, '')
}
return v
}
/**
* 处理为外部使用
*/
const handleParamsFormat = () => {
// termsvalueitem
const cloneParams = cloneDeep(terms)
return {
terms: cloneParams.terms.map(item => {
if (item.terms) {
item.terms = item.terms.filter(iItem => iItem && iItem.value)
.map(iItem => {
// handleValuerename
const _item = columnOptionMap.get(iItem.column)
if (_item.rename) {
iItem.column = _item.rename
}
if (_item.handleValue && isFunction(_item.handleValue)) {
iItem.value = _item.handleValue(iItem.value)
}
if (['like','nlike'].includes(iItem.termType) && !!iItem.value) {
iItem.value = `%${handleLikeValue(iItem.value)}%`
}
return iItem
})
}
return item
})
}
}
/**
* 提交
*/
const searchSubmit = () => {
emit('search', handleParamsFormat())
console.log('searchSubmit')
if (props.type === 'advanced') {
addUrlParams()
}
const searchSubmit = (data: any) => {
emit('search', data)
}
/**
* 重置查询
*/
const reset = () => {
terms.terms = []
expand.value = false
if (props.type === 'advanced') {
urlParams.q = null
urlParams.target = null
}
resetNumber.value += 1
emit('search', { terms: []})
}
watch(width, (value) => {
if (value < 1000) {
layout.value = 'vertical'
screenSize.value = false
} else {
layout.value = 'horizontal'
screenSize.value = true
}
})
const historyItemClick = (content: string) => {
try {
terms.terms = JSON.parse(content)?.terms || []
if (terms.terms.length === 2) {
expand.value = true
}
addUrlParams()
} catch (e) {
console.warn(`Search组件中handleUrlParams处理JSON时异常${e}`)
}
}
/**
* 处理URL中的查询参数
* @param _params
*/
const handleUrlParams = (_params: UrlParam) => {
// URLtargetprops
if (_params.target === props.target && _params.q) {
try {
terms.terms = JSON.parse(_params.q)?.terms || []
if (terms.terms.length === 2) {
expand.value = true
}
emit('search', handleParamsFormat())
} catch (e) {
console.warn(`Search组件中handleUrlParams处理JSON时异常${e}`)
}
}
}
nextTick(() => {
handleUrlParams(urlParams)
})
handleItems()
</script>
<style scoped lang='less'>
.JSearch-warp {
padding: 24px;
background-color: #fff;
margin-bottom: 24px;
.JSearch-content {
display: flex;
gap: 12px;
.JSearch-items,& .JSearch-footer {
flex-grow: 1;
}
.JSearch-items {
display: flex;
gap: 16px;
.left, & .right {
display: flex;
gap: 12px;
flex-direction: column;
width: 0;
flex-grow: 1;
min-width: 0;
}
.center {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 80px;
}
&.vertical {
flex-direction: column;
.left,& .right,& .center {
width: 100%;
}
.center {
flex-direction: row;
}
.center-select {
width: 120px;
}
}
}
.JSearch-footer {
display: flex;
gap: 64px;
position: relative;
&.expand {
margin-top: 12px;
width: 100%;
justify-content: center;
.more-btn {
position: absolute;
right: 0;
}
}
.JSearch-footer--btns {
display: flex;
gap: 8px;
}
}
&.senior-expand {
display: block;
}
.more-up {
transform: rotate(-180deg);
}
&.big {
gap: 64px;
}
&.small {
flex-direction: column;
}
&.simple {
.JSearch-items {
flex-grow: 1;
}
.JSearch-footer {
flex-grow: 0;
}
}
}
}
</style>

View File

@ -1,6 +1,5 @@
<template>
<div class="title" :style='style'>
<div class="title-before"></div>
<span>{{ data }}</span>
<slot name="extra"></slot>
</div>
@ -23,23 +22,22 @@ const props = defineProps({
</script>
<style lang="less" scoped>
.title {
position: relative;
width: 100%;
margin-bottom: 10px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
font-size: 16px;
}
.title-before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: calc(100% - 2px);
background-color: @primary-color;
border-radius: 0 3px 3px 0;
position: relative;
width: 100%;
margin-bottom: 10px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.8);
font-weight: 600;
font-size: 16px;
&::before {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: @primary-color;
content: '';
}
}
</style>

View File

@ -5,7 +5,7 @@ import JTable from './Table/index'
import TitleComponent from "./TitleComponent/index.vue";
import Form from './Form';
import CardBox from './CardBox/index.vue';
// import Search from './Search'
import Search from './Search'
import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JProUpload from './JUpload/index.vue'
@ -25,7 +25,7 @@ export default {
.component('TitleComponent', TitleComponent)
.component('Form', Form)
.component('CardBox', CardBox)
// .component('Search', Search)
.component('ProSearch', Search)
.component('NormalUpload', NormalUpload)
.component('FileFormat', FileFormat)
.component('JProUpload', JProUpload)

View File

@ -8,7 +8,7 @@ export const AccountMenu = {
meta: {
title: '个人中心',
icon: '',
hideInMenu: false
hideInMenu: true
},
children: [
{

View File

@ -37,6 +37,10 @@ type MenuStateType = {
}
}
siderMenus: MenuItem[]
params: {
key: string
params: Record<string, any>
}
}
@ -44,6 +48,10 @@ export const useMenuStore = defineStore({
id: 'menu',
state: (): MenuStateType => ({
menus: {},
params: {
key: '',
params: {}
},
siderMenus: []
}),
getters: {

View File

@ -11,21 +11,10 @@
:model="formData"
name="basic"
autocomplete="off"
:rules="ModBusRules"
ref="formRef"
>
<j-form-item
label="点位名称"
name="name"
:rules="[
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-form-item label="点位名称" name="name">
<j-input
placeholder="请输入点位名称"
v-model:value="formData.name"
@ -34,12 +23,7 @@
<j-form-item
label="功能码"
:name="['configuration', 'function']"
:rules="[
{
required: true,
message: '请选择功能码',
},
]"
:rules="ModBusRules.function"
>
<j-select
style="width: 100%"
@ -57,18 +41,11 @@
</j-form-item>
<j-form-item
label="地址"
:name="['configuration', 'parameter', 'address']"
:name="['pointKey']"
:rules="[
...ModBusRules.pointKey,
{
required: true,
message: '请输入地址',
},
{
pattern: regOnlyNumber,
message: '请输入0-255之间的正整数',
},
{
validator: checkAddress,
validator: checkPointKey,
trigger: 'blur',
},
]"
@ -76,7 +53,7 @@
<j-input-number
style="width: 100%"
placeholder="请输入地址"
v-model:value="formData.configuration.parameter.address"
v-model:value="formData.pointKey"
:min="0"
:max="255"
/>
@ -84,16 +61,7 @@
<j-form-item
label="寄存器数量"
:name="['configuration', 'parameter', 'quantity']"
:rules="[
{
required: true,
message: '请输入寄存器数量',
},
{
pattern: regOnlyNumber,
message: '请输入1-255之间的正整数',
},
]"
:rules="ModBusRules.quantity"
>
<j-input-number
style="width: 100%"
@ -104,12 +72,14 @@
/>
</j-form-item>
<j-form-item
v-if="formData.configuration.function === 'HoldingRegisters'"
label="数据类型"
:name="['configuration', 'codec', 'provider']"
:rules="[
...ModBusRules.provider,
{
required: true,
message: '请选择数据类型',
validator: checkProvider,
trigger: 'change',
},
]"
>
@ -131,31 +101,18 @@
'configuration',
'scaleFactor',
]"
:rules="[
{
required: true,
message: '请输入缩放因子',
},
]"
:rules="ModBusRules.scaleFactor"
>
<j-input
<j-input-number
style="width: 100%"
placeholder="请输入缩放因子"
v-model:value="
formData.configuration.codec.configuration.scaleFactor
"
/>
</j-form-item>
<j-form-item
label="访问类型"
:name="['accessModes']"
:rules="[
{
required: true,
message: '请选择访问类型',
},
]"
>
<RadioCard
<j-form-item label="访问类型" name="accessModes">
<!-- <RadioCard
layout="horizontal"
:checkStyle="true"
:options="[
@ -163,42 +120,36 @@
{ label: '写', value: 'write' },
]"
v-model="formData.accessModes"
/>
<!-- <j-card-select
/> -->
<j-checkbox-group
v-model:value="formData.accessModes"
:options="[
{
label: '读',
value: 'read',
iconUrl:
'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
},
{
label: '写',
value: 'write',
iconUrl:
'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
},
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
]"
multiple
/> -->
/>
</j-form-item>
<!-- <j-form-item label="非标准协议写入配置" :name="['nspwc']"> -->
<j-form-item :name="['nspwc']">
<span>非标准协议写入配置</span>
<j-form-item
:name="['nspwc']"
v-if="
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
>
<span style="margin-right: 10px">非标准协议写入配置</span>
<j-switch v-model:checked="formData.nspwc" />
</j-form-item>
<j-form-item
v-if="
!!formData.nspwc &&
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
label="是否写入数据区长度"
:name="['configuration', 'parameter', 'writeByteCount']"
:rules="[
{
required: true,
message: '请选择是否写入数据区长度',
},
]"
:rules="ModBusRules.writeByteCount"
>
<RadioCard
layout="horizontal"
@ -211,14 +162,14 @@
/>
</j-form-item>
<j-form-item
v-if="
!!formData.nspwc &&
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
label="自定义数据区长度byte"
:name="['configuration', 'parameter', 'byteCount']"
:rules="[
{
required: true,
message: '请输入自定义数据区长度byte',
},
]"
:rules="ModBusRules.byteCount"
>
<j-input
placeholder="请输入自定义数据区长度byte"
@ -229,9 +180,10 @@
label="采集频率"
:name="['configuration', 'interval']"
:rules="[
...ModBusRules.interval,
{
required: true,
message: '请输入采集频率',
validator: checkLength,
trigger: 'change',
},
]"
>
@ -240,6 +192,7 @@
placeholder="请输入采集频率"
v-model:value="formData.configuration.interval"
:min="1"
addon-after="ms"
/>
</j-form-item>
@ -279,22 +232,16 @@
</j-modal>
</template>
<script lang="ts" setup>
import { Form } from 'ant-design-vue';
import {
save,
update,
savePointBatch,
updatePoint,
_validateField,
queryCodecProvider,
} from '@/api/data-collect/collector';
import { Store } from 'jetlinks-store';
const loading = ref(false);
const useForm = Form.useForm;
const channelListAll = ref();
const channelList = ref();
const visibleEndian = ref(false);
const visibleUnitId = ref(false);
const providerList = ref([]);
import { ModBusRules, checkProviderData } from '../../data.ts';
import type { FormInstance } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep, isArray } from 'lodash-es';
const props = defineProps({
data: {
@ -304,105 +251,72 @@ const props = defineProps({
});
const emit = defineEmits(['change']);
const loading = ref(false);
const providerList = ref([]);
const formRef = ref<FormInstance>();
const id = props.data.id;
const treeId = props.data.treeId;
const collectorId = props.data.collectorId;
const provider = props.data.provider;
const oldPointKey = props.data.pointKey;
const formData = ref({
name: '',
configuration: {
function: undefined,
interval: '',
interval: 3000,
parameter: {
address: '',
quantity: '',
quantity: 1,
writeByteCount: '',
byteCount: '',
byteCount: 2,
address: '',
},
codec: {
provider: '',
provider: undefined,
configuration: {
scaleFactor: '',
scaleFactor: 1,
},
},
},
accessModes: undefined,
nspwc: '',
byte: '',
features: '',
pointKey: '',
accessModes: [],
nspwc: false,
features: [],
description: '',
});
const regOnlyNumber = new RegExp(/^\d+$/);
const onSubmit = async () => {
const data = await formRef.value?.validate();
const checkAddress = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
const res = await _validateField(props.data.treeId, {
pointKey: value,
});
return res.result.passed ? resolve('') : reject(res.result.reason);
}
});
delete data?.nspwc;
const { codec } = data?.configuration;
// const { resetFields, validate, validateInfos } = useForm(
// formData,
// reactive({
// channelId: [
// { required: true, message: '', trigger: 'blur' },
// ],
// name: [
// { required: true, message: '', trigger: 'blur' },
// { max: 64, message: '64' },
// ],
// 'configuration.unitId': [
// { required: true, message: '', trigger: 'blur' },
// {
// pattern: regOnlyNumber,
// message: '0-255',
// },
// ],
// 'circuitBreaker.type': [
// { required: true, message: '', trigger: 'blur' },
// ],
// 'configuration.endian': [
// { required: true, message: '', trigger: 'blur' },
// ],
// description: [{ max: 200, message: '200' }],
// }),
// );
const onSubmit = () => {
// validate()
// .then(async (res) => {
// const { provider, name } = channelListAll.value.find(
// (item) => item.id === formData.value.channelId,
// );
// const params = {
// ...toRaw(formData.value),
// provider,
// channelName: name,
// };
// loading.value = true;
// const response = !id
// ? await save(params)
// : await update(id, { ...props.data, ...params });
// if (response.status === 200) {
// emit('change', true);
// }
// loading.value = false;
// })
// .catch((err) => {
// loading.value = false;
// });
if (data?.configuration.function !== 'HoldingRegisters') {
codec.provider = 'int8';
}
const params = {
...props.data,
...data,
provider,
collectorId,
};
// addressreact使
params.configuration.parameter = {
...params.configuration.parameter,
address: data?.pointKey,
};
loading.value = true;
const response = !id
? await savePointBatch(params)
: await updatePoint(id, { ...props.data, ...params });
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const getTypeTooltip = (value: string) =>
value === 'LowerFrequency'
? '连续20次异常降低连接频率至原有频率的1/10重试间隔不超过1分钟故障处理后自动恢复至设定连接频率'
: value === 'Break'
? '连续10分钟异常停止采集数据进入熔断状态设备重新启用后恢复采集状态'
: '忽略异常保持原采集频率超时时间为5s';
const handleOk = () => {
onSubmit();
};
@ -410,19 +324,42 @@ const handleCancel = () => {
emit('change', false);
};
// const getChannelNoPaging = async () => {
// channelListAll.value = Store.get('channelListAll');
// channelList.value = channelListAll.value.map((item) => ({
// value: item.id,
// label: item.name,
// }));
// };
// getChannelNoPaging();
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
}
});
const checkProvider = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
const { quantity } = formData.value.configuration.parameter;
return checkProviderData[value] > Number(quantity) * 2
? reject('数据类型长度需 <= 寄存器数量 * 2')
: resolve('');
}
});
const checkPointKey = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
if (Number(oldPointKey) === Number(value)) return resolve('');
const res = await _validateField(collectorId, {
pointKey: value,
});
return res.result.passed ? resolve('') : reject(res.result.reason);
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const getProviderList = async () => {
const res = await queryCodecProvider();
console.log(222, res.result);
providerList.value = res.result
.filter((i) => i.id !== 'property')
.map((item) => ({
@ -432,20 +369,31 @@ const getProviderList = async () => {
};
getProviderList();
// watch(
// () => formData.value.channelId,
// (value) => {
// const dt = channelListAll.value.find((item) => item.id === value);
// visibleUnitId.value = visibleEndian.value =
// dt?.provider && dt?.provider === 'MODBUS_TCP';
// },
// { deep: true },
// );
watch(
() => formData.value.configuration.parameter.quantity,
(value) => {
formData.value.configuration.parameter.byteCount = Number(value) * 2;
},
{ immediate: true, deep: true },
);
watch(
() => props.data,
(value) => {
if (value.id) formData.value = value;
if (value.id && value.provider === 'MODBUS_TCP') {
const _value = cloneDeep(value);
const { writeByteCount, byteCount } =
_value.configuration.parameter;
formData.value = _value;
if (!!_value.accessModes[0]?.value) {
formData.value.accessModes = value.accessModes.map(
(i) => i.value,
);
}
if (!!_value.features[0]?.value) {
formData.value.features = value.features.map((i) => i.value);
}
formData.value.nspwc = !!writeByteCount || !!byteCount;
}
},
{ immediate: true, deep: true },
);

View File

@ -0,0 +1,217 @@
<template lang="">
<j-modal title="编辑" :visible="true" width="700px" @cancel="handleCancel">
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
:rules="OPCUARules"
ref="formRef"
>
<j-form-item label="点位名称" name="name">
<j-input
placeholder="请输入点位名称"
v-model:value="formData.name"
/>
</j-form-item>
<j-form-item label="数据类型" :name="['configuration', 'type']">
<j-select
style="width: 100%"
v-model:value="formData.configuration.type"
:options="[
{ value: 'Number', label: '数值类型' },
{ value: 'DateTime', label: '时间类型' },
{ value: 'Array', label: '数组类型' },
{ value: 'String', label: '文本类型' },
{ value: 'Boolean', label: '布尔' },
]"
placeholder="请选择数据类型"
allowClear
show-search
:filter-option="filterOption"
/>
</j-form-item>
<j-form-item label="访问类型" name="accessModes">
<j-checkbox-group
v-model:value="formData.accessModes"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
/>
</j-form-item>
<j-form-item
label="采集频率"
:name="['configuration', 'interval']"
:rules="[
...OPCUARules.interval,
{
validator: checkLength,
trigger: 'change',
},
]"
>
<j-input-number
style="width: 100%"
placeholder="请输入采集频率"
v-model:value="formData.configuration.interval"
:min="1"
addon-after="ms"
/>
</j-form-item>
<a-form-item label="" :name="['features']">
<a-checkbox-group v-model:value="formData.features">
<a-checkbox value="changedOnly" name="type"
>只推送变化的数据</a-checkbox
>
</a-checkbox-group>
</a-form-item>
<j-form-item label="说明" :name="['description']">
<j-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/>
</j-form-item>
</j-form>
<template #footer>
<j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:${
id ? 'update' : 'add'
}`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import {
savePoint,
updatePoint,
_validateField,
} from '@/api/data-collect/collector';
import { OPCUARules, checkProviderData } from '../../data.ts';
import type { FormInstance } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['change']);
const loading = ref(false);
const providerList = ref([]);
const formRef = ref<FormInstance>();
const id = props.data.id;
const collectorId = props.data.collectorId;
const provider = props.data.provider;
const formData = ref({
name: '',
configuration: {
type: undefined,
interval: 3000,
},
accessModes: [],
features: [],
description: '',
});
const onSubmit = async () => {
const data = await formRef.value?.validate();
const params = {
...props.data,
...data,
provider,
collectorId,
};
loading.value = true;
const response = !id
? await savePoint(params)
: await updatePoint(id, { ...props.data, ...params });
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch(
() => props.data,
(value) => {
if (value.id && value.provider === 'OPC_UA') {
const _value = cloneDeep(value);
formData.value = _value;
if (!!_value.accessModes[0]?.value) {
formData.value.accessModes = value.accessModes.map(
(i) => i.value,
);
}
if (!!_value.features[0]?.value) {
formData.value.features = value.features.map((i) => i.value);
}
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.form {
.form-radio-button {
width: 148px;
height: 80px;
padding: 0;
img {
width: 100%;
height: 100%;
}
}
.form-upload-button {
margin-top: 10px;
}
.form-submit {
background-color: @primary-color !important;
}
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<j-form style="width: 80%" ref="formTableRef" :model="modelRef">
<j-table
:dataSource="modelRef.dataSource"
:columns="FormTableColumns"
:scroll="{ x: 1100, y: 500 }"
>
<template #bodyCell="{ column: { dataIndex }, record, index }">
<template v-if="dataIndex === 'name'">
<a-form-item
:name="['dataSource', index, 'name']"
:rules="[
{
required: true,
message: '请输入',
},
]"
>
<j-input
v-model:value="record[dataIndex]"
placeholder="请输入"
allowClear
></j-input>
</a-form-item>
</template>
<template v-if="dataIndex === 'id'">
<a-form-item :name="['dataSource', index, 'id']">
<j-input v-model:value="record[dataIndex]" disabled>
</j-input>
</a-form-item>
</template>
<template v-if="dataIndex === 'accessModes'">
<a-form-item
class="form-item"
:name="['dataSource', index, 'accessModes', 'value']"
:rules="[
{
required: true,
message: '请选择',
},
]"
>
<j-select
style="width: 75%"
v-model:value="record[dataIndex].value"
placeholder="请选择"
allowClear
mode="multiple"
:filter-option="filterOption"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
:disabled="index !== 0 && record[dataIndex].check"
@change="changeValue(index, dataIndex)"
>
</j-select>
<j-checkbox
style="margin-left: 5px"
v-if="index !== 0"
v-model:checked="record[dataIndex].check"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'interval'">
<a-form-item
class="form-item"
:name="[
'dataSource',
index,
'configuration',
'interval',
'value',
]"
:rules="[
{
required: true,
message: '请输入',
},
]"
>
<j-input
style="width: 70%"
v-model:value="
record.configuration[dataIndex].value
"
placeholder="请输入"
allowClear
addon-after="ms"
:disabled="
index !== 0 &&
record.configuration[dataIndex].check
"
@blur="changeValue(index, dataIndex)"
></j-input>
<j-checkbox
style="margin-left: 5px"
v-show="index !== 0"
v-model:checked="
record.configuration[dataIndex].check
"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'features'">
<a-form-item
class="form-item"
:name="['dataSource', index, 'features', 'value']"
:rules="[
{
required: true,
message: '请选择',
},
]"
>
<j-select
style="width: 50%"
v-model:value="record[dataIndex].value"
placeholder="请选择"
allowClear
:filter-option="filterOption"
:options="[
{
label: '是',
value: true,
},
{
label: '否',
value: false,
},
]"
:disabled="index !== 0 && record[dataIndex].check"
@change="changeValue(index, dataIndex)"
>
</j-select>
<j-checkbox
style="margin-left: 5px"
v-show="index !== 0"
v-model:checked="record[dataIndex].check"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'action'">
<a-tooltip title="删除">
<a-popconfirm
title="确认删除"
@confirm="clickDelete(record.id)"
>
<AIcon type="DeleteOutlined" />
</a-popconfirm>
</a-tooltip>
</template>
</template>
</j-table>
</j-form>
</template>
<script lang="ts" setup>
import { FormTableColumns } from '../../data';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['change']);
const formTableRef = ref();
const defaultType = ['accessModes', 'interval', 'features'];
const modelRef = reactive({
dataSource: [],
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const clickDelete = (value: string) => {
emits('change', value);
};
const getTargetData = (index: number, type: string) => {
const { dataSource } = modelRef;
const Interval = type === 'interval';
return !Interval
? dataSource[index][type]
: dataSource[index].configuration[type];
};
const changeValue = (index: number, type: string) => {
const { dataSource } = modelRef;
const originData = getTargetData(index, type);
for (let i = index + 1; i < dataSource.length; i++) {
const targetType = getTargetData(i, type);
if (!targetType.check) return;
targetType.value = originData.value;
}
};
const changeCheckbox = (index: number, type: string) => {
// console.log(1, getTargetData(index, type).check,getTargetData(index, type));
//使setTimeout
setTimeout(() => {
// console.log(2, getTargetData(index, type).check,getTargetData(index, type));
let startIndex = 0;
const { dataSource } = modelRef;
const currentCheck = getTargetData(index, type).check;
if (!currentCheck) return;
for (let i = index; i >= 0; i--) {
const preDatCheck = getTargetData(i, type).check;
if (!preDatCheck) {
startIndex = i;
break;
}
}
const originData = getTargetData(startIndex, type);
for (let i = startIndex; i < dataSource.length - 1; i++) {
const targetType = getTargetData(i + 1, type);
if (!targetType.check) return;
targetType.value = originData.value;
}
}, 0);
};
const validate = () => {
return new Promise((res, rej) => {
formTableRef.value
.validate()
.then(() => {
res(modelRef.dataSource);
})
.catch((err: any) => {
rej(err);
});
});
};
defineExpose({
validate,
});
watch(
() => props.data,
(value, preValue) => {
modelRef.dataSource = value;
//
const vlength = value.length,
plength = preValue.length;
if (plength !== 0 && plength < vlength) {
defaultType.forEach((type) => {
vlength === 2
? changeValue(0, type)
: changeCheckbox(vlength - 1, type);
});
}
},
{ deep: true },
);
</script>
<style lang="less" scoped>
.form-item {
display: flex;
}
</style>

View File

@ -0,0 +1,240 @@
<template>
<div class="tree-content">
<div class="tree-header">
<div>数据源</div>
<j-checkbox v-model:checked="isSelected">隐藏已有节点</j-checkbox>
</div>
<j-spin :spinning="spinning">
<a-tree
v-model:checkedKeys="checkedKeys"
:tree-data="treeData"
default-expand-all
checkable
@check="onCheck"
:height="600"
>
<!-- <a-tree
:load-data="onLoadData"
:tree-data="treeData"
v-model:checkedKeys="checkedKeys"
checkable
@check="onCheck"
> -->
<template #title="{ name, key }">
<span
:class="[
selectKeys.includes(key)
? 'tree-selected'
: 'tree-title',
]"
>
{{ name }}
</span>
</template>
</a-tree>
</j-spin>
</div>
</template>
<script lang="ts" setup>
import type { TreeProps } from 'ant-design-vue';
import {
scanOpcUAList,
queryPointNoPaging,
} from '@/api/data-collect/collector';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
unSelectKeys: {
type: String,
default: '',
},
});
const emits = defineEmits(['change']);
// const channelId = '1610517801347788800'; //
const channelId = props.data?.channelId;
const checkedKeys = ref<string[]>([]);
const selectKeys = ref<string[]>([]);
const spinning = ref(false);
const isSelected = ref(false);
const treeData = ref<TreeProps['treeData']>();
const treeAllData = ref<TreeProps['treeData']>();
const onLoadData = (node: any) =>
new Promise<void>(async (resolve) => {
if ((node?.children && node.children?.length) || !node?.folder) {
resolve();
return;
}
const resp = await scanOpcUAList({
id: channelId,
nodeId: node.key,
});
if (resp.status === 200) {
const list = resp.result.map((item: any) => {
return {
...item,
key: item.id,
title: item.name,
disabled: item?.folder,
isLeaf: !item?.folder,
};
});
treeAllData.value = updateTreeData(
cloneDeep(treeAllData.value),
node.key,
[...list],
);
}
resolve();
});
const handleData = (arr: any[]): any[] => {
const data = arr.filter((item) => {
return (
(isSelected && !selectKeys.value.includes(item.id)) || !isSelected
);
});
return data.map((item) => {
if (item.children && item.children?.length) {
return {
...item,
children: handleData(item.children),
};
} else {
return item;
}
});
};
const onCheck = (checkedKeys, info) => {
const one: any = { ...info.node };
const list: any = [];
const last: any = list.length ? list[list.length - 1] : undefined;
if (list.map((i: any) => i?.id).includes(one.id)) {
return;
}
const item = {
features: {
value: last
? last?.features?.value
: (one?.features || []).includes('changedOnly'),
check: true,
},
id: one?.id || '',
name: one?.name || '',
accessModes: {
value: last ? last?.accessModes?.value : one?.accessModes || [],
check: true,
},
configuration: {
...one?.configuration,
interval: {
value: last
? last?.configuration?.interval?.value
: one?.configuration?.interval || 3000,
check: true,
},
nodeId: one?.id,
},
};
emits('change', item, info.checked);
};
const updateTreeData = (list: any[], key: string, children: any[]): any[] => {
const arr = list.map((node) => {
if (node.key === key) {
return {
...node,
children,
};
}
if (node?.children && node?.children?.length) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
return arr;
};
const getPoint = async () => {
spinning.value = true;
const res = await queryPointNoPaging();
if (res.status === 200) {
selectKeys.value = res.result.map((item: any) => item.pointKey);
}
spinning.value = false;
};
getPoint();
const getScanOpcUAList = async () => {
const res = await scanOpcUAList({ id: channelId });
treeAllData.value = res.result.map((item: any) => ({
...item,
key: item.id,
title: item.name,
disabled: item?.folder || false,
}));
};
getScanOpcUAList();
watch(
() => isSelected.value,
(value) => {
if (value) {
treeData.value = handleData(treeAllData.value);
} else {
treeData.value = treeAllData.value;
}
},
{ deep: true },
);
watch(
() => treeAllData.value,
(value) => {
if (isSelected.value) {
treeData.value = handleData(value);
} else {
treeData.value = value;
}
},
{ deep: true },
);
watch(
() => props.unSelectKeys,
(value) => {
checkedKeys.value = checkedKeys.value.filter((i) => i !== value);
},
{ deep: true },
);
</script>
<style lang="less" scoped>
.tree-content {
padding: 16px;
padding-left: 0;
.tree-header {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
}
.tree-selected {
color: #1d39c4;
}
.tree-title {
color: black;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template lang="">
<j-modal title="扫描" :visible="true" width="90%" @cancel="handleCancel">
<div class="content">
<Tree
:data="treeData"
class="tree"
@change="changeTree"
:unSelectKeys="unSelectKeys"
></Tree>
<Table
:data="tableData"
class="table"
@change="changeTable"
ref="formTableRef"
></Table>
</div>
<template #footer>
<j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:update`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { savePointBatch } from '@/api/data-collect/collector';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash';
import Table from './Table.vue';
import Tree from './Tree.vue';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const treeData = ref(props.data);
const emit = defineEmits(['change']);
const loading = ref(false);
const formTableRef = ref<FormInstance>();
const tableData = ref();
const tableDataMap = new Map();
const unSelectKeys = ref();
const handleOk = async () => {
loading.value = true;
const data = await formTableRef.value?.validate();
const list = data.map((item: any) => {
return {
name: item.name,
provider: 'OPC_UA',
collectorId: props.data?.id,
collectorName: props.data?.name,
pointKey: item.id,
configuration: {
interval: item.configuration?.interval?.value,
},
features: !item.features?.value ? [] : ['changedOnly'],
accessModes: item.accessModes?.value || [],
};
});
console.log(1112, props.data, data, list);
const resp = await savePointBatch([...list]);
if (resp.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleCancel = () => {
emit('change', false);
};
const changeTree = (row: any, checked: boolean) => {
checked ? tableDataMap.set(row.id, row) : tableDataMap.delete(row.id);
tableData.value = [...tableDataMap.values()];
};
const changeTable = (value: string) => {
unSelectKeys.value = value;
tableDataMap.delete(value);
tableData.value = [...tableDataMap.values()];
};
</script>
<style lang="less" scoped>
.content {
display: flex;
min-height: 600px;
.tree {
width: 300px;
}
.table {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,152 @@
<template lang="">
<j-modal
title="批量编辑"
:visible="true"
width="700px"
@cancel="handleCancel"
>
<div class="sizeText">
将批量修改{{
data.length
}}条数据的访问类型采集频率只推送变化的数据
</div>
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
ref="formRef"
>
<j-form-item label="访问类型" name="accessModes">
<j-checkbox-group
v-model:value="formData.accessModes"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
/>
</j-form-item>
<j-form-item :name="['interval']">
<template #label>
<span>
采集频率
<j-tooltip title="采集频率为0时不执行轮询任务">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-input-number
style="width: 100%"
placeholder="请输入采集频率"
v-model:value="formData.interval"
:min="1"
addon-after="ms"
/>
</j-form-item>
<a-form-item label="" :name="['features']">
<a-checkbox-group v-model:value="formData.features">
<a-checkbox value="changedOnly" name="type"
>只推送变化的数据</a-checkbox
>
</a-checkbox-group>
</a-form-item>
</j-form>
<template #footer>
<j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:update`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { savePointBatch } from '@/api/data-collect/collector';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['change']);
const loading = ref(false);
const formRef = ref<FormInstance>();
const formData = ref({
accessModes: [],
interval: '',
features: [],
});
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
}
});
const onSubmit = async () => {
const data = await formRef.value?.validate();
const { accessModes, features, interval } = data;
const ischange =
accessModes.length !== 0 || features.length !== 0 || !!interval;
if (ischange) {
const params = cloneDeep(props.data);
params.forEach((i: any) => {
accessModes.length !== 0 && (i.accessModes = data.accessModes);
features.length !== 0 && (i.features = data.features);
if (!!interval) {
i.interval = data.interval;
i.configuration = {
...i.configuration,
interval: data.interval,
};
}
});
loading.value = true;
const response = await savePointBatch(params);
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
} else {
emit('change', true);
}
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
</script>
<style lang="less" scoped>
.sizeText {
margin-bottom: 20px;
}
</style>

View File

@ -1,10 +1,6 @@
<template>
<div class="card">
<div
class="card-warp"
:class="{ active: active ? 'active' : '' }"
@click="handleClick"
>
<div class="card-warp" :class="{ active: active ? 'active' : '' }">
<div class="card-content">
<div class="card-header">
<div class="card-header-left">
@ -15,7 +11,11 @@
</div>
</div>
<div style="display: flex">
<div
class="card-item"
style="display: flex"
@click="handleClick"
>
<!-- 图片 -->
<div class="card-item-avatar">
<slot name="img"> </slot>
@ -169,7 +169,7 @@ const handleClick = () => {
border: 1px solid #e6e6e6;
&:hover {
cursor: pointer;
// cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask {
@ -214,6 +214,9 @@ const handleClick = () => {
align-items: center;
}
.card-item {
cursor: pointer;
}
.card-item-avatar {
margin-right: 16px;
}

View File

@ -0,0 +1,169 @@
<template lang="">
<j-modal title="写入" :visible="true" width="500px" @cancel="handleCancel">
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
ref="formRef"
>
<j-form-item
:label="data.name"
name="value"
:rules="[
{
required: true,
message: `请输入${data.name}`,
},
]"
v-if="
data.provider === 'MODBUS_TCP' &&
data?.configuration.function === 'Coils'
"
>
<j-textarea
placeholder="请输入"
v-model:value="formData.value"
:maxlength="200"
:rows="3"
showCount
/>
</j-form-item>
<j-form-item
:label="data.name"
name="value"
:rules="[
{
required: true,
message: `请输入${data.name}`,
},
]"
v-else
>
<j-input-number
v-if="valueTypeArray.includes(valueType)"
style="width: 100%"
placeholder="请输入"
v-model:value="formData.value"
/>
<j-select
v-else-if="['boolean'].includes(valueType)"
style="width: 100%"
v-model:value="formData.value"
:options="[
{
label: '是',
value: true,
},
{
label: '否',
value: false,
},
]"
placeholder="请选择"
allowClear
show-search
:filter-option="filterOption"
/>
<j-date-picker
v-else-if="['datetime'].includes(valueType)"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
show-time
placeholder="请选择"
@change="onChange"
/>
<j-input
v-else
placeholder="请输入"
v-model:value="formData.value"
/>
</j-form-item>
</j-form>
<template #footer>
<j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:update`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import type { Dayjs } from 'dayjs';
import { writePoint } from '@/api/data-collect/collector';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const valueType: string = (
props.data?.provider === 'OPC_UA'
? props?.data?.configuration?.type || 'Number'
: props.data?.configuration?.codec?.provider || 'int8'
).toLocaleLowerCase();
const valueTypeArray = [
'int8',
'int16',
'int32',
'ieee754_float',
'ieee754_double',
'hex,',
'number',
];
const emit = defineEmits(['change']);
const loading = ref(false);
const formRef = ref<FormInstance>();
const collectorId = props.data.collectorId;
const pointId = props.data.id;
const formData = ref({
value: '',
});
const onChange = (value: Dayjs, dateString: string) => {
formData.value.value = dateString;
};
const onSubmit = async () => {
const data = await formRef.value?.validate();
const params = {
...data,
pointId,
};
loading.value = true;
const response = await writePoint(collectorId, [params]);
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
</script>
<style lang="less" scoped></style>

View File

@ -1,5 +1,5 @@
<template>
<div>
<j-spin :spinning="spinning">
<j-advanced-search
:columns="columns"
target="search"
@ -13,9 +13,7 @@
:gridColumn="2"
:gridColumns="[1, 2]"
:request="queryPoint"
:defaultParams="{
sorts: [{ name: 'id', order: 'desc' }],
}"
:defaultParams="defaultParams"
:params="params"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
@ -26,12 +24,23 @@
<template #headerTitle>
<j-space>
<PermissionButton
v-if="data?.provider !== 'OPC_UA'"
type="primary"
@click="handlAdd"
hasPermission="DataCollect/Collector:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
{{ data?.provider === 'OPC_UA' ? '扫描' : '新增点位' }}
新增点位
</PermissionButton>
<PermissionButton
v-if="data?.provider === 'OPC_UA'"
type="primary"
@click="handlScan"
hasPermission="DataCollect/Collector:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
扫描
</PermissionButton>
<j-dropdown v-if="data?.provider === 'OPC_UA'">
<j-button
@ -42,6 +51,7 @@
<j-menu-item>
<PermissionButton
hasPermission="DataCollect/Collector:update"
@click="handlBatchUpdate()"
>
<template #icon
><AIcon type="FormOutlined"
@ -52,6 +62,10 @@
<j-menu-item>
<PermissionButton
hasPermission="DataCollect/Collector:delete"
:popConfirm="{
title: `确定删除?`,
onConfirm: () => handlDelete(),
}"
>
<template #icon
><AIcon type="EditOutlined"
@ -75,17 +89,6 @@
:statusText="getState(slotProps)?.text"
:statusNames="Object.fromEntries(colorMap.entries())"
>
<!-- <PointCardBox
:showStatus="true"
:value="slotProps"
:actions="getActions(slotProps)"
:active="_selectedRowKeys.includes(slotProps.id)"
v-bind="slotProps"
class="card-box"
:status="getState(slotProps).value"
:statusText="slotProps.runningState?.text"
:statusNames="Object.fromEntries(colorMap.entries())"
> -->
<template #title>
<slot name="title">
<div class="card-box-title">
@ -95,8 +98,19 @@
</template>
<template #action>
<div class="card-box-action">
<a><AIcon type="DeleteOutlined" /></a>
<a><AIcon type="FormOutlined" /></a>
<a>
<j-popconfirm
title="确定删除?"
@confirm="handlDelete(slotProps.id)"
>
<AIcon type="DeleteOutlined" />
</j-popconfirm>
</a>
<a
><AIcon
@click="handlEdit(slotProps)"
type="FormOutlined"
/></a>
</div>
</template>
<template #img>
@ -112,8 +126,26 @@
<div class="card-box-content">
<div class="card-box-content-left">
<span>--</span>
<a><AIcon type="EditOutlined" /></a>
<a><AIcon type="RedoOutlined" /></a>
<a
v-if="
getAccessModes(slotProps).includes(
'write',
)
"
><AIcon
@click.stop="clickEdit(slotProps)"
type="EditOutlined"
/></a>
<a
v-if="
getAccessModes(slotProps).includes(
'read',
)
"
><AIcon
@click.stop="clickRedo(slotProps)"
type="RedoOutlined"
/></a>
</div>
<div class="card-box-content-right">
<div
@ -135,21 +167,46 @@
</template>
</j-pro-table>
<SaveModBus
v-if="visibleSaveModBus"
v-if="visible.saveModBus"
:data="current"
@change="saveChange"
/>
</div>
<SaveOPCUA
v-if="visible.saveOPCUA"
:data="current"
@change="saveChange"
/>
<WritePoint
v-if="visible.writePoint"
:data="current"
@change="saveChange"
/>
<BatchUpdate
v-if="visible.batchUpdate"
:data="current"
@change="saveChange"
/>
<Scan v-if="visible.scan" :data="current" @change="saveChange" />
</j-spin>
</template>
<script lang="ts" setup name="PointPage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import { queryPoint } from '@/api/data-collect/collector';
import {
queryPoint,
batchDeletePoint,
removePoint,
readPoint,
} from '@/api/data-collect/collector';
import { message } from 'ant-design-vue';
import { useMenuStore } from 'store/menu';
import PointCardBox from './components/PointCardBox/index.vue';
import { colorMap, getState } from '../data.ts';
import WritePoint from './components/WritePoint/index.vue';
import BatchUpdate from './components/BatchUpdate/index.vue';
import SaveModBus from './Save/SaveModBus.vue';
import SaveOPCUA from './Save/SaveOPCUA.vue';
import Scan from './Scan/index.vue';
import { colorMap, getState } from '../data.ts';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
@ -163,11 +220,35 @@ const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const opcImage = getImage('/DataCollect/device-opcua.png');
const modbusImage = getImage('/DataCollect/device-modbus.png');
const visibleSaveModBus = ref(false);
const visible = reactive({
saveModBus: false,
saveOPCUA: false,
writePoint: false,
batchUpdate: false,
scan: false,
});
const current = ref({});
const accessModesOption = ref();
const _selectedRowKeys = ref<string[]>([]);
const spinning = ref(false);
const collectorId = ref(props.data.id);
const defaultParams = ref({
sorts: [{ name: 'id', order: 'desc' }],
terms: [
{
terms: [
{
column: 'collectorId',
value: collectorId.value,
// value: '1610517928766550016', //
},
],
},
],
});
const accessModesMODBUS_TCP = [
{
label: '读',
@ -179,11 +260,6 @@ const accessModesMODBUS_TCP = [
},
];
const accessModesOPC_UA = accessModesMODBUS_TCP.concat({
label: '订阅',
value: 'subscribe',
});
const columns = [
{
title: '点位名称',
@ -256,81 +332,57 @@ const columns = [
},
];
// const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
// if (!data) return [];
// const state = data.state.value;
// const stateText = state === 'enabled' ? '' : '';
// const actions = [
// {
// key: 'update',
// text: '',
// tooltip: {
// title: '',
// },
// icon: 'EditOutlined',
// onClick: () => {
// handlEdit(data.id);
// },
// },
// {
// key: 'action',
// text: stateText,
// tooltip: {
// title: stateText,
// },
// icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
// popConfirm: {
// title: `${stateText}?`,
// onConfirm: async () => {
// let res =
// state === 'enabled'
// ? await disable(data.id)
// : await enalbe(data.id);
// if (res.success) {
// message.success('');
// tableRef.value?.reload();
// } else {
// message.error('');
// }
// },
// },
// },
// {
// key: 'delete',
// text: '',
// disabled: state === 'enabled',
// tooltip: {
// title: state === 'enabled' ? '' : '',
// },
// popConfirm: {
// title: '?',
// onConfirm: async () => {
// const res = await remove(data.id);
// if (res.success) {
// message.success('');
// tableRef.value.reload();
// } else {
// message.error('');
// }
// },
// },
// icon: 'DeleteOutlined',
// },
// ];
// return actions;
// };
const handlAdd = () => {
visibleSaveModBus.value = true;
visible.saveModBus = true;
current.value = {
treeId: props.data.id,
collectorId: collectorId.value,
provider: props.data?.provider || 'MODBUS_TCP',
};
};
const handlEdit = (id: string) => {
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: false });
const handlEdit = (data: Object) => {
if (data?.provider === 'OPC_UA') {
visible.saveOPCUA = true;
} else {
visible.saveModBus = true;
}
current.value = cloneDeep(data);
};
const handlEye = (id: string) => {
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: true });
const handlDelete = async (data: string | undefined = undefined) => {
spinning.value = true;
const res = !data
? await batchDeletePoint(_selectedRowKeys.value)
: await removePoint(data as string);
if (res.status === 200) {
cancelSelect();
tableRef.value?.reload();
message.success('操作成功');
}
spinning.value = false;
};
const handlBatchUpdate = () => {
const dataSet = new Set(_selectedRowKeys.value);
const dataMap = new Map();
tableRef?.value?._dataSource.forEach((i) => {
dataSet.has(i.id) && dataMap.set(i.id, i);
});
current.value = [...dataMap.values()];
visible.batchUpdate = true;
};
const handlScan = () => {
visible.scan = true;
current.value = cloneDeep(props.data);
};
const clickEdit = async (data: object) => {
visible.writePoint = true;
current.value = cloneDeep(data);
};
const clickRedo = async (data: object) => {
const res = await readPoint(data?.collectorId, [data?.id]);
if (res.status === 200) {
cancelSelect();
tableRef.value?.reload();
message.success('操作成功');
}
};
const getQuantity = (item: Partial<Record<string, any>>) => {
@ -345,11 +397,9 @@ const getScaleFactor = (item: Partial<Record<string, any>>) => {
const { scaleFactor } = item.configuration?.codec?.configuration || '';
return !!scaleFactor ? scaleFactor + '(缩放因子)' : '';
};
const getRight1 = (item: Partial<Record<string, any>>) => {
return !!getQuantity(item) || getAddress(item) || getScaleFactor(item);
};
const getText = (item: Partial<Record<string, any>>) => {
return (item?.accessModes || []).map((i) => i?.text).join(',');
};
@ -357,22 +407,18 @@ const getInterval = (item: Partial<Record<string, any>>) => {
const { interval } = item.configuration || '';
return !!interval ? '采集频率' + interval + 'ms' : '';
};
const getaccessModesOption = () => {
return props.data?.provider !== 'MODBUS_TCP'
? accessModesMODBUS_TCP.concat({
label: '订阅',
value: 'subscribe',
})
: accessModesMODBUS_TCP;
const getAccessModes = (item: Partial<Record<string, any>>) => {
return item?.accessModes?.map((i) => i?.value);
};
const saveChange = (value: object) => {
visibleSaveModBus.value = false;
for (let key in visible) {
visible[key] = false;
}
current.value = {};
if (value) {
handleSearch(params.value);
// message.success('');
tableRef.value?.reload();
message.success('操作成功');
}
};
@ -404,22 +450,12 @@ watch(
label: '订阅',
value: 'subscribe',
});
params.value = {
terms: [
{
terms: [
{
column: 'collectorId',
value: value.id,
},
],
},
],
};
defaultParams.value.terms[0].terms[0].value = value.id;
// defaultParams.value.terms[0].terms[0].value = '1610517928766550016'; //
tableRef?.value?.reload && tableRef?.value?.reload();
}
},
{ deep: true, immediate: true },
{ immediate: true, deep: true },
);
/**
@ -435,9 +471,11 @@ const handleSearch = (e: any) => {
min-width: 480px;
a {
color: #474747;
z-index: 1;
}
a:hover {
color: #315efb;
z-index: 1;
}
.card-box-title {
font-size: 18px;
@ -459,7 +497,10 @@ const handleSearch = (e: any) => {
height: 68px;
padding-right: 20px;
display: flex;
justify-content: space-between;
justify-content: flex-start;
a {
margin-left: 10px;
}
}
.card-box-content-right {
flex: 0.8;

View File

@ -201,7 +201,6 @@ const onSubmit = () => {
};
loading.value = true;
const response = !id
? await save(params)
: await update(id, { ...props.data, ...params });
@ -238,6 +237,10 @@ const getChannelNoPaging = async () => {
};
getChannelNoPaging();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch(
() => formData.value.channelId,
(value) => {

View File

@ -19,7 +19,7 @@
新增采集器
</PermissionButton>
</div>
<a-spin :spinning="spinning">
<j-spin :spinning="spinning">
<a-tree
:tree-data="defualtDataSource"
v-model:selected-keys="selectedKeys"
@ -92,7 +92,7 @@
</template>
</a-tree>
<j-empty v-else description="暂无数据" />
</a-spin>
</j-spin>
<Save v-if="visible" :data="current" @change="saveChange" />
</div>
</template>

View File

@ -16,3 +16,166 @@ export const getState = (record: any) => {
return {};
}
};
const regOnlyNumber = new RegExp(/^\d+$/);
export const checkProviderData = {
int8: 1,
int16: 2,
int32: 4,
int64: 8,
ieee754_float: 4,
ieee754_double: 8,
hex: 1,
};
export const ModBusRules = {
name: [
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
],
function: [
{
required: true,
message: '请选择功能码',
},
],
pointKey: [
{
required: true,
message: '请输入地址',
},
{
pattern: regOnlyNumber,
message: '请输入0-255之间的正整数',
},
],
quantity: [
{
required: true,
message: '请输入寄存器数量',
},
{
pattern: regOnlyNumber,
message: '请输入1-255之间的正整数',
},
],
provider: [
{
required: true,
message: '请选择数据类型',
},
],
scaleFactor: [
{
required: true,
message: '请输入缩放因子',
},
],
accessModes: [
{
required: true,
message: '请选择访问类型',
},
],
writeByteCount: [
{
required: true,
message: '请选择是否写入数据区长度',
},
],
byteCount: [
{
required: true,
message: '请输入自定义数据区长度byte',
},
],
interval: [
{
required: true,
message: '请输入采集频率',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
};
export const OPCUARules = {
name: [
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
],
type: [
{
required: true,
message: '请选择数据类型',
},
],
accessModes: [
{
required: true,
message: '请选择访问类型',
},
],
interval: [
{
required: true,
message: '请输入采集频率',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
};
export const FormTableColumns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
},
{
title: 'nodeId',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '访问类型',
dataIndex: 'accessModes',
key: 'accessModes',
width: 300,
},
{
title: '采集频率',
key: 'interval',
dataIndex: 'interval',
width: 280,
},
{
title: '只推送变化的数据',
key: 'features',
dataIndex: 'features',
width: 200,
},
{
title: '操作',
key: 'action',
dataIndex: 'action',
fixed: 'right',
width: 80,
},
];

View File

@ -5,7 +5,7 @@
<Tree @change="changeTree" />
</div>
<div class="right">
<Point :data="data"></Point>
<Point v-if="!!data" :data="data" />
</div>
</div>
</page-container>
@ -32,7 +32,6 @@ const changeTree = (row: any) => {
.left {
width: 300px;
border-right: 1px #eeeeee solid;
// padding: 10px;
margin: 10px;
}
.right {

View File

@ -1693,7 +1693,7 @@ const Status = defineComponent({
/>
)}
{
bindParentVisible && (
bindParentVisible.value && (
<BindParentDevice
data={device.value}
onCancel={() => {

View File

@ -25,13 +25,13 @@
instanceStore.current.deviceType?.text
}}</j-descriptions-item>
<j-descriptions-item label="固件版本">{{
instanceStore.current.firmwareInfo?.version
instanceStore.current?.firmwareInfo?.version
}}</j-descriptions-item>
<j-descriptions-item label="连接协议">{{
instanceStore.current?.protocolName
instanceStore.current?.transport
}}</j-descriptions-item>
<j-descriptions-item label="消息协议">{{
instanceStore.current.transport
instanceStore.current.protocolName
}}</j-descriptions-item>
<j-descriptions-item label="创建时间">{{
instanceStore.current.createTime

View File

@ -109,7 +109,9 @@ const loading = ref<boolean>(false);
const instanceStore = useInstanceStore();
const formRef = ref();
const modelRef = reactive({
const modelRef = reactive<{
metrics: any[]
}>({
metrics: [],
});

View File

@ -23,11 +23,12 @@
:tab="i.tab"
/>
</j-tabs>
<JEmpty v-else style="margin: 250px 0" />
<JEmpty v-else style="margin: 180px 0" />
</div>
<div class="property-box-right">
<Event v-if="type === 'event'" :data="data" />
<Property v-else :data="properties" />
<Property v-else-if="type === 'property'" :data="properties" />
<JEmpty v-else style="margin: 220px 0" />
</div>
</div>
</j-card>
@ -97,6 +98,13 @@ const onSearch = () => {
} else {
tabList.value = _.cloneDeep(arr)
}
const dt = tabList.value?.[0]
if (dt) {
data.value = dt
type.value = dt.type;
} else {
type.value = ''
}
};
const tabChange = (key: string) => {
const dt = tabList.value.find((i) => i.key === key);

View File

@ -9,7 +9,9 @@
<div>
<div style="display: flex; align-items: center">
<AIcon type="ArrowLeftOutlined" @click="onBack" />
<div style="margin-left: 20px">{{ instanceStore.current.name }}</div>
<div style="margin-left: 20px">
{{ instanceStore.current.name }}
</div>
<j-divider type="vertical" />
<j-space>
<j-badge
@ -79,7 +81,7 @@
<j-descriptions-item label="所属产品">
<PermissionButton
type="link"
style="margin-top: -5px; padding: 0"
style="margin-top: -5px; padding: 0"
@click="jumpProduct"
hasPermission="device/Product:view"
>
@ -116,8 +118,8 @@ import Function from './Function/index.vue';
import Modbus from './Modbus/index.vue';
import OPCUA from './OPCUA/index.vue';
import EdgeMap from './EdgeMap/index.vue';
import Parsing from './Parsing/index.vue'
import Log from './Log/index.vue'
import Parsing from './Parsing/index.vue';
import Log from './Log/index.vue';
import { _deploy, _disconnect } from '@/api/device/instance';
import { message } from 'jetlinks-ui-components';
import { getImage } from '@/utils/comm';
@ -149,17 +151,13 @@ const list = ref([
key: 'Metadata',
tab: '物模型',
},
{
key: 'Log',
tab: '日志管理',
},
{
key: 'Function',
tab: '设备功能',
},
{
key: 'ChildDevice',
tab: '子设备',
key: 'Log',
tab: '日志管理',
},
]);
@ -174,7 +172,7 @@ const tabs = {
OPCUA,
EdgeMap,
Parsing,
Log
Log,
};
const getStatus = (id: string) => {
@ -255,6 +253,17 @@ watchEffect(() => {
tab: '设备诊断',
});
}
if (
instanceStore.current.features?.find(
(item: any) => item.id === 'transparentCodec',
) &&
!keys.includes('Parsing')
) {
list.value.push({
key: 'Parsing',
tab: '数据解析',
});
}
if (
instanceStore.current.protocol === 'modbus-tcp' &&
!keys.includes('Modbus')
@ -273,6 +282,13 @@ watchEffect(() => {
tab: 'OPC UA',
});
}
if (instanceStore.current.deviceType?.value === 'gateway') {
//
list.value.push({
key: 'ChildDevice',
tab: '子设备',
});
}
if (
instanceStore.current.accessProvider === 'edge-child-device' &&
instanceStore.current.parentId &&
@ -283,15 +299,6 @@ watchEffect(() => {
tab: '边缘端映射',
});
}
if (
instanceStore.current.features?.find((item: any) => item.id === 'transparentCodec') &&
!keys.includes('Parsing')
) {
list.value.push({
key: 'Parsing',
tab: '数据解析',
});
}
});
onUnmounted(() => {

View File

@ -51,7 +51,6 @@
import { queryNoPagingPost } from '@/api/device/product';
import { downloadFile } from '@/utils/utils';
import encodeQuery from '@/utils/encodeQuery';
import { BASE_API_PATH } from '@/utils/variable';
import { deviceExport } from '@/api/device/instance';
const emit = defineEmits(['close']);

View File

@ -1,92 +1,106 @@
<template>
<j-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleCancel" @cancel="handleCancel">
<j-modal
:maskClosable="false"
width="800px"
:visible="true"
title="当前进度"
@ok="handleCancel"
@cancel="handleCancel"
>
<div>
<j-badge v-if="flag" status="processing" text="进行中" />
<j-badge v-else status="success" text="已完成" />
<j-badge v-else status="success" text="已完成" />
</div>
<p>总数量{{count}}</p>
<a style="color: red">{{errMessage}}</a>
<p>总数量{{ count }}</p>
<a style="color: red">{{ errMessage }}</a>
</j-modal>
</template>
<script lang="ts" setup>
import { EventSourcePolyfill } from 'event-source-polyfill'
import { EventSourcePolyfill } from 'event-source-polyfill';
const emit = defineEmits(['close'])
const emit = defineEmits(['close', 'save']);
const props = defineProps({
api: {
type: String,
default: ''
default: '',
},
type: {
type: String,
default: ''
}
})
const eventSource = ref<Record<string, any>>({})
const count = ref<number>(0)
const flag = ref<boolean>(false)
const errMessage = ref<string>('')
const isSource = ref<boolean>(false)
const id = ref<string>('')
const source = ref<Record<string, any>>({})
default: '',
},
});
// const eventSource = ref<Record<string, any>>({})
const count = ref<number>(0);
const flag = ref<boolean>(false);
const errMessage = ref<string>('');
const isSource = ref<boolean>(false);
const id = ref<string>('');
const source = ref<Record<string, any>>({});
const handleCancel = () => {
emit('close')
}
emit('close');
emit('save');
};
// const handleOk = () => {
// emit('close');
// emit('save');
// };
const getData = (api: string) => {
let dt = 0
const _source = new EventSourcePolyfill(api)
source.value = _source
let dt = 0;
const _source = new EventSourcePolyfill(api);
source.value = _source;
_source.onmessage = (e: any) => {
const res = JSON.parse(e.data);
switch (props.type) {
case 'active':
if (res.success) {
dt += res.total;
count.value = dt
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg
id.value = res.source.id
isSource.value = true
} else {
errMessage.value = res.message
}
}
break;
case 'sync':
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;
}
const res = JSON.parse(e.data);
switch (props.type) {
case 'active':
if (res.success) {
dt += res.total;
count.value = dt;
} else {
if (res.source) {
const msg = `${res.source.name}: ${res.message}`;
errMessage.value = msg;
id.value = res.source.id;
isSource.value = true;
} else {
errMessage.value = res.message;
}
}
break;
case 'sync':
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.close();
};
_source.onopen = () => {};
}
};
watch(() => props.api,
watch(
() => props.api,
(newValue) => {
if(newValue) {
getData(newValue)
if (newValue) {
getData(newValue);
}
},
{deep: true, immediate: true}
)
},
{ deep: true, immediate: true },
);
</script>

View File

@ -9,12 +9,7 @@
:confirmLoading="loading"
>
<div style="margin-top: 10px">
<j-form
:layout="'vertical'"
ref="formRef"
:rules="rules"
:model="modelRef"
>
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-row type="flex">
<j-col flex="180px">
<j-form-item name="photoUrl">
@ -22,14 +17,33 @@
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item name="id">
<j-form-item
name="id"
:rules="[
{
pattern: /^[a-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
max: 64,
message: '最多输入64个字符',
},
{
validator: vailId,
trigger: 'blur',
},
]"
>
<template #label>
<span>
ID
<j-tooltip title="若不填写系统将自动生成唯一ID">
<j-tooltip
title="若不填写系统将自动生成唯一ID"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px;" />
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
@ -39,7 +53,20 @@
:disabled="!!data?.id"
/>
</j-form-item>
<j-form-item label="名称" name="name">
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
]"
>
<j-input
v-model:value="modelRef.name"
placeholder="请输入名称"
@ -47,13 +74,23 @@
</j-form-item>
</j-col>
</j-row>
<j-form-item name="productId">
<j-form-item
name="productId"
:rules="[
{
required: true,
message: '请选择所属产品',
},
]"
>
<template #label>
<span>所属产品
<span
>所属产品
<j-tooltip title="只能选择“正常”状态的产品">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px" />
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
@ -68,10 +105,20 @@
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{item.name}}</j-select-option>
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
<j-form-item label="说明" name="describe">
<j-form-item
label="说明"
name="describe"
:rules="[
{
max: 200,
message: '最多输入200个字符'
},
]"
>
<j-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
@ -123,45 +170,6 @@ const vailId = async (_: Record<string, any>, value: string) => {
}
};
const rules = {
name: [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
],
photoUrl: [
{
required: true,
message: '请上传图标',
},
],
productId: [
{
required: true,
message: '请选择所属产品',
},
],
id: [
{
max: 64,
message: '最多输入64个字符',
},
{
pattern: /^[j-zA-Z0-9_\-]+$/,
message: '请输入英文或者数字或者-或者_',
},
{
validator: vailId,
trigger: 'blur',
},
],
};
watch(
() => props.data,
(newValue) => {
@ -199,13 +207,13 @@ const handleSave = () => {
.validate()
.then(async (_data: any) => {
loading.value = true;
const obj = {...toRaw(modelRef), ..._data}
if(!obj.id){
delete obj.id
const obj = { ..._data };
if (!obj.id) {
delete obj.id;
}
const resp = await update(obj).finally(() => {
loading.value = false;
})
});
if (resp.status === 200) {
message.success('操作成功!');
emit('save');

View File

@ -252,6 +252,7 @@
@close="operationVisible = false"
:api="api"
:type="type"
@save="onRefresh"
/>
<Save
v-if="visible"
@ -314,6 +315,7 @@ const columns = [
key: 'id',
search: {
type: 'string',
defaultTermType: 'eq'
},
},
{
@ -322,6 +324,7 @@ const columns = [
key: 'name',
search: {
type: 'string',
first: true
},
},
{
@ -389,6 +392,7 @@ const columns = [
hideInTable: true,
search: {
type: 'select',
rename: 'productId$product-info',
options: () =>
new Promise((resolve) => {
getProviders().then((resp: any) => {

View File

@ -76,7 +76,7 @@ const deviceBootConfig: bootConfig[] = [
{
english: 'STEP1',
label: '创建产品',
link: '/iot/device/Product',
link: 'device/Product',
auth: productPermission('add'),
image: '/images/home/guide-home1.png',
params: {
@ -86,7 +86,7 @@ const deviceBootConfig: bootConfig[] = [
{
english: 'STEP2',
label: '创建设备',
link: '/iot/device/Instance',
link: 'device/Instance',
auth: devicePermission('add'),
image: '/images/home/guide-home1.png',
params: {
@ -96,7 +96,7 @@ const deviceBootConfig: bootConfig[] = [
{
english: 'STEP3',
label: '规则引擎',
link: '/iot/rule-engine/Instance',
link: 'rule-engine/Instance',
auth: rulePermission('add'),
image: '/images/home/guide-home3.png',
params: {

View File

@ -10,7 +10,7 @@
@cancel="_vis = false"
:confirmLoading="loading"
>
<j-advanced-search
<pro-search
type="simple"
:columns="columns"
target="media"

View File

@ -1,7 +1,7 @@
<!-- 国标级联-通道列表 -->
<template>
<page-container>
<j-advanced-search
<pro-search
type="simple"
:columns="columns"
target="media"

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<j-advanced-search
<pro-search
:columns="columns"
target="media-cascade"
@search="handleSearch"

View File

@ -19,7 +19,7 @@
</div>
</div>
<div class="right">
<j-advanced-search
<pro-search
type="simple"
:columns="columns"
target="product"

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<j-advanced-search
<pro-search
:columns="columns"
target="notice-config"
@search="handleSearch"

View File

@ -135,11 +135,14 @@
v-model:value="
formData.configuration.port
"
:min="1"
:max="65535"
/>
<j-checkbox
v-model:value="
v-model:checked="
formData.configuration.ssl
"
@change="handleSslChange"
>
开启SSL
</j-checkbox>
@ -408,7 +411,6 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
);
const getDetail = async () => {
console.log('getDetail', route)
if (route.params.id === ':id') return;
const res = await configApi.detail(route.params.id as string);
// formData.value = res.result;
@ -438,6 +440,12 @@ const handleProviderChange = () => {
resetPublicFiles();
};
const handleSslChange = () => {
formData.value.configuration.port = formData.value.configuration.ssl
? 465
: 25;
};
/**
* 重置字段值
*/

View File

@ -1,6 +1,6 @@
<template>
<j-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%">
<j-advanced-search
<pro-search
type="simple"
:columns="columns"
@search="handleSearch"

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<j-advanced-search
<pro-search
:columns="columns"
target="notice-config"
@search="handleSearch"

View File

@ -11,10 +11,26 @@
<span v-if="column.dataIndex === 'id'">
{{ record[column.dataIndex] }}
</span>
<j-input
v-if="column.dataIndex === 'name'"
v-model:value="record.name"
/>
<template v-if="column.dataIndex === 'name'">
<j-input
v-model:value="record.name"
:class="
!record.name || record.name.length > 64
? 'has-error'
: ''
"
/>
<!-- antd useForm 无table表单校验 手动添加校验 -->
<div
class="error-text"
v-show="!record.name || record.name.length > 64"
>
<span v-show="!record.name"> 该字段是必填字段 </span>
<span v-show="record.name.length > 64">
最多可输入64个字符
</span>
</div>
</template>
<j-select
v-if="column.dataIndex === 'type'"
v-model:value="record.type"
@ -91,7 +107,7 @@ const columns = [
{
title: '名称',
dataIndex: 'name',
// width: 160,
width: 160,
},
{
title: '类型',
@ -133,4 +149,20 @@ const handleTypeChange = (record: IVariable) => {
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.table-wrapper {
.has-error {
border-color: rgba(255, 77, 79);
&:focus {
border-color: rgba(255, 120, 117);
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
border-right-width: 1px !important;
outline: 0;
}
}
.error-text {
color: rgba(255, 77, 79);
font-size: 12px;
}
}
</style>

View File

@ -526,6 +526,7 @@
</j-form-item>
<j-form-item
v-if="formData.template.templateType === 'tts'"
v-bind="validateInfos['template.ttsmessage']"
>
<template #label>
<span>
@ -541,8 +542,7 @@
</span>
</template>
<j-textarea
v-model:value="formData.template.message"
show-count
v-model:value="formData.template.ttsmessage"
:rows="5"
placeholder="内容中的变量将用于阿里云语音验证码"
/>
@ -840,7 +840,8 @@ const resetPublicFiles = () => {
formData.value.template.templateType = 'tts';
formData.value.template.templateCode = '';
formData.value.template.ttsCode = '';
formData.value.template.message = '';
// formData.value.template.message = '';
formData.value.template.ttsmessage = '';
formData.value.template.playTimes = 1;
formData.value.template.calledShowNumbers = '';
formData.value.template.calledNumber = '';
@ -910,26 +911,29 @@ const formRules = ref({
//
'template.templateType': [{ required: true, message: '请选择类型' }],
'template.templateCode': [{ required: true, message: '请输入模板ID' }],
'template.calledShowNumbers': [
{
trigger: 'change',
validator(_rule: Rule, value: string) {
if (!value) return Promise.resolve();
if (!phoneRegEx(value)) return Promise.reject('请输入有效号码');
return Promise.resolve();
},
},
],
//
'template.code': [{ required: true, message: '请选择模板' }],
'template.signName': [{ required: true, message: '请输入签名' }],
// webhook
description: [{ max: 200, message: '最多可输入200个字符' }],
'template.message': [
{ required: true, message: '请输入' },
{
required: true,
message: '请输入',
},
{ max: 500, message: '最多可输入500个字符' },
],
'template.calledShowNumbers': [
{
trigger: 'blur',
validator(_rule: Rule, value: string) {
if (!phoneRegEx(value)) {
return Promise.reject('请输入有效号码');
}
return Promise.resolve();
},
},
],
'template.ttsmessage': [{ max: 500, message: '最多可输入500个字符' }],
});
const { resetFields, validate, validateInfos, clearValidate } = useForm(
@ -1033,6 +1037,7 @@ const variableReg = () => {
* 钉钉机器人 消息类型选择改变
*/
const handleMessageTypeChange = () => {
if (formData.value.type !== 'dingTalk') return;
delete formData.value.template.markdown;
delete formData.value.template.link;
delete formData.value.template.text;
@ -1159,6 +1164,13 @@ const getSignsList = async () => {
*/
const btnLoading = ref<boolean>(false);
const handleSubmit = () => {
// ,
if (
formData.value.variableDefinitions.length &&
formData.value.variableDefinitions.some((s: any) => !s.name)
)
return;
//
if (formData.value.type === 'email') delete formData.value.configId;
if (formData.value.template.messageType === 'markdown')
delete formData.value.template.link;
@ -1169,9 +1181,16 @@ const handleSubmit = () => {
setTimeout(() => {
validate()
.then(async () => {
if (formData.value.provider === 'ttsCode')
if (formData.value.provider === 'aliyun') {
formData.value.template.ttsCode =
formData.value.template.templateCode;
// messagemessage,
// ttsmessage, , message,
formData.value.template.message =
formData.value.template.ttsmessage;
delete formData.value.template.ttsmessage;
}
btnLoading.value = true;
let res;
if (!formData.value.id) {

View File

@ -1,6 +1,6 @@
<template>
<j-modal v-model:visible="_vis" title="通知记录" :footer="null" width="70%">
<j-advanced-search
<pro-search
type="simple"
:columns="columns"
@search="handleSearch"

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<j-advanced-search
<pro-search
:columns="columns"
target="notice-config"
@search="handleSearch"

View File

@ -60,6 +60,7 @@ export type TemplateFormData = {
templateCode?: string;
ttsCode?: string;
// message?: string;
ttsmessage?: string;
playTimes?: number;
calledShowNumbers?: string;
calledNumber?: string;

View File

@ -189,7 +189,8 @@ export const TEMPLATE_FIELD_MAP = {
templateType: 'tts',
templateCode: '',
ttsCode: '',
message: '',
// message: '',
ttsmessage: '',
playTimes: 1,
calledShowNumbers: '',
calledNumber: '',

View File

@ -13,17 +13,16 @@
import { useSceneStore } from '@/store/scene';
import Action from '../action/index.vue';
import { storeToRefs } from 'pinia';
import { ActionsType } from '@/components/Table';
import { ActionsType } from '@/views/rule-engine/Scene/typings';
const sceneStore = useSceneStore();
const { data } = storeToRefs(sceneStore);
const onActionAdd = (_data: ActionsType) => {
console.log(_data)
// if (data?.branches && _data) {
// const newThen = [...data?.branches?.[0].then, data];
// data.branches[0].then = newThen;
// }
const onActionAdd = (_data: any) => {
if (data.value?.branches && _data) {
data?.value.branches?.[0].then.push(_data)
console.log(data?.value.branches?.[0].then)
}
};
const onActionUpdate = (_data: ActionsType, type: boolean) => {

View File

@ -18,13 +18,7 @@
},
]"
>
<!-- <j-card-select
v-model:value="formModel.type"
:options="options"
type="horizontal"
float="right"
/> -->
<a-radio-group v-model:value="formModel.type" :options="options" />
<CardSelect v-model:value="formModel.type" :options="options"/>
</a-form-item>
<ActionTypeComponent
v-bind="props"
@ -46,6 +40,7 @@ import { PropType } from 'vue';
import { ActionsType } from '../../../typings';
import ActionTypeComponent from './ActionTypeComponent.vue';
import { randomString } from '@/utils/utils';
import CardSelect from '../../components/CardSelect.vue'
const props = defineProps({
branchesName: {

View File

@ -1,11 +1,19 @@
<template>
<a-spin :spinning="loading">
<!-- <j-card-select
v-model:value="notifyType"
:options="options"
:icon-size="106"
/> -->
<a-radio-group v-model:value="notifyType" :options="options" @change="onRadioChange" />
<div class="notify-type-warp" :class="{ disabled: disabled }">
<div
:key="item.id"
v-for="item in options"
class="notify-type-item"
:class="{ active: notifyType === item.value }"
@click="onSelect(item.value)"
>
<div class="notify-type-item-image">
<img :width="106" :src="item.iconUrl" />
</div>
<div class="notify-type-item-title">{{item.label}}</div>
</div>
</div>
</a-spin>
</template>
@ -26,9 +34,13 @@ const props = defineProps({
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:value'])
const emit = defineEmits(['update:value']);
const loading = ref<boolean>(false);
const notifyType = ref('');
@ -37,17 +49,19 @@ const options = ref<any[]>([]);
watch(
() => props.value,
(newVal) => {
notifyType.value = newVal
notifyType.value = newVal;
},
{ deep: true, immediate: true },
);
const onRadioChange = (e: any) => {
emit('update:value', e.target.value)
}
const onSelect = (val: string) => {
if (!props.disabled) {
emit('update:value', val);
}
};
onMounted(() => {
loading.value = true
loading.value = true;
notice.queryMessageType().then((resp) => {
if (resp.status === 200) {
options.value = (resp.result as any[]).map((item) => {
@ -58,11 +72,65 @@ onMounted(() => {
};
});
}
loading.value = false
loading.value = false;
});
notifyType.value = props.value
notifyType.value = props.value;
});
</script>
<style lang="less" scoped>
.notify-type-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.notify-type-item {
display: flex;
flex-direction: column;
align-items: center;
width: 172px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.notify-type-item-title {
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
}
.notify-type-item-image {
width: 106px;
margin: 16px 33px;
}
&:hover {
color: @primary-color-hover;
opacity: 0.8;
}
&.active {
border-color: @primary-color-active;
opacity: 1;
}
}
&.disabled {
.notify-type-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -26,7 +26,7 @@
:actions="
serialArray.length ? serialArray[0].actions : []
"
@add="onAdd"
@add="(_item) => onAdd(_item, false)"
@delete="onDelete"
/>
</div>
@ -50,7 +50,7 @@
? parallelArray[0].actions
: []
"
@add="onAdd"
@add="(_item) => onAdd(_item, true)"
@delete="onDelete"
/>
</div>
@ -98,6 +98,8 @@ watch(
parallelArray.value = newVal.filter((item) => item.parallel);
serialArray.value = newVal.filter((item) => !item.parallel);
console.log(parallelArray.value, serialArray.value, '123')
const isSerialActions = serialArray.value.some((item) => {
return !!item.actions.length;
});
@ -128,7 +130,7 @@ const onDelete = (_key: string) => {
emit('update', serialArray[0], false);
}
};
const onAdd = (actionItem: any) => {
const onAdd = (actionItem: any, _parallel: boolean) => {
const newParallelArray = [...parallelArray.value];
if (newParallelArray.length) {
const indexOf = newParallelArray[0].actions?.findIndex(
@ -140,12 +142,11 @@ const onAdd = (actionItem: any) => {
newParallelArray[0].actions.push(actionItem);
}
parallelArray.value = [...newParallelArray];
console.log(parallelArray.value);
emit('update', newParallelArray[0], true);
emit('update', newParallelArray[0], _parallel);
} else {
actionItem.key = randomString();
emit('add', {
parallel: true,
parallel: _parallel,
key: randomString(),
actions: [actionItem],
});

View File

@ -0,0 +1,128 @@
<template>
<div class="scene-trigger-way-warp" :class="{disabled: disabled}">
<template v-for="item in options" :key="item.value">
<div
class="trigger-way-item"
:class="{ active: item?.value === value }"
:style="{width: `${cardSize}px`}"
@click="onSelect(item.value)"
>
<div class="way-item-title">
<p>{{ item.label }}</p>
<span>{{ item.subLabel }}</span>
</div>
<div class="way-item-image">
<img :src="item.iconUrl" :width="imageSize" />
</div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
const props = defineProps({
value: {
type: String,
default: '',
},
options: {
type: Array as PropType<any[]>,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String as PropType<'vertical' | 'horizontal'>,
default: 'horizontal',
},
imageSize: {
type: Number,
default: 48
},
cardSize: {
type: Number,
default: 237
}
});
const emit = defineEmits(['update:value'])
const onSelect = (_type: string) => {
if (!props.disabled) {
emit('update:value', _type)
}
}
</script>
<style lang="less" scoped>
.scene-trigger-way-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.trigger-way-item {
display: flex;
justify-content: space-between;
padding: 16px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.way-item-title {
p {
margin-bottom: 8px;
font-weight: bold;
font-size: 16px;
}
span {
color: rgba(#000, 0.35);
font-size: 12px;
}
}
.way-item-image {
display: flex;
align-items: center;
height: 100%;
margin: 0 !important;
opacity: 0.6;
}
&:hover {
color: @primary-color-hover;
.way-item-image {
opacity: 0.8;
}
}
&.active {
border-color: @primary-color-active;
.way-item-image {
opacity: 1;
}
}
}
&.disabled {
.trigger-way-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -19,6 +19,7 @@
:format='myFormat'
:valueFormat='myFormat'
:getPopupContainer='getPopupContainer'
popupClassName='manual-time-picker-popup'
@change='change'
/>
</div>
@ -62,7 +63,7 @@ const change = (e: string) => {
}
</script>
<style lang='less' scoped>
<style lang='less'>
.dropdown-time-picker {
>div{
position: relative !important;
@ -80,6 +81,9 @@ const change = (e: string) => {
.ant-picker-panel {
width: 100%
}
.ant-picker-footer {
border-bottom: 0px;
}
}
.ant-picker-panel-container {

View File

@ -20,6 +20,10 @@ export const getComponent = (type: string): string => {
return 'date'
case 'tree':
return 'tree'
case 'file':
return 'file'
case 'geoPoint':
return 'geoPoint'
default:
return 'input'
}
@ -33,7 +37,7 @@ export const getOption = (data: any[], value?: string | number | boolean, key: s
if (item[key] === value) {
option = data[i]
break
} else if (item.children && item.children.length){
} else if (item.children && item.children.length) {
option = getOption(item.children, value, key)
if (option) {
break

View File

@ -1,6 +1,6 @@
<template>
<page-container>
<j-advanced-search :columns="columns" target="scene" @search="handleSearch" />
<pro-search :columns="columns" target="scene" @search="handleSearch" />
<JProTable
ref="sceneRef"
:columns="columns"

View File

@ -12,7 +12,6 @@
<j-form-item label="系统名称" name="title">
<j-input
v-model:value="formValue.title"
:maxlength="64"
placeholder="请输入系统名称"
/>
</j-form-item>
@ -43,7 +42,7 @@
placeholder="请输入高德API Key"
/>
</j-form-item>
<j-form-item name="'base-path'">
<j-form-item name="base-path">
<template #label>
<span>base-path</span>
<j-tooltip title="系统后台访问的url">
@ -221,7 +220,7 @@
:action="action"
:headers="headers"
:beforeUpload="
uploader.beforeBackUpload
uploader.beforeLogoUpload
"
:showUploadList="false"
@change="uploader.changeBackUpload"
@ -286,7 +285,7 @@
</template>
<script setup lang="ts" name="Basis">
import { formType, uploaderType } from './index';
import { formType, uploaderType } from './typing';
import { getImage } from '@/utils/comm.ts';
import { message } from 'ant-design-vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
@ -312,7 +311,16 @@ const form = reactive<formType>({
title: [
{
required: true,
message: '请选择本地地址',
message: '名称必填',
trigger: 'blur',
},
{
max: 64,
message: '最多可输入64个字符',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
],
@ -411,21 +419,34 @@ const form = reactive<formType>({
const { formValue, rulesFrom } = toRefs(form);
const uploader: uploaderType = {
imageTypes: ['image/jpeg', 'image/png'],
imageTypes: ['jpg', 'jpeg', 'png', 'jfif', 'pjp', 'pjpeg'],
iconTypes: ['image/x-icon'],
// logo
beforeLogoUpload: (file) => {
const isType = uploader.imageTypes.includes(file.type);
if (!isType) {
beforeLogoUpload: ({ size, type }: File) => {
const typeBool =
uploader.imageTypes.filter((typeStr) => type.includes(typeStr))
.length > 0;
const sizeBool = size / 1024 / 1024 < 4;
if (!typeBool) {
message.error(`请上传.jpg.png.jfif.pjp.pjpeg.jpeg格式的图片`);
return false;
} else if (!sizeBool) {
message.error(`图片大小必须小于4M`);
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
message.error(`图片大小必须小于${4}M`);
}
return isType && isSize;
return typeBool && sizeBool;
},
//
beforeIconUpload: (file) => {
const typeBool = uploader.iconTypes.includes(file.type);
const sizeBool = file.size / 1024 / 1024 < 1;
if (!typeBool) {
message.error(`请上传ico格式的图片`);
} else if (!sizeBool) {
message.error(`图片大小必须小于${1}M`);
}
return typeBool && sizeBool;
},
// logo
handleChangeLogo: (info) => {
if (info.file.status === 'uploading') {
@ -435,63 +456,32 @@ const uploader: uploaderType = {
form.logoLoading = false;
form.formValue.logo = info.file.response?.result;
} else if (info.file.status === 'error') {
console.log(info.file);
form.logoLoading = false;
message.error('系统logo上传失败请稍后再试');
}
},
//
beforeBackUpload: (file) => {
const isType = uploader.imageTypes.includes(file.type);
if (!isType) {
message.error(`请上传.jpg.png.jfif.pjp.pjpeg.jpeg格式的图片`);
return false;
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
message.error(`图片大小必须小于${4}M`);
}
return isType && isSize;
},
//
//
changeBackUpload: (info) => {
if (info.file.status === 'uploading') {
form.backLoading = true;
} else if (info.file.status === 'done') {
console.log(info);
info.file.url = info.file.response?.result;
form.backLoading = false;
form.formValue.backgroud = info.file.response?.result;
} else if (info.file.status === 'error') {
console.log(info.file);
form.logoLoading = false;
message.error('背景图上传失败,请稍后再试');
}
},
//
beforeIconUpload: (file) => {
const isType = uploader.iconTypes.includes(file.type);
if (!isType) {
message.error(`请上传ico格式的图片`);
return false;
}
const isSize = file.size / 1024 / 1024 < 1;
if (!isSize) {
message.error(`图片大小必须小于${1}M`);
}
return isType && isSize;
},
//
//
changeIconUpload: (info) => {
if (info.file.status === 'uploading') {
form.iconLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
form.iconLoading = true;
form.iconLoading = false;
form.formValue.ico = info.file.response?.result;
} else if (info.file.status === 'error') {
console.log(info.file);
form.logoLoading = false;
message.error('浏览器页签上传失败,请稍后再试');
}

View File

@ -1,18 +0,0 @@
import BaseService from '@/utils/BaseService';
import { request } from 'umi';
import SystemConst from '@/utils/const';
class Service extends BaseService<ReationItem> {
getTypes = () =>
request(`/${SystemConst.API_BASE}/relation/types`, {
method: 'GET',
});
validator = (params: any) =>
request(`/${SystemConst.API_BASE}/relation/_validate?`, {
method: 'GET',
params,
});
}
export default Service;

View File

@ -30,7 +30,7 @@ export interface uploaderType {
iconTypes: Array<string>,
beforeLogoUpload: (file: UploadProps['beforeUpload']) => void
handleChangeLogo: (info: UploadChangeParam) => void
beforeBackUpload: (file: UploadProps['beforeUpload']) => void
beforeBackUpload?: (file: UploadProps['beforeUpload']) => void
changeBackUpload: (info: UploadChangeParam) => void
beforeIconUpload: (file: UploadProps['beforeUpload']) => void
changeIconUpload: (info: UploadChangeParam) => void

View File

@ -36,6 +36,7 @@
:params="query.params.value"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys.value,
onChange: pageChange
}"
@cancelSelect="table.cancelSelect"
>
@ -420,6 +421,9 @@ const table: any = {
},
};
table.init();
const pageChange = ()=>{
console.log(1111,table._selectedRowKeys.value);
}
</script>
<style lang="less" scoped>

View File

@ -3,7 +3,7 @@
visible
:title="title"
width="520px"
@cancel="emits('update:visible',false)"
@cancel="emits('update:visible', false)"
@ok="confirm"
class="edit-dialog-container"
cancelText="取消"
@ -16,7 +16,7 @@
v-model:value="form.data.parentId"
style="width: 100%"
placeholder="请选择上级组织"
:tree-data="props.treeData"
:tree-data="treeData"
:field-names="{ value: 'id' }"
>
<template #title="{ name }"> {{ name }} </template>
@ -80,6 +80,42 @@ const confirm = () => {
})
.finally(() => (loading.value = false));
};
const treeData = computed(() => {
if (!props.data.id) return props.treeData;
const result = cloneDeep(props.treeData) as treeType[];
const me = findItemById(result, props.data.id) as treeType;
me.disabled = true;
me.children && me.children.length > 0 && filterTree(me.children);
return result;
});
/**
* 在给定的树中通过id匹配
* @param node
* @param id
*/
const findItemById = (node: treeType[], id: string): treeType | null => {
let result = null;
for (const item of node) {
if (item.id === id) return item;
else if (item.children && item.children.length > 0) {
result = findItemById(item.children, id);
if (result) return result;
}
}
return null;
};
/**
* 将此树下的所有节点禁用
* @param treeNode
*/
const filterTree = (treeNode: treeType[]) => {
if (treeNode.length < 1) return;
treeNode.forEach((item) => {
item.disabled = true;
item.children && item.children.length > 0 && filterTree(item.children);
});
};
//
const formRef = ref<FormInstance>();
const form = reactive({
@ -124,6 +160,14 @@ const form = reactive({
});
form.init();
type treeType = {
id: string;
parentId?: string;
name: string;
sortIndex: string | number;
children?: treeType[];
disabled?: boolean;
};
type formType = {
id?: string;
parentId?: string;

View File

@ -14,7 +14,7 @@
<PermissionButton
type="primary"
class="add-btn"
:uhasPermission="`${permission}:add`"
:hasPermission="`${permission}:add`"
@click="openDialog()"
>
新增
@ -31,7 +31,7 @@
<span>{{ name }}</span>
<span class="func-btns" @click="(e) => e.stopPropagation()">
<PermissionButton
:uhasPermission="`${permission}:update`"
:hasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
@ -41,7 +41,7 @@
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:add`"
:hasPermission="`${permission}:add`"
type="link"
:tooltip="{
title: '新增子组织',
@ -58,7 +58,7 @@
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:hasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确定要删除吗`,
@ -103,7 +103,7 @@ const treeMap = new Map(); // 数据的map版本
const treeData = ref<any[]>([]); //
const selectedKeys = ref<string[]>([]); //
function getTree() {
function getTree(cb?: Function) {
loading.value = true;
const params = {
paging: false,
@ -121,6 +121,7 @@ function getTree() {
sourceTree.value = resp.result; //
handleTreeMap(resp.result); // map
treeData.value = resp.result; //
cb && cb();
})
.finally(() => {
loading.value = false;
@ -193,8 +194,9 @@ const openDialog = (row: any = {}) => {
if (row.parentId) {
childrens = row.children;
} else childrens = treeData.value;
const indexs = childrens.length > 0 ? childrens?.map((item) => item.sortIndex) :[0]
sortIndex =
Math.max(...(childrens?.map((item) => item.sortIndex) || [0])) + 1;
Math.max(...indexs) + 1;
}
dialog.selectItem = { ...row, sortIndex };
@ -202,15 +204,10 @@ const openDialog = (row: any = {}) => {
};
init();
function init() {
getTree();
getTree(save ? openDialog : undefined);
watch(selectedKeys, (n) => {
emits('change', n[0]);
});
if (save) {
nextTick(() => {
openDialog();
});
}
}
</script>

View File

@ -10,17 +10,18 @@
:gridColumn="2"
:params="queryParams"
:rowSelection="{
selectedRowKeys: table._selectedRowKeys.value,
selectedRowKeys: tableData._selectedRowKeys,
onChange:(keys:string[])=>tableData._selectedRowKeys = [...keys],
onSelectNone: table.cancelSelect
}"
:columns="columns"
@cancelSelect="table.cancelSelect"
>
<template #headerTitle>
<j-space>
<PermissionButton
:uhasPermission="`${permission}:assert`"
type="primary"
@click="table.clickAdd"
@click="dialogs.addShow = true"
>
<AIcon type="PlusOutlined" />资产分配
</PermissionButton>
@ -61,9 +62,7 @@
:value="slotProps"
:actions="table.getActions(slotProps, 'card')"
v-bind="slotProps"
:active="
table._selectedRowKeys.value.includes(slotProps.id)
"
:active="tableData._selectedRowKeys.includes(slotProps.id)"
@click="table.onSelectChange"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
@ -104,7 +103,7 @@
class="card-item-content-value"
>
{{
table.permissionList.value.length &&
tableData.permissionList.length &&
table.getPermissLabel(
slotProps.permission,
)
@ -145,7 +144,7 @@
</a-dropdown>
<PermissionButton
v-else
:uhasPermission="item.permission"
:hasPermission="item.permission"
:tooltip="item.tooltip"
:pop-confirm="item.popConfirm"
@click="item.onClick"
@ -157,30 +156,13 @@
}}</span>
</PermissionButton>
</a-tooltip>
<!-- <PermissionButton
:uhasPermission="`${permission}:assert`"
@click="() => table.clickEdit(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:bind`"
:popConfirm="{
title: `是否解除绑定`,
onConfirm: () => table.clickUnBind(slotProps),
}"
>
<AIcon type="DisconnectOutlined" />
</PermissionButton> -->
</template>
</CardBox>
</template>
<template #permission="slotProps">
{{
table.permissionList.value.length &&
tableData.permissionList.length &&
table.getPermissLabel(slotProps.permission)
}}
</template>
@ -199,7 +181,7 @@
<a-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
:hasPermission="i.permission"
type="link"
:tooltip="i?.tooltip"
:pop-confirm="i.popConfirm"
@ -218,7 +200,7 @@
v-model:visible="dialogs.addShow"
:query-columns="columns"
:parent-id="props.parentId"
:all-permission="table.permissionList.value"
:all-permission="tableData.permissionList"
asset-type="product"
@confirm="table.addConfirm"
/>
@ -228,7 +210,7 @@
:ids="dialogs.selectIds"
:permission-list="dialogs.permissList"
:parent-id="props.parentId"
:all-permission="table.permissionList.value"
:all-permission="tableData.permissionList"
asset-type="product"
@confirm="table.refresh"
/>
@ -331,11 +313,12 @@ const columns = [
const queryParams = ref({});
const tableRef = ref();
const table = {
_selectedRowKeys: ref<string[]>([]),
const tableData = reactive({
_selectedRowKeys: [] as string[],
selectedRows: [] as any[],
permissionList: ref<dictType>([]),
permissionList: [] as any[],
});
const table = {
init: () => {
table.getPermissionDict();
watch(
@ -354,14 +337,14 @@ const table = {
else
return [
{
permission: true,
permission: `${permission}:assert`,
key: 'edit',
tooltip: { title: '编辑' },
icon: 'EditOutlined',
onClick: () => table.clickEdit(data),
},
{
permission: true,
permission: `${permission}:assert`,
key: 'unbind',
tooltip: { title: '解除绑定' },
popConfirm: {
@ -376,12 +359,12 @@ const table = {
//
getPermissionDict: () => {
getPermissionDict_api().then((resp: any) => {
table.permissionList.value = resp.result;
tableData.permissionList = resp.result;
});
},
//
getPermissLabel: (values: string[]) => {
const permissionList = table.permissionList.value;
const permissionList = tableData.permissionList;
if (permissionList.length < 1 || values.length < 1) return '';
const result = values.map(
(key) => permissionList.find((item: any) => item.id === key)?.name,
@ -390,21 +373,22 @@ const table = {
},
//
onSelectChange: (row: any) => {
const selectedRowKeys = table._selectedRowKeys.value;
const selectedRowKeys = tableData._selectedRowKeys;
const index = selectedRowKeys.indexOf(row.id);
if (index === -1) {
selectedRowKeys.push(row.id);
table.selectedRows.push(row);
tableData.selectedRows.push(row);
} else {
selectedRowKeys.splice(index, 1);
table.selectedRows.splice(index, 1);
tableData.selectedRows.splice(index, 1);
}
},
//
cancelSelect: () => {
table._selectedRowKeys.value = [];
table.selectedRows = [];
console.log(1111);
tableData._selectedRowKeys = [];
tableData.selectedRows = [];
},
//
getData: (params: object, parentId: string) =>
@ -460,8 +444,6 @@ const table = {
}),
//
requestFun: async (oParams: any) => {
table._selectedRowKeys.value = [];
table.selectedRows = [];
if (props.parentId) {
const params = {
...oParams,
@ -502,21 +484,18 @@ const table = {
};
}
},
clickAdd: () => {
dialogs.addShow = true;
},
clickEdit: (row?: any) => {
const ids = row ? [row.id] : [...table._selectedRowKeys.value];
const ids = row ? [row.id] : [...tableData._selectedRowKeys];
if (row || table.selectedRows.length === 1) {
if (row || tableData.selectedRows.length === 1) {
const permissionList =
row?.permission || table.selectedRows[0].permission;
row?.permission || tableData.selectedRows[0].permission;
dialogs.selectIds = ids;
dialogs.permissList = permissionList;
dialogs.editShow = true;
return;
} else if (table.selectedRows.length === 0) return;
const permissionList = table.selectedRows.map(
} else if (tableData.selectedRows.length === 0) return;
const permissionList = tableData.selectedRows.map(
(item) => item.permission,
);
const mixPermissionList = intersection(...permissionList) as string[];
@ -526,7 +505,7 @@ const table = {
dialogs.editShow = true;
},
clickUnBind: (row?: any) => {
const ids = row ? [row.id] : [...table._selectedRowKeys.value];
const ids = row ? [row.id] : [...tableData._selectedRowKeys];
if (ids.length < 1) return message.warning('请勾选需要解绑的数据');
const params = [
{

View File

@ -1,6 +1,6 @@
<template>
<div>
<Search :columns="columns" @search="(p:any)=>params = p" />
<j-advanced-search :columns="columns" @search="(p:any)=>params = p" />
<j-pro-table
ref="tableRef"
@ -17,7 +17,7 @@
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:bind-user`"
:hasPermission="`${permission}:bind-user`"
@click="dialogVisible = true"
style="margin-right: 15px"
>
@ -27,7 +27,7 @@
style="display: inline-block; width: 12px; height: 1px"
></div>
<PermissionButton
:uhasPermission="`${permission}:bind`"
:hasPermission="`${permission}:bind`"
:popConfirm="{
title: `是否解除绑定`,
onConfirm: () => table.unBind(),
@ -50,7 +50,7 @@
<j-space :size="16">
<PermissionButton
type="link"
:uhasPermission="`${permission}:bind`"
:hasPermission="`${permission}:bind`"
:popConfirm="{
title: `是否解除绑定`,
onConfirm: () => table.unBind(slotProps),

View File

@ -16,13 +16,7 @@
<j-form-item
name="name"
label="姓名"
:rules="[
{ required: true, message: '请输入姓名' },
{
max: 64,
message: '最多可输入64个字符',
},
]"
:rules="[{ required: true, message: '请输入姓名' }]"
>
<j-input
v-model:value="form.data.name"
@ -35,7 +29,7 @@
name="username"
label="用户名"
:rules="[
{ required: true },
{ required: true, message: '' },
{
validator: form.rules.checkUserName,
trigger: 'blur',
@ -56,7 +50,7 @@
name="password"
label="密码"
:rules="[
{ required: true },
{ required: true, message: '' },
{
validator: form.rules.checkPassword,
trigger: 'blur',
@ -76,10 +70,10 @@
name="confirmPassword"
label="确认密码"
:rules="[
{ required: true, message: '请输入8~64位的密码' },
{ required: true, message: '' },
{
validator: form.rules.checkAgainPassword,
trigger: 'change',
trigger: 'blur',
},
]"
>
@ -91,7 +85,6 @@
</j-form-item>
</j-col>
</j-row>
<!-- 还差页面权限 -->
<j-row :gutter="24" v-if="form.IsShow('add', 'edit')">
<j-col :span="12">
<j-form-item name="roleIdList" label="角色" class="flex">
@ -104,9 +97,8 @@
></j-select>
<PermissionButton
:uhasPermission="`${rolePermission}:update`"
:hasPermission="`${rolePermission}:add`"
@click="form.clickAddItem('roleIdList', 'Role')"
class="add-item"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
@ -128,9 +120,10 @@
</template>
</j-tree-select>
<PermissionButton
:uhasPermission="`${deptPermission}:update`"
@click="form.clickAddItem('roleIdList', 'Role')"
class="add-item"
:hasPermission="`${deptPermission}:add`"
@click="
form.clickAddItem('orgIdList', 'Department')
"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
@ -235,7 +228,6 @@ const form = reactive({
rules: {
checkUserName: (_rule: Rule, value: string): Promise<any> =>
new Promise((resolve, reject) => {
console.log(_rule);
if (props.type === 'edit') return resolve('');
if (!value) return reject('请输入用户名');
@ -248,7 +240,7 @@ const form = reactive({
}),
checkPassword: (_rule: Rule, value: string): Promise<any> =>
new Promise((resolve, reject) => {
if (!value) return reject('请输入8~64位的密码');
if (!value) return reject('请输入密码');
else if (value.length > 64) return reject('最多可输入64个字符');
else if (value.length < 8) return reject('密码不能少于8位');
validateField_api('password', value).then((resp: any) => {
@ -258,7 +250,7 @@ const form = reactive({
});
}),
checkAgainPassword: (_rule: Rule, value: string): Promise<any> => {
if (!value) return Promise.reject('');
if (!value) return Promise.reject('请输入8~64位的密码');
return value === form.data.password
? Promise.resolve()
: Promise.reject('两次密码输入不一致');
@ -386,6 +378,15 @@ type optionType = {
.ant-select {
flex: 1;
}
.ant-tooltip-disabled-compatible-wrapper {
.ant-btn {
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
background: #f5f5f5;
text-shadow: none;
box-shadow: none;
}
}
.ant-btn {
width: 32px;
height: 32px;

View File

@ -1,7 +1,10 @@
<template>
<page-container>
<div class="user-container">
<j-advanced-search :columns="columns" @search="(params:any)=>queryParams = {...params}" />
<j-advanced-search
:columns="columns"
@search="(params:any)=>queryParams = {...params}"
/>
<j-pro-table
ref="tableRef"
@ -15,7 +18,7 @@
>
<template #headerTitle>
<PermissionButton
:uhasPermission="`${permission}:add`"
:hasPermission="`${permission}:add`"
type="primary"
@click="table.openDialog('add')"
>
@ -38,7 +41,7 @@
<template #action="slotProps">
<j-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
:hasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
@ -48,7 +51,7 @@
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
:hasPermission="`${permission}:action`"
type="link"
:tooltip="{
title: `${slotProps.status ? '禁用' : '启用'}`,
@ -64,7 +67,7 @@
<play-circle-outlined v-else />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:update`"
:hasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '重置密码',
@ -75,7 +78,7 @@
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:hasPermission="`${permission}:delete`"
:tooltip="{
title: slotProps.status
? '请先禁用,再删除'
@ -133,7 +136,6 @@ const columns = [
dataIndex: 'username',
key: 'username',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
@ -143,7 +145,6 @@ const columns = [
dataIndex: 'type',
key: 'type',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: () =>
@ -186,7 +187,6 @@ const columns = [
dataIndex: 'telephone',
key: 'telephone',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
@ -196,7 +196,6 @@ const columns = [
dataIndex: 'email',
key: 'email',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
@ -205,10 +204,11 @@ const columns = [
title: '操作',
dataIndex: 'action',
key: 'action',
fixed: 'right',
scopedSlots: true,
},
];
const queryParams = ({});
const queryParams = ref({});
const tableRef = ref<Record<string, any>>({}); //
const table = {

View File

@ -3693,10 +3693,10 @@ jetlinks-store@^0.0.3:
resolved "https://registry.npmjs.org/jetlinks-store/-/jetlinks-store-0.0.3.tgz"
integrity sha512-AZf/soh1hmmwjBZ00fr1emuMEydeReaI6IBTGByQYhTmK1Zd5pQAxC7WLek2snRAn/HHDgJfVz2hjditKThl6Q==
jetlinks-ui-components@^1.0.4:
version "1.0.4"
resolved "https://registry.jetlinks.cn/jetlinks-ui-components/-/jetlinks-ui-components-1.0.4.tgz#41d52892f0f4d38adc6df02a87290a3042eb5645"
integrity sha512-aX+XiGigzxZnrG52xqipxd+WuFwBeZ6+dvLkcvOfLLBqSu8sgfvr/8NJ5hFgv5Eo2QFnUJq3Qf4HXLw9Ogv/yw==
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#30de07a15481f13ea86ebc817baaab3c99034403"
integrity sha512-ULgSPU0xY6xUky3beeHVvpHyAHmT6xHsO5eS5m7a3h7AmCoxA3oTWyF20vC+K1zTJBQ7LFCouySqRRz6GimAPg==
dependencies:
"@vueuse/core" "^9.12.0"
ant-design-vue "^3.2.15"