This commit is contained in:
hero920103 committed 2026-11-11 17:30:06 +08:00
1 parent 8fc52c574f
commit fb04d66e61
7 files changed
+284 -9

No files matched your search

+61
View File
@@ -130,6 +130,67 @@ func (c *AppAuthController) GetCurrentUser() {
})
}
// UpdateCurrentUserProfile 修改当前用户资料(姓名 / 邮箱)。
// 手机号由微信验证并作为登录账号,不开放修改;同步统一身份与当前企业绑定表,
// 避免下次登录或切换企业后又读到旧的姓名。
func (c *AppAuthController) UpdateCurrentUserProfile() {
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndBackend, identity.EndApp)
if err != nil {
code := identity.Status(err)
c.serveJSON(map[string]interface{}{"code": code, "msg": err.Error()})
return
}
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
body := c.Ctx.Input.RequestBody
if len(body) == 0 {
body, _ = io.ReadAll(c.Ctx.Request.Body)
}
if err := json.Unmarshal(body, &req); err != nil {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
return
}
name := strings.TrimSpace(req.Name)
email := strings.TrimSpace(req.Email)
if name == "" {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请填写姓名"})
return
}
if len([]rune(name)) > 30 {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "姓名不能超过 30 个字符"})
return
}
if email != "" && (!strings.Contains(email, "@") || len(email) > 128) {
c.serveJSON(map[string]interface{}{"code": 400, "msg": "邮箱格式不正确"})
return
}
uid := services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
num, 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": "未找到当前企业下的用户资料"})
return
}
_, _ = models.Orm.QueryTable(new(models.AuthIdentity)).
Filter("id", uint64(claims.UserID)).
Update(map[string]interface{}{"nickname": name, "email": email})
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
Filter("identity_id", uint64(claims.UserID)).
Filter("tid", claims.TenantId).
Update(map[string]interface{}{"name": name, "email": email})
c.serveJSON(map[string]interface{}{
"code": 200,
"msg": "资料已更新",
"data": map[string]interface{}{"name": name, "email": email},
})
}
// SendLoginCode 发送App端登录验证码
func (c *AppAuthController) SendLoginCode() {
var req struct {
+2
View File
@@ -54,6 +54,8 @@ func Register() {
// 当前用户信息
beego.Router("/api/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser")
// 修改当前用户资料(姓名 / 邮箱;手机号由微信验证,不开放修改)
beego.Router("/api/currentUser/profile", &controllers.AppAuthController{}, "post:UpdateCurrentUserProfile")
// 极验与登录验证配置
beego.Router("/api/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos")
+10
View File
@@ -208,6 +208,16 @@ export function getCurrentUser() {
})
}
/** 修改当前用户资料:{ name, email };手机号由微信验证,只读不传 */
export function updateCurrentUserProfile(data) {
return request({
url: '/api/currentUser/profile',
method: 'POST',
data,
silent: true
})
}
/** 是否开启登录人机/验证码校验 */
export function getOpenVerify() {
return request({
+8 -1
View File
@@ -56,12 +56,19 @@ const DEFAULT_TABS = [
{ name: 'home', text: '概览', icon: 'gauge' },
{ name: 'functions', text: '功能', icon: 'table-cells' },
{ name: 'todo', text: '待办', icon: 'circle-check' },
{ name: 'mine', text: '我的', icon: 'user' }
// 末位不再是「我的」,而是回平台(模块选择页);name 保留 mine 以兼容 current="mine"
{ name: 'mine', text: '回平台', icon: 'arrow-left' }
]
const list = computed(() => (props.tabs && props.tabs.length ? props.tabs : DEFAULT_TABS))
function onTap(name) {
// 「回平台」:清空页面栈回到模块选择页。必须放在 current 判重之前,
// 否则停在原「我的」页时点击会被判重拦下。
if (name === 'mine' || name === 'platform') {
uni.reLaunch({ url: '/pages/nav/nav' })
return
}
if (name === props.current) return
if (props.variant === 'switch') {
emit('change', name)
+11 -2
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"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
<view v-else class="account-chip"><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.account || '我的账号' }}</text></view>
<view v-else class="account-chip" @tap="goProfile"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></view>
</view>
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
@@ -471,6 +471,15 @@ async function load(force = false) {
/* ==================== 交互 ==================== */
/** 点击右上角用户名 → 进入个人中心修改资料;未授权时提示先点模块授权手机号 */
function goProfile() {
if (guest.value) {
uni.showToast({ title: '请先点击模块授权手机号', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/profile/profile' })
}
function chooseTenant() {
const list = tenants.value
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个企业' : '暂无可切换的企业', icon: 'none' })
+190 -5
View File
@@ -1,6 +1,12 @@
<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>
@@ -58,16 +64,45 @@
</view>
<AppTabbar current="profile" />
<!-- 编辑资料:姓名 / 邮箱可改,手机号由微信验证只读 -->
<view v-if="editVisible" class="edit-mask" @tap="closeEdit">
<view class="edit-sheet" @tap.stop>
<view class="edit-head">
<text class="edit-title">编辑资料</text>
<text class="edit-close" @tap="closeEdit">取消</text>
</view>
<view class="edit-field">
<text class="edit-label">姓名</text>
<input v-model="editForm.name" class="edit-input" placeholder="请输入姓名" :maxlength="30" />
</view>
<view class="edit-field">
<text class="edit-label">邮箱</text>
<input v-model="editForm.email" class="edit-input" placeholder="选填" />
</view>
<view class="edit-field">
<text class="edit-label">手机号</text>
<view class="edit-readonly">
<text>{{ phoneText }}</text>
<text class="readonly-tag">微信验证,不可修改</text>
</view>
</view>
<text v-if="editError" class="edit-error">{{ editError }}</text>
<button class="edit-submit" :disabled="saving" @tap="saveProfile">
{{ saving ? '保存中…' : '保存' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { mpHeadPad } from '@/utils/mpHead.js'
const headPad = mpHeadPad()
import { getUser, setUser, logout, isLoggedIn, isSSOEnabled, getToken } from '@/utils/auth.js'
import { getCurrentUser, logoutApi, logoutUAC } from '@/api/auth.js'
import { getCurrentUser, updateCurrentUserProfile, logoutApi, logoutUAC } from '@/api/auth.js'
import { requestSubscribeNotice } from '@/utils/subscribe.js'
import AppTabbar from '@/components/MainTabbar.vue'
@@ -133,14 +168,74 @@ async function loadUser() {
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
// 新流程没有独立登录页:回平台点击模块即可重新授权手机号
uni.reLaunch({ url: '/pages/nav/nav' })
return
}
loadUser()
})
/* ==================== 编辑资料 ==================== */
const editVisible = ref(false)
const saving = ref(false)
const editError = ref('')
const editForm = reactive({ name: '', email: '' })
// 手机号来自微信验证,只读展示(掩码),不随表单提交
const phoneText = computed(() => {
const u = user.value || {}
const p = u.phone || (u.account && /^1\d{10}$/.test(String(u.account)) ? String(u.account) : '')
return p ? String(p).replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '微信已验证'
})
function onEdit() {
uni.showToast({ title: '编辑资料', icon: 'none' })
editForm.name = user.value?.nickname || user.value?.name || ''
editForm.email = user.value?.email || ''
editError.value = ''
editVisible.value = true
}
function closeEdit() {
if (!saving.value) editVisible.value = false
}
async function saveProfile() {
if (saving.value) return
const name = String(editForm.name || '').trim()
const email = String(editForm.email || '').trim()
if (!name) {
editError.value = '请填写姓名'
return
}
if (email && !email.includes('@')) {
editError.value = '邮箱格式不正确'
return
}
saving.value = true
editError.value = ''
try {
const res = await updateCurrentUserProfile({ name, email })
const next = {
...(getUser() || {}),
...(user.value || {}),
...(res || {}),
name,
nickname: name,
email
}
setUser(next)
user.value = next
editVisible.value = false
uni.showToast({ title: '资料已更新', icon: 'success' })
} catch (e) {
editError.value = (e && e.message) || '保存失败,请重试'
} finally {
saving.value = false
}
}
function goBackPlatform() {
uni.reLaunch({ url: '/pages/nav/nav' })
}
function onMenuTap(item) {
@@ -172,7 +267,7 @@ function handleLogout() {
}
// 再清本地并跳转,避免接口超时/失败导致“退出无效”
logout()
uni.reLaunch({ url: '/pages/login/login' })
uni.reLaunch({ url: '/pages/nav/nav' })
}
})
}
@@ -357,4 +452,94 @@ 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;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-end;
background: rgba(15, 23, 42, .48);
@include mask-responsive;
}
.edit-sheet {
width: 100%;
padding: 28rpx $page-padding-x 40rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
border-radius: 32rpx 32rpx 0 0;
background: #ffffff;
@include sheet-responsive;
}
.edit-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.edit-title { font-size: 32rpx; font-weight: 700; color: $color-text; }
.edit-close { font-size: 26rpx; color: $color-text-muted; }
.edit-field { margin-bottom: 26rpx; }
.edit-label { display: block; margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #334155; }
.edit-input {
height: 88rpx;
padding: 0 22rpx;
box-sizing: border-box;
border: 1rpx solid #dce7f7;
border-radius: $radius-md;
background: #fbfdff;
font-size: 28rpx;
color: $color-text;
}
.edit-readonly {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22rpx;
box-sizing: border-box;
border: 1rpx solid #e2e8f0;
border-radius: $radius-md;
background: #f1f5f9;
font-size: 27rpx;
color: #475569;
}
.readonly-tag { font-size: 21rpx; color: #94a3b8; }
.edit-error { display: block; margin: 4rpx 0 16rpx; font-size: 24rpx; color: $color-danger; }
.edit-submit {
height: 92rpx;
line-height: 92rpx;
margin-top: 12rpx;
border: none;
border-radius: $radius-full;
background: linear-gradient(135deg, #60a5fa, #2563eb);
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
&::after { border: none; }
&[disabled] { opacity: .6; }
}
</style>
+2 -1
View File
@@ -55,7 +55,8 @@ const TAB_ORDER = ['home', 'functions', 'mine']
const tabs = [
{ name: 'home', text: '首页', icon: 'house' },
{ name: 'functions', text: '功能', icon: 'table-cells' },
{ name: 'mine', text: '我的', icon: 'user' }
// 末位为「回平台」:点击由 AppTabbar 统一 reLaunch 到 /pages/nav/nav
{ name: 'mine', text: '回平台', icon: 'arrow-left' }
]
const slug = ref('')