项目详情、收藏进行登录

This commit is contained in:
2025-05-07 16:47:11 +08:00
parent 8e4d1ad5f6
commit 863b31a3e6
13 changed files with 704 additions and 137 deletions

View File

@@ -1,8 +1,11 @@
<script setup>
import { ref, onMounted } from 'vue'
import {ref, onMounted, watch} from 'vue'
import { useUserStore } from '@/stores/user'
import AOS from 'aos'
import 'aos/dist/aos.css'
import {changePasswordApi, userMyInfoApi, userStatsApi} from "@/api/request/user.js";
import {collectionListApi} from "@/api/request/collection.js";
import {message} from "ant-design-vue";
// Initialize userStore outside of userData to avoid conditional hook call
const userStore = useUserStore()
@@ -20,6 +23,14 @@ const userData = ref({
favorites: 12
})
const getMyInfo = () => {
userMyInfoApi().then((res) => {
userData.value = res;
})
}
getMyInfo();
onMounted(() => {
AOS.init({
duration: 800,
@@ -27,29 +38,134 @@ onMounted(() => {
easing: 'ease-out-cubic',
once: false
})
// Update userData from userStore after component is mounted
if (userStore.user) {
userData.value.username = userStore.user.username || '张三'
userData.value.avatar = userStore.user.avatar || 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
userData.value.createdAt = userStore.user.createdAt || '2023-01-15'
userData.value.lastLogin = userStore.user.lastLogin || '2023-06-20'
}
})
const userStats = [
{ label: '已购项目', value: userData.value.projects, icon: 'shopping-bag' },
{ label: '收藏项目', value: userData.value.favorites, icon: 'heart' },
{ label: '账户类型', value: userData.value.role, icon: 'user' }
]
const userStats = ref([
{ label: '已购项目', value: 0, icon: 'shopping-bag' },
{ label: '收藏项目', value: 0, icon: 'heart' },
{ label: '账户类型', value: 0, icon: 'user' }
])
const getUserStats = () => {
userStatsApi().then((res) => {
userStats.value = res;
})
}
getUserStats();
const recentProjects = [
{ id: 1, title: '电商平台开发', date: '2023-05-15', status: '进行中' },
{ id: 2, title: 'CRM系统定制', date: '2023-04-10', status: '已完成' },
{ id: 3, title: '数据分析平台', date: '2023-03-22', status: '已完成' }
]
const myCollectionProjects = ref([]);
const getMyCollectionProjects = () => {
collectionListApi({
page: 1,
pageSize: 1000,
}).then((res) => {
myCollectionProjects.value = res.items;
})
}
const activeTab = ref('profile')
watch(() => activeTab.value, (newValue) => {
if (newValue === 'collection') {
getMyCollectionProjects();
}
})
const tabs = [
{ name: '个人资料', value: 'profile' },
{ name: '我的收藏', value: 'collection' },
// { name: '我的项目', value: 'projects' },
// { name: '我的订单', value: 'orders' },
// { name: '我的消息', value: 'messages' },
{ name: '我的设置', value: 'settings' }
]
const formatPrice = (price) => {
return '¥ ' + price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const getAosEffect = (index) => {
const screenWidth = window.innerWidth;
let itemsPerRow;
if (screenWidth >= 1024) { // PC
itemsPerRow = 3;
} else if (screenWidth >= 768) { // 平板
itemsPerRow = 2;
} else { // 手机
itemsPerRow = 1;
}
const rowIndex = Math.floor(index / itemsPerRow);
const colIndex = index % itemsPerRow;
const aosEffects = [
['fade-up', 'fade-down', 'fade-left'],
['fade-right', 'zoom-in', 'zoom-out'],
['flip-left', 'flip-right', 'slide-up']
];
return aosEffects[rowIndex % aosEffects.length][colIndex % aosEffects[0].length];
}
const showChangePasswordModal = ref(false);
const changePasswordLoading = ref(false);
const oldPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const passwordError = ref("")
const openChangePasswordModal = () => {
oldPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
showChangePasswordModal.value = true;
}
const closeChangePasswordModal = () => {
showChangePasswordModal.value = false;
}
const handleChangePassword = () => {
// 验证密码
if (!oldPassword.value) {
passwordError.value = "请输入旧密码"
return
}
// 验证密码
if (!newPassword.value) {
passwordError.value = "请输入新密码"
return
}
if (newPassword.value.length < 6) {
passwordError.value = "密码长度不能少于6位"
return
}
if (newPassword.value !== confirmPassword.value) {
passwordError.value = "两次输入的密码不一致"
return
}
changePasswordLoading.value = true
changePasswordApi({
old_password: oldPassword.value,
new_password: newPassword.value,
confirm_password: confirmPassword.value,
}).then(() => {
message.success(`密码修改成功`)
closeChangePasswordModal()
}).finally(() => {
passwordError.value = "";
changePasswordLoading.value = false
})
}
</script>
<template>
@@ -100,12 +216,12 @@ const activeTab = ref('profile')
<div class="pt-20">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">{{ userData.username }}</h1>
<p class="text-rose-600 dark:text-rose-400">{{ userData.role }}</p>
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">{{ userData?.nick_name }}</h1>
<p class="text-rose-600 dark:text-rose-400">{{ userData.roles?.name }}</p>
</div>
<div class="mt-4 md:mt-0">
<button class="px-6 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-full shadow-md hover:shadow-lg transition-all duration-300">
<button @click="message.success('正在开发')" class="px-6 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-full shadow-md hover:shadow-lg transition-all duration-300 cursor-pointer">
升级会员
</button>
</div>
@@ -140,25 +256,13 @@ const activeTab = ref('profile')
<div class="border-b border-gray-200 dark:border-gray-700">
<nav class="flex space-x-8">
<button
@click="activeTab = 'profile'"
v-for="(item) in tabs"
:key="item.value"
@click="activeTab = item.value"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'profile' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
:class="activeTab === item.value ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
个人资料
</button>
<button
@click="activeTab = 'projects'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'projects' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
我的项目
</button>
<button
@click="activeTab = 'settings'"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 cursor-pointer"
:class="activeTab === 'settings' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'"
>
账户设置
{{item.name}}
</button>
</nav>
@@ -171,7 +275,7 @@ const activeTab = ref('profile')
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">用户名</p>
<p class="text-gray-900 dark:text-white">{{ userData.username }}</p>
<p class="text-gray-900 dark:text-white">{{ userData.nick_name }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">邮箱</p>
@@ -179,11 +283,11 @@ const activeTab = ref('profile')
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">手机号</p>
<p class="text-gray-900 dark:text-white">{{ userData.phone }}</p>
<p class="text-gray-900 dark:text-white">{{ userData.account }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">注册时间</p>
<p class="text-gray-900 dark:text-white">{{ userData.createdAt }}</p>
<p class="text-gray-900 dark:text-white">{{ userData.created_day_at }}</p>
</div>
</div>
</div>
@@ -196,7 +300,7 @@ const activeTab = ref('profile')
<p class="font-medium text-gray-900 dark:text-white">登录密码</p>
<p class="text-sm text-gray-500 dark:text-gray-400">上次修改时间: 2023-03-15</p>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
<button @click="openChangePasswordModal" class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium cursor-pointer">
修改
</button>
</div>
@@ -205,7 +309,7 @@ const activeTab = ref('profile')
<p class="font-medium text-gray-900 dark:text-white">手机验证</p>
<p class="text-sm text-gray-500 dark:text-gray-400">已绑定: {{ userData.phone }}</p>
</div>
<button class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium">
<button @click="message.warn('正在开发~')" class="text-rose-600 dark:text-rose-400 hover:text-rose-700 dark:hover:text-rose-300 font-medium cursor-pointer">
修改
</button>
</div>
@@ -213,6 +317,73 @@ const activeTab = ref('profile')
</div>
</div>
<!-- 我的收藏 -->
<div v-else-if="activeTab === 'collection'" class="space-y-6">
<!-- 项目卡片网格 -->
<div v-if="myCollectionProjects.length > 0" class="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-8 mt-10">
<div
v-for="(project, index) in myCollectionProjects"
:key="project.project.id"
class="group bg-white dark:bg-gray-800 rounded-3xl shadow-lg hover:shadow-xl transition-all duration-500 overflow-hidden transform hover:-translate-y-2 cursor-pointer"
:data-aos="getAosEffect(index)"
@click="$router.push(`/project/${project.project.id}`)"
>
<div class="relative h-56 overflow-hidden">
<img
:src="project.project.cover"
class="w-full h-full object-cover transform transition-transform duration-700 group-hover:scale-110"
:alt="project.project.title"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="absolute top-4 right-4">
<span
class="px-3 py-1 bg-black/50 backdrop-blur-md text-white text-xs font-medium rounded-full"
>
{{ project.project.category.name }}
</span>
</div>
</div>
<div class="p-6">
<div class="flex flex-wrap gap-2 mb-3">
<span
v-for="label in project.project.labels"
:key="label.id"
class="px-2 py-1 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-xs font-medium rounded-lg"
>
{{ label.name }}
</span>
</div>
<h3 class="text-xl font-bold mb-2 text-gray-900 dark:text-white group-hover:text-rose-600 dark:group-hover:text-rose-400 transition-colors">
{{ project.project.title }}
</h3>
<p class="text-gray-600 dark:text-gray-300 text-sm mb-4 line-clamp-2">
{{ project.project.description }}
</p>
<div class="flex justify-between items-center">
<div>
<p class="text-lg font-bold text-rose-600 dark:text-rose-400">{{ formatPrice(project.project.price) }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ project.project.created_at }}</p>
</div>
<button
class="w-10 h-10 rounded-full bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 flex items-center justify-center hover:bg-rose-100 dark:hover:bg-rose-900/40 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 我的项目 -->
<div v-else-if="activeTab === 'projects'" class="space-y-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
@@ -265,17 +436,17 @@ const activeTab = ref('profile')
<p class="text-sm text-gray-500 dark:text-gray-400">接收系统更新和安全提醒</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<input type="checkbox" value="" class="sr-only peer" disabled :checked="userData.is_sys_notifications === 1">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">项目更新</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收项目进度和更新通知</p>
<p class="text-sm text-gray-500 dark:text-gray-400">接收收藏的项目进度和更新通知</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer" checked>
<input type="checkbox" value="" class="sr-only peer" disabled :checked="userData.is_collection_notifications === 1">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
@@ -285,7 +456,7 @@ const activeTab = ref('profile')
<p class="text-sm text-gray-500 dark:text-gray-400">接收优惠和促销信息</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="" class="sr-only peer">
<input type="checkbox" value="" class="sr-only peer" disabled :checked="userData.is_marketing_notifications === 1">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-rose-300 dark:peer-focus:ring-rose-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-rose-600"></div>
</label>
</div>
@@ -315,6 +486,97 @@ const activeTab = ref('profile')
</div>
</div>
<!-- 修改密码模态框 -->
<div v-if="showChangePasswordModal" class="fixed inset-0 z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- 背景遮罩 -->
<div
class="fixed inset-0 transition-opacity backdrop-blur-xl bg-gray-500/80 dark:bg-gray-900/80"
aria-hidden="true"
@click="closeChangePasswordModal"
></div>
<!-- 使模态框居中 -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!-- 模态框内容 -->
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full relative z-50">
<div class="bg-white dark:bg-gray-800 px-6 pt-5 pb-4 sm:p-6">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white" id="modal-title">
修改密码 - {{ changePasswordUsername }}
</h3>
<button
class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 cursor-pointer"
@click="closeChangePasswordModal"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form @submit.prevent="handleChangePassword" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">旧密码</label>
<input
v-model="oldPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入旧密码"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">新密码</label>
<input
v-model="newPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请输入新密码"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">确认密码</label>
<input
v-model="confirmPassword"
type="password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-rose-500 focus:border-rose-500 dark:bg-gray-700 dark:text-white"
placeholder="请再次输入新密码"
/>
</div>
<div v-if="passwordError" class="text-sm text-red-600 dark:text-red-400">
{{ passwordError }}
</div>
<div class="flex justify-end space-x-3 pt-4">
<button
type="button"
class="px-5 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
@click="closeChangePasswordModal"
>
取消
</button>
<button
type="submit"
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center cursor-pointer"
:disabled="changePasswordLoading"
>
<span v-if="changePasswordLoading" class="mr-2">
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
确认重置
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>