企业入驻

This commit is contained in:
wangyilin 2024-08-30 18:01:34 +08:00
parent 5a563b8625
commit cae9ca1142
16 changed files with 424 additions and 8 deletions

View File

@ -5,7 +5,11 @@ VITE_DROP_CONSOLE=false
# axios # axios
VITE_APP_BASE_API=/api VITE_APP_BASE_API=/api
VITE_APP_PROXY_URL=http://localhost:8765 VITE_APP_PROXY_URL=http://172.10.10.151:8765
# rsa 公钥 # rsa 公钥
VITE_APP_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJps/EXxxSpEM1Ix4R0NWIOBciHCr7P7coDT8tNKfelgR7txcJOqHCO/MIWe7T04aHQTcpQxqx9hMca7dbqz8TZpz9jvLzE/6ZonVKxHsoFnNlHMp1/CPAJ9f6D9wYicum2KltJkmQ0g//D9W2zPCYoGOmSRFcZx/KEBa4EM53jQIDAQAB VITE_APP_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJps/EXxxSpEM1Ix4R0NWIOBciHCr7P7coDT8tNKfelgR7txcJOqHCO/MIWe7T04aHQTcpQxqx9hMca7dbqz8TZpz9jvLzE/6ZonVKxHsoFnNlHMp1/CPAJ9f6D9wYicum2KltJkmQ0g//D9W2zPCYoGOmSRFcZx/KEBa4EM53jQIDAQAB
# minio
VITE_APP_MINIO_URL=http://118.253.177.137:9000
VITE_APP_MINIO_BUCKET=police-security-dev

View File

@ -23,3 +23,7 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
package-lock.json
yarn.lock
components.d.ts

View File

@ -17,9 +17,12 @@
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.0",
"sass": "^1.77.8", "sass": "^1.77.8",
"vue": "^3.4.37", "vue": "^3.4.37",
"vue-router": "4" "vue-router": "4",
"vue-uuid": "^3.0.0",
"lodash-es": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash-es": "^4.17.8",
"@types/node": "^22.5.1", "@types/node": "^22.5.1",
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",

View File

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

View File

