Prechádzať zdrojové kódy

feat(layout): 实现后台管理布局和登录功能

- 新增后台管理布局,包括侧边栏、顶部栏、面包屑等
- 实现用户登录、登出功能
- 添加全局路由守卫,进行登录状态验证
-集成 Element Plus 组件库- 配置 axios 请求
fusu 1 mesiac pred
rodič
commit
8547a13694
7 zmenil súbory, kde vykonal 979 pridanie a 167 odobranie
  1. 54 136
      package-lock.json
  2. 2 0
      package.json
  3. 280 14
      src/App.vue
  4. 24 1
      src/main.ts
  5. 88 5
      src/router/index.ts
  6. 356 11
      src/views/index/HomeView.vue
  7. 175 0
      src/views/login/LoginView.vue

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 54 - 136
package-lock.json


+ 2 - 0
package.json

@@ -7,7 +7,9 @@
     "build": "vue-cli-service build"
   },
   "dependencies": {
+    "axios": "^1.8.4",
     "core-js": "^3.8.3",
+    "element-plus": "^2.9.7",
     "register-service-worker": "^1.7.2",
     "vue": "^3.2.13",
     "vue-router": "^4.0.3",

+ 280 - 14
src/App.vue

@@ -1,30 +1,296 @@
 <template>
-  <nav>
-    <router-link to="/">Home</router-link> |
-    <router-link to="/about">About</router-link>
-  </nav>
-  <router-view/>
+  <div id="app">
+    <!-- 后台管理布局 -->
+    <el-container v-if="isLoggedIn" class="admin-layout">
+      <!-- 侧边栏 -->
+      <el-aside width="220px" class="aside">
+        <div class="logo-container">
+          <img src="./assets/logo.png" alt="Logo" class="logo" />
+          <h2>管理系统</h2>
+        </div>
+        
+        <el-scrollbar>
+          <el-menu
+            :default-active="activeMenu"
+            class="el-menu-vertical"
+            background-color="#304156"
+            text-color="#bfcbd9"
+            active-text-color="#409EFF"
+            :collapse="isCollapse"
+            router
+          >
+            <el-sub-menu index="dashboard">
+              <template #title>
+                <el-icon><HomeFilled /></el-icon>
+                <span>控制台</span>
+              </template>
+              <el-menu-item index="/dashboard">首页</el-menu-item>
+            </el-sub-menu>
+            
+            <el-sub-menu index="user">
+              <template #title>
+                <el-icon><User /></el-icon>
+                <span>用户管理</span>
+              </template>
+              <el-menu-item index="/user-management">用户列表</el-menu-item>
+              <!-- 暂时注释掉有问题的菜单项 
+              <el-menu-item index="/user-address-management">用户地址</el-menu-item>
+              -->
+            </el-sub-menu>
+            
+            <el-sub-menu index="product">
+              <template #title>
+                <el-icon><Goods /></el-icon>
+                <span>商品管理</span>
+              </template>
+              <!-- 暂时注释掉有问题的菜单项 
+              <el-menu-item index="/category-management">分类管理</el-menu-item>
+              -->
+              <el-menu-item index="/goods-management">商品列表</el-menu-item>
+              <el-menu-item index="/picture-management">图片管理</el-menu-item>
+            </el-sub-menu>
+            
+            <el-sub-menu index="order">
+              <template #title>
+                <el-icon><List /></el-icon>
+                <span>订单管理</span>
+              </template>
+              <el-menu-item index="/order-management">订单列表</el-menu-item>
+              <el-menu-item index="/appraises-management">评价管理</el-menu-item>
+            </el-sub-menu>
+            
+            <el-sub-menu index="more">
+              <template #title>
+                <el-icon><More /></el-icon>
+                <span>更多功能</span>
+              </template>
+              <el-menu-item index="/chat-message-management">消息管理</el-menu-item>
+              <el-menu-item index="/favorite-good-management">收藏管理</el-menu-item>
+            </el-sub-menu>
+            
+            <el-sub-menu index="system">
+              <template #title>
+                <el-icon><Setting /></el-icon>
+                <span>系统管理</span>
+              </template>
+              <el-menu-item index="/about">关于系统</el-menu-item>
+            </el-sub-menu>
+          </el-menu>
+        </el-scrollbar>
+      </el-aside>
+      
+      <!-- 主要内容 -->
+      <el-container>
+        <!-- 头部 -->
+        <el-header class="header">
+          <div class="header-left">
+            <el-icon class="collapse-btn" @click="toggleSidebar">
+              <component :is="isCollapse ? 'Expand' : 'Fold'" />
+            </el-icon>
+            <div class="breadcrumb">
+              <el-breadcrumb separator="/">
+                <el-breadcrumb-item :to="{ path: '/dashboard' }">首页</el-breadcrumb-item>
+                <el-breadcrumb-item v-if="routeName">{{ routeName }}</el-breadcrumb-item>
+              </el-breadcrumb>
+            </div>
+          </div>
+          
+          <div class="header-right">
+            <el-dropdown trigger="click">
+              <div class="avatar-wrapper">
+                <el-avatar size="small" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
+                <span class="user-name">管理员</span>
+                <el-icon><CaretBottom /></el-icon>
+              </div>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item>个人中心</el-dropdown-item>
+                  <el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </el-header>
+        
+        <!-- 主要内容区域 -->
+        <el-main>
+          <router-view />
+        </el-main>
+      </el-container>
+    </el-container>
+    
+    <!-- 未登录时的布局 -->
+    <div v-else>
+      <router-view />
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import {
+  HomeFilled, User, Goods, List, Setting, Fold, Expand, CaretBottom, More
+} from '@element-plus/icons-vue';
+
+const router = useRouter();
+const route = useRoute();
+const isCollapse = ref(false);
+
+// 是否登录的状态变量
+const loggedIn = ref(false);
+
+// 判断是否已登录
+const isLoggedIn = computed(() => {
+  return loggedIn.value;
+});
+
+// 当前活动菜单
+const activeMenu = computed(() => {
+  return route.path;
+});
+
+// 获取当前路由名称用于面包屑显示
+const routeName = computed(() => {
+  return route.name;
+});
+
+// 检查登录状态并更新loggedIn变量
+function checkLoginStatus() {
+  const token = localStorage.getItem('admin_token');
+  console.log('App.vue检查登录状态,token:', token ? '存在' : '不存在');
+  
+  if (token) {
+    loggedIn.value = true;
+    console.log('登录状态已更新为:', loggedIn.value);
+  } else {
+    loggedIn.value = false;
+    console.log('登录状态已更新为:', loggedIn.value);
+  }
+}
+
+// 切换侧边栏收起/展开
+const toggleSidebar = () => {
+  isCollapse.value = !isCollapse.value;
+};
+
+// 退出登录
+const logout = () => {
+  localStorage.removeItem('admin_token');
+  loggedIn.value = false;
+  router.push('/');
+};
+
+// 每次挂载组件时检查登录状态
+onMounted(() => {
+  console.log('App组件挂载,检查登录状态');
+  checkLoginStatus();
+  
+  // 添加自定义事件监听器用于跨页面同步登录状态
+  window.addEventListener('login-state-change', () => {
+    console.log('检测到登录状态变化事件');
+    checkLoginStatus();
+  });
+  
+  // 监听storage变化,当token变化时更新状态
+  window.addEventListener('storage', event => {
+    console.log('检测到存储变化:', event.key);
+    if (event.key === 'admin_token') {
+      checkLoginStatus();
+    }
+  });
+  
+  // 每秒检查一次登录状态,确保状态同步
+  const interval = setInterval(checkLoginStatus, 1000);
+  return () => {
+    clearInterval(interval);
+    window.removeEventListener('login-state-change', checkLoginStatus);
+    window.removeEventListener('storage', checkLoginStatus);
+  };
+});
+</script>
+
 <style>
+/* 全局样式 */
+html, body, #app {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+}
+
 #app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
-  text-align: center;
   color: #2c3e50;
 }
 
