Files
2026-11-11 18:42:09 +08:00

371 lines
14 KiB
Vue
Raw Permalink 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="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>