feat: 区域管理新增

This commit is contained in:
100011797 2023-10-25 18:04:34 +08:00
parent d5fe1702d2
commit 6b2360cac1
6 changed files with 466 additions and 153 deletions

22
src/api/system/region.ts Normal file
View File

@ -0,0 +1,22 @@
import server from '@/utils/request';
// 获取全部地区(树结构)
export const getRegionTree = (): Promise<any> => server.post(`/area/_all/tree`);
// 校验名称是否存在
export const validateName = (name: string, id?: string): Promise<any> => server.get(`/area/name/_validate?name=${name}${id ? `&id=${id}` : ''}`);
//校验行政区划代码是否存在
export const validateCode = (code: string, id?: string): Promise<any> => server.get(`/area/code/_validate?code=${code}${id ? `&id=${id}` : ''}`);
// 删除
export const delRegion = (id: string): Promise<any> => server.remove(`/area/${id}`);
// 保存
export const saveRegion = (data: any): Promise<any> => server.post(`/area`, data);
// 更新
export const updateRegion = (data: any): Promise<any> => server.patch(`/area`, data);
// 获取全部内置地区(树结构)
export const getBuiltinRegionTree = (data: any): Promise<any> => server.post(`/area/builtin/_all/tree`, data);

View File

@ -1,92 +1,237 @@
<template>
<j-input
placeholder="请输入区域名称或行政区划代码"
class="search-input"
v-model:value="searchValue"
@change="(e) => onSearch(e.target.value)"
>
<template #prefix>
<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
class="draggable-tree"
draggable
block-node
v-if="treeData.length"
:tree-data="treeData"
@dragenter="onDragEnter"
:tree-data="_treeData"
@drop="onDrop"
/>
:defaultExpandAll="true"
:height="700"
:show-line="{ showLeafIcon: false }"
:show-icon="true"
>
<template #title="_data">
<div class="tree-box">
<div class="name">
<j-ellipsis>{{ _data?.name }}</j-ellipsis>
</div>
<div class="actions">
<j-space :size="8">
<j-tooltip title="重命名">
<j-button
@click.stop="onEdit(_data?.data)"
class="actions-btn"
type="link"
><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>
</j-tooltip>
<j-tooltip title="删除">
<j-popconfirm @confirm="onRemove(_data?.id)">
<j-button
@click.stop
class="actions-btn"
type="link"
danger
><AIcon type="DeleteOutlined"
/></j-button>
</j-popconfirm>
</j-tooltip>
</j-space>
</div>
</div>
</template>
</j-tree>
<j-empty v-else style="margin-top: 150px" />
<Save
:mode="mode"
v-if="visible"
:data="current"
@save="onSave"
@close="onClose"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onlyMessage } from '@/utils/comm';
import { debounce } from 'lodash-es';
import { onMounted, ref, watch } from 'vue';
import Save from '../Save/index.vue';
import { getRegionTree, delRegion } from '@/api/system/region';
const treeData = ref([]);
const treeData = ref<any[]>([]);
const _treeData = ref<any[]>([]);
const visible = ref<boolean>(false);
const current = ref<any>({});
const mode = ref<'add' | 'edit'>('add');
const searchValue = ref<string>();
const onDragEnter = (info) => {
// console.log(info);
const filterTreeNodes = (tree: any[], condition: string) => {
return tree.filter((item) => {
if (item?.title && item.title.includes(condition)) {
return true;
}
if (item?.code && item.code.includes(condition)) {
return true;
}
if (item.children) {
item.children = filterTreeNodes(item.children, condition);
return !!item.children.length;
}
return false;
});
};
const onDrop = (info) => {
// console.log(info);
// const dropKey = info.node.key;
// const dragKey = info.dragNode.key;
// const dropPos = info.node.pos.split('-');
// const dropPosition =
// info.dropPosition - Number(dropPos[dropPos.length - 1]);
// const loop = (data: any, key: string | number, callback: any) => {
// data.forEach((item, index) => {
// if (item.key === key) {
// return callback(item, index, data);
// }
// if (item.children) {
// return loop(item.children, key, callback);
// }
// });
// };
// const data = [...gData.value];
const onSearch = debounce((v: string) => {
_treeData.value = filterTreeNodes(treeData.value, v);
});
const onSave = () => {};
const onClose = () => {
visible.value = false;
};
const onEdit = (_data: any) => {
mode.value = 'edit';
current.value = _data;
visible.value = true;
};
const onRemove = async (id: string) => {
const resp = await delRegion(id);
if (resp.success) {
onlyMessage('操作成功!');
handleSearch();
}
};
const onAdd = (_data?: any) => {
mode.value = 'add';
current.value = _data ? _data : {};
visible.value = true;
};
const onDrop = (info: any) => {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition =
info.dropPosition - Number(dropPos[dropPos.length - 1]);
const loop = (data: any, key: string | number, callback: any) => {
data.forEach((item: any, index: number) => {
if (item.key === key) {
return callback(item, index, data);
}
if (item.children) {
return loop(item.children, key, callback);
}
});
};
const data = [...treeData.value];
// // Find dragObject
// let dragObj: TreeDataItem;
// loop(
// data,
// dragKey,
// (item: TreeDataItem, index: number, arr: TreeProps['treeData']) => {
// arr.splice(index, 1);
// dragObj = item;
// },
// );
// if (!info.dropToGap) {
// // Drop on the content
// loop(data, dropKey, (item: TreeDataItem) => {
// item.children = item.children || [];
// /// where to insert
// item.children.unshift(dragObj);
// });
// } else if (
// (info.node.children || []).length > 0 && // Has children
// info.node.expanded && // Is expanded
// dropPosition === 1 // On the bottom gap
// ) {
// loop(data, dropKey, (item: TreeDataItem) => {
// item.children = item.children || [];
// // where to insert
// item.children.unshift(dragObj);
// });
// } else {
// let ar: TreeProps['treeData'] = [];
// let i = 0;
// loop(
// data,
// dropKey,
// (
// _item: TreeDataItem,
// index: number,
// arr: TreeProps['treeData'],
// ) => {
// ar = arr;
// i = index;
// },
// );
// if (dropPosition === -1) {
// ar.splice(i, 0, dragObj);
// } else {
// ar.splice(i + 1, 0, dragObj);
// }
// }
// gData.value = data;
let dragObj: any;
loop(data, dragKey, (item: any, index: number, arr: any[]) => {
arr.splice(index, 1);
dragObj = item;
});
if (!info.dropToGap) {
// Drop on the content
loop(data, dropKey, (item: any) => {
item.children = item.children || [];
/// where to insert
item.children.unshift(dragObj);
});
} else if (
(info.node.children || []).length > 0 && // Has children
info.node.expanded && // Is expanded
dropPosition === 1 // On the bottom gap
) {
loop(data, dropKey, (item: any) => {
item.children = item.children || [];
// where to insert
item.children.unshift(dragObj);
});
} else {
let ar: any[] = [];
let i = 0;
loop(data, dropKey, (_item: any, index: number, arr: any[]) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
} else {
ar.splice(i + 1, 0, dragObj);
}
}
treeData.value = data;
};
watch(
() => treeData.value,
() => {
if (searchValue.value) {
onSearch(searchValue.value);
} else {
_treeData.value = treeData.value;
}
},
{
deep: true,
immediate: true,
},
);
const handleSearch = async () => {
const resp = await getRegionTree();
if (resp.success) {
treeData.value = resp?.result || [];
}
};
onMounted(() => {
handleSearch();
});
</script>
<style lang="less" scoped>
.btn {
width: 100%;
margin: 18px 0;
}
.tree-box {
display: flex;
justify-content: space-between;
align-items: center;
.actions {
.actions-btn {
margin: 0;
padding: 0;
}
}
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<j-tree-select
showSearch
placeholder="1级区域不需要选择"
:tree-data="builtInAreaList"
:value="_value"
:field-names="{
children: 'children',
label: 'name',
value: 'code',
}"
tree-node-filter-prop="name"
@select="onSelect"
>
<template #title="{ name, code }">
<span v-if="code">{{ name }} | {{ code }}</span>
</template>
</j-tree-select>
<j-checkbox
@change="onCheckChange"
v-model:checked="_checked"
style="margin-top: 5px"
>同步添加下一级区域</j-checkbox
>
</template>
<script lang="ts" setup>
import { getBuiltinRegionTree } from '@/api/system/region';
import { omit } from 'lodash-es';
import { onMounted, ref } from 'vue';
const props = defineProps({
value: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['update:value']);
const features = ref<any>();
const _value = ref<string>('100000')
const builtInAreaList = ref<Record<string, any>[]>([]);
const _checked = ref<boolean>(false);
const queryBuiltinRegionTree = async () => {
const resp = await getBuiltinRegionTree({
paging: false,
sorts: [{ name: 'sortIndex', order: 'asc' }],
});
if (resp.success) {
builtInAreaList.value = resp?.result || [];
}
};
const onCheckChange = (e: any) => {
if (e.target.checked) {
emits('update:value', features.value);
} else {
emits('update:value', omit(features.value, 'children'));
}
};
const onSelect = (val: string, node: any) => {
features.value = node;
_value.value = val
if (_checked.value) {
emits('update:value', node);
} else {
emits('update:value', omit(node, 'children'));
}
};
onMounted(() => {
queryBuiltinRegionTree();
});
</script>

View File

@ -0,0 +1,21 @@
<template>
<j-button v-if="_value.length" type="link">请在地图上描点</j-button>
<div v-else>
已完成描点<j-button type="link">编辑</j-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const props = defineProps({
value: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:value'])
const _value = ref<any[]>([])
</script>

View File

@ -10,95 +10,136 @@
>
<div style="margin-top: 10px">
<j-form :layout="'vertical'" ref="formRef" :model="modelRef">
<j-form-item name="productId" label="上级区域">
<j-select
<j-form-item name="parentId" label="上级区域">
<j-tree-select
showSearch
v-model:value="modelRef.productId"
v-model:value="modelRef.parentId"
placeholder="1级区域不需要选择"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
:tree-data="areaList"
allowClear
:field-names="{
children: 'children',
label: 'name',
value: 'id',
}"
tree-node-filter-prop="name"
/>
</j-form-item>
<j-form-item name="type" label="添加方式">
<j-radio-group
v-model:value="modelRef.type"
button-style="solid"
@change="onChange"
>
<a-radio-button value="builtIn"
>内置行政区</a-radio-button
>
<a-radio-button value="Custom"
>自定义数据</a-radio-button
>
<a-radio-button value="a">内置行政区</a-radio-button>
<a-radio-button value="b">自定义数据</a-radio-button>
</j-radio-group>
</j-form-item>
<j-form-item>
<j-select
showSearch
v-model:value="modelRef.productId"
placeholder="1级区域不需要选择"
>
<j-select-option
:value="item.id"
v-for="item in productList"
:key="item.id"
:label="item.name"
>{{ item.name }}</j-select-option
>
</j-select>
<j-checkbox v-model:checked="modelRef.productId">同步添加下一级区域</j-checkbox>
<j-form-item v-if="modelRef.type === 'builtIn'">
<BuildIn v-model:value="modelRef.features" />
</j-form-item>
<j-form-item
label="区域名称"
name="name"
required
:rules="[
{
max: 200,
message: '最多输入200个字符',
required: true,
message: '请输入区域名称',
},
{
max: 64,
message: '最多输入64个字符',
},
{
validator: vailName,
trigger: 'blur',
},
]"
>
<j-textarea
v-model:value="modelRef.describe"
placeholder="请输入说明"
showCount
:maxlength="200"
<j-input
v-model:value="modelRef.name"
placeholder="请输入区域名称"
/>
</j-form-item>
<j-form-item
label="行政区划代码"
name="code"
required
:rules="[
{
required: true,
message: '请输入行政区划代码',
},
{
validator: vailCode,
trigger: 'blur',
},
]"
>
<j-input-number
v-model:value="modelRef.code"
style="width: 100%"
placeholder="请输入行政区划代码"
/>
</j-form-item>
<j-form-item
v-if="modelRef.type !== 'builtIn'"
label="区划划分"
required
name="features"
>
<TracePoint v-model:value="modelRef.features" />
</j-form-item>
</j-form>
</div>
</j-modal>
</template>
<script lang="ts" setup>
import { ref, watch, reactive } from 'vue';
import { ref, watch, reactive, PropType, onMounted } from 'vue';
import TracePoint from './TracePoint.vue';
import BuildIn from './BuildIn.vue';
import {
validateName,
getRegionTree,
validateCode,
} from '@/api/system/region';
const emit = defineEmits(['close', 'save']);
const props = defineProps({
data: {
type: Object,
default: undefined,
default: () => {},
},
mode: {
type: String as PropType<'add' | 'edit'>,
default: 'add',
},
});
const productList = ref<Record<string, any>[]>([]);
const areaList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const formRef = ref();
const modelRef = reactive({
productId: undefined,
parentId: undefined,
id: undefined,
name: '',
describe: '',
type: 'a',
name: undefined,
code: undefined,
type: 'builtIn',
features: undefined,
});
watch(
() => props.data,
(newValue) => {
Object.assign(modelRef, newValue);
() => {
console.log(props.data);
Object.assign(modelRef, props.data);
},
{ immediate: true, deep: true },
);
@ -117,4 +158,46 @@ const handleSave = () => {
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) {
return Promise.reject('区域名称重复');
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
};
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) {
return Promise.reject('行政区域代码重复');
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
};
const onChange = () => {
// console.log(e.target.value)
modelRef.features = undefined
}
const handleSearch = async () => {
const resp = await getRegionTree();
if (resp.success) {
areaList.value = resp?.result || [];
}
};
onMounted(() => {
handleSearch();
});
</script>

View File

@ -3,12 +3,6 @@
<full-page>
<div class="region">
<div class="left">
<j-input-search
v-model:value="searchValue"
placeholder="请输入区域名称或行政区划代码"
@search="onSearch"
/>
<j-button @click="onAdd" type="primary" class="btn">新增区域</j-button>
<LeftTree />
</div>
<div class="right">
@ -16,36 +10,13 @@
</div>
</div>
</full-page>
<Save v-if="visible" :data="current" @save="onSave" @close="onClose" />
</page-container>
</template>
<script setup lang="ts" name="RegionMange">
import LeftTree from './LeftTree/index.vue'
import Save from './Save/index.vue'
import Map from './MapTool/map.vue'
import FullPage from "components/Layout/FullPage.vue";
const searchValue = ref()
const visible = ref<boolean>(false)
const current = ref<any>({})
const onSearch = () => {
}
const onAdd = () => {
visible.value = true
current.value = {}
}
const onSave = () => {
}
const onClose = () => {
visible.value = false
}
</script>
<style lang="less" scoped>
@ -57,12 +28,6 @@ const onClose = () => {
.left {
width: 300px;
.btn {
width: 100%;
margin: 18px 0;
}
}
.right {
flex: 1;