-nav {
-  padding: 30px;
+/* 管理后台样式 */
+.admin-layout {
+  height: 100%;
 }
 
-nav a {
-  font-weight: bold;
-  color: #2c3e50;
+.aside {
+  background-color: #304156;
+  transition: width 0.3s;
+  overflow: hidden;
+}
+
+.logo-container {
+  height: 60px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+  background-color: #263445;
+  color: #fff;
+}
+
+.logo {
+  width: 32px;
+  height: 32px;
+  margin-right: 12px;
+}
+
+.el-menu {
+  border-right: none;
+}
+
+.header {
+  background-color: #fff;
+  border-bottom: 1px solid #dcdfe6;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+}
+
+.header-left, .header-right {
+  display: flex;
+  align-items: center;
+}
+
+.collapse-btn {
+  padding: 0 15px;
+  cursor: pointer;
+  font-size: 20px;
+}
+
+.avatar-wrapper {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+}
+
+.user-name {
+  margin: 0 8px;
+}
+
+.breadcrumb {
+  margin-left: 20px;
 }
 
-nav a.router-link-exact-active {
-  color: #42b983;
+.el-main {
+  background-color: #f0f2f5;
+  padding: 20px;
 }
 </style>

+ 24 - 1
src/main.ts

@@ -4,4 +4,27 @@ import './registerServiceWorker'
 import router from './router'
 import store from './store'
 
-createApp(App).use(store).use(router).mount('#app')
+// Start Change: Import Element Plus and its CSS
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+// End Change: Import Element Plus and its CSS
+
+// 导入axios实例
+import request from './utils/request'
+import axios from 'axios'
+
+// 设置axios的默认配置
+axios.defaults.withCredentials = true; // 确保跨域请求带上cookie
+axios.defaults.timeout = 15000; // 设置默认超时时间
+
+// 将axios实例挂载到全局
+const app = createApp(App)
+app.config.globalProperties.$axios = request
+// 同时保留原始axios便于特殊请求(如登录)使用
+app.config.globalProperties.$http = axios
+
+app
+  .use(store)
+  .use(router)
+  .use(ElementPlus) // Use Element Plus
+  .mount('#app')

+ 88 - 5
src/router/index.ts

@@ -4,6 +4,11 @@ import HomeView from '../views/index/HomeView.vue'
 const routes: Array<RouteRecordRaw> = [
     {
         path: '/',
+        name: 'login',
+        component: () => import('../views/login/LoginView.vue')
+    },
+    {
+        path: '/dashboard',
         name: 'home',
         component: HomeView
     },
@@ -12,16 +17,62 @@ const routes: Array<RouteRecordRaw> = [
         name: 'about',
         component: () => import('../views/AboutView.vue')
     },
-    {
-        path: '/login',
-        name: 'login',
-        component: () => import('../views/login/LoginView.vue')
-    },
     {
         path: '/register',
         name: 'register',
         component: () => import('../views/login/RegisterView.vue')
     },
+    // 管理后台路由
+    {
+        path: '/user-management',
+        name: 'userManagement',
+        component: () => import('../views/UserManagement.vue')
+    },
+    {
+        path: '/order-management',
+        name: 'orderManagement',
+        component: () => import('../views/OrderManagement.vue')
+    },
+    {
+        path: '/goods-management',
+        name: 'goodsManagement',
+        component: () => import('../views/GoodsManagement.vue')
+    },
+    /* 暂时注释掉有问题的组件
+    {
+        path: '/category-management',
+        name: 'categoryManagement',
+        component: () => import('../views/CategoryManagement.vue')
+    },
+    */
+    // 添加其他已有页面的路由
+    {
+        path: '/appraises-management',
+        name: 'appraisesManagement',
+        component: () => import('../views/AppraisesManagement.vue')
+    },
+    /* 暂时注释掉有问题的组件
+    {
+        path: '/user-address-management',
+        name: 'userAddressManagement',
+        component: () => import('../views/UserAddressManagement.vue')
+    },
+    */
+    {
+        path: '/chat-message-management',
+        name: 'chatMessageManagement',
+        component: () => import('../views/ChatMessageManagement.vue')
+    },
+    {
+        path: '/favorite-good-management',
+        name: 'favoriteGoodManagement',
+        component: () => import('../views/FavoriteGoodManagement.vue')
+    },
+    {
+        path: '/picture-management',
+        name: 'pictureManagement',
+        component: () => import('../views/PictureManagement.vue')
+    }
 ]
 
 const router = createRouter({
@@ -29,4 +80,36 @@ const router = createRouter({
     routes
 })
 
+// 全局路由守卫,添加鉴权功能
+router.beforeEach((to, from, next) => {
+    // 免登录白名单
+    const whiteList = ['/', '/login', '/register'];
+    
+    // 获取token
+    const token = localStorage.getItem('admin_token');
+    console.log('路由守卫检查token:', !!token, '当前路由:', to.path);
+    
+    if (token) {
+        // 已登录状态访问登录页,直接跳转到首页
+        if (to.path === '/' || to.path === '/login') {
+            console.log('已登录状态访问登录页,重定向到dashboard');
+            next('/dashboard');
+        } else {
+            console.log('已登录状态访问受保护页面,正常通过');
+            next();
+        }
+    } else {
+        // 未登录
+        if (whiteList.includes(to.path)) {
+            // 访问白名单,直接通过
+            console.log('未登录状态访问白名单页面,正常通过');
+            next();
+        } else {
+            // 未登录且访问非白名单页面,重定向到登录页
+            console.log('未登录状态访问受保护页面,重定向到登录页');
+            next('/');
+        }
+    }
+});
+
 export default router

+ 356 - 11
src/views/index/HomeView.vue

@@ -1,18 +1,363 @@
 <template>
-  <div class="home">
-    <img alt="Vue logo" src="../../assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
+  <div class="admin-dashboard">
+    <el-row :gutter="20">
+      <el-col :span="24">
+        <el-card shadow="hover" class="welcome-card">
+          <template #header>
+            <div class="card-header">
+              <span>欢迎使用管理后台</span>
+              <el-tag type="success">在线</el-tag>
+            </div>
+          </template>
+          <div class="welcome-content">
+            <el-avatar :size="64" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"></el-avatar>
+            <div class="welcome-text">
+              <h3>管理员,您好!</h3>
+              <p>今天是 {{ currentDate }},祝您工作愉快!</p>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="stat-row">
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-inner">
+            <div class="stat-icon user-icon">
+              <el-icon><User /></el-icon>
+            </div>
+            <div class="stat-info">
+              <div class="stat-title">用户总数</div>
+              <div class="stat-value">{{ stats.userCount }}</div>
+              <div class="stat-desc">较昨日 <span class="up">+{{ stats.userIncrease }}</span></div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-inner">
+            <div class="stat-icon order-icon">
+              <el-icon><List /></el-icon>
+            </div>
+            <div class="stat-info">
+              <div class="stat-title">订单总数</div>
+              <div class="stat-value">{{ stats.orderCount }}</div>
+              <div class="stat-desc">较昨日 <span class="up">+{{ stats.orderIncrease }}</span></div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-inner">
+            <div class="stat-icon product-icon">
+              <el-icon><Goods /></el-icon>
+            </div>
+            <div class="stat-info">
+              <div class="stat-title">商品总数</div>
+              <div class="stat-value">{{ stats.productCount }}</div>
+              <div class="stat-desc">较昨日 <span class="same">+{{ stats.productIncrease }}</span></div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" class="stat-card">
+          <div class="stat-card-inner">
+            <div class="stat-icon income-icon">
+              <el-icon><Money /></el-icon>
+            </div>
+            <div class="stat-info">
+              <div class="stat-title">收入统计</div>
+              <div class="stat-value">¥{{ stats.income }}</div>
+              <div class="stat-desc">较昨日 <span class="up">+{{ stats.incomeIncrease }}%</span></div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20">
+      <el-col :span="24">
+        <el-card shadow="hover" class="quick-access-card">
+          <template #header>
+            <div class="card-header">
+              <span>快速访问</span>
+            </div>
+          </template>
+          <div class="quick-access-grid">
+            <router-link to="/user-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><User /></el-icon>
+                <span>用户管理</span>
+              </el-card>
+            </router-link>
+            <!-- 暂时注释掉有问题的链接
+            <router-link to="/user-address-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><Location /></el-icon>
+                <span>地址管理</span>
+              </el-card>
+            </router-link>
+            -->
+            <router-link to="/order-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><List /></el-icon>
+                <span>订单管理</span>
+              </el-card>
+            </router-link>
+            <router-link to="/goods-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><Goods /></el-icon>
+                <span>商品管理</span>
+              </el-card>
+            </router-link>
+            <!-- 暂时注释掉有问题的链接
+            <router-link to="/category-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><Menu /></el-icon>
+                <span>分类管理</span>
+              </el-card>
+            </router-link>
+            -->
+            <router-link to="/appraises-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><ChatDotRound /></el-icon>
+                <span>评价管理</span>
+              </el-card>
+            </router-link>
+            <router-link to="/picture-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><Picture /></el-icon>
+                <span>图片管理</span>
+              </el-card>
+            </router-link>
+            <router-link to="/chat-message-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><ChatSquare /></el-icon>
+                <span>消息管理</span>
+              </el-card>
+            </router-link>
+            <router-link to="/favorite-good-management" class="quick-link">
+              <el-card shadow="hover">
+                <el-icon><Star /></el-icon>
+                <span>收藏管理</span>
+              </el-card>
+            </router-link>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { User, List, Menu, Money, Goods, Location, Picture, ChatDotRound, ChatSquare, Star } from '@element-plus/icons-vue';
+import { getCurrentInstance } from 'vue';
+
+// 获取当前日期
+const currentDate = new Date().toLocaleDateString('zh-CN', {
+  year: 'numeric',
+  month: 'long',
+  day: 'numeric',
+  weekday: 'long'
+});
+
+// 定义统计数据
+const stats = ref({
+  userCount: '8,546',
+  userIncrease: '25',
+  orderCount: '1,280',
+  orderIncrease: '17',
+  productCount: '398',
+  productIncrease: '5',
+  income: '23,589.00',
+  incomeIncrease: '12.5'
+});
+
+// 获取当前组件实例以访问全局属性
+const { proxy } = getCurrentInstance() || {};
+const $axios = proxy?.$axios;
+
+// 获取统计数据
+const fetchStats = async () => {
+  try {
+    // 如果后端有对应API可以打开下面的注释
+    /*
+    const response = await $axios.get('/api/admin/stats/dashboard');
+    if (response.data.code === 200) {
+      stats.value = response.data.data;
+    }
+    */
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+  }
+};
 
-export default defineComponent({
-  name: 'HomeView',
-  components: {
-    HelloWorld,
-  },
+onMounted(() => {
+  fetchStats();
 });
 </script>
+
+<style scoped>
+.admin-dashboard {
+  padding: 20px;
+}
+
+.welcome-card {
+  margin-bottom: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.welcome-content {
+  display: flex;
+  align-items: center;
+  padding: 10px 0;
+}
+
+.welcome-text {
+  margin-left: 20px;
+}
+
+.welcome-text h3 {
+  margin: 0;
+  font-size: 18px;
+  color: #333;
+}
+
+.welcome-text p {
+  margin: 5px 0 0;
+  color: #666;
+}
+
+.stat-row {
+  margin-bottom: 20px;
+}
+
+.stat-card {
+  height: 100%;
+}
+
+.stat-card-inner {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+}
+
+.stat-icon {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 60px;
+  height: 60px;
+  border-radius: 10px;
+  margin-right: 15px;
+  font-size: 24px;
+}
+
+.user-icon {
+  background-color: rgba(64, 158, 255, 0.1);
+  color: #409EFF;
+}
+
+.order-icon {
+  background-color: rgba(103, 194, 58, 0.1);
+  color: #67C23A;
+}
+
+.product-icon {
+  background-color: rgba(230, 162, 60, 0.1);
+  color: #E6A23C;
+}
+
+.income-icon {
+  background-color: rgba(245, 108, 108, 0.1);
+  color: #F56C6C;
+}
+
+.stat-info {
+  flex: 1;
+}
+
+.stat-title {
+  font-size: 14px;
+  color: #999;
+  margin-bottom: 5px;
+}
+
+.stat-value {
+  font-size: 22px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 5px;
+}
+
+.stat-desc {
+  font-size: 12px;
+  color: #999;
+}
+
+.up {
+  color: #67C23A;
+}
+
+.down {
+  color: #F56C6C;
+}
+
+.same {
+  color: #E6A23C;
+}
+
+.quick-access-card {
+  margin-bottom: 20px;
+}
+
+.quick-access-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 15px;
+}
+
+.quick-link {
+  text-decoration: none;
+}
+
+.quick-link .el-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100px;
+  text-align: center;
+  transition: all 0.3s;
+  cursor: pointer;
+}
+
+.quick-link .el-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
+}
+
+.quick-link .el-icon {
+  font-size: 24px;
+  margin-bottom: 8px;
+  color: #409EFF;
+}
+
+.quick-link span {
+  color: #333;
+  font-size: 14px;
+}
+</style>

+ 175 - 0
src/views/login/LoginView.vue

@@ -1,11 +1,186 @@
 <script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElForm, ElInput, ElButton, ElCard, ElFormItem } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+import axios from 'axios'; // Make sure axios is configured
+
+const router = useRouter();
+const loginFormRef = ref<FormInstance>();
+const loading = ref(false);
+
+// --- Login Form Data ---
+const loginForm = reactive({
+  username: '',
+  password: '',
+});
+
+// --- Login Form Validation Rules ---
+const loginRules = reactive<FormRules>({
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
+  ],
+});
+
+// --- API Endpoint ---
+const loginApiUrl = '/api/login'; // 使用相对路径,因为baseURL已在axios实例中配置
+
+// --- Login Handler ---
+const handleLogin = async () => {
+  if (!loginFormRef.value) return;
+  await loginFormRef.value.validate(async (valid) => {
+    if (valid) {
+      loading.value = true;
+      try {
+        console.log('Sending login request:', loginForm);
+        
+        // Create form data with phone instead of username
+        const formData = new URLSearchParams();
+        formData.append('phone', loginForm.username); // Change field name from username to phone
+        formData.append('password', loginForm.password);
+        
+        const response = await axios.post('http://127.0.0.1:1323/api/login', formData, {
+          headers: {
+            'Content-Type': 'application/x-www-form-urlencoded' // Send as form data instead of JSON
+          }
+        });
+
+        console.log('Login response:', response.data);
+
+        // --- Adjust based on your actual API response structure ---
+        if (response.data && response.data.code === 1 && response.data.data?.token) {
+          // 记录完整响应以便调试
+          console.log('完整登录响应:', JSON.stringify(response.data));
+          
+          // 清除可能存在的旧token
+          localStorage.removeItem('admin_token');
+          
+          // 保存新token - 确保使用服务器返回的完整token字符串
+          const token = response.data.data.token;
+          localStorage.setItem('admin_token', token);
+          
+          // 检查token是否正确保存
+          setTimeout(() => {
+            const savedToken = localStorage.getItem('admin_token');
+            console.log('保存到本地存储的token:', savedToken);
+          }, 100);
+          
+          // 保存用户信息
+          if (response.data.data.user_info) {
+            localStorage.setItem('admin_user', JSON.stringify(response.data.data.user_info));
+          }
+
+          ElMessage.success('登录成功');
+          
+          // 触发自定义事件通知登录状态变化
+          window.dispatchEvent(new Event('login-state-change'));
+
+          // 重要:确保token保存后再跳转
+          setTimeout(() => {
+            router.push('/dashboard');
+          }, 300);
+          
+        } else {
+          // Handle login failure (e.g., wrong credentials)
+          ElMessage.error(response.data.msg || '登录失败,请检查用户名和密码');
+        }
+        // -------
+
+      } catch (error: any) {
+        console.error('Login failed:', error);
+        // Handle network errors or other exceptions
+        let errorMessage = '登录请求失败';
+        if (error.response && error.response.data && error.response.data.message) {
+          errorMessage = error.response.data.message;
+        } else if (error.message) {
+          errorMessage = error.message;
+        }
+        ElMessage.error(errorMessage);
+      } finally {
+        loading.value = false;
+      }
+    } else {
+      console.log('Form validation failed!');
+      return false;
+    }
+  });
+};
+
+// --- Reset Form Handler ---
+const resetForm = () => {
+  loginFormRef.value?.resetFields();
+};
 
 </script>
 
 <template>
+  <div class="login-container">
+    <el-card class="login-card" shadow="always">
+      <template #header>
+        <div class="card-header">
+          <span>管理员登录</span>
+        </div>
+      </template>
 
+      <el-form
+        ref="loginFormRef"
+        :model="loginForm"
+        :rules="loginRules"
+        label-width="80px"
+        @keyup.enter="handleLogin"
+      >
+        <el-form-item label="用户名" prop="username">
+          <el-input v-model="loginForm.username" placeholder="请输入用户名" clearable />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input
+            v-model="loginForm.password"
+            type="password"
+            placeholder="请输入密码"
+            show-password
+            clearable
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleLogin" :loading="loading">
+            登录
+          </el-button>
+          <el-button @click="resetForm">重置</el-button>
+          <!-- Optional: Add link to registration or password recovery -->
+          <router-link to="/register" class="register-link">没有账号?去注册</router-link>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
 </template>
 
 <style scoped>
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh; /* Full viewport height */
+  background-color: #f5f7fa; /* Optional: Add a background color */
+}
+
+.login-card {
+  width: 450px;
+}
+
+.card-header {
+  text-align: center;
+  font-size: 20px;
+  font-weight: bold;
+}
 
+.register-link {
+  margin-left: 15px;
+  color: #409eff;
+  text-decoration: none;
+}
+.register-link:hover {
+  text-decoration: underline;
+}
 </style>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov