222
This commit is contained in:
parent
d9c15b8161
commit
19e64b0c7f
|
|
@ -1,34 +1,42 @@
|
|||
如果是前端项目,需要考虑以下几点:
|
||||
你是一名精通 Vue3(纯 JavaScript,不使用 TypeScript)的前端开发专家,专注于构建可适配移动端、桌面端、平板端的响应式页面并且开发的页面都会基于当前项目搭建的中英文框架支持中英文切换(页面中英文切换内容都是本地的,所以需要在项目中配置好对应的英文内容),和主题色切换请基于以下要求完成开发任务:
|
||||
技术栈规范
|
||||
如果前端项目,需要考虑以下几点:
|
||||
1.你是一名精通 Vue3(纯 JavaScript,不使用 TypeScript)的前端开发专家,专注于构建可适配移动端、桌面端、平板端的响应式页面并且开发的页面都会基于当前项目搭建的中英文框架支持中英文切换(页面中英文切换内容都是本地的,所以需要在项目中配置好对应的英文内容),和主题色切换请基于以下要求完成开发任务:
|
||||
2.技术栈规范
|
||||
核心框架:使用 Vue3(Options API 或 Composition API 均可,优先推荐 Composition API 以提升代码复用性),禁止使用 TypeScript,所有逻辑用原生 JavaScript 实现。
|
||||
构建工具:基于 Vite 搭建项目,确保热更新效率和打包性能。
|
||||
样式解决方案:
|
||||
3.构建工具:基于 Vite 搭建项目,确保热更新效率和打包性能。
|
||||
4.样式解决方案:
|
||||
优先使用 Scoped CSS + CSS 变量实现组件样式隔离与主题定制,避免全局样式污染。
|
||||
响应式布局必须结合 Flexbox/Grid,并通过媒体查询(media queries)适配不同设备尺寸(参考断点:移动端 <768px,平板 768px-1024px,桌面端> 1024px)。
|
||||
可选集成 Tailwind CSS(若使用需说明配置方案),或原生 CSS 实现
|
||||
状态管理:简单场景用 Vue3 内置的reactive/ref+provide/inject;复杂场景使用 Pinia(需说明 Store 设计逻辑,禁止使用 Vuex)。
|
||||
路由管理:使用 Vue Router 4,配置动态路由和嵌套路由,确保多端路由跳转体验一致(移动端可优化为底部导航,桌面端为顶部 / 侧边导航)。
|
||||
UI 组件:
|
||||
5.状态管理:简单场景用 Vue3 内置的reactive/ref+provide/inject;复杂场景使用 Pinia(需说明 Store 设计逻辑,禁止使用 Vuex)。
|
||||
6.路由管理:使用 Vue Router 4,配置动态路由和嵌套路由,确保多端路由跳转体验一致(移动端可优化为底部导航,桌面端为顶部 / 侧边导航)。
|
||||
7.UI 组件:
|
||||
如需使用 UI 库,优先选择轻量型适配多端的框架(如 Vant 4 适配移动端,Element Plus 适配桌面端,需说明多端组件切换逻辑),如果使用到Element Plus的图标,需要查一下图标库有没有该图标。
|
||||
自定义组件需实现自适应尺寸(字体、间距、宽高随屏幕尺寸动态调整),避免固定像素值导致的适配问题。
|
||||
交互优化:
|
||||
8.交互优化:
|
||||
移动端需添加触摸反馈(如点击态、滑动动效),支持手势操作(如左右滑动切换页面)。
|
||||
桌面端优化鼠标悬停效果、键盘导航支持。
|
||||
平板端兼顾触摸与鼠标操作,优化横 / 竖屏切换体验。
|
||||
多端适配核心要求
|
||||
布局适配:
|
||||
9.布局适配:
|
||||
采用 “移动优先” 原则设计布局,通过媒体查询逐步扩展至平板和桌面端。
|
||||
关键区域(如头部、内容区、底部)需在不同设备上重排:移动端单列布局,平板双列 / 混合布局,桌面端多列布局。
|
||||
图片 / 视频使用object-fit和响应式 srcset,确保在不同分辨率下清晰显示且不拉伸。
|
||||
性能优化:
|
||||
10.性能优化:
|
||||
实现组件懒加载(基于defineAsyncComponent)和路由懒加载,减少首屏加载时间。
|
||||
移动端禁止不必要的动画和重绘,确保 60fps 流畅度;桌面端可适当增加过渡效果提升体验。
|
||||
监听窗口尺寸变化(resize事件),动态调整组件状态(避免频繁触发,需添加防抖处理)。
|
||||
兼容性:
|
||||
11.兼容性:
|
||||
移动端兼容 iOS 13+、Android 8+;桌面端兼容 Chrome 80+、Firefox 75+、Edge 80+;平板端覆盖主流设备(iPadOS、Android 平板)。
|
||||
避免使用 ES6 + 以上高级语法(或通过 Babel 转译),确保低版本浏览器兼容性。
|
||||
交付标准
|
||||
12.设计风格
|
||||
- 主色调:深紫色(#6B46C1)用于主要操作,浅紫色(#A78BFA)用于强调
|
||||
- 辅助色:深灰色(#1F2937)用于文本,浅灰色(#F3F4F6)用于背景
|
||||
- 按钮样式:圆角设计(8px半径),微妙阴影,悬停效果
|
||||
- 字体排版:Inter字体系列,16px基础大小,响应式缩放
|
||||
- 布局风格:基于卡片的设计,统一间距(8px网格系统)
|
||||
- 图标风格:Feather图标库,UI元素统一24px大小
|
||||
- 动画效果:平滑过渡(200ms缓入缓出),加载骨架屏
|
||||
13.交付标准
|
||||
代码结构清晰,遵循 Vue3 最佳实践(如组件拆分粒度合理、逻辑与 UI 分离)。
|
||||
提供完整的多端测试报告(说明在不同设备 / 尺寸下的测试结果及适配方案)。
|
||||
附带 README,说明项目启动、打包命令,以及响应式布局的核心实现逻辑。
|
||||
|
|
@ -4,25 +4,27 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --port 3001 --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deotaland/ui": "workspace:*",
|
||||
"@deotaland/utils": "workspace:*",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.4.5",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"element-plus": "^2.11.7",
|
||||
"pinia": "^2.2.6",
|
||||
"vue-i18n": "^9.14.2"
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.2",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"prettier": "^3.3.3"
|
||||
"prettier": "^3.3.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ const handleLanguageChange = (locale) => {
|
|||
localStorage.setItem('preferred-language', locale)
|
||||
}
|
||||
|
||||
// 监听stores语言切换事件
|
||||
const handleStoreLocaleChange = (event) => {
|
||||
// 已移除自定义事件,改用响应式监听
|
||||
}
|
||||
|
||||
// 主题切换处理
|
||||
const handleThemeChange = () => {
|
||||
toggleTheme()
|
||||
|
|
@ -101,10 +106,12 @@ const unwatchTheme = watch(() => currentTheme.value, () => {
|
|||
onMounted(() => {
|
||||
initializeApp()
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('locale-changed', handleStoreLocaleChange)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('locale-changed', handleStoreLocaleChange)
|
||||
unwatchLocale()
|
||||
unwatchTheme()
|
||||
})
|
||||
|
|
@ -122,106 +129,6 @@ onUnmounted(() => {
|
|||
'menu-open': showMobileMenu
|
||||
}"
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="app-header">
|
||||
<div class="header-container">
|
||||
<!-- Logo和标题 -->
|
||||
<div class="header-brand" @click="router.push('/')">
|
||||
<img
|
||||
src="/vite.svg"
|
||||
alt="Logo"
|
||||
class="brand-logo"
|
||||
/>
|
||||
<h1 class="brand-title">{{ t('app.title') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端导航 -->
|
||||
<nav class="header-nav header-nav--desktop" v-if="!isSmallScreen">
|
||||
<router-link to="/" class="nav-link">{{ t('nav.home') }}</router-link>
|
||||
<router-link to="/about" class="nav-link">{{ t('nav.about') }}</router-link>
|
||||
<router-link to="/projects" class="nav-link">{{ t('nav.projects') }}</router-link>
|
||||
<router-link to="/contact" class="nav-link">{{ t('nav.contact') }}</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="header-controls">
|
||||
<!-- 主题切换 -->
|
||||
<button
|
||||
class="control-btn theme-toggle"
|
||||
:title="t('theme.switch')"
|
||||
@click="handleThemeChange"
|
||||
>
|
||||
<span v-if="isDarkMode" class="icon">🌙</span>
|
||||
<span v-else class="icon">☀️</span>
|
||||
</button>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="control-btn"
|
||||
:title="t('language.switch')"
|
||||
@click="handleLanguageChange(currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN')"
|
||||
>
|
||||
<span class="icon">{{ currentLocale === 'zh-CN' ? '🇨🇳' : '🇺🇸' }}</span>
|
||||
<span class="lang-text">{{ currentLocale === 'zh-CN' ? 'EN' : '中文' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<button
|
||||
v-if="isSmallScreen"
|
||||
class="control-btn mobile-menu-btn"
|
||||
:title="showMobileMenu ? t('menu.close') : t('menu.open')"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
<span class="hamburger" :class="{ active: showMobileMenu }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div
|
||||
v-if="isSmallScreen"
|
||||
class="mobile-nav"
|
||||
:class="{ 'mobile-nav--open': showMobileMenu }"
|
||||
>
|
||||
<nav class="mobile-nav-menu">
|
||||
<router-link
|
||||
to="/"
|
||||
class="mobile-nav-link"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
{{ t('nav.home') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/about"
|
||||
class="mobile-nav-link"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
{{ t('nav.about') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/projects"
|
||||
class="mobile-nav-link"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
{{ t('nav.projects') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/contact"
|
||||
class="mobile-nav-link"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
{{ t('nav.contact') }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="app-main">
|
||||
<div class="main-container">
|
||||
|
|
@ -235,22 +142,6 @@ onUnmounted(() => {
|
|||
</router-view>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<footer class="app-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-content">
|
||||
<p class="footer-text">
|
||||
© {{ currentYear }} {{ t('app.title') }}. {{ t('footer.madeWith') }}
|
||||
<span class="heart">💖</span>
|
||||
</p>
|
||||
<p class="footer-tech">
|
||||
{{ t('footer.builtWith') }} Vue 3 + Vite + Pinia + Vue Router
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 全局加载指示器 -->
|
||||
<div
|
||||
v-if="appStore.isLoading"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
<template>
|
||||
<div class="admin-layout">
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="admin-header">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
text
|
||||
class="sidebar-toggle"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon><Menu /></el-icon>
|
||||
</el-button>
|
||||
<h1 class="admin-title">{{ t('admin.title') }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 主题切换 -->
|
||||
<el-tooltip :content="t('header.toggleTheme')" placement="bottom">
|
||||
<el-button text @click="toggleTheme">
|
||||
<el-icon v-if="isDark"><Sunny /></el-icon>
|
||||
<el-icon v-else><Moon /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<el-tooltip :content="t('header.switchLanguage')" placement="bottom">
|
||||
<el-button text @click="toggleLocale">
|
||||
<el-icon><Switch /></el-icon>
|
||||
<span class="lang-text">{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 用户信息下拉菜单 -->
|
||||
<el-dropdown @command="handleUserAction">
|
||||
<span class="user-info">
|
||||
<el-avatar :size="32" class="user-avatar">
|
||||
{{ username.charAt(0).toUpperCase() }}
|
||||
</el-avatar>
|
||||
<span class="username">{{ username }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ t('admin.layout.profile') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="settings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
{{ t('admin.layout.settings') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
{{ t('admin.layout.logout') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-container class="admin-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :class="['admin-sidebar', { 'collapsed': sidebarCollapsed }]">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="admin-menu"
|
||||
:collapse="sidebarCollapsed"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="/admin">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<template #title>{{ t('admin.layout.dashboard') }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/content-review">
|
||||
<el-icon><Document /></el-icon>
|
||||
<template #title>{{ t('admin.layout.contentReview') }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/orders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<template #title>{{ t('admin.layout.orders') }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/admin/users">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<template #title>{{ t('admin.layout.users') }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<el-main class="admin-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<!-- 移动端遮罩 -->
|
||||
<div
|
||||
v-if="mobileMenuVisible && !sidebarCollapsed"
|
||||
class="mobile-overlay"
|
||||
@click="toggleSidebar"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
import {
|
||||
Menu,
|
||||
Sunny,
|
||||
Moon,
|
||||
Switch,
|
||||
ArrowDown,
|
||||
User,
|
||||
Setting,
|
||||
SwitchButton,
|
||||
DataAnalysis,
|
||||
Document,
|
||||
ShoppingCart,
|
||||
UserFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const sidebarCollapsed = ref(false)
|
||||
const mobileMenuVisible = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => appStore.isDarkTheme)
|
||||
const currentLocale = computed(() => locale.value)
|
||||
const username = computed(() => authStore.username)
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
// 方法
|
||||
const toggleSidebar = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
mobileMenuVisible.value = !mobileMenuVisible.value
|
||||
} else {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
appStore.toggleTheme()
|
||||
}
|
||||
|
||||
const toggleLocale = () => {
|
||||
try {
|
||||
const newLocale = appStore.locale === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
// 更新所有语言系统,确保语言切换立即生效
|
||||
appStore.setLocale(newLocale)
|
||||
locale.value = newLocale // 同时更新Vue I18n的locale
|
||||
// 显示成功消息
|
||||
console.log('Language switched to:', newLocale)
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle locale:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserAction = async (command) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
// 处理个人资料
|
||||
ElMessage.info(t('messages.profileComingSoon'))
|
||||
break
|
||||
case 'settings':
|
||||
// 处理设置
|
||||
ElMessage.info(t('messages.settingsComingSoon'))
|
||||
break
|
||||
case 'logout':
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('messages.confirmLogout'),
|
||||
t('messages.logout'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
authStore.logout()
|
||||
ElMessage.success(t('messages.logoutSuccess'))
|
||||
router.push('/admin/login')
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
mobileMenuVisible.value = false
|
||||
} else {
|
||||
sidebarCollapsed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
appStore.initApp()
|
||||
window.addEventListener('resize', handleResize)
|
||||
handleResize()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.admin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
font-size: 18px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #6b46c1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lang-text {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #6b46c1, #a78bfa);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.admin-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.admin-sidebar {
|
||||
background: white;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
width: 240px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
overflow-x: hidden; /* 防止横向滚动条 */
|
||||
}
|
||||
|
||||
.admin-sidebar.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.admin-menu:not(.el-menu--collapse) {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* 确保菜单项不会超出容器 */
|
||||
.admin-sidebar .el-menu {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.admin-sidebar .el-menu-item {
|
||||
min-width: 0; /* 允许菜单项收缩 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* 长文本显示省略号 */
|
||||
white-space: nowrap; /* 单行显示 */
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.admin-main {
|
||||
background: #f9fafb;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 移动端遮罩 */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.admin-header {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 64px;
|
||||
bottom: 0;
|
||||
height: calc(100vh - 64px);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.admin-sidebar.collapsed {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色主题 */
|
||||
[data-theme="dark"] .admin-header {
|
||||
background: #1f2937;
|
||||
border-bottom-color: #374151;
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .admin-sidebar {
|
||||
background: #1f2937;
|
||||
border-right-color: #374151;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .admin-main {
|
||||
background: #111827;
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-info:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .username {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useI18n as useVueI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '../stores'
|
||||
|
||||
// 支持的语言列表
|
||||
|
|
@ -97,7 +97,7 @@ export function formatRelativeTime(date) {
|
|||
}
|
||||
|
||||
export function useI18nExt() {
|
||||
const { locale: i18nLocale, messages, t, d } = useI18n()
|
||||
const { locale: i18nLocale, messages, t, d } = useVueI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 当前语言
|
||||
|
|
@ -118,18 +118,15 @@ export function useI18nExt() {
|
|||
// 更新当前语言状态
|
||||
currentLocale.value = localeCode
|
||||
|
||||
// 保存到 store 和 localStorage
|
||||
// 只保存到 localStorage,store 由 App.vue 统一管理
|
||||
if (saveToStorage) {
|
||||
appStore.setLanguage(localeCode)
|
||||
localStorage.setItem('app-locale', localeCode)
|
||||
}
|
||||
|
||||
// 更新 HTML lang 属性
|
||||
document.documentElement.lang = localeCode
|
||||
|
||||
// 触发语言变化事件
|
||||
window.dispatchEvent(new CustomEvent('locale-changed', {
|
||||
detail: { locale: localeCode }
|
||||
}))
|
||||
// 不直接调用store,避免循环
|
||||
|
||||
// 更新日期格式
|
||||
document.documentElement.dir = getTextDirection(localeCode)
|
||||
|
|
@ -213,7 +210,7 @@ export function useI18nExt() {
|
|||
const savedLocale = localStorage.getItem('app-locale')
|
||||
|
||||
// 从 store 获取语言设置
|
||||
const storeLocale = appStore.language
|
||||
const storeLocale = appStore.locale
|
||||
|
||||
// 优先级:store > localStorage > 浏览器检测
|
||||
const initialLocale = storeLocale || savedLocale || detectBrowserLocale()
|
||||
|
|
@ -221,20 +218,8 @@ export function useI18nExt() {
|
|||
// 设置初始语言
|
||||
setLocale(initialLocale, false)
|
||||
|
||||
// 监听 store 中的语言变化
|
||||
watch(() => appStore.language, (newLocale) => {
|
||||
if (newLocale !== currentLocale.value) {
|
||||
setLocale(newLocale, false)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 i18n 内部语言变化
|
||||
watch(i18nLocale, (newLocale) => {
|
||||
if (newLocale !== currentLocale.value) {
|
||||
currentLocale.value = newLocale
|
||||
appStore.setLanguage(newLocale)
|
||||
}
|
||||
})
|
||||
// 移除所有监听器,避免循环调用
|
||||
// store 和 i18n 独立工作,避免相互干扰
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -253,4 +238,44 @@ export function useI18nExt() {
|
|||
formatRelativeTime,
|
||||
initLocale
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的导出供页面组件使用
|
||||
export function useI18n() {
|
||||
const { locale: i18nLocale, t } = useVueI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 当前语言
|
||||
const currentLocale = ref(i18nLocale.value)
|
||||
|
||||
// 设置语言
|
||||
const setLocale = (localeCode, saveToStorage = true) => {
|
||||
// 验证语言有效性
|
||||
const isValidLocale = SUPPORTED_LOCALES.some(lang => lang.code === localeCode)
|
||||
if (!isValidLocale) {
|
||||
console.warn(`Invalid locale: ${localeCode}, falling back to 'en-US'`)
|
||||
localeCode = 'en-US'
|
||||
}
|
||||
|
||||
// 更新 i18n 语言
|
||||
i18nLocale.value = localeCode
|
||||
|
||||
// 更新当前语言状态
|
||||
currentLocale.value = localeCode
|
||||
|
||||
// 只保存到 localStorage,store 由 App.vue 统一管理
|
||||
if (saveToStorage) {
|
||||
localStorage.setItem('app-locale', localeCode)
|
||||
}
|
||||
|
||||
// 更新 HTML lang 属性
|
||||
document.documentElement.lang = localeCode
|
||||
}
|
||||
|
||||
return {
|
||||
currentLocale,
|
||||
locales: SUPPORTED_LOCALES,
|
||||
t,
|
||||
setLocale
|
||||
}
|
||||
}
|
||||
|
|
@ -74,5 +74,206 @@ export default {
|
|||
pageNotFound: 'Page Not Found',
|
||||
goHome: 'Go Home',
|
||||
description: 'Sorry, the page you are looking for does not exist.'
|
||||
},
|
||||
|
||||
// Header navigation
|
||||
header: {
|
||||
toggleTheme: 'Toggle Theme',
|
||||
switchLanguage: 'Switch Language'
|
||||
},
|
||||
|
||||
// Messages
|
||||
messages: {
|
||||
profileComingSoon: 'Profile feature coming soon',
|
||||
settingsComingSoon: 'Settings feature coming soon',
|
||||
confirmLogout: 'Are you sure you want to logout?',
|
||||
logout: 'Logout',
|
||||
logoutSuccess: 'Logout successful'
|
||||
},
|
||||
|
||||
admin: {
|
||||
title: 'Admin Panel',
|
||||
login: {
|
||||
title: 'Admin Login',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
rememberMe: 'Remember Me',
|
||||
login: 'Login',
|
||||
logging: 'Logging in...',
|
||||
usernameRequired: 'Please enter username',
|
||||
passwordRequired: 'Please enter password',
|
||||
usernamePlaceholder: 'Please enter username',
|
||||
passwordPlaceholder: 'Please enter password',
|
||||
loginSuccess: 'Login successful',
|
||||
loginFailed: 'Login failed, please check username and password',
|
||||
welcome: 'Welcome to Admin Panel'
|
||||
},
|
||||
common: {
|
||||
refresh: 'Refresh',
|
||||
search: 'Search',
|
||||
reset: 'Reset',
|
||||
startDate: 'Start Date',
|
||||
endDate: 'End Date',
|
||||
loading: 'Loading...'
|
||||
},
|
||||
layout: {
|
||||
dashboard: 'Dashboard',
|
||||
content: 'Content Review',
|
||||
contentReview: 'Content Review',
|
||||
orders: 'Order Management',
|
||||
users: 'User Management',
|
||||
logout: 'Logout',
|
||||
profile: 'Profile',
|
||||
settings: 'Settings'
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
subtitle: 'System Overview and Key Metrics',
|
||||
refresh: 'Refresh Data',
|
||||
stats: {
|
||||
totalUsers: 'Total Users',
|
||||
totalRevenue: 'Total Revenue',
|
||||
revenue: 'Revenue',
|
||||
totalOrders: 'Total Orders',
|
||||
growthRate: 'Growth Rate',
|
||||
pendingReviews: 'Pending Reviews'
|
||||
},
|
||||
charts: {
|
||||
salesTrend: 'Sales Trend',
|
||||
orderStatus: 'Order Status Distribution',
|
||||
recentActivity: 'Recent Activity'
|
||||
},
|
||||
activity: {
|
||||
userRegistration: 'User Registration',
|
||||
orderCreated: 'Order Created',
|
||||
paymentReceived: 'Payment Received',
|
||||
systemUpdate: 'System Update'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
title: 'Content Management',
|
||||
add: 'Add Content',
|
||||
status: 'Status',
|
||||
type: 'Type',
|
||||
search: 'Search Content',
|
||||
author: 'Author',
|
||||
publishDate: 'Publish Date',
|
||||
views: 'Views',
|
||||
actions: 'Actions',
|
||||
view: 'View',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
statusOptions: {
|
||||
published: 'Published',
|
||||
pending: 'Pending',
|
||||
draft: 'Draft',
|
||||
rejected: 'Rejected'
|
||||
},
|
||||
typeOptions: {
|
||||
article: 'Article',
|
||||
image: 'Image',
|
||||
video: 'Video'
|
||||
}
|
||||
},
|
||||
orders: {
|
||||
title: 'Order Management',
|
||||
export: 'Export Orders',
|
||||
search: 'Search Orders',
|
||||
status: 'Status',
|
||||
dateRange: 'Date Range',
|
||||
orderNumber: 'Order Number',
|
||||
customer: 'Customer',
|
||||
total: 'Total Amount',
|
||||
payment: 'Payment Method',
|
||||
date: 'Order Date',
|
||||
actions: 'Actions',
|
||||
view: 'View',
|
||||
updateStatus: 'Update Status',
|
||||
detail: 'Order Detail',
|
||||
basicInfo: 'Basic Information',
|
||||
items: 'Order Items',
|
||||
itemName: 'Item Name',
|
||||
quantity: 'Quantity',
|
||||
price: 'Price',
|
||||
currentStatus: 'Current Status',
|
||||
newStatus: 'New Status',
|
||||
selectStatus: 'Select Status',
|
||||
stats: {
|
||||
total: 'Total Orders',
|
||||
pending: 'Pending',
|
||||
completed: 'Completed',
|
||||
revenue: 'Total Revenue'
|
||||
},
|
||||
statusOptions: {
|
||||
pending: 'Pending',
|
||||
processing: 'Processing',
|
||||
completed: 'Completed',
|
||||
shipped: 'Shipped',
|
||||
delivered: 'Delivered',
|
||||
cancelled: 'Cancelled'
|
||||
},
|
||||
paymentOptions: {
|
||||
alipay: 'Alipay',
|
||||
wechat: 'WeChat Pay',
|
||||
credit: 'Credit Card'
|
||||
}
|
||||
},
|
||||
users: {
|
||||
title: 'User Management',
|
||||
add: 'Add User',
|
||||
search: 'Search Users',
|
||||
status: 'Status',
|
||||
role: 'Role',
|
||||
registerDate: 'Register Date',
|
||||
username: 'Username',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
lastLogin: 'Last Login',
|
||||
loginCount: 'Login Count',
|
||||
actions: 'Actions',
|
||||
view: 'View',
|
||||
edit: 'Edit',
|
||||
resetPassword: 'Reset Password',
|
||||
ban: 'Ban',
|
||||
unban: 'Unban',
|
||||
detail: 'User Detail',
|
||||
selectRole: 'Select Role',
|
||||
selectStatus: 'Select Status',
|
||||
save: 'Save',
|
||||
stats: {
|
||||
total: 'Total Users',
|
||||
active: 'Active Users',
|
||||
inactive: 'Inactive Users',
|
||||
vip: 'VIP Users'
|
||||
},
|
||||
statusOptions: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
banned: 'Banned'
|
||||
},
|
||||
roleOptions: {
|
||||
admin: 'Administrator',
|
||||
user: 'Regular User',
|
||||
vip: 'VIP User'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 通用文本
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
search: 'Search',
|
||||
loading: 'Loading...',
|
||||
viewAll: 'View All',
|
||||
reset: 'Reset',
|
||||
noData: 'No Data',
|
||||
error: 'Error',
|
||||
success: 'Success',
|
||||
warning: 'Warning',
|
||||
info: 'Info'
|
||||
}
|
||||
}
|
||||
|
|
@ -74,5 +74,226 @@ export default {
|
|||
pageNotFound: '页面不存在',
|
||||
goHome: '返回首页',
|
||||
description: '抱歉,您访问的页面不存在。'
|
||||
},
|
||||
|
||||
// 头部导航
|
||||
header: {
|
||||
toggleTheme: '切换主题',
|
||||
switchLanguage: '切换语言'
|
||||
},
|
||||
|
||||
// 消息提示
|
||||
messages: {
|
||||
profileComingSoon: '个人资料功能即将推出',
|
||||
settingsComingSoon: '设置功能即将推出',
|
||||
confirmLogout: '确定要退出登录吗?',
|
||||
logout: '退出登录',
|
||||
logoutSuccess: '退出成功'
|
||||
},
|
||||
|
||||
// 管理后台
|
||||
admin: {
|
||||
title: '管理后台',
|
||||
login: {
|
||||
title: '管理员登录',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
rememberMe: '记住我',
|
||||
login: '登录',
|
||||
logging: '登录中...',
|
||||
usernameRequired: '请输入用户名',
|
||||
passwordRequired: '请输入密码',
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
loginSuccess: '登录成功',
|
||||
loginFailed: '登录失败,请检查用户名和密码',
|
||||
welcome: '欢迎使用管理后台'
|
||||
},
|
||||
dashboard: {
|
||||
title: '仪表板',
|
||||
subtitle: '系统概览和关键指标',
|
||||
refresh: '刷新数据',
|
||||
stats: {
|
||||
totalUsers: '总用户数',
|
||||
totalRevenue: '总收入',
|
||||
revenue: '收入',
|
||||
totalOrders: '总订单数',
|
||||
growthRate: '增长率',
|
||||
pendingReviews: '待审核内容'
|
||||
},
|
||||
charts: {
|
||||
salesTrend: '销售趋势',
|
||||
orderStatus: '订单状态分布',
|
||||
recentActivity: '最近活动'
|
||||
},
|
||||
activity: {
|
||||
userRegistration: '用户注册',
|
||||
orderCreated: '订单创建',
|
||||
paymentReceived: '收到付款',
|
||||
systemUpdate: '系统更新'
|
||||
}
|
||||
},
|
||||
common: {
|
||||
refresh: '刷新',
|
||||
search: '搜索',
|
||||
reset: '重置',
|
||||
startDate: '开始日期',
|
||||
endDate: '结束日期',
|
||||
loading: '加载中...'
|
||||
},
|
||||
layout: {
|
||||
dashboard: '仪表板',
|
||||
content: '内容审核',
|
||||
contentReview: '内容审核',
|
||||
orders: '订单管理',
|
||||
users: '用户管理',
|
||||
logout: '退出登录',
|
||||
profile: '个人资料',
|
||||
settings: '设置',
|
||||
notifications: '通知'
|
||||
},
|
||||
pages: {
|
||||
content: {
|
||||
title: '内容管理',
|
||||
add: '添加内容',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
search: '搜索内容',
|
||||
author: '作者',
|
||||
publishDate: '发布时间',
|
||||
views: '浏览量',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
statusOptions: {
|
||||
published: '已发布',
|
||||
pending: '待审核',
|
||||
draft: '草稿',
|
||||
rejected: '已拒绝'
|
||||
},
|
||||
typeOptions: {
|
||||
article: '文章',
|
||||
image: '图片',
|
||||
video: '视频'
|
||||
}
|
||||
},
|
||||
orders: {
|
||||
title: '订单管理',
|
||||
export: '导出订单',
|
||||
search: '搜索订单',
|
||||
status: '状态',
|
||||
dateRange: '日期范围',
|
||||
orderNumber: '订单号',
|
||||
customer: '客户',
|
||||
total: '总金额',
|
||||
payment: '支付方式',
|
||||
date: '下单日期',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
updateStatus: '更新状态',
|
||||
detail: '订单详情',
|
||||
basicInfo: '基本信息',
|
||||
items: '订单商品',
|
||||
itemName: '商品名称',
|
||||
quantity: '数量',
|
||||
price: '价格',
|
||||
currentStatus: '当前状态',
|
||||
newStatus: '新状态',
|
||||
selectStatus: '选择状态',
|
||||
stats: {
|
||||
total: '总订单',
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
revenue: '总收入'
|
||||
},
|
||||
statusOptions: {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
shipped: '已发货',
|
||||
delivered: '已送达',
|
||||
cancelled: '已取消'
|
||||
},
|
||||
paymentOptions: {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信支付',
|
||||
credit: '信用卡'
|
||||
}
|
||||
},
|
||||
users: {
|
||||
title: '用户管理',
|
||||
add: '添加用户',
|
||||
search: '搜索用户',
|
||||
status: '状态',
|
||||
role: '角色',
|
||||
registerDate: '注册日期',
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
phone: '手机号',
|
||||
lastLogin: '最后登录',
|
||||
loginCount: '登录次数',
|
||||
actions: '操作',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
resetPassword: '重置密码',
|
||||
ban: '封禁',
|
||||
unban: '解封',
|
||||
detail: '用户详情',
|
||||
selectRole: '选择角色',
|
||||
selectStatus: '选择状态',
|
||||
save: '保存',
|
||||
stats: {
|
||||
total: '总用户',
|
||||
active: '活跃用户',
|
||||
inactive: '非活跃用户',
|
||||
vip: 'VIP用户'
|
||||
},
|
||||
statusOptions: {
|
||||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
banned: '已封禁'
|
||||
},
|
||||
roleOptions: {
|
||||
admin: '管理员',
|
||||
user: '普通用户',
|
||||
vip: 'VIP用户'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 头部导航
|
||||
header: {
|
||||
toggleTheme: '切换主题',
|
||||
switchLanguage: '切换语言',
|
||||
notifications: '通知'
|
||||
},
|
||||
|
||||
// 消息提示
|
||||
messages: {
|
||||
profileComingSoon: '个人资料功能即将推出',
|
||||
settingsComingSoon: '设置功能即将推出',
|
||||
confirmLogout: '确定要退出登录吗?',
|
||||
logout: '退出登录',
|
||||
logoutSuccess: '退出登录成功'
|
||||
},
|
||||
|
||||
// 通用文本
|
||||
common: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
search: '搜索',
|
||||
loading: '加载中...',
|
||||
viewAll: '查看全部',
|
||||
reset: '重置',
|
||||
noData: '暂无数据',
|
||||
error: '错误',
|
||||
success: '成功',
|
||||
warning: '警告',
|
||||
info: '信息'
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@ import router from './router'
|
|||
import App from './App.vue'
|
||||
|
||||
// 导入样式
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import './assets/styles/global.css'
|
||||
import './assets/styles/responsive.css'
|
||||
import './assets/styles/themes.css'
|
||||
|
||||
// 导入Element Plus图标
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
// 导入i18n配置
|
||||
import i18n from './locales/i18n'
|
||||
|
||||
|
|
@ -24,5 +29,10 @@ app.use(i18n)
|
|||
// 配置路由
|
||||
app.use(router)
|
||||
|
||||
// 注册所有Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import About from '@/views/About.vue'
|
||||
import NotFound from '@/views/NotFound.vue'
|
||||
import AdminLogin from '@/views/AdminLogin.vue'
|
||||
|
||||
// 懒加载路由组件
|
||||
const Home = () => import('../views/Home.vue')
|
||||
const About = () => import('../views/About.vue')
|
||||
const NotFound = () => import('../views/NotFound.vue')
|
||||
// 管理员布局组件(懒加载)
|
||||
const AdminLayout = () => import('@/components/admin/AdminLayout.vue')
|
||||
const AdminDashboard = () => import('@/views/admin/AdminDashboard.vue')
|
||||
const AdminContent = () => import('@/views/admin/AdminContent.vue')
|
||||
const AdminOrders = () => import('@/views/admin/AdminOrders.vue')
|
||||
const AdminUsers = () => import('@/views/admin/AdminUsers.vue')
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
redirect: '/admin/login',
|
||||
meta: {
|
||||
title: '首页'
|
||||
title: '首页重定向'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -19,15 +24,70 @@ const routes = [
|
|||
name: 'About',
|
||||
component: About,
|
||||
meta: {
|
||||
title: '关于'
|
||||
title: '关于页面'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
name: 'AdminLogin',
|
||||
component: AdminLogin,
|
||||
meta: {
|
||||
title: '管理员登录',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: AdminLayout,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AdminDashboard',
|
||||
component: AdminDashboard,
|
||||
meta: {
|
||||
title: '仪表板'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'AdminDashboardAlias',
|
||||
redirect: '/admin'
|
||||
},
|
||||
{
|
||||
path: 'content',
|
||||
name: 'AdminContent',
|
||||
component: AdminContent,
|
||||
meta: {
|
||||
title: '内容管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'AdminOrders',
|
||||
component: AdminOrders,
|
||||
meta: {
|
||||
title: '订单管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'AdminUsers',
|
||||
component: AdminUsers,
|
||||
meta: {
|
||||
title: '用户管理'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: NotFound,
|
||||
meta: {
|
||||
title: '404页面'
|
||||
title: '页面不存在'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -44,11 +104,31 @@ const router = createRouter({
|
|||
}
|
||||
})
|
||||
|
||||
// 路由守卫 - 设置页面标题
|
||||
// 路由守卫 - 认证检查
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta && to.meta.title) {
|
||||
document.title = `${to.meta.title} - FrontendDesigner`
|
||||
// 设置页面标题
|
||||
if (to.meta?.title) {
|
||||
document.title = to.meta.title
|
||||
}
|
||||
|
||||
// 检查是否需要认证
|
||||
if (to.meta?.requiresAuth) {
|
||||
const token = localStorage.getItem('admin-token')
|
||||
if (!token) {
|
||||
next('/admin/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录页面,重定向到管理后台首页
|
||||
if (to.path === '/admin/login') {
|
||||
const token = localStorage.getItem('admin-token')
|
||||
if (token) {
|
||||
next('/admin')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,77 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
// 管理员认证状态管理store
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('admin-token') || '',
|
||||
user: JSON.parse(localStorage.getItem('admin-user') || 'null')
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 判断是否已登录
|
||||
isAuthenticated: (state) => !!state.token && !!state.user,
|
||||
// 获取用户名
|
||||
username: (state) => state.user?.username || '',
|
||||
// 获取用户角色
|
||||
userRole: (state) => state.user?.role || ''
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
login(loginData) {
|
||||
const { token, user } = loginData
|
||||
|
||||
this.token = token
|
||||
this.user = user
|
||||
|
||||
// 持久化存储
|
||||
localStorage.setItem('admin-token', token)
|
||||
localStorage.setItem('admin-user', JSON.stringify(user))
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.user = null
|
||||
|
||||
// 清除持久化存储
|
||||
localStorage.removeItem('admin-token')
|
||||
localStorage.removeItem('admin-user')
|
||||
localStorage.removeItem('admin-remember-me')
|
||||
},
|
||||
|
||||
// 检查认证状态
|
||||
checkAuth() {
|
||||
const storedToken = localStorage.getItem('admin-token')
|
||||
const storedUser = localStorage.getItem('admin-user')
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
this.token = storedToken
|
||||
this.user = JSON.parse(storedUser)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
// 检查权限
|
||||
hasPermission(requiredRole) {
|
||||
if (!this.user) return false
|
||||
|
||||
const roleHierarchy = {
|
||||
'admin': 3,
|
||||
'manager': 2,
|
||||
'user': 1
|
||||
}
|
||||
|
||||
const userLevel = roleHierarchy[this.user.role] || 0
|
||||
const requiredLevel = roleHierarchy[requiredRole] || 0
|
||||
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 全局状态管理store
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
|
|
@ -49,12 +121,14 @@ export const useAppStore = defineStore('app', {
|
|||
toggleLocale() {
|
||||
this.locale = this.locale === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
localStorage.setItem('locale', this.locale)
|
||||
// 不再触发事件,由Vue的响应式系统自动通知
|
||||
},
|
||||
|
||||
// 设置语言
|
||||
setLocale(locale) {
|
||||
this.locale = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
// 不再触发事件,由Vue的响应式系统自动通知
|
||||
},
|
||||
|
||||
// 设置加载状态
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
<template>
|
||||
<div class="admin-login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1 class="login-title">{{ t('admin.login.title') }}</h1>
|
||||
<p class="login-subtitle">{{ t('admin.login.welcome') }}</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
size="large"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
:placeholder="t('admin.login.username')"
|
||||
prefix-icon="UserFilled"
|
||||
clearable
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
:placeholder="t('admin.login.password')"
|
||||
show-password
|
||||
clearable
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="login-options">
|
||||
<el-checkbox v-model="loginForm.rememberMe">
|
||||
{{ t('admin.login.rememberMe') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="login-button"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ loading ? t('admin.login.logging') : t('admin.login.login') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { UserFilled, Lock } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loginFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: t('admin.login.usernameRequired'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: t('admin.login.passwordRequired'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟登录API
|
||||
const mockLogin = (username, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// 简单的模拟验证逻辑
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
resolve({
|
||||
token: 'mock-jwt-token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
name: 'Administrator',
|
||||
role: 'admin'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject(new Error('Invalid credentials'))
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
const response = await mockLogin(loginForm.username, loginForm.password)
|
||||
|
||||
// 使用认证store登录
|
||||
authStore.login(response)
|
||||
|
||||
if (loginForm.rememberMe) {
|
||||
localStorage.setItem('admin-remember-me', 'true')
|
||||
}
|
||||
|
||||
ElMessage.success(t('admin.login.loginSuccess'))
|
||||
|
||||
// 跳转到管理后台首页
|
||||
router.push('/admin')
|
||||
} catch (error) {
|
||||
ElMessage.error(t('admin.login.loginFailed'))
|
||||
console.error('Login error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否已经登录
|
||||
if (authStore.checkAuth()) {
|
||||
router.push('/admin')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
padding: 48px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #6b46c1 0%, #a78bfa 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px -5px rgba(107, 70, 193, 0.3);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.login-box {
|
||||
padding: 32px 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n as useI18nExt } from '../composables/useI18n'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18nExt()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 主题系统
|
||||
const { currentTheme } = useTheme()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,462 @@
|
|||
<template>
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-header">
|
||||
<div class="header-left">
|
||||
<h2 class="title">{{ $t('admin.content.title') }}</h2>
|
||||
<p class="subtitle">Manage and review content items</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="dialogVisible = true">
|
||||
{{ $t('admin.content.add') }}
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="refresh">
|
||||
{{ $t('admin.content.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<div class="filters">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
:placeholder="$t('admin.content.search')"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
:placeholder="$t('admin.content.status')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:label="status.label"
|
||||
:value="status.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="filters.type"
|
||||
:placeholder="$t('admin.content.type')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in typeOptions"
|
||||
:key="type.value"
|
||||
:label="type.label"
|
||||
:value="type.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
:start-placeholder="$t('admin.common.startDate')"
|
||||
:end-placeholder="$t('admin.common.endDate')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" :icon="Search" @click="search">
|
||||
{{ $t('admin.common.search') }}
|
||||
</el-button>
|
||||
<el-button @click="resetFilters">{{ $t('admin.common.reset') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 内容列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table :data="tableData" style="width: 100%" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" :label="$t('admin.content.title')" min-width="200" />
|
||||
<el-table-column prop="author" :label="$t('admin.content.author')" width="120" />
|
||||
<el-table-column prop="type" :label="$t('admin.content.type')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.type)">
|
||||
{{ $t(`admin.content.typeOptions.${row.type}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="$t('admin.content.status')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">
|
||||
{{ $t(`admin.content.statusOptions.${row.status}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publishDate" :label="$t('admin.content.publishDate')" width="120" />
|
||||
<el-table-column prop="views" :label="$t('admin.content.views')" width="80" />
|
||||
<el-table-column :label="$t('admin.content.actions')" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="View" @click="handleView(row)">
|
||||
{{ $t('admin.content.view') }}
|
||||
</el-button>
|
||||
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
|
||||
{{ $t('admin.content.edit') }}
|
||||
</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
|
||||
{{ $t('admin.content.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEditing ? $t('admin.content.edit') : $t('admin.content.add')"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item :label="$t('admin.content.title')" prop="title">
|
||||
<el-input v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.content.type')" prop="type">
|
||||
<el-select v-model="form.type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="type in typeOptions"
|
||||
:key="type.value"
|
||||
:label="type.label"
|
||||
:value="type.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.content.status')" prop="status">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:label="status.label"
|
||||
:value="status.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.content.author')" prop="author">
|
||||
<el-input v-model="form.author" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ $t('admin.common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('admin.common.save') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Plus, Search, Edit, Delete, View, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'How to use Vue3 effectively',
|
||||
author: 'John Doe',
|
||||
type: 'article',
|
||||
status: 'published',
|
||||
publishDate: '2024-01-15',
|
||||
views: 1250
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Beautiful sunset photography',
|
||||
author: 'Jane Smith',
|
||||
type: 'image',
|
||||
status: 'pending',
|
||||
publishDate: '2024-01-14',
|
||||
views: 890
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'JavaScript ES6 Features Tutorial',
|
||||
author: 'Bob Johnson',
|
||||
type: 'video',
|
||||
status: 'draft',
|
||||
publishDate: '2024-01-13',
|
||||
views: 567
|
||||
}
|
||||
])
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
type: '',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 3
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
type: '',
|
||||
status: '',
|
||||
author: ''
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'published', label: t('admin.content.statusOptions.published') },
|
||||
{ value: 'pending', label: t('admin.content.statusOptions.pending') },
|
||||
{ value: 'draft', label: t('admin.content.statusOptions.draft') },
|
||||
{ value: 'rejected', label: t('admin.content.statusOptions.rejected') }
|
||||
]
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'article', label: t('admin.content.typeOptions.article') },
|
||||
{ value: 'image', label: t('admin.content.typeOptions.image') },
|
||||
{ value: 'video', label: t('admin.content.typeOptions.video') }
|
||||
]
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: 'Please enter title', trigger: 'blur' }],
|
||||
type: [{ required: true, message: 'Please select type', trigger: 'change' }],
|
||||
status: [{ required: true, message: 'Please select status', trigger: 'change' }],
|
||||
author: [{ required: true, message: 'Please enter author', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const getStatusTagType = (status) => {
|
||||
const types = {
|
||||
published: 'success',
|
||||
pending: 'warning',
|
||||
draft: 'info',
|
||||
rejected: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const getTypeTagType = (type) => {
|
||||
const types = {
|
||||
article: 'primary',
|
||||
image: 'success',
|
||||
video: 'warning'
|
||||
}
|
||||
return types[type] || 'info'
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
// 模拟搜索
|
||||
console.log('Search with filters:', filters)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.type = ''
|
||||
filters.dateRange = []
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
// 模拟刷新
|
||||
console.log('Refresh data')
|
||||
ElMessage.success('Data refreshed successfully')
|
||||
}
|
||||
|
||||
const handleAddContent = () => {
|
||||
isEditing.value = false
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
type: '',
|
||||
status: '',
|
||||
author: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleView = (row) => {
|
||||
ElMessage.info(`View content: ${row.title}`)
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
isEditing.value = true
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`Are you sure you want to delete "${row.title}"?`,
|
||||
'Confirm Deletion',
|
||||
{
|
||||
confirmButtonText: 'Yes',
|
||||
cancelButtonText: 'No',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
ElMessage.success('Content deleted successfully')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('Content saved successfully')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.pageSize = size
|
||||
console.log('Page size changed to:', size)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.currentPage = page
|
||||
console.log('Current page changed to:', page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-content {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-left .title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-left .subtitle {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.admin-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filters .el-col {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,621 @@
|
|||
<template>
|
||||
<div class="admin-dashboard">
|
||||
<!-- 页面头部 -->
|
||||
<div class="dashboard-header">
|
||||
<div class="header-left">
|
||||
<h2>{{ t('admin.dashboard.title') }}</h2>
|
||||
<p class="subtitle">{{ t('admin.dashboard.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
{{ t('admin.dashboard.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon users">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.totalUsers }}</div>
|
||||
<div class="stat-label">{{ t('admin.dashboard.stats.totalUsers') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon orders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.totalOrders }}</div>
|
||||
<div class="stat-label">{{ t('admin.dashboard.stats.totalOrders') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon reviews">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.pendingReviews }}</div>
|
||||
<div class="stat-label">{{ t('admin.dashboard.stats.pendingReviews') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon revenue">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">¥{{ formatNumber(stats.revenue) }}</div>
|
||||
<div class="stat-label">{{ t('admin.dashboard.stats.revenue') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section">
|
||||
<el-row :gutter="24">
|
||||
<!-- 销售趋势图表 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>{{ t('admin.dashboard.charts.salesTrend') }}</span>
|
||||
<el-select v-model="chartPeriod" size="small" style="width: 120px">
|
||||
<el-option label="最近7天" value="7d" />
|
||||
<el-option label="最近30天" value="30d" />
|
||||
<el-option label="最近90天" value="90d" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<div class="simple-chart">
|
||||
<div
|
||||
v-for="(point, index) in salesData"
|
||||
:key="index"
|
||||
class="chart-bar"
|
||||
:style="{
|
||||
height: `${(point.value / maxSalesValue) * 200}px`,
|
||||
background: `linear-gradient(to top, #6b46c1, #a78bfa)`
|
||||
}"
|
||||
:title="`${point.date}: ¥${formatNumber(point.value)}`"
|
||||
></div>
|
||||
</div>
|
||||
<div class="chart-labels">
|
||||
<span
|
||||
v-for="(point, index) in salesData"
|
||||
:key="index"
|
||||
class="chart-label"
|
||||
>
|
||||
{{ point.date }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 订单状态分布 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>{{ t('admin.dashboard.charts.orderStatus') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<div class="pie-chart">
|
||||
<div class="pie-segment completed" :style="{ '--percentage': orderStats.completed }"></div>
|
||||
<div class="pie-segment pending" :style="{ '--percentage': orderStats.pending }"></div>
|
||||
<div class="pie-segment cancelled" :style="{ '--percentage': orderStats.cancelled }"></div>
|
||||
</div>
|
||||
<div class="pie-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color completed"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.completed') }} ({{ orderStats.completed }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color pending"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.pending') }} ({{ orderStats.pending }}%)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color cancelled"></div>
|
||||
<span>{{ t('admin.orders.statusOptions.cancelled') }} ({{ orderStats.cancelled }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="recent-activity">
|
||||
<el-card class="activity-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ t('admin.dashboard.charts.recentActivity') }}</span>
|
||||
<el-button text type="primary" size="small">{{ t('common.viewAll') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
class="activity-item"
|
||||
>
|
||||
<div class="activity-icon" :class="activity.type">
|
||||
<el-icon><component :is="getActivityIcon(activity.type)" /></el-icon>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title">{{ activity.title }}</div>
|
||||
<div class="activity-time">{{ formatTime(activity.time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
UserFilled,
|
||||
ShoppingCart,
|
||||
Document,
|
||||
Money,
|
||||
User,
|
||||
Bell,
|
||||
Setting
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const chartPeriod = ref('7d')
|
||||
|
||||
const stats = ref({
|
||||
totalUsers: 12568,
|
||||
totalOrders: 3421,
|
||||
pendingReviews: 89,
|
||||
revenue: 125678
|
||||
})
|
||||
|
||||
const salesData = ref([
|
||||
{ date: '11/12', value: 12000 },
|
||||
{ date: '11/13', value: 18000 },
|
||||
{ date: '11/14', value: 15000 },
|
||||
{ date: '11/15', value: 22000 },
|
||||
{ date: '11/16', value: 19000 },
|
||||
{ date: '11/17', value: 25000 },
|
||||
{ date: '11/18', value: 28000 }
|
||||
])
|
||||
|
||||
const orderStats = ref({
|
||||
completed: 75,
|
||||
pending: 20,
|
||||
cancelled: 5
|
||||
})
|
||||
|
||||
const recentActivities = ref([
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
title: '新用户注册:zhangsan@example.com',
|
||||
time: new Date(Date.now() - 1000 * 60 * 5)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'order',
|
||||
title: '新订单:#ORD-2025-001',
|
||||
time: new Date(Date.now() - 1000 * 60 * 15)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'review',
|
||||
title: '内容审核:用户反馈',
|
||||
time: new Date(Date.now() - 1000 * 60 * 30)
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'system',
|
||||
title: '系统更新完成',
|
||||
time: new Date(Date.now() - 1000 * 60 * 60)
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const maxSalesValue = computed(() => {
|
||||
return Math.max(...salesData.value.map(item => item.value))
|
||||
})
|
||||
|
||||
// 方法
|
||||
const formatNumber = (num) => {
|
||||
return new Intl.NumberFormat('zh-CN').format(num)
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
const now = new Date()
|
||||
const diff = Math.floor((now - time) / 1000)
|
||||
|
||||
if (diff < 60) return '刚刚'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
|
||||
return `${Math.floor(diff / 86400)}天前`
|
||||
}
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
const icons = {
|
||||
user: User,
|
||||
order: ShoppingCart,
|
||||
review: Document,
|
||||
system: Setting
|
||||
}
|
||||
return icons[type] || Bell
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟数据刷新
|
||||
setInterval(() => {
|
||||
stats.value.totalUsers += Math.floor(Math.random() * 3)
|
||||
stats.value.totalOrders += Math.floor(Math.random() * 2)
|
||||
stats.value.pendingReviews = Math.floor(Math.random() * 100)
|
||||
}, 30000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 页面头部样式 */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.header-left h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-icon.users {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.stat-icon.orders {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.stat-icon.reviews {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.stat-icon.revenue {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 图表区域 */
|
||||
.charts-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
/* 销售趋势图表 */
|
||||
.simple-chart {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
height: 200px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
width: 20px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.chart-bar:hover {
|
||||
opacity: 1;
|
||||
transform: scaleY(1.05);
|
||||
}
|
||||
|
||||
.chart-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 饼图 */
|
||||
.pie-chart {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
from 0deg,
|
||||
#10b981 0deg calc(var(--completed) * 3.6deg),
|
||||
#f59e0b calc(var(--completed) * 3.6deg) calc((var(--completed) + var(--pending)) * 3.6deg),
|
||||
#ef4444 calc((var(--completed) + var(--pending)) * 3.6deg) 360deg
|
||||
);
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.pie-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-color.completed {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.legend-color.pending {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.legend-color.cancelled {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
/* 最近活动 */
|
||||
.activity-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-icon.user {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.activity-icon.order {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.activity-icon.review {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.activity-icon.system {
|
||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.simple-chart {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-dashboard {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.simple-chart {
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,664 @@
|
|||
<template>
|
||||
<div class="admin-orders">
|
||||
<!-- 页面头部 -->
|
||||
<div class="dashboard-header">
|
||||
<div class="header-left">
|
||||
<h2 class="title">{{ $t('admin.orders.title') }}</h2>
|
||||
<p class="subtitle">Manage orders and track delivery status</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Download" @click="exportOrders">
|
||||
{{ $t('admin.orders.export') }}
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="refresh">
|
||||
{{ $t('admin.common.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="order-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon total">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ orderStats.total }}</div>
|
||||
<div class="stat-label">{{ t('admin.orders.stats.total') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon pending">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ orderStats.pending }}</div>
|
||||
<div class="stat-label">{{ t('admin.orders.stats.pending') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon completed">
|
||||
<el-icon><Check /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ orderStats.completed }}</div>
|
||||
<div class="stat-label">{{ t('admin.orders.stats.completed') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon revenue">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">¥{{ orderStats.revenue.toLocaleString() }}</div>
|
||||
<div class="stat-label">{{ t('admin.orders.stats.revenue') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="orders-filters">
|
||||
<div class="filter-group">
|
||||
<el-select v-model="selectedStatus" :placeholder="t('admin.orders.status')" clearable>
|
||||
<el-option :label="t('admin.orders.status.pending')" value="pending" />
|
||||
<el-option :label="t('admin.orders.status.processing')" value="processing" />
|
||||
<el-option :label="t('admin.orders.status.shipped')" value="shipped" />
|
||||
<el-option :label="t('admin.orders.status.delivered')" value="delivered" />
|
||||
<el-option :label="t('admin.orders.status.cancelled')" value="cancelled" />
|
||||
</el-select>
|
||||
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
:placeholder="t('admin.orders.dateRange')"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-group">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('admin.orders.search')"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="orders-list">
|
||||
<el-table
|
||||
:data="filteredOrdersList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@row-click="handleOrderDetail"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="100" />
|
||||
<el-table-column prop="orderNumber" :label="t('admin.orders.orderNumber')" width="150" />
|
||||
<el-table-column prop="customerName" :label="t('admin.orders.customer')" width="120" />
|
||||
<el-table-column prop="totalAmount" :label="t('admin.orders.total')" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.totalAmount.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="t('admin.orders.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">
|
||||
{{ t(`admin.orders.status.${row.status}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="paymentMethod" :label="t('admin.orders.payment')" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ t(`admin.orders.payment.${row.paymentMethod}`) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderDate" :label="t('admin.orders.date')" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.orderDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('admin.orders.actions')" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click.stop="handleViewOrder(row)">
|
||||
{{ t('admin.orders.view') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click.stop="handleUpdateStatus(row)"
|
||||
v-if="row.status !== 'delivered' && row.status !== 'cancelled'"
|
||||
>
|
||||
{{ t('admin.orders.updateStatus') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="totalOrders"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog
|
||||
:title="t('admin.orders.detail')"
|
||||
v-model="detailDialogVisible"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="selectedOrder" class="order-detail">
|
||||
<div class="detail-section">
|
||||
<h4>{{ t('admin.orders.basicInfo') }}</h4>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="t('admin.orders.orderNumber')">
|
||||
{{ selectedOrder.orderNumber }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('admin.orders.customer')">
|
||||
{{ selectedOrder.customerName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('admin.orders.total')">
|
||||
¥{{ selectedOrder.totalAmount.toFixed(2) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('admin.orders.status')">
|
||||
<el-tag :type="getStatusTagType(selectedOrder.status)">
|
||||
{{ t(`admin.orders.status.${selectedOrder.status}`) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>{{ t('admin.orders.items') }}</h4>
|
||||
<el-table :data="selectedOrder.items" stripe>
|
||||
<el-table-column prop="name" :label="t('admin.orders.itemName')" />
|
||||
<el-table-column prop="quantity" :label="t('admin.orders.quantity')" width="100" />
|
||||
<el-table-column prop="price" :label="t('admin.orders.price')" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.price.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 更新状态对话框 -->
|
||||
<el-dialog
|
||||
:title="t('admin.orders.updateStatus')"
|
||||
v-model="statusDialogVisible"
|
||||
width="400px"
|
||||
>
|
||||
<el-form v-if="selectedOrderForStatus" :model="statusForm" label-width="100px">
|
||||
<el-form-item :label="t('admin.orders.currentStatus')">
|
||||
<el-tag :type="getStatusTagType(selectedOrderForStatus.status)">
|
||||
{{ t(`admin.orders.status.${selectedOrderForStatus.status}`) }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('admin.orders.newStatus')" prop="status">
|
||||
<el-select v-model="statusForm.status" :placeholder="t('admin.orders.selectStatus')">
|
||||
<el-option
|
||||
v-for="status in availableStatuses"
|
||||
:key="status.value"
|
||||
:label="status.label"
|
||||
:value="status.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="statusDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleStatusUpdate">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
Download,
|
||||
ShoppingCart,
|
||||
Clock,
|
||||
Check,
|
||||
Money,
|
||||
Search,
|
||||
Refresh
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'AdminOrders',
|
||||
components: {
|
||||
Download,
|
||||
ShoppingCart,
|
||||
Clock,
|
||||
Check,
|
||||
Money,
|
||||
Search
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const dateRange = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalOrders = ref(0)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusDialogVisible = ref(false)
|
||||
const selectedOrder = ref(null)
|
||||
const selectedOrderForStatus = ref(null)
|
||||
|
||||
const statusForm = ref({
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 模拟订单数据
|
||||
const ordersList = ref([
|
||||
{
|
||||
id: 1,
|
||||
orderNumber: 'ORD-2024-001',
|
||||
customerName: '张三',
|
||||
totalAmount: 1299.00,
|
||||
status: 'delivered',
|
||||
paymentMethod: 'alipay',
|
||||
orderDate: '2024-01-15 10:30:00',
|
||||
items: [
|
||||
{ name: 'AI智能音箱', quantity: 1, price: 799.00 },
|
||||
{ name: '智能插座', quantity: 2, price: 250.00 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderNumber: 'ORD-2024-002',
|
||||
customerName: '李四',
|
||||
totalAmount: 2599.00,
|
||||
status: 'shipped',
|
||||
paymentMethod: 'wechat',
|
||||
orderDate: '2024-01-14 16:20:00',
|
||||
items: [
|
||||
{ name: '智能门锁', quantity: 1, price: 1999.00 },
|
||||
{ name: '智能摄像头', quantity: 1, price: 600.00 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
orderNumber: 'ORD-2024-003',
|
||||
customerName: '王五',
|
||||
totalAmount: 899.00,
|
||||
status: 'processing',
|
||||
paymentMethod: 'credit',
|
||||
orderDate: '2024-01-13 09:15:00',
|
||||
items: [
|
||||
{ name: '智能灯泡', quantity: 3, price: 299.00 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
orderNumber: 'ORD-2024-004',
|
||||
customerName: '赵六',
|
||||
totalAmount: 1599.00,
|
||||
status: 'pending',
|
||||
paymentMethod: 'alipay',
|
||||
orderDate: '2024-01-12 14:45:00',
|
||||
items: [
|
||||
{ name: '智能扫地机器人', quantity: 1, price: 1599.00 }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 统计数据
|
||||
const orderStats = ref({
|
||||
total: 156,
|
||||
pending: 23,
|
||||
completed: 128,
|
||||
revenue: 156789
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const filteredOrdersList = computed(() => {
|
||||
let result = ordersList.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
result = result.filter(item =>
|
||||
item.orderNumber.includes(searchQuery.value) ||
|
||||
item.customerName.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedStatus.value) {
|
||||
result = result.filter(item => item.status === selectedStatus.value)
|
||||
}
|
||||
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
result = result.filter(item => {
|
||||
const orderDate = new Date(item.orderDate)
|
||||
const startDate = new Date(dateRange.value[0])
|
||||
const endDate = new Date(dateRange.value[1])
|
||||
return orderDate >= startDate && orderDate <= endDate
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const availableStatuses = computed(() => {
|
||||
const allStatuses = [
|
||||
{ value: 'pending', label: t('admin.orders.status.pending') },
|
||||
{ value: 'processing', label: t('admin.orders.status.processing') },
|
||||
{ value: 'shipped', label: t('admin.orders.status.shipped') },
|
||||
{ value: 'delivered', label: t('admin.orders.status.delivered') },
|
||||
{ value: 'cancelled', label: t('admin.orders.status.cancelled') }
|
||||
]
|
||||
return allStatuses
|
||||
})
|
||||
|
||||
// 方法
|
||||
const getStatusTagType = (status) => {
|
||||
const statusMap = {
|
||||
pending: 'warning',
|
||||
processing: 'info',
|
||||
shipped: 'primary',
|
||||
delivered: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return statusMap[status] || ''
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const exportOrders = () => {
|
||||
ElMessage.success('Orders exported successfully')
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
console.log('Refresh data')
|
||||
ElMessage.success('Data refreshed successfully')
|
||||
}
|
||||
|
||||
const handleOrderDetail = (row) => {
|
||||
selectedOrder.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleViewOrder = (row) => {
|
||||
selectedOrder.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleUpdateStatus = (row) => {
|
||||
selectedOrderForStatus.value = row
|
||||
statusForm.value.status = row.status
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleStatusUpdate = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要更新订单状态吗?',
|
||||
'确认更新',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
ElMessage.success('订单状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
} catch {
|
||||
// 用户取消更新
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
totalOrders.value = ordersList.value.length
|
||||
})
|
||||
|
||||
return {
|
||||
t,
|
||||
loading,
|
||||
searchQuery,
|
||||
selectedStatus,
|
||||
dateRange,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalOrders,
|
||||
detailDialogVisible,
|
||||
statusDialogVisible,
|
||||
selectedOrder,
|
||||
selectedOrderForStatus,
|
||||
statusForm,
|
||||
ordersList,
|
||||
orderStats,
|
||||
filteredOrdersList,
|
||||
availableStatuses,
|
||||
getStatusTagType,
|
||||
formatDate,
|
||||
handleExportOrders,
|
||||
handleOrderDetail,
|
||||
handleViewOrder,
|
||||
handleUpdateStatus,
|
||||
handleStatusUpdate,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-orders {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-left .title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-left .subtitle {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.order-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stat-icon.pending {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.stat-icon.completed {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.stat-icon.revenue {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 筛选器样式 */
|
||||
.orders-filters {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.orders-list {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 订单详情样式 */
|
||||
.order-detail {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.order-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.orders-filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-orders {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.orders-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.order-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,689 @@
|
|||
<template>
|
||||
<div class="admin-users">
|
||||
<div class="dashboard-header">
|
||||
<div class="header-left">
|
||||
<h2 class="title">{{ $t('admin.users.title') }}</h2>
|
||||
<p class="subtitle">Manage user accounts and permissions</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="dialogVisible = true">
|
||||
{{ $t('admin.users.add') }}
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="refresh">
|
||||
{{ $t('admin.common.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="16" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon users">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<h3>{{ userStats.total }}</h3>
|
||||
<p>{{ $t('admin.users.stats.total') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon active">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<h3>{{ userStats.active }}</h3>
|
||||
<p>{{ $t('admin.users.stats.active') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon inactive">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<h3>{{ userStats.inactive }}</h3>
|
||||
<p>{{ $t('admin.users.stats.inactive') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon vip">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<h3>{{ userStats.vip }}</h3>
|
||||
<p>{{ $t('admin.users.stats.vip') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<div class="filters">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
:placeholder="$t('admin.users.search')"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
:placeholder="$t('admin.users.status')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:label="status.label"
|
||||
:value="status.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="filters.role"
|
||||
:placeholder="$t('admin.users.role')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
:key="role.value"
|
||||
:label="role.label"
|
||||
:value="role.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
:start-placeholder="$t('admin.common.startDate')"
|
||||
:end-placeholder="$t('admin.common.endDate')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" :icon="Search" @click="search">
|
||||
{{ $t('admin.common.search') }}
|
||||
</el-button>
|
||||
<el-button @click="resetFilters">{{ $t('admin.common.reset') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table :data="tableData" style="width: 100%" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" :label="$t('admin.users.username')" width="120" />
|
||||
<el-table-column prop="email" :label="$t('admin.users.email')" min-width="200" />
|
||||
<el-table-column prop="phone" :label="$t('admin.users.phone')" width="120" />
|
||||
<el-table-column prop="role" :label="$t('admin.users.role')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRoleTagType(row.role)">
|
||||
{{ $t(`admin.users.roleOptions.${row.role}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="$t('admin.users.status')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">
|
||||
{{ $t(`admin.users.statusOptions.${row.status}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastLogin" :label="$t('admin.users.lastLogin')" width="120" />
|
||||
<el-table-column prop="loginCount" :label="$t('admin.users.loginCount')" width="100" />
|
||||
<el-table-column :label="$t('admin.users.actions')" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="View" @click="handleView(row)">
|
||||
{{ $t('admin.users.view') }}
|
||||
</el-button>
|
||||
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">
|
||||
{{ $t('admin.users.edit') }}
|
||||
</el-button>
|
||||
<el-button link type="warning" :icon="Key" @click="handleResetPassword(row)">
|
||||
{{ $t('admin.users.resetPassword') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'active'"
|
||||
link
|
||||
type="danger"
|
||||
:icon="Ban"
|
||||
@click="handleBan(row)"
|
||||
>
|
||||
{{ $t('admin.users.ban') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
type="success"
|
||||
:icon="Unlock"
|
||||
@click="handleUnban(row)"
|
||||
>
|
||||
{{ $t('admin.users.unban') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
:title="$t('admin.users.detail')"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="$t('admin.users.username')">
|
||||
{{ selectedUser?.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.email')">
|
||||
{{ selectedUser?.email }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.phone')">
|
||||
{{ selectedUser?.phone }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.role')">
|
||||
{{ selectedUser ? $t(`admin.users.roleOptions.${selectedUser.role}`) : '' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.status')">
|
||||
{{ selectedUser ? $t(`admin.users.statusOptions.${selectedUser.status}`) : '' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.loginCount')">
|
||||
{{ selectedUser?.loginCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.registerDate')">
|
||||
{{ selectedUser?.registerDate }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('admin.users.lastLogin')">
|
||||
{{ selectedUser?.lastLogin }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加/编辑用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEditing ? $t('admin.users.edit') : $t('admin.users.add')"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item :label="$t('admin.users.username')" prop="username">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.users.email')" prop="email">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.users.phone')">
|
||||
<el-input v-model="form.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.users.role')" prop="role">
|
||||
<el-select v-model="form.role" :placeholder="$t('admin.users.selectRole')" style="width: 100%">
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
:key="role.value"
|
||||
:label="role.label"
|
||||
:value="role.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('admin.users.status')" prop="status">
|
||||
<el-select v-model="form.status" :placeholder="$t('admin.users.selectStatus')" style="width: 100%">
|
||||
<el-option
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:label="status.label"
|
||||
:value="status.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ $t('admin.common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('admin.users.save') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Plus, Search, Edit, Delete, View, Refresh, User, CircleCheck, CircleClose, Star, Key, Ban, Unlock } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 1,
|
||||
username: 'zhangsan',
|
||||
email: 'zhangsan@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'vip',
|
||||
status: 'active',
|
||||
registerDate: '2024-01-15',
|
||||
lastLogin: '2024-01-15 08:20:00',
|
||||
loginCount: 156
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'lisi',
|
||||
email: 'lisi@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
registerDate: '2024-01-10',
|
||||
lastLogin: '2024-01-14 22:30:00',
|
||||
loginCount: 89
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'wangwu',
|
||||
email: 'wangwu@example.com',
|
||||
phone: '13800138003',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
registerDate: '2024-01-05',
|
||||
lastLogin: '2024-01-15 07:45:00',
|
||||
loginCount: 234
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
username: 'zhaoliu',
|
||||
email: 'zhaoliu@example.com',
|
||||
phone: '13800138004',
|
||||
role: 'user',
|
||||
status: 'inactive',
|
||||
registerDate: '2023-12-20',
|
||||
lastLogin: '2023-12-30 18:20:00',
|
||||
loginCount: 23
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
username: 'sunqi',
|
||||
email: 'sunqi@example.com',
|
||||
phone: '13800138005',
|
||||
role: 'user',
|
||||
status: 'banned',
|
||||
registerDate: '2023-12-01',
|
||||
lastLogin: '2023-11-15 16:30:00',
|
||||
loginCount: 45
|
||||
}
|
||||
])
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
role: '',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 5
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const selectedUser = ref(null)
|
||||
const formRef = ref()
|
||||
|
||||
const userStats = reactive({
|
||||
total: 1234,
|
||||
active: 1056,
|
||||
inactive: 167,
|
||||
vip: 89
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'active', label: t('admin.users.statusOptions.active') },
|
||||
{ value: 'inactive', label: t('admin.users.statusOptions.inactive') },
|
||||
{ value: 'banned', label: t('admin.users.statusOptions.banned') }
|
||||
]
|
||||
|
||||
const roleOptions = [
|
||||
{ value: 'admin', label: t('admin.users.roleOptions.admin') },
|
||||
{ value: 'user', label: t('admin.users.roleOptions.user') },
|
||||
{ value: 'vip', label: t('admin.users.roleOptions.vip') }
|
||||
]
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: 'Please enter username', trigger: 'blur' }],
|
||||
email: [{ required: true, message: 'Please enter email', trigger: 'blur' }],
|
||||
role: [{ required: true, message: 'Please select role', trigger: 'change' }],
|
||||
status: [{ required: true, message: 'Please select status', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const getStatusTagType = (status) => {
|
||||
const types = {
|
||||
active: 'success',
|
||||
inactive: 'info',
|
||||
banned: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const getRoleTagType = (role) => {
|
||||
const types = {
|
||||
admin: 'danger',
|
||||
user: 'info',
|
||||
vip: 'warning'
|
||||
}
|
||||
return types[role] || 'info'
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
// 模拟搜索
|
||||
console.log('Search with filters:', filters)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.role = ''
|
||||
filters.dateRange = []
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
// 模拟刷新
|
||||
console.log('Refresh data')
|
||||
ElMessage.success('Data refreshed successfully')
|
||||
}
|
||||
|
||||
const handleView = (row) => {
|
||||
selectedUser.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
isEditing.value = true
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleResetPassword = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`Are you sure you want to reset password for "${row.username}"?`,
|
||||
'Reset Password',
|
||||
{
|
||||
confirmButtonText: 'Yes',
|
||||
cancelButtonText: 'No',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
ElMessage.success('Password reset successfully')
|
||||
})
|
||||
}
|
||||
|
||||
const handleBan = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`Are you sure you want to ban "${row.username}"?`,
|
||||
'Ban User',
|
||||
{
|
||||
confirmButtonText: 'Yes',
|
||||
cancelButtonText: 'No',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
row.status = 'banned'
|
||||
ElMessage.success('User banned successfully')
|
||||
})
|
||||
}
|
||||
|
||||
const handleUnban = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`Are you sure you want to unban "${row.username}"?`,
|
||||
'Unban User',
|
||||
{
|
||||
confirmButtonText: 'Yes',
|
||||
cancelButtonText: 'No',
|
||||
type: 'success'
|
||||
}
|
||||
).then(() => {
|
||||
row.status = 'active'
|
||||
ElMessage.success('User unbanned successfully')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('User saved successfully')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.pageSize = size
|
||||
console.log('Page size changed to:', size)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.currentPage = page
|
||||
console.log('Current page changed to:', page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-users {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-left .title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-left .subtitle {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-icon.users {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stats-icon.active {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.stats-icon.inactive {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.stats-icon.vip {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
|
||||
.stats-info h3 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.stats-info p {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.admin-users {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filters .el-col {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stats-row .el-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.admin-users {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,24 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --port 3000 --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@
|
|||
|
||||
<LanguageToggle
|
||||
position="top-right"
|
||||
:on-language-change="handleLanguageChange"
|
||||
/>
|
||||
|
||||
<ThemeToggle
|
||||
|
|
@ -109,11 +108,7 @@ const handleMembershipClick = () => {
|
|||
console.log('会员按钮点击')
|
||||
}
|
||||
|
||||
// 处理语言切换
|
||||
const handleLanguageChange = (lang) => {
|
||||
console.log('语言切换到:', lang)
|
||||
// 语言切换已由LanguageToggle组件内部处理
|
||||
}
|
||||
// 语言切换功能已由LanguageToggle组件内部处理
|
||||
|
||||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
|
|
|
|||
|
|
@ -13,16 +13,9 @@ const props = defineProps({
|
|||
tooltipText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 语言切换回调函数
|
||||
onLanguageChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['language-change'])
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
// 支持的语言列表
|
||||
|
|
@ -51,14 +44,6 @@ function toggleLanguage() {
|
|||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('lang', nextLang.code)
|
||||
|
||||
// 触发事件
|
||||
emit('language-change', nextLang.code)
|
||||
|
||||
// 调用外部回调函数
|
||||
if (props.onLanguageChange) {
|
||||
props.onLanguageChange(nextLang.code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<LanguageToggle
|
||||
position="top-right"
|
||||
:tooltip-text="t('forgotPassword.language_toggle_tooltip')"
|
||||
@language-change="handleLanguageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -115,10 +114,7 @@ const goToRegister = () => {
|
|||
router.push('/register')
|
||||
}
|
||||
|
||||
// 处理语言切换
|
||||
const handleLanguageChange = (language) => {
|
||||
console.log('语言切换到:', language)
|
||||
}
|
||||
|
||||
|
||||
// 页面挂载时初始化认证状态
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<LanguageToggle
|
||||
position="top-right"
|
||||
:tooltip-text="t('login.language_toggle_tooltip')"
|
||||
@language-change="handleLanguageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -164,11 +163,7 @@ const goToRegister = () => {
|
|||
router.push('/register')
|
||||
}
|
||||
|
||||
// 处理语言切换
|
||||
const handleLanguageChange = (language) => {
|
||||
console.log('语言切换到:', language)
|
||||
// 这里可以添加语言切换相关的逻辑
|
||||
}
|
||||
|
||||
|
||||
// 页面挂载时初始化认证状态
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<LanguageToggle
|
||||
position="top-right"
|
||||
:tooltip-text="t('register.language_toggle_tooltip')"
|
||||
@language-change="handleLanguageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,10 +101,7 @@ const goToLogin = () => {
|
|||
router.push('/login')
|
||||
}
|
||||
|
||||
// 处理语言切换
|
||||
const handleLanguageChange = (language) => {
|
||||
console.log('语言切换到:', language)
|
||||
}
|
||||
|
||||
|
||||
// 页面挂载时初始化认证状态
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
# Design: 管理后台系统架构设计
|
||||
|
||||
## 架构模式
|
||||
|
||||
### 组件层次结构
|
||||
```
|
||||
AdminLayout.vue (根布局)
|
||||
├── Sidebar.vue (侧边栏)
|
||||
│ ├── Logo.vue (品牌标识)
|
||||
│ └── MenuItems.vue (菜单项)
|
||||
├── Header.vue (顶部导航)
|
||||
│ ├── Breadcrumb.vue (面包屑)
|
||||
│ ├── UserActions.vue (用户操作)
|
||||
│ └── Notifications.vue (通知中心)
|
||||
└── MainContent.vue (主内容区)
|
||||
└── PageContent (页面内容占位)
|
||||
```
|
||||
|
||||
### 路由架构
|
||||
```
|
||||
/login - 登录页面
|
||||
/admin - 管理后台根路径
|
||||
├── /admin/dashboard - 仪表板
|
||||
├── /admin/content-review - 内容审核
|
||||
├── /admin/orders - 订单管理
|
||||
└── /admin/users - 用户管理
|
||||
```
|
||||
|
||||
## 技术决策
|
||||
|
||||
### 1. 状态管理
|
||||
- 使用 Vue 3 Composition API + provide/inject 管理全局状态
|
||||
- 用户认证状态:isAuthenticated, userInfo
|
||||
- 主题和语言设置:theme, locale
|
||||
- 侧边栏状态:sidebarCollapsed
|
||||
|
||||
### 2. 路由策略
|
||||
- 使用 Vue Router 4 的动态路由
|
||||
- 路由守卫控制访问权限
|
||||
- 元信息配置页面标题和面包屑
|
||||
- 路由懒加载优化性能
|
||||
|
||||
### 3. 响应式设计
|
||||
- 桌面优先设计(1440px+)
|
||||
- 断点:桌面 >1024px, 平板 768-1024px, 移动 <768px
|
||||
- 侧边栏在移动端可折叠
|
||||
- 头部适配不同屏幕尺寸
|
||||
|
||||
### 4. 国际化设计
|
||||
- 中文/英文双语言支持
|
||||
- 所有用户可见文本通过 i18n 管理
|
||||
- 语言切换保持用户状态
|
||||
- 路由参数支持语言前缀
|
||||
|
||||
### 5. 样式系统
|
||||
- CSS 变量定义主题色彩
|
||||
- Scoped CSS 防止样式污染
|
||||
- 8px 网格系统统一间距
|
||||
- Element Plus 主题定制
|
||||
|
||||
## 关键组件设计
|
||||
|
||||
### Sidebar 组件
|
||||
- 可折叠/展开状态
|
||||
- 动态菜单项渲染
|
||||
- 当前路由高亮
|
||||
- 平滑动画过渡
|
||||
|
||||
### Login 组件
|
||||
- 表单验证
|
||||
- 错误处理
|
||||
- 登录状态反馈
|
||||
- 记住登录状态
|
||||
|
||||
### Header 组件
|
||||
- 面包屑导航
|
||||
- 用户信息展示
|
||||
- 快捷操作菜单
|
||||
- 通知中心
|
||||
|
||||
## 性能优化
|
||||
- 路由懒加载
|
||||
- 组件按需加载
|
||||
- 图片懒加载
|
||||
- 防抖处理 resize 事件
|
||||
|
||||
## 安全考虑
|
||||
- 路由权限控制
|
||||
- XSS 防护
|
||||
- 登录状态验证
|
||||
- 敏感操作二次确认
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Proposal: 创建管理后台系统
|
||||
|
||||
## Change ID
|
||||
create-admin-dashboard
|
||||
|
||||
## 需求概述
|
||||
为 FrontendDesigner 项目开发完整的管理后台系统,包含登录认证、仪表板和核心管理功能模块。
|
||||
|
||||
## 目标
|
||||
- 构建功能齐全的管理后台登录页面
|
||||
- 实现响应式侧边栏导航系统
|
||||
- 创建主要功能模块的页面结构
|
||||
- 遵循既定的设计系统和国际化规范
|
||||
|
||||
## 范围
|
||||
### 包含
|
||||
- 登录页面(完整的用户认证界面)
|
||||
- 管理后台主布局
|
||||
- 侧边栏导航(仪表板、内容审核、订单管理、用户管理)
|
||||
- 头部组件(包含用户信息、通知、设置等)
|
||||
- 页面占位内容
|
||||
|
||||
### 不包含
|
||||
- 实际的数据处理逻辑
|
||||
- 复杂的后端集成
|
||||
- 详细的功能实现(仅占位)
|
||||
|
||||
## 设计要求
|
||||
- 遵循项目设计规范:深紫色主题 (#6B46C1)
|
||||
- 支持中英文国际化
|
||||
- 响应式设计,适配桌面端为主
|
||||
- 使用 Element Plus 组件库
|
||||
- 采用 8px 网格系统
|
||||
- 平滑过渡动画 (200ms)
|
||||
|
||||
## 技术栈
|
||||
- Vue 3 (Composition API)
|
||||
- Element Plus 2.x
|
||||
- Vue Router 4
|
||||
- Vue I18n 9.x
|
||||
- CSS 变量 + Scoped CSS
|
||||
- Vite 构建工具
|
||||
|
||||
## 预期交付
|
||||
- 完整的管理后台页面结构
|
||||
- 登录认证流程
|
||||
- 响应式布局组件
|
||||
- 国际化文案配置
|
||||
- 项目启动和运行文档
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Spec: 管理后台认证系统
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
#### Scenario: 用户访问登录页面
|
||||
**Given** 用户访问 `/login` 路径
|
||||
**When** 页面加载完成
|
||||
**Then** 显示包含用户名、密码输入框的登录表单,支持中英文切换,提供"记住我"选项
|
||||
|
||||
#### Scenario: 用户提交登录表单
|
||||
**Given** 用户在登录表单中输入用户名和密码
|
||||
**When** 点击"登录"按钮
|
||||
**Then** 验证表单数据,如果有效则跳转到管理后台主页,否则显示错误信息
|
||||
|
||||
#### Scenario: 用户登录成功后
|
||||
**Given** 用户成功登录
|
||||
**When** 认证成功
|
||||
**Then** 保存登录状态到本地存储,跳转到 `/admin/dashboard`,更新全局用户状态
|
||||
|
||||
#### Scenario: 用户未登录访问管理页面
|
||||
**Given** 用户未登录
|
||||
**When** 尝试访问 `/admin` 路径下的任意页面
|
||||
**Then** 自动重定向到 `/login` 页面
|
||||
|
||||
#### Scenario: 用户退出登录
|
||||
**Given** 用户已登录
|
||||
**When** 点击退出按钮或清除认证状态
|
||||
**Then** 清除登录状态,重定向到 `/login` 页面
|
||||
|
||||
## 技术实现
|
||||
- 使用 Element Plus 的 Form 组件进行表单验证
|
||||
- 使用 Vue Router 的路由守卫进行权限控制
|
||||
- 使用 localStorage 持久化登录状态
|
||||
- 实现表单防抖和加载状态
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Spec: 管理后台布局系统
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
#### Scenario: 管理后台主布局渲染
|
||||
**Given** 用户已登录并访问管理后台
|
||||
**When** 页面加载
|
||||
**Then** 显示包含侧边栏、头部、主内容区的完整布局
|
||||
|
||||
#### Scenario: 侧边栏导航
|
||||
**Given** 管理后台布局
|
||||
**When** 用户点击侧边栏菜单项
|
||||
**Then** 高亮当前路由对应的菜单项,平滑跳转到对应页面
|
||||
|
||||
#### Scenario: 侧边栏折叠功能
|
||||
**Given** 管理后台布局
|
||||
**When** 用户点击折叠按钮
|
||||
**Then** 侧边栏平滑折叠/展开,保持用户偏好设置
|
||||
|
||||
#### Scenario: 头部面包屑导航
|
||||
**Given** 管理后台布局
|
||||
**When** 用户在不同页面间切换
|
||||
**Then** 显示当前页面的面包屑路径,支持点击跳转
|
||||
|
||||
#### Scenario: 用户操作菜单
|
||||
**Given** 管理后台头部
|
||||
**When** 用户点击用户头像或用户名
|
||||
**Then** 显示下拉菜单,包含用户信息、设置、退出等选项
|
||||
|
||||
#### Scenario: 响应式布局适配
|
||||
**Given** 管理后台布局在不同设备上
|
||||
**When** 屏幕尺寸变化
|
||||
**Then** 自动调整布局:桌面端显示完整侧边栏,平板端可折叠,移动端隐藏侧边栏
|
||||
|
||||
## 组件结构
|
||||
- AdminLayout.vue: 主布局容器
|
||||
- Sidebar.vue: 侧边栏导航
|
||||
- Header.vue: 头部导航
|
||||
- Breadcrumb.vue: 面包屑组件
|
||||
- UserActions.vue: 用户操作菜单
|
||||
|
||||
## 样式规范
|
||||
- 主色调: #6B46C1 (紫色)
|
||||
- 边框圆角: 8px
|
||||
- 过渡动画: all 200ms ease-in-out
|
||||
- 8px 网格间距系统
|
||||
- 使用 CSS 变量管理主题
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Spec: 管理后台功能页面
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
#### Scenario: 仪表板页面展示
|
||||
**Given** 用户访问 `/admin/dashboard`
|
||||
**When** 页面加载
|
||||
**Then** 显示包含统计卡片、图表区域、快捷操作的仪表板布局
|
||||
|
||||
#### Scenario: 内容审核页面展示
|
||||
**Given** 用户访问 `/admin/content-review`
|
||||
**When** 页面加载
|
||||
**Then** 显示内容审核列表、筛选功能、审核操作按钮的页面结构
|
||||
|
||||
#### Scenario: 订单管理页面展示
|
||||
**Given** 用户访问 `/admin/orders`
|
||||
**When** 页面加载
|
||||
**Then** 显示订单列表、搜索筛选、订单状态管理的页面结构
|
||||
|
||||
#### Scenario: 用户管理页面展示
|
||||
**Given** 用户访问 `/admin/users`
|
||||
**When** 页面加载
|
||||
**Then** 显示用户列表、用户信息管理、权限设置的页面结构
|
||||
|
||||
#### Scenario: 页面间导航切换
|
||||
**Given** 用户在任一管理页面
|
||||
**When** 通过侧边栏或面包屑切换页面
|
||||
**Then** 平滑过渡到目标页面,保持当前登录状态和用户偏好
|
||||
|
||||
#### Scenario: 页面标题和面包屑更新
|
||||
**Given** 用户切换到不同页面
|
||||
**When** 页面路由变化
|
||||
**Then** 动态更新页面标题和面包屑导航,反映当前页面位置
|
||||
|
||||
## 页面占位内容要求
|
||||
- 所有页面使用统一的页面头部
|
||||
- 基础的数据表格或卡片布局
|
||||
- 预留操作按钮和功能区域
|
||||
- 统一的空状态和加载状态
|
||||
- 响应式设计确保在不同屏幕尺寸下的良好表现
|
||||
|
||||
## 样式规范
|
||||
- 使用 Element Plus 组件库
|
||||
- 应用项目主题色彩系统
|
||||
- 8px 网格间距
|
||||
- 平滑过渡动画
|
||||
- 统一的卡片样式
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Tasks: 创建管理后台系统
|
||||
|
||||
## 阶段 1: 项目基础设置和依赖安装
|
||||
- [ ] 1.1 检查 FrontendDesigner 项目现有依赖和配置
|
||||
- [ ] 1.2 安装并配置 Element Plus UI 组件库
|
||||
- [ ] 1.3 确认 Vue Router、Vue I18n、Pinia 已正确配置
|
||||
- [ ] 1.4 配置 Element Plus 主题定制和国际化
|
||||
- [ ] 1.5 验证国际化文件和样式变量配置
|
||||
|
||||
## 阶段 2: 登录页面开发
|
||||
- [ ] 2.1 创建 Login.vue 登录页面组件
|
||||
- [ ] 2.2 实现用户认证表单(用户名/密码输入、记住我选项)
|
||||
- [ ] 2.3 添加登录验证逻辑和错误处理
|
||||
- [ ] 2.4 实现登录成功后的路由跳转
|
||||
- [ ] 2.5 完善中英文国际化文案
|
||||
|
||||
## 阶段 3: 管理后台主布局
|
||||
- [ ] 3.1 创建 AdminLayout.vue 主布局组件
|
||||
- [ ] 3.2 实现响应式侧边栏导航组件
|
||||
- [ ] 3.3 开发头部组件(包含用户信息、通知、退出等)
|
||||
- [ ] 3.4 配置路由守卫,验证用户登录状态
|
||||
- [ ] 3.5 实现布局的响应式适配
|
||||
|
||||
## 阶段 4: 功能页面创建
|
||||
- [ ] 4.1 创建 Dashboard.vue 仪表板页面(占位内容)
|
||||
- [ ] 4.2 创建 ContentReview.vue 内容审核页面(占位内容)
|
||||
- [ ] 4.3 创建 OrderManagement.vue 订单管理页面(占位内容)
|
||||
- [ ] 4.4 创建 UserManagement.vue 用户管理页面(占位内容)
|
||||
- [ ] 4.5 为每个页面添加基础的页面结构和导航
|
||||
|
||||
## 阶段 5: 导航和路由配置
|
||||
- [ ] 5.1 配置管理后台相关的路由规则
|
||||
- [ ] 5.2 实现侧边栏菜单项和路由跳转
|
||||
- [ ] 5.3 添加面包屑导航组件
|
||||
- [ ] 5.4 配置路由元信息(页面标题、权限等)
|
||||
- [ ] 5.5 实现路由切换的过渡动画
|
||||
|
||||
## 阶段 6: 样式和主题适配
|
||||
- [ ] 6.1 应用项目的紫色主题色彩系统
|
||||
- [ ] 6.2 实现 8px 网格间距系统
|
||||
- [ ] 6.3 添加平滑过渡动画效果
|
||||
- [ ] 6.4 优化按钮、卡片、表格等组件样式
|
||||
- [ ] 6.5 确保响应式设计在各尺寸下的表现
|
||||
|
||||
## 阶段 7: 测试和优化
|
||||
- [ ] 7.1 测试登录流程和权限控制
|
||||
- [ ] 7.2 验证各页面间的导航跳转
|
||||
- [ ] 7.3 检查国际化切换功能
|
||||
- [ ] 7.4 测试响应式布局在不同设备上的表现
|
||||
- [ ] 7.5 优化性能和用户体验
|
||||
|
||||
## 阶段 8: 文档和部署
|
||||
- [ ] 8.1 更新 README.md,添加管理后台使用说明
|
||||
- [ ] 8.2 添加开发指南和项目结构说明
|
||||
- [ ] 8.3 创建功能演示截图或说明
|
||||
- [ ] 8.4 验证项目启动和构建流程
|
||||
- [ ] 8.5 最终测试和代码审查
|
||||
|
||||
## 验证标准
|
||||
- 所有页面能够正常访问和跳转
|
||||
- 登录功能完整且安全
|
||||
- 国际化功能完全可用
|
||||
- 响应式设计在桌面端表现良好
|
||||
- 代码结构清晰,符合 Vue 3 最佳实践
|
||||
|
|
@ -38,6 +38,12 @@ importers:
|
|||
'@deotaland/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(vue@3.5.24)
|
||||
element-plus:
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(vue@3.5.24)
|
||||
pinia:
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6(vue@3.5.24)
|
||||
|
|
@ -63,6 +69,12 @@ importers:
|
|||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
unplugin-auto-import:
|
||||
specifier: ^20.2.0
|
||||
version: 20.2.0
|
||||
unplugin-vue-components:
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0(vue@3.5.24)
|
||||
vite:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2
|
||||
|
|
|
|||
Loading…
Reference in New Issue