项目详情、收藏进行登录
This commit is contained in:
@@ -31,10 +31,17 @@ service.interceptors.response.use(
|
||||
if (code === 0) {
|
||||
return result;
|
||||
} else {
|
||||
if (code === 401) {
|
||||
localStorage.removeItem('token');
|
||||
msg.error('登录已过期,请重新登录').then(r => {
|
||||
window.location.href = '/'
|
||||
msg.destroy()
|
||||
})
|
||||
return;
|
||||
}
|
||||
msg.error(message).then(r => {
|
||||
msg.destroy()
|
||||
})
|
||||
return Promise.reject(new Error(result));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
|
||||
22
src/api/request/collection.js
Normal file
22
src/api/request/collection.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {get, post} from '../request.js'
|
||||
|
||||
const prefix = 'collection'
|
||||
|
||||
export function collectionListApi(credentials) {
|
||||
return get(`${prefix}/list`, credentials)
|
||||
}
|
||||
export function collectionOptionApi(credentials) {
|
||||
return get(`${prefix}/option`, credentials)
|
||||
}
|
||||
export function createCollectionApi(credentials) {
|
||||
return post(`${prefix}/create`, credentials)
|
||||
}
|
||||
export function unCollectionApi(credentials) {
|
||||
return post(`${prefix}/un-collection`, credentials)
|
||||
}
|
||||
export function updateCollectionApi(credentials) {
|
||||
return post(`${prefix}/update`, credentials)
|
||||
}
|
||||
export function deleteCollectionApi(credentials) {
|
||||
return post(`${prefix}/delete`, credentials)
|
||||
}
|
||||
30
src/api/request/home.js
Normal file
30
src/api/request/home.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import {get, post} from '../request.js'
|
||||
|
||||
const prefix = 'home'
|
||||
|
||||
/**
|
||||
* 获取首页精选项目列表
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function homeProjectListApi(credentials) {
|
||||
return get(`${prefix}/list`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首页轮播图列表
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function homeCarouselListApi(credentials) {
|
||||
return get(`${prefix}/carousel`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首页项目详情
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function homeProjectDetailApi(credentials) {
|
||||
return get(`${prefix}/detail`, credentials)
|
||||
}
|
||||
@@ -10,6 +10,14 @@ const prefix = 'user'
|
||||
export function loginApi(credentials) {
|
||||
return post('login', credentials)
|
||||
}
|
||||
/**
|
||||
* 注册
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function registerApi(credentials) {
|
||||
return post('register', credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
@@ -20,6 +28,24 @@ export function userListApi(credentials) {
|
||||
return get(`${prefix}/list`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function userMyInfoApi(credentials) {
|
||||
return get(`${prefix}/my-info`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计信息
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function userStatsApi(credentials) {
|
||||
return get(`${prefix}/user-stats`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* @param credentials
|
||||
@@ -54,4 +80,13 @@ export function deleteUserApi(credentials) {
|
||||
*/
|
||||
export function resetPasswordApi(credentials) {
|
||||
return post(`${prefix}/reset-password`, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param credentials
|
||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||
*/
|
||||
export function changePasswordApi(credentials) {
|
||||
return post(`${prefix}/change-password`, credentials)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css'
|
||||
import {registerApi} from "@/api/request/user.js";
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -44,8 +45,17 @@ const close = () => visible.value = false
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await userStore.login(formState.value)
|
||||
close()
|
||||
await userStore.login(formState.value, close)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const handleRegister = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await userStore.register(formState.value, close)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
@@ -222,7 +232,7 @@ defineExpose({
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<form @submit.prevent="handleRegister" class="space-y-6">
|
||||
<!-- 用户名 -->
|
||||
<div data-aos="fade-up" data-aos-delay="200">
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {ref, onMounted, provide} from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { UserOutlined, MenuOutlined } from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@@ -10,7 +10,7 @@ import 'aos/dist/aos.css'
|
||||
// 合并后的状态管理
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const userStore = ref(useUserStore())
|
||||
const authModal = ref(null)
|
||||
const isScrolled = ref(false)
|
||||
const mobileMenuVisible = ref(false)
|
||||
@@ -35,7 +35,15 @@ onMounted(() => {
|
||||
once: false
|
||||
})
|
||||
})
|
||||
console.log(userStore.user)
|
||||
|
||||
const hasClose = () => {
|
||||
userStore.value = useUserStore()
|
||||
}
|
||||
|
||||
const hiddenLoginModalBtn = ref(null)
|
||||
provide('openLoginModal', () => {
|
||||
hiddenLoginModalBtn.value.click();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,6 +81,14 @@ console.log(userStore.user)
|
||||
|
||||
<!-- 用户操作区 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
|
||||
<button
|
||||
ref="hiddenLoginModalBtn"
|
||||
class="hidden"
|
||||
@click="authModal?.show()"
|
||||
>
|
||||
登录/注册
|
||||
</button>
|
||||
<button
|
||||
v-if="!userStore.isAuthenticated"
|
||||
class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-full shadow-md hover:shadow-lg transition-all duration-300 text-sm font-medium"
|
||||
@@ -111,7 +127,7 @@ console.log(userStore.user)
|
||||
</a>
|
||||
<hr class="my-1 border-gray-200 dark:border-gray-700" />
|
||||
<a
|
||||
@click="userStore.logout()"
|
||||
@click="userStore.logout(); mobileMenuVisible = false; router.push('/');"
|
||||
href="javascript:;"
|
||||
class="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:text-rose-600 dark:hover:text-rose-400"
|
||||
>
|
||||
@@ -202,7 +218,7 @@ console.log(userStore.user)
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
@click="userStore.logout(); mobileMenuVisible = false"
|
||||
@click="userStore.logout(); mobileMenuVisible = false; router.push('/');"
|
||||
class="block px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:text-rose-600 dark:hover:text-rose-400 rounded-xl"
|
||||
>
|
||||
退出登录
|
||||
@@ -215,7 +231,7 @@ console.log(userStore.user)
|
||||
<!-- 主内容区 -->
|
||||
<main>
|
||||
<!-- 登录/注册按钮 -->
|
||||
<AuthModal ref="authModal" />
|
||||
<AuthModal ref="authModal" @close="hasClose" />
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ const routes = [
|
||||
path: '/personal',
|
||||
name: 'Personal',
|
||||
component: () => import('@/views/frontend/Personal.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import {loginApi} from "@/api/request/user.js";
|
||||
import {loginApi, registerApi} from "@/api/request/user.js";
|
||||
import {message, notification} from "ant-design-vue";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
|
||||
|
||||
const modal = ref(localStorage.getItem('authModal') || 'false')
|
||||
|
||||
// 添加初始化状态校验
|
||||
const initUser = () => {
|
||||
const userData = localStorage.getItem('user')
|
||||
@@ -20,7 +22,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
const isAuthenticated = computed(() => !!user.value?.nick_name)
|
||||
|
||||
function login(credentials) {
|
||||
function login(credentials, functionName) {
|
||||
console.log(functionName, 'sssssssssssss')
|
||||
loginApi({
|
||||
account: credentials.username,
|
||||
password: credentials.password,
|
||||
@@ -31,8 +34,33 @@ export const useUserStore = defineStore('user', () => {
|
||||
role_name: res.role_name,
|
||||
role_value: res.role_value,
|
||||
}
|
||||
user.value = userData;
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
localStorage.setItem('token', res.token)
|
||||
notification.success({
|
||||
message: '登录成功',
|
||||
description: `欢迎回来${res.nick_name}`,
|
||||
})
|
||||
functionName()
|
||||
})
|
||||
}
|
||||
function register(credentials, functionName) {
|
||||
registerApi({
|
||||
account: credentials.username,
|
||||
password: credentials.password,
|
||||
email: credentials.email,
|
||||
code: credentials.code,
|
||||
}).then((res) => {
|
||||
const userData = {
|
||||
nick_name: res.nick_name,
|
||||
role_name: res.role_name,
|
||||
role_value: res.role_value,
|
||||
}
|
||||
user.value = userData;
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
localStorage.setItem('token', res.token)
|
||||
message.success('注册成功')
|
||||
functionName()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,10 +70,18 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
function setModal(value) {
|
||||
modal.value = modal.value === 'false' ? 'true' : 'false';
|
||||
localStorage.setItem('authModal', value)
|
||||
}
|
||||
|
||||
return {
|
||||
modal,
|
||||
user,
|
||||
isAuthenticated,
|
||||
setModal,
|
||||
login,
|
||||
register,
|
||||
logout
|
||||
}
|
||||
})
|
||||
0
src/utils/authModal.js
Normal file
0
src/utils/authModal.js
Normal file
@@ -584,9 +584,9 @@ getList();
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
item.status === 0
|
||||
item.status === 1
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||
: item.status === 1
|
||||
: item.status === 2
|
||||
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted,reactive, nextTick, inject } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import { Autoplay, Pagination, Navigation, EffectFade } from 'swiper/modules'
|
||||
@@ -9,6 +9,11 @@ import 'swiper/css'
|
||||
import 'swiper/css/pagination'
|
||||
import 'swiper/css/navigation'
|
||||
import 'swiper/css/effect-fade'
|
||||
import {homeProjectDetailApi} from "@/api/request/home.js";
|
||||
import {DollarSign, User} from "lucide-vue-next";
|
||||
import {createCollectionApi, unCollectionApi} from "@/api/request/collection.js";
|
||||
import {message} from "ant-design-vue";
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
|
||||
const route = useRoute()
|
||||
const activeTab = ref('1')
|
||||
@@ -40,35 +45,16 @@ const allTags = [
|
||||
{ id: 5, name: 'Docker' }
|
||||
]
|
||||
|
||||
const project = ref({
|
||||
id: parseInt(route.params.id),
|
||||
title: '电商平台',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
category: categories[0],
|
||||
tags: allTags,
|
||||
author: '技术团队',
|
||||
price: 29999,
|
||||
status: '已发布',
|
||||
createdAt: new Date().toISOString(),
|
||||
description: '基于微服务架构的电商平台,包含商品管理、订单系统、支付对接等模块。采用前后端分离设计,提供完整的API文档和部署方案。系统具有高度可扩展性和定制化能力,适合中小型企业快速搭建自有电商平台。',
|
||||
features: [
|
||||
'响应式设计,支持多端访问',
|
||||
'完整的商品管理系统',
|
||||
'订单处理和物流跟踪',
|
||||
'多种支付方式集成',
|
||||
'会员管理和积分系统',
|
||||
'数据分析和报表功能'
|
||||
],
|
||||
technicalStack: ['前端:Vue3 + TypeScript', '后端:PHP【Laravel 12.x】 + MySQL', '部署:Docker + Kubernetes'],
|
||||
screenshots: [
|
||||
'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
'https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
|
||||
'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280'
|
||||
],
|
||||
video: 'http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4',
|
||||
wechatQR: '/assets/qr-code.png'
|
||||
})
|
||||
const project = ref()
|
||||
|
||||
const getDetail = () => {
|
||||
homeProjectDetailApi({
|
||||
id: route.params.id
|
||||
}).then((res) => {
|
||||
project.value = res
|
||||
})
|
||||
}
|
||||
// 推荐项目
|
||||
const relatedProjects = ref([
|
||||
{
|
||||
id: 1,
|
||||
@@ -111,11 +97,50 @@ const swiperOptions = {
|
||||
const formatPrice = (price) => {
|
||||
return '¥ ' + price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
getDetail();
|
||||
|
||||
const userStore = ref(useUserStore())
|
||||
|
||||
const checkUserInfo = () => {
|
||||
if (!userStore.value.user) {
|
||||
message.error("请先登录")
|
||||
openLoginModalApi();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const collection = () => {
|
||||
if (!checkUserInfo()) {
|
||||
return;
|
||||
}
|
||||
createCollectionApi({
|
||||
project_id: project.value.id
|
||||
}).then(() => {
|
||||
message.success("收藏成功")
|
||||
project.value.is_collection = !project.value.is_collection
|
||||
})
|
||||
}
|
||||
|
||||
const openLoginModalApi = inject('openLoginModal');
|
||||
|
||||
const unCollection = () => {
|
||||
if (!checkUserInfo()) {
|
||||
return;
|
||||
}
|
||||
unCollectionApi({
|
||||
project_id: project.value.id
|
||||
}).then(() => {
|
||||
message.success("取消收藏成功")
|
||||
project.value.is_collection = !project.value.is_collection
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800 min-h-screen py-12">
|
||||
<div class="max-w-6xl mx-auto px-6">
|
||||
<div class="max-w-6xl mx-auto px-6" v-if="project">
|
||||
<!-- 项目封面 -->
|
||||
<div class="mb-10" data-aos="fade-up">
|
||||
<div class="relative h-[50vh] min-h-[400px] rounded-3xl overflow-hidden shadow-2xl">
|
||||
@@ -127,24 +152,37 @@ const formatPrice = (price) => {
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 p-8 md:p-12">
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
|
||||
<h1 class="text-3xl md:text-5xl font-bold text-white mb-4">
|
||||
{{ project.title }}
|
||||
<span
|
||||
class="px-4 py-1.5 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20"
|
||||
>
|
||||
{{ project.category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<h1 class="text-3xl md:text-5xl font-bold text-white mb-4">{{ project.title }}</h1>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<span
|
||||
v-for="tag in project.tags"
|
||||
:key="tag.id"
|
||||
class="px-3 py-1 bg-rose-500/20 backdrop-blur-md text-rose-100 text-sm font-medium rounded-full border border-rose-500/30"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex flex-wrap gap-2 mb-3" data-aos="fade-up" data-aos-delay="200">
|
||||
<span
|
||||
v-for="item in project.frameworks"
|
||||
:key="item.id"
|
||||
class="px-4 py-2 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20 shadow-lg hover:bg-white/20 transition-colors duration-300"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mb-6" data-aos="fade-up" data-aos-delay="200">
|
||||
<span
|
||||
v-for="label in project.labels"
|
||||
:key="label.id"
|
||||
class="px-3 py-1.5 bg-rose-100 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg"
|
||||
>
|
||||
{{ label.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +256,7 @@ const formatPrice = (price) => {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ feature }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ feature.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -233,9 +271,19 @@ const formatPrice = (price) => {
|
||||
技术栈
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
<!-- <li-->
|
||||
<!-- v-for="(tech, index) in project.technicalStack"-->
|
||||
<!-- :key="index"-->
|
||||
<!-- class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">-->
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">-->
|
||||
<!-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />-->
|
||||
<!-- </svg>-->
|
||||
<!-- </div>-->
|
||||
<!-- <span class="text-gray-700 dark:text-gray-300">{{ tech }}</span>-->
|
||||
<!-- </li>-->
|
||||
<li
|
||||
v-for="(tech, index) in project.technicalStack"
|
||||
:key="index"
|
||||
class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"
|
||||
>
|
||||
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">
|
||||
@@ -243,7 +291,27 @@ const formatPrice = (price) => {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ tech }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">前端技术栈: {{ project.front }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"
|
||||
>
|
||||
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-gray-700 dark:text-gray-300">后端技术栈: {{ project.service }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="flex items-center bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3"
|
||||
>
|
||||
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-400 mr-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-gray-700 dark:text-gray-300">部署架构: {{ project.deploy }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -270,7 +338,7 @@ const formatPrice = (price) => {
|
||||
>
|
||||
<div class="relative h-[50vh] min-h-[400px] w-full">
|
||||
<img
|
||||
:src="img"
|
||||
:src="img.url"
|
||||
class="w-full h-full object-contain bg-gray-100 dark:bg-gray-900"
|
||||
:alt="`项目截图 ${index + 1}`"
|
||||
/>
|
||||
@@ -283,7 +351,7 @@ const formatPrice = (price) => {
|
||||
<div v-else-if="activeTab === '3'">
|
||||
<div class="aspect-w-16 aspect-h-9 rounded-2xl overflow-hidden shadow-lg">
|
||||
<iframe
|
||||
:src="project.video"
|
||||
:src="project.video_url"
|
||||
class="w-full h-[50vh] min-h-[400px]"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
@@ -298,13 +366,42 @@ const formatPrice = (price) => {
|
||||
<div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-xl p-8 sticky top-8">
|
||||
<div class="mb-6">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">价格</p>
|
||||
<p class="text-3xl font-bold text-rose-600 dark:text-rose-400">{{ formatPrice(project.price) }}</p>
|
||||
<h3 class="text-lg font-semibold text-white flex items-center">
|
||||
<DollarSign class="w-5 h-5 mr-1.5 text-rose-500"/>
|
||||
项目价格
|
||||
</h3>
|
||||
<!-- <p class="text-3xl font-bold text-rose-600 dark:text-rose-400">{{ formatPrice(project.price) }}</p>-->
|
||||
<div v-if="project.preferential_price" class="flex items-center text-3xl font-bold text-rose-600 dark:text-rose-400">
|
||||
<span class="text-2xl font-bold text-rose-400">
|
||||
{{ formatPrice(project.preferential_price) }}
|
||||
</span>
|
||||
<span class="ml-2 text-sm text-gray-400 line-through">
|
||||
{{ formatPrice(project.price) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-2xl font-bold text-white">
|
||||
{{ formatPrice(project.price) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">开发团队</p>
|
||||
<p class="text-gray-900 dark:text-white font-medium">{{ project.author }}</p>
|
||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<User class="w-5 h-5 mr-1.5 text-rose-500"/>
|
||||
项目作者
|
||||
</h3>
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full overflow-hidden mr-4">
|
||||
<img
|
||||
:src="project.author.avatar"
|
||||
:alt="project.author.nick_name"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-white">{{ project.author.nick_name }}</h4>
|
||||
<p class="text-sm text-gray-400">邮箱: {{ project.author.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -312,12 +409,18 @@ const formatPrice = (price) => {
|
||||
<span>立即咨询</span>
|
||||
</button>
|
||||
|
||||
<button class="w-full py-3 bg-white dark:bg-gray-700 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-800/30 rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center justify-center cursor-pointer">
|
||||
<button v-if="project.is_collection === false" @click="collection" class="w-full py-3 bg-white dark:bg-gray-700 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-800/30 rounded-xl shadow-md hover:shadow-lg transition-all duration-300 flex items-center justify-center cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
<span>收藏项目</span>
|
||||
</button>
|
||||
<button v-else @click="unCollection" class="w-full py-3 bg-rose-600 hover:bg-rose-700 text-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="gold" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
<span>取消收藏</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'swiper/css/pagination'
|
||||
import 'swiper/css/navigation'
|
||||
import 'swiper/css/effect-fade'
|
||||
import 'aos/dist/aos.css'
|
||||
import {homeCarouselListApi, homeProjectListApi} from "@/api/request/home.js";
|
||||
|
||||
const modules = [Autoplay, Pagination, Navigation, EffectFade]
|
||||
|
||||
@@ -20,11 +21,11 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const projects = [
|
||||
const projects = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '电商平台',
|
||||
tags: ['Vue3', 'Node.js', 'PHP'],
|
||||
labels: ['Vue3', 'Node.js', 'PHP'],
|
||||
category: ['Web', 'Uni app', '微信小程序'],
|
||||
description: '基于Vue3和Node.js开发的电商平台,提供完整的购物流程,包括注册、登录、购物、支付等功能。',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
@@ -33,19 +34,19 @@ const projects = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'OA系统',
|
||||
tags: ['React', 'PHP'],
|
||||
labels: ['React', 'PHP'],
|
||||
category: ['Web', 'Uni app', '微信小程序'],
|
||||
description: '基于React和PHP开发的OA系统,提供完整的流程管理、审批、报表等功能。亮点:1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
route: '2'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const carousel = [
|
||||
const carousel = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '电商平台',
|
||||
tags: ['Vue3', 'Node.js', 'PHP'],
|
||||
labels: ['Vue3', 'Node.js', 'PHP'],
|
||||
category: ['Web', 'Uni app', '微信小程序'],
|
||||
description: '基于Vue3和Node.js开发的电商平台,提供完整的购物流程,包括注册、登录、购物、支付等功能。',
|
||||
cover: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280',
|
||||
@@ -54,7 +55,7 @@ const carousel = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'OA系统',
|
||||
tags: ['React', 'PHP'],
|
||||
labels: ['React', 'PHP'],
|
||||
category: ['Web', 'Uni app', '微信小程序'],
|
||||
description: '基于React和PHP开发的OA系统,提供完整的流程管理、审批、报表等功能。亮点:1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统',
|
||||
cover: 'https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
|
||||
@@ -63,13 +64,13 @@ const carousel = [
|
||||
{
|
||||
id: 2,
|
||||
title: '团购app',
|
||||
tags: ['React', 'PHP'],
|
||||
labels: ['React', 'PHP'],
|
||||
category: ['Web', 'Uni app', '微信小程序'],
|
||||
description: '基于React和PHP开发的OA系统,提供完整的流程管理、审批、报表等功能。亮点:1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统1、100万用户、2、Saas系统',
|
||||
cover: 'https://img1.baidu.com/it/u=362305842,3264792867&fm=253&fmt=auto?w=712&h=712',
|
||||
route: '2'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
@@ -101,6 +102,26 @@ const partners = [
|
||||
{ name: 'Microsoft', logo: 'https://pic.rmb.bdstatic.com/bjh/80852bfe7c321988191838517ba64e309354.jpeg@h_1280' },
|
||||
]
|
||||
|
||||
const getProjectList = () => {
|
||||
homeProjectListApi({
|
||||
page: 1,
|
||||
pageSize: 6
|
||||
}).then((res) => {
|
||||
projects.value = res.items
|
||||
})
|
||||
}
|
||||
|
||||
const getCarouseList = () => {
|
||||
homeCarouselListApi({
|
||||
page: 1,
|
||||
pageSize: 6
|
||||
}).then((res) => {
|
||||
carousel.value = res.items
|
||||
})
|
||||
}
|
||||
|
||||
getProjectList();
|
||||
getCarouseList();
|
||||
// 轮播图自动播放配置
|
||||
const swiperOptions = {
|
||||
delay: 5000,
|
||||
@@ -152,7 +173,7 @@ const breakpoints = {
|
||||
v-for="(project, index) in carousel"
|
||||
:key="project.id"
|
||||
class="group cursor-pointer"
|
||||
@click="$router.push(`/project/${project.route}`)"
|
||||
@click="$router.push(`/project/${project.id}`)"
|
||||
>
|
||||
<div class="relative h-[70vh] max-h-[800px] min-h-[500px] w-full overflow-hidden">
|
||||
<img
|
||||
@@ -163,24 +184,39 @@ const breakpoints = {
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 p-8 md:p-12 lg:p-16 z-10 transform transition-transform duration-500">
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex flex-wrap gap-2 mb-6" data-aos="fade-up" data-aos-delay="200">
|
||||
<span
|
||||
v-for="tag in project.tags"
|
||||
:key="tag"
|
||||
class="px-4 py-2 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20 shadow-lg hover:bg-white/20 transition-colors duration-300"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="text-3xl md:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight leading-tight"
|
||||
data-aos="fade-up"
|
||||
>
|
||||
{{ project.title }}
|
||||
<span
|
||||
class="px-4 py-2 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20 shadow-lg hover:bg-white/20 transition-colors duration-300"
|
||||
>
|
||||
{{ project.category.name }}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex flex-wrap gap-2 mb-3" data-aos="fade-up" data-aos-delay="200">
|
||||
<span
|
||||
v-for="item in project.frameworks"
|
||||
:key="item.id"
|
||||
class="px-4 py-2 bg-white/10 backdrop-blur-md text-white text-sm font-medium rounded-full border border-white/20 shadow-lg hover:bg-white/20 transition-colors duration-300"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mb-6" data-aos="fade-up" data-aos-delay="200">
|
||||
<span
|
||||
v-for="label in project.labels"
|
||||
:key="label.id"
|
||||
class="px-3 py-1.5 bg-rose-100 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg"
|
||||
>
|
||||
{{ label.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-lg md:text-xl text-white/80 max-w-2xl line-clamp-3 mb-8"
|
||||
data-aos="fade-up"
|
||||
@@ -190,7 +226,7 @@ const breakpoints = {
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="px-8 py-3 bg-white/10 backdrop-blur-md text-white rounded-full border border-white/20 hover:bg-white/20 transition-all duration-300 shadow-lg group"
|
||||
class="px-8 py-3 bg-rose-50/10 backdrop-blur-md text-white rounded-full border border-white/20 hover:bg-rose-50/20 transition-all duration-300 shadow-lg group"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay="300"
|
||||
>
|
||||
@@ -232,7 +268,7 @@ const breakpoints = {
|
||||
class="group bg-white dark:bg-gray-800 rounded-3xl shadow-xl hover:shadow-2xl transition-all duration-500 overflow-hidden transform hover:-translate-y-2 hover:scale-[1.02] cursor-pointer"
|
||||
data-aos="fade-up"
|
||||
:data-aos-delay="index * 150"
|
||||
@click="$router.push(`/project/${project.route}`)"
|
||||
@click="$router.push(`/project/${project.id}`)"
|
||||
>
|
||||
<div class="relative h-64 md:h-72 overflow-hidden">
|
||||
<img
|
||||
@@ -243,7 +279,7 @@ const breakpoints = {
|
||||
<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 flex gap-2">
|
||||
<span
|
||||
<!-- <span
|
||||
v-for="(category, idx) in project.category.slice(0, 1)"
|
||||
:key="idx"
|
||||
class="px-3 py-1.5 bg-black/50 backdrop-blur-md text-white text-xs font-medium rounded-full"
|
||||
@@ -255,6 +291,11 @@ const breakpoints = {
|
||||
class="px-3 py-1.5 bg-black/50 backdrop-blur-md text-white text-xs font-medium rounded-full"
|
||||
>
|
||||
+{{ project.category.length - 1 }}
|
||||
</span>-->
|
||||
<span
|
||||
class="px-3 py-1.5 bg-black/50 backdrop-blur-md text-white text-xs font-medium rounded-full"
|
||||
>
|
||||
{{ project.category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,11 +303,17 @@ const breakpoints = {
|
||||
<div class="p-7 md:p-8">
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
v-for="tag in project.tags"
|
||||
:key="tag"
|
||||
v-for="label in project.labels.slice(0, 5)"
|
||||
:key="label.id"
|
||||
class="px-3 py-1.5 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg"
|
||||
>
|
||||
{{ tag }}
|
||||
{{ label.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="project.labels.length > 5"
|
||||
class="px-3 py-1.5 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-300 text-sm font-medium rounded-lg"
|
||||
>
|
||||
+{{ project.labels.length - 5 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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">​</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>
|
||||
|
||||
Reference in New Issue
Block a user