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

This commit is contained in:
jackhoo_98 2023-01-17 18:03:36 +08:00
commit 693da4ec6d
33 changed files with 5793 additions and 4377 deletions

View File

@ -19,6 +19,7 @@
"ant-design-vue": "^3.2.15", "ant-design-vue": "^3.2.15",
"axios": "^1.2.1", "axios": "^1.2.1",
"echarts": "^5.4.1", "echarts": "^5.4.1",
"event-source-polyfill": "^1.0.31",
"jetlinks-store": "^0.0.3", "jetlinks-store": "^0.0.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"less": "^4.1.3", "less": "^4.1.3",

25
src/api/comm.ts Normal file
View File

@ -0,0 +1,25 @@
import { BASE_API_PATH } from "@/utils/variable";
import server from '@/utils/request'
import { SearchHistoryList } from 'components/Search/types'
export const FILE_UPLOAD = `${BASE_API_PATH}/file/static`;
/**
*
* @param data
* @param target
*/
export const saveSearchHistory = (data: any, target:string) => server.post(`/user/settings/${target}`, data)
/**
*
* @param target
*/
export const getSearchHistory = (target:string) => server.get<SearchHistoryList[]>(`/user/settings/${target}`)
/**
*
* @param id
* @param target
*/
export const deleteSearchHistory = (target:string, id:string) => server.remove<SearchHistoryList[]>(`/user/settings/${target}/${id}`)

View File

@ -51,4 +51,25 @@ export const _deploy = (id: string) => server.post(`/device-instance/${id}/deplo
* @param data * @param data
* @returns * @returns
*/ */
export const _undeploy = (id: string) => server.post(`/device-instance/${id}/undeploy`) export const _undeploy = (id: string) => server.post(`/device-instance/${id}/undeploy`)
/**
*
* @param data id数组
* @returns
*/
export const batchDeployDevice = (data: string[]) => server.put(`/device-instance/batch/_deploy`, data)
/**
*
* @param data id数组
* @returns
*/
export const batchUndeployDevice = (data: string[]) => server.put(`/device-instance/batch/_unDeploy`, data)
/**
*
* @param data id数组
* @returns
*/
export const batchDeleteDevice = (data: string[]) => server.put(`/device-instance/batch/_delete`, data)

View File

@ -36,4 +36,10 @@ export const getCodecs = () => server.get<{id: string, name: string}>('/device/p
* @param id ID * @param id ID
* @returns * @returns
*/ */
export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`) export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`)
/**
*
* @param data
*/
export const category = (data: any) => server.post('/device/category/_tree', data)

View File

@ -3,4 +3,4 @@ import server from '@/utils/request';
// 保存 // 保存
export const save_api = (data: any) => server.post(`/system/config/scope/_save`, data) export const save_api = (data: any) => server.post(`/system/config/scope/_save`, data)
// 获取详情 // 获取详情
export const getDetails_api = (data: any) => server.post(`/system/config/scopes`, data) export const getDetails_api = (data: any) => server.post(`/system/config/scopes`, data)

View File

@ -21,7 +21,11 @@ const iconKeys = [
'StopOutlined', 'StopOutlined',
'CheckOutlined', 'CheckOutlined',
'CloseOutlined', 'CloseOutlined',
'DownOutlined' 'DownOutlined',
'ImportOutlined',
'ExportOutlined',
'SyncOutlined',
'ExclamationCircleOutlined'
] ]
const Icon = (props: {type: string}) => { const Icon = (props: {type: string}) => {

View File

@ -31,7 +31,7 @@
v-model:value='formData.data[item.name]' v-model:value='formData.data[item.name]'
:options='item.options' :options='item.options'
/> />
<a-inputnumber <a-input-number
v-else-if='item.component === componentType.inputNumber' v-else-if='item.component === componentType.inputNumber'
v-bind='item.componentProps' v-bind='item.componentProps'
v-model:value='formData.data[item.name]' v-model:value='formData.data[item.name]'

View File

@ -32,6 +32,7 @@ self.MonacoEnvironment = {
const props = defineProps({ const props = defineProps({
modelValue: [String, Number], modelValue: [String, Number],
theme: { type: String, default: 'vs-dark' },
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@ -48,7 +49,7 @@ onMounted(() => {
tabSize: 2, tabSize: 2,
automaticLayout: true, automaticLayout: true,
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
theme: 'vs-dark', // : vs(), vs-dark(), hc-black() theme: props.theme, // : vs(), vs-dark(), hc-black()
}); });
instance.onDidChangeModelContent(() => { instance.onDidChangeModelContent(() => {

View File

@ -0,0 +1,125 @@
<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

@ -6,6 +6,7 @@
:options='typeOptions' :options='typeOptions'
v-model:value='termsModel.type' v-model:value='termsModel.type'
style='width: 100%;' style='width: 100%;'
@change='valueChange'
/> />
<span v-else> <span v-else>
{{ {{
@ -17,65 +18,86 @@
class='JSearch-item--column' class='JSearch-item--column'
:options='columnOptions' :options='columnOptions'
v-model:value='termsModel.column' v-model:value='termsModel.column'
@change='columnChange'
/> />
<a-select <a-select
class='JSearch-item--termType' class='JSearch-item--termType'
:options='termTypeOptions' :options='termTypeOptions.option'
v-model:value='termsModel.termType' v-model:value='termsModel.termType'
@change='termTypeChange'
/> />
<div class='JSearch-item--value'> <div class='JSearch-item--value'>
<a-input <a-input
v-if='component === componentType.input' v-if='component === componentType.input'
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-select <a-select
v-else-if='component === componentType.select' v-else-if='component === componentType.select'
showSearch
:loading='optionLoading'
v-model:value='termsModel.value' v-model:value='termsModel.value'
:options='options' :options='options'
@change='(v) => valueChange(v)' style='width: 100%'
:filterOption='(v, option) => filterTreeSelectNode(v, option, "label")'
@change='valueChange'
/> />
<a-inputnumber <a-input-number
v-else-if='component === componentType.inputNumber' v-else-if='component === componentType.inputNumber'
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-input-password <a-input-password
v-else-if='component === componentType.password' v-else-if='component === componentType.password'
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-switch <a-switch
v-else-if='component === componentType.switch' v-else-if='component === componentType.switch'
v-model:checked='termsModel.value' v-model:checked='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-radio-group <a-radio-group
v-else-if='component === componentType.radio' v-else-if='component === componentType.radio'
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-checkbox-group <a-checkbox-group
v-else-if='component === componentType.checkbox' v-else-if='component === componentType.checkbox'
v-model:value='termsModel.value' v-model:value='termsModel.value'
:options='options' :options='options'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-time-picker <a-time-picker
v-else-if='component === componentType.time' v-else-if='component === componentType.time'
valueFormat='HH:mm:ss'
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' style='width: 100%'
@change='valueChange'
/> />
<a-date-picker <a-date-picker
v-else-if='component === componentType.date' v-else-if='component === componentType.date'
showTime
v-model:value='termsModel.value' v-model:value='termsModel.value'
@change='(v) => valueChange(v)' valueFormat='YYYY-MM-DD HH:mm:ss'
style='width: 100%'
@change='valueChange'
/> />
<a-tree-select <a-tree-select
v-else-if='component === componentType.tree' v-else-if='component === componentType.treeSelect'
showSearch
v-model:value='termsModel.value' v-model:value='termsModel.value'
:tree-data='options' :tree-data='options'
@change='(v) => valueChange(v)' style='width: 100%'
:fieldNames='{ label: "name", value: "id" }'
@change='valueChange'
:filterTreeNode='(v, option) => filterSelectNode(v, option)'
/> />
</div> </div>
</div> </div>
@ -85,18 +107,19 @@
import { componentType } from 'components/Form' import { componentType } from 'components/Form'
import { typeOptions, termType } from './util' import { typeOptions, termType } from './util'
import { PropType } from 'vue' import { PropType } from 'vue'
import type { SearchItemProps, SearchItemData } from './types' import type { SearchItemData, SearchProps, Terms } from './types'
import { cloneDeep } from 'lodash-es' import { cloneDeep, get, isArray, isFunction } from 'lodash-es'
import { filterTreeSelectNode, filterSelectNode } from '@/utils/comm'
type ItemDataProps = Omit<SearchItemData, 'title'> type ItemType = SearchProps['type']
interface Emit { interface Emit {
(e: 'change', data: ItemDataProps): void (e: 'change', data: SearchItemData): void
} }
const props = defineProps({ const props = defineProps({
columns: { columns: {
type: Array as PropType<SearchItemProps[]>, type: Array as PropType<SearchProps[]>,
default: () => [], default: () => [],
required: true required: true
}, },
@ -107,66 +130,146 @@ const props = defineProps({
expand: { expand: {
type: Boolean, type: Boolean,
default: false default: false
},
termsItem: {
type: Object as PropType<Terms>,
default: {}
} }
}) })
const emit = defineEmits<Emit>() const emit = defineEmits<Emit>()
const termsModel = reactive<ItemDataProps>({ const termsModel = reactive<SearchItemData>({
type: 'or', type: 'or',
value: '', value: '',
termType: 'eq', termType: 'like',
column: '' column: ''
}) })
const component = ref(componentType.input) const component = ref(componentType.input)
const options = ref([]) const options = ref<any[]>([])
const columnOptions = ref<({ label: string, value: string})[]>([]) const columnOptions = ref<({ label: string, value: string})[]>([])
const columnOptionMap = new Map() const columnOptionMap = new Map()
const termTypeOptions = reactive(termType) const termTypeOptions = reactive({option: termType})
const getTermType = (type: string) => { const optionLoading = ref(false)
/**
* 根据类型切换默termType值
* @param type
*/
const getTermType = (type?: ItemType) => {
termTypeOptions.option = termType
switch (type) { switch (type) {
case 'select': case 'select':
case 'treeSelect': case 'treeSelect':
case 'number':
return 'eq' return 'eq'
case 'date': case 'date':
case 'time': case 'time':
//
termTypeOptions.option = termType.filter(item => ['gt','lt'].includes(item.value))
return 'gt' return 'gt'
default: default:
return 'like' 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 = () => { const handleItem = () => {
columnOptionMap.clear() columnOptionMap.clear()
columnOptions.value = [] columnOptions.value = []
if (!props.columns.length) return if (!props.columns.length) return
// columnOptions.value = props.columns.map(item => { // columnsMap
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]
termsModel.column = _itemColumn.column
termsModel.termType = _itemColumn.defaultTermType || getTermType(_itemColumn.type as string)
columnOptions.value = props.columns.map(item => {
columnOptionMap.set(item.column, item) columnOptionMap.set(item.column, item)
return { return {
label: item.title, label: item.title,
value: item.column 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 valueChange = (value: any) => { const termTypeChange = () => {
valueChange()
}
const valueChange = () => {
emit('change', { emit('change', {
type: termsModel.type, type: termsModel.type,
value: termsModel.value, value: termsModel.value,
@ -177,6 +280,27 @@ const valueChange = (value: any) => {
handleItem() handleItem()
watch( props.termsItem, (newValue) => {
const path = props.index < 4 ? [0, 'terms', props.index - 1] : [1, 'terms', props.index - 4]
const itemData: SearchItemData = get(newValue.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)
}
} else {
handleItem()
}
}, { immediate: true, deep: true })
</script> </script>
<style scoped lang='less'> <style scoped lang='less'>

View File

@ -0,0 +1,100 @@
<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

@ -4,9 +4,9 @@
<div v-if='props.type === "advanced"' :class='["JSearch-content senior", expand ? "senior-expand" : "", screenSize ? "big" : "small"]'> <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='["JSearch-items", expand ? "items-expand" : "", layout]'>
<div class='left'> <div class='left'>
<SearchItem :expand='expand' :index='1' :columns='searchItems' /> <SearchItem :expand='expand' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms'/>
<SearchItem v-if='expand' :expand='expand' :index='2' :columns='searchItems' /> <SearchItem v-if='expand' :expand='expand' :index='2' :columns='searchItems' @change='(v) => itemValueChange(v, 2)' :termsItem='terms'/>
<SearchItem v-if='expand' :expand='expand' :index='3' :columns='searchItems' /> <SearchItem v-if='expand' :expand='expand' :index='3' :columns='searchItems' @change='(v) => itemValueChange(v, 3)' :termsItem='terms'/>
</div> </div>
<div class='center' v-if='expand'> <div class='center' v-if='expand'>
<a-select <a-select
@ -16,31 +16,17 @@
/> />
</div> </div>
<div class='right' v-if='expand'> <div class='right' v-if='expand'>
<SearchItem :expand='expand' :index='4' :columns='searchItems' /> <SearchItem :expand='expand' :index='4' :columns='searchItems' @change='(v) => itemValueChange(v, 4)' :termsItem='terms'/>
<SearchItem :expand='expand' :index='5' :columns='searchItems' /> <SearchItem :expand='expand' :index='5' :columns='searchItems' @change='(v) => itemValueChange(v, 5)' :termsItem='terms'/>
<SearchItem :expand='expand' :index='6' :columns='searchItems' /> <SearchItem :expand='expand' :index='6' :columns='searchItems' @change='(v) => itemValueChange(v, 6)' :termsItem='terms'/>
</div> </div>
</div> </div>
<div :class='["JSearch-footer", expand ? "expand" : ""]'> <div :class='["JSearch-footer", expand ? "expand" : ""]'>
<div class='JSearch-footer--btns'> <div class='JSearch-footer--btns'>
<a-dropdown-button type="primary"> <History :target='target' @click='searchSubmit' @itemClick='historyItemClick' />
搜索 <SaveHistory :terms='terms' :target='target'/>
<template #overlay> <a-button @click='reset'>
<a-menu v-if='!!historyList.length'> <template #icon><RedoOutlined /></template>
<a-menu-item>
</a-menu-item>
</a-menu>
<a-empty v-else />
</template>
<template #icon><SearchOutlined /></template>
</a-dropdown-button>
<a-button>
<template #icon><PoweroffOutlined /></template>
保存
</a-button>
<a-button>
<template #icon><PoweroffOutlined /></template>
重置 重置
</a-button> </a-button>
</div> </div>
@ -54,17 +40,17 @@
<div v-else class='JSearch-content simple big'> <div v-else class='JSearch-content simple big'>
<div class='JSearch-items'> <div class='JSearch-items'>
<div class='left'> <div class='left'>
<SearchItem :expand='false' :index='1' /> <SearchItem :expand='false' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms'/>
</div> </div>
</div> </div>
<div class='JSearch-footer'> <div class='JSearch-footer'>
<div class='JSearch-footer--btns'> <div class='JSearch-footer--btns'>
<a-button type="primary"> <a-button type="primary" @click='searchSubmit'>
<template #icon><SearchOutlined /></template> <template #icon><SearchOutlined /></template>
搜索 搜索
</a-button> </a-button>
<a-button> <a-button @click='reset'>
<template #icon><PoweroffOutlined /></template> <template #icon><RedoOutlined /></template>
重置 重置
</a-button> </a-button>
</div> </div>
@ -76,18 +62,25 @@
<script setup lang='ts' name='Search'> <script setup lang='ts' name='Search'>
import SearchItem from './Item.vue' import SearchItem from './Item.vue'
import { typeOptions } from './util' import { typeOptions } from './util'
import { useElementSize } from '@vueuse/core' import { useElementSize, useUrlSearchParams } from '@vueuse/core'
import { omit } from 'lodash-es' import { cloneDeep, isFunction, isString, set } from 'lodash-es'
import { SearchOutlined, DownOutlined } from '@ant-design/icons-vue'; import { SearchOutlined, DownOutlined, RedoOutlined } from '@ant-design/icons-vue';
import type { SearchItemProps } from './types'
import { PropType } from 'vue' import { PropType } from 'vue'
import { JColumnsProps } from 'components/Table/types' 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
}
interface Emit {
(e: 'search', data: Terms): void
}
const props = defineProps({ const props = defineProps({
defaultParams: {
type: Object,
default: () => ({})
},
columns: { columns: {
type: Array as PropType<JColumnsProps[]>, type: Array as PropType<JColumnsProps[]>,
default: () => [], default: () => [],
@ -97,7 +90,7 @@ const props = defineProps({
type: String, type: String,
default: 'advanced' default: 'advanced'
}, },
key: { target: {
type: String, type: String,
default: '', default: '',
required: true required: true
@ -107,11 +100,13 @@ const props = defineProps({
const searchRef = ref(null) const searchRef = ref(null)
const { width } = useElementSize(searchRef) const { width } = useElementSize(searchRef)
const urlParams = useUrlSearchParams<UrlParam>('hash')
// //
const expand = ref(false) const expand = ref(false)
// //
const termType = ref('or') const termType = ref('and')
// //
const historyList = ref([]) const historyList = ref([])
@ -120,7 +115,13 @@ const layout = ref('horizontal')
// true 1000 // true 1000
const screenSize = ref(true) const screenSize = ref(true)
const searchItems = ref<SearchItemProps[]>([]) const searchItems = ref<SearchProps[]>([])
//
const terms = reactive<Terms>({ terms: [] })
const columnOptionMap = new Map()
const emit = defineEmits<Emit>()
const expandChange = () => { const expandChange = () => {
expand.value = !expand.value expand.value = !expand.value
@ -132,10 +133,12 @@ const searchParams = reactive({
const handleItems = () => { const handleItems = () => {
searchItems.value = [] searchItems.value = []
columnOptionMap.clear()
props.columns!.forEach((item, index) => { props.columns!.forEach((item, index) => {
if (item.search && Object.keys(item.search).length) { if (item.search && Object.keys(item.search).length) {
columnOptionMap.set(item.dataIndex, item.search)
searchItems.value.push({ searchItems.value.push({
...omit(item.search, ['rename']), ...item.search,
sortIndex: item.search.first ? 0 : index + 1, sortIndex: item.search.first ? 0 : index + 1,
title: item.title, title: item.title,
column: item.dataIndex, column: item.dataIndex,
@ -144,6 +147,92 @@ const handleItems = () => {
}) })
} }
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())
if (props.type === 'advanced') {
addUrlParams()
}
}
/**
* 重置查询
*/
const reset = () => {
terms.terms = []
expand.value = false
if (props.type === 'advanced') {
urlParams.q = null
urlParams.target = null
}
}
watch(width, (value) => { watch(width, (value) => {
if (value < 1000) { if (value < 1000) {
layout.value = 'vertical' layout.value = 'vertical'
@ -154,6 +243,41 @@ watch(width, (value) => {
} }
}) })
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() handleItems()
</script> </script>
@ -253,4 +377,5 @@ handleItems()
} }
} }
} }
</style> </style>

View File

@ -0,0 +1,107 @@
# Search组件
- 需要结合Table使用
## 属性
| 名称 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| columns | 查询下拉列表 | JColumnsProps[] | [] |
| type | 查询模式 | 'advanced', 'simple' | 'advanced' |
| target | 查询组件唯一key | String | |
| search | 查询回调事件 | Function | |
> JColumnsProps[*].search
| 名称 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| rename | 用来重命名查询字段值 | String | |
| type | 查询值组件类型 | 'select', 'number', 'string', 'treeSelect', 'date', 'time' | |
| options | Select和TreeSelect组件下拉值 | Array, Promise | |
| first | 控制查询字段下拉默认值默认为name即名称 | Boolean | |
| defaultTermType | 查询条件 | String | |
| handleValue | 处理单个查询value值 | Function | |
## 基础用法
> columns中包含search属性才会出现在查询下拉中
```vue
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
}
}
]
const search = (params) => {
}
<Search
:columns='columns'
target='device'
@search='search'
/>
```
> rename的作用在于search抛出params会根据rename修改数据中column的值
```vue
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
rename: 'TestName'
}
}
]
const search = (params) => {
terms: [
{
column: 'TestName',
value: '',
termType: 'like'
}
]
}
<Search
:columns='columns'
target='device'
@search='search'
/>
```
> defaultTermType的作用在于设置查询条件,相关条件参考util中的termType
```vue
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
defaultTermType: 'gt'
}
}
]
const search = (params) => {
terms: [
{
column: 'TestName',
value: '',
termType: 'gt'
}
]
}
<Search
:columns='columns'
target='device'
@search='search'
/>
```

View File

@ -1,4 +1,4 @@
export interface SearchProps { export interface SearchBaseProps {
rename?: string rename?: string
type?: 'select' | 'number' | 'string' | 'treeSelect' | 'date' | 'time' type?: 'select' | 'number' | 'string' | 'treeSelect' | 'date' | 'time'
format?: string format?: string
@ -7,15 +7,43 @@ export interface SearchProps {
defaultTermType?: string // 默认 eq defaultTermType?: string // 默认 eq
title?: ColumnType.title title?: ColumnType.title
sortIndex?: number sortIndex?: number
handleValue?: (value: SearchItemData) => any
}
export interface SearchItemProps {
rename?: SearchBaseProps['rename']
title: string
column: ColumnType.dataIndex
} }
export interface SearchItemData { export interface SearchItemData {
column: ColumnType.dataIndex column: ColumnType.dataIndex
rename?: string
value: any value: any
termType: string termType: string
type?: string type?: string
title: string
} }
export interface SearchItemProps extends SearchProps, SearchItemData {} export interface TermsItem {
terms: SearchItemData[]
}
export interface Terms {
terms: TermsItem[]
}
export interface SortItem {
name: string
order?: 'desc' | 'asc'
value?: any
}
export interface SearchHistoryList {
content?: string
name: string
id: string
key: string
}
export interface SearchProps extends SearchBaseProps, SearchItemProps {
}

View File

@ -204,9 +204,13 @@ const JTable = defineComponent<JTableProps>({
loading.value = false loading.value = false
} }
watchEffect(() => { watch(
handleSearch(props.params) () => props.params,
}) (newValue) => {
handleSearch(newValue)
},
{deep: true, immediate: true}
)
onMounted(() => { onMounted(() => {
window.onresize = () => { window.onresize = () => {
@ -266,7 +270,7 @@ const JTable = defineComponent<JTableProps>({
onClose={() => { onClose={() => {
emit('cancelSelect') emit('cancelSelect')
}} }}
closeText={<a></a>} closeText={<a-button type="link"></a-button>}
/> />
</div> : null </div> : null
} }

View File

@ -1,7 +1,7 @@
import { SearchItemProps } from 'components/Search/types' import { SearchProps } from 'components/Search/types'
import { ColumnType } from 'ant-design-vue/es/table' import { ColumnType } from 'ant-design-vue/es/table'
export interface JColumnsProps extends ColumnType{ export interface JColumnsProps extends ColumnType{
scopedSlots?: boolean; scopedSlots?: boolean;
search: SearchItemProps search: SearchProps
} }

View File

@ -45,7 +45,7 @@
<template #addonAfter> <template #addonAfter>
<a-upload <a-upload
name="file" name="file"
:action="action" :action="FILE_UPLOAD"
:headers="headers" :headers="headers"
:showUploadList="false" :showUploadList="false"
@change="handleFileChange" @change="handleFileChange"
@ -89,6 +89,7 @@ import GeoComponent from '@/components/GeoComponent/index.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable'; import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm'; import { LocalStore } from '@/utils/comm';
import { ItemData, ITypes } from './types'; import { ItemData, ITypes } from './types';
import { FILE_UPLOAD } from '@/api/comm';
type Emits = { type Emits = {
(e: 'update:modelValue', data: string | number | boolean): void; (e: 'update:modelValue', data: string | number | boolean): void;
@ -161,7 +162,6 @@ const handleItemModalSubmit = () => {
}; };
// //
const action = ref<string>(`${BASE_API_PATH}/file/static`);
const headers = ref({ [TOKEN_KEY]: LocalStore.get(TOKEN_KEY) }); const headers = ref({ [TOKEN_KEY]: LocalStore.get(TOKEN_KEY) });
const handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => { const handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'done') { if (info.file.status === 'done') {

View File

@ -1,4 +1,5 @@
import { TOKEN_KEY } from '@/utils/variable' import { TOKEN_KEY } from '@/utils/variable'
import { Terms } from 'components/Search/types'
/** /**
* *
@ -35,4 +36,24 @@ export const LocalStore = {
export const getToken = () => { export const getToken = () => {
return LocalStore.get(TOKEN_KEY) return LocalStore.get(TOKEN_KEY)
} }
/**
* TreeSelect过滤
* @param value
* @param treeNode
* @param key
*/
export const filterTreeSelectNode = (value: string, treeNode: any, key: string = 'name'): boolean => {
return treeNode[key]?.includes(value)
}
/**
* Select过滤
* @param value
* @param option
* @param key
*/
export const filterSelectNode = (value: string, option: any, key: string = 'label'): boolean => {
return option[key]?.includes(value)
}

64
src/utils/encodeQuery.ts Normal file
View File

@ -0,0 +1,64 @@
export default function encodeQuery(params: any) {
if (!params) return {};
const queryParam = {
// pageIndex: 0,
current: params.current,
};
const { terms, sorts } = params;
Object.keys(params).forEach((key: string) => {
if (key === 'terms') {
let index = 0;
if (!terms) return;
Object.keys(terms).forEach((k: string) => {
if (
!(
terms[k] === '' ||
terms[k] === undefined ||
terms[k].length === 0 ||
terms[k] === {} ||
terms[k] === null
)
) {
if (k.indexOf('$LIKE') > -1 && terms[k].toString().indexOf('%') === -1) {
terms[k] = `%${terms[k]}%`;
}
if (k.indexOf('$IN') > -1) {
terms[k] = terms[k].toString();
} else if (k.indexOf('$START') > -1) {
terms[k] = `%${terms[k]}`;
} else if (k.indexOf('$END') > -1) {
terms[k] = `${terms[k]}%`;
}
if (k.indexOf('@') > -1) {
const temp = k.split('@');
// eslint-disable-next-line prefer-destructuring
queryParam[`terms[${index}].column`] = temp[0];
// eslint-disable-next-line prefer-destructuring
queryParam[`terms[${index}].type`] = temp[1];
} else {
queryParam[`terms[${index}].column`] = k;
}
queryParam[`terms[${index}].value`] = terms[k];
index += 1;
}
});
} else if (key === 'sorts') {
// 当前Ant Design排序只支持单字段排序
if (!sorts) return;
Object.keys(sorts).forEach((s, index) => {
queryParam[`sorts[${index}].name`] = s;
queryParam[`sorts[${index}].order`] = sorts[s].replace('end', '');
});
// if (Object.keys(sorts).length > 0) {
// queryParam[`sorts[0].name`] = sorts.field;
// queryParam[`sorts[0].order`] = (sorts.order || '').replace('end', '');
// }
} else {
queryParam[key] = params[key];
}
});
// queryParam.pageIndex = current - 1;
return queryParam;
}

View File

@ -1,3 +1,7 @@
import moment from "moment";
import { LocalStore } from "./comm";
import { TOKEN_KEY } from "./variable";
/** /**
* JSON * JSON
* @param record * @param record
@ -18,4 +22,34 @@ export const downloadObject = (record: Record<string, any>, fileName: string, fo
ghostLink.click(); ghostLink.click();
//移除 //移除
document.body.removeChild(ghostLink); document.body.removeChild(ghostLink);
};
/**
*
* @param url
* @param params
*/
export const downloadFile = (url: string, params?: Record<string, any>) => {
const formElement = document.createElement('form');
formElement.style.display = 'display:none;';
formElement.method = 'GET';
formElement.action = url;
// 添加参数
if (params) {
Object.keys(params).forEach((key: string) => {
const inputElement = document.createElement('input');
inputElement.type = 'hidden';
inputElement.name = key;
inputElement.value = params[key];
formElement.appendChild(inputElement);
});
}
const inputElement = document.createElement('input');
inputElement.type = 'hidden';
inputElement.name = ':X_Access_Token';
inputElement.value = LocalStore.get(TOKEN_KEY);
formElement.appendChild(inputElement);
document.body.appendChild(formElement);
formElement.submit();
document.body.removeChild(formElement);
}; };

View File

@ -2,13 +2,22 @@
<div class='search'> <div class='search'>
<Search <Search
:columns='columns' :columns='columns'
target='device-instance-search'
@search='search'
/>
<Search
type='simple'
:columns='columns'
target='product'
@search='search'
/> />
<Search type='simple' />
</div> </div>
</template> </template>
<script setup name='demoSearch'> <script setup name='demoSearch'>
import { category } from '../../api/device/product'
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@ -17,20 +26,59 @@ const columns = [
search: { search: {
rename: 'deviceId', rename: 'deviceId',
type: 'select', type: 'select',
handValue: (v) => { options: [
{
label: '测试1',
value: 'test1'
},
{
label: '测试2',
value: 'test2'
},
{
label: '测试3',
value: 'test3'
},
],
handleValue: (v) => {
return '123' return '123'
} }
} }
}, },
{
title: '序号',
dataIndex: 'sortIndex',
key: 'sortIndex',
scopedSlots: true,
search: {
type: 'number',
}
},
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
key: 'id', key: 'id',
scopedSlots: true,
search: { search: {
type: 'string', type: 'string',
} }
}, },
{
title: '时间',
dataIndex: 'date',
key: 'date',
search: {
type: 'date',
}
},
{
title: '时间2',
dataIndex: 'date2',
key: 'date2',
search: {
type: 'time',
defaultTermType: 'lt'
}
},
{ {
title: '分类', title: '分类',
dataIndex: 'classifiedName', dataIndex: 'classifiedName',
@ -38,9 +86,13 @@ const columns = [
search: { search: {
first: true, first: true,
type: 'treeSelect', type: 'treeSelect',
// options: async () => { options: async () => {
// return await return new Promise((res) => {
// } category().then(resp => {
res(resp.result)
})
})
}
} }
}, },
{ {
@ -51,6 +103,9 @@ const columns = [
scopedSlots: true, scopedSlots: true,
} }
] ]
const search = (params) => {
console.log(params)
}
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,75 @@
<template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="导出" @ok="handleOk" @cancel="handleCancel">
<div style="background-color: rgb(236, 237, 238)">
<p style="padding: 10px">
<AIcon type="ExclamationCircleOutlined" />
选择单个产品时可导出其下属设备的详细数据,不选择产品时导出所有设备的基础数据
</p>
</div>
<div style="margin-top: 20px">
<a-form :layout="'vertical'">
<a-form-item label="产品">
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="文件格式">
<a-radio-group button-style="solid" v-model:value="modelRef.fileType" placeholder="请选择文件格式">
<a-radio-button value="xlsx">xlsx</a-radio-button>
<a-radio-button value="csv">csv</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { queryNoPagingPost } from '@/api/device/product'
import { downloadFile } from '@/utils/utils'
import encodeQuery from '@/utils/encodeQuery'
import { BASE_API_PATH } from '@/utils/variable'
const emit = defineEmits(['close'])
const props = defineProps({
data: {
type: Object,
default: undefined
}
})
const modelRef = reactive({
product: undefined,
fileType: 'xlsx'
});
const productList = ref<Record<string, any>[]>([])
watch(
() => props.data,
() => {
queryNoPagingPost({paging: false}).then(resp => {
if(resp.status === 200){
productList.value = resp.result as Record<string, any>[]
}
})
},
{immediate: true, deep: true}
)
const handleOk = () => {
const params = encodeQuery(props.data);
if(modelRef.product){
downloadFile(
`${BASE_API_PATH}/device/instance/${modelRef.product}/export.${modelRef.fileType}`,
params
);
} else {
downloadFile(`${BASE_API_PATH}/device/instance/export.${modelRef.fileType}`, params);
}
emit('close')
}
const handleCancel = () => {
emit('close')
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleOk" @cancel="handleCancel">
123
</a-modal>
</template>
<script lang="ts" setup>
const handleOk = () => {
}
const handleCancel = () => {
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<a-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleOk" @cancel="handleCancel">
<div>
<a-badge v-if="flag" status="processing" text="进行中" />
<a-badge v-else status="success" text="已完成" />
</div>
<p>总数量{{count}}</p>
<a></a>
</a-modal>
</template>
<script lang="ts" setup>
import { downloadFile } from '@/utils/utils'
const emit = defineEmits(['close'])
const props = defineProps({
api: {
type: String,
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 handleOk = () => {
emit('close')
}
const handleCancel = () => {
emit('close')
}
const getData = () => {
}
watch(() => props.api,
() => {
getData()
},
{deep: true, immediate: true}
)
</script>

View File

@ -1,5 +1,16 @@
<template> <template>
<JTable ref="instanceRef" :columns="columns" :request="query" :defaultParams="{sorts: [{name: 'createTime', order: 'desc'}]}" :params="{pageIndex: 0, pageSize: 12}"> <JTable
ref="instanceRef"
:columns="columns"
:request="query"
:defaultParams="{sorts: [{name: 'createTime', order: 'desc'}]}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange
}"
@cancelSelect="cancelSelect"
:params="params"
>
<template #headerTitle> <template #headerTitle>
<a-space> <a-space>
<a-button type="primary" @click="handleAdd">新增</a-button> <a-button type="primary" @click="handleAdd">新增</a-button>
@ -8,13 +19,33 @@
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
<a href="javascript:;">1st menu item</a> <a-button @click="exportVisible = true"><AIcon type="ExportOutlined" />批量导出设备</a-button>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<a href="javascript:;">2nd menu item</a> <a-button @click="importVisible = true"><AIcon type="ImportOutlined" />批量导入设备</a-button>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<a href="javascript:;">3rd menu item</a> <a-popconfirm @confirm="activeAllDevice" title="确认激活全部设备?">
<a-button type="primary" ghost><AIcon type="CheckCircleOutlined" />激活全部设备</a-button>
</a-popconfirm>
</a-menu-item>
<a-menu-item>
<a-button @click="syncDeviceStatus" type="primary"><AIcon type="SyncOutlined" />同步设备状态</a-button>
</a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length">
<a-popconfirm @confirm="delSelectedDevice" title="已启用的设备无法删除,确认删除选中的禁用状态设备?">
<a-button type="primary" danger><AIcon type="DeleteOutlined" />删除选中设备</a-button>
</a-popconfirm>
</a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length" title="确认激活选中设备?">
<a-popconfirm @confirm="activeSelectedDevice" >
<a-button type="primary"><AIcon type="CheckOutlined" />激活选中设备</a-button>
</a-popconfirm>
</a-menu-item>
<a-menu-item v-if="_selectedRowKeys.length">
<a-popconfirm @confirm="disabledSelectedDevice" title="确认禁用选中设备?">
<a-button type="primary" danger><AIcon type="StopOutlined" />禁用选中设备</a-button>
</a-popconfirm>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</template> </template>
@ -24,9 +55,10 @@
<template #card="slotProps"> <template #card="slotProps">
<CardBox <CardBox
:value="slotProps" :value="slotProps"
@click="handleView" @click="handleClick"
:actions="getActions(slotProps, 'card')" :actions="getActions(slotProps, 'card')"
v-bind="slotProps" v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state.value" :status="slotProps.state.value"
:statusText="slotProps.state.text" :statusText="slotProps.state.text"
:statusNames="{ :statusNames="{
@ -41,7 +73,7 @@
</slot> </slot>
</template> </template>
<template #content> <template #content>
<h3>{{ slotProps.name }}</h3> <h3 @click="handleView(slotProps.id)">{{ slotProps.name }}</h3>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text">设备类型</div> <div class="card-item-content-text">设备类型</div>
@ -116,15 +148,30 @@
</a-space> </a-space>
</template> </template>
</JTable> </JTable>
<Import v-if="importVisible" @close="importVisible = false" />
<Export v-if="exportVisible" @close="exportVisible = false" :data="params" />
<Process v-if="operationVisible" @close="operationVisible = false" :api="api" :type="type" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { query, _delete, _deploy, _undeploy } from '@/api/device/instance' import { query, _delete, _deploy, _undeploy, batchUndeployDevice, batchDeployDevice, batchDeleteDevice } from '@/api/device/instance'
import type { ActionsType } from '@/components/Table/index.vue' import type { ActionsType } from '@/components/Table/index.vue'
import { getImage } from '@/utils/comm'; import { getImage, LocalStore } from '@/utils/comm';
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import Import from './Import/index.vue'
import Export from './Export/index.vue'
import Process from './Process/index.vue'
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
const instanceRef = ref<Record<string, any>>({}); const instanceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({pageIndex: 0, pageSize: 12})
const _selectedRowKeys = ref<string[]>([])
const importVisible = ref<boolean>(false)
const exportVisible = ref<boolean>(false)
const current = ref<Record<string, any>>({})
const operationVisible = ref<boolean>(false)
const api = ref<string>('')
const type = ref<string>('')
const statusMap = new Map(); const statusMap = new Map();
statusMap.set('online', 'processing'); statusMap.set('online', 'processing');
@ -173,12 +220,45 @@ const columns = [
} }
] ]
const paramsFormat = (config: any, _terms: any, name?: string) => {
if (config?.terms && Array.isArray(config.terms) && config?.terms.length > 0) {
(config?.terms || []).map((item: any, index: number) => {
if (item?.type) {
_terms[`${name ? `${name}.` : ''}terms[${index}].type`] = item.type;
}
paramsFormat(item, _terms, `${name ? `${name}.` : ''}terms[${index}]`);
});
} else if (!config?.terms && Object.keys(config).length > 0) {
Object.keys(config).forEach((key) => {
if (config[key]) {
_terms[`${name ? `${name}.` : ''}${key}`] = config[key];
}
});
}
}
const handleParams = (config: any) => {
const _terms: any = {};
paramsFormat(config, _terms);
const url = new URLSearchParams();
Object.keys(_terms).forEach((key) => {
url.append(key, _terms[key]);
});
return url.toString();
}
/**
* 新增
*/
const handleAdd = () => { const handleAdd = () => {
message.warn('123') message.warn('新增')
} }
/**
* 查看
*/
const handleView = (dt: any) => { const handleView = (dt: any) => {
// message.warn('')
} }
const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => { const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => {
@ -257,4 +337,61 @@ const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'):
return actions return actions
} }
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys]
}
const cancelSelect = () => {
_selectedRowKeys.value = []
}
const handleClick = (dt: any) => {
if(_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex(i => i === dt.id)
_selectedRowKeys.value.splice(_index, 1)
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id]
}
}
const activeAllDevice = () => {
type.value = 'active'
const activeAPI = `/${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
api.value = activeAPI
operationVisible.value = true
}
const syncDeviceStatus = () => {
type.value = 'sync'
const syncAPI = `/${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
api.value = syncAPI
operationVisible.value = true
}
const delSelectedDevice = async () => {
const resp = await batchDeleteDevice(_selectedRowKeys.value)
if(resp.status === 200){
message.success('操作成功!')
_selectedRowKeys.value = []
instanceRef.value?.reload()
}
}
const activeSelectedDevice = async () => {
const resp = await batchDeployDevice(_selectedRowKeys.value)
if(resp.status === 200){
message.success('操作成功!')
_selectedRowKeys.value = []
instanceRef.value?.reload()
}
}
const disabledSelectedDevice = async () => {
const resp = await batchUndeployDevice(_selectedRowKeys.value)
if(resp.status === 200){
message.success('操作成功!')
_selectedRowKeys.value = []
instanceRef.value?.reload()
}
}
</script> </script>

View File

@ -313,7 +313,6 @@ const formData = ref<ConfigFormData>({
configuration: { configuration: {
appKey: '', appKey: '',
appSecret: '', appSecret: '',
url: '',
}, },
description: '', description: '',
name: '', name: '',
@ -325,13 +324,20 @@ const formData = ref<ConfigFormData>({
watch( watch(
() => formData.value.type, () => formData.value.type,
(val) => { (val) => {
formData.value.configuration = CONFIG_FIELD_MAP[val]; // formData.value.configuration = Object.values<any>(CONFIG_FIELD_MAP[val])[0];
msgType.value = MSG_TYPE[val]; msgType.value = MSG_TYPE[val];
formData.value.provider = msgType.value[0].value; formData.value.provider = msgType.value[0].value;
}, },
); );
computed(() =>
Object.assign(
formData.value.configuration,
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider],
),
);
// //
const formRules = ref({ const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }], type: [{ required: true, message: '请选择通知方式' }],

View File

@ -3,34 +3,62 @@ export interface IHeaders {
key: string; key: string;
value: string; value: string;
} }
export interface IConfiguration {
// 钉钉
appKey?: string;
appSecret?: string;
url?: string;
// 微信
corpId?: string;
corpSecret?: string;
// 邮件
host?: string;
port?: number;
ssl?: boolean;
sender?: string;
username?: string;
password?: string;
// 语音
regionId?: string;
accessKeyId?: string;
secret?: string;
// 短信
regionId?: string;
accessKeyId?: string;
secret?: string;
// webhook
// url?: string;
headers?: IHeaders[];
}
export type ConfigFormData = { export type ConfigFormData = {
configuration: { configuration: IConfiguration;
// 钉钉 // configuration: {
appKey?: string; // // 钉钉
appSecret?: string; // appKey?: string;
url?: string; // appSecret?: string;
// 微信 // url?: string;
corpId?: string; // // 微信
corpSecret?: string; // corpId?: string;
// 邮件 // corpSecret?: string;
host?: string; // // 邮件
port?: number; // host?: string;
ssl?: boolean; // port?: number;
sender?: string; // ssl?: boolean;
username?: string; // sender?: string;
password?: string; // username?: string;
// 语音 // password?: string;
regionId?: string; // // 语音
accessKeyId?: string; // regionId?: string;
secret?: string; // accessKeyId?: string;
// 短信 // secret?: string;
regionId?: string; // // 短信
accessKeyId?: string; // regionId?: string;
secret?: string; // accessKeyId?: string;
// webhook // secret?: string;
// url?: string; // // webhook
headers?: IHeaders[]; // // url?: string;
}; // headers?: IHeaders[];
// };
description: string; description: string;
name: string; name: string;
provider: string; provider: string;

View File

@ -0,0 +1,105 @@
<!-- webhook请求头可编辑表格 -->
<template>
<div class="attachment-wrapper">
<div
class="attachment-item"
v-for="(item, index) in fileList"
:key="index"
>
<a-input v-model:value="item.name">
<template #addonAfter>
<a-upload
name="file"
:action="FILE_UPLOAD"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:showUploadList="false"
@change="handleChange"
>
<upload-outlined />
</a-upload>
</template>
</a-input>
<delete-outlined @click="handleDelete" style="cursor: pointer" />
</div>
<a-button
type="dashed"
@click="handleAdd"
style="width: 100%; margin-top: 5px"
>
<template #icon>
<plus-outlined />
</template>
添加
</a-button>
</div>
</template>
<script setup lang="ts" name="Attachments">
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
} from '@ant-design/icons-vue';
import { PropType } from 'vue';
import { IAttachments } from '../../types';
import { FILE_UPLOAD } from '@/api/comm';
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { UploadChangeParam } from 'ant-design-vue';
type Emits = {
(e: 'update:attachments', data: IAttachments[]): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
attachments: {
type: Array as PropType<IAttachments[]>,
default: () => [],
},
});
const handleChange = (info: UploadChangeParam) => {
if (info.file.status === 'done') {
const result = info.file.response?.result;
console.log('result: ', result);
}
};
const fileList = ref<IAttachments[]>([]);
watch(
() => props.attachments,
(val) => {
fileList.value = val;
},
{ deep: true },
);
const handleDelete = (id: number) => {
const idx = fileList.value.findIndex((f) => f.id === id);
fileList.value.splice(idx, 1);
emit('update:attachments', fileList.value);
};
const handleAdd = () => {
fileList.value.push({
id: fileList.value.length,
name: '',
location: '',
});
emit('update:attachments', fileList.value);
};
</script>
<style lang="less" scoped>
.attachment-wrapper {
.attachment-item {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
}
</style>

View File

@ -31,40 +31,46 @@
<a-form-item <a-form-item
label="类型" label="类型"
v-bind="validateInfos.provider" v-bind="validateInfos.provider"
v-if="formData.type !== 'email'" v-if="
formData.type !== 'email' &&
formData.type !== 'webhook'
"
> >
<RadioCard <RadioCard
:options="msgType" :options="msgType"
v-model="formData.provider" v-model="formData.provider"
/> />
</a-form-item> </a-form-item>
<a-form-item
label="绑定配置"
v-bind="validateInfos.configId"
v-if="formData.type !== 'email'"
>
<a-select
v-model:value="formData.configId"
placeholder="请选择绑定配置"
>
<a-select-option
v-for="(item, index) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 钉钉 --> <!-- 钉钉 -->
<template v-if="formData.type === 'dingTalk'"> <template v-if="formData.type === 'dingTalk'">
<template <template
v-if="formData.provider === 'dingTalkMessage'" v-if="formData.provider === 'dingTalkMessage'"
> >
<a-form-item <a-form-item
label="AppKey" label="AgentId"
v-bind=" v-bind="validateInfos['template.agentId']"
validateInfos['configuration.appKey']
"
> >
<a-input <a-input
v-model:value=" v-model:value="
formData.configuration.appKey formData.template.agentId
"
placeholder="请输入AppKey"
/>
</a-form-item>
<a-form-item
label="AppSecret"
v-bind="
validateInfos['configuration.appSecret']
"
>
<a-input
v-model:value="
formData.configuration.appSecret
" "
placeholder="请输入AppSecret" placeholder="请输入AppSecret"
/> />
@ -76,155 +82,340 @@
" "
> >
<a-form-item <a-form-item
label="webHook" label="消息类型"
v-bind="validateInfos['configuration.url']" v-bind="
validateInfos['template.messageType']
"
> >
<a-input <a-select
v-model:value=" v-model:value="
formData.configuration.url formData.template.messageType
" "
placeholder="请输入webHook" placeholder="请选择消息类型"
/> >
<a-select-option
v-for="(
item, index
) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<template
v-if="
formData.template.messageType ===
'markdown'
"
>
<a-form-item
label="标题"
v-bind="
validateInfos[
'template.markdown.title'
]
"
>
<!-- <a-input
v-model:value="
formData.template.markdown
?.title
"
placeholder="请输入标题"
/> -->
</a-form-item>
</template>
<!-- <template
v-if="
formData.template.messageType === 'link'
"
>
<a-form-item
label="标题"
v-bind="
validateInfos['template.link.title']
"
>
<a-input
v-model:value="
formData.template.link?.title
"
placeholder="请输入标题"
/>
</a-form-item>
<a-form-item label="图片链接">
<a-input
v-model:value="
formData.template.link?.picUrl
"
placeholder="请输入图片链接"
/>
</a-form-item>
<a-form-item label="内容链接">
<a-input
v-model:value="
formData.template.link
?.messageUrl
"
placeholder="请输入内容链接"
/>
</a-form-item>
</template> -->
</template> </template>
</template> </template>
<!-- 微信 --> <!-- 微信 -->
<template v-if="formData.type === 'weixin'"> <template v-if="formData.type === 'weixin'">
<a-form-item <a-form-item
label="corpId" label="AgentId"
v-bind="validateInfos['configuration.corpId']" v-bind="validateInfos['template.agentId']"
> >
<a-input <a-input
v-model:value=" v-model:value="formData.template.agentId"
formData.configuration.corpId placeholder="请输入agentId"
"
placeholder="请输入corpId"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-row :gutter="10">
label="corpSecret" <a-col :span="12">
v-bind=" <a-form-item label="收信人">
validateInfos['configuration.corpSecret'] <a-select
" v-model:value="
> formData.template.toUser
<a-input "
v-model:value=" placeholder="请选择收信人"
formData.configuration.corpSecret >
" <a-select-option
placeholder="请输入corpSecret" v-for="(
/> item, index
) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="收信部门">
<a-select
v-model:value="
formData.template.toParty
"
placeholder="请选择收信部门"
>
<a-select-option
v-for="(
item, index
) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="标签推送">
<a-select
v-model:value="formData.template.toTag"
placeholder="请选择标签推送"
>
<a-select-option
v-for="(item, index) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item> </a-form-item>
</template> </template>
<!-- 邮件 --> <!-- 邮件 -->
<template v-if="formData.type === 'email'"> <template v-if="formData.type === 'email'">
<a-form-item <a-form-item
label="服务器地址" label="标题"
v-bind="validateInfos['configuration.host']" v-bind="validateInfos['template.subject']"
> >
<a-space> <a-input
<a-input v-model:value="formData.template.subject"
v-model:value=" placeholder="请输入标题"
formData.configuration.host />
" </a-form-item>
placeholder="请输入服务器地址" <a-form-item label="收件人">
/> <a-select
<a-input-number v-model:value="formData.template.sendTo"
v-model:value=" placeholder="请选择收件人"
formData.configuration.port >
" <a-select-option
/> v-for="(item, index) in ROBOT_MSG_TYPE"
<a-checkbox :key="index"
v-model:value=" :value="item.value"
formData.configuration.ssl
"
> >
开启SSL {{ item.label }}
</a-checkbox> </a-select-option>
</a-space> </a-select>
</a-form-item> </a-form-item>
<a-form-item <a-form-item label="附件信息">
label="发件人" <Attachments
v-bind="validateInfos['configuration.sender']" v-model:attachments="
> formData.template.attachments
<a-input
v-model:value="
formData.configuration.sender
" "
placeholder="请输入发件人"
/>
</a-form-item>
<a-form-item
label="用户名"
v-bind="validateInfos['configuration.username']"
>
<a-input
v-model:value="
formData.configuration.username
"
placeholder="请输入用户名"
/>
</a-form-item>
<a-form-item
label="密码"
v-bind="validateInfos['configuration.password']"
>
<a-input
v-model:value="
formData.configuration.password
"
placeholder="请输入密码"
/> />
</a-form-item> </a-form-item>
</template> </template>
<!-- 语音/短信 --> <!-- 语音 -->
<template <template v-if="formData.type === 'voice'">
v-if="
formData.type === 'voice' ||
formData.type === 'sms'
"
>
<a-form-item <a-form-item
label="AccessKeyId" label="类型"
v-bind=" v-bind="validateInfos['template.templateType']"
validateInfos['configuration.accessKeyId']
"
> >
<a-select
v-model:value="
formData.template.templateType
"
placeholder="请选择类型"
>
<a-select-option
v-for="(item, index) in VOICE_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="10">
<a-col :span="12">
<a-form-item
label="模板ID"
v-bind="
validateInfos[
'template.templateCode'
]
"
>
<a-input
v-model:value="
formData.template.templateCode
"
placeholder="请输入模板ID"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="被叫号码">
<a-input
v-model:value="
formData.template.calledNumber
"
placeholder="请输入被叫号码"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="被叫显号">
<a-input <a-input
v-model:value=" v-model:value="
formData.configuration.accessKeyId formData.template.calledShowNumbers
" "
placeholder="请输入AccessKeyId" placeholder="请输入被叫显号"
/>
</a-form-item>
<a-form-item label="播放次数">
<a-input
v-model:value="formData.template.playTimes"
placeholder="请输入播放次数"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="Secret" label="模板内容"
v-bind="validateInfos['configuration.secret']" v-if="formData.template.templateType === 'tts'"
>
<a-textarea
v-model:value="formData.template.ttsCode"
show-count
:rows="5"
placeholder="内容中的变量将用于阿里云语音验证码"
/>
</a-form-item>
</template>
<!-- 短信 -->
<template v-if="formData.type === 'sms'">
<a-row :gutter="10">
<a-col :span="12">
<a-form-item
label="模板"
v-bind="validateInfos['template.code']"
>
<a-select
v-model:value="
formData.template.code
"
placeholder="请选择模板"
>
<a-select-option
v-for="(
item, index
) in ROBOT_MSG_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="收信人">
<a-input
v-model:value="
formData.template.phoneNumber
"
placeholder="请输入收信人"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="签名"
v-bind="validateInfos['template.signName']"
> >
<a-input <a-input
v-model:value=" v-model:value="formData.template.signName"
formData.configuration.secret placeholder="请输入签名"
"
placeholder="Secret"
/> />
</a-form-item> </a-form-item>
</template> </template>
<!-- webhook --> <!-- webhook -->
<template v-if="formData.type === 'webhook'"> <template v-if="formData.type === 'webhook'">
<a-form-item <a-form-item label="请求体">
label="Webhook" <a-radio-group
v-bind="validateInfos['configuration.url']" v-model:value="
> formData.template.contextAsBody
<a-input
v-model:value="formData.configuration.url"
placeholder="请输入Webhook"
/>
</a-form-item>
<a-form-item label="请求头">
<!-- <EditTable
v-model:headers="
formData.configuration.headers
" "
/> --> style="margin-bottom: 20px"
>
<a-radio :value="true">默认</a-radio>
<a-radio :value="false">自定义</a-radio>
</a-radio-group>
<a-textarea
v-model:value="formData.template.body"
placeholder="请求体中的数据来自于发送通知时指定的所有变量"
v-if="formData.template.contextAsBody"
disabled
:rows="5"
/>
<div v-else style="height: 400px">
<MonacoEditor
theme="vs"
v-model:modelValue="
formData.template.body
"
/>
</div>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label="说明"> <a-form-item label="说明">
@ -263,12 +454,15 @@ import { message } from 'ant-design-vue';
import { TemplateFormData } from '../types'; import { TemplateFormData } from '../types';
import { import {
NOTICE_METHOD, NOTICE_METHOD,
CONFIG_FIELD_MAP, TEMPLATE_FIELD_MAP,
MSG_TYPE, MSG_TYPE,
ROBOT_MSG_TYPE,
VOICE_TYPE,
} from '@/views/notice/const'; } from '@/views/notice/const';
// import EditTable from './components/EditTable.vue'; import templateApi from '@/api/notice/template';
import configApi from '@/api/notice/config';
import Doc from './doc/index'; import Doc from './doc/index';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
import Attachments from './components/Attachments.vue'
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -290,30 +484,34 @@ const msgType = ref([
// //
const formData = ref<TemplateFormData>({ const formData = ref<TemplateFormData>({
description: '', template: {},
name: '', name: '',
provider: '', type: 'email',
type: NOTICE_METHOD[2].value, provider: 'embedded',
template: { description: '',
subject: '', variableDefinitions: [],
sendTo: [],
attachments: [],
message: '',
text: '',
},
}); });
// //
watch( watch(
() => formData.value.type, () => formData.value.type,
(val) => { (val) => {
formData.value.configuration = CONFIG_FIELD_MAP[val]; // formData.value.template = TEMPLATE_FIELD_MAP[val];
msgType.value = MSG_TYPE[val]; msgType.value = MSG_TYPE[val];
formData.value.provider = msgType.value[0].value; formData.value.provider = msgType.value[0].value;
console.log('formData.value.template: ', formData.value.template);
}, },
); );
computed(() => {
console.log('formData.value.type: ', formData.value.type);
Object.assign(
formData.value.template,
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider],
);
});
// //
const formRules = ref({ const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }], type: [{ required: true, message: '请选择通知方式' }],
@ -322,58 +520,23 @@ const formRules = ref({
{ max: 64, message: '最多可输入64个字符' }, { max: 64, message: '最多可输入64个字符' },
], ],
provider: [{ required: true, message: '请选择类型' }], provider: [{ required: true, message: '请选择类型' }],
configId: [{ required: true, message: '请选择绑定配置' }],
// //
'configuration.appKey': [ 'template.agentId': [{ required: true, message: '请输入agentId' }],
{ required: true, message: '请输入AppKey' }, 'template.messageType': [{ required: true, message: '请选择消息类型' }],
{ max: 64, message: '最多可输入64个字符' }, 'template.markdown.title': [{ required: true, message: '请输入标题' }],
], // 'template.url': [{ required: true, message: 'WebHook' }],
'configuration.appSecret': [
{ required: true, message: '请输入AppSecret' },
{ max: 64, message: '最多可输入64个字符' },
],
// 'configuration.url': [{ required: true, message: 'WebHook' }],
// //
'configuration.corpId': [ // 'template.agentId': [{ required: true, message: 'agentId' }],
{ required: true, message: '请输入corpId' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.corpSecret': [
{ required: true, message: '请输入corpSecret' },
{ max: 64, message: '最多可输入64个字符' },
],
// /
'configuration.regionId': [
{ required: true, message: '请输入RegionId' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.accessKeyId': [
{ required: true, message: '请输入AccessKeyId' },
{ max: 64, message: '最多可输入64个字符' },
],
'configuration.secret': [
{ required: true, message: '请输入Secret' },
{ max: 64, message: '最多可输入64个字符' },
],
// //
'configuration.host': [{ required: true, message: '请输入服务器地址' }], 'template.subject': [{ required: true, message: '请输入标题' }],
'configuration.sender': [{ required: true, message: '请输入发件人' }], //
'configuration.username': [ 'template.templateType': [{ required: true, message: '请选择类型' }],
{ required: true, message: '请输入用户名' }, 'template.templateCode': [{ required: true, message: '请输入模板ID' }],
{ max: 64, message: '最多可输入64个字符' }, //
], 'template.code': [{ required: true, message: '请选择模板' }],
'configuration.password': [ 'template.signName': [{ required: true, message: '请输入签名' }],
{ required: true, message: '请输入密码' },
{ max: 64, message: '最多可输入64个字符' },
],
// webhook // webhook
'configuration.url': [
{ required: true, message: '请输入Webhook' },
{
pattern:
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
message: 'Webhook需要是一个合法的URL',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }], description: [{ max: 200, message: '最多可输入200个字符' }],
}); });
@ -390,12 +553,12 @@ watch(
); );
const getDetail = async () => { const getDetail = async () => {
const res = await configApi.detail(route.params.id as string); const res = await templateApi.detail(route.params.id as string);
// console.log('res: ', res); // console.log('res: ', res);
formData.value = res.result; formData.value = res.result;
// console.log('formData.value: ', formData.value); // console.log('formData.value: ', formData.value);
}; };
getDetail(); // getDetail();
/** /**
* 表单提交 * 表单提交
@ -404,19 +567,19 @@ const btnLoading = ref<boolean>(false);
const handleSubmit = () => { const handleSubmit = () => {
validate() validate()
.then(async () => { .then(async () => {
// console.log('formData.value: ', formData.value); console.log('formData.value: ', formData.value);
btnLoading.value = true; btnLoading.value = true;
let res; // let res;
if (!formData.value.id) { // if (!formData.value.id) {
res = await configApi.save(formData.value); // res = await templateApi.save(formData.value);
} else { // } else {
res = await configApi.update(formData.value); // res = await templateApi.update(formData.value);
} // }
// console.log('res: ', res); // // console.log('res: ', res);
if (res?.success) { // if (res?.success) {
message.success('保存成功'); // message.success('');
router.back(); // router.back();
} // }
btnLoading.value = false; btnLoading.value = false;
}) })
.catch((err) => { .catch((err) => {

View File

@ -3,6 +3,20 @@
<div class="page-container">通知模板</div> <div class="page-container">通知模板</div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import templateApi from '@/api/notice/template';
const getList = async () => {
const res = await templateApi.list({
current: 1,
pageIndex: 0,
pageSize: 12,
sorts: [{ name: 'createTime', order: 'desc' }],
terms: [],
});
console.log('res: ', res);
};
getList();
</script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -7,6 +7,7 @@ export interface IHeaders {
interface IAttachments { interface IAttachments {
location: string; location: string;
name: string; name: string;
id?: number;
} }
interface IVariableDefinitions { interface IVariableDefinitions {
id: string; id: string;
@ -16,14 +17,6 @@ interface IVariableDefinitions {
} }
export type TemplateFormData = { export type TemplateFormData = {
name: string;
type: string;
provider: string;
description: string;
id?: string;
creatorId?: string;
createTime?: number;
configId?: string;
template: { template: {
// 钉钉消息 // 钉钉消息
agentId?: string; agentId?: string;
@ -41,7 +34,7 @@ export type TemplateFormData = {
text: string; text: string;
}; };
// 微信 // 微信
agentId?: string; // agentId?: string;
// message?: string; // message?: string;
toParty?: string; toParty?: string;
toUser?: string; toUser?: string;
@ -69,6 +62,13 @@ export type TemplateFormData = {
contextAsBody?: boolean; contextAsBody?: boolean;
body?: string; body?: string;
}; };
name: string;
type: string;
provider: string;
description: string;
variableDefinitions: IVariableDefinitions[]; variableDefinitions: IVariableDefinitions[];
id?: string;
creatorId?: string;
createTime?: number;
configId?: string;
}; };

View File

@ -33,7 +33,7 @@ export const NOTICE_METHOD: INoticeMethod[] = [
}, },
]; ];
// 消息类型 // 类型
export const MSG_TYPE = { export const MSG_TYPE = {
dingTalk: [ dingTalk: [
{ {
@ -93,36 +93,52 @@ export const MSG_TYPE = {
// 配置 // 配置
export const CONFIG_FIELD_MAP = { export const CONFIG_FIELD_MAP = {
dingTalk: { dingTalk: {
appKey: undefined, dingTalkMessage: {
appSecret: undefined, appKey: '',
url: undefined, appSecret: '',
},
dingTalkRobotWebHook: {
url: '',
}
}, },
weixin: { weixin: {
corpId: undefined, corpMessage: {
corpSecret: undefined, corpId: '',
corpSecret: '',
},
// officialMessage: {},
}, },
email: { email: {
host: undefined, embedded: {
port: 25, host: '',
ssl: false, port: 25,
sender: undefined, ssl: false,
username: undefined, sender: '',
password: undefined, username: '',
password: '',
}
}, },
voice: { voice: {
regionId: undefined, aliyun: {
accessKeyId: undefined, regionId: '',
secret: undefined, accessKeyId: '',
secret: '',
}
}, },
sms: { sms: {
regionId: undefined, aliyunSms: {
accessKeyId: undefined, regionId: '',
secret: undefined, accessKeyId: '',
secret: '',
}
}, },
webhook: { webhook: {
url: undefined, http: {
headers: [], url: undefined,
headers: [],
}
}, },
}; };
// 模板 // 模板
@ -187,8 +203,20 @@ export const TEMPLATE_FIELD_MAP = {
}, },
webhook: { webhook: {
http: { http: {
contextAsBody: false, contextAsBody: true,
body: '' body: ''
} }
}, },
}; };
// 钉钉机器人-消息类型
export const ROBOT_MSG_TYPE = [
{ label: 'markdown', value: 'markdown' },
{ label: 'text', value: 'text' },
{ label: 'link', value: 'link' },
]
// 语音通知类型
export const VOICE_TYPE = [
{ label: '语音通知', value: 'voice' },
{ label: '语音验证码', value: 'tts' },
]

7952
yarn.lock

File diff suppressed because it is too large Load Diff