This commit is contained in:
hero920103 committed 2026-11-11 18:42:09 +08:00
1 parent fb04d66e61
commit b6b528490c
13 files changed
+518 -168

No files matched your search

+17 -4
View File
@@ -194,9 +194,18 @@ func (c *AppAggregateController) noTenantHome(claims *jwtutil.Claims) map[string
}
}
// 展示姓名取统一身份昵称(个人中心可改),不再把账号/手机号当名字下发
displayName := ""
var ai models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", uint64(claims.UserID)).
Filter("delete_time__isnull", true).One(&ai); err == nil && ai.Nickname != nil {
displayName = strings.TrimSpace(*ai.Nickname)
}
return map[string]interface{}{
"user": map[string]interface{}{
"account": claims.Username, "tenant": "", "tid": 0, "need_tenant": true,
"name": displayName, "account": claims.Username, "tenant": "", "tid": 0, "need_tenant": true,
},
"modules": modules,
"menus": menus,
@@ -396,15 +405,19 @@ func (c *AppAggregateController) appUser(claims *jwtutil.Claims, uid, tid uint64
out := map[string]interface{}{
"uid": uid,
"tid": tid,
"name": "用户",
"name": "",
"account": "",
"role_name": "",
"tenant": "",
"tenant_id": tid,
"avatar": "",
}
if claims.Username != "" {
out["name"] = claims.Username
// 姓名只取真实资料(统一身份昵称,个人中心可改),不再用账号/手机号冒充
var ai models.AuthIdentity
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", uint64(claims.UserID)).
Filter("delete_time__isnull", true).One(&ai); err == nil && ai.Nickname != nil {
out["name"] = strings.TrimSpace(*ai.Nickname)
}
var tu models.SystemTenantUser
if models.Orm.QueryTable(new(models.SystemTenantUser)).
+5 -3
View File
@@ -168,12 +168,14 @@ func (c *AppAuthController) UpdateCurrentUserProfile() {
}
uid := services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
num, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
// 尚未加入企业(tid=0)的新注册用户没有老表行,属正常情况;
// 姓名仍写入统一身份,工作台首页直接读它显示。
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("uid", uid).
Filter("tid", claims.TenantId).
Update(map[string]interface{}{"name": name, "email": email})
if err != nil || num == 0 {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "未找到当前企业下的用户资料"})
if err != nil {
c.serveJSON(map[string]interface{}{"code": 500, "msg": "资料更新失败,请重试"})
return
}
_, _ = models.Orm.QueryTable(new(models.AuthIdentity)).
+46 -30
View File
@@ -249,7 +249,9 @@ func (c *OnboardingController) issueNoTenantTokens(identityID uint64, mobile str
}
// Status GET /api/onboarding/status
// 返回企业绑定、最新申请状态、微信手机号(前端只读展示)。
// 返回已绑定企业列表、申请记录、微信手机号(前端只读展示)。
// 「我的 → 企业绑定」用 tenants + applies 展示多租户与审核进度;
// apply 保留最新一条,兼容首次加入企业的工作台分流逻辑。
func (c *OnboardingController) Status() {
cl := c.mustClaims()
if cl == nil {
@@ -264,46 +266,65 @@ func (c *OnboardingController) Status() {
mobile = *ai.Mobile
}
// 一人多企业:返回全部启用中的绑定,默认企业排在最前
var binds []models.AuthTenantUser
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
Filter("identity_id", identityID).
Filter("status", 1).
OrderBy("-is_default", "id").
All(&binds)
hasTenant := len(binds) > 0
tenants := make([]map[string]interface{}, 0, len(binds))
for _, b := range binds {
name := ""
var t models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", b.Tid).One(&t); err == nil {
name = t.TenantName
}
tenants = append(tenants, map[string]interface{}{
"tid": b.Tid, "tenant_name": name, "is_default": b.IsDefault,
})
}
tenantName := ""
tid := uint64(0)
if hasTenant {
if len(tenants) > 0 {
tenantName, _ = tenants[0]["tenant_name"].(string)
tid = binds[0].Tid
var t models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&t); err == nil {
tenantName = t.TenantName
}
}
var applyData interface{}
var apply models.AuthTenantApply
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
// 最近 20 条申请:可同时存在多家企业的待审绑定申请
var rows []models.AuthTenantApply
_, _ = models.Orm.QueryTable(new(models.AuthTenantApply)).
Filter("identity_id", identityID).
Filter("delete_time__isnull", true).
OrderBy("-id").One(&apply); err == nil {
applyData = map[string]interface{}{
"id": apply.ID,
"type": apply.Type,
"status": apply.Status,
"tenant_name": apply.TenantName,
"reject_reason": apply.RejectReason,
"department_name": apply.DepartmentName,
"position_name": apply.PositionName,
"applicant_name": apply.ApplicantName,
}
OrderBy("-id").Limit(20).
All(&rows)
applies := make([]map[string]interface{}, 0, len(rows))
for _, a := range rows {
applies = append(applies, map[string]interface{}{
"id": a.ID,
"type": a.Type,
"status": a.Status,
"tenant_name": a.TenantName,
"reject_reason": a.RejectReason,
"department_name": a.DepartmentName,
"position_name": a.PositionName,
"applicant_name": a.ApplicantName,
"create_time": a.CreateTime,
})
}
var applyData interface{}
if len(applies) > 0 {
applyData = applies[0]
}
c.ok(map[string]interface{}{
"has_tenant": hasTenant,
"has_tenant": len(tenants) > 0,
"tid": tid,
"tenant_name": tenantName,
"mobile": mobile,
"mobile_mask": maskPhone(mobile),
"tenants": tenants,
"applies": applies,
"apply": applyData,
})
}
@@ -383,10 +404,8 @@ func (c *OnboardingController) ApplyJoin() {
if cl == nil {
return
}
if c.hasTenant(uint64(cl.UserID)) {
c.fail(400, "你已加入企业,无需重复申请")
return
}
// 一人可绑定多家企业:已加入企业也能继续申请绑定其他企业。
// 「是否已绑定该企业 / 是否重复待审」由 CreateJoinApply 内部按 tid 判定。
var req struct {
Tid uint64 `json:"tid"`
ApplicantName string `json:"applicant_name"`
@@ -429,10 +448,7 @@ func (c *OnboardingController) ApplyCreate() {
if cl == nil {
return
}
if c.hasTenant(uint64(cl.UserID)) {
c.fail(400, "你已加入企业,无需重复申请")
return
}
// 多租户:已有企业也允许再创建一家(同身份只允许一条待审创建申请,由 CreateTenantApply 校验)
var req services.TenantCreateApplyInput
if !c.readJSON(&req) {
return
+18 -5
View File
@@ -2,7 +2,7 @@
<view class="app-tabbar">
<view class="tabbar-inner">
<view
v-for="tab in tabs"
v-for="tab in list"
:key="tab.name"
class="tab-item"
:class="{ active: current === tab.name }"
@@ -21,23 +21,32 @@
</template>
<script setup>
import { computed } from 'vue'
/**
* 主 tabbar —— 工作台 / 应用 / 我的。
* 仅这三个页面使用;子系统内部页面请用 AppTabbar(带 group 参数)。
* 主 tabbar —— 默认「工作台 / 应用 / 我的」。
* 传入 tabs 时按自定义页签渲染(个人中心用「我的 / 回平台」两项);
* name=platform 为特殊动作:清空页面栈回到平台模块选择页。
*/
const props = defineProps({
current: {
type: String,
default: 'dashboard'
},
tabs: {
type: Array,
default: null
}
})
const tabs = [
const DEFAULT_TABS = [
{ name: 'dashboard', text: '工作台', icon: 'grip' },
{ name: 'features', text: '应用', icon: 'th-large' },
{ name: 'profile', text: '我的', icon: 'user' }
]
const list = computed(() => (props.tabs && props.tabs.length ? props.tabs : DEFAULT_TABS))
const routes = {
dashboard: '/pages/dashboard/dashboard',
features: '/pages/features/features',
@@ -45,7 +54,11 @@ const routes = {
}
function onChange(name) {
if (name === props.current) return
if (name === 'platform') {
uni.reLaunch({ url: '/pages/nav/nav' })
return
}
if (name === props.current || !routes[name]) return
uni.redirectTo({ url: routes[name] })
}
</script>
+2 -16
View File
@@ -1,10 +1,6 @@
<template>
<view class="page" :class="{ embedded }">
<view v-if="!embedded" class="head" :style="headPad">
<view class="head-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#1f2b48" :size="18" />
<text class="head-back">返回导航</text>
</view>
<text class="head-title">{{ sub.title }}</text>
</view>
@@ -204,20 +200,10 @@ onShow(() => {
box-shadow: $shadow-header;
}
.head-row {
display: flex;
align-items: center;
gap: 4rpx;
}
.head-back {
font-size: 24rpx;
color: $color-text-muted;
}
.head-title {
display: block;
margin-top: 14rpx;
/* 取消顶部返回按钮后,保留头部避让高度 */
margin-top: 32rpx;
font-size: 36rpx;
font-weight: 600;
color: $color-text;
+2 -17
View File
@@ -2,10 +2,6 @@
<view class="page" :class="{ embedded }">
<!-- 顶部标识条(独立页模式使用;嵌入 tabs.vue 时由父页提供) -->
<view v-if="!embedded" class="hero" :class="sub.tint" :style="headPad">
<view class="hero-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#ffffff" :size="18" />
<text class="hero-back">返回导航</text>
</view>
<view class="hero-main">
<view class="hero-icon">
<FaIcon :name="sub.icon" color="#ffffff" :size="26" />
@@ -446,23 +442,12 @@ onShow(() => {
color: #ffffff;
}
.hero-row {
display: flex;
align-items: center;
gap: 4rpx;
opacity: 0.85;
}
.hero-back {
font-size: 24rpx;
color: #ffffff;
}
.hero-main {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 24rpx;
/* 取消顶部返回按钮后,补偿其占位,保证与状态栏/胶囊的间距 */
margin-top: 40rpx;
}
.hero-icon {
+2 -17
View File
@@ -2,10 +2,6 @@
<view class="page" :class="{ embedded }">
<!-- 独立页模式:渐变用户头部(嵌入 tabs.vue 时由父页提供顶部条) -->
<view v-if="!embedded" class="head" :style="headPad">
<view class="head-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#ffffff" :size="18" />
<text class="head-back">返回导航</text>
</view>
<view class="user">
<view class="avatar">
<text class="avatar-text">{{ avatarText }}</text>
@@ -242,23 +238,12 @@ onShow(() => {
background: $hero-gradient;
}
.head-row {
display: flex;
align-items: center;
gap: 4rpx;
opacity: 0.85;
}
.head-back {
font-size: 24rpx;
color: #ffffff;
}
.user {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 26rpx;
/* 取消顶部返回按钮后,补偿其占位 */
margin-top: 42rpx;
}
.avatar {
+2 -16
View File
@@ -1,10 +1,6 @@
<template>
<view class="page" :class="{ embedded }">
<view v-if="!embedded" class="head" :style="headPad">
<view class="head-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#1f2b48" :size="18" />
<text class="head-back">返回导航</text>
</view>
<text class="head-title">{{ sub.title }} · 待办</text>
<view class="kpi">
<view class="kpi-item">
@@ -149,20 +145,10 @@ onShow(() => {
box-shadow: $shadow-header;
}
.head-row {
display: flex;
align-items: center;
gap: 4rpx;
}
.head-back {
font-size: 24rpx;
color: $color-text-muted;
}
.head-title {
display: block;
margin-top: 14rpx;
/* 取消顶部返回按钮后,保留头部避让高度 */
margin-top: 32rpx;
font-size: 36rpx;
font-weight: 600;
color: $color-text;
+7
View File
@@ -114,6 +114,13 @@
"navigationStyle": "custom",
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/profile/bindTenant",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "企业绑定"
}
}
],
"subPackages": [
+30 -12
View File
@@ -7,8 +7,8 @@
<text class="head-eyebrow">WENXIN WORKSPACE</text>
<text class="head-title">工作台</text>
</view>
<view v-if="!guest" class="account-chip" @tap="goProfile"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
<view v-else class="account-chip" @tap="goProfile"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></view>
<view v-if="!guest" class="account-chip" @tap="goProfile"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.name || user.nickname || '未设置姓名' }}</text></view>
<button v-else class="account-chip account-chip-button" open-type="getPhoneNumber" @getphonenumber="onCardPhone($event)"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></button>
</view>
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
@@ -471,12 +471,8 @@ async function load(force = false) {
/* ==================== 交互 ==================== */
/** 点击右上角用户名 → 进入个人中心修改资料;未授权时提示先点模块授权手机号 */
/** 点击右上角用户名 → 进入个人中心修改资料(游客态 chip 不绑定此函数) */
function goProfile() {
if (guest.value) {
uni.showToast({ title: '请先点击模块授权手机号', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/profile/profile' })
}
@@ -491,8 +487,8 @@ function chooseTenant() {
/* ==================== 手机号授权 / 注册 / 企业分流 ==================== */
/** 点卡片(未授权时)直接触发微信手机号授权;有账号静默登录,无账号去设密码注册 */
async function onCardPhone(e, card) {
/** 手机号授权统一入口:card 为空表示只做识别/注册(如点「游客」),不进入模块 */
async function onCardPhone(e, card = null) {
const detail = (e && e.detail) || {}
if (String(detail.errMsg || '').indexOf('ok') === -1 || !detail.code) {
uni.showToast({ title: '需要授权手机号才能使用', icon: 'none' })
@@ -509,7 +505,7 @@ async function onCardPhone(e, card) {
} catch (err) {
appid = ''
}
setPendingCard(card)
if (card) setPendingCard(card)
const res = await onboardingPhone({ code, phoneCode: detail.code, appid })
if (res && res.need_register) {
uni.hideLoading()
@@ -523,8 +519,10 @@ async function onCardPhone(e, card) {
const ready = await silentLogin(code, detail.code)
if (!ready) return
await load(true)
await enter(card)
takePendingCard()
if (card) {
await enter(card)
takePendingCard()
}
} catch (err) {
console.error('[nav] onboarding phone failed:', err)
uni.showToast({ title: (err && err.message) || '授权失败,请重试', icon: 'none' })
@@ -678,6 +676,26 @@ onShow(() => {
font-size: 22rpx;
}
/* 游客态 chip 是微信手机号授权按钮:抹平 button 默认样式,外观与普通 chip 一致 */
.account-chip-button {
display: flex;
align-items: center;
gap: 9rpx;
margin: 0;
padding: 12rpx 17rpx;
border: none;
border-radius: 30rpx;
background: rgba(255, 255, 255, .85);
color: #526b8d;
font-size: 22rpx;
line-height: 1.4;
text-align: left;
&::after {
border: none;
}
}
.tenant-switch {
display: flex;
align-items: center;
+370
View File
@@ -0,0 +1,370 @@
<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>
+15 -29
View File
@@ -1,12 +1,6 @@
<template>
<view class="page">
<view class="profile-header" :style="headPad">
<view class="header-top">
<view class="back-platform" @tap="goBackPlatform">
<FaIcon name="chevron-left" color="#ffffff" :size="14" />
<text>回平台</text>
</view>
</view>
<view class="profile-info">
<view class="profile-avatar">
<text class="avatar-text">{{ avatarText }}</text>
@@ -63,7 +57,7 @@
<view class="bottom-space" />
</view>
<AppTabbar current="profile" />
<AppTabbar current="profile" :tabs="profileTabs" />
<!-- 编辑资料:姓名 / 邮箱可改,手机号由微信验证只读 -->
<view v-if="editVisible" class="edit-mask" @tap="closeEdit">
@@ -108,6 +102,12 @@ import AppTabbar from '@/components/MainTabbar.vue'
const user = ref(null)
// 个人中心底部只保留两项:左侧「我的」、右侧「回平台」(platform 由 MainTabbar 处理跳转)
const profileTabs = [
{ name: 'profile', text: '我的', icon: 'user' },
{ name: 'platform', text: '回平台', icon: 'arrow-left' }
]
const avatarText = computed(() => {
const name = user.value?.nickname || user.value?.name || '云'
return name.charAt(0).toUpperCase()
@@ -135,6 +135,7 @@ const menuGroups = ref([
{ title: '浏览历史', icon: 'clock-rotate-left' }
],
[
{ title: '企业绑定', icon: 'building' },
{ title: '消息通知', icon: 'bell', badge: '3' },
{ title: '账号安全', icon: 'lock' },
{ title: '隐私设置', icon: 'eye' }
@@ -234,11 +235,13 @@ async function saveProfile() {
}
}
function goBackPlatform() {
uni.reLaunch({ url: '/pages/nav/nav' })
}
function onMenuTap(item) {
if (item.title === '企业绑定') {
uni.navigateTo({ url: '/pages/profile/bindTenant' })
return
}
if (item.title === '账号安全') {
uni.navigateTo({ url: '/pages/login/forget' })
return
@@ -319,6 +322,8 @@ function handleLogout() {
display: flex;
align-items: center;
gap: 24rpx;
/* 取消顶部回平台按钮后补偿占位,headPad 内联 paddingTop 继续避让状态栏/胶囊 */
margin-top: 20rpx;
position: relative;
z-index: 1;
}
@@ -452,25 +457,6 @@ function handleLogout() {
.bottom-space {
height: 48rpx;
}
/* ============ 回平台 ============ */
.header-top {
display: flex;
align-items: center;
margin-bottom: 24rpx;
position: relative;
z-index: 1;
}
.back-platform {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 20rpx;
border-radius: $radius-full;
background: rgba(255, 255, 255, .18);
color: #ffffff;
font-size: 24rpx;
}
/* ============ 编辑资料弹层 ============ */
.edit-mask {
position: fixed;
+2 -19
View File
@@ -2,9 +2,6 @@
<view class="page">
<!-- 顶部子系统标识条 -->
<view v-if="sub" class="hero" :class="sub.tint" :style="headPad">
<view class="hero-back" @tap="goBackNav">
<FaIcon name="chevron-left" color="#ffffff" :size="14" />
</view>
<view class="hero-main">
<view class="hero-icon">
<FaIcon :name="sub.icon" color="#ffffff" :size="24" />
@@ -106,26 +103,12 @@ function goBackNav() {
padding: calc(var(--status-bar-height, 44px) + 32rpx) $page-padding-x 72rpx;
}
.hero-back {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.22);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
&:active {
transform: scale(0.92);
}
}
.hero-main {
display: flex;
align-items: center;
gap: 22rpx;
margin-top: 28rpx;
/* 取消顶部回平台按钮后,补偿其占位;headPad 内联 paddingTop 继续避让胶囊 */
margin-top: 44rpx;
}
.hero-icon {