@ -0,0 +1,95 @@
<template>
<div class="simpleUploadDiv">
<a-progress v-if="uploading" type="circle" :percent="percent"/>
<a-image
height="80%"
v-else
:src="minioBaseUrl+modelValue"
alt="avatar"/>
<a-button class="btn-success" @click="selectFile">{{ btnLabel }}</a-button>
<input id="myFileInput" type="file" style="display: none"/>
</div>
</template>
<script setup lang="ts">
import {message} from "ant-design-vue";
import {onMounted, onUnmounted, ref} from "vue";
import {generateSimpleObjectName, getResignedObjectUrl} from "@/utils/minioUtil";
import axios, {CancelTokenSource} from "axios";
import {convertFileSizeToStr} from "@/utils/index.ts";
const minioBaseUrl = __APP_ENV.VITE_APP_MINIO_URL
const modelValue = defineModel<string>('value')
const props = withDefaults(defineProps<{
parentDir?: string,
allowedExtensions?: string[],
maxSize?: number,
width?: string | number,
height?: string | number,
btnLabel?: string
}>(), {
parentDir: '',
allowedExtensions: () => ['jpg', 'jpeg', 'png', 'gif'],
maxSize: 1024 * 1024 * 4,
width: '150px',
height: '150px',
btnLabel: '选择图片'
})
const uploading = ref(false)
const percent = ref(0)
let cancelToken: CancelTokenSource | null = null
const selectFile = () => {
document.getElementById('myFileInput')?.click()
}
async function inputFileListener(this: HTMLInputElement) {
const selectedFile: File = this.files?.[0] as File;
const fileExtension = selectedFile.name?.split('.').pop().toLowerCase() as string;
if (!props.allowedExtensions.includes(fileExtension)) {
return message.error(`错误:不支持的文件格式,目前支持:【${props.allowedExtensions}`)
}
const isMax = selectedFile.size > props.maxSize;
if (isMax) {
return message.error(`文件大小超出限制,最大支持:【${convertFileSizeToStr(props.maxSize)}`);
}
cancelToken?.cancel();
percent.value = 0;
uploading.value = true;
const objectName = generateSimpleObjectName(selectedFile.name, props.parentDir)
const uploadUrl = await getResignedObjectUrl(__APP_ENV.VITE_APP_MINIO_BUCKET, objectName);
cancelToken = axios.CancelToken.source()
await axios.put(uploadUrl, selectedFile, {
cancelToken: cancelToken.token,
onUploadProgress: (progressEvent) => {
percent.value = (progressEvent.loaded / (progressEvent.total as number) * 100 | 0)
}
})
modelValue.value = '/' + __APP_ENV.VITE_APP_MINIO_BUCKET + objectName;
uploading.value = false;
}
onMounted(() => {
document.getElementById('myFileInput')?.addEventListener('change', inputFileListener);
})
onUnmounted(() => {
document.getElementById('myFileInput')?.removeEventListener('change', inputFileListener);
})
</script>
<style scoped lang="scss">
.simpleUploadDiv {
width: v-bind(width);
height: v-bind(height);
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,2 +1,2 @@
export const CLIENT_TYPE = "MANAGEMENT_SUPER"; export const CLIENT_TYPE = "MANAGEMENT_SUPER";
export const ROUTER_WHITE_LIST: string[] = ['/login', '/test']; export const ROUTER_WHITE_LIST: string[] = ['/login', '/test','/enterprise'];

View File

@ -8,3 +8,56 @@ interface JsonResult<T> {
message: string; message: string;
data?: T; data?: T;
} }
/**
*
*/
class SelectNodeVo<T, E = Record<string, any>> {
value: T;
label: string;
options?: SelectNodeVo<T>[]
orderIndex?: number;
disabled?: boolean;
extData?: E
}
/**
*
*/
class TreeNodeVo<T, E = Record<string, any>> {
value: T;
parentValue: T;
label: string;
orderIndex?: number;
children?: TreeNodeVo<T>[]
extData?: E;
}
/**
*
*/
declare interface Grid {
//栅格占据的列数
span?: number;
//栅格左侧的间隔格数
offset?: number;
//栅格向右移动格数
push?: number;
//栅格向左移动格数
pull?: number;
//<768px 响应式栅格数或者栅格属性对象
xs?: number;
//≥768px 响应式栅格数或者栅格属性对象
sm?: number;
//≥992px 响应式栅格数或者栅格属性对象
md?: number;
//≥1200px 响应式栅格数或者栅格属性对象
lg?: number;
//≥1920px 响应式栅格数或者栅格属性对象
xl?: number;
}
interface BaseEnum<T> {
value: T;
label: string
}

View File

@ -29,7 +29,7 @@ router.beforeEach(async (to, from, next) => {
if (ROUTER_WHITE_LIST.includes(to.path)) { if (ROUTER_WHITE_LIST.includes(to.path)) {
return next(); return next();
} }
//不在白名单内需要查看是否携带token 没有token需要返回登录页进行登录 // 不在白名单内需要查看是否携带token 没有token需要返回登录页进行登录
if (!userStore.getTokenInfo?.value) { if (!userStore.getTokenInfo?.value) {
await message.warn('未找到token请重新登陆!') await message.warn('未找到token请重新登陆!')
return next('/login'); return next('/login');

View File

@ -1,6 +1,14 @@
import {RouteRecordRaw} from "vue-router"; import {RouteRecordRaw} from "vue-router";
export const staticRouter: RouteRecordRaw[] = [ export const staticRouter: RouteRecordRaw[] = [
{
path: '/enterprise',
name: 'enterprise',
meta: {
title: '企业入驻',
},
component: () => import("@/views/enterprise.vue"),
},
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
@ -11,6 +19,13 @@ export const staticRouter: RouteRecordRaw[] = [
}, { }, {
path: "/", path: "/",
redirect: '/index', redirect: '/index',
}, {
path: '/test',
name: 'test',
meta: {
title: '测试',
},
component: () => import("@/views/test.vue"),
}, { }, {
path: '/layout', path: '/layout',
name: 'layout', name: 'layout',
@ -29,5 +44,5 @@ export const staticRouter: RouteRecordRaw[] = [
component: () => import('@/views/index.vue') component: () => import('@/views/index.vue')
} }
] ]
} },
] ]

