feat(superManagement): 新增行政区划树组件并集成到表单中

- 新增 AdministrativeDivisionTree组件用于显示行政区划树
- 在 FormProMax 组件中集成行政区划树组件
- 修改 PoliceUnit 页面,将行政区划选择改为使用新的行政区划树组件
- 优化测试页面,添加行政区划树组件的测试
This commit is contained in:
luozhun 2024-10-31 09:45:28 +08:00
parent 5fe3af2758
commit b70ab10f95
26 changed files with 236 additions and 64 deletions

View File

@ -46,6 +46,11 @@ public class TreeNodeVo<T> implements Serializable {
*/
@Schema(description = "节点名称")
private String label;
/**
* 是否是叶子节点
*/
@Schema(description = "是否是叶子节点")
private Boolean isLeaf;
/**
* 排序
*/

View File

@ -58,10 +58,4 @@ public class RsaUtil {
}
}
public static void main(String[] args) {
RSA rs = new RSA();
System.out.println(rs.getPublicKeyBase64());
System.out.println(rs.getPrivateKeyBase64());
}
}

View File

@ -63,16 +63,16 @@ public class WebConfig implements WebMvcConfigurer {
.addPathPatterns("/open/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "OPTION", "PUT", "DELETE")
.allowedHeaders("Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
"Access-Control-Request-Headers", "Authorization","Token","*")
.allowCredentials(true)
.maxAge(3600);
}
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOriginPatterns("*")
// .allowedMethods("GET", "POST", "OPTION", "PUT", "DELETE")
// .allowedHeaders("Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
// "Access-Control-Request-Headers", "Authorization","Token","*")
// .allowCredentials(true)
// .maxAge(3600);
// }
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

View File

@ -1,6 +1,5 @@
package com.changhu.controller;
import cn.hutool.core.util.IdUtil;
import com.changhu.common.annotation.CheckOpenApi;
import com.changhu.common.annotation.JsonBody;
import com.changhu.common.enums.OpenApiType;

View File

@ -23,4 +23,12 @@ public interface AdministrativeDivisionMapper extends BaseMapper<AdministrativeD
* @return 结果
*/
List<TreeNodeVo<String>> treeList(@Param("level") Integer level);
/**
* 根据父编码查询子级行政区划
*
* @param parentCode 父编码
* @return 结果
*/
List<TreeNodeVo<String>> administrativeDivisionByParentCode(@Param("parentCode") String parentCode);
}

View File

@ -25,15 +25,6 @@ public class AdministrativeDivisionServiceImpl extends ServiceImpl<Administrativ
@Override
public List<TreeNodeVo<String>> administrativeDivisionByParentCode(String parentCode) {
return this.lambdaQuery()
.eq(AdministrativeDivision::getParentCode, parentCode)
.list()
.stream()
.map(item -> TreeNodeVo.<String>builder()
.value(item.getCode())
.label(item.getName())
.parentValue(item.getParentCode())
.build())
.toList();
return baseMapper.administrativeDivisionByParentCode(parentCode);
}
}

View File

@ -2,16 +2,24 @@ package com.changhu.support.minio;
import io.minio.MinioClient;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* fileName: MinioProperties
* author: LuoZhun
* createTime: 2023/11/10 17:26
* description: some...
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
@ -39,9 +47,43 @@ public class MinioProperties {
@Bean
public MinioClient minioClient() {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// Do nothing (trust any client certificate)
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// Do nothing (trust any server certificate)
}
}
};
// Install the all-trusting trust manager
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
} catch (Exception e) {
log.error("Install the all-trusting trust manager error:{}", e.getMessage());
}
// Create a custom OkHttpClient that trusts all certificates
OkHttpClient customHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
.hostnameVerifier((hostname, session) -> true)
.build();
return MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.httpClient(customHttpClient)
.build();
}
}

View File

@ -39,9 +39,9 @@ spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://118.253.177.137:3306/police_security_dev?serverTimezone=Asia/Shanghai&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: police_security_dev
password: GejDeCNj3ZBSNxSP
url: jdbc:mysql://118.253.177.137:3306/police_security?serverTimezone=Asia/Shanghai&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: police_security
password: xRxNWErNMKa6Th64
# druid 连接池管理
druid:
# 初始化时建立物理连接的个数
@ -119,7 +119,7 @@ minio:
url: https://www.hnjinglian.cn:9000
accessKey: admin
secretKey: lonsung301
bucketName: police-security-dev
bucketName: police-security
sa-token:
# token 名称(同时也是 cookie 名称)

