OfficeSystem/components/my/My.vue
2025-11-27 11:05:44 +08:00

500 lines
11 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="mine-page">
<!-- 个人资料卡片 -->
<view class="profile-card" @click="goToProfile">
<view class="profile-header">
<view class="avatar-wrapper">
<image
v-if="userInfo?.user?.avatar"
:src="userInfo.user.avatar"
class="avatar-img"
mode="aspectFill"
/>
<view v-else class="avatar-placeholder">
<text class="avatar-text">{{ getAvatarText(userInfo?.user?.nickName) }}</text>
</view>
</view>
<view class="profile-info">
<view class="name-row">
<text class="user-name">{{ userInfo?.user?.nickName || '--' }}</text>
<!-- <text v-if="userInfo?.roles?.[0]" class="role-badge">{{ userInfo.roles[0] }}</text>-->
</view>
<text class="dept-name">{{ userInfo?.user?.deptName || '--' }}</text>
</view>
<view class="arrow-icon"></view>
</view>
<!-- <view class="profile-stats">-->
<!-- <view class="stat-item">-->
<!-- <text class="stat-value">{{ userInfo?.user?.taskCount || 0 }}</text>-->
<!-- <text class="stat-label">任务数</text>-->
<!-- </view>-->
<!-- <view class="stat-divider"></view>-->
<!-- <view class="stat-item">-->
<!-- <text class="stat-value">{{ userInfo?.user?.projectCount || 0 }}</text>-->
<!-- <text class="stat-label">项目数</text>-->
<!-- </view>-->
<!-- <view class="stat-divider"></view>-->
<!-- <view class="stat-item">-->
<!-- <text class="stat-value">{{ userInfo?.user?.taskNum || 0 }}</text>-->
<!-- <text class="stat-label">任务编号</text>-->
<!-- </view>-->
<!-- </view>-->
</view>
<view
v-if="showPrivateSwitch"
class="card settings-card"
>
<view class="setting-row">
<text class="setting-label">私有视角</text>
<uv-switch v-model="filterSelf"/>
</view>
<text class="setting-desc">开启后仅展示分配给我的客户和任务数据</text>
</view>
<!-- 版本管理 -->
<view class="card version-card">
<view class="version-header">
<text class="version-title">版本管理</text>
</view>
<view class="version-info">
<view class="version-item">
<text class="version-label">当前版本</text>
<text class="version-value">{{ currentVersion }}</text>
</view>
<view v-if="latestVersion" class="version-item">
<text class="version-label">最新版本</text>
<text :class="{ 'new-version': hasUpdate }" class="version-value">
{{ latestVersion }}
<text v-if="hasUpdate" class="update-badge">有新版本</text>
</text>
</view>
<view v-else class="version-item">
<text class="version-label">最新版本</text>
<text class="version-value">未检查</text>
</view>
</view>
<view class="version-actions">
<uv-button
:loading="checking"
:plain="true"
size="small"
type="primary"
@click="checkUpdate"
>
{{ checking ? '检查中...' : '检查更新' }}
</uv-button>
<uv-button
v-if="hasUpdate && updateInfo"
size="small"
style="margin-left: 20rpx;"
type="error"
@click="handleUpdate"
>
立即更新
</uv-button>
</view>
</view>
<view class="card">
<uv-button :loading="loading" :plain="true" type="error" @click="onLogout">
退出登录
</uv-button>
</view>
</view>
</template>
<script setup>
import {computed, onMounted, ref} from 'vue'
import {storeToRefs} from 'pinia'
import {useUserStore} from '@/store/user'
import {logout} from '@/api/user'
import {checkForUpdate, getCurrentVersion, showUpdateDialog} from '@/utils/update'
const loading = ref(false)
const checking = ref(false)
const currentVersion = ref('')
const latestVersion = ref('')
const updateInfo = ref(null)
const hasUpdate = ref(false) // 直接使用 API 返回的 hasUpdate 值
const userStore = useUserStore()
const {privateView, userInfo} = storeToRefs(userStore)
const filterSelf = computed({
get: () => privateView.value,
set: (val) => userStore.setPrivateView(val)
})
const showPrivateSwitch = computed(() =>
userInfo.value?.roles?.some(r => ['admin', 'sys_admin'].includes(r))
)
// 获取当前版本
onMounted(() => {
const version = getCurrentVersion()
currentVersion.value = version.versionName || '未知'
})
// 检查更新
const checkUpdate = async () => {
if (checking.value) return
checking.value = true
try {
const result = await checkForUpdate(true) // 静默检查,不显示提示
console.log('检查更新结果:', result)
if (result && result.hasUpdate) {
// 确保版本号有值,即使 API 返回为空也显示一个默认值
latestVersion.value = result.versionName || result.versionNumber || result.version || '未知版本'
updateInfo.value = result
hasUpdate.value = true // 设置更新标志
// 检查下载地址是否存在(兼容 packageUrl、downloadUrl、download_url
if (!result.downloadUrl && !result.packageUrl && !result.download_url) {
console.warn('更新信息缺少下载地址:', result)
uni.showToast({
title: '更新信息不完整',
icon: 'none',
duration: 2000
})
} else {
uni.showToast({
title: '发现新版本',
icon: 'success',
duration: 2000
})
}
} else {
// 没有新版本时,也显示当前版本号
latestVersion.value = currentVersion.value
updateInfo.value = null
hasUpdate.value = false // 清除更新标志
uni.showToast({
title: '已是最新版本',
icon: 'success',
duration: 2000
})
}
} catch (error) {
console.error('检查更新失败:', error)
uni.showToast({
title: '检查更新失败',
icon: 'none',
duration: 2000
})
} finally {
checking.value = false
}
}
// 处理更新
const handleUpdate = () => {
if (!updateInfo.value) {
uni.showToast({
title: '更新信息无效',
icon: 'none'
})
return
}
// 兼容不同的字段名packageUrl、downloadUrl 或 download_url
const downloadUrl = updateInfo.value.downloadUrl || updateInfo.value.packageUrl || updateInfo.value.download_url
if (!downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none'
})
return
}
// 确保 updateInfo 有 downloadUrl 字段
const updateData = {
...updateInfo.value,
downloadUrl: downloadUrl
}
// 显示更新确认对话框,用户确认后会自动下载和安装
showUpdateDialog(updateData)
}
// 获取头像文字
const getAvatarText = (name) => {
if (!name) return '?'
return name.charAt(0).toUpperCase()
}
// 跳转到个人资料详情页
const goToProfile = () => {
uni.navigateTo({
url: '/pages/my/profile/index'
})
}
const onLogout = async () => {
if (loading.value) return
loading.value = true
try {
await logout()
} catch (e) {
// 忽略错误
} finally {
userStore.logout()
uni.$uv.toast('已退出登录')
setTimeout(() => {
uni.reLaunch({url: '/pages/login/index'})
}, 200)
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.mine-page {
padding: 24rpx;
//background-color: #f6f7fb;
//min-height: 100vh;
box-sizing: border-box;
}
.profile-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
padding: 40rpx 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
.profile-card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
pointer-events: none;
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
position: relative;
z-index: 1;
}
.avatar-wrapper {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.2);
border: 4rpx solid rgba(255, 255, 255, 0.3);
margin-right: 24rpx;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
}
.avatar-text {
font-size: 48rpx;
font-weight: 600;
color: #ffffff;
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.name-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.user-name {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
margin-right: 16rpx;
}
.role-badge {
font-size: 20rpx;
background: rgba(255, 255, 255, 0.25);
color: #ffffff;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-weight: 500;
}
.dept-name {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
}
.arrow-icon {
font-size: 48rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: 300;
margin-left: 16rpx;
}
.profile-stats {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 32rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 1;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.2);
margin: 0 16rpx;
}
.card {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.setting-label {
font-size: 30rpx;
font-weight: 500;
color: #111827;
}
.setting-desc {
margin-top: 14rpx;
font-size: 24rpx;
color: #6b7280;
}
.version-header {
margin-bottom: 24rpx;
}
.version-title {
font-size: 30rpx;
font-weight: 600;
color: #111827;
}
.version-info {
margin-bottom: 24rpx;
}
.version-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f3f4f6;
}
.version-item:last-child {
border-bottom: none;
}
.version-label {
font-size: 28rpx;
color: #6b7280;
}
.version-value {
font-size: 28rpx;
font-weight: 500;
color: #111827;
display: flex;
align-items: center;
gap: 12rpx;
}
.version-value.new-version {
color: #ef4444;
}
.update-badge {
font-size: 20rpx;
background: #fee2e2;
color: #dc2626;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-weight: normal;
}
.version-actions {
display: flex;
align-items: center;
margin-top: 8rpx;
}
.title {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 24rpx;
color: #111827;
}
</style>