View File

@ -0,0 +1,17 @@
import {ceil, divide} from "lodash-es";
/**
*
* @param fileSizeInBytes
*/
export const convertFileSizeToStr = (fileSizeInBytes: number): string => {
if (fileSizeInBytes < 1024) {
return fileSizeInBytes + "B";
} else if (fileSizeInBytes < 1024 * 1024) {
return (ceil(divide(fileSizeInBytes, 1024), 2)) + "KB";
} else if (fileSizeInBytes < 1024 * 1024 * 1024) {
return (ceil(divide(fileSizeInBytes, (1024 * 1024)), 2)) + "MB";
} else {
return (ceil(divide(fileSizeInBytes, (1024 * 1024 * 1024)), 2)) + "GB";
}
}

View File

@ -0,0 +1,26 @@
import api from "@/axios";
import dayjs from "dayjs";
import {uuid} from "vue-uuid";
/**
*
* @param fileName
* @param parentDir
*/
export const generateSimpleObjectName = (fileName: string, parentDir?: String): string => {
let objectName = parentDir + dayjs().format('/YYYY/MM/DD/') + uuid.v4().replace(/-/g, '');
if (fileName && fileName.length > 0) {
objectName += fileName.substring(fileName.lastIndexOf('.'))
}
return objectName;
}
/**
* URL
*/
export const getResignedObjectUrl = async (bucketName: string, objectName: string): Promise<string> => {
return (await api.get<string>('/common/getResignedObjectUrl', {
bucketName,
objectName
})).data as string;
}

View File