View File

@ -12,4 +12,22 @@
<if test="level!=null">and level &lt;= #{level}</if>
order by code
</select>
<resultMap id="administrativeDivisionByParentCodeVoResultMap" type="com.changhu.common.pojo.vo.TreeNodeVo">
<result
column="extData"
typeHandler="com.baomidou.mybatisplus.extension.handlers.Fastjson2TypeHandler"
property="extData"/>
</resultMap>
<select id="administrativeDivisionByParentCode" resultMap="administrativeDivisionByParentCodeVoResultMap">
select ad1.code as 'value',
ad1.name as 'label',
ad1.parent_code as 'parentValue',
any_value(if(count(ad2.snow_flake_id) > 0, false, true)) as 'isLeaf'
from administrative_division ad1
left join administrative_division ad2 on ad1.code = ad2.parent_code
and ad2.delete_flag = 0
where ad1.delete_flag = 0
and ad1.parent_code = #{parentCode}
group by ad1.code
</select>
</mapper>

View File

@ -0,0 +1,28 @@
package com.changhu;
import com.changhu.module.management.pojo.params.ManagementSuperUserSaveOrUpdateParams;
import com.changhu.module.management.service.SuperService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author 20252
* @createTime 2024/10/30 上午11:23
* @desc SuperUserInitTest...
*/
@SpringBootTest
public class SuperUserInitTest {
@Autowired
private SuperService superService;
@Test
public void initAdmin() {
ManagementSuperUserSaveOrUpdateParams params = new ManagementSuperUserSaveOrUpdateParams();
params.setName("超级管理员");
params.setTelephone("15576404472");
superService.saveOrUpdateUser(params);
}
}

View File

@ -2,10 +2,10 @@ VITE_APP_NAME=超级后台
VITE_APP_ENV=development
VITE_APP_PORT=1000
VITE_DROP_CONSOLE=false
VITE_APP_MODULE_NAME=superManagement
# axios
VITE_APP_BASE_API=/api
# VITE_APP_PROXY_URL=http://localhost:8765
VITE_APP_PROXY_URL=http://172.10.10.93:8765
# rsa 公钥

View File

@ -2,11 +2,15 @@ VITE_APP_NAME=超级后台
VITE_APP_ENV=production
VITE_APP_PORT=1001
VITE_DROP_CONSOLE=true
VITE_APP_MODULE_NAME=superManagement
# axios
VITE_APP_BASE_API=/api
VITE_APP_PROXY_URL=https://172.10.10.238:8765
VITE_APP_PROXY_URL=http://118.253.177.137:8765
# rsa 公钥
VITE_APP_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpu1C3JHZ+Ng/eVVCZtwKsOZv9RktpAL13pKy4FoRHyNv2t8TPV2AMzLzfEzlWx001nBxyVxEMR2N9jAcqFLHv7r16ciOzbtzB9dky2G+bc9jIs4/EdVK5bAZcPRh5Jrb78sC9PHyR4AeceDyCIKHLUbWBJB4NTZE0s1Wh5kMynQIDAQAB
# minio
VITE_APP_MINIO_URL=http://118.253.177.137:9000
VITE_APP_MINIO_BUCKET=police-security-dev
VITE_APP_MINIO_URL=https://www.hnjinglian.cn:9000
VITE_APP_MINIO_BUCKET=police-security

View File

@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
dist
superManagement
dist-ssr
*.local

View File

@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"build": "vue-tsc -b && vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
@ -28,6 +28,7 @@
"@types/node": "^22.5.1",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"terser": "^5.36.0",
"typescript": "^5.5.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.1",

View File

@ -97,6 +97,12 @@
:allowClear="item.componentsProps?.allowClear ?? true"
:options="item.options"
/>
<administrative-division-tree-comp
v-else-if="item.type==='administrativeDivisionTree'"
style="width: 100%"
v-model:value="modelValue[field]"
v-bind="item.componentsProps"
/>
<a-range-picker
v-else-if="item.type ==='rangePicker'"
style="width: 100%"
@ -141,11 +147,14 @@
<script setup lang="ts" generic="T extends Record<string,any>">
import {FormInstance} from "ant-design-vue";
import {ref} from "vue";
import {defineAsyncComponent, ref} from "vue";
import {FormExpose} from "ant-design-vue/es/form/Form";
import {QuestionCircleOutlined} from '@ant-design/icons-vue'
import {FormProMaxItemOptions, FormProMaxItemProps, FormProMaxProps} from "@/types/components/form/index.ts";
import {ComponentProps} from "vue-component-type-helpers";
import AdministrativeDivisionTree from "@/components/tree/AdministrativeDivisionTree.vue";
const AdministrativeDivisionTreeComp: ComponentProps<typeof AdministrativeDivisionTree> = defineAsyncComponent(() => import("@/components/tree/AdministrativeDivisionTree.vue"));
const modelValue = defineModel<T>('value', {
default: {}

View File

@ -10,7 +10,7 @@
import {IconFontProps} from "@/types/components/iconfont/IconFont";
const props = withDefaults(defineProps<IconFontProps>(), {
withDefaults(defineProps<IconFontProps>(), {
size: 25,
type: "svg"
});

View File

@ -18,7 +18,7 @@
@click="router.push(item.path)"
>
<template #icon>
<icon-font :font-class="item.icon" :size="item.size"/>
<icon-font :font-class="item.icon"/>
<!-- <icon-font font-class="icon-guanlianbaoan" type="class" size="10"/>-->
</template>
<span class="margin-left-xs">{{ item.title }}</span>

View File

@ -14,7 +14,6 @@ import {computed} from "vue";
import {useRoute} from "vue-router";
import {SYSTEM_MENUS} from "@/config";
import MenuItem from "@/components/layout/MenuItem.vue";
import IconFont from "@/components/iconfont/IconFont.vue";
const route = useRoute()

View File

@ -39,7 +39,7 @@
<script lang="ts" setup>
import {ref} from 'vue'
import {FormInstance, message, notification} from "ant-design-vue";
import {FormInstance, notification} from "ant-design-vue";
import {Rule} from "ant-design-vue/es/form";
import {LoginParams} from "@/types/views/login.ts";
import api from "@/axios";

View File

@ -0,0 +1,81 @@
<template>
<a-cascader
v-model:value="modelValue"
:placeholder="placeholder"
:change-on-select="changeOnSelect"
:options="administrativeDivisionTree"
:load-data="loadData"
style="width: 500px"
:allow-clear="allowClear"
/>
</template>
<script setup lang="ts">
import api from "@/axios";
import {onMounted, ref} from "vue";
import {CascaderProps} from "ant-design-vue";
import {isEmpty} from "lodash-es";
withDefaults(defineProps<{
placeholder?: string,
changeOnSelect?: boolean
allowClear?: boolean
}>(), {
placeholder: '请选择行政区划',
changeOnSelect: true,
allowClear: true
})
const modelValue = defineModel('value', {
default: []
})
const administrativeDivisionTree = ref<TreeNodeVo<string>[]>([])
const loadData: CascaderProps['loadData'] = selectedOptions => {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
administrativeDivisionByParentCode(targetOption.value as string).then(data => {
targetOption.loading = false
targetOption.children = data
administrativeDivisionTree.value = [...administrativeDivisionTree.value]
})
}
/**
* 根据父级编码查询行政区划
* @param code
*/
const administrativeDivisionByParentCode = async (code: string = '0'): Promise<TreeNodeVo<string>[]> => {
const resp = await api.get<TreeNodeVo<string>[]>('/common/administrativeDivisionByParentCode', {
parentCode: code
})
return resp.data;
}
onMounted(async () => {
administrativeDivisionTree.value = await administrativeDivisionByParentCode()
if (!isEmpty(modelValue.value)) {
const ps = modelValue.value.map(code => administrativeDivisionByParentCode(code))
Promise.all(ps).then(data => {
let i = 0;
const deepChildren = (treeData: TreeNodeVo<string>[]) => {
treeData.forEach(item => {
if (item.value === modelValue.value[i]) {
item.children = data[i]
i++;
deepChildren(item.children)
}
})
}
deepChildren(administrativeDivisionTree.value)
})
}
})
</script>
<style scoped lang="scss">
</style>

View File

@ -10,10 +10,10 @@ import {ROUTER_WHITE_LIST} from "@/config";
* createWebHashHistory: 路径带# URL SEO
*/
const router = createRouter({
history: createWebHistory(),
history: createWebHistory(__APP_ENV.VITE_APP_MODULE_NAME),
routes: [...staticRouter],
strict: false,
scrollBehavior: () => ({left: 0, top: 0})
scrollBehavior: () => ({left: 0, top: 0}),
});
router.beforeEach(async (to, from, next) => {

View File

@ -15,6 +15,7 @@ import {
} from "ant-design-vue";
import {Ref, UnwrapRef, VNode} from "vue";
import {ComponentProps} from "vue-component-type-helpers";
import AdministrativeDivisionTree from "@/components/tree/AdministrativeDivisionTree.vue";
type FormProMaxItemType =
| 'custom'
@ -32,7 +33,8 @@ type FormProMaxItemType =
| 'datePicker'
| 'rangePicker'
| 'timeRangePicker'
| 'timePicker';
| 'timePicker'
| 'administrativeDivisionTree'
interface FormProMaxItemCommonProps extends ComponentProps<typeof FormItem> {
label?: string,
@ -64,6 +66,7 @@ export type FormProMaxItemOptions<T> = {
| FormProMaxItemProps<'rangePicker', ComponentProps<typeof RangePicker>>
| FormProMaxItemProps<'timeRangePicker', ComponentProps<typeof TimeRangePicker>>
| FormProMaxItemProps<'timePicker', ComponentProps<typeof TimePicker>>
| FormProMaxItemProps<'administrativeDivisionTree', ComponentProps<typeof AdministrativeDivisionTree>>
}
export interface FormProMaxProps<T = {}> extends FormProps {

View File

@ -1,15 +1,10 @@
<template>
{{ url}}
<SingleImageFileUpload
v-model:value="url"
/>
<administrative-division-tree/>
123123
</template>
<script setup lang="ts">
import SingleImageFileUpload from "@/components/upload/SingleImageFileUpload.vue";
import {ref} from "vue";
const url = ref<string>('')
import AdministrativeDivisionTree from "@/components/tree/AdministrativeDivisionTree.vue";
</script>
<style scoped lang="scss">

View File

@ -25,12 +25,9 @@ import {message, Modal} from "ant-design-vue";
import {UNIT_TYPE} from "@/config";
import {PageParams} from "@/types/hooks/useTableProMax.ts";
import {submitSimpleFormModal, deleteDataModal} from "@/components/tsx/ModalPro.tsx";
import useSelectAndTreeNodeVos from "@/hooks/useSelectAndTreeNodeVos.ts";
type TableProps = TableProMaxProps<PoliceUnitPagerVo, PoliceUnitPagerQueryParams>
const {administrativeDivisionTree} = useSelectAndTreeNodeVos('administrativeDivisionTree')
const tableRef = ref<ComponentExposed<typeof TableProMax>>(null!)
const reqApi: TableProps['requestApi'] = (params) => api.post('/management/super/policeUnit/pager', params)
const columns: TableProps['columns'] = [
@ -111,9 +108,8 @@ const searchFormOptions = ref<TableProps["searchFormOptions"]>({
type: 'input',
label: '代码'
}, administrativeDivisionCodes: {
type: 'cascader',
type: 'administrativeDivisionTree',
label: '行政区划',
options: administrativeDivisionTree
}, isEnable: {
type: 'select',
label: '是否启用',
@ -152,13 +148,9 @@ const showEnterprisesUnit = (policeUnitPagerVo: PoliceUnitPagerVo) => {
required: true
},
administrativeDivisionCodes: {
type: 'cascader',
type: 'administrativeDivisionTree',
label: '行政区划',
required: true,
options: administrativeDivisionTree.value,
componentsProps: {
showSearch: true
}
},
address: {
type: 'inputTextArea',

View File

@ -6,6 +6,8 @@ interface ImportMetaEnv {
readonly VITE_APP_ENV: 'development' | 'production';
// 启动端口
readonly VITE_APP_PORT: number;
// 模块名称
readonly VITE_APP_MODULE_NAME: string;
// axios
readonly VITE_APP_BASE_API: string;

View File

@ -14,7 +14,7 @@ export default defineConfig(({mode}) => {
define: {
__APP_ENV: JSON.stringify(env)
},
base: '/',
base: `/${env['VITE_APP_MODULE_NAME']}/`,
plugins: [
vue(),
vueJsx(),
@ -43,7 +43,7 @@ export default defineConfig(({mode}) => {
}
},
build: {
outDir: 'dist',
outDir: env['VITE_APP_MODULE_NAME'],
target: 'modules',
chunkSizeWarningLimit: 1500,
minify: 'terser',