项目初始化
This commit is contained in:
4
frontend/.env.development
Normal file
4
frontend/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
# 开发环境变量
|
||||
|
||||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
4
frontend/.env.production
Normal file
4
frontend/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# 生产环境变量
|
||||
|
||||
# API 基础地址(生产环境需要修改为实际地址)
|
||||
VITE_API_BASE_URL=https://api.example.com
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
5
frontend/README.md
Normal file
5
frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2085
frontend/package-lock.json
generated
Normal file
2085
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remixicon/vue": "^4.8.0",
|
||||
"axios": "^1.13.2",
|
||||
"naive-ui": "^2.43.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
}
|
||||
57
frontend/src/App.vue
Normal file
57
frontend/src/App.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<n-config-provider :theme="theme" :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-message-provider>
|
||||
<router-view />
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NConfigProvider, NMessageProvider, darkTheme, zhCN, dateZhCN } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 主题配置,默认使用亮色主题
|
||||
const theme = ref(null)
|
||||
// 可以根据需要切换为暗色主题
|
||||
// const theme = ref(darkTheme)
|
||||
|
||||
// 主题覆盖配置
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#18a058',
|
||||
primaryColorHover: '#36ad6a',
|
||||
primaryColorPressed: '#0c7a43',
|
||||
primaryColorSuppl: '#36ad6a',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
Button: {
|
||||
borderRadius: '4px'
|
||||
},
|
||||
Input: {
|
||||
borderRadius: '4px'
|
||||
},
|
||||
Card: {
|
||||
borderRadius: '8px'
|
||||
},
|
||||
Modal: {
|
||||
borderRadius: '8px'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
16
frontend/src/api/admin.ts
Normal file
16
frontend/src/api/admin.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const login = (username: string, password: string) => {
|
||||
return request({
|
||||
url: '/api/admin/login',
|
||||
method: 'post',
|
||||
data: { username, password }
|
||||
})
|
||||
}
|
||||
|
||||
export const getCurrentUser = () => {
|
||||
return request({
|
||||
url: '/api/admin/me',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
23
frontend/src/api/player.ts
Normal file
23
frontend/src/api/player.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const login = (username: string, password: string) => {
|
||||
return request({
|
||||
url: '/api/player/login',
|
||||
method: 'post',
|
||||
data: { username, password }
|
||||
})
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
return request({
|
||||
url: '/api/player/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export const getAccountInfo = () => {
|
||||
return request({
|
||||
url: '/api/player/account',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
119
frontend/src/layouts/AdminLayout.vue
Normal file
119
frontend/src/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<n-layout style="height: 100vh" has-sider>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
show-trigger
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<div style="height: 60px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #e8e8e8;">
|
||||
<h3 style="margin: 0; color: #18a058;">运营管理</h3>
|
||||
</div>
|
||||
<n-menu
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:value="activeKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header bordered style="height: 60px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div class="header-left">
|
||||
<h2 style="margin: 0;">梦幻西游运营管理系统</h2>
|
||||
</div>
|
||||
<div class="header-right" v-if="adminStore.userInfo">
|
||||
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
|
||||
<div class="user-dropdown-trigger">
|
||||
<span>{{ adminStore.userInfo.username }}</span>
|
||||
<RiArrowDownSLine style="margin-left: 4px;" />
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content style="padding: 24px;">
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
<n-layout-footer bordered style="padding: 16px; text-align: center;">
|
||||
<p style="margin: 0;">© 2026 梦幻西游一站式运营管理平台 - 运营管理系统</p>
|
||||
</n-layout-footer>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, h } from 'vue'
|
||||
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent, NLayoutFooter, NMenu, NDropdown } from 'naive-ui'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine } from '@remixicon/vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
const activeKey = ref(String(route.name))
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/admin/dashboard' }, { default: () => '工作台' }),
|
||||
key: 'AdminDashboard',
|
||||
icon: () => h(RiDashboardLine, { size: '20px' })
|
||||
}
|
||||
]
|
||||
|
||||
const userMenuOptions = [
|
||||
{
|
||||
label: '用户信息',
|
||||
key: 'userInfo',
|
||||
icon: () => h(RiUserLine, { size: '18px' })
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'd1'
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: () => h(RiLogoutBoxRLine, { size: '18px' })
|
||||
}
|
||||
]
|
||||
|
||||
const handleMenuSelect = (key: string) => {
|
||||
activeKey.value = key
|
||||
}
|
||||
|
||||
const handleUserMenuSelect = (key: string) => {
|
||||
if (key === 'logout') {
|
||||
handleLogout()
|
||||
} else if (key === 'userInfo') {
|
||||
console.log('用户信息')
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
adminStore.logout()
|
||||
router.push('/admin/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-left h2 {
|
||||
color: #18a058;
|
||||
}
|
||||
|
||||
.user-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.user-dropdown-trigger:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
39
frontend/src/layouts/PlayerLayout.vue
Normal file
39
frontend/src/layouts/PlayerLayout.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<n-layout style="height: 100vh">
|
||||
<n-layout-header bordered style="height: 60px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div class="header-left">
|
||||
<h2 style="margin: 0;">梦幻西游玩家服务中心</h2>
|
||||
</div>
|
||||
<div class="header-right" v-if="playerStore.userInfo">
|
||||
<span style="margin-right: 16px;">欢迎, {{ playerStore.userInfo.username }}</span>
|
||||
<n-button type="error" @click="handleLogout">退出登录</n-button>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content style="padding: 24px;">
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
<n-layout-footer bordered style="padding: 16px; text-align: center;">
|
||||
<p style="margin: 0;">© 2026 梦幻西游一站式运营管理平台 - 玩家服务中心</p>
|
||||
</n-layout-footer>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NLayout, NLayoutHeader, NLayoutContent, NLayoutFooter, NButton } from 'naive-ui'
|
||||
import { usePlayerStore } from '@/stores/player'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogout = async () => {
|
||||
await playerStore.logout()
|
||||
router.push('/player/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-left h2 {
|
||||
color: #18a058;
|
||||
}
|
||||
</style>
|
||||
11
frontend/src/main.ts
Normal file
11
frontend/src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
81
frontend/src/router/index.ts
Normal file
81
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/player'
|
||||
},
|
||||
{
|
||||
path: '/player/login',
|
||||
name: 'PlayerLogin',
|
||||
component: () => import('@/views/player/Login.vue'),
|
||||
meta: { title: '玩家登录' }
|
||||
},
|
||||
{
|
||||
path: '/player',
|
||||
component: () => import('@/layouts/PlayerLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'PlayerDashboard',
|
||||
component: () => import('@/views/player/Dashboard.vue'),
|
||||
meta: { title: '玩家控制台', requiresAuth: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
name: 'AdminLogin',
|
||||
component: () => import('@/views/admin/Login.vue'),
|
||||
meta: { title: '管理员登录' }
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('@/layouts/AdminLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'AdminDashboard',
|
||||
component: () => import('@/views/admin/Dashboard.vue'),
|
||||
meta: { title: '管理控制台', requiresAdminAuth: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = (to.meta.title as string) || '梦幻西游一站式运营管理平台'
|
||||
|
||||
if (to.path.startsWith('/player')) {
|
||||
if (to.path === '/player/login') {
|
||||
next()
|
||||
} else {
|
||||
const playerToken = sessionStorage.getItem('player_token')
|
||||
if (playerToken) {
|
||||
next()
|
||||
} else {
|
||||
next('/player/login')
|
||||
}
|
||||
}
|
||||
} else if (to.path.startsWith('/admin')) {
|
||||
if (to.path === '/admin/login') {
|
||||
next()
|
||||
} else {
|
||||
const adminToken = localStorage.getItem('admin_token')
|
||||
if (adminToken) {
|
||||
next()
|
||||
} else {
|
||||
next('/admin/login')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
60
frontend/src/stores/admin.ts
Normal file
60
frontend/src/stores/admin.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { login as loginApi } from '@/api/admin'
|
||||
|
||||
export const useAdminStore = defineStore('admin', () => {
|
||||
const token = ref<string | null>(localStorage.getItem('admin_token'))
|
||||
const userInfo = ref<any>(JSON.parse(localStorage.getItem('admin_userInfo') || 'null'))
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('admin_token', newToken)
|
||||
}
|
||||
|
||||
const setUserInfo = (info: any) => {
|
||||
userInfo.value = info
|
||||
localStorage.setItem('admin_userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
const clearToken = () => {
|
||||
token.value = null
|
||||
localStorage.removeItem('admin_token')
|
||||
}
|
||||
|
||||
const clearUserInfo = () => {
|
||||
userInfo.value = null
|
||||
localStorage.removeItem('admin_userInfo')
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
try {
|
||||
const response = await loginApi(username, password)
|
||||
const data = response.data
|
||||
if (data.success && data.data) {
|
||||
setToken(data.data.token)
|
||||
setUserInfo(data.data.user)
|
||||
return { success: true, message: data.message || '登录成功' }
|
||||
}
|
||||
return { success: false, message: data.message || '登录失败' }
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
return { success: false, message: error.response?.data?.message || '网络错误,请稍后重试' }
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
clearToken()
|
||||
clearUserInfo()
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
clearToken,
|
||||
clearUserInfo,
|
||||
login,
|
||||
logout
|
||||
}
|
||||
})
|
||||
61
frontend/src/stores/player.ts
Normal file
61
frontend/src/stores/player.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { login as loginApi, logout as logoutApi, getAccountInfo } from '@/api/player'
|
||||
|
||||
export const usePlayerStore = defineStore('player', () => {
|
||||
const token = ref<string | null>(sessionStorage.getItem('player_token'))
|
||||
const userInfo = ref<any>(null)
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
sessionStorage.setItem('player_token', newToken)
|
||||
}
|
||||
|
||||
const clearToken = () => {
|
||||
token.value = null
|
||||
sessionStorage.removeItem('player_token')
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await loginApi(username, password)
|
||||
if (response.success && response.data) {
|
||||
setToken(response.data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await logoutApi()
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
} finally {
|
||||
clearToken()
|
||||
userInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const response = await getAccountInfo()
|
||||
if (response.success && response.data) {
|
||||
userInfo.value = response.data
|
||||
return response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
setToken,
|
||||
clearToken,
|
||||
login,
|
||||
logout,
|
||||
fetchUserInfo
|
||||
}
|
||||
})
|
||||
38
frontend/src/utils/request.ts
Normal file
38
frontend/src/utils/request.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
if (config.url?.startsWith('/api/player/')) {
|
||||
const playerToken = sessionStorage.getItem('player_token')
|
||||
if (playerToken) {
|
||||
config.headers.Authorization = playerToken
|
||||
}
|
||||
} else if (config.url?.startsWith('/api/admin/')) {
|
||||
const adminToken = localStorage.getItem('admin_token')
|
||||
if (adminToken) {
|
||||
config.headers.Authorization = `Bearer ${adminToken}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
35
frontend/src/views/admin/Dashboard.vue
Normal file
35
frontend/src/views/admin/Dashboard.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<n-card title="欢迎来到运营管理系统">
|
||||
<n-space vertical size="large">
|
||||
<n-alert type="success" title="欢迎回来!">
|
||||
感谢您使用梦幻西游一站式运营管理平台。
|
||||
</n-alert>
|
||||
<n-statistic label="系统状态" value="正常运行">
|
||||
<template #suffix>
|
||||
<RiCheckboxCircleLine size="24px" color="#18a058" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
<n-card title="快速导航" size="small">
|
||||
<n-space>
|
||||
<n-button type="primary" disabled>用户管理</n-button>
|
||||
<n-button type="primary" disabled>工单管理</n-button>
|
||||
<n-button type="primary" disabled>公告管理</n-button>
|
||||
<n-button type="primary" disabled>数据看板</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NCard, NSpace, NAlert, NStatistic, NButton } from 'naive-ui'
|
||||
import { RiCheckboxCircleLine } from '@remixicon/vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
86
frontend/src/views/admin/Login.vue
Normal file
86
frontend/src/views/admin/Login.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<n-card title="管理员登录" style="width: 400px;">
|
||||
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="密码">
|
||||
<n-input
|
||||
v-model:value="formValue.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||
登录
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formValue = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: {
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: 'blur'
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const result = await adminStore.login(formValue.value.username, formValue.value.password)
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
router.push('/admin/dashboard')
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
</style>
|
||||
38
frontend/src/views/player/Dashboard.vue
Normal file
38
frontend/src/views/player/Dashboard.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<n-card title="账号信息" v-if="playerStore.userInfo">
|
||||
<n-descriptions bordered :column="1">
|
||||
<n-descriptions-item label="用户名">
|
||||
{{ playerStore.userInfo.username }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="账号ID">
|
||||
{{ playerStore.userInfo.accountId }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">
|
||||
{{ playerStore.userInfo.createdAt }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
<n-card title="欢迎来到玩家控制台" v-else>
|
||||
<p>正在加载账号信息...</p>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui'
|
||||
import { usePlayerStore } from '@/stores/player'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await playerStore.fetchUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
86
frontend/src/views/player/Login.vue
Normal file
86
frontend/src/views/player/Login.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<n-card title="玩家登录" style="width: 400px;">
|
||||
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="密码">
|
||||
<n-input
|
||||
v-model:value="formValue.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||
登录
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||
import { usePlayerStore } from '@/stores/player'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const playerStore = usePlayerStore()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formValue = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: {
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: 'blur'
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
const success = await playerStore.login(formValue.value.username, formValue.value.password)
|
||||
|
||||
if (success) {
|
||||
message.success('登录成功')
|
||||
router.push('/player/dashboard')
|
||||
} else {
|
||||
message.error('登录失败,请检查用户名和密码')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
</style>
|
||||
20
frontend/tsconfig.app.json
Normal file
20
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
13
frontend/vite.config.ts
Normal file
13
frontend/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user