完善生产查询与登录权限配置

This commit is contained in:
2026-09-03 19:56:19 +08:00
parent 793f140a45
commit 02c8fb7021
438 changed files with 1595 additions and 573 deletions
+325
View File
@@ -0,0 +1,325 @@
<template>
<view class="profile-form-page" :class="`theme-${themeMode}`">
<view class="form-card">
<view class="form-header">
<text class="form-title">修改资料</text>
<text class="form-desc">用户名、邮箱和手机号为账号信息,暂不支持修改</text>
</view>
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
<u-form-item label="用户名" :border-bottom="false">
<view class="field-card locked">
<text class="field-text locked-text">{{ form.username || '-' }}</text>
</view>
</u-form-item>
<u-form-item label="邮箱" :border-bottom="false">
<view class="field-card locked">
<text class="field-text locked-text">{{ form.email || '-' }}</text>
</view>
</u-form-item>
<u-form-item label="手机号" :border-bottom="false">
<view class="field-card locked">
<text class="field-text locked-text">{{ form.phone || '暂未绑定手机号' }}</text>
</view>
</u-form-item>
<u-form-item label="昵称" prop="nickname" required :border-bottom="false">
<view class="field-card">
<u-input v-model="form.nickname" clearable placeholder="请输入昵称" placeholder-style="color: #a0a7b5;" />
</view>
</u-form-item>
<u-form-item label="性别" prop="gender" :border-bottom="false">
<view class="gender-options">
<view
v-for="item in genderOptions"
:key="item.value"
class="gender-option"
:class="{ active: form.gender === item.value }"
@tap="form.gender = item.value"
>
<view class="gender-dot"></view>
<text>{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="生日" prop="birthday" :border-bottom="false">
<picker mode="date" :value="form.birthday || today" start="1900-01-01" :end="today" @change="handleBirthdayChange">
<view class="field-card picker-card">
<text class="field-text" :class="{ placeholder: !form.birthday }">{{ form.birthday || '请选择生日' }}</text>
<u-icon name="calendar" color="#a0a7b5" size="24" />
</view>
</picker>
</u-form-item>
<u-form-item label="个人简介" prop="bio" :border-bottom="false">
<view class="textarea-card">
<textarea v-model="form.bio" maxlength="120" placeholder="请输入个人简介" placeholder-style="color: #a0a7b5;" />
</view>
</u-form-item>
</u-form>
</view>
<view class="bottom-actions">
<u-button type="primary" shape="circle" size="large" :loading="submitting" @click="submitForm">保存资料</u-button>
</view>
</view>
</template>
<script>
import { getUserInfo, updateUserInfo } from '@/api/member.js'
export default {
data() {
return {
submitting: false,
today: '',
form: {
username: '',
email: '',
phone: '',
nickname: '',
gender: 0,
birthday: '',
bio: ''
},
genderOptions: [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
{ label: '其他', value: 0 }
]
}
},
onLoad() {
this.today = this.getToday()
this.loadProfile()
},
methods: {
getToday() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
async loadProfile() {
try {
const profile = await getUserInfo()
this.form = {
username: profile.username || '',
email: profile.email || '',
phone: profile.phone || '',
nickname: profile.nickname || profile.username || '',
gender: Number(profile.gender || 0),
birthday: profile.birthday || '',
bio: profile.bio || ''
}
} catch (error) {
console.error('[scanapp] 加载资料失败', error)
}
},
handleBirthdayChange(e) {
this.form.birthday = e.detail.value
},
validateForm() {
if (!this.form.nickname.trim()) {
uni.showToast({ title: '请输入昵称', icon: 'none' })
return false
}
return true
},
async submitForm() {
if (this.submitting || !this.validateForm()) {
return
}
this.submitting = true
try {
await updateUserInfo({
nickname: this.form.nickname.trim(),
gender: this.form.gender,
birthday: this.form.birthday,
bio: this.form.bio
})
const profile = await getUserInfo()
uni.setStorageSync('scanapp_user', profile)
uni.showToast({ title: '资料已保存', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 400)
} catch (error) {
console.error('[scanapp] 保存资料失败', error)
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="scss" scoped>
.profile-form-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: #f4f6fb;
}
.form-card {
padding: 34rpx 30rpx;
border: 1rpx solid rgba(255, 255, 255, 0.75);
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.form-header {
margin-bottom: 24rpx;
}
.form-title,
.form-desc,
.field-text {
display: block;
}
.form-title {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.form-desc {
margin-top: 10rpx;
color: #8a94a6;
font-size: 22rpx;
line-height: 1.45;
}
.field-card {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 88rpx;
padding: 0 24rpx;
box-sizing: border-box;
border: 1rpx solid #edf1f7;
border-radius: 22rpx;
background: #f8fbff;
}
.field-card.locked {
border-color: #edf0f5;
background: #f4f6fb;
}
.field-card.picker-card {
padding-right: 20rpx;
}
.field-text {
color: #17233d;
font-size: 22rpx;
line-height: 1.4;
}
.field-text.locked-text,
.field-text.placeholder {
color: #a0a7b5;
}
.gender-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 16rpx;
width: 100%;
}
.gender-option {
display: flex;
align-items: center;
justify-content: center;
height: 76rpx;
border: 1rpx solid #edf1f7;
border-radius: 20rpx;
background: #f8fbff;
color: #6b778c;
font-size: 22rpx;
font-weight: 600;
}
.gender-dot {
width: 14rpx;
height: 14rpx;
margin-right: 10rpx;
border: 3rpx solid #c5ccda;
border-radius: 50%;
box-sizing: border-box;
}
.gender-option.active {
border-color: #1677ff;
background: rgba(22, 119, 255, 0.1);
color: #1677ff;
}
.gender-option.active .gender-dot {
border-color: #1677ff;
background: #1677ff;
box-shadow: 0 0 0 4rpx rgba(22, 119, 255, 0.16);
}
.textarea-card {
width: 100%;
min-height: 156rpx;
padding: 20rpx 24rpx;
box-sizing: border-box;
border: 1rpx solid #edf1f7;
border-radius: 22rpx;
background: #f8fbff;
}
.textarea-card textarea {
width: 100%;
height: 116rpx;
color: #17233d;
font-size: 22rpx;
line-height: 1.5;
}
.bottom-actions {
margin-top: 36rpx;
}
.theme-dark.profile-form-page {
background: #151923;
}
.theme-dark .form-card,
.theme-dark .field-card,
.theme-dark .gender-option,
.theme-dark .textarea-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .field-card.locked {
background: #1c2431;
}
.theme-dark .form-title,
.theme-dark .field-text,
.theme-dark .textarea-card textarea {
color: #f2f5fa;
}
.theme-dark .form-desc,
.theme-dark .field-text.locked-text,
.theme-dark .field-text.placeholder,
.theme-dark .gender-option {
color: #aeb9ca;
}
.theme-dark :deep(textarea),
.theme-dark :deep(.u-input__input) {
color: #f2f5fa !important;
}
</style>
+384
View File
@@ -0,0 +1,384 @@
<template>
<view class="profile-page" :class="`theme-${themeMode}`">
<view class="profile-header">
<view class="profile-top">
<view class="avatar-wrap">
<u-icon name="account-fill" color="#ffffff" size="52" />
</view>
<view class="profile-meta">
<text class="profile-name">{{ user.name }}</text>
<text class="profile-account">{{ user.account }}</text>
</view>
<text class="profile-role">{{ user.role }}</text>
</view>
<view class="profile-stat-row">
<view class="stat-item" v-for="item in stats" :key="item.label">
<text class="stat-value">{{ item.value }}</text>
<text class="stat-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="profile-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">手机号</text>
<text class="info-value">{{ user.phone }}</text>
</view>
<view class="info-row">
<text class="info-label">所属组织</text>
<text class="info-value">{{ user.department }}</text>
</view>
<view class="info-row">
<text class="info-label">账号状态</text>
<text class="info-value success">{{ user.status }}</text>
</view>
</view>
</view>
<view class="profile-card">
<view class="card-title">快捷操作</view>
<view class="action-list">
<view class="action-item" @tap="handleAction('修改资料')">
<view class="action-left">
<u-icon name="edit-pen" color="#2f6cf6" size="30" />
<text>修改资料</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
<view class="action-item" @tap="handleAction('修改密码')">
<view class="action-left">
<u-icon name="lock-fill" color="#f7b733" size="30" />
<text>修改密码</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
<view class="action-item" @tap="handleAction('切换账号')">
<view class="action-left">
<u-icon name="account" color="#35bfa3" size="30" />
<text>切换账号</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
<view class="action-item danger" @tap="handleAction('退出登录')">
<view class="action-left">
<u-icon name="logout" color="#ef4f4f" size="30" />
<text>退出登录</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
</view>
</view>
</view>
</template>
<script>
import { getUserInfo } from '@/api/member.js'
export default {
data() {
return {
user: {
name: '-',
account: '-',
role: '会员用户',
phone: '-',
department: '-',
status: '正常'
},
stats: [
{ label: '今日扫码', value: '-' },
{ label: '异常处理', value: '-' },
{ label: '累计天数', value: '-' }
]
}
},
onShow() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const profile = await getUserInfo()
uni.setStorageSync('scanapp_user', profile)
this.user = {
name: profile.nickname || profile.username || '-',
account: profile.username || profile.email || '-',
role: '会员用户',
phone: profile.phone || '-',
department: profile.department_name || '-',
status: Number(profile.enabled) === 0 ? '已停用' : '正常'
}
} catch (error) {
console.error('[scanapp] 获取用户信息失败', error)
}
},
handleAction(title) {
const actionMap = {
'修改资料': this.goEditProfile,
'修改密码': this.goChangePassword,
'切换账号': this.switchAccount,
'退出登录': this.logout
}
const action = actionMap[title]
if (action) {
action()
}
},
goEditProfile() {
uni.navigateTo({
url: '/pages/profile/edit'
})
},
goChangePassword() {
uni.navigateTo({
url: '/pages/profile/password'
})
},
switchAccount() {
uni.showModal({
title: '切换账号',
content: '将清除当前登录状态并返回登录页,是否继续?',
confirmText: '切换',
success: (res) => {
if (res.confirm) {
this.clearLoginState()
uni.reLaunch({ url: '/pages/auth/login' })
}
}
})
},
logout() {
uni.showModal({
title: '退出登录',
content: '确认退出当前账号?',
confirmText: '退出',
success: (res) => {
if (res.confirm) {
this.clearLoginState()
uni.reLaunch({ url: '/pages/auth/login' })
}
}
})
},
clearLoginState() {
uni.removeStorageSync('scanapp_token')
uni.removeStorageSync('scanapp_refresh_token')
uni.removeStorageSync('scanapp_user')
}
}
}
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eef6ff 0%, #f4f6fb 36%, #f4f6fb 100%);
}
.profile-header,
.profile-card {
padding: 38rpx 34rpx;
border: 1rpx solid rgba(255, 255, 255, 0.75);
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.profile-card {
margin-top: 30rpx;
}
.profile-top {
display: flex;
align-items: center;
gap: 0;
}
.profile-meta {
flex: 1;
min-width: 0;
margin-left: 36rpx;
}
.avatar-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 92rpx;
height: 92rpx;
border-radius: 50%;
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
box-shadow: 0 12rpx 28rpx rgba(53, 191, 163, 0.28);
flex: none;
}
.profile-meta {
flex: 1;
min-width: 0;
}
.profile-name {
display: block;
color: #17233d;
font-size: 28rpx;
font-weight: 800;
line-height: 1.35;
}
.profile-account,
.profile-role,
.info-label,
.info-value,
.action-item text {
display: block;
}
.profile-account {
margin-top: 14rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.4;
}
.profile-role {
display: inline-flex;
align-items: center;
flex: none;
margin-left: 24rpx;
padding: 6rpx 16rpx;
border-radius: 999rpx;
background: rgba(25, 190, 107, 0.1);
color: #19be6b;
font-size: 18rpx;
font-weight: 600;
line-height: 1.35;
}
.profile-stat-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
margin-top: 34rpx;
}
.stat-item {
padding: 24rpx 12rpx;
border-radius: 20rpx;
background: #f7f9fc;
text-align: center;
}
.stat-value {
display: block;
color: #1677ff;
font-size: 28rpx;
font-weight: 800;
line-height: 1.35;
}
.stat-label {
display: block;
margin-top: 10rpx;
color: #8a94a6;
font-size: 18rpx;
line-height: 1.35;
}
.card-title {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.info-list {
margin-top: 26rpx;
}
.info-row,
.action-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 26rpx 0;
border-bottom: 1rpx solid #eef1f6;
}
.info-row:last-child,
.action-item:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.info-label {
color: #8a94a6;
font-size: 20rpx;
line-height: 1.35;
}
.info-value {
color: #17233d;
font-size: 22rpx;
font-weight: 600;
line-height: 1.35;
text-align: right;
}
.info-value.success {
color: #19be6b;
}
.action-list {
margin-top: 26rpx;
}
.action-left {
display: flex;
align-items: center;
gap: 18rpx;
color: #17233d;
font-size: 22rpx;
font-weight: 600;
line-height: 1.35;
}
.action-item.danger .action-left {
color: #ef4f4f;
}
.theme-dark.profile-page {
background: #151923;
}
.theme-dark .profile-header,
.theme-dark .profile-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .stat-item {
background: #1c2431;
}
.theme-dark .profile-name,
.theme-dark .card-title,
.theme-dark .info-value,
.theme-dark .action-left {
color: #f2f5fa;
}
.theme-dark .profile-account,
.theme-dark .info-label,
.theme-dark .stat-label {
color: #aeb9ca;
}
.theme-dark .info-row,
.theme-dark .action-item {
border-color: #364152;
}
</style>
+205
View File
@@ -0,0 +1,205 @@
<template>
<view class="password-page" :class="`theme-${themeMode}`">
<view class="form-card">
<view class="form-header">
<text class="form-title">修改密码</text>
<text class="form-desc">修改成功后需要重新登录</text>
</view>
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
<u-form-item label="原密码" prop="oldPassword" :border-bottom="false">
<view class="field-card">
<u-input v-model="form.oldPassword" type="password" clearable password-icon placeholder="请输入原密码" placeholder-style="color: #a0a7b5;" />
</view>
</u-form-item>
<u-form-item label="新密码" prop="newPassword" :border-bottom="false">
<view class="field-card">
<u-input v-model="form.newPassword" type="password" clearable password-icon placeholder="请输入新密码" placeholder-style="color: #a0a7b5;" />
</view>
</u-form-item>
<u-form-item label="确认新密码" prop="confirmPassword" :border-bottom="false">
<view class="field-card">
<u-input v-model="form.confirmPassword" type="password" clearable password-icon placeholder="请再次输入新密码" placeholder-style="color: #a0a7b5;" />
</view>
</u-form-item>
</u-form>
</view>
<view class="tips-card">
<text class="tips-title">密码要求</text>
<text class="tips-text">长度不少于 6 位,建议同时包含字母、数字或符号。</text>
</view>
<view class="bottom-actions">
<u-button type="primary" shape="circle" size="large" :loading="submitting" @click="submitForm">确认修改</u-button>
</view>
</view>
</template>
<script>
import { changePassword } from '@/api/member.js'
export default {
data() {
return {
submitting: false,
form: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
}
},
methods: {
validateForm() {
if (!this.form.oldPassword) {
uni.showToast({ title: '请输入原密码', icon: 'none' })
return false
}
if (!this.form.newPassword || this.form.newPassword.length < 6) {
uni.showToast({ title: '新密码至少 6 位', icon: 'none' })
return false
}
if (this.form.newPassword !== this.form.confirmPassword) {
uni.showToast({ title: '两次新密码不一致', icon: 'none' })
return false
}
if (this.form.oldPassword === this.form.newPassword) {
uni.showToast({ title: '新密码不能与原密码相同', icon: 'none' })
return false
}
return true
},
async submitForm() {
if (this.submitting || !this.validateForm()) {
return
}
this.submitting = true
try {
await changePassword({
old_password: this.form.oldPassword,
new_password: this.form.newPassword
})
uni.removeStorageSync('scanapp_token')
uni.removeStorageSync('scanapp_refresh_token')
uni.removeStorageSync('scanapp_user')
uni.showToast({ title: '密码已修改,请重新登录', icon: 'none' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/auth/login' })
}, 700)
} catch (error) {
console.error('[scanapp] 修改密码失败', error)
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="scss" scoped>
.password-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: #f4f6fb;
}
.form-card,
.tips-card {
padding: 34rpx 30rpx;
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.form-header {
margin-bottom: 24rpx;
}
.form-title,
.form-desc,
.tips-title,
.tips-text {
display: block;
}
.form-title {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.form-desc {
margin-top: 10rpx;
color: #8a94a6;
font-size: 22rpx;
line-height: 1.45;
}
.field-card {
display: flex;
align-items: center;
width: 100%;
height: 88rpx;
padding: 0 24rpx;
box-sizing: border-box;
border: 1rpx solid #edf1f7;
border-radius: 22rpx;
background: #f8fbff;
}
.tips-card {
margin-top: 24rpx;
background: #f8fbff;
}
.tips-title {
color: #17233d;
font-size: 24rpx;
font-weight: 700;
line-height: 1.4;
}
.tips-text {
margin-top: 10rpx;
color: #6b778c;
font-size: 22rpx;
line-height: 1.55;
}
.bottom-actions {
margin-top: 36rpx;
}
.theme-dark.password-page {
background: #151923;
}
.theme-dark .form-card,
.theme-dark .tips-card,
.theme-dark .field-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .tips-card,
.theme-dark .field-card {
background: #1c2431;
}
.theme-dark .form-title,
.theme-dark .tips-title {
color: #f2f5fa;
}
.theme-dark .form-desc,
.theme-dark .tips-text {
color: #aeb9ca;
}
.theme-dark :deep(.u-input__input) {
color: #f2f5fa !important;
}
</style>