371 lines
14 KiB
Vue
371 lines
14 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="header" :style="headPad">
|
||
<view class="header-back" @tap="goBack">
|
||
<FaIcon name="chevron-left" color="#ffffff" :size="14" />
|
||
<text>返回</text>
|
||
</view>
|
||
<text class="header-title">企业绑定</text>
|
||
<text class="header-sub">一个账号可绑定多家企业,绑定申请由对应企业的管理员审核</text>
|
||
</view>
|
||
|
||
<scroll-view class="body" scroll-y>
|
||
<!-- 已绑定企业 -->
|
||
<view class="card">
|
||
<view class="card-head">
|
||
<text class="card-title">已绑定企业</text>
|
||
<text class="card-count">{{ tenants.length }} 家</text>
|
||
</view>
|
||
<view v-if="!tenants.length" class="empty">暂未绑定企业,可在下方提交绑定申请</view>
|
||
<view v-for="t in tenants" :key="t.tid" class="tenant-row">
|
||
<view class="tenant-badge"><FaIcon name="building" color="#ffffff" :size="17" /></view>
|
||
<view class="tenant-copy">
|
||
<text class="tenant-name">{{ t.tenant_name || '企业 ' + t.tid }}</text>
|
||
<text class="tenant-meta">企业 ID {{ t.tid }}</text>
|
||
</view>
|
||
<text v-if="String(t.tid) === String(getTid())" class="current-tag">当前</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 绑定其他企业 -->
|
||
<view class="card">
|
||
<text class="card-title">绑定其他企业</text>
|
||
<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 v-if="formError" class="form-error">{{ formError }}</text>
|
||
<button class="submit" :disabled="submitting" @tap="submitBind">
|
||
{{ submitting ? '提交中…' : '提交绑定申请' }}
|
||
</button>
|
||
<text class="form-tip">提交后由该企业管理员审核,通过后可在工作台切换企业</text>
|
||
</view>
|
||
|
||
<!-- 申请记录 -->
|
||
<view class="card">
|
||
<text class="card-title">申请记录</text>
|
||
<view v-if="!applies.length" class="empty">暂无申请记录</view>
|
||
<view v-for="a in applies" :key="a.id" class="apply-row">
|
||
<view class="apply-copy">
|
||
<text class="apply-name">{{ a.tenant_name || '企业申请' }}</text>
|
||
<text class="apply-meta">{{ a.type === 2 ? '创建企业' : '绑定企业' }}<text v-if="a.department_name || a.position_name"> · {{ a.department_name || '' }} {{ a.position_name || '' }}</text></text>
|
||
<text v-if="a.status === 2" class="apply-reason">驳回原因:{{ a.reject_reason || '未填写' }}</text>
|
||
</view>
|
||
<text class="apply-status" :class="statusClass(a.status)">{{ statusText(a.status) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="bottom-space" />
|
||
</scroll-view>
|
||
|
||
<AppTabbar current="bind" :tabs="profileTabs" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
/**
|
||
* 「我的 → 企业绑定」:展示已绑定的多租户,并向其他企业提交绑定申请。
|
||
* 复用挂靠申请链路(企业搜索 / 部门 / 职位 / 微信手机号只读 / 租户管理员审核)。
|
||
*/
|
||
import { ref, computed } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import {
|
||
getOnboardingStatus, searchOnboardingTenants, getOnboardingDepartments,
|
||
getOnboardingPositions, applyJoin
|
||
} from '@/api/onboarding.js'
|
||
import { getTid, getUser, setTenants } from '@/utils/auth.js'
|
||
import { mpHeadPad } from '@/utils/mpHead.js'
|
||
import AppTabbar from '@/components/MainTabbar.vue'
|
||
|
||
const headPad = mpHeadPad()
|
||
const profileTabs = [
|
||
{ name: 'profile', text: '我的', icon: 'user' },
|
||
{ name: 'platform', text: '回平台', icon: 'arrow-left' }
|
||
]
|
||
|
||
const tenants = ref([])
|
||
const applies = ref([])
|
||
const mobileMask = ref('')
|
||
const submitting = ref(false)
|
||
const formError = 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((getUser()?.name || getUser()?.nickname || ''))
|
||
|
||
const deptNames = computed(() => deptList.value.map((d) => d.name))
|
||
const positionNames = computed(() => positionList.value.map((p) => p.name))
|
||
|
||
onShow(() => { loadStatus() })
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await getOnboardingStatus()
|
||
const list = Array.isArray(res?.tenants) ? res.tenants : []
|
||
tenants.value = list
|
||
applies.value = Array.isArray(res?.applies) ? res.applies : []
|
||
mobileMask.value = res?.mobile_mask || ''
|
||
// 同步本地企业列表,工作台右上角「切换」立即可用
|
||
setTenants(list.map((t) => ({ tid: t.tid, tenant_name: t.tenant_name })))
|
||
} catch (e) {
|
||
console.warn('[bindTenant] status failed:', e)
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack({ fail: () => uni.redirectTo({ url: '/pages/profile/profile' }) })
|
||
}
|
||
|
||
async function searchTenants() {
|
||
const kw = keyword.value.trim()
|
||
if (!kw) {
|
||
formError.value = '请输入企业全称'
|
||
return
|
||
}
|
||
formError.value = ''
|
||
try {
|
||
const res = await searchOnboardingTenants(kw)
|
||
tenantList.value = Array.isArray(res?.list) ? res.list : []
|
||
searched.value = true
|
||
selectedTenant.value = null
|
||
deptList.value = []
|
||
positionList.value = []
|
||
} catch (e) {
|
||
formError.value = (e && e.message) || '企业搜索失败'
|
||
}
|
||
}
|
||
|
||
async function chooseTenant(t) {
|
||
selectedTenant.value = t
|
||
keyword.value = t.tenant_name
|
||
deptIndex.value = 0
|
||
positionIndex.value = 0
|
||
positionList.value = []
|
||
formError.value = ''
|
||
try {
|
||
const res = await getOnboardingDepartments(t.tid)
|
||
deptList.value = Array.isArray(res?.list) ? res.list : []
|
||
if (deptList.value.length) await loadPositions()
|
||
} catch (e) {
|
||
formError.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 = Array.isArray(res?.list) ? res.list : []
|
||
positionIndex.value = 0
|
||
} catch (e) {
|
||
formError.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 ? '请选择职位' : '请先选择部门')
|
||
}
|
||
|
||
function statusText(s) {
|
||
if (s === 0) return '待审核'
|
||
if (s === 1) return '已通过'
|
||
return '已驳回'
|
||
}
|
||
|
||
function statusClass(s) {
|
||
if (s === 0) return 'pending'
|
||
if (s === 1) return 'passed'
|
||
return 'rejected'
|
||
}
|
||
|
||
function resetForm() {
|
||
keyword.value = ''
|
||
searched.value = false
|
||
tenantList.value = []
|
||
selectedTenant.value = null
|
||
deptList.value = []
|
||
positionList.value = []
|
||
deptIndex.value = 0
|
||
positionIndex.value = 0
|
||
}
|
||
|
||
async function submitBind() {
|
||
if (submitting.value) return
|
||
formError.value = ''
|
||
if (!selectedTenant.value) {
|
||
formError.value = '请先搜索并选择要绑定的企业'
|
||
return
|
||
}
|
||
const dept = deptList.value[deptIndex.value]
|
||
const pos = positionList.value[positionIndex.value]
|
||
if (!dept || !pos || !applicantName.value.trim()) {
|
||
formError.value = '请选择部门、职位并填写姓名'
|
||
return
|
||
}
|
||
submitting.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' })
|
||
resetForm()
|
||
await loadStatus()
|
||
} catch (e) {
|
||
formError.value = (e && e.message) || '提交失败,请重试'
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page { @include page-shell; }
|
||
|
||
.header {
|
||
flex-shrink: 0;
|
||
padding: calc(var(--status-bar-height, 44px) + 28rpx) $page-padding-x 36rpx;
|
||
/* #ifdef MP-WEIXIN */
|
||
padding-top: calc(env(safe-area-inset-top, 25px) + 88rpx);
|
||
/* #endif */
|
||
background: $hero-gradient;
|
||
color: #ffffff;
|
||
}
|
||
.header-back {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 20rpx;
|
||
border-radius: $radius-full;
|
||
background: rgba(255, 255, 255, .18);
|
||
font-size: 24rpx;
|
||
}
|
||
.header-title { display: block; margin-top: 28rpx; font-size: 40rpx; font-weight: 700; }
|
||
.header-sub { display: block; margin-top: 10rpx; font-size: 23rpx; line-height: 1.6; color: rgba(255, 255, 255, .8); }
|
||
|
||
.body { flex: 1; min-height: 0; padding: 24rpx $page-padding-x 0; box-sizing: border-box; }
|
||
.card { @include card; padding: 28rpx 26rpx 32rpx; margin-bottom: 24rpx; }
|
||
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
|
||
.card-title { font-size: 30rpx; font-weight: 700; color: $color-text; }
|
||
.card-count { font-size: 23rpx; color: $color-text-muted; }
|
||
.empty { padding: 32rpx 0; text-align: center; font-size: 24rpx; color: $color-text-muted; }
|
||
|
||
.tenant-row { display: flex; align-items: center; gap: 18rpx; padding: 20rpx 0; border-bottom: 1rpx solid $color-divider; }
|
||
.tenant-row:last-child { border-bottom: none; }
|
||
.tenant-badge {
|
||
width: 68rpx; height: 68rpx; flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-radius: 18rpx;
|
||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||
}
|
||
.tenant-copy { flex: 1; min-width: 0; }
|
||
.tenant-name { display: block; font-size: 27rpx; font-weight: 600; color: $color-text; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.tenant-meta { display: block; margin-top: 6rpx; font-size: 21rpx; color: $color-text-muted; }
|
||
.current-tag { flex-shrink: 0; padding: 6rpx 16rpx; border-radius: $radius-full; background: $color-primary-bg; font-size: 20rpx; color: $color-primary; }
|
||
|
||
.field { margin-top: 24rpx; }
|
||
.label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
|
||
.required { color: #ef4444; }
|
||
.input {
|
||
height: 84rpx; padding: 0 22rpx; box-sizing: border-box;
|
||
border: 1rpx solid #dce7f7; border-radius: $radius-md;
|
||
background: #fbfdff; font-size: 27rpx; color: $color-text;
|
||
}
|
||
.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; }
|
||
.form-error { display: block; margin: 20rpx 0 0; font-size: 24rpx; color: $color-danger; }
|
||
.form-tip { display: block; margin-top: 16rpx; font-size: 22rpx; line-height: 1.6; color: $color-text-muted; }
|
||
.submit {
|
||
height: 90rpx; line-height: 90rpx; margin-top: 28rpx;
|
||
border: none; border-radius: $radius-full;
|
||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||
color: #fff; font-size: 29rpx; font-weight: 600;
|
||
|
||
&::after { border: none; }
|
||
&[disabled] { opacity: .6; }
|
||
}
|
||
|
||
.apply-row { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 0; border-bottom: 1rpx solid $color-divider; }
|
||
.apply-row:last-child { border-bottom: none; }
|
||
.apply-copy { flex: 1; min-width: 0; }
|
||
.apply-name { display: block; font-size: 27rpx; font-weight: 600; color: $color-text; }
|
||
.apply-meta { display: block; margin-top: 6rpx; font-size: 22rpx; color: $color-text-muted; }
|
||
.apply-reason { display: block; margin-top: 6rpx; font-size: 22rpx; color: #b42318; }
|
||
.apply-status { flex-shrink: 0; padding: 6rpx 16rpx; border-radius: $radius-full; font-size: 20rpx; background: #f1f5f9; color: #64748b; }
|
||
.apply-status.pending { background: #fff7e6; color: #b45309; }
|
||
.apply-status.passed { background: #e8f8f0; color: #047857; }
|
||
.apply-status.rejected { background: #fef2f2; color: #b91c1c; }
|
||
|
||
.bottom-space { height: 40rpx; }
|
||
</style>
|