更新
This commit is contained in:
1 parent
fb04d66e61
commit
b6b528490c
13 files changed
+518
-168
No files matched your search
@@ -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)).
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -114,6 +114,13 @@
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/bindTenant",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "企业绑定"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
|
||||
+30
-12
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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 {
|
||||
|
||||
Reference in new issue
Block a user