This commit is contained in:
oneao committed 2025-12-16 17:21:56 +08:00
1 parent 1da52dc4f4
commit 276644f473
223 files changed
+9609 -262

No files matched your search

+13 -9
View File
@@ -1,8 +1,8 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
import { getAreaListApi } from './request/api/area'
import { getPropertyListApi } from './request/api/industry'
import { useCommonStore } from './store/common'
import { initBaseApi } from '@/request/api/base'
import { useCommonStore } from '@/store/common'
const commonOptions = useCommonStore()
@@ -11,14 +11,18 @@ onMounted(() => {
})
function handleInitData() {
getAreaListApi().then((res) => {
commonOptions.areaOptions.splice(0, commonOptions.areaOptions.length, ...res.data || [])
})
getPropertyListApi().then((res) => {
commonOptions.industryOptions.splice(0, commonOptions.industryOptions.length, ...res.data || [])
initBaseApi().then((res) => {
console.log(res)
if (res.isSuccess) {
commonOptions.areaOptions.splice(0, commonOptions.areaOptions.length, ...res.data.areas || [])
commonOptions.industryOptions.splice(0, commonOptions.industryOptions.length, ...res.data.industries || [])
}
else {
ElMessage.error(res.message || '初始化失败,请刷新页面后重试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('初始化失败,请刷新页面后重试')
})
}
</script>
@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { useCommonStore } from '@/store/common'
const props = defineProps({
multiple: {
type: Boolean,
default: false,
},
checkStrictly: {
type: Boolean,
default: false,
},
})
const commonStore = useCommonStore()
</script>
<template>
<el-cascader
placeholder="请选择地域"
:options="commonStore.areaOptions"
filterable
:props="{
label: 'name',
value: 'name',
children: 'children',
multiple: props.multiple,
checkStrictly: props.checkStrictly,
}"
class="area-select-cascader"
v-bind="$attrs"
/>
</template>
<style lang="scss">
.area-select-cascader {
width: 100%;
}
</style>
@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { useCommonStore } from '@/store/common'
const commonStore = useCommonStore()
</script>
<template>
<el-select
filterable
placeholder="请选择行业"
value-key="name"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
v-bind="$attrs"
>
<el-option
v-for="item in commonStore.industryOptions"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</template>
<style lang="scss" scoped>
//
</style>
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import type { Area, CompanyQuery, Industry } from '@/types/company/index'
import type { Area, CompanyQuery, Industry } from '@/types/base'
import { computed } from 'vue'
import AreaSelect from './AreaSelect.vue'
import IndustrySelect from './IndustrySelect.vue'
const props = defineProps({
modelValue: {
@@ -31,35 +33,16 @@ const query = computed({
<template>
<div class="query">
<div class="query-item">
<el-select
v-model="query.industries"
filterable
placeholder="请选择行业"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
>
<el-option
v-for="item in props.industries"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<IndustrySelect v-model="query.industries" />
</div>
<div class="query-item">
<el-tree-select
<AreaSelect
v-model="query.areas"
placeholder="请选择地域"
:data="areas"
filterable
multiple
check-strictly
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
:render-after-expand="false"
/>
</div>
<div class="query-item">
@@ -106,10 +89,20 @@ const query = computed({
padding: 0px;
}
:deep(.el-input__inner) {
height: 60px;
height: 60px !important;
font-size: 18px;
}
:deep(.el-cascader__tags) {
padding: 0;
}
:deep(.el-cascader__search-input) {
margin-left: 0px;
font-size: 18px;
}
:deep(.el-input__suffix){
padding-right: 12px;
}
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
@@ -51,11 +51,13 @@ function exitLogin() {
<el-dropdown>
<div class="user">
<el-avatar :size="32" :src="userStore.userInfo.avatar" />
<span class="name">
{{ userStore.userInfo.name }}
</span>
<el-avatar :size="36" :src="userStore.userInfo.company.logo ? userStore.userInfo.company.logo : '/no-avatar.png'" />
<div class="info">
<span class="name">{{ userStore.userInfo.user.name }}</span>
<span class="company">{{ userStore.userInfo.company.name }}</span>
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
@@ -173,9 +175,22 @@ function exitLogin() {
align-items: center;
cursor: pointer;
outline: 0;
.name {
margin-left: 8px;
font-size: 18px;
.info {
display: flex;
flex-direction: column;
margin-left: 12px;
.name {
font-size: 16px;
font-weight: 500;
}
.company {
font-size: 12px;
color: #888;
margin-top: 6px;
}
}
}
}
@@ -1,6 +0,0 @@
import type { AreaResult } from '@/types/company/index'
import { httpGet } from '../utils'
export function getAreaListApi() {
return httpGet<AreaResult>(`/webArea/list`)
}
@@ -1,13 +0,0 @@
import type { forgetApiParams, loginApiParams, loginApiResult, registerApiParams } from '@/types/auth'
import { httpPostJson } from '../utils'
export function loginApi(params: loginApiParams) {
return httpPostJson<loginApiResult>(`/auth/login`, params)
}
export function registerApi(params: registerApiParams) {
return httpPostJson<null>(`/auth/register`, params)
}
export function forgetApi(params: forgetApiParams) {
return httpPostJson<null>(`/auth/forget`, params)
}
@@ -0,0 +1,14 @@
import type { LoginApiResult, LoginEmailCodeApiParams, LoginPasswordApiParams, RegisterApiParams } from '@/types/auth/index'
import { httpPostJson } from '@/request/utils'
export function registerApi(params: RegisterApiParams) {
return httpPostJson<void>('/auth/identity/register', params)
}
export function loginByPasswordApi(params: LoginPasswordApiParams) {
return httpPostJson<LoginApiResult>('/auth/identity/login/password', params)
}
export function loginByEmailCodeApi(params: LoginEmailCodeApiParams) {
return httpPostJson<LoginApiResult>('/auth/identity/login/emailCode', params)
}
@@ -0,0 +1,6 @@
import type { InitBaseDataResult } from '@/types/base'
import { httpGet } from '@/request/utils'
export function initBaseApi() {
return httpGet<InitBaseDataResult>(`/base/init`)
}
@@ -1,6 +0,0 @@
import type { IndustryResult } from '@/types/company/index'
import { httpGet } from '../utils'
export function getPropertyListApi() {
return httpGet<IndustryResult>(`/base/industry/list`)
}
@@ -57,7 +57,8 @@ axiosInstance.interceptors.request.use(
}
config.headers.Authorization = `Bearer ${userStore.userInfo.token}`
config.headers.id = userStore.userInfo.id
config.headers['X-Company-Id'] = userStore.userInfo.company.id
config.headers['X-User-Id'] = userStore.userInfo.user.id
return config
},
@@ -90,11 +91,14 @@ axiosInstance.interceptors.response.use(
return Promise.reject(response.data)
}
else {
// 500的话需要做特殊处理
resData.isSuccess = resData.code === 200
return Promise.resolve(response.data)
}
},
(error: AxiosError) => {
console.log(error)
if (error.status === 500) {
router.push('/error/500')
}
@@ -1,4 +1,5 @@
import type { Area, CompanyQuery, Industry } from '@/types/company/index'
import type { Area, CompanyQuery, Industry } from '@/types/base'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
@@ -13,10 +14,63 @@ export const useCommonStore = defineStore(
name: '',
})
/**
* 根据 Area id 获取完整路径,例如 "天津市/滨海新区/塘沽"
*/
function getAreaFullPathById(id: string): string {
// 递归查找节点
function findNode(id: string, nodes: Area[]): Area | null {
for (const node of nodes) {
if (node.id === id)
return node
if (node.children && node.children.length) {
const found = findNode(id, node.children)
if (found)
return found
}
}
return null
}
const node = findNode(id, areaOptions)
if (!node)
return ''
// 构建完整路径
const names: string[] = []
let current: Area | undefined | null = node
while (current) {
names.unshift(current.name)
current = current.fatherId ? findNode(current.fatherId, areaOptions) || undefined : undefined
}
return names.join(',')
}
function getAreaIdByFullPath(fullPath: string): string | null {
const names = fullPath.split(',').map(s => s.trim())
if (!names.length)
return null
let currentNodes = areaOptions
let foundNode: Area | null = null
for (const name of names) {
foundNode = currentNodes.find(n => n.name === name) || null
if (!foundNode)
return null
currentNodes = foundNode.children || []
}
return foundNode?.id || null
}
return {
areaOptions,
industryOptions,
companyQuery,
getAreaFullPathById,
getAreaIdByFullPath,
}
},
)
@@ -2,19 +2,36 @@ import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
interface IUserInfo {
id: string
account?: string
email?: string
name?: string
token?: string
avatar?: string
user: {
id: number
name: string
position: string
role: number
}
company: {
id: number
logo: string
name: string
}
token: string
}
export const useUserStore = defineStore(
'user',
() => {
const userInfo = ref<IUserInfo>({
id: '',
user: {
id: 0,
name: '',
position: '',
role: 0,
},
company: {
id: 0,
logo: '',
name: '',
},
token: '',
})
const setUserInfo = (val: IUserInfo) => {
@@ -22,23 +39,25 @@ export const useUserStore = defineStore(
}
const clearUserInfo = () => {
userInfo.value.id = ""
userInfo.value.account = ""
userInfo.value.email = ""
userInfo.value.name = ""
userInfo.value.token = ""
userInfo.value.avatar = ""
userInfo.value = {
user: {
id: 0,
name: '',
position: '',
role: 0,
},
company: {
id: 0,
logo: '',
name: '',
},
token: '',
}
}
// ✅ 如果需要同时验证多个字段
// ✅ 判断是否登录
const isLogined = computed(() => {
return !!(
(
userInfo.value.token
&& userInfo.value.id
&& userInfo.value.account
)
)
return !!(userInfo.value.token && userInfo.value.user.id && userInfo.value.company.id)
})
return {
@@ -1,32 +1,30 @@
export interface loginApiParams {
loginCredential: string
export interface LoginPasswordApiParams {
email: string
password: string
}
export interface loginApiResult {
id: string
account: string
name: string
export interface LoginEmailCodeApiParams {
email: string
code: string
}
export interface LoginApiResult {
companyId: number
companyLogo: string
companyName: string
userId: number
userRole: number
userName: string
userPosition: string
token: string
avatar: string
}
export interface registerApiParams {
account: string
name: string
email: string
password: string
}
export interface forgetApiParams {
forgetCredential: string
}
export interface RegisterCompanyForm {
name: string
industry: string
address: string
industry: string[]
address: string[]
phone: string
email: string
profile: string
@@ -43,3 +41,21 @@ export interface RegisterAdminForm {
position: string
}
export interface RegisterApiParams {
company: {
name: string
industry: string
address: string
phone: string
email: string
profile: string
}
admin: {
name: string
phone: string
email: string
password: string
position: string
}
}
@@ -11,14 +11,21 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AreaSelect: typeof import('./../components/AreaSelect.vue')['default']
HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
ICarbonAccessibility: typeof import('~icons/carbon/accessibility')['default']
'IFa7Solid:userShield': typeof import('~icons/fa7-solid/user-shield')['default']
'IIc:twotoneHeadphones': typeof import('~icons/ic/twotone-headphones')['default']
'IMaterialSymbols:lockOpenRightSharp': typeof import('~icons/material-symbols/lock-open-right-sharp')['default']
IMdiAccountBox: typeof import('~icons/mdi/account-box')['default']
'IMingcute:eye2Line': typeof import('~icons/mingcute/eye2-line')['default']
Industry: typeof import('../components/IndustrySelect.vue')['default']
IndustrySelect: typeof import('./../components/IndustrySelect.vue')['default']
'INrk:show': typeof import('~icons/nrk/show')['default']
'IRi:buildingFill': typeof import('~icons/ri/building-fill')['default']
'IRi:buildingLine': typeof import('~icons/ri/building-line')['default']
'ISolar:eyeBroken': typeof import('~icons/solar/eye-broken')['default']
'ISolar:eyeClosedBroken': typeof import('~icons/solar/eye-closed-broken')['default']
'ITdesign:notification': typeof import('~icons/tdesign/notification')['default']
'ITdesign:search': typeof import('~icons/tdesign/search')['default']
QueryCompany: typeof import('./../components/QueryCompany.vue')['default']
@@ -0,0 +1,24 @@
export interface Area {
id: string
xh: number
name: string
fatherId: string
children: Area[]
}
export interface Industry {
id: number
xh: number
name: string
}
export interface InitBaseDataResult {
industries: Industry[]
areas: Area[]
}
export interface CompanyQuery {
industries: string[]
areas: string[]
name: string
}
@@ -1,20 +0,0 @@
export interface Area {
label: string
value: number
children?: AreaResult[]
}
export type AreaResult = Area[]
export interface Industry {
label: string
value: string
}
export type IndustryResult = Industry[]
export interface CompanyQuery {
industries: string[]
areas: string[]
name: string
}
@@ -0,0 +1,23 @@
/**
* 校验邮箱格式
* @param value 待校验的邮箱字符串
* @returns true 格式正确,false 格式错误
*/
export function validateEmail(value: string): boolean {
if (!value)
return false // 如果为空,不校验格式,可按需求改成 false
const emailReg = /^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
return emailReg.test(value)
}
/**
* 校验中国手机号格式(支持 +86 开头)
* @param value 待校验的手机号字符串
* @returns true 格式正确,false 格式错误
*/
export function validatePhone(value: string): boolean {
if (!value)
return false // 如果为空,不校验格式,可按需求改成 false
const phoneReg = /^(?:\+?86)?1(?:3\d{3}|5[^4\D]\d{2}|8\d{3}|7(?:[0-35-9]\d{2}|4(?:0\d|1[0-2]|9\d))|9[0-35-9]\d{2}|6[2567]\d{2}|4(?:(?:10|4[01])\d{3}|[68]\d{4}|[579]\d{2}))\d{6}$/
return phoneReg.test(value)
}
@@ -1,14 +1,29 @@
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { loginApi } from '@/request/api/auth'
import { loginByEmailCodeApi, loginByPasswordApi } from '@/request/api/auth'
import { authCodeSendApi } from '@/request/api/auth/code'
import { useUserStore } from '@/store/user'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { validateEmail } from '@/utils/validate'
const userStore = useUserStore()
const router = useRouter()
const loginOptions = [
{
label: '密码登录',
value: '0',
},
{
label: '验证码登录',
value: '1',
},
]
const loginMode = ref('0')
const carouselList = [
{
title: '值得信赖',
@@ -37,59 +52,160 @@ const carouselList = [
},
]
// --- 发送验证码状态 & 倒计时 ---
const sending = ref(false)
const countdown = ref(0)
let timer: number | undefined
function startCountdown(seconds = 60) {
countdown.value = seconds
timer = window.setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
clearInterval(timer)
timer = undefined
}
}, 1000)
}
const form = reactive<{
loginCredential: string
email: string
password: string
code: string
rememberMe: boolean
}>({
loginCredential: '',
email: '',
password: '',
code: '',
rememberMe: true,
})
const formRef = ref<FormInstance>()
const formRules = {
loginCredential: [
email: [
{ required: true, message: '邮箱号不能为空', trigger: 'blur' },
{ type: 'email', required: true, message: '邮箱格式错误', trigger: 'blur' },
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (loginMode.value === '0') {
if (!value) {
callback(new Error('密码不能为空'))
return
}
}
callback() // 校验通过
},
trigger: 'blur',
},
],
code: [
{
validator: (rule: any, value: any, callback: any) => {
if (loginMode.value === '1') {
if (!value) {
callback(new Error('验证码不能为空'))
return
}
}
callback() // 校验通过
},
trigger: 'blur',
},
],
}
const isEmailValid = computed(() => validateEmail(form.email))
const submitLoading = ref(false)
function handleSendEmailCode() {
if (sending.value || countdown.value > 0 || !isEmailValid.value)
return
sending.value = true
authCodeSendApi({
channel: AuthCodeChannel.EMAIL,
purpose: AuthCodePurpose.LOGIN,
target: form.email,
}).then((res) => {
if (res.isSuccess) {
ElMessage.success('发送成功')
startCountdown()
}
else {
sending.value = false
ElMessage.error(res.message || '发送失败,请检查邮箱是否正确或稍后再试!')
}
}).catch((err) => {
console.log(err)
ElMessage.error('发送失败,请稍后再试')
sending.value = false
}).finally(() => {
sending.value = false
})
}
async function submitForm(formEl: FormInstance | undefined) {
if (!formEl)
return
await formEl.validate((valid) => {
if (valid) {
submitLoading.value = true
loginApi({
loginCredential: form.loginCredential,
password: form.password,
}).then((res) => {
if (res.isSuccess) {
userStore.setUserInfo({
id: res.data.id,
account: res.data.account,
email: res.data.email,
name: res.data.name,
token: res.data.token,
avatar: res.data.avatar || '/no-avatar.png',
})
ElMessage.success('登录成功')
router.push('/')
}
else {
ElMessage.error(res.message || '登录失败,请稍后再试!')
}
}).catch((err) => {
console.log(err)
}).finally(() => {
submitLoading.value = false
})
await formEl.validate(async (valid) => {
if (!valid)
return
submitLoading.value = true
try {
let res
if (loginMode.value === '0') {
// 密码登录
res = await loginByPasswordApi({
email: form.email,
password: form.password,
})
}
else {
// 验证码登录
res = await loginByEmailCodeApi({
email: form.email,
code: form.code,
})
}
if (res.isSuccess) {
ElMessage.success('登录成功')
userStore.setUserInfo({
user: {
id: res.data.userId,
name: res.data.userName,
position: res.data.userPosition,
role: res.data.userRole,
},
company: {
id: res.data.companyId,
logo: res.data.companyLogo,
name: res.data.companyName,
},
token: res.data.token,
})
router.push('/')
}
else {
ElMessage.error(res.message || '登录失败,请稍后再试')
}
}
catch (err) {
console.log(err)
ElMessage.error('登录失败,请稍后再试')
}
finally {
submitLoading.value = false
}
})
}
@@ -130,8 +246,19 @@ function navigateTo(path: string) {
<h4 class="title">
登录
</h4>
<div style="padding: 8px 0px;">
<el-segmented
v-model="loginMode" :options="loginOptions" size="large"
/>
</div>
<p class="title-extra">
输入你的 邮箱 和 密码 登录账户.
<template v-if="loginMode === '0'">
输入你的 邮箱 和 密码 登录系统
</template>
<template v-else-if="loginMode === '1'">
输入你的 邮箱 和 验证码 登录系统
</template>
</p>
<el-form
ref="formRef"
@@ -141,11 +268,14 @@ function navigateTo(path: string) {
label-position="top"
hide-required-asterisk
>
<el-form-item label="邮箱" prop="loginCredential">
<el-input v-model="form.loginCredential" placeholder="请输入邮箱" size="large" />
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-form-item
v-if="loginMode === '0'"
prop="password"
>
<template #label>
<span class="form-label-flex">
<span>密码</span>
@@ -155,11 +285,47 @@ function navigateTo(path: string) {
<el-input
v-model="form.password"
placeholder="请输入密码"
show-password
size="large"
@keyup.enter="submitForm(formRef)"
/>
</el-form-item>
<el-form-item
v-else
prop="code"
>
<template #label>
<span class="form-label-flex">
<span>密码</span>
<span class="forgot" @click="navigateTo('/auth/forget')">忘记密码?</span>
</span>
</template>
<el-input
v-model="form.code"
placeholder="请输入邮箱验证码"
class="email-code"
size="large"
:class="(sending || countdown > 0 || !isEmailValid) ? 'disabled' : ''"
>
<template #append>
<el-button
:disabled="sending || countdown > 0 || !isEmailValid"
@click="handleSendEmailCode"
>
<template v-if="sending">
发送中...
</template>
<template v-else-if="countdown > 0">
{{ countdown }}s
</template>
<template v-else>
发送验证码
</template>
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.rememberMe" label="记住我" class="remeber" />
</el-form-item>
@@ -257,6 +423,20 @@ function navigateTo(path: string) {
width: 100%;
margin-bottom: 12px;
}
.email-code {
:deep(.el-input-group__append) {
background-color: #409eff;
color: #fff;
cursor: pointer;
}
&.disabled {
:deep(.el-input-group__append) {
background-color: #f5f7fa;
color: #c0c4cc;
}
}
}
.remeber {
:deep(.el-checkbox__inner) {
@@ -267,6 +447,7 @@ function navigateTo(path: string) {
font-size: 16px;
}
}
.title {
font-size: 28px;
font-weight: 600;
@@ -5,6 +5,7 @@ import { ElMessage } from 'element-plus'
import { computed, onUnmounted, reactive, ref } from 'vue'
import { authCodeSendApi, authCodeVerifyApi } from '@/request/api/auth/code'
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
import { validateEmail, validatePhone } from '@/utils/validate'
const emit = defineEmits<{
(e: 'prev'): void
@@ -12,6 +13,7 @@ const emit = defineEmits<{
}>()
const submitLoading = ref(false)
const isEmailCodeValid = ref(true)
const formRef = ref<FormInstance>()
const form = reactive<RegisterAdminForm>({
@@ -53,11 +55,25 @@ const formRules = {
trigger: ['blur', 'change'],
},
],
phone: [{ validator: phoneValidator, trigger: 'blur' }],
}
// 校验邮箱
const emailRegex = /^\w+(?:[-.]\w+)*@\w+(?:[-.]\w+)*\.\w+(?:[-.]\w+)*$/
const isEmailValid = computed(() => emailRegex.test(form.email.trim()))
function phoneValidator(rule: any, value: any, callback: any) {
// 未填写:直接通过
if (!value) {
callback()
return
}
if (!validatePhone(value)) {
callback(new Error('手机号格式不正确'))
}
else {
callback()
}
}
const isEmailValid = computed(() => validateEmail(form.email))
// --- 发送验证码状态 & 倒计时 ---
const sending = ref(false)
@@ -118,6 +134,11 @@ async function handleSubmit(formEl: FormInstance | undefined) {
await formEl.validate((valid) => {
if (valid) {
if (isEmailCodeValid.value) {
emit('submit', form)
return
}
submitLoading.value = true
authCodeVerifyApi({
channel: AuthCodeChannel.EMAIL,
@@ -127,6 +148,8 @@ async function handleSubmit(formEl: FormInstance | undefined) {
}).then((res) => {
if (res.isSuccess) {
emit('submit', form)
isEmailCodeValid.value = true
}
else {
ElMessage.error(res.message || '验证码无效或已过期,请重新获取')
@@ -134,6 +157,8 @@ async function handleSubmit(formEl: FormInstance | undefined) {
}).catch((err) => {
console.log(err)
ElMessage.error('邮箱验证码校验失败,请稍后再试')
isEmailCodeValid.value = false
}).finally(() => {
submitLoading.value = false
})
@@ -207,12 +232,12 @@ function handlePrev() {
</el-col>
<el-col :span="12">
<el-form-item label="登录密码" prop="password">
<el-input v-model="form.password" placeholder="请输入登录密码" />
<el-input v-model="form.password" placeholder="请输入登录密码" show-password />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="form.confirmPassword" placeholder="请输入确认密码" />
<el-input v-model="form.confirmPassword" placeholder="请输入确认密码" show-password />
</el-form-item>
</el-col>
<el-col :span="24">
@@ -1,7 +1,10 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import type { RegisterCompanyForm } from '@/types/auth'
import { reactive, ref } from 'vue'
import { reactive, ref, watch } from 'vue'
import AreaSelect from '@/components/AreaSelect.vue'
import IndustrySelect from '@/components/IndustrySelect.vue'
import { validateEmail, validatePhone } from '@/utils/validate'
const emit = defineEmits<{
(e: 'submit', value: RegisterCompanyForm): void
@@ -19,6 +22,7 @@ const formRules = {
{ required: true, message: '公司地址不能为空', trigger: 'blur' },
],
email: [{ validator: emailValidator, trigger: 'blur' }],
phone: [{ validator: phoneValidator, trigger: 'blur' }],
}
function emailValidator(rule: any, value: any, callback: any) {
@@ -28,27 +32,42 @@ function emailValidator(rule: any, value: any, callback: any) {
return
}
// 填了就校验格式
const emailReg
= /^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
if (!emailReg.test(value)) {
if (!validateEmail(value)) {
callback(new Error('邮箱格式不正确'))
}
else {
callback()
}
}
function phoneValidator(rule: any, value: any, callback: any) {
// 未填写:直接通过
if (!value) {
callback()
return
}
if (!validatePhone(value)) {
callback(new Error('手机号格式不正确'))
}
else {
callback()
}
}
const form = reactive<RegisterCompanyForm>({
name: '',
industry: '',
address: '',
industry: [],
address: [],
phone: '',
email: '',
profile: '',
})
watch(() => form.industry, (newVal) => {
console.log(newVal)
})
watch(() => form.address, (newVal) => {
console.log(newVal)
})
async function handleSubmit(formEl: FormInstance | undefined) {
if (!formEl)
return
@@ -83,12 +102,15 @@ async function handleSubmit(formEl: FormInstance | undefined) {
</el-col>
<el-col :span="12">
<el-form-item label="所属行业" prop="industry">
<el-input v-model="form.industry" placeholder="请输入所属行业" />
<IndustrySelect v-model="form.industry" placeholder="请输入所属行业" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司地址" prop="address">
<el-input v-model="form.address" placeholder="请输入公司地址" />
<AreaSelect
v-model="form.address"
placeholder="请输入公司地址"
/>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -1,9 +1,11 @@
<script setup lang="ts">
import type { PropType } from 'vue'
import type { RegisterAdminForm, RegisterCompanyForm } from '@/types/auth'
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import { registerApi } from '@/request/api/auth'
defineProps({
const props = defineProps({
company: {
type: Object as PropType<RegisterCompanyForm>,
required: true,
@@ -18,11 +20,48 @@ const emit = defineEmits<{
(e: 'prev'): void
(e: 'submit'): void
}>()
const showPassword = ref(false)
const submitLoading = ref(false)
const isAgree = ref(false)
function handleSubmit() {
emit('submit')
if (!isAgree.value) {
return
}
submitLoading.value = true
registerApi({
company: {
name: props.company.name,
industry: props.company.industry.join(','),
address: props.company.address.join(','),
phone: props.company.phone,
email: props.company.email,
profile: props.company.profile,
},
admin: {
name: props.admin.name,
phone: props.admin.phone,
email: props.admin.email,
password: props.admin.password,
position: props.admin.position,
},
}).then((res) => {
if (res.isSuccess) {
ElMessage.success('注册成功')
emit('submit')
}
else {
ElMessage.error(res.message || '注册失败,请稍后再试')
}
}).catch((err) => {
console.log(err)
ElMessage.error('注册失败,请稍后再试')
}).finally(() => {
submitLoading.value = false
})
}
</script>
@@ -33,24 +72,51 @@ function handleSubmit() {
公司信息
</h2>
<el-row :gutter="16" style="padding: 12px 0px;">
<el-row :gutter="16" class="info-group">
<el-col :span="8">
公司全称:-
<div class="info-item">
<span class="info-label">公司全称</span>
<span class="info-value">{{ company.name || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
所属行业:-
<div class="info-item">
<span class="info-label">所属行业</span>
<span class="info-value">
{{ company.industry?.join(',') || '-' }}
</span>
</div>
</el-col>
<el-col :span="8">
公司地址:-
<div class="info-item">
<span class="info-label">公司地址</span>
<span class="info-value">
{{ company.address?.join('/') || '-' }}
</span>
</div>
</el-col>
<el-col :span="8">
联系电话:-
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ company.phone || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
公司邮箱:-
<div class="info-item">
<span class="info-label">公司邮箱</span>
<span class="info-value">{{ company.email || '-' }}</span>
</div>
</el-col>
<el-col :span="24">
公司简介:-
<div class="info-item">
<span class="info-label">公司简介</span>
<span class="info-value">{{ company.profile || '-' }}</span>
</div>
</el-col>
</el-row>
@@ -58,21 +124,45 @@ function handleSubmit() {
管理员信息
</h2>
<el-row :gutter="16" style="padding: 12px 0px;">
<el-row :gutter="16" class="info-group">
<el-col :span="8">
管理员名称:-
<div class="info-item">
<span class="info-label">管理员名称</span>
<span class="info-value">{{ admin.name || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
管理员电话:-
<div class="info-item">
<span class="info-label">管理员电话</span>
<span class="info-value">{{ admin.phone || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
管理员邮箱:-
<div class="info-item">
<span class="info-label">管理员邮箱</span>
<span class="info-value">{{ admin.email || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
登录密码:-
<div class="info-item">
<span class="info-label">登录密码</span>
<span class="info-value">{{ showPassword ? admin.password : '******' }}</span>
<span class="password" @click="showPassword = !showPassword">
<i-solar:eye-broken v-if="showPassword" />
<i-solar:eye-closed-broken v-else />
</span>
</div>
</el-col>
<el-col :span="8">
所属职务:-
<div class="info-item">
<span class="info-label">所属职务</span>
<span class="info-value">{{ admin.position || '-' }}</span>
</div>
</el-col>
</el-row>
</div>
@@ -96,7 +186,7 @@ function handleSubmit() {
>
上一步
</el-button>
<el-button type="primary" :disabled="!isAgree" @click="handleSubmit()">
<el-button type="primary" :disabled="!isAgree" :loading="submitLoading" @click="handleSubmit()">
确认提交
</el-button>
</div>
@@ -113,6 +203,24 @@ function handleSubmit() {
flex-direction: column;
height: 100%;
.info-item {
display: flex;
font-size: 14px;
padding: 6px 0px;
}
.info-label {
color: #71717a;
white-space: nowrap;
}
.info-value {
color: #111827;
font-weight: 500;
word-break: break-all;
margin-left: 8px;
}
:deep(.el-col) {
word-break: break-all;
padding: 3px 0px;
@@ -125,7 +233,7 @@ function handleSubmit() {
.section-title {
font-size: 17px;
font-weight: 600;
padding-bottom: 6px;
padding: 16px 0px;
border-bottom: 1px solid #e4e4e7;
}
.section-subtitle {
@@ -134,6 +242,12 @@ function handleSubmit() {
margin-bottom: 10px;
}
.password {
margin-left: 6px;
cursor: pointer;
color: var(--el-color-primary);
}
.terms {
display: flex;
font-size: 14px;
@@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { RegisterAdminForm, RegisterCompanyForm } from '@/types/auth'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import AdminForm from './components/AdminForm.vue'
import CompanyForm from './components/CompanyForm.vue'
@@ -52,8 +51,8 @@ const adminForm = ref<RegisterAdminForm>({
})
const companyForm = ref<RegisterCompanyForm>({
name: '',
industry: '',
address: '',
industry: [],
address: [],
phone: '',
email: '',
profile: '',
@@ -71,6 +70,16 @@ function handlePrevStep() {
function navigateTo(path: string) {
router.push(path)
}
function handleCompanySubmit(data: RegisterCompanyForm) {
companyForm.value = data
handleNextStep()
}
function handleAdminSubmit(data: RegisterAdminForm) {
adminForm.value = data
handleNextStep()
}
</script>
<template>
@@ -100,7 +109,7 @@ function navigateTo(path: string) {
<div class="auth-container">
<div class="form-container">
<div>
<img src="/logo.png" class="logo">
<img src="/logo.png" class="logo" @click="navigateTo('/')">
<el-steps finish-status="success" :active="activeStep" align-center>
<el-step title="公司信息" />
<el-step title="管理员信息" />
@@ -108,8 +117,8 @@ function navigateTo(path: string) {
</el-steps>
</div>
<CompanyForm v-show="activeStep === 0" @submit="handleNextStep" />
<AdminForm v-show="activeStep === 1" @submit="handleNextStep" @prev="handlePrevStep()" />
<CompanyForm v-show="activeStep === 0" @submit="handleCompanySubmit" />
<AdminForm v-show="activeStep === 1" @submit="handleAdminSubmit" @prev="handlePrevStep()" />
<ConfirmForm v-show="activeStep === 2" :admin="adminForm" :company="companyForm" @prev="handlePrevStep()" @submit="handleNextStep()" />
<ResultForm v-show="activeStep === 3" />
@@ -30,10 +30,11 @@ const tableData = [
企业名录
</div>
<el-table :data="tableData" border stripe style="width: 100%">
<!-- <el-table :data="tableData" style="width: 100%" :show-header="false">
<el-table-column prop="date" label="地区" width="180" />
<el-table-column prop="name" label="公司" />
</el-table>
</el-table> -->
<el-empty description="暂无企业名录" style="background-color: #fff;" />
</div>
<div class="right">
@@ -42,10 +43,12 @@ const tableData = [
热门推荐
</div>
<div class="hot-card-list">
<div v-for="i in 10" :key="i" class="hot-card-item">
<el-empty description="暂无热门推荐" />
<!-- <div v-for="i in 10" :key="i" class="hot-card-item">
<span class="number">{{ i }}</span>
<span class="name">xxxxxxxxxx公司公司</span>
</div>
</div> -->
</div>
</div>
</div>