基本完成了用户相关前端页面的编写,经过测试能够实现相关功能

This commit is contained in:
puzvv
2025-12-20 01:37:06 +08:00
parent 15885463d3
commit 57fe499080
21 changed files with 1800 additions and 203 deletions

126
auto-imports.d.ts vendored
View File

@@ -6,69 +6,69 @@
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isShallow: typeof import('vue')['isShallow']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const EffectScope: typeof import('vue').EffectScope
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const effectScope: typeof import('vue').effectScope
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {

View File

@@ -16,6 +16,7 @@
"@vueuse/core": "^13.9.0",
"axios": "^1.11.0",
"daisyui": "^5.0.50",
"element-plus": "^2.12.0",
"motion-v": "^1.6.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",

14
src/apis/product.ts Normal file
View File

@@ -0,0 +1,14 @@
import http from "../utils/http";
export const addProduct = (data: {
name: string;
price: number;
description: string;
image: string;
}) => {
return http({
url: "/product",
method: "post",
data,
});
};

View File

@@ -1,19 +1,97 @@
import http from "../utils/http";
import {
LoginRequest,
LoginResponse,
RegisterRequest,
UserInfo,
} from "@/types/user.ts";
import { UserAddress, UserAddressForm } from "@/types/address.ts";
/**
* This is an example, please remove if not needed
*/
export const getUserList = () => {
return http({
url: "/user/list",
method: "get",
});
};
// export const getUserList = () => {
// return http({
// url: "/user/list",
// method: "get",
// });
// };
//
// export const insertUser = (data: { account: string; password: string }) => {
// return http({
// url: "/user/insert",
// method: "post",
// data,
// });
// };
export const insertUser = (data: { account: string; passowrd: string }) => {
return http({
url: "/user/insert",
// 用户登录
export const userLogin = (data: LoginRequest) => {
return http<LoginResponse>({
url: "/user/login",
method: "post",
data,
});
};
// 用户注册
export const userRegister = (data: RegisterRequest) => {
return http({
url: "/user/register",
method: "post",
data,
});
};
// 获取用户信息
export const getUserProfile = (id: number) => {
return http<UserInfo>({
url: `/user/profile/${id}`,
method: "get",
});
};
// 获取用户地址列表
export const getAddressList = (userId: number) => {
return http<UserAddress[]>({
url: "/user/address",
method: "get",
params: { userId },
});
};
// 添加地址
export const addAddress = (data: UserAddressForm) => {
return http({
url: "/user/address",
method: "post",
data,
});
};
// 更新地址
export const updateAddress = (data: UserAddressForm) => {
return http({
url: "/user/address",
method: "put",
data,
});
};
// 删除地址
export const deleteAddress = (id: number, userId: number) => {
return http({
url: `/user/address/${id}`,
method: "delete",
params: { userId },
});
};
// 设置默认地址
export const setDefaultAddress = (id: number, userId: number) => {
return http({
url: `/user/address/${id}/default`,
method: "put",
params: { userId },
});
};

112
src/layouts/AuthLayout.vue Normal file
View File

@@ -0,0 +1,112 @@
NEW_FILE_CODE
<script lang="ts" setup>
// import { useRouter } from "vue-router";
// const router = useRouter();
//
// // 跳转到登录页
// const goToLogin = () => {
// router.push("/user/login");
// };
//
// // 跳转到注册页
// const goToRegister = () => {
// router.push("/user/register");
// };
</script>
<template>
<div class="auth-layout min-h-screen flex flex-col">
<!-- 极简头部 -->
<header class="auth-header py-6">
<div class="container mx-auto px-4">
<div class="flex justify-center">
<div class="flex items-center space-x-3">
<i class="fa-solid fa-store text-3xl text-blue-600"></i>
<h1 class="text-2xl font-bold text-gray-800">致微商城</h1>
</div>
</div>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-1">
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="md:flex">
<!-- 左侧宣传区 -->
<div
class="md:w-2/5 bg-gradient-to-br from-blue-500 to-indigo-600 p-8 text-white hidden md:block"
>
<div class="h-full flex flex-col justify-center">
<h2 class="text-3xl font-bold mb-4">欢迎加入致微商城</h2>
<p class="mb-6 opacity-90">
发现更多优质商品享受便捷购物体验
</p>
<ul class="space-y-3">
<li class="flex items-center">
<i class="fa-solid fa-check-circle mr-2"></i>
<span>海量商品品质保证</span>
</li>
<li class="flex items-center">
<i class="fa-solid fa-check-circle mr-2"></i>
<span>快速配送售后无忧</span>
</li>
<li class="flex items-center">
<i class="fa-solid fa-check-circle mr-2"></i>
<span>专享优惠会员特权</span>
</li>
</ul>
</div>
</div>
<!-- 右侧表单区 -->
<div class="md:w-3/5 p-8">
<slot />
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 简化底部 -->
<footer class="auth-footer py-6">
<div class="container mx-auto px-4">
<div class="text-center text-gray-500 text-sm">
<div class="flex flex-wrap justify-center gap-4 mb-2">
<a href="#" class="hover:text-blue-600 transition-colors"
>用户协议</a
>
<a href="#" class="hover:text-blue-600 transition-colors"
>隐私政策</a
>
<a href="#" class="hover:text-blue-600 transition-colors"
>联系我们</a
>
</div>
<p>© 2025 致微商城 版权所有</p>
</div>
</div>
</footer>
</div>
</template>
<style scoped>
.auth-layout {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
}
.auth-header {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.auth-footer {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
</style>

276
src/layouts/MainLayout.vue Normal file
View File

@@ -0,0 +1,276 @@
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/UserStore";
import { ElMessage } from "element-plus";
const router = useRouter();
const userStore = useUserStore();
const isMobileMenuOpen = ref(false);
// 计算属性:判断用户是否已登录
const isLoggedIn = computed(() => {
return !!userStore.token;
});
// 计算属性:获取用户昵称
const userNickname = computed(() => {
return userStore.userInfo?.nickname || userStore.userInfo?.username || "用户";
});
// 跳转登录页
const goToLogin = () => {
router.push("/user/login");
isMobileMenuOpen.value = false;
};
// 跳转注册页
const goToRegister = () => {
router.push("/user/register");
isMobileMenuOpen.value = false;
};
// 用户退出登录
const handleLogout = () => {
userStore.clearUserInfo();
ElMessage.success("退出登录成功");
router.push("/user/login");
};
// 跳转到个人中心
const goToProfile = () => {
router.push(`/user/profile/${userStore.userInfo?.id}`);
isMobileMenuOpen.value = false;
};
</script>
<template>
<div class="flex flex-col min-h-screen bg-slate-50">
<!-- 顶部导航栏 -->
<header class="sticky top-0 z-50 bg-white shadow-sm">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<!-- 网站Logo -->
<div class="flex items-center space-x-2">
<i class="fa-solid fa-store text-2xl text-blue-600"></i>
<h1 class="text-xl font-bold text-gray-800">致微商城</h1>
</div>
<!-- PC端导航 -->
<nav class="hidden md:flex items-center space-x-8">
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
>首页</a
>
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
>商品分类</a
>
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
>购物车</a
>
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
>我的订单</a
>
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
@click.prevent="$router.push('/user/address')"
>
地址管理
</a>
</nav>
<!-- 登录/注册按钮 用户信息 (PC端) -->
<div class="hidden md:flex items-center space-x-4">
<template v-if="isLoggedIn">
<el-dropdown>
<div class="flex items-center cursor-pointer">
<el-avatar size="small" class="mr-2">{{
userNickname.charAt(0)
}}</el-avatar>
<span>{{ userNickname }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="goToProfile"
>个人中心</el-dropdown-item
>
<el-dropdown-item @click="handleLogout"
>退出登录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<button
@click="goToLogin"
class="px-4 py-2 text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 transition-colors"
>
登录
</button>
<button
@click="goToRegister"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
注册
</button>
</template>
</div>
<!-- 移动端菜单按钮 -->
<button
class="md:hidden text-gray-700"
@click="isMobileMenuOpen = !isMobileMenuOpen"
>
<i class="fa-solid fa-bars text-xl"></i>
</button>
</div>
</div>
<!-- 移动端下拉菜单 -->
<div
v-show="isMobileMenuOpen"
class="md:hidden bg-white border-t border-gray-100 shadow-lg animate-fadeIn"
>
<div class="container mx-auto px-4 py-3 flex flex-col space-y-4">
<a
href="#"
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
>首页</a
>
<a
href="#"
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
>商品分类</a
>
<a
href="#"
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
>购物车</a
>
<a
href="#"
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
>我的订单</a
>
<div class="pt-2 border-t border-gray-100">
<template v-if="isLoggedIn">
<div class="py-2 text-gray-700">欢迎{{ userNickname }}</div>
<button
@click="goToProfile"
class="w-full mb-2 px-4 py-2 text-left text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
个人中心
</button>
<button
@click="handleLogout"
class="w-full px-4 py-2 text-left text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
退出登录
</button>
</template>
<template v-else>
<div class="flex space-x-4 pt-2">
<button
@click="goToLogin"
class="flex-1 px-4 py-2 text-blue-600 border border-blue-600 rounded-lg hover:bg-blue-50 transition-colors"
>
登录
</button>
<button
@click="goToRegister"
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
注册
</button>
</div>
</template>
</div>
</div>
</div>
</header>
<!-- 主内容区域 (插槽) -->
<main class="flex-1">
<slot />
</main>
<!-- 页脚 -->
<footer class="bg-gray-800 text-white py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 class="text-lg font-semibold mb-4">关于我们</h3>
<p class="text-gray-300 text-sm">
致微商城专注于提供高品质商品用心服务每一位用户
</p>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">联系方式</h3>
<p class="text-gray-300 text-sm mb-2">
<i class="fa-solid fa-phone mr-2"></i> 17816776925
</p>
<p class="text-gray-300 text-sm">
<i class="fa-solid fa-envelope mr-2"></i> 1505204847@qq.com
</p>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">关注我们</h3>
<div class="flex space-x-4">
<a
href="#"
class="text-gray-300 hover:text-white transition-colors"
>
<i class="fa-brands fa-weixin text-xl"></i>
</a>
<a
href="#"
class="text-gray-300 hover:text-white transition-colors"
>
<i class="fa-brands fa-weibo text-xl"></i>
</a>
<a
href="#"
class="text-gray-300 hover:text-white transition-colors"
>
<i class="fa-brands fa-qq text-xl"></i>
</a>
</div>
</div>
</div>
<div
class="mt-8 pt-4 border-t border-gray-700 text-center text-gray-400 text-sm"
>
© 2025 致微商城 版权所有
</div>
</div>
</footer>
</div>
</template>
<style scoped>
/* 简单动画 */
.animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -4,8 +4,10 @@ import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
createApp(App).use(router).use(pinia).mount("#app");
createApp(App).use(router).use(pinia).use(ElementPlus).mount("#app");

View File

@@ -1,19 +0,0 @@
<script lang="ts" setup>
// About page component
</script>
<template>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">关于我们</h1>
<p class="py-6">这是一个自动路由注册的示例页面</p>
<p>路径将自动转换为 /about</p>
</div>
</div>
</div>
</template>
<style scoped>
/* Component styles */
</style>

View File

@@ -1,77 +0,0 @@
<script lang="ts" setup>
import { range } from "radash";
import { usePagination } from "@/utils/pagination";
const merchandise = ref([
{
id: 1,
name: "商品1",
},
]);
const { currentPage, changePage, pageNumbers, totalPages, getPaginatedData } =
usePagination(() => merchandise.value);
const handlePageChange = (page: number) => {
changePage(page);
};
onMounted(async () => {
await new Promise(() => {
setTimeout(() => {
merchandise.value = Array.from(range(1, 50)).map((item) => ({
id: item,
name: `商品${item}`,
}));
}, 1000);
});
});
</script>
<template>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">帮助</h1>
</div>
<div>
<ul>
<li v-for="item in getPaginatedData" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
<button
class="join-item btn btn-sm"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
«
</button>
<button
v-for="page in pageNumbers"
:key="page"
class="join-item btn btn-sm"
:class="{
'btn-active': page === currentPage,
'btn-disabled': page === '...',
}"
@click="page !== '...' && handlePageChange(Number(page))"
>
{{ page }}
</button>
<button
class="join-item btn btn-sm"
:disabled="currentPage === totalPages"
@click="handlePageChange(currentPage + 1)"
>
»
</button>
</div>
</div>
</template>
<style scoped>
/* Component styles */
</style>

View File

@@ -1,18 +1,207 @@
<script lang="ts" setup>
// Home page component
import MainLayout from "@/layouts/MainLayout.vue";
import { useRouter } from "vue-router";
const router = useRouter();
// 跳转登录/注册
const goToLogin = () => router.push("/user/login");
const goToRegister = () => router.push("/user/register");
</script>
<template>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">首页</h1>
<p class="py-6">欢迎使用自动路由系统</p>
<MainLayout>
<!-- 英雄区 -->
<section
class="relative bg-gradient-to-r from-blue-600 to-indigo-700 text-white"
>
<div class="container mx-auto px-4 py-20 md:py-32">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6 leading-tight">
发现好物轻松购物
</h1>
<p class="text-xl md:text-2xl text-blue-100 mb-8">
一站式购物平台品质保障售后无忧
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<button
@click="goToLogin"
class="px-8 py-3 bg-white text-blue-700 font-semibold rounded-lg hover:bg-blue-50 transition-colors shadow-lg"
>
立即登录
</button>
<button
@click="goToRegister"
class="px-8 py-3 bg-transparent border-2 border-white text-white font-semibold rounded-lg hover:bg-white/10 transition-colors shadow-lg"
>
免费注册
</button>
</div>
</div>
</div>
<!-- 装饰元素 -->
<div
class="absolute bottom-0 left-0 w-full h-16 bg-gradient-to-t from-slate-50 to-transparent"
></div>
</section>
<!-- 特色板块 -->
<section class="py-16 bg-slate-50">
<div class="container mx-auto px-4">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-800">为什么选择我们</h2>
<p class="mt-4 text-gray-600">用心服务让购物更简单</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- 特色1 -->
<div
class="bg-white p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow"
>
<div
class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto"
>
<i class="fa-solid fa-shield-halved text-2xl text-blue-600"></i>
</div>
<h3 class="text-xl font-semibold text-center mb-3">品质保障</h3>
<p class="text-gray-600 text-center">
所有商品均经过严格筛选正品保障假一赔十
</p>
</div>
<!-- 特色2 -->
<div
class="bg-white p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow"
>
<div
class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto"
>
<i class="fa-solid fa-truck-fast text-2xl text-blue-600"></i>
</div>
<h3 class="text-xl font-semibold text-center mb-3">极速配送</h3>
<p class="text-gray-600 text-center">
全国覆盖的物流网络最快24小时送达
</p>
</div>
<!-- 特色3 -->
<div
class="bg-white p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow"
>
<div
class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto"
>
<i class="fa-solid fa-headset text-2xl text-blue-600"></i>
</div>
<h3 class="text-xl font-semibold text-center mb-3">贴心售后</h3>
<p class="text-gray-600 text-center">
7天无理由退换专业客服团队24小时在线
</p>
</div>
</div>
</div>
</section>
<!-- 热门商品推荐 (占位) -->
<section class="py-16 bg-white">
<div class="container mx-auto px-4">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-800">热门商品</h2>
<p class="mt-4 text-gray-600">为你精选热门好物</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6">
<!-- 商品卡片占位 -->
<div
class="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow overflow-hidden"
>
<div class="bg-gray-200 h-48 flex items-center justify-center">
<i class="fa-solid fa-box text-4xl text-gray-400"></i>
</div>
<div class="p-4">
<h4 class="font-medium text-gray-800 mb-2">商品名称</h4>
<p class="text-red-600 font-bold">¥99.00</p>
<button
class="mt-4 w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
加入购物车
</button>
</div>
</div>
<!-- 重复3个占位卡片 -->
<div
class="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow overflow-hidden"
>
<div class="bg-gray-200 h-48 flex items-center justify-center">
<i class="fa-solid fa-box text-4xl text-gray-400"></i>
</div>
<div class="p-4">
<h4 class="font-medium text-gray-800 mb-2">商品名称</h4>
<p class="text-red-600 font-bold">¥199.00</p>
<button
class="mt-4 w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
加入购物车
</button>
</div>
</div>
<div
class="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow overflow-hidden"
>
<div class="bg-gray-200 h-48 flex items-center justify-center">
<i class="fa-solid fa-box text-4xl text-gray-400"></i>
</div>
<div class="p-4">
<h4 class="font-medium text-gray-800 mb-2">商品名称</h4>
<p class="text-red-600 font-bold">¥299.00</p>
<button
class="mt-4 w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
加入购物车
</button>
</div>
</div>
<div
class="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow overflow-hidden"
>
<div class="bg-gray-200 h-48 flex items-center justify-center">
<i class="fa-solid fa-box text-4xl text-gray-400"></i>
</div>
<div class="p-4">
<h4 class="font-medium text-gray-800 mb-2">商品名称</h4>
<p class="text-red-600 font-bold">¥399.00</p>
<button
class="mt-4 w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
加入购物车
</button>
</div>
</div>
</div>
<!-- 查看更多按钮 -->
<div class="text-center mt-10">
<button
class="px-8 py-3 border border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 transition-colors"
>
查看更多商品
</button>
</div>
</div>
</section>
</MainLayout>
</template>
<style scoped>
/* Component styles */
/* 自定义样式补充 */
section {
scroll-margin-top: 70px; /* 适配固定导航栏的锚点定位 */
}
/* 响应式调整 */
@media (max-width: 768px) {
.hero-content h1 {
font-size: 3rem !important;
}
}
</style>

View File

@@ -1,15 +0,0 @@
<script lang="ts" setup></script>
<template>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">我的</h1>
<p class="py-6">这是一个自动路由注册的示例页面</p>
<p>路径将自动转换为 /panel/me</p>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,352 @@
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import {
ElCard,
ElTable,
ElTableColumn,
ElButton,
ElMessage,
ElPopconfirm,
ElTag,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElSwitch,
} from "element-plus";
import { Plus, Edit, Delete, Star } from "@element-plus/icons-vue";
import { useUserStore } from "@/stores/UserStore";
import {
getAddressList,
addAddress,
updateAddress,
deleteAddress,
setDefaultAddress,
} from "@/apis/user";
import type { UserAddress, UserAddressForm } from "@/types/address";
const userStore = useUserStore();
const addressList = ref<UserAddress[]>([]);
const loading = ref(false);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const isEdit = ref(false);
// 表单数据
const addressForm = ref<UserAddressForm>({
id: undefined,
userId: userStore.userInfo?.id || 0,
receiverName: "",
receiverPhone: "",
province: "",
city: "",
district: "",
detailAddress: "",
isDefault: 0,
});
// 表单验证规则
const rules = {
receiverName: [
{ required: true, message: "请输入收货人姓名", trigger: "blur" },
],
receiverPhone: [
{ required: true, message: "请输入收货人手机号", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
trigger: "blur",
},
],
province: [{ required: true, message: "请输入省份", trigger: "blur" }],
city: [{ required: true, message: "请输入城市", trigger: "blur" }],
district: [{ required: true, message: "请输入区县", trigger: "blur" }],
detailAddress: [
{ required: true, message: "请输入详细地址", trigger: "blur" },
],
};
const formRef = ref();
// 获取地址列表
const fetchAddressList = async () => {
if (!userStore.userInfo?.id) {
ElMessage.error("用户未登录");
return;
}
loading.value = true;
try {
const response = await getAddressList(userStore.userInfo.id);
if (response.code === 200) {
addressList.value = response.data;
} else {
ElMessage.error(response.message || "获取地址列表失败");
}
} catch (err) {
console.error(err);
ElMessage.error("获取地址列表失败");
} finally {
loading.value = false;
}
};
// 新增地址
const handleAddAddress = () => {
dialogTitle.value = "新增地址";
isEdit.value = false;
addressForm.value = {
id: undefined,
userId: userStore.userInfo?.id || 0,
receiverName: "",
receiverPhone: "",
province: "",
city: "",
district: "",
detailAddress: "",
isDefault: 0,
};
dialogVisible.value = true;
};
// 编辑地址
const handleEditAddress = (row: UserAddress) => {
dialogTitle.value = "编辑地址";
isEdit.value = true;
addressForm.value = { ...row };
dialogVisible.value = true;
};
// 删除地址
const handleDeleteAddress = async (id: number) => {
if (!userStore.userInfo?.id) {
ElMessage.error("用户未登录");
return;
}
try {
const response = await deleteAddress(id, userStore.userInfo.id);
if (response.code === 200) {
ElMessage.success("删除成功");
fetchAddressList(); // 重新获取地址列表
} else {
ElMessage.error(response.message || "删除失败");
}
} catch (err) {
console.error(err);
ElMessage.error("删除失败");
}
};
// 设置默认地址
const handleSetDefault = async (id: number) => {
if (!userStore.userInfo?.id) {
ElMessage.error("用户未登录");
return;
}
try {
const response = await setDefaultAddress(id, userStore.userInfo.id);
if (response.code === 200) {
ElMessage.success("设置默认地址成功");
fetchAddressList(); // 重新获取地址列表
} else {
ElMessage.error(response.message || "设置失败");
}
} catch (err) {
console.error(err);
ElMessage.error("设置失败");
}
};
// 提交表单
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
try {
let response;
if (isEdit.value) {
// 编辑地址
response = await updateAddress(addressForm.value);
} else {
// 新增地址
response = await addAddress(addressForm.value);
}
if (response.code === 200) {
ElMessage.success(isEdit.value ? "更新成功" : "添加成功");
dialogVisible.value = false;
fetchAddressList(); // 重新获取地址列表
} else {
ElMessage.error(
response.message || (isEdit.value ? "更新失败" : "添加失败"),
);
}
} catch (err) {
console.error(err);
ElMessage.error(isEdit.value ? "更新失败" : "添加失败");
}
});
};
// 格式化地址
const formatAddress = (address: UserAddress): string => {
return `${address.province}${address.city}${address.district}${address.detailAddress}`;
};
onMounted(() => {
fetchAddressList();
});
</script>
<template>
<div class="address-page">
<div class="container mx-auto px-4 py-8">
<el-card class="address-card">
<template #header>
<div class="flex justify-between items-center">
<span class="text-xl font-bold">地址管理</span>
<el-button type="primary" @click="handleAddAddress">
<el-icon><plus /></el-icon>
新增收货地址
</el-button>
</div>
</template>
<!-- 地址列表 -->
<el-table
:data="addressList"
v-loading="loading"
empty-text="暂无地址信息"
stripe
>
<el-table-column prop="receiverName" label="收货人" width="100" />
<el-table-column prop="receiverPhone" label="手机号" width="120" />
<el-table-column label="地址">
<template #default="{ row }">
{{ formatAddress(row) }}
</template>
</el-table-column>
<el-table-column label="默认地址" width="100">
<template #default="{ row }">
<el-tag v-if="row.isDefault === 1" type="success">默认</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<div class="flex space-x-2">
<el-button
size="small"
type="primary"
link
@click="handleEditAddress(row)"
>
<el-icon><edit /></el-icon>
编辑
</el-button>
<el-popconfirm
title="确定要删除这个地址吗?"
@confirm="handleDeleteAddress(row.id)"
>
<template #reference>
<el-button size="small" type="danger" link>
<el-icon><delete /></el-icon>
删除
</el-button>
</template>
</el-popconfirm>
<el-button
v-if="row.isDefault !== 1"
size="small"
type="warning"
link
@click="handleSetDefault(row.id)"
>
<el-icon><star /></el-icon>
设为默认
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 地址编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="formRef?.resetFields()"
>
<el-form
ref="formRef"
:model="addressForm"
:rules="rules"
label-width="100px"
>
<el-form-item label="收货人" prop="receiverName">
<el-input
v-model="addressForm.receiverName"
placeholder="请输入收货人姓名"
/>
</el-form-item>
<el-form-item label="手机号" prop="receiverPhone">
<el-input
v-model="addressForm.receiverPhone"
placeholder="请输入收货人手机号"
/>
</el-form-item>
<el-form-item label="省份" prop="province">
<el-input v-model="addressForm.province" placeholder="请输入省份" />
</el-form-item>
<el-form-item label="城市" prop="city">
<el-input v-model="addressForm.city" placeholder="请输入城市" />
</el-form-item>
<el-form-item label="区县" prop="district">
<el-input v-model="addressForm.district" placeholder="请输入区县" />
</el-form-item>
<el-form-item label="详细地址" prop="detailAddress">
<el-input
v-model="addressForm.detailAddress"
type="textarea"
placeholder="请输入详细地址"
/>
</el-form-item>
<el-form-item label="设为默认">
<el-switch
v-model="addressForm.isDefault"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.address-page {
min-height: 100vh;
background-color: #f5f7fa;
}
.address-card {
border-radius: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -0,0 +1,161 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import AuthLayout from "@/layouts/AuthLayout.vue";
import { ElForm, ElFormItem, ElInput, ElButton, ElMessage } from "element-plus";
import { User, Lock } from "@element-plus/icons-vue";
import { userLogin } from "@/apis/user";
import { useUserStore } from "@/stores/UserStore";
import type { LoginRequest } from "@/types/user";
import type { FormRules } from "element-plus";
// 表单数据
const loginForm = ref<LoginRequest>({
username: "",
password: "",
});
// 表单验证规则
const rules = ref<FormRules>({
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{
min: 3,
max: 20,
message: "用户名长度应在3-20个字符之间",
trigger: "blur",
},
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, max: 20, message: "密码长度应在6-20个字符之间", trigger: "blur" },
],
});
const formRef = ref();
const loading = ref(false);
const router = useRouter();
const userStore = useUserStore();
// 处理登录
const handleLogin = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
const response = await userLogin(loginForm.value);
console.log("登录响应数据:", response);
if (response.code === 200) {
// 保存 token 和用户信息到 store
userStore.setToken(response.data.token);
userStore.setUserInfo({
id: response.data.id,
username: response.data.username,
nickname: response.data.nickname,
});
ElMessage.success("登录成功");
// 跳转到首页
router.push("/");
} else {
ElMessage.error(response.message || "登录失败");
}
} catch (_err: unknown) {
// 错误处理,显示更具体的错误信息
if (_err instanceof Error) {
ElMessage.error(_err.message || "登录请求失败,请稍后再试");
} else {
ElMessage.error("登录请求失败,请稍后再试");
}
} finally {
loading.value = false;
}
});
};
// 跳转到注册页面
const goToRegister = () => {
router.push("/user/register");
};
// 忘记密码功能(占位)
const handleForgetPassword = () => {
ElMessage.info("忘记密码功能开发中");
};
</script>
<template>
<AuthLayout>
<div class="auth-form">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold text-gray-800 mb-2">用户登录</h2>
<p class="text-gray-600">欢迎回来请登录您的账户</p>
</div>
<el-form
ref="formRef"
:model="loginForm"
:rules="rules"
label-position="top"
@keyup.enter="handleLogin"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
:prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<div class="flex justify-end mb-4">
<el-button type="text" @click="handleForgetPassword">
忘记密码
</el-button>
</div>
<el-form-item>
<el-button
type="primary"
class="w-full"
size="large"
:loading="loading"
@click="handleLogin"
>
{{ loading ? "登录中..." : "登录" }}
</el-button>
</el-form-item>
</el-form>
<div class="text-center mt-6">
<p class="text-gray-600">
还没有账户
<el-button type="text" @click="goToRegister">立即注册</el-button>
</p>
</div>
</div>
</AuthLayout>
</template>
<style scoped>
.auth-form {
width: 100%;
max-width: 350px;
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,242 @@
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import {
ElCard,
ElDescriptions,
ElDescriptionsItem,
ElTag,
ElButton,
ElMessage,
} from "element-plus";
import {
Iphone,
Message,
Male,
Female,
UserFilled,
} from "@element-plus/icons-vue";
import { getUserProfile } from "@/apis/user";
import { useUserStore } from "@/stores/UserStore";
import type { UserInfo } from "@/types/user";
// 用户信息
const userInfo = ref<UserInfo | null>(null);
const loading = ref(true);
const router = useRouter();
const userStore = useUserStore();
// 性别映射
const genderMap: { [key: number]: string } = {
0: "未知",
1: "男",
2: "女",
};
// 状态映射
const statusMap: {
[key: number]: { label: string; type: "danger" | "success" | "info" };
} = {
0: { label: "禁用", type: "danger" },
1: { label: "正常", type: "success" },
};
// 格式化时间显示
const formatDateTime = (dateTime: string | undefined | null): string => {
if (!dateTime) return "-";
// 如果是完整的日期时间字符串,可以直接返回
// 如果需要特定格式,可以使用 date-fns 或 dayjs 等库进行格式化
return dateTime;
};
// 获取用户信息
const fetchUserProfile = async () => {
try {
if (!userStore.userInfo?.id) {
ElMessage.error("用户信息不存在");
router.push("/user/login");
return;
}
const response = await getUserProfile(userStore.userInfo.id);
if (response.code === 200) {
userInfo.value = response.data;
} else {
ElMessage.error(response.message || "获取用户信息失败");
}
} catch (err: unknown) {
console.error(err);
ElMessage.error("获取用户信息失败");
} finally {
loading.value = false;
}
};
// 编辑个人信息
const handleEditProfile = () => {
ElMessage.info("编辑功能开发中");
};
// 修改密码
const handleChangePassword = () => {
ElMessage.info("修改密码功能开发中");
};
// 退出登录
const handleLogout = () => {
userStore.clearUserInfo();
ElMessage.success("退出登录成功");
router.push("/user/login");
};
// 获取性别文本
const getGenderText = (gender: number | undefined | null): string => {
if (gender === undefined || gender === null) return "-";
return genderMap[gender] || "未知";
};
// 获取状态标签信息
const getStatusInfo = (
status: number | undefined,
): { label: string; type: "danger" | "success" | "info" } => {
if (status === undefined) return { label: "未知", type: "info" };
return statusMap[status] || { label: "未知", type: "info" };
};
onMounted(() => {
fetchUserProfile();
});
</script>
<template>
<div class="profile-page">
<div class="container mx-auto px-4 py-8">
<el-card class="profile-card">
<template #header>
<div
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"
>
<span class="text-xl font-bold">个人中心</span>
<div class="flex flex-wrap gap-2">
<el-button @click="handleEditProfile" size="small">
编辑信息
</el-button>
<el-button
type="primary"
@click="handleChangePassword"
size="small"
>
修改密码
</el-button>
<el-button type="danger" @click="handleLogout" size="small">
退出登录
</el-button>
</div>
</div>
</template>
<div v-if="loading" class="text-center py-10">
<i class="el-icon-loading text-2xl"></i>
<p class="mt-2">加载中...</p>
</div>
<div v-else-if="userInfo" class="profile-content">
<div
class="flex flex-col md:flex-row items-center mb-8 pb-6 border-b"
>
<el-avatar :size="80" class="mb-4 md:mb-0 md:mr-6">
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="头像" />
<user-filled v-else />
</el-avatar>
<div class="text-center md:text-left">
<h2 class="text-2xl font-bold mb-2">
{{ userInfo.nickname || userInfo.username }}
</h2>
<p class="text-gray-600 mb-2 text-sm">@{{ userInfo.username }}</p>
<el-tag :type="getStatusInfo(userInfo.status).type" size="small">
{{ getStatusInfo(userInfo.status).label }}
</el-tag>
</div>
</div>
<el-descriptions title="基本信息" :column="1" border>
<el-descriptions-item label="用户ID">
{{ userInfo.id }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ userInfo.username }}
</el-descriptions-item>
<el-descriptions-item label="昵称">
{{ userInfo.nickname || "-" }}
</el-descriptions-item>
<el-descriptions-item label="性别">
<div class="flex items-center">
<male
v-if="userInfo.gender === 1"
class="text-blue-500 mr-1"
:size="14"
/>
<female
v-else-if="userInfo.gender === 2"
class="text-pink-500 mr-1"
:size="14"
/>
<span>{{ getGenderText(userInfo.gender) }}</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="手机号">
<div class="flex items-center">
<iphone class="mr-2 text-gray-500" :size="14" />
<span>{{ userInfo.phone || "-" }}</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="邮箱">
<div class="flex items-center">
<message class="mr-2 text-gray-500" :size="14" />
<span>{{ userInfo.email || "-" }}</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(userInfo.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(userInfo.updateTime) }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else class="text-center py-10">
<p>无法加载用户信息</p>
</div>
</el-card>
</div>
</div>
</template>
<style scoped>
.profile-page {
min-height: 100vh;
background-color: #f5f7fa;
}
.profile-card {
border-radius: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
max-width: 800px;
margin: 0 auto;
}
.profile-content {
padding: 20px 0;
}
:deep(.el-descriptions__label) {
width: 100px !important;
font-weight: 500;
}
:deep(.el-descriptions__content) {
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,172 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import AuthLayout from "@/layouts/AuthLayout.vue";
import { ElForm, ElFormItem, ElInput, ElButton, ElMessage } from "element-plus";
import { User, Lock, Iphone, Message } from "@element-plus/icons-vue";
import { userRegister } from "@/apis/user";
import type { RegisterRequest } from "@/types/user";
import type { FormRules } from "element-plus";
// 表单数据
const registerForm = ref<RegisterRequest>({
username: "",
password: "",
phone: "",
email: "",
});
// 表单验证规则
const rules = ref<FormRules>({
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{
min: 3,
max: 20,
message: "用户名长度应在3-20个字符之间",
trigger: "blur",
},
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, max: 20, message: "密码长度应在6-20个字符之间", trigger: "blur" },
],
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
trigger: "blur",
},
],
email: [
{ required: true, message: "请输入邮箱", trigger: "blur" },
{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" },
],
});
const formRef = ref();
const loading = ref(false);
const router = useRouter();
// 处理注册
const handleRegister = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
loading.value = true;
try {
const response = await userRegister(registerForm.value);
console.log("注册响应数据:", response);
if (response.code === 200) {
ElMessage.success("注册成功");
// 注册成功后跳转到登录页面
router.push("/user/login");
} else {
ElMessage.error(response.message || "注册失败");
}
} catch (_err: unknown) {
// 错误处理,显示更具体的错误信息
if (_err instanceof Error) {
ElMessage.error(_err.message || "注册请求失败,请稍后再试");
} else {
ElMessage.error("注册请求失败,请稍后再试");
}
} finally {
loading.value = false;
}
});
};
// 跳转到登录页面
const goToLogin = () => {
router.push("/user/login");
};
</script>
<template>
<AuthLayout>
<div class="auth-form">
<div class="text-center mb-8">
<h2 class="text-2xl font-bold text-gray-800 mb-2">用户注册</h2>
<p class="text-gray-600">欢迎加入我们请填写以下信息完成注册</p>
</div>
<el-form
ref="formRef"
:model="registerForm"
:rules="rules"
label-position="top"
@keyup.enter="handleRegister"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
:prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
:prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input
v-model="registerForm.phone"
placeholder="请输入手机号"
:prefix-icon="Iphone"
size="large"
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="registerForm.email"
placeholder="请输入邮箱"
:prefix-icon="Message"
size="large"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="w-full"
size="large"
:loading="loading"
@click="handleRegister"
>
{{ loading ? "注册中..." : "注册" }}
</el-button>
</el-form-item>
</el-form>
<div class="text-center mt-6">
<p class="text-gray-600">
已有账户
<el-button type="text" @click="goToLogin">立即登录</el-button>
</p>
</div>
</div>
</AuthLayout>
</template>
<style scoped>
.auth-form {
width: 100%;
max-width: 350px;
margin: 0 auto;
}
</style>

