fix: bug#24641、24804、25111、22958、24806、25377、25597
* fix: 优化物联卡批量导入数量统计;优化物联卡状态查询列表 * fix: 24641、24804、25111、22958、24806、25377、25597 * feat: 修改区域管理删除逻辑 * fix: bug#24628、25704 * fix: bug#24545 * fix: bug#25414 * fix: bug#24479 * fix: bug#23410 * fix: bug#24404 * fix: bug#25374 * fix: bug#25062 * fix: bug#22409 * fix: bug#25525、26083 * fix: bug#26186 * fix: bug#26233
This commit is contained in:
parent
bc0c4fa55b
commit
251c7b982b
2
build.sh
2
build.sh
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-SNAPSHOT .
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-SNAPSHOT.
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-SNAPSHOT
|
||||
|
|
|
@ -38,8 +38,14 @@ export const detail = (id:string) => server.get(`/alarm/record/${id}`);
|
|||
/**
|
||||
* 告警历史记录
|
||||
*/
|
||||
export const queryHistoryLogList = (alarmConfigId: string, data:any) => server.post(`/alarm/history/${alarmConfigId}/_query`,data);
|
||||
|
||||
/**
|
||||
* 告警日志
|
||||
*/
|
||||
export const queryHistoryList = (data:any) => server.post('/alarm/history/_query',data);
|
||||
|
||||
|
||||
/**
|
||||
* 获取告警处理结果
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
// 获取全部地区(树结构)
|
||||
export const getRegionTree = (): Promise<any> => server.post(`/area/_all/tree`);
|
||||
export const getRegionTree = (data?: any): Promise<any> => server.post(`/area/_all/tree`, data);
|
||||
|
||||
// 校验名称是否存在
|
||||
export const validateName = (name: string, id?: string): Promise<any> => server.get(`/area/name/_validate?name=${name}${id ? `&id=${id}` : ''}`);
|
||||
|
@ -20,3 +20,5 @@ export const updateRegion = (data: any): Promise<any> => server.patch(`/area`, d
|
|||
|
||||
// 获取全部内置地区(树结构)
|
||||
export const getBuiltinRegionTree = (data: any): Promise<any> => server.post(`/area/builtin/_all/tree`, data);
|
||||
|
||||
export const queryAreaCount = (id: string) => server.get(`/ams/asset/area/${id}/_count`)
|
||||
|
|
|
@ -20,6 +20,7 @@ export const getPrimissTree_api = (id: string): Promise<any> => server.get(`/men
|
|||
// 更新角色对应的权限树
|
||||
export const updatePrimissTree_api = (id: string, data:object): Promise<any> => server.put(`/menu/role/${id}/_grant`,data);
|
||||
|
||||
export const clearPrimissTree_api = (id: string, data?:object): Promise<any> => server.put(`/menu/role/${id}/iot/clear-grant`,data);
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserByRole_api = (data: any): Promise<any> => server.post(`/user/_query/`, data);
|
||||
|
|
|
@ -22,7 +22,7 @@ import '@vuemap/vue-amap/dist/style.css';
|
|||
import { getAMapUiPromise } from './utils';
|
||||
import { useSystem } from '@/store/system';
|
||||
|
||||
const emit = defineEmits('init')
|
||||
const emit = defineEmits(['init'])
|
||||
|
||||
const system = useSystem();
|
||||
interface AMapProps {
|
||||
|
@ -33,7 +33,9 @@ interface AMapProps {
|
|||
const amapKey = system.$state.configInfo.amap?.apiKey;
|
||||
|
||||
initAMapApiLoader({
|
||||
key: amapKey || '',
|
||||
key: amapKey || 'c86c1b22c6b223e3ed08815532676445',
|
||||
securityJsCode: 'b0efcf1ce14cbf2d56d3cde630cd19cf',
|
||||
plugins: ['AMap.DistrictSearch'],
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
@ -56,7 +56,7 @@ export const defaultBranches = [
|
|||
then: [],
|
||||
executeAnyway: true,
|
||||
branchId: Math.floor(Math.random() * 100000000),
|
||||
branchName:'条件1'
|
||||
branchName:''
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<template lang="">
|
||||
<template>
|
||||
<j-modal
|
||||
:title="data.id ? '编辑' : '新增'"
|
||||
ok-text="确认"
|
||||
|
@ -9,6 +9,7 @@
|
|||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<a-spin :spinning="_loading">
|
||||
<j-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
|
@ -187,11 +188,11 @@
|
|||
></j-col>
|
||||
</j-row>
|
||||
</j-form>
|
||||
</a-spin>
|
||||
</j-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { message, Form } from 'jetlinks-ui-components';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { Form } from 'jetlinks-ui-components';
|
||||
import FileUpload from './FileUpload.vue';
|
||||
import {
|
||||
save,
|
||||
|
@ -223,6 +224,7 @@ const addList = () => {
|
|||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const _loading = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const productOptions = ref([]);
|
||||
|
||||
|
@ -282,6 +284,19 @@ const validatorVersionOrder = async (_: Record<string, any>, value: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const validatorProductExist = async (_: Record<string, any>, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const dt = productOptions.value.find((i: any) => i.value === value)
|
||||
if(dt){
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('当前产品不存在,请选择产品')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validatorVersionValue = async (_rule:any,value:any) => {
|
||||
return new Promise (async(resolve,reject)=>{
|
||||
const posReg = /^[1-9]\d*$/;
|
||||
|
@ -301,7 +316,10 @@ const { resetFields, validate, validateInfos } = useForm(
|
|||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
productId: [{ required: true, message: '请选择所属产品' }],
|
||||
productId: [
|
||||
{ required: true, message: '请选择所属产品' },
|
||||
{ validator: validatorProductExist, trigger: 'blur' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号' },
|
||||
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
|
||||
|
@ -368,11 +386,13 @@ const changeSignMethod = () => {
|
|||
};
|
||||
|
||||
onMounted(() => {
|
||||
_loading.value = true
|
||||
queryProduct({
|
||||
paging: false,
|
||||
terms: [{ column: 'state', value: 1 }],
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}).then((resp: any) => {
|
||||
_loading.value = false
|
||||
productOptions.value = resp.result.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<template lang="">
|
||||
<template>
|
||||
<j-modal
|
||||
:title="data.id ? '查看' : '新增' + '任务'"
|
||||
ok-text="确认"
|
||||
|
|
|
@ -16,21 +16,31 @@
|
|||
<div style="flex: 1">
|
||||
<j-ellipsis> {{ instanceStore.current?.id }} </j-ellipsis>
|
||||
</div>
|
||||
<div v-if='instanceStore.current?.accessProvider === "plugin_gateway"'>
|
||||
<div
|
||||
v-if="
|
||||
instanceStore.current?.accessProvider ===
|
||||
'plugin_gateway'
|
||||
"
|
||||
>
|
||||
<j-tooltip>
|
||||
<template #title>
|
||||
<p>通过调用SDK或HTTP请求的方式接入第三方系统设备数据时,第三方系统与平台当前设备对应的设备ID。</p>
|
||||
<p>
|
||||
通过调用SDK或HTTP请求的方式接入第三方系统设备数据时,第三方系统与平台当前设备对应的设备ID。
|
||||
</p>
|
||||
如双方ID值一致,则无需填写
|
||||
</template>
|
||||
<a v-if="!inklingDeviceId" type='link' @click='giveAnInkling'>
|
||||
<a
|
||||
v-if="!inklingDeviceId"
|
||||
type="link"
|
||||
@click="giveAnInkling"
|
||||
>
|
||||
未映射
|
||||
</a>
|
||||
<a v-else type='link' @click='inkingVisible = true'>
|
||||
<a v-else type="link" @click="inkingVisible = true">
|
||||
已映射
|
||||
</a>
|
||||
</j-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</j-descriptions-item>
|
||||
<j-descriptions-item label="产品名称">{{
|
||||
|
@ -71,9 +81,7 @@
|
|||
}}</j-descriptions-item>
|
||||
<j-descriptions-item
|
||||
label="父设备"
|
||||
v-if="
|
||||
instanceStore.current?.deviceType?.value === 'childrenDevice'
|
||||
"
|
||||
v-if="instanceStore.current?.deviceType?.value === 'childrenDevice'"
|
||||
>{{ instanceStore.current?.parentId }}</j-descriptions-item
|
||||
>
|
||||
<j-descriptions-item label="说明">{{
|
||||
|
@ -100,12 +108,12 @@
|
|||
@save="saveBtn"
|
||||
/>
|
||||
<InkingModal
|
||||
v-if='inkingVisible'
|
||||
:id='inklingDeviceId'
|
||||
:accessId='instanceStore.current.accessId'
|
||||
v-if="inkingVisible"
|
||||
:id="inklingDeviceId"
|
||||
:accessId="instanceStore.current.accessId"
|
||||
:pluginId="channelId"
|
||||
@cancel="inkingVisible = false"
|
||||
@submit='saveInkling'
|
||||
@submit="saveInkling"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -115,16 +123,16 @@ import Save from '../../Save/index.vue';
|
|||
import Config from './components/Config/index.vue';
|
||||
import Tags from './components/Tags/index.vue';
|
||||
import Relation from './components/Relation/index.vue';
|
||||
import InkingModal from './components/InklingModal'
|
||||
import InkingModal from './components/InklingModal';
|
||||
import moment from 'moment';
|
||||
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig'
|
||||
import { getPluginData } from '@/api/link/plugin'
|
||||
import { detail as queryPluginAccessDetail } from '@/api/link/accessConfig';
|
||||
import { getPluginData } from '@/api/link/plugin';
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const inkingVisible = ref<boolean>(false);
|
||||
const instanceStore = useInstanceStore();
|
||||
const inklingDeviceId = ref()
|
||||
const channelId = ref()
|
||||
const inklingDeviceId = ref();
|
||||
const channelId = ref();
|
||||
|
||||
const saveBtn = () => {
|
||||
if (instanceStore.current?.id) {
|
||||
|
@ -137,33 +145,48 @@ const saveInkling = (id: string) => {
|
|||
if (instanceStore.current?.id) {
|
||||
instanceStore.refresh(instanceStore.current?.id);
|
||||
}
|
||||
channelId.value = id
|
||||
queryInkling()
|
||||
inkingVisible.value = false
|
||||
}
|
||||
channelId.value = id;
|
||||
queryInkling();
|
||||
inkingVisible.value = false;
|
||||
};
|
||||
|
||||
const giveAnInkling = () => {
|
||||
inkingVisible.value = true
|
||||
}
|
||||
inkingVisible.value = true;
|
||||
};
|
||||
|
||||
const queryInkling = () => {
|
||||
if (instanceStore.current?.accessProvider === 'plugin_gateway') {
|
||||
queryPluginAccessDetail(instanceStore.current?.accessId).then(async res => {
|
||||
queryPluginAccessDetail(instanceStore.current?.accessId).then(
|
||||
async (res) => {
|
||||
if (res.success) {
|
||||
channelId.value = res.result.channelId
|
||||
const pluginRes = await getPluginData('device',instanceStore.current?.accessId, instanceStore.current?.id)
|
||||
channelId.value = res.result.channelId;
|
||||
const pluginRes = await getPluginData(
|
||||
'device',
|
||||
instanceStore.current?.accessId,
|
||||
instanceStore.current?.id,
|
||||
);
|
||||
if (pluginRes.success) {
|
||||
inklingDeviceId.value = pluginRes.result?.externalId
|
||||
inklingDeviceId.value = pluginRes.result?.externalId;
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => instanceStore.current?.id, () => {
|
||||
onMounted(() => {
|
||||
// 设备编辑标签后,返回实力信息页面,标签栏没有更新
|
||||
if (instanceStore?.current?.id) {
|
||||
instanceStore.refresh(instanceStore.current.id);
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => instanceStore.current?.id,
|
||||
() => {
|
||||
if (instanceStore.current?.id) {
|
||||
queryInkling()
|
||||
queryInkling();
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -638,6 +638,7 @@ const getGuide = async (isDriver1: boolean = false) => {
|
|||
};
|
||||
|
||||
const checkAccess = async (data: any) => {
|
||||
console.log(data)
|
||||
visible.value = false
|
||||
accessId.value = data.access.id
|
||||
access.value = data.access
|
||||
|
@ -648,6 +649,20 @@ const checkAccess = async (data: any) => {
|
|||
metadata.value = data.metadata?.[0] || {
|
||||
properties: []
|
||||
}
|
||||
if (metadata.value?.properties) {
|
||||
metadata.value?.properties.forEach((item) => {
|
||||
if (
|
||||
item.name === '流传输模式' &&
|
||||
(!productStore.current?.configuration ||
|
||||
!productStore.current?.configuration.hasOwnProperty(
|
||||
item.property,
|
||||
))
|
||||
) {
|
||||
formData.data[item.property] =
|
||||
item.type.expands?.defaultValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (data.access.channel === 'plugin') { // 插件设备
|
||||
markdownToHtml.value = ''
|
||||
productTypes.value = data.productTypes.map(item => ({ ...item, label: item.name, value: item.id}))
|
||||
|
|
|
@ -369,12 +369,12 @@ onMounted(() => {
|
|||
|
||||
onUnmounted(() => {
|
||||
if (_delTag.value && dataSourceCache.value.length) {
|
||||
// 保存数据
|
||||
const arr = dataSourceCache.value.filter((i: any) => i?.plugin).map((item: any) => {
|
||||
// 保存数据.filter((i: any) => i?.plugin)
|
||||
const arr = dataSourceCache.value.map((item: any) => {
|
||||
return {
|
||||
metadataType: 'property',
|
||||
metadataId: item.id,
|
||||
originalId: item.plugin,
|
||||
originalId: item.plugin || null,
|
||||
}
|
||||
})
|
||||
onMapData(arr)
|
||||
|
|
|
@ -892,6 +892,17 @@ const handleImport = async () => {
|
|||
// _object as DeviceMetadata,
|
||||
// );
|
||||
// console.log(copyOperateLimits,_object); // 导入取并集逻辑
|
||||
Object.keys(_object).forEach((i: any) => {
|
||||
if (i === 'functions') {
|
||||
_object[i].forEach((a: any) => {
|
||||
a?.inputs?.forEach((item: any) => {
|
||||
item.expands = {
|
||||
required: false,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
const params = {
|
||||
id,
|
||||
metadata: JSON.stringify(_object),
|
||||
|
|
|
@ -4,7 +4,6 @@ export const testProperties = (data:any) =>{
|
|||
}
|
||||
|
||||
export const testType = (data:any,index:number,isArray?:boolean,isObject?:boolean)=>{
|
||||
console.log(data,index)
|
||||
if(data.type === 'boolean'){
|
||||
if(!data?.trueText || !data?.trueValue || !data?.falseText || !data?.falseValue){
|
||||
onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失必填属性`,'error')
|
||||
|
@ -39,7 +38,6 @@ export const testType = (data:any,index:number,isArray?:boolean,isObject?:boolea
|
|||
}
|
||||
}
|
||||
if(data.type === 'object' && !isArray && !isObject){
|
||||
console.log(data,'data123')
|
||||
if(data?.properties?.length > 0){
|
||||
return testObject(data.properties,index)
|
||||
}else{
|
||||
|
|
|
@ -71,17 +71,17 @@
|
|||
<j-descriptions-item label="总流量">{{
|
||||
detail.totalFlow
|
||||
? detail.totalFlow.toFixed(2) + ' M'
|
||||
: ''
|
||||
: '0 M'
|
||||
}}</j-descriptions-item>
|
||||
<j-descriptions-item label="使用流量">{{
|
||||
detail.usedFlow
|
||||
? detail.usedFlow.toFixed(2) + ' M'
|
||||
: ''
|
||||
: '0 M'
|
||||
}}</j-descriptions-item>
|
||||
<j-descriptions-item label="剩余流量">{{
|
||||
detail.residualFlow
|
||||
? detail.residualFlow.toFixed(2) + ' M'
|
||||
: ''
|
||||
: '0 M'
|
||||
}}</j-descriptions-item>
|
||||
<j-descriptions-item label="状态">
|
||||
{{
|
||||
|
@ -282,12 +282,12 @@ const getData = (
|
|||
};
|
||||
|
||||
/**
|
||||
* 查询今日、当月、本年数据
|
||||
* 查询左日、当月、本年数据
|
||||
*/
|
||||
const getDataTotal = () => {
|
||||
const dTime = [
|
||||
moment(new Date()).startOf('day').valueOf(),
|
||||
moment(new Date()).endOf('day').valueOf(),
|
||||
moment(new Date()).subtract(1, 'day').startOf('day').valueOf(),
|
||||
moment(new Date()).subtract(1, 'day').endOf('day').valueOf(),
|
||||
];
|
||||
const mTime = [
|
||||
moment().startOf('month').valueOf(),
|
||||
|
|
|
@ -150,10 +150,10 @@ const handleImport = async (file: any) => {
|
|||
event.onmessage = (e) => {
|
||||
const result = JSON.parse(e.data);
|
||||
if (result.success) {
|
||||
successNumber.value++;
|
||||
successNumber.value += result.result?.total || 1;
|
||||
} else {
|
||||
if (result.rowNumber !== -1) {
|
||||
failNumber.value++;
|
||||
failNumber.value += result.result?.total || 1;
|
||||
message.push({
|
||||
rowNumber: `第${result.rowNumber}行`,
|
||||
message: result.message,
|
||||
|
|
|
@ -575,10 +575,12 @@ const columns = [
|
|||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '未同步', value: 'notReady' },
|
||||
{ label: '同步失败', value: 'error' },
|
||||
{ label: '激活', value: 'using' },
|
||||
{ label: '未激活', value: 'toBeActivated' },
|
||||
{ label: '停机', value: 'deactivate' },
|
||||
{ label: '其它', value: 'using,toBeActivated,deactivate' },
|
||||
{ label: '其它', value: 'other' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -759,20 +761,7 @@ const getActions = (
|
|||
};
|
||||
|
||||
const handleSearch = (e: any) => {
|
||||
const newParams = (e?.terms as any[])?.map((item1) => {
|
||||
item1.terms = item1.terms.map((item2: any) => {
|
||||
if (
|
||||
['cardStateType'].includes(item2.column) &&
|
||||
!['using', 'toBeActivated', 'deactivate'].includes(item2.value)
|
||||
) {
|
||||
// 处理其它状态
|
||||
item2.termType = item2.termType === 'not' ? 'in' : 'nin';
|
||||
}
|
||||
return item2;
|
||||
});
|
||||
return item1;
|
||||
});
|
||||
params.value = { terms: newParams || [] };
|
||||
params.value = { terms: e?.terms || [] };
|
||||
};
|
||||
|
||||
const onSelectChange = (keys: string[], rows: []) => {
|
||||
|
|
|
@ -415,7 +415,7 @@ const formData = ref({
|
|||
onvifUsername: '',
|
||||
},
|
||||
// 编辑字段
|
||||
streamMode: 'UDP',
|
||||
streamMode: '',
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
firmware: '',
|
||||
|
@ -442,12 +442,13 @@ const getProductList = async () => {
|
|||
const { result } = await DeviceApi.queryProductList(params);
|
||||
productList.value = result;
|
||||
};
|
||||
getProductList();
|
||||
|
||||
const handleProductChange = () => {
|
||||
formData.value.others.access_pwd =
|
||||
productList.value.find((f: any) => f.id === formData.value.productId)
|
||||
?.configuration.access_pwd || '';
|
||||
formData.value.streamMode = productList.value.find((f: any) => f.id === formData.value.productId)
|
||||
?.configuration.stream_mode || '';
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -463,11 +464,24 @@ const getDetail = async () => {
|
|||
const res = await DeviceApi.detail(route.query.id as string);
|
||||
Object.assign(formData.value, res.result);
|
||||
formData.value.channel = res.result.provider;
|
||||
console.log(formData.value,'formData')
|
||||
if (formData.value.productId) {
|
||||
const productData = productList.value.find((i: any) => {
|
||||
return i.id === formData.value.productId;
|
||||
});
|
||||
if (productData) {
|
||||
formData.value.others.access_pwd = formData.value.others.access_pwd
|
||||
? formData.value.others.access_pwd
|
||||
: productData?.configuration?.access_pwd;
|
||||
formData.value.streamMode = formData.value.streamMode
|
||||
? formData.value.streamMode
|
||||
: productData?.configuration?.stream_mode;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDetail();
|
||||
getProductList();
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -493,11 +507,7 @@ const handleSubmit = () => {
|
|||
: { id, streamMode, manufacturer, model, firmware, ...extraParams };
|
||||
} else if (formData.value.channel === 'gb28181-2016') {
|
||||
// 国标
|
||||
others = omit(others, [
|
||||
'onvifUrl',
|
||||
'onvifPassword',
|
||||
'onvifUsername',
|
||||
]);
|
||||
others = omit(others, ['onvifUrl', 'onvifPassword', 'onvifUsername']);
|
||||
const getParmas = () => {
|
||||
if (others?.stream_mode) {
|
||||
others.stream_mode = streamMode;
|
||||
|
@ -516,8 +526,16 @@ const handleSubmit = () => {
|
|||
} else {
|
||||
others = omit(others, ['access_pwd']);
|
||||
params = !id
|
||||
? {others,...extraParams}
|
||||
: { id, streamMode, manufacturer, model, firmware,others, ...extraParams };
|
||||
? { others, ...extraParams }
|
||||
: {
|
||||
id,
|
||||
streamMode,
|
||||
manufacturer,
|
||||
model,
|
||||
firmware,
|
||||
others,
|
||||
...extraParams,
|
||||
};
|
||||
}
|
||||
|
||||
formRef.value
|
||||
|
|
|
@ -202,6 +202,8 @@ const columns = [
|
|||
{
|
||||
title: '变量',
|
||||
dataIndex: 'id',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
scopedSlots: { customRender: 'id' },
|
||||
},
|
||||
{
|
||||
|
|
|
@ -203,6 +203,8 @@ const columns = [
|
|||
{
|
||||
title: '变量',
|
||||
dataIndex: 'id',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
scopedSlots: { customRender: 'id' },
|
||||
},
|
||||
{
|
||||
|
|
|
@ -103,6 +103,7 @@ const columns = [
|
|||
title: '变量',
|
||||
dataIndex: 'id',
|
||||
width: 80,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
|
@ -151,6 +152,7 @@ const handleTypeChange = (record: IVariable) => {
|
|||
|
||||
<style lang="less" scoped>
|
||||
.table-wrapper {
|
||||
background-color: #1d39c4;
|
||||
.has-error {
|
||||
border-color: rgba(255, 77, 79);
|
||||
&:focus {
|
||||
|
|
|
@ -662,6 +662,7 @@
|
|||
v-model:modelValue="
|
||||
formData.template.body
|
||||
"
|
||||
language="JavaScript"
|
||||
/>
|
||||
</div>
|
||||
</j-form-item>
|
||||
|
|
|
@ -147,39 +147,40 @@ const columns = [
|
|||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const res = await query(
|
||||
{
|
||||
sorts: [
|
||||
{
|
||||
name: 'createTime',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
column: 'id',
|
||||
termType: 'alarm-bind-rule$not',
|
||||
value: props.id,
|
||||
type: 'and',
|
||||
},
|
||||
{
|
||||
column: 'triggerType',
|
||||
termType: 'eq',
|
||||
value: props.type === 'other' ? undefined : 'device',
|
||||
},
|
||||
]
|
||||
}
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.result.data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
return []
|
||||
}
|
||||
type: 'string',
|
||||
// rename: "id",
|
||||
// options: async () => {
|
||||
// const res = await query(
|
||||
// {
|
||||
// sorts: [
|
||||
// {
|
||||
// name: 'createTime',
|
||||
// order: 'desc',
|
||||
// },
|
||||
// ],
|
||||
// terms: [
|
||||
// {
|
||||
// column: 'id',
|
||||
// termType: 'alarm-bind-rule$not',
|
||||
// value: props.id,
|
||||
// type: 'and',
|
||||
// },
|
||||
// {
|
||||
// column: 'triggerType',
|
||||
// termType: 'eq',
|
||||
// value: props.type === 'other' ? undefined : 'device',
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// );
|
||||
// if (res.status === 200) {
|
||||
// return res.result.data.map((item: any) => ({
|
||||
// label: item.name,
|
||||
// value: item.id,
|
||||
// }));
|
||||
// }
|
||||
// return []
|
||||
// }
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<JProTable
|
||||
:columns="columns"
|
||||
model="TABLE"
|
||||
ref="tableRef"
|
||||
:request="queryList"
|
||||
:params="params"
|
||||
:defaultParams="{
|
||||
|
@ -45,7 +46,7 @@
|
|||
</JProTable>
|
||||
</FullPage>
|
||||
<Info
|
||||
v-if="visiable"
|
||||
v-if="visible"
|
||||
:data="current"
|
||||
@close="close"
|
||||
:description="description"
|
||||
|
@ -54,20 +55,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { detail, queryHistoryList } from '@/api/rule-engine/log';
|
||||
import {detail, queryHistoryLogList} from '@/api/rule-engine/log';
|
||||
import { detail as configurationDetail } from '@/api/rule-engine/configuration';
|
||||
import { useRoute } from 'vue-router';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { useAlarmStore } from '@/store/alarm';
|
||||
import Info from './info.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRouterParams } from '@/utils/hooks/useParams';
|
||||
const route = useRoute();
|
||||
const id = route.params?.id;
|
||||
const { params: routerParams } = useRouterParams();
|
||||
let visiable = ref(false);
|
||||
let visible = ref(false);
|
||||
let description = ref<string>();
|
||||
const tableRef = ref()
|
||||
const columns = [
|
||||
{
|
||||
title: '告警时间',
|
||||
|
@ -98,7 +98,7 @@ const columns = [
|
|||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type?: 'table',
|
||||
): ActionsType[] => {
|
||||
): any[] => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ const getActions = (
|
|||
icon: 'SearchOutlined',
|
||||
onClick: () => {
|
||||
current.value = data;
|
||||
visiable.value = true;
|
||||
visible.value = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -135,7 +135,8 @@ let details = ref(); // 告警记录的详情
|
|||
* 获取详情列表
|
||||
*/
|
||||
const queryList = async (params: any) => {
|
||||
const res = await queryHistoryList({
|
||||
if(data.current?.alarmConfigId){
|
||||
const res = await queryHistoryLogList(data.current?.alarmConfigId,{
|
||||
...params,
|
||||
// sorts: [{ name: 'alarmTime', order: 'desc' }],
|
||||
});
|
||||
|
@ -151,6 +152,7 @@ const queryList = async (params: any) => {
|
|||
},
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
code: 200,
|
||||
|
@ -167,11 +169,13 @@ const queryList = async (params: any) => {
|
|||
/**
|
||||
* 根据id初始化数据
|
||||
*/
|
||||
watchEffect(async () => {
|
||||
|
||||
watch(() => id, async () => {
|
||||
const res = await detail(id);
|
||||
if (res.status === 200) {
|
||||
data.current = res.result;
|
||||
if (res.result.targetType === 'device') {
|
||||
data.current = res.result || {};
|
||||
tableRef.value?.reload()
|
||||
if (res.result?.targetType === 'device') {
|
||||
columns.splice(2, 0, {
|
||||
dataIndex: 'targetName',
|
||||
title: '告警设备',
|
||||
|
@ -184,7 +188,10 @@ watchEffect(async () => {
|
|||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
const handleSearch = (_params: any) => {
|
||||
params.value = _params;
|
||||
};
|
||||
|
@ -193,13 +200,13 @@ const handleSearch = (_params: any) => {
|
|||
* 关闭模态弹窗
|
||||
*/
|
||||
const close = () => {
|
||||
visiable.value = false;
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
current.value = details.value;
|
||||
if (routerParams.value.detail && details.value) {
|
||||
visiable.value = true;
|
||||
visible.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -120,13 +120,17 @@
|
|||
{{ titleMap.get(slotProps.targetType) }}
|
||||
</template>
|
||||
<template #alarmTime="slotProps">
|
||||
{{ dayjs(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')}}
|
||||
{{
|
||||
dayjs(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}}
|
||||
</template>
|
||||
<template #level="slotProps">
|
||||
<Ellipsis style="width: calc(100% - 20px)">
|
||||
{{ data.defaultLevel.find((i)=>{
|
||||
return i.level === slotProps.level
|
||||
}).title}}
|
||||
{{
|
||||
data.defaultLevel.find((i) => {
|
||||
return i.level === slotProps.level;
|
||||
}).title
|
||||
}}
|
||||
</Ellipsis>
|
||||
</template>
|
||||
<template #state="slotProps">
|
||||
|
@ -229,26 +233,26 @@ titleMap.set('other', '其他');
|
|||
titleMap.set('org', '组织');
|
||||
const columns = [
|
||||
{
|
||||
title:'配置名称',
|
||||
dataIndex:'alarmName',
|
||||
key:'alarmName',
|
||||
},{
|
||||
title:'类型',
|
||||
dataIndex:'targetType',
|
||||
key:'targetType',
|
||||
scopedSlots:true
|
||||
title: '配置名称',
|
||||
dataIndex: 'alarmName',
|
||||
key: 'alarmName',
|
||||
},
|
||||
{
|
||||
title:'关联场景联动',
|
||||
dataIndex:'sourceName',
|
||||
key:'sourceName',
|
||||
|
||||
title: '类型',
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '关联场景联动',
|
||||
dataIndex: 'sourceName',
|
||||
key: 'sourceName',
|
||||
},
|
||||
{
|
||||
title: '告警级别',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width:200,
|
||||
width: 200,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: data.value.defaultLevel.map((item: any) => {
|
||||
|
@ -258,7 +262,7 @@ const columns = [
|
|||
};
|
||||
}),
|
||||
},
|
||||
scopedSlots: true
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '最近告警时间',
|
||||
|
@ -300,49 +304,52 @@ const columns = [
|
|||
const newColumns = computed(() => {
|
||||
const otherColumns = {
|
||||
title: '产品名称',
|
||||
dataIndex: 'targetId',
|
||||
key: 'targetId',
|
||||
dataIndex: 'targetName',
|
||||
key: 'targetName',
|
||||
// search: {
|
||||
// type: 'select',
|
||||
// options: async () => {
|
||||
// const termType = [
|
||||
// {
|
||||
// column: 'targetType',
|
||||
// termType: 'eq',
|
||||
// type: 'and',
|
||||
// value: props.type,
|
||||
// },
|
||||
// ];
|
||||
|
||||
// if (props.id) {
|
||||
// termType.push({
|
||||
// termType: 'eq',
|
||||
// column: 'alarmConfigId',
|
||||
// value: props.id,
|
||||
// type: 'and',
|
||||
// });
|
||||
// }
|
||||
|
||||
// const resp: any = await handleSearch({
|
||||
// sorts: [{ name: 'alarmTime', order: 'desc' }],
|
||||
// terms: termType,
|
||||
// });
|
||||
// const listMap: Map<string, any> = new Map();
|
||||
|
||||
// if (resp.status === 200) {
|
||||
// resp.result.data.forEach((item) => {
|
||||
// if (item.targetId) {
|
||||
// listMap.set(item.targetId, {
|
||||
// label: item.targetName,
|
||||
// value: item.targetId,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// return [...listMap.values()];
|
||||
// }
|
||||
// return [];
|
||||
// },
|
||||
// },
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const termType = [
|
||||
{
|
||||
column: 'targetType',
|
||||
termType: 'eq',
|
||||
type: 'and',
|
||||
value: props.type,
|
||||
},
|
||||
];
|
||||
|
||||
if (props.id) {
|
||||
termType.push({
|
||||
termType: 'eq',
|
||||
column: 'alarmConfigId',
|
||||
value: props.id,
|
||||
type: 'and',
|
||||
});
|
||||
}
|
||||
|
||||
const resp: any = await handleSearch({
|
||||
sorts: [{ name: 'alarmTime', order: 'desc' }],
|
||||
terms: termType,
|
||||
});
|
||||
const listMap: Map<string, any> = new Map();
|
||||
|
||||
if (resp.status === 200) {
|
||||
resp.result.data.forEach((item) => {
|
||||
if (item.targetId) {
|
||||
listMap.set(item.targetId, {
|
||||
label: item.targetName,
|
||||
value: item.targetId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...listMap.values()];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -234,10 +234,10 @@ const getDashBoard = () => {
|
|||
const _data = res.result as DashboardItem[];
|
||||
state.today = _data.find(
|
||||
(item) => item.group === 'today',
|
||||
)?.data.value;
|
||||
)?.data.value || 0;
|
||||
state.thisMonth = _data.find(
|
||||
(item) => item.group === 'thisMonth',
|
||||
)?.data.value;
|
||||
)?.data.value || 0;
|
||||
currentMonAlarm.value[0].value = state.thisMonth;
|
||||
const fifteenData = _data
|
||||
.filter((item) => item.group === '15day')
|
||||
|
|
|
@ -72,7 +72,10 @@ watchEffect(() => {
|
|||
const _item = _props.value?.find((i) => i.name === item?.id) || {};
|
||||
return {
|
||||
...item,
|
||||
..._item,
|
||||
...{
|
||||
name: _item.id,
|
||||
..._item.value
|
||||
},
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
|
@ -83,9 +86,11 @@ const onChange = () => {
|
|||
const arr = [...dataSource.value].map((item) => {
|
||||
return {
|
||||
name: item.id,
|
||||
value: {
|
||||
source: item.source,
|
||||
upperKey: item.upperKey || item.value,
|
||||
value: item.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
emit('update:value', arr);
|
||||
|
|
|
@ -124,6 +124,7 @@ const sceneStore = useSceneStore();
|
|||
const { data: formModel } = storeToRefs(sceneStore);
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: TermsType): void;
|
||||
};
|
||||
|
@ -163,6 +164,10 @@ const props = defineProps({
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
branches_Index:{
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<TermsType>,
|
||||
default: () => ({
|
||||
|
@ -401,16 +406,15 @@ const columnSelect = (option: any) => {
|
|||
|
||||
paramsValue.value = newValue;
|
||||
}
|
||||
console.log(paramsValue, hasTypeChange);
|
||||
handOptionByColumn(option);
|
||||
emit('update:value', { ...paramsValue });
|
||||
nextTick(() => {
|
||||
formItemContext.onFieldChange();
|
||||
});
|
||||
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
|
||||
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
|
||||
props.termsName
|
||||
][0] = option.name;
|
||||
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
|
||||
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
|
||||
props.termsName
|
||||
][1] = paramsValue.termType;
|
||||
};
|
||||
|
@ -463,7 +467,7 @@ const termsTypeSelect = (e: { key: string; name: string }) => {
|
|||
paramsValue.value = newValue;
|
||||
emit('update:value', { ...paramsValue });
|
||||
formItemContext.onFieldChange();
|
||||
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
|
||||
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
|
||||
props.termsName
|
||||
][1] = e.name;
|
||||
};
|
||||
|
@ -483,14 +487,14 @@ const valueSelect = (
|
|||
}
|
||||
emit('update:value', { ...newValues });
|
||||
formItemContext.onFieldChange();
|
||||
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
|
||||
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
|
||||
props.termsName
|
||||
][2] = labelObj;
|
||||
};
|
||||
|
||||
const typeSelect = (e: any) => {
|
||||
emit('update:value', { ...paramsValue });
|
||||
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
|
||||
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
|
||||
props.termsName
|
||||
][3] = e.label;
|
||||
};
|
||||
|
|
|
@ -193,7 +193,7 @@ const addGroup = () => {
|
|||
then: [],
|
||||
executeAnyway: true,
|
||||
branchId: Math.floor(Math.random() * 100000000),
|
||||
branchName:'条件'+ data.value.branches?.length
|
||||
branchName:''
|
||||
}
|
||||
data.value.branches?.push(branchesItem)
|
||||
data.value.branches?.push(null as any)
|
||||
|
|
|
@ -82,7 +82,6 @@ const save = async () => {
|
|||
getDetail(route.query.id as string)
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('scene-onUnmounted')
|
||||
refresh?.()
|
||||
})
|
||||
|
||||
|
|
|
@ -108,10 +108,12 @@
|
|||
tooltip="填写访问其它平台的地址"
|
||||
/>
|
||||
</template>
|
||||
<j-input
|
||||
|
||||
<!-- <j-input
|
||||
v-model:value="form.data.page.baseUrl"
|
||||
placeholder="请输入接入地址"
|
||||
/>
|
||||
/> -->
|
||||
<InputGroup v-model:value="form.data.page.baseUrl" style="width: 100%;"/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="路由方式"
|
||||
|
@ -1436,6 +1438,7 @@ import { cloneDeep, difference } from 'lodash-es';
|
|||
import { useMenuStore } from '@/store/menu';
|
||||
import { Rule } from 'ant-design-vue/lib/form';
|
||||
import ApplyList from './ApplyList/index.vue';
|
||||
import InputGroup from './InputGroup.vue'
|
||||
|
||||
const emit = defineEmits(['changeApplyType']);
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-input
|
||||
@change="onChange"
|
||||
placeholder="请输入"
|
||||
v-model:value="_value.last"
|
||||
>
|
||||
<template #addonBefore>
|
||||
<a-select
|
||||
@change="onChange"
|
||||
v-model:value="_value.first"
|
||||
:options="options"
|
||||
style="width: 100px"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="InputGroup">
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:value']);
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: 'https://',
|
||||
value: 'https://',
|
||||
},
|
||||
{
|
||||
label: 'http://',
|
||||
value: 'http://',
|
||||
},
|
||||
];
|
||||
|
||||
const _value = reactive({
|
||||
first: 'https://',
|
||||
last: undefined,
|
||||
});
|
||||
const onChange = () => {
|
||||
emits('update:value', `${_value.first || ''}${_value.last || ''}`);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (val && typeof val === 'string') {
|
||||
options.forEach((i) => {
|
||||
if (val.startsWith(i.value)) {
|
||||
_value.first = i.value;
|
||||
_value.last = val.replace(i.value, '');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_value.first = 'https://';
|
||||
_value.last = undefined;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -242,17 +242,6 @@ const typeOptions = ref<any[]>([]);
|
|||
const visible = ref<boolean>(false);
|
||||
const addMenuVisible = ref<boolean>(false);
|
||||
|
||||
onMounted(() => {
|
||||
queryType().then((resp: any) => {
|
||||
if (resp.status === 200) {
|
||||
const arr = resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.provider,
|
||||
}));
|
||||
typeOptions.value = arr;
|
||||
}
|
||||
});
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
|
@ -272,17 +261,17 @@ const columns = [
|
|||
search: {
|
||||
type: 'select',
|
||||
options: typeOptions,
|
||||
// options: () =>
|
||||
// new Promise((resolve) => {
|
||||
// queryType().then((resp: any) => {
|
||||
// resolve(
|
||||
// resp.result.map((item: any) => ({
|
||||
// label: item.name,
|
||||
// value: item.provider,
|
||||
// })),
|
||||
// );
|
||||
// });
|
||||
// }),
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryType().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.provider,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
title="权限控制"
|
||||
@cancel="emit('close')"
|
||||
@ok="onSave"
|
||||
:maskClosable="false"
|
||||
>
|
||||
<div class="alert">
|
||||
<AIcon type="InfoCircleOutlined" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<j-modal :width="644" visible title="配置详情" @cancel="emit('close')">
|
||||
<j-modal :width="644" visible title="配置详情" @cancel="emit('close')" :maskClosable="false">
|
||||
<div class="detail">
|
||||
<div class="item">
|
||||
<div class="label">通知方式</div>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
title="配置通知方式"
|
||||
@cancel="emit('close')"
|
||||
:bodyStyle="{ padding: 0 }"
|
||||
:maskClosable="false"
|
||||
>
|
||||
<div style="background-color: #f8f9fc; padding: 25px 100px">
|
||||
<j-steps :current="current" size="small" @change="onChange">
|
||||
|
|
|
@ -63,8 +63,7 @@ export function getCodeText(
|
|||
result[item.paramsName] = '';
|
||||
break;
|
||||
default: {
|
||||
const properties = schemas[item.paramsType]
|
||||
.properties as object;
|
||||
const properties = schemas[item.paramsType]?.properties as object || {};
|
||||
const newArr = Object.entries(properties).map(
|
||||
(item: [string, any]) => ({
|
||||
paramsName: item[0],
|
||||
|
|
|
@ -9,73 +9,101 @@
|
|||
<AIcon type="SearchOutlined" style="color: rgba(0, 0, 0, 0.45)" />
|
||||
</template>
|
||||
</j-input>
|
||||
<j-button @click="onAdd" type="primary" class="btn">新增区域</j-button>
|
||||
<j-tree
|
||||
<div style="display: flex; gap: 8px; margin: 18px 0">
|
||||
<j-button type="primary" class="btn" @click="() => onAdd()"
|
||||
>新增区域</j-button
|
||||
>
|
||||
</div>
|
||||
<div class="tree-content">
|
||||
<ResizeObserver v-if="_treeData.length" @resize="divResize">
|
||||
<div style="height: 100%; width: 100%">
|
||||
<a-tree
|
||||
class="draggable-tree"
|
||||
draggable
|
||||
block-node
|
||||
v-if="treeData.length"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
:tree-data="_treeData"
|
||||
@drop="onDrop"
|
||||
:defaultExpandAll="true"
|
||||
:height="700"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:show-icon="true"
|
||||
:field-names="{ key: 'id' }"
|
||||
:virtual="true"
|
||||
:height="heightSize"
|
||||
@drop="onDrop"
|
||||
@select="areaSelect"
|
||||
>
|
||||
<template #title="_data">
|
||||
<div class="tree-box">
|
||||
<div class="name">
|
||||
<j-ellipsis>{{ _data?.name }}</j-ellipsis>
|
||||
<j-ellipsis
|
||||
>{{ _data?.name }} ({{
|
||||
_data.code
|
||||
}})</j-ellipsis
|
||||
>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<j-space :size="8">
|
||||
<j-tooltip title="重命名">
|
||||
<j-tooltip title="编辑">
|
||||
<j-button
|
||||
@click.stop="onEdit(_data?.data)"
|
||||
class="actions-btn"
|
||||
type="link"
|
||||
><AIcon type="EditOutlined"
|
||||
/></j-button>
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</j-button>
|
||||
</j-tooltip>
|
||||
<j-tooltip title="新增子区域">
|
||||
<j-button
|
||||
@click.stop="onAdd(_data?.data)"
|
||||
class="actions-btn"
|
||||
type="link"
|
||||
><AIcon type="PlusCircleOutlined"
|
||||
/></j-button>
|
||||
>
|
||||
<AIcon type="PlusCircleOutlined" />
|
||||
</j-button>
|
||||
</j-tooltip>
|
||||
<j-tooltip title="删除">
|
||||
<j-popconfirm @confirm="onRemove(_data?.id)">
|
||||
<j-popconfirm
|
||||
@confirm="onRemove(_data?.id)"
|
||||
>
|
||||
<j-button
|
||||
@click.stop
|
||||
class="actions-btn"
|
||||
type="link"
|
||||
danger
|
||||
><AIcon type="DeleteOutlined"
|
||||
/></j-button>
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</j-button>
|
||||
</j-popconfirm>
|
||||
</j-tooltip>
|
||||
</j-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</j-tree>
|
||||
<j-empty v-else style="margin-top: 150px" />
|
||||
</a-tree>
|
||||
</div>
|
||||
</ResizeObserver>
|
||||
<div v-else class="tree-empty">
|
||||
<j-empty />
|
||||
</div>
|
||||
</div>
|
||||
<Save
|
||||
:mode="mode"
|
||||
v-if="visible"
|
||||
:data="current"
|
||||
:treeData="_treeData"
|
||||
:areaTree="areaTree"
|
||||
@save="onSave"
|
||||
@close="onClose"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onlyMessage } from '@/utils/comm';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import Save from '../Save/index.vue';
|
||||
import { getRegionTree, delRegion } from '@/api/system/region';
|
||||
import { useArea } from '../hooks';
|
||||
import ResizeObserver from 'ant-design-vue/lib/vc-resize-observer';
|
||||
import { onlyMessage } from '@/utils/comm';
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
const _treeData = ref<any[]>([]);
|
||||
|
@ -83,12 +111,22 @@ const visible = ref<boolean>(false);
|
|||
const current = ref<any>({});
|
||||
const mode = ref<'add' | 'edit'>('add');
|
||||
const searchValue = ref<string>();
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
|
||||
const heightSize = ref(550);
|
||||
const type = ref<string | undefined>(undefined);
|
||||
|
||||
const { areaTree } = useArea();
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const filterTreeNodes = (tree: any[], condition: string) => {
|
||||
return tree.filter((item) => {
|
||||
if (item?.title && item.title.includes(condition)) {
|
||||
if (item?.name && item.name.includes(condition)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item?.code && item.code.includes(condition)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -102,19 +140,43 @@ const filterTreeNodes = (tree: any[], condition: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
const getTreeId = (data: any[], cb: (id: string) => void) => {
|
||||
data.forEach((item) => {
|
||||
if (item.children) {
|
||||
cb?.(item.id);
|
||||
getTreeId(item.children, cb);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSearch = debounce((v: string) => {
|
||||
_treeData.value = filterTreeNodes(treeData.value, v);
|
||||
});
|
||||
_treeData.value = v
|
||||
? filterTreeNodes(cloneDeep(treeData.value), v)
|
||||
: cloneDeep(treeData.value);
|
||||
expandedKeys.value = [];
|
||||
if (v) {
|
||||
getTreeId(_treeData.value, (id: string) => {
|
||||
expandedKeys.value.push(id);
|
||||
});
|
||||
expandedKeys.value = [...expandedKeys.value];
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const onSave = () => {
|
||||
visible.value = false;
|
||||
handleSearch()
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const divResize = ({ height }) => {
|
||||
setTimeout(() => {
|
||||
heightSize.value = height;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onEdit = (_data: any) => {
|
||||
mode.value = 'edit';
|
||||
current.value = _data;
|
||||
|
@ -131,7 +193,21 @@ const onRemove = async (id: string) => {
|
|||
|
||||
const onAdd = (_data?: any) => {
|
||||
mode.value = 'add';
|
||||
current.value = _data ? _data : {};
|
||||
const _children = _data ? _data.children || [] : _treeData.value;
|
||||
const lastItem = _children.length ? _children[_children.length - 1] : null;
|
||||
const sortIndex = lastItem ? lastItem.sortIndex + 1 : 1;
|
||||
current.value = _data
|
||||
? {
|
||||
parentId: _data.id,
|
||||
parentFullName: _data.fullName,
|
||||
sortIndex: sortIndex,
|
||||
}
|
||||
: {
|
||||
parentId: '',
|
||||
parentFullName: '',
|
||||
sortIndex: sortIndex,
|
||||
};
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
|
@ -206,10 +282,27 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 区域选择
|
||||
*/
|
||||
const areaSelect = (key, { node }) => {
|
||||
selectedKeys.value = key;
|
||||
emit('select', node?.code);
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const resp = await getRegionTree();
|
||||
const resp = await getRegionTree({
|
||||
paging: false,
|
||||
sorts: [{ name: 'sortIndex', order: 'asc' }],
|
||||
});
|
||||
if (resp.success) {
|
||||
treeData.value = resp?.result || [];
|
||||
// 默认选择第一个数据
|
||||
const dt = treeData.value?.[0];
|
||||
if (dt) {
|
||||
selectedKeys.value = dt?.id ? [dt?.id] : [];
|
||||
emit('select', dt?.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -220,15 +313,36 @@ onMounted(() => {
|
|||
|
||||
<style lang="less" scoped>
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin: 18px 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tree-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
|
||||
.tree-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
:deep(.ant-tree-node-content-wrapper) {
|
||||
transform: translateY(-4px) !important;
|
||||
}
|
||||
|
||||
.tree-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.actions {
|
||||
padding-right: 4px;
|
||||
|
||||
.actions-btn {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -236,5 +350,3 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -1,241 +1,73 @@
|
|||
<template>
|
||||
<div class="region-map">
|
||||
<AMapComponent
|
||||
@init="initMap"
|
||||
>
|
||||
<el-amap-polygon
|
||||
v-if="overlay.type === 'polygon'"
|
||||
:key="JSON.stringify(overlay.path || [])"
|
||||
:editable="overlay.editable"
|
||||
:path="overlay.path"
|
||||
:visible="visible"
|
||||
@addnode="polygonDraw"
|
||||
@adjust="polygonDraw"
|
||||
@init="overlaysInit"
|
||||
@removenode="polygonDraw"
|
||||
/>
|
||||
<el-amap-circle
|
||||
v-else-if="overlay.type === 'circle'"
|
||||
:key="`${overlay.radius}_${JSON.stringify(overlay.path || [])}`"
|
||||
:center="overlay.path"
|
||||
:editable="overlay.editable"
|
||||
:radius="overlay.radius"
|
||||
:visible="visible"
|
||||
@adjust="circleDraw"
|
||||
@init="overlaysInit"
|
||||
/>
|
||||
<el-amap-rectangle
|
||||
v-else-if="overlay.type === 'rectangle'"
|
||||
:key="JSON.stringify(overlay.path || [])"
|
||||
:bounds="overlay.path"
|
||||
:editable="overlay.editable"
|
||||
:visible="visible"
|
||||
@adjust="rectangleDraw"
|
||||
@init="overlaysInit"
|
||||
/>
|
||||
<el-amap-mouse-tool
|
||||
v-else-if="overlay.type === 'create' && toolType"
|
||||
:type="toolType"
|
||||
@draw="toolDraw"
|
||||
/>
|
||||
</AMapComponent>
|
||||
<div v-show="overlay.editable || overlay.type === 'create'" class="map-tool">
|
||||
<div class="map-tool-content">
|
||||
<div class="tool-item-group">
|
||||
<div class="tool-item">
|
||||
<j-tooltip title="双击保存描点" >
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</j-tooltip>
|
||||
<AMapComponent @init="initMap"/>
|
||||
<div class="region-map-loading" v-if="_loading || loading">
|
||||
<a-spin :spinning="_loading || loading"></a-spin>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item-group">
|
||||
<div :class="{'tool-item': true, 'active': toolType === 'rectangle'}" @click="() => { drawOverlays('rectangle') }">
|
||||
<j-tooltip title="矩形" >
|
||||
<AIcon type="icon-huajuxing" />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
<div :class="{'tool-item': true, 'active': toolType === 'circle'}" @click="() => { drawOverlays('circle') }">
|
||||
<j-tooltip title="圆" >
|
||||
<AIcon type="icon-draw-circle" />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
<div :class="{'tool-item': true, 'active': toolType === 'polygon'}" @click="() => { drawOverlays('polygon') }">
|
||||
<j-tooltip title="多边形" >
|
||||
<AIcon type="icon-huaduobianxing" />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="tool-item-group">-->
|
||||
<!-- <div class="tool-item">-->
|
||||
<!-- <j-tooltip title="缩放" >-->
|
||||
<!-- <AIcon type="GetwayOutlined" />-->
|
||||
<!-- </j-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="tool-item">-->
|
||||
<!-- <j-tooltip title="旋转" >-->
|
||||
<!-- <AIcon type="GetwayOutlined" />-->
|
||||
<!-- </j-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="tool-item-group">
|
||||
<div :class="{'tool-item': true, 'disabled': historyList.length <= 1 }" @click="cancel">
|
||||
<j-tooltip title="撤销" >
|
||||
<AIcon type="RollbackOutlined" />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
<div class="tool-item">
|
||||
<j-tooltip title="删除" @click="deleteOverlays">
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</j-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script name="RegionMap" setup>
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
type: {
|
||||
selectCode: {
|
||||
type: String,
|
||||
default: 'polygon'
|
||||
default: undefined
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits('dragend')
|
||||
const MapRef = ref()
|
||||
const toolType = ref('')
|
||||
const historyList = ref([])
|
||||
|
||||
const overlay = reactive({
|
||||
type: props.type,
|
||||
path: props.path,
|
||||
center: [],
|
||||
editable: false,
|
||||
status: 0 // 0 预览;1 编辑
|
||||
})
|
||||
|
||||
const visible = computed(() => {
|
||||
return true
|
||||
})
|
||||
|
||||
const setHistory = (data) => {
|
||||
if (historyList.value.length > 10) {
|
||||
historyList.value.shift()
|
||||
}
|
||||
historyList.value.push(data)
|
||||
}
|
||||
const loading = ref(true)
|
||||
const _loading = ref(true)
|
||||
let polygon = null
|
||||
let district = null
|
||||
const initMap = (e) => {
|
||||
loading.value = true
|
||||
MapRef.value = e
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const polygonDraw = (e) => {
|
||||
|
||||
const target = e.target
|
||||
const path = target.getPath()
|
||||
setHistory({
|
||||
type: 'polygon',
|
||||
toolType: toolType.value,
|
||||
editable: true,
|
||||
path: path.map(item => [item.lng, item.lat]),
|
||||
})
|
||||
}
|
||||
|
||||
const circleDraw = (e) => {
|
||||
setHistory({
|
||||
type: 'circle',
|
||||
toolType: toolType.value,
|
||||
editable: true,
|
||||
path: [e.lnglat.lng, e.lnglat.lat],
|
||||
radius: e.radius
|
||||
})
|
||||
}
|
||||
|
||||
const rectangleDraw = (e) => {
|
||||
const northEast = e.bounds.getNorthEast()
|
||||
const southWest = e.bounds.getSouthWest()
|
||||
const path = [
|
||||
[southWest.lng, southWest.lat],
|
||||
[northEast.lng, northEast.lat]
|
||||
]
|
||||
|
||||
setHistory({
|
||||
type: 'rectangle',
|
||||
toolType: toolType.value,
|
||||
editable: true,
|
||||
path: path,
|
||||
})
|
||||
}
|
||||
|
||||
const overlaysInit = (e) => {
|
||||
if (MapRef.value) {
|
||||
MapRef.value.setFitView(e)
|
||||
const queryBounds = (code) => {
|
||||
_loading.value = true
|
||||
if (!district) {
|
||||
//实例化DistrictSearch
|
||||
const opts = {
|
||||
subdistrict: 0,
|
||||
extensions: 'all',
|
||||
level: 'district'
|
||||
};
|
||||
district = new AMap.DistrictSearch(opts);
|
||||
}
|
||||
}
|
||||
|
||||
const drawOverlays = (e) => {
|
||||
overlay.type = 'create'
|
||||
toolType.value = e
|
||||
}
|
||||
|
||||
const toolDraw = (e) => {
|
||||
if (toolType.value === 'circle') {
|
||||
overlay.path = e.center
|
||||
overlay.radius = e.radius
|
||||
} else {
|
||||
overlay.path = e
|
||||
district.search(code, function (status, result) {
|
||||
if (polygon) {
|
||||
MapRef.value.remove(polygon)// 清除上次结果
|
||||
polygon = null;
|
||||
}
|
||||
overlay.editable = true
|
||||
overlay.type = toolType.value
|
||||
setHistory({
|
||||
type: toolType.value,
|
||||
toolType: toolType.value,
|
||||
editable: overlay.editable,
|
||||
path: overlay.path,
|
||||
radius: overlay.radius
|
||||
})
|
||||
}
|
||||
|
||||
const deleteOverlays = () => {
|
||||
overlay.type = 'create'
|
||||
overlay.editable = false
|
||||
toolType.value = ''
|
||||
|
||||
setHistory({
|
||||
type: 'create',
|
||||
toolType: '',
|
||||
editable: false,
|
||||
path: [],
|
||||
radius: 0
|
||||
})
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
if (historyList.value.length > 1) {
|
||||
historyList.value.pop()
|
||||
const bounds = result?.districtList?.[0]?.boundaries;
|
||||
if (bounds) {
|
||||
//生成行政区划polygon
|
||||
for (let i = 0; i < bounds.length; i += 1) {// 构造MultiPolygon的path
|
||||
bounds[i] = [bounds[i]]
|
||||
}
|
||||
|
||||
const oldData = historyList.value[historyList.value.length - 1]
|
||||
|
||||
if (oldData) {
|
||||
overlay.type = oldData.type
|
||||
overlay.editable = oldData.editable
|
||||
overlay.path = oldData.path
|
||||
overlay.radius = oldData.radius
|
||||
toolType.value = oldData.toolType
|
||||
polygon = new AMap.Polygon({
|
||||
strokeWeight: 1,
|
||||
path: bounds,
|
||||
fillOpacity: 0.4,
|
||||
fillColor: '#80d8ff',
|
||||
strokeColor: '#0091ea'
|
||||
});
|
||||
MapRef.value.add(polygon)
|
||||
MapRef.value.setFitView(polygon);// 视口自适应
|
||||
}
|
||||
_loading.value = false
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => [props.selectCode, loading.value], () => {
|
||||
if (props.selectCode && !loading.value) {
|
||||
queryBounds(String(props.selectCode).padEnd(6, '0'))
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -243,62 +75,17 @@ const cancel = () => {
|
|||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.map-tool{
|
||||
.region-map-loading {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
right: 20px;
|
||||
|
||||
|
||||
.map-tool-content {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background-color: yellow;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
|
||||
.tool-item-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e3e3e3;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 16px rgba(#000, .15);
|
||||
|
||||
.tool-item {
|
||||
padding: 4px 6px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--ant-primary-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed !important;
|
||||
background-color: #efefef;
|
||||
|
||||
> span {
|
||||
cursor: not-allowed !important;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(#000, 0.35);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
<j-tree-select
|
||||
showSearch
|
||||
placeholder="1级区域不需要选择"
|
||||
:tree-data="builtInAreaList"
|
||||
:tree-data="areaTree"
|
||||
:value="_value"
|
||||
:field-names="{
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'code',
|
||||
value: 'code'
|
||||
}"
|
||||
tree-node-filter-prop="name"
|
||||
@select="onSelect"
|
||||
|
@ -17,16 +16,15 @@
|
|||
</template>
|
||||
</j-tree-select>
|
||||
<j-checkbox
|
||||
@change="onCheckChange"
|
||||
v-model:checked="_checked"
|
||||
@change="onCheckChange"
|
||||
style="margin-top: 5px"
|
||||
>同步添加下一级区域</j-checkbox
|
||||
>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getBuiltinRegionTree } from '@/api/system/region';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
|
@ -41,28 +39,49 @@ const props = defineProps({
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
areaTree: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:value', 'update:name', 'update:children']);
|
||||
|
||||
const features = ref<any>({});
|
||||
const _value = ref<string>();
|
||||
const builtInAreaList = ref<Record<string, any>[]>([]);
|
||||
const _checked = ref<boolean>(false);
|
||||
const _checked = ref<boolean>(props.children?.length ?? false);
|
||||
|
||||
const queryBuiltinRegionTree = async () => {
|
||||
const resp = await getBuiltinRegionTree({
|
||||
paging: false,
|
||||
sorts: [{ name: 'sortIndex', order: 'asc' }],
|
||||
});
|
||||
if (resp.success) {
|
||||
builtInAreaList.value = resp?.result || [];
|
||||
|
||||
const findChildren = (data: any, code: string) => {
|
||||
let children: any[] = []
|
||||
|
||||
data.some((item: any) => {
|
||||
if (item.code === code) {
|
||||
children = item.children
|
||||
return true
|
||||
}
|
||||
};
|
||||
|
||||
if (item.children) {
|
||||
children = findChildren(item.children, code)
|
||||
return !!children.length
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
const onCheckChange = (e: any) => {
|
||||
console.log('e',props.children, e.target.checked)
|
||||
if (e.target.checked) {
|
||||
emits('update:children', features.value?.children || []);
|
||||
const children = features.value?.children ? features.value?.children : findChildren(props.areaTree, _value.value)
|
||||
emits('update:children', children.map((item, index) => {
|
||||
if (!item.sortIndex) {
|
||||
item.sortIndex = index + 1
|
||||
}
|
||||
return item
|
||||
}));
|
||||
} else {
|
||||
emits('update:children', []);
|
||||
}
|
||||
|
@ -87,13 +106,19 @@ const getObj = (node: any): any => {
|
|||
const onSelect = (val: string, node: any) => {
|
||||
features.value = getObj(node);
|
||||
_value.value = val;
|
||||
|
||||
emits('update:name', features.value?.name);
|
||||
emits('update:value', features.value?.code);
|
||||
|
||||
if (_checked.value) {
|
||||
emits('update:children', node?.children.map(item => ({
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
parentId: item.parentId,
|
||||
})));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryBuiltinRegionTree();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
|
@ -110,15 +135,4 @@ watch(
|
|||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.children,
|
||||
() => {
|
||||
_checked.value = !!props.children?.length
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
|
@ -1,45 +0,0 @@
|
|||
<template>
|
||||
<j-button @click="onClick" v-if="!_value.length" type="link"
|
||||
>请在地图上描点</j-button
|
||||
>
|
||||
<div v-else>已完成描点<j-button type="link" @click="onEdit">编辑</j-button></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, inject } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:value', 'close']);
|
||||
|
||||
const _value = ref<any[]>([]);
|
||||
|
||||
const __data: any = inject('system-region')
|
||||
|
||||
const onClick = () => {
|
||||
console.log(__data)
|
||||
__data.type.value = 'edit'
|
||||
emits('close')
|
||||
};
|
||||
|
||||
const onEdit = () => {
|
||||
// __data.type.value = 'edit'
|
||||
// emits('close')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
console.log(props.value)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
|
@ -4,46 +4,34 @@
|
|||
width="650px"
|
||||
:visible="true"
|
||||
:title="mode === 'edit' ? '编辑区域' : '新增区域'"
|
||||
@ok="handleSave"
|
||||
@cancel="handleCancel"
|
||||
:confirmLoading="loading"
|
||||
@ok="handleSave"
|
||||
@cancel="() => { handleCancel() }"
|
||||
>
|
||||
<div style="margin-top: 10px">
|
||||
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
|
||||
<j-form-item name="parentId" label="上级区域">
|
||||
<j-tree-select
|
||||
showSearch
|
||||
allowClear
|
||||
v-model:value="modelRef.parentId"
|
||||
placeholder="1级区域不需要选择"
|
||||
:tree-data="areaList"
|
||||
allowClear
|
||||
:field-names="{
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
}"
|
||||
tree-node-filter-prop="name"
|
||||
@change="treeSelect"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item :name="['properties', 'type']" label="添加方式">
|
||||
<j-radio-group
|
||||
v-model:value="modelRef.properties.type"
|
||||
button-style="solid"
|
||||
@change="onChange"
|
||||
>
|
||||
<j-radio-button value="builtin"
|
||||
>内置行政区</j-radio-button
|
||||
>
|
||||
<j-radio-button value="Custom"
|
||||
>自定义数据</j-radio-button
|
||||
>
|
||||
</j-radio-group>
|
||||
</j-form-item>
|
||||
<j-form-item v-if="modelRef.properties.type === 'builtin'">
|
||||
<j-form-item label="内置行政区" v-if="modelRef.properties.type === 'builtin'">
|
||||
<BuildIn
|
||||
v-model:value="modelRef.code"
|
||||
v-model:children="modelRef.children"
|
||||
v-model:name="modelRef.name"
|
||||
:areaTree="areaTree"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
|
@ -88,39 +76,41 @@
|
|||
<j-input-number
|
||||
v-model:value="modelRef.code"
|
||||
style="width: 100%"
|
||||
:disabled="modelRef.properties.type === 'builtin'"
|
||||
placeholder="请输入行政区划代码"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
v-if="modelRef.properties.type !== 'builtin'"
|
||||
label="区划划分"
|
||||
required
|
||||
name="features"
|
||||
>
|
||||
<TracePoint @close="emit('close')" v-model:value="modelRef.features" />
|
||||
</j-form-item>
|
||||
</j-form>
|
||||
</div>
|
||||
</j-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, reactive, PropType, onMounted } from 'vue';
|
||||
import TracePoint from './TracePoint.vue';
|
||||
<script lang="ts" setup name="Save">
|
||||
import {ref, watch, reactive} from 'vue';
|
||||
import type {PropType} from 'vue';
|
||||
import BuildIn from './BuildIn.vue';
|
||||
import {
|
||||
validateName,
|
||||
getRegionTree,
|
||||
validateCode,
|
||||
updateRegion,
|
||||
} from '@/api/system/region';
|
||||
import { onlyMessage } from '@/utils/comm';
|
||||
import {omit} from "lodash-es";
|
||||
import {onlyMessage} from "@/utils/comm";
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
areaTree: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String as PropType<'add' | 'edit'>,
|
||||
|
@ -142,6 +132,7 @@ const init = {
|
|||
properties: {
|
||||
type: 'builtin',
|
||||
},
|
||||
sortIndex: props.data.sortIndex || 1
|
||||
};
|
||||
|
||||
const modelRef = reactive(init);
|
||||
|
@ -150,11 +141,11 @@ watch(
|
|||
() => props.data,
|
||||
() => {
|
||||
Object.assign(modelRef, {});
|
||||
if (props.mode === 'add' && props.data?.id) {
|
||||
if (props.mode === 'add') {
|
||||
// 添加子
|
||||
Object.assign(modelRef, {
|
||||
...init,
|
||||
parentId: props.data.id,
|
||||
...props.data,
|
||||
});
|
||||
} else if (props.mode === 'edit') {
|
||||
// 编辑
|
||||
|
@ -163,22 +154,60 @@ watch(
|
|||
Object.assign(modelRef, init);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
{immediate: true, deep: true},
|
||||
);
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('close');
|
||||
const handleCancel = (data: any) => {
|
||||
emit('close', data);
|
||||
};
|
||||
|
||||
const traceEdit = () => {
|
||||
const newData: any = {
|
||||
...props.data,
|
||||
...modelRef,
|
||||
}
|
||||
|
||||
handleCancel(newData)
|
||||
}
|
||||
|
||||
const treeSelect = (id: string, label: string, extra: any) => {
|
||||
let children: any[]
|
||||
if (extra) {
|
||||
children = extra?.triggerNode?.props.children || []
|
||||
} else {
|
||||
children = props.treeData
|
||||
}
|
||||
const lastItem = children.length ? children[children.length-1] : {}
|
||||
modelRef.sortIndex = lastItem.sortIndex ? lastItem.sortIndex + 1 : 1
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async (_data: any) => {
|
||||
loading.value = true;
|
||||
const resp = await updateRegion({
|
||||
...props.data,
|
||||
const newData: any = {
|
||||
...omit(props.data, ['parentFullName', 'parentId']),
|
||||
...modelRef,
|
||||
}).finally(() => {
|
||||
}
|
||||
newData.fullName = props.data.parentFullName ? props.data.parentFullName + modelRef.name : modelRef.name
|
||||
newData.parentId = newData.parentId || ''
|
||||
|
||||
const arr = areaList.value.map(item=>item.code)
|
||||
|
||||
if (newData.children?.length) {
|
||||
newData.children = newData.children.map(item => {
|
||||
if (!item.fullName) {
|
||||
item.fullName = newData.fullName + item.name
|
||||
}
|
||||
item = {
|
||||
...item,
|
||||
children: []
|
||||
}
|
||||
return item
|
||||
}).filter(item =>!arr.includes(item.code))
|
||||
}
|
||||
loading.value = true;
|
||||
const resp = await updateRegion(newData).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
|
@ -189,13 +218,13 @@ const handleSave = () => {
|
|||
.catch((err: any) => {
|
||||
console.log('error', err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const vailName = async (_: Record<string, any>, value: string) => {
|
||||
if (!props?.data?.id && value) {
|
||||
const resp = await validateName(value, props.data.id);
|
||||
if (resp.status === 200 && !resp.result?.passed) {
|
||||
return Promise.reject(resp.result?.reason || '区域名称重复');
|
||||
return Promise.reject('区域名称重复');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -208,7 +237,7 @@ const vailCode = async (_: Record<string, any>, value: string) => {
|
|||
if (!props?.data?.id && value) {
|
||||
const resp = await validateCode(value, props.data.id);
|
||||
if (resp.status === 200 && !resp.result?.passed) {
|
||||
return Promise.reject(resp.result?.reason || '行政区域代码重复');
|
||||
return Promise.reject('行政区域代码重复');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -221,14 +250,11 @@ const onChange = () => {
|
|||
modelRef.features = undefined;
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const resp = await getRegionTree();
|
||||
if (resp.success) {
|
||||
areaList.value = resp?.result || [];
|
||||
watch(() => JSON.stringify(props.treeData), () => {
|
||||
const item = JSON.parse(JSON.stringify(props.treeData))
|
||||
areaList.value = item
|
||||
if(props.mode === 'add'){
|
||||
modelRef.children = props.areaTree?.[0]?.children
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
}, {immediate: true})
|
||||
</script>
|
|
@ -0,0 +1,51 @@
|
|||
import { getBuiltinRegionTree } from '@/api/system/region';
|
||||
import {useRequest} from "@/hook";
|
||||
|
||||
export const useArea = () => {
|
||||
const areaMap = ref(new Map())
|
||||
|
||||
|
||||
const handleAreaMap = (data: any[] = [], parentCode?: string) => {
|
||||
for (let i = 0;i < data.length; i++) {
|
||||
const _data = data[i]
|
||||
areaMap.value.set(_data.code, {
|
||||
parentId: parentCode,
|
||||
name: _data.name
|
||||
})
|
||||
if (_data.children?.length) {
|
||||
handleAreaMap(_data.children, _data.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data: areaTree } = useRequest(getBuiltinRegionTree, {
|
||||
onSuccess(resp) {
|
||||
handleAreaMap(resp?.result)
|
||||
},
|
||||
defaultParams: [{
|
||||
paging: false,
|
||||
sorts: [{ name: 'sortIndex', order: 'asc' }],
|
||||
}]
|
||||
})
|
||||
|
||||
const getParentNameById = (id?: string) => {
|
||||
let _id = id
|
||||
const name = []
|
||||
|
||||
while (!!_id) {
|
||||
const item = areaMap.value.get(_id)
|
||||
if (item) { // 不添加本身
|
||||
name.unshift(item.name)
|
||||
}
|
||||
_id = item?.parentId
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
return {
|
||||
areaTree,
|
||||
areaMap,
|
||||
getParentNameById
|
||||
}
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<full-page>
|
||||
<full-page fixed>
|
||||
<div class="region">
|
||||
<div class="left">
|
||||
<LeftTree />
|
||||
<div class="mask" v-if="type === 'edit'"></div>
|
||||
<LeftTree @select="onSelect" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<Map :path="path" :type="mapType" />
|
||||
<Map ref="mapRef" :selectCode="selectCode" />
|
||||
</div>
|
||||
</div>
|
||||
</full-page>
|
||||
|
@ -17,18 +16,13 @@
|
|||
<script setup lang="ts" name="RegionMange">
|
||||
import LeftTree from './LeftTree/index.vue'
|
||||
import Map from './MapTool/map.vue'
|
||||
import FullPage from "components/Layout/FullPage.vue";
|
||||
import { provide } from 'vue'
|
||||
import FullPage from "@/components/Layout/FullPage.vue";
|
||||
|
||||
const path = ref([[121.5273285, 31.21515044], [121.5293285, 31.21515044], [121.5293285, 31.21915044], [121.5273285, 31.21515044]])
|
||||
const type = ref<'view' | 'edit'>('view')
|
||||
const mapType = ref<string>('create')
|
||||
const selectCode = ref('')
|
||||
|
||||
provide('system-region', {
|
||||
type,
|
||||
mapType: '',
|
||||
path
|
||||
})
|
||||
const onSelect = (dt: string) => {
|
||||
selectCode.value = dt
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -57,7 +57,7 @@ import {
|
|||
getRoleDetails_api,
|
||||
updateRole_api,
|
||||
editRole_api,
|
||||
updatePrimissTree_api,
|
||||
updatePrimissTree_api, clearPrimissTree_api,
|
||||
} from '@/api/system/role';
|
||||
import { onlyMessage } from '@/utils/comm';
|
||||
|
||||
|
@ -85,6 +85,7 @@ const form = reactive({
|
|||
clickSave: () => {
|
||||
// formRef.value?.validate().then(() => {
|
||||
// const updateRole = editRole_api(roleId, form.data);
|
||||
if(form.menus?.length > 0){
|
||||
const updateTree = updatePrimissTree_api(roleId, {
|
||||
menus: form.menus,
|
||||
});
|
||||
|
@ -92,6 +93,13 @@ const form = reactive({
|
|||
onlyMessage('操作成功');
|
||||
// jumpPage(`system/Role`);
|
||||
})
|
||||
} else {
|
||||
clearPrimissTree_api(roleId).then(resp => {
|
||||
if(resp.success){
|
||||
onlyMessage('操作成功');
|
||||
}
|
||||
})
|
||||
}
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
>
|
||||
<PermissionButton
|
||||
type="text"
|
||||
style="padding: 0"
|
||||
hasPermission="system/Role:groupDelete"
|
||||
:popConfirm="{
|
||||
title: `确定要删除?`,
|
||||
|
|
|
@ -14,7 +14,10 @@
|
|||
model="TABLE"
|
||||
:params="queryParams"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
sorts: [
|
||||
{ name: 'createTime', order: 'desc' },
|
||||
{ name: 'username', order: 'asc', value: 'admin' },
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #headerTitle>
|
||||
|
@ -31,9 +34,13 @@
|
|||
</template>
|
||||
<template #roleList="slotProps">
|
||||
<j-ellipsis>
|
||||
{{ slotProps?.roleList?.map((item)=>{
|
||||
return item.name
|
||||
}).join(',') }}
|
||||
{{
|
||||
slotProps?.roleList
|
||||
?.map((item) => {
|
||||
return item.name;
|
||||
})
|
||||
.join(',')
|
||||
}}
|
||||
</j-ellipsis>
|
||||
</template>
|
||||
<template #status="slotProps">
|
||||
|
@ -101,11 +108,10 @@
|
|||
: '删除',
|
||||
}"
|
||||
:popConfirm="{
|
||||
title:'确认删除?',
|
||||
title: '确认删除?',
|
||||
onConfirm: () =>
|
||||
table.clickDel(slotProps.id),
|
||||
}"
|
||||
|
||||
:disabled="slotProps.status"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
|
@ -134,7 +140,7 @@ import {
|
|||
getUserList_api,
|
||||
changeUserStatus_api,
|
||||
deleteUser_api,
|
||||
queryRole_api
|
||||
queryRole_api,
|
||||
} from '@/api/system/user';
|
||||
import { onlyMessage } from '@/utils/comm';
|
||||
|
||||
|
@ -184,28 +190,26 @@ const columns = [
|
|||
title: '角色',
|
||||
dataIndex: 'roleList',
|
||||
key: 'roleList',
|
||||
search:{
|
||||
type:'select',
|
||||
search: {
|
||||
type: 'select',
|
||||
// rename:'id$in-dimension$role',
|
||||
options:() =>
|
||||
new Promise((resolve)=>{
|
||||
queryRole_api(
|
||||
{
|
||||
paging:false,
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
queryRole_api({
|
||||
paging: false,
|
||||
sorts: [
|
||||
{ name: 'createTime', order: 'desc' },
|
||||
{ name: 'id', order: 'desc' },
|
||||
]
|
||||
}
|
||||
).then((resp:any)=>{
|
||||
],
|
||||
}).then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: dictType) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
})
|
||||
})
|
||||
});
|
||||
}),
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
|
@ -304,13 +308,15 @@ type modalType = '' | 'add' | 'edit' | 'reset';
|
|||
|
||||
const handleParams = (params: any) => {
|
||||
const newParams = (params?.terms as any[])?.map((termsGroupA) => {
|
||||
let arr: any[] = []
|
||||
let arr: any[] = [];
|
||||
termsGroupA.terms = termsGroupA.terms.map((termsItem: any) => {
|
||||
|
||||
if (termsItem.column === 'id$in-dimension$role') {
|
||||
let _termType = termsItem.termType === 'nin' ? 'not$in' : termsItem.termType
|
||||
termsItem.column = `${termsItem.column}$${_termType}`
|
||||
delete termsItem.termType
|
||||
let _termType =
|
||||
termsItem.termType === 'nin'
|
||||
? 'not$in'
|
||||
: termsItem.termType;
|
||||
termsItem.column = `${termsItem.column}$${_termType}`;
|
||||
delete termsItem.termType;
|
||||
}
|
||||
if (['telephone', 'email'].includes(termsItem.column)) {
|
||||
return {
|
||||
|
@ -318,7 +324,10 @@ const handleParams = (params: any) => {
|
|||
value: [termsItem],
|
||||
};
|
||||
}
|
||||
if (['type'].includes(termsItem.column) && termsItem.value === 'other') {
|
||||
if (
|
||||
['type'].includes(termsItem.column) &&
|
||||
termsItem.value === 'other'
|
||||
) {
|
||||
arr = [
|
||||
{
|
||||
...termsItem,
|
||||
|
@ -331,8 +340,26 @@ const handleParams = (params: any) => {
|
|||
type: 'or',
|
||||
termType: 'empty',
|
||||
value: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (termsItem.column === 'roleList') {
|
||||
if (
|
||||
termsItem.termType === 'eq' ||
|
||||
termsItem.termType === 'in'
|
||||
) {
|
||||
return {
|
||||
column: 'id$in-dimension$role',
|
||||
type: termsItem.type,
|
||||
value: termsItem.value,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
column: 'id$in-dimension$role$not',
|
||||
type: termsItem.type,
|
||||
value: termsItem.value,
|
||||
};
|
||||
}
|
||||
]
|
||||
}
|
||||
if(termsItem.column === 'roleList'){
|
||||
if(termsItem.termType === 'eq' || termsItem.termType === 'in'){
|
||||
|
@ -352,8 +379,8 @@ const handleParams = (params: any) => {
|
|||
return termsItem;
|
||||
});
|
||||
|
||||
if(arr.length){
|
||||
termsGroupA.terms = [...termsGroupA.terms, ...arr]
|
||||
if (arr.length) {
|
||||
termsGroupA.terms = [...termsGroupA.terms, ...arr];
|
||||
}
|
||||
|
||||
return termsGroupA;
|
||||
|
|
|
@ -98,7 +98,7 @@ export default defineConfig(({ mode}) => {
|
|||
// target: 'http://120.77.179.54:8844', // 120测试
|
||||
target: 'http://192.168.33.46:8844', // 本地开发环境
|
||||
// target: 'http://192.168.32.167:8844', // 本地开发环境1
|
||||
// target: 'http://192.168.33.1:8848', // 社区版开发环境
|
||||
// target: 'http://192.168.33.6:8848', // 社区版开发环境
|
||||
// target: 'http://192.168.32.207:8844', // 刘本地
|
||||
// target: 'http://192.168.32.187:8844', // 谭本地
|
||||
// target: 'http://192.168.33.66:8844', // 苟本地
|
||||
|
|
Loading…
Reference in New Issue