基本完成了用户相关前端页面的编写,经过测试能够实现相关功能
This commit is contained in:
126
auto-imports.d.ts
vendored
126
auto-imports.d.ts
vendored
@@ -6,69 +6,69 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: typeof import('vue').computed
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: typeof import('vue').createApp
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: typeof import('vue').customRef
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: typeof import('vue').defineComponent
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue').effectScope
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue').h
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue').inject
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: typeof import('vue').isProxy
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue').isReactive
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue').isReadonly
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue').isRef
|
||||||
const isShallow: typeof import('vue')['isShallow']
|
const isShallow: typeof import('vue').isShallow
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue').markRaw
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue').nextTick
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: typeof import('vue').onActivated
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: typeof import('vue').onDeactivated
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
const onMounted: typeof import('vue').onMounted
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: typeof import('vue').onUnmounted
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: typeof import('vue').onUpdated
|
||||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: typeof import('vue').provide
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: typeof import('vue').reactive
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: typeof import('vue').readonly
|
||||||
const ref: typeof import('vue')['ref']
|
const ref: typeof import('vue').ref
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: typeof import('vue').resolveComponent
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: typeof import('vue').shallowReactive
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue').shallowRef
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: typeof import('vue').toRaw
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue').toRef
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue').toRefs
|
||||||
const toValue: typeof import('vue')['toValue']
|
const toValue: typeof import('vue').toValue
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: typeof import('vue').triggerRef
|
||||||
const unref: typeof import('vue')['unref']
|
const unref: typeof import('vue').unref
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: typeof import('vue').useAttrs
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: typeof import('vue').useCssModule
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: typeof import('vue').useCssVars
|
||||||
const useId: typeof import('vue')['useId']
|
const useId: typeof import('vue').useId
|
||||||
const useLink: typeof import('vue-router')['useLink']
|
const useLink: typeof import('vue-router').useLink
|
||||||
const useModel: typeof import('vue')['useModel']
|
const useModel: typeof import('vue').useModel
|
||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: typeof import('vue-router').useRoute
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: typeof import('vue-router').useRouter
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue').useSlots
|
||||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: typeof import('vue').watch
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: typeof import('vue').watchEffect
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"daisyui": "^5.0.50",
|
"daisyui": "^5.0.50",
|
||||||
|
"element-plus": "^2.12.0",
|
||||||
"motion-v": "^1.6.1",
|
"motion-v": "^1.6.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
|
|||||||
14
src/apis/product.ts
Normal file
14
src/apis/product.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,19 +1,97 @@
|
|||||||
import http from "../utils/http";
|
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
|
* This is an example, please remove if not needed
|
||||||
*/
|
*/
|
||||||
export const getUserList = () => {
|
// export const getUserList = () => {
|
||||||
return http({
|
// return http({
|
||||||
url: "/user/list",
|
// url: "/user/list",
|
||||||
method: "get",
|
// 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({
|
export const userLogin = (data: LoginRequest) => {
|
||||||
url: "/user/insert",
|
return http<LoginResponse>({
|
||||||
|
url: "/user/login",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
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
112
src/layouts/AuthLayout.vue
Normal 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
276
src/layouts/MainLayout.vue
Normal 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>
|
||||||
@@ -4,8 +4,10 @@ import App from "./App.vue";
|
|||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
import ElementPlus from "element-plus";
|
||||||
|
import "element-plus/dist/index.css";
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
pinia.use(piniaPluginPersistedstate);
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
createApp(App).use(router).use(pinia).mount("#app");
|
createApp(App).use(router).use(pinia).use(ElementPlus).mount("#app");
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,18 +1,207 @@
|
|||||||
<script lang="ts" setup>
|
<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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<MainLayout>
|
||||||
<div class="hero-content text-center">
|
<!-- 英雄区 -->
|
||||||
<div class="max-w-md">
|
<section
|
||||||
<h1 class="text-5xl font-bold">首页</h1>
|
class="relative bg-gradient-to-r from-blue-600 to-indigo-700 text-white"
|
||||||
<p class="py-6">欢迎使用自动路由系统</p>
|
>
|
||||||
|
<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>
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Component styles */
|
/* 自定义样式补充 */
|
||||||
|
section {
|
||||||
|
scroll-margin-top: 70px; /* 适配固定导航栏的锚点定位 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-content h1 {
|
||||||
|
font-size: 3rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
352
src/pages/user/AddressPage.vue
Normal file
352
src/pages/user/AddressPage.vue
Normal 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>
|
||||||
161
src/pages/user/LoginPage.vue
Normal file
161
src/pages/user/LoginPage.vue
Normal 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>
|
||||||
242
src/pages/user/ProfilePage.vue
Normal file
242
src/pages/user/ProfilePage.vue
Normal 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>
|
||||||
172
src/pages/user/RegisterPage.vue
Normal file
172
src/pages/user/RegisterPage.vue
Normal 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>
|
||||||
@@ -90,9 +90,22 @@ function generateRoutesFromPages(): RouteRecordRaw[] {
|
|||||||
// Generate routes from pages directory
|
// Generate routes from pages directory
|
||||||
const routes: RouteRecordRaw[] = generateRoutesFromPages();
|
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({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
routes: allRoutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import type { UserInfo } from "@/types/user";
|
||||||
|
|
||||||
export const useUserStore = defineStore(
|
export const useUserStore = defineStore(
|
||||||
"user",
|
"user",
|
||||||
() => {
|
() => {
|
||||||
const token = ref("");
|
const token = ref<string>("");
|
||||||
|
const userInfo = ref<UserInfo | null>(null);
|
||||||
|
|
||||||
const setToken = (newToken: string) => {
|
const setToken = (newToken: string) => {
|
||||||
token.value = newToken;
|
token.value = newToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setUserInfo = (info: UserInfo) => {
|
||||||
|
userInfo.value = info;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearUserInfo = () => {
|
||||||
|
token.value = "";
|
||||||
|
userInfo.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
|
userInfo,
|
||||||
setToken,
|
setToken,
|
||||||
|
setUserInfo,
|
||||||
|
clearUserInfo,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
27
src/types/address.ts
Normal file
27
src/types/address.ts
Normal 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
1
src/types/http.d.ts
vendored
@@ -2,4 +2,5 @@ declare interface Result<T> {
|
|||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
data: T;
|
data: T;
|
||||||
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/types/user.ts
Normal file
36
src/types/user.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios, { type AxiosRequestConfig } from "axios";
|
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({
|
const instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_SERVER,
|
baseURL: import.meta.env.VITE_SERVER,
|
||||||
@@ -10,17 +11,34 @@ instance.interceptors.request.use((config) => {
|
|||||||
if (!store.token) {
|
if (!store.token) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
const token = store.token;
|
|
||||||
if (!token) {
|
// 确保 token 被正确添加到请求头
|
||||||
return config;
|
config.headers.token = store.token;
|
||||||
}
|
|
||||||
config.headers["token"] = token;
|
|
||||||
return config;
|
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 http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
|
||||||
const { data } = await instance.request<Result<T>>(config);
|
const response = await instance.request<Result<T>>(config);
|
||||||
return data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|||||||
Reference in New Issue
Block a user