View File

@@ -90,9 +90,22 @@ function generateRoutesFromPages(): RouteRecordRaw[] {
// Generate routes from pages directory
const routes: RouteRecordRaw[] = generateRoutesFromPages();
// 添加额外的路由配置,特别是带参数的路由
const extraRoutes: RouteRecordRaw[] = [
{
path: "/user/profile/:id",
name: "UserProfile",
component: () => import("@/pages/user/ProfilePage.vue"),
},
// 可以在这里添加其他需要特殊配置的路由
];
// 合并自动生成的路由和额外的路由
const allRoutes = [...routes, ...extraRoutes];
const router = createRouter({
history: createWebHashHistory(),
routes,
routes: allRoutes,
});
export default router;

View File

@@ -1,18 +1,32 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import type { UserInfo } from "@/types/user";
export const useUserStore = defineStore(
"user",
() => {
const token = ref("");
const token = ref<string>("");
const userInfo = ref<UserInfo | null>(null);
const setToken = (newToken: string) => {
token.value = newToken;
};
const setUserInfo = (info: UserInfo) => {
userInfo.value = info;
};
const clearUserInfo = () => {
token.value = "";
userInfo.value = null;
};
return {
token,
userInfo,
setToken,
setUserInfo,
clearUserInfo,
};
},
{

27
src/types/address.ts Normal file
View File

@@ -0,0 +1,27 @@
// 用户地址实体
export interface UserAddress {
id: number;
userId: number;
receiverName: string;
receiverPhone: string;
province: string;
city: string;
district: string;
detailAddress: string;
isDefault: number; // 0-非默认1-默认
createTime?: string;
updateTime?: string;
}
// 用户地址表单
export interface UserAddressForm {
id?: number; // 地址ID新增时可不传修改时必传
userId: number; // 用户ID
receiverName: string; // 收货人姓名
receiverPhone: string; // 收货人手机号
province: string; // 省
city: string; // 市
district: string; // 区/县
detailAddress: string; // 详细地址
isDefault: number; // 是否默认0-非默认1-默认)
}

1
src/types/http.d.ts vendored
View File

@@ -2,4 +2,5 @@ declare interface Result<T> {
code: number;
message: string;
data: T;
timestamp: string;
}

36
src/types/user.ts Normal file
View File

@@ -0,0 +1,36 @@
// src/types/user.ts
// 登录请求参数类型
export interface LoginRequest {
username: string;
password: string;
}
// 注册请求参数类型
export interface RegisterRequest {
username: string;
password: string;
phone: string;
email: string;
}
// 登录响应数据类型
export interface LoginResponse {
id: number;
username: string;
nickname: string;
token: string;
}
// 用户信息类型
export interface UserInfo {
id: number;
username: string;
nickname?: string;
phone?: string;
email?: string;
avatar?: string;
gender?: number;
status?: number;
createTime?: string;
updateTime?: string;
}

View File

@@ -1,5 +1,6 @@
import axios, { type AxiosRequestConfig } from "axios";
import { useUserStore } from "@/stores/UserStore.ts";
import { useUserStore } from "@/stores/UserStore";
import { ElMessage } from "element-plus";
const instance = axios.create({
baseURL: import.meta.env.VITE_SERVER,
@@ -10,17 +11,34 @@ instance.interceptors.request.use((config) => {
if (!store.token) {
return config;
}
const token = store.token;
if (!token) {
return config;
}
config.headers["token"] = token;
// 确保 token 被正确添加到请求头
config.headers.token = store.token;
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// 处理错误响应
if (error.response?.status === 401) {
// token过期或无效
const store = useUserStore();
store.clearUserInfo();
ElMessage.error("登录已过期,请重新登录");
} else {
ElMessage.error(error.message || "请求失败");
}
return Promise.reject(error);
},
);
const http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
const { data } = await instance.request<Result<T>>(config);
return data;
const response = await instance.request<Result<T>>(config);
return response.data;
};
export default http;