基本完成了用户相关前端页面的编写,经过测试能够实现相关功能
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
|
||||
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 {
|
||||
|
||||
@@ -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
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 {
|
||||
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
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 { 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");
|
||||
|
||||
@@ -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>
|
||||
// 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>
|
||||
|
||||
@@ -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
|
||||
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;
|
||||
|
||||
@@ -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
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;
|
||||
message: string;
|
||||
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 { 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;
|
||||
|
||||
Reference in New Issue
Block a user