@ -0,0 +1,182 @@
<template>
<div class="enterprise">
<div class="enterpriseIndex">
<div class="enterpriseItem">
<a-tabs v-model:activeKey="activeKey" :tabBarGutter="300" centered >
<a-tab-pane key="1" tab="企业入驻">
<a-form
ref="formDateRef"
:rules="rules"
:label-col="labelCol"
:wrapper-col="wrapperCol"
layout="horizontal"
:model="formDate"
@finish="onFinish"
>
<a-form-item label="名称" name="name">
<a-input v-model:value="formDate.name" />
</a-form-item>
<a-form-item label="统一社会编码" name="socialCode">
<a-input v-model:value="formDate.socialCode"/>
</a-form-item>
<a-form-item label="公司性质" name="nature">
<a-input v-model:value="formDate.nature"/>
</a-form-item>
<a-form-item label="行政区划" >
<a-cascader v-model:value="formDate.administrativeDivisionCodes" :show-search="{ filter }" :options="administrativeDivisionTree" @change="searchAdministrativeDivisionTree" />
</a-form-item>
<a-form-item label="营业执照" name="businessLicense">
<SingleImageFileUpload v-model:value="formDate.businessLicense"></SingleImageFileUpload>
</a-form-item>
<a-form-item label="法人名字">
<a-input v-model:value="formDate.legalPersonInfo" />
</a-form-item>
<a-form-item label="法人手机号码">
<a-input v-model:value="formDate.telephone" />
</a-form-item>
<a-form-item label="详细地址" >
<a-input v-model:value="formDate.address" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit" style="width: 100px">确认</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="查询企业状态" >
22
</a-tab-pane>
</a-tabs>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted} from 'vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { ShowSearchType } from 'ant-design-vue/es/cascader';
import api from "@/axios";
import { message } from 'ant-design-vue';
import SingleImageFileUpload from "@/components/upload/SingleImageFileUpload.vue";
const activeKey = ref('1');
const labelCol = { style: { width: '120px' } };
const wrapperCol = { span: 14 };
const administrativeDivisionTree = ref<TreeNodeVo<string>[]>([])
const formDateRef = ref();
interface formDatePort {
name:string,
socialCode:string,
businessLicense:string,
legalPersonInfo:string,
telephone:string,
administrativeDivisionCodes:Record<string, any>,
address:string,
nature:string
}
const formDate = ref<formDatePort>({
name:'',
socialCode:'',
businessLicense:'',
legalPersonInfo:'',
telephone:'',
administrativeDivisionCodes:[''],
address:'',
nature:''
})
const rules: Record<string, Rule[]> = {
name: [
{ required: true, message: '请输入姓名', trigger: 'change' },
],
socialCode:[
{ required: true, message: '请输入社会编码', trigger: 'change' },
],
nature:[
{ required: true, message: '请填写公司性质', trigger: 'change' },
],
businessLicense:[
{ required: true, message: '请上传营业执照', trigger: 'change' },
],
administrativeDivisionCodes:[
{ required: true, message: '请选择行政区划', trigger: 'change' },
]
};
// 1
const DivisionTree = async ()=>{
const resp = await api.get<TreeNodeVo<string>[]>('/common/administrativeDivisionTree')
administrativeDivisionTree.value = resp.data as TreeNodeVo<string>[]
}
// 2
const filter: ShowSearchType['filter'] = (inputValue, path) => {
return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);
};
//
const searchAdministrativeDivisionTree = (e:Array<string>)=>{
formDate.value.administrativeDivisionCodes = e
}
//
const onFinish = async ()=>{
//
await formDateRef.value.validate()
const legalPersonInfo = {
name:formDate.value.legalPersonInfo,
telephone:formDate.value.telephone
}
const securityUnitRegisterParams = {
name:formDate.value.name,
socialCode:formDate.value.socialCode,
businessLicense:formDate.value.businessLicense,
legalPersonInfo:legalPersonInfo,
nature:formDate.value.nature,
administrativeDivisionCodes:formDate.value.administrativeDivisionCodes,
address:formDate.value.address
}
const resp = await api.post('/common/securityUnitRegister',securityUnitRegisterParams)
console.log(resp)
await message.loading('正在注册中...')
message.success('企业入驻成功')
await formDateRef.value.resetFields() //
formDate.value = {
name:'',
socialCode:'',
businessLicense:'',
legalPersonInfo:'',
telephone:'',
administrativeDivisionCodes:[''],
address:'',
nature:''
}
}
onMounted( async ()=>{
await DivisionTree()
})
</script>
<style scoped lang="scss">
.enterprise{
display: flex;
justify-content: center; /* 水平居中 */
align-items: center;
text-align: center;
height: 100vh;
width: 100%;
.enterpriseIndex{
border: 1px solid #cccccc;
height: 90vh;
width: 800px;
}
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
<SingleImageFileUpload v-model:value="url"></SingleImageFileUpload>
</div>
</template>
<script setup lang="ts">
import SingleImageFileUpload from "@/components/upload/SingleImageFileUpload.vue";
import {ref} from "vue";
const url = ref<string>('')
</script>
<style scoped lang="scss">
</style>

View File

@ -11,6 +11,10 @@ interface ImportMetaEnv {
readonly VITE_APP_BASE_API: string; readonly VITE_APP_BASE_API: string;
readonly VITE_APP_PROXY_URL: string; readonly VITE_APP_PROXY_URL: string;
// minio
readonly VITE_APP_MINIO_URL: string
readonly VITE_APP_MINIO_BUCKET: string
// RSA公钥 // RSA公钥
readonly VITE_APP_RSA_PUBLIC_KEY: string; readonly VITE_APP_RSA_PUBLIC_KEY: string;
} }

View File

@ -18,7 +18,7 @@
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "vue", "jsxImportSource": "vue",
/* Linting */ /* Linting */
"strict": true, "strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,

View File

@ -13,7 +13,7 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true