403 lines
16 KiB
Vue
403 lines
16 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="hero">
|
||
<text class="eyebrow">YUNZE WORKSPACE</text>
|
||
<text class="title">加入企业</text>
|
||
<text class="desc">使用业务功能需要先创建企业,或申请挂靠已有企业</text>
|
||
</view>
|
||
|
||
<!-- 审核状态 -->
|
||
<view v-if="status" class="status-card" :class="statusClass">
|
||
<text class="status-title">{{ statusTitle }}</text>
|
||
<text class="status-desc">{{ statusDesc }}</text>
|
||
</view>
|
||
|
||
<view class="tabs">
|
||
<view class="tab" :class="{ active: tab === 'create' }" @tap="tab = 'create'">创建企业</view>
|
||
<view class="tab" :class="{ active: tab === 'join' }" @tap="tab = 'join'">挂靠企业</view>
|
||
</view>
|
||
|
||
<!-- ============ 创建企业:沿用完整字段与后端校验 ============ -->
|
||
<view v-if="tab === 'create'" class="card">
|
||
<view class="field">
|
||
<text class="label">企业全称 <text class="required">*</text></text>
|
||
<input v-model="createForm.tenant_name" class="input" placeholder="营业执照上的公司全称" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">统一社会信用代码 <text class="required">*</text></text>
|
||
<input v-model="createForm.credit_code" class="input" :maxlength="18" placeholder="18 位大写字母或数字" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">法定代表人 <text class="required">*</text></text>
|
||
<input v-model="createForm.legal_person" class="input" placeholder="姓名" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">营业执照 <text class="required">*</text></text>
|
||
<view class="upload" @tap="chooseLicense">
|
||
<text>{{ licenseName || '点击上传 JPG / PNG,不超过 5MB' }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">联系人 <text class="required">*</text></text>
|
||
<input v-model="createForm.contact_person" class="input" placeholder="姓名" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">联系电话 <text class="required">*</text></text>
|
||
<input v-model="createForm.contact_phone" class="input" type="number" :maxlength="11" placeholder="手机号" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">电子邮箱 <text class="required">*</text></text>
|
||
<input v-model="createForm.contact_email" class="input" placeholder="用于接收审核结果" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">企业地址</text>
|
||
<input v-model="createForm.address" class="input" placeholder="选填" />
|
||
</view>
|
||
<text class="hint">提交后由平台管理员审核,通过后自动成为该企业管理员</text>
|
||
</view>
|
||
|
||
<!-- ============ 挂靠企业 ============ -->
|
||
<view v-else class="card">
|
||
<view class="field">
|
||
<text class="label">企业全称 <text class="required">*</text></text>
|
||
<view class="search-row">
|
||
<input v-model="keyword" class="input search-input" placeholder="输入企业全称后搜索" />
|
||
<view class="search-btn" @tap="searchTenants">搜索</view>
|
||
</view>
|
||
<view v-if="tenantList.length" class="result-list">
|
||
<view v-for="t in tenantList" :key="t.tid" class="result-row" @tap="chooseTenant(t)">
|
||
<text>{{ t.tenant_name }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="searched" class="no-result">未找到企业,请核对全称或改走「创建企业」</view>
|
||
<text v-if="selectedTenant" class="selected">已选:{{ selectedTenant.tenant_name }}</text>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="label">部门 <text class="required">*</text></text>
|
||
<picker :range="deptNames" :value="deptIndex" :disabled="!selectedTenant || !deptNames.length" @change="onDeptChange">
|
||
<view class="picker-value">{{ deptName() }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">职位 <text class="required">*</text></text>
|
||
<picker :range="positionNames" :value="positionIndex" :disabled="!positionNames.length" @change="onPositionChange">
|
||
<view class="picker-value">{{ positionName() }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">姓名 <text class="required">*</text></text>
|
||
<input v-model="applicantName" class="input" placeholder="真实姓名" />
|
||
</view>
|
||
<view class="field">
|
||
<text class="label">手机号</text>
|
||
<view class="readonly"><text>{{ mobileMask || '微信已验证' }}</text><text class="readonly-tag">不可修改</text></view>
|
||
</view>
|
||
<text class="hint">提交后由该企业管理员审核,通过后即可使用业务功能</text>
|
||
</view>
|
||
|
||
<view v-if="errorMsg" class="error"><text>{{ errorMsg }}</text></view>
|
||
<button class="submit" :disabled="loading || (status && status.has_tenant)" @tap="handleSubmit">
|
||
{{ loading ? '提交中…' : (tab === 'create' ? '提交创建申请' : '提交挂靠申请') }}
|
||
</button>
|
||
<view class="back" @tap="goBack"><text>返回工作台</text></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
/**
|
||
* 需企业模块的引导页:创建企业(平台审核) / 挂靠企业(租户管理员审核)。
|
||
* 手机号由后端从微信验证结果读取,前端只读展示,不接受输入。
|
||
*/
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import {
|
||
getOnboardingStatus, searchOnboardingTenants, getOnboardingDepartments,
|
||
getOnboardingPositions, applyCreate, applyJoin
|
||
} from '@/api/onboarding.js'
|
||
import { getBaseURL } from '@/api/request.js'
|
||
|
||
const tab = ref('create')
|
||
const status = ref(null)
|
||
const loading = ref(false)
|
||
const errorMsg = ref('')
|
||
const mobileMask = ref('')
|
||
|
||
const createForm = reactive({
|
||
tenant_name: '', credit_code: '', legal_person: '', license_file: '',
|
||
contact_person: '', contact_phone: '', contact_email: '', address: ''
|
||
})
|
||
const licenseName = ref('')
|
||
|
||
const keyword = ref('')
|
||
const searched = ref(false)
|
||
const tenantList = ref([])
|
||
const selectedTenant = ref(null)
|
||
const deptList = ref([])
|
||
const positionList = ref([])
|
||
const deptIndex = ref(0)
|
||
const positionIndex = ref(0)
|
||
const applicantName = ref('')
|
||
|
||
const deptNames = computed(() => deptList.value.map((d) => d.name))
|
||
const positionNames = computed(() => positionList.value.map((p) => p.name))
|
||
|
||
const statusClass = computed(() => {
|
||
if (!status.value) return ''
|
||
if (status.value.has_tenant) return 'passed'
|
||
const a = status.value.apply
|
||
if (!a) return ''
|
||
return a.status === 0 ? 'pending' : (a.status === 2 ? 'rejected' : 'passed')
|
||
})
|
||
const statusTitle = computed(() => {
|
||
if (!status.value) return ''
|
||
if (status.value.has_tenant) return '已加入 ' + (status.value.tenant_name || '企业')
|
||
const a = status.value.apply
|
||
if (!a) return '尚未提交申请'
|
||
if (a.status === 0) return '审核中'
|
||
if (a.status === 2) return '申请已驳回'
|
||
return '审核已通过'
|
||
})
|
||
const statusDesc = computed(() => {
|
||
if (!status.value) return ''
|
||
if (status.value.has_tenant) return '返回工作台即可使用业务功能'
|
||
const a = status.value.apply
|
||
if (!a) return '请选择「创建企业」或「挂靠企业」并提交资料'
|
||
if (a.status === 0) return '请等待管理员审核,通过后即可使用业务功能'
|
||
if (a.status === 2) return (a.reject_reason || '资料不符合要求') + ',可修改后重新提交'
|
||
return '审核已通过,返回工作台重新进入即可'
|
||
})
|
||
|
||
onShow(() => { loadStatus() })
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await getOnboardingStatus()
|
||
status.value = res || null
|
||
mobileMask.value = (res && res.mobile_mask) || ''
|
||
} catch (e) {
|
||
console.warn('[onboarding] status failed:', e)
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
uni.reLaunch({ url: '/pages/nav/nav' })
|
||
}
|
||
|
||
function chooseLicense() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
success: (res) => {
|
||
const filePath = res.tempFilePaths && res.tempFilePaths[0]
|
||
if (!filePath) return
|
||
uni.showLoading({ title: '上传中...', mask: true })
|
||
uni.uploadFile({
|
||
url: getBaseURL().replace(/\/$/, '') + '/auth/register/upload-license',
|
||
filePath,
|
||
name: 'file',
|
||
success: (r) => {
|
||
try {
|
||
const body = JSON.parse(r.data || '{}')
|
||
if (body.code === 200 && body.data && body.data.url) {
|
||
createForm.license_file = body.data.url
|
||
licenseName.value = body.data.name || '已上传'
|
||
} else {
|
||
uni.showToast({ title: body.msg || '上传失败', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||
}
|
||
},
|
||
fail: () => uni.showToast({ title: '上传失败', icon: 'none' }),
|
||
complete: () => uni.hideLoading()
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
async function searchTenants() {
|
||
const kw = keyword.value.trim()
|
||
if (!kw) {
|
||
errorMsg.value = '请输入企业全称'
|
||
return
|
||
}
|
||
errorMsg.value = ''
|
||
try {
|
||
const res = await searchOnboardingTenants(kw)
|
||
tenantList.value = (res && res.list) || []
|
||
searched.value = true
|
||
selectedTenant.value = null
|
||
deptList.value = []
|
||
positionList.value = []
|
||
} catch (e) {
|
||
errorMsg.value = (e && e.message) || '企业搜索失败'
|
||
}
|
||
}
|
||
|
||
async function chooseTenant(t) {
|
||
selectedTenant.value = t
|
||
keyword.value = t.tenant_name
|
||
deptIndex.value = 0
|
||
positionIndex.value = 0
|
||
positionList.value = []
|
||
try {
|
||
const res = await getOnboardingDepartments(t.tid)
|
||
deptList.value = (res && res.list) || []
|
||
if (deptList.value.length) await loadPositions()
|
||
} catch (e) {
|
||
errorMsg.value = (e && e.message) || '部门列表加载失败'
|
||
}
|
||
}
|
||
|
||
async function loadPositions() {
|
||
const dept = deptList.value[deptIndex.value]
|
||
if (!selectedTenant.value || !dept) return
|
||
try {
|
||
const res = await getOnboardingPositions(selectedTenant.value.tid, dept.id)
|
||
positionList.value = (res && res.list) || []
|
||
positionIndex.value = 0
|
||
} catch (e) {
|
||
errorMsg.value = (e && e.message) || '职位列表加载失败'
|
||
}
|
||
}
|
||
|
||
function onDeptChange(e) {
|
||
deptIndex.value = Number(e.detail.value) || 0
|
||
loadPositions()
|
||
}
|
||
|
||
function onPositionChange(e) {
|
||
positionIndex.value = Number(e.detail.value) || 0
|
||
}
|
||
|
||
function deptName() {
|
||
const d = deptList.value[deptIndex.value]
|
||
return d ? d.name : (selectedTenant.value ? '请选择部门' : '请先搜索并选择企业')
|
||
}
|
||
|
||
function positionName() {
|
||
const p = positionList.value[positionIndex.value]
|
||
return p ? p.name : (deptList.value.length ? '请选择职位' : '请先选择部门')
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (loading.value) return
|
||
errorMsg.value = ''
|
||
if (status.value && status.value.has_tenant) {
|
||
goBack()
|
||
return
|
||
}
|
||
if (tab.value === 'create') await submitCreate()
|
||
else await submitJoin()
|
||
}
|
||
|
||
async function submitCreate() {
|
||
const f = createForm
|
||
f.tenant_name = f.tenant_name.trim()
|
||
f.credit_code = f.credit_code.trim().toUpperCase()
|
||
if (!f.tenant_name || !/^[0-9A-Z]{18}$/.test(f.credit_code) || !f.legal_person.trim() ||
|
||
!f.license_file || !f.contact_person.trim() || !/^1\d{10}$/.test(f.contact_phone.trim()) ||
|
||
!f.contact_email.includes('@')) {
|
||
errorMsg.value = '请完整填写企业信息:信用代码 18 位、营业执照、法人、联系人、11 位手机号与邮箱为必填'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await applyCreate({
|
||
tenant_name: f.tenant_name,
|
||
credit_code: f.credit_code,
|
||
legal_person: f.legal_person.trim(),
|
||
license_file: f.license_file,
|
||
contact_person: f.contact_person.trim(),
|
||
contact_phone: f.contact_phone.trim(),
|
||
contact_email: f.contact_email.trim(),
|
||
address: f.address.trim()
|
||
})
|
||
uni.showToast({ title: '已提交,等待平台审核', icon: 'none' })
|
||
await loadStatus()
|
||
} catch (e) {
|
||
errorMsg.value = (e && e.message) || '提交失败,请重试'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function submitJoin() {
|
||
if (!selectedTenant.value) {
|
||
errorMsg.value = '请先搜索并选择要挂靠的企业'
|
||
return
|
||
}
|
||
const dept = deptList.value[deptIndex.value]
|
||
const pos = positionList.value[positionIndex.value]
|
||
if (!dept || !pos || !applicantName.value.trim()) {
|
||
errorMsg.value = '请选择部门、职位并填写姓名'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await applyJoin({
|
||
tid: selectedTenant.value.tid,
|
||
applicant_name: applicantName.value.trim(),
|
||
department_id: dept.id,
|
||
department_name: dept.name,
|
||
position_id: pos.id,
|
||
position_name: pos.name
|
||
})
|
||
uni.showToast({ title: '已提交,等待企业管理员审核', icon: 'none' })
|
||
await loadStatus()
|
||
} catch (e) {
|
||
errorMsg.value = (e && e.message) || '提交失败,请重试'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page { min-height: 100vh; padding: 0 $page-padding-x 60rpx; box-sizing: border-box; background: #f5f8fd; }
|
||
.hero { padding: 100rpx 8rpx 40rpx; }
|
||
.eyebrow { display: block; font-size: 20rpx; font-weight: 700; letter-spacing: 3rpx; color: #6e9de5; }
|
||
.title { display: block; margin-top: 14rpx; font-size: 44rpx; font-weight: 700; color: #17345f; }
|
||
.desc { display: block; margin-top: 14rpx; font-size: 25rpx; line-height: 1.6; color: #64748b; }
|
||
|
||
.status-card { @include card; padding: 26rpx 28rpx; margin-bottom: 28rpx; border-left: 8rpx solid #94a3b8; }
|
||
.status-card.pending { border-left-color: #f59e0b; }
|
||
.status-card.rejected { border-left-color: #ef4444; }
|
||
.status-card.passed { border-left-color: #10b981; }
|
||
.status-title { display: block; font-size: 30rpx; font-weight: 700; color: #0f172a; }
|
||
.status-desc { display: block; margin-top: 8rpx; font-size: 23rpx; line-height: 1.6; color: #64748b; }
|
||
|
||
.tabs { display: flex; gap: 12rpx; padding: 8rpx; margin-bottom: 24rpx; border-radius: $radius-full; background: #e8eef8; }
|
||
.tab { flex: 1; height: 76rpx; display: flex; align-items: center; justify-content: center; border-radius: $radius-full; font-size: 27rpx; color: #64748b; }
|
||
.tab.active { background: #ffffff; color: #2563eb; font-weight: 700; box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, .08); }
|
||
|
||
.card { @include card; padding: 32rpx 28rpx 36rpx; }
|
||
.field { margin-bottom: 26rpx; }
|
||
.label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
|
||
.required { color: #ef4444; }
|
||
.input { height: 84rpx; padding: 0 22rpx; border: 1rpx solid #dce7f7; border-radius: $radius-md; background: #fbfdff; font-size: 27rpx; color: $color-text; box-sizing: border-box; }
|
||
.hint { display: block; font-size: 22rpx; line-height: 1.6; color: $color-text-muted; }
|
||
.error { margin-top: 24rpx; font-size: 24rpx; color: $color-danger; }
|
||
|
||
.upload { min-height: 88rpx; padding: 24rpx 22rpx; border: 1rpx dashed #b9d2ff; border-radius: $radius-md; background: #f7faff; font-size: 24rpx; color: #2563eb; }
|
||
.search-row { display: flex; gap: 14rpx; }
|
||
.search-input { flex: 1; }
|
||
.search-btn { width: 140rpx; height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: $radius-md; background: #2563eb; color: #fff; font-size: 26rpx; }
|
||
.result-list { margin-top: 14rpx; border: 1rpx solid #e2e8f0; border-radius: $radius-md; overflow: hidden; }
|
||
.result-row { padding: 20rpx 22rpx; font-size: 26rpx; color: #334155; border-bottom: 1rpx solid #eef2f7; }
|
||
.result-row:last-child { border-bottom: none; }
|
||
.no-result { margin-top: 14rpx; font-size: 23rpx; color: #94a3b8; }
|
||
.selected { display: block; margin-top: 14rpx; font-size: 24rpx; color: #2563eb; }
|
||
.picker-value { height: 84rpx; line-height: 84rpx; padding: 0 22rpx; border: 1rpx solid #dce7f7; border-radius: $radius-md; background: #fbfdff; font-size: 27rpx; color: $color-text; }
|
||
.readonly { height: 84rpx; display: flex; align-items: center; justify-content: space-between; padding: 0 22rpx; border: 1rpx solid #e2e8f0; border-radius: $radius-md; background: #f1f5f9; font-size: 27rpx; color: #475569; }
|
||
.readonly-tag { font-size: 21rpx; color: #94a3b8; }
|
||
|
||
.submit { margin-top: 36rpx; height: 92rpx; line-height: 92rpx; border: none; border-radius: $radius-full; background: linear-gradient(135deg, #60a5fa, #2563eb); color: #fff; font-size: 30rpx; font-weight: 600; box-shadow: 0 10rpx 24rpx rgba(37, 99, 235, .22);
|
||
&::after { border: none; }
|
||
&[disabled] { opacity: .6; }
|
||
}
|
||
.back { margin-top: 28rpx; text-align: center; font-size: 25rpx; color: #94a3b8; }
|
||
</style>
|