Files
PartsInquiry/frontend/pages/my/security.vue
2025-09-27 22:57:59 +08:00

201 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="security">
<view class="card">
<view class="cell" @click="openAvatarDialog">
<text class="cell-label">头像</text>
<image class="avatar-preview" :src="avatarPreview" mode="aspectFill" />
<text class="arrow"></text>
</view>
<view class="cell">
<text class="cell-label">姓名</text>
<input class="cell-input" type="text" v-model.trim="form.name" placeholder="请输入姓名" />
</view>
<button class="btn" type="primary" :loading="savingProfile" @click="saveProfile">保存资料</button>
</view>
<view class="card">
<view class="row">
<text class="label">旧密码</text>
<input class="input" password v-model.trim="pwd.oldPassword" placeholder="如从未设置可留空" />
</view>
<view class="row">
<text class="label">新密码</text>
<input class="input" password v-model.trim="pwd.newPassword" placeholder="至少6位" />
</view>
<button class="btn" :loading="savingPwd" @click="changePassword">修改密码</button>
</view>
<view class="card">
<view class="row">
<text class="label">手机号</text>
<input class="input" type="text" v-model.trim="phone.phone" placeholder="11位手机号" />
</view>
<button class="btn" :loading="savingPhone" @click="changePhoneDirect">保存手机号</button>
</view>
</view>
</template>
<script>
import { get, put, post, upload } from '../../common/http.js'
import { API_BASE_URL } from '../../common/config.js'
export default {
data() {
return {
form: { name: '', avatarUrl: '' },
pwd: { oldPassword: '', newPassword: '' },
phone: { phone: '' },
savingProfile: false,
savingPwd: false,
savingPhone: false,
sendingCode: false,
originalAvatarUrl: ''
}
},
onShow() { this.loadProfile() },
computed: {
avatarPreview() {
return this.normalizeAvatar(this.form.avatarUrl)
},
canSendPhone() {
const p = String(this.phone.phone || '').trim()
return /^1\d{10}$/.test(p)
}
},
methods: {
async loadProfile() {
try {
const data = await get('/api/user/me')
const rawAvatar = data?.avatarUrl || (uni.getStorageSync('USER_AVATAR_RAW') || '')
this.originalAvatarUrl = rawAvatar
this.form.name = data?.name || (uni.getStorageSync('USER_NAME') || '')
this.form.avatarUrl = rawAvatar
} catch (e) {}
},
normalizeAvatar(url) {
if (!url) return '/static/icons/icons8-mitt-24.png'
const s = String(url)
if (/^https?:\/\//i.test(s)) return s
const base = API_BASE_URL || ''
if (!base) return s
if (s.startsWith('/')) return `${base}${s}`
return `${base}/${s}`
},
openAvatarDialog() {
uni.showActionSheet({
itemList: ['粘贴图片URL', '从相册选择并上传'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showModal({
title: '头像URL',
editable: true,
placeholderText: 'https://...',
success: async (m) => {
if (m.confirm && m.content) {
this.form.avatarUrl = m.content.trim()
await this.saveProfile({ auto: true })
}
}
})
} else if (res.tapIndex === 1) {
uni.chooseImage({ count: 1, sizeType: ['compressed'], success: (ci) => {
const filePath = (ci.tempFilePaths && ci.tempFilePaths[0]) || ''
if (!filePath) return
uni.showLoading({ title: '上传中...' })
upload('/api/attachments', filePath, { ownerType: 'user_avatar', ownerId: 0 }).then(async (data) => {
const url = data && (data.url || data.path)
if (url) {
this.form.avatarUrl = url
await this.saveProfile({ auto: true })
}
uni.showToast({ title: '已上传', icon: 'success' })
}).catch(e => {
uni.showToast({ title: (e && e.message) || '上传失败', icon: 'none' })
}).finally(() => { uni.hideLoading() })
} })
}
}
})
},
async saveProfile(opts = {}) {
const auto = opts && opts.auto
const payload = {}
if (this.form.name && this.form.name !== uni.getStorageSync('USER_NAME')) payload.name = this.form.name
if (this.form.avatarUrl && this.form.avatarUrl !== this.originalAvatarUrl) payload.avatarUrl = this.form.avatarUrl
if (Object.keys(payload).length === 0) {
if (!auto) uni.showToast({ title: '无需修改', icon: 'none' })
return
}
if (this.savingProfile) return
this.savingProfile = true
try {
await put('/api/user/me', payload)
try {
if (payload.name) uni.setStorageSync('USER_NAME', payload.name)
if (payload.avatarUrl) {
const rawUrl = payload.avatarUrl
const displayUrl = `${rawUrl}${rawUrl.includes('?') ? '&' : '?'}t=${Date.now()}`
uni.setStorageSync('USER_AVATAR_RAW', rawUrl)
uni.setStorageSync('USER_AVATAR', rawUrl)
this.originalAvatarUrl = rawUrl
this.form.avatarUrl = rawUrl
}
} catch(_) {}
if (!payload.avatarUrl && this.form.avatarUrl) {
uni.setStorageSync('USER_AVATAR_RAW', this.form.avatarUrl)
uni.setStorageSync('USER_AVATAR', this.form.avatarUrl)
}
uni.showToast({ title: auto ? '头像已更新' : '已保存', icon: 'success' })
} catch (e) {
const msg = (e && e.message) || '保存失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
this.savingProfile = false
}
},
async changePassword() {
if (!this.pwd.newPassword || this.pwd.newPassword.length < 6) return uni.showToast({ title: '新密码至少6位', icon: 'none' })
this.savingPwd = true
try {
await put('/api/user/me/password', { oldPassword: this.pwd.oldPassword || undefined, newPassword: this.pwd.newPassword })
this.pwd.oldPassword = ''
this.pwd.newPassword = ''
uni.showToast({ title: '密码已修改', icon: 'success' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '修改失败', icon: 'none' })
} finally { this.savingPwd = false }
},
async changePhoneDirect() {
if (!this.canSendPhone) return uni.showToast({ title: '请输入正确手机号', icon: 'none' })
this.savingPhone = true
try {
await put('/api/user/me/phone', { phone: this.phone.phone })
uni.setStorageSync('USER_MOBILE', this.phone.phone)
uni.showToast({ title: '手机号已保存', icon: 'success' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '保存失败', icon: 'none' })
} finally { this.savingPhone = false }
}
}
}
</script>
<style lang="scss">
.security { padding: 24rpx; }
.card { background: $uni-bg-color-grey; border-radius: 16rpx; padding: 22rpx; margin-bottom: 24rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.08); }
.cell { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 0; border-bottom: 1rpx solid $uni-border-color; }
.cell:last-of-type { border-bottom: none; }
.cell-label { flex: 1; font-size: 28rpx; color: $uni-text-color; }
.cell-input { flex: 2; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.avatar-preview { width: 100rpx; height: 100rpx; border-radius: 16rpx; background: $uni-bg-color-hover; }
.arrow { margin-left: 12rpx; color: #99a2b3; font-size: 32rpx; }
.row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.label { width: 160rpx; color: $uni-text-color; font-size: 28rpx; }
.input { flex: 1; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.btn { margin-top: 8rpx; }
.btn.minor { background: $uni-bg-color-hover; color: $uni-text-color; }
</style>