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