更新
This commit is contained in:
1 parent
8fc52c574f
commit
fb04d66e61
7 files changed
+284
-9
No files matched your search
@@ -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端登录验证码
|
// SendLoginCode 发送App端登录验证码
|
||||||
func (c *AppAuthController) SendLoginCode() {
|
func (c *AppAuthController) SendLoginCode() {
|
||||||
var req struct {
|
var req struct {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ func Register() {
|
|||||||
|
|
||||||
// 当前用户信息
|
// 当前用户信息
|
||||||
beego.Router("/api/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser")
|
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")
|
beego.Router("/api/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos")
|
||||||
|
|||||||
@@ -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() {
|
export function getOpenVerify() {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@@ -56,12 +56,19 @@ const DEFAULT_TABS = [
|
|||||||
{ name: 'home', text: '概览', icon: 'gauge' },
|
{ name: 'home', text: '概览', icon: 'gauge' },
|
||||||
{ name: 'functions', text: '功能', icon: 'table-cells' },
|
{ name: 'functions', text: '功能', icon: 'table-cells' },
|
||||||
{ name: 'todo', text: '待办', icon: 'circle-check' },
|
{ 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))
|
const list = computed(() => (props.tabs && props.tabs.length ? props.tabs : DEFAULT_TABS))
|
||||||
|
|
||||||
function onTap(name) {
|
function onTap(name) {
|
||||||
|
// 「回平台」:清空页面栈回到模块选择页。必须放在 current 判重之前,
|
||||||
|
// 否则停在原「我的」页时点击会被判重拦下。
|
||||||
|
if (name === 'mine' || name === 'platform') {
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (name === props.current) return
|
if (name === props.current) return
|
||||||
if (props.variant === 'switch') {
|
if (props.variant === 'switch') {
|
||||||
emit('change', name)
|
emit('change', name)
|
||||||
|
|||||||
@@ -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"><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.account || '我的账号' }}</text></view>
|
||||||
<view v-else class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></view>
|
<view v-else class="account-chip" @tap="goProfile"><FaIcon name="user" color="#3478f6" :size="15" /><text>游客</text></view>
|
||||||
</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,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() {
|
function chooseTenant() {
|
||||||
const list = tenants.value
|
const list = tenants.value
|
||||||
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个企业' : '暂无可切换的企业', icon: 'none' })
|
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个企业' : '暂无可切换的企业', icon: 'none' })
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<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>
|
||||||
@@ -58,16 +64,45 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<AppTabbar current="profile" />
|
<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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { mpHeadPad } from '@/utils/mpHead.js'
|
import { mpHeadPad } from '@/utils/mpHead.js'
|
||||||
|
|
||||||
const headPad = mpHeadPad()
|
const headPad = mpHeadPad()
|
||||||
import { getUser, setUser, logout, isLoggedIn, isSSOEnabled, getToken } from '@/utils/auth.js'
|
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 { requestSubscribeNotice } from '@/utils/subscribe.js'
|
||||||
import AppTabbar from '@/components/MainTabbar.vue'
|
import AppTabbar from '@/components/MainTabbar.vue'
|
||||||
|
|
||||||
@@ -133,14 +168,74 @@ async function loadUser() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
// 新流程没有独立登录页:回平台点击模块即可重新授权手机号
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadUser()
|
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() {
|
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) {
|
function onMenuTap(item) {
|
||||||
@@ -172,7 +267,7 @@ function handleLogout() {
|
|||||||
}
|
}
|
||||||
// 再清本地并跳转,避免接口超时/失败导致“退出无效”
|
// 再清本地并跳转,避免接口超时/失败导致“退出无效”
|
||||||
logout()
|
logout()
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -357,4 +452,94 @@ 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 {
|
||||||
|
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>
|
</style>
|
||||||
@@ -55,7 +55,8 @@ const TAB_ORDER = ['home', 'functions', 'mine']
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: 'home', text: '首页', icon: 'house' },
|
{ name: 'home', text: '首页', icon: 'house' },
|
||||||
{ name: 'functions', text: '功能', icon: 'table-cells' },
|
{ 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('')
|
const slug = ref('')
|
||||||
|
|||||||
Reference in new issue
Block a user