Files
yunzerwebsiteallinone/uniapp/pages/onboarding/company.vue
T
2026-11-11 16:58:30 +08:00

403 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>