mirror of
https://github.com/wangdage12/Snap.Server.Web.git
synced 2026-02-18 02:42:14 +08:00
添加部分用户管理功能和公告管理功能
This commit is contained in:
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL = http://localhost:5222/
|
||||
87
src/api/announcement.ts
Normal file
87
src/api/announcement.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 公告数据类型 */
|
||||
export interface Announcement {
|
||||
Content: string
|
||||
Id: number
|
||||
LastUpdateTime: number
|
||||
Link: string | null
|
||||
Locale: string | null
|
||||
MaxPresentVersion: string | null
|
||||
Severity: number
|
||||
Title: string
|
||||
}
|
||||
|
||||
/** 公告列表响应数据类型 */
|
||||
export interface AnnouncementListResponse {
|
||||
code: number
|
||||
data: Announcement[]
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告列表 API
|
||||
* POST /Announcement/List
|
||||
*/
|
||||
export function getAnnouncementListApi(): Promise<Announcement[]> {
|
||||
return request({
|
||||
url: '/Announcement/List',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建公告请求参数类型 */
|
||||
export interface CreateAnnouncementRequest {
|
||||
Content: string
|
||||
Title: string
|
||||
Link?: string | null
|
||||
Locale?: string | null
|
||||
MaxPresentVersion?: string | null
|
||||
Severity?: number | null
|
||||
}
|
||||
|
||||
/** 创建公告响应数据类型 */
|
||||
export interface CreateAnnouncementResponse {
|
||||
code: number
|
||||
data: {
|
||||
Id: number
|
||||
}
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建公告 API
|
||||
* POST /web-api/announcement
|
||||
* 注意:由于request.ts拦截器处理,实际返回的是data部分,即 { Id: number }
|
||||
*/
|
||||
export function createAnnouncementApi(params: CreateAnnouncementRequest): Promise<{ Id: number }> {
|
||||
return request({
|
||||
url: '/web-api/announcement',
|
||||
method: 'post',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑公告 API
|
||||
* PUT /web-api/announcement/{announcement_id}
|
||||
*/
|
||||
export function updateAnnouncementApi(id: number, params: CreateAnnouncementRequest): Promise<null> {
|
||||
return request({
|
||||
url: `/web-api/announcement/${id}`,
|
||||
method: 'put',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除公告 API
|
||||
* DELETE /web-api/announcement/{announcement_id}
|
||||
*/
|
||||
export function deleteAnnouncementApi(id: number): Promise<null> {
|
||||
return request({
|
||||
url: `/web-api/announcement/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
25
src/api/auth.ts
Normal file
25
src/api/auth.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 登录请求参数 */
|
||||
export interface WebLoginParams {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 登录返回数据 */
|
||||
export interface WebLoginResult {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
/**
|
||||
* WEB 登录 API
|
||||
* POST /web-api/login
|
||||
*/
|
||||
export function webLoginApi(data: WebLoginParams) {
|
||||
return request<WebLoginResult>({
|
||||
url: '/web-api/login',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
60
src/api/user.ts
Normal file
60
src/api/user.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 用户信息数据结构 */
|
||||
export interface UserInfo {
|
||||
CdnExpireAt: string
|
||||
GachaLogExpireAt: string
|
||||
IsLicensedDeveloper: boolean
|
||||
IsMaintainer: boolean
|
||||
NormalizedUserName: string
|
||||
UserName: string
|
||||
}
|
||||
|
||||
/** 用户列表中的用户数据结构 */
|
||||
export interface UserListItem {
|
||||
CdnExpireAt: string
|
||||
CreatedAt: string
|
||||
GachaLogExpireAt: string
|
||||
IsLicensedDeveloper: boolean
|
||||
IsMaintainer: boolean
|
||||
NormalizedUserName: string
|
||||
UserName: string
|
||||
_id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
/** API响应数据结构 */
|
||||
export interface ApiResponse<T> {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /Passport/v2/UserInfo
|
||||
*/
|
||||
export function getUserInfoApi(): Promise<UserInfo> {
|
||||
return request({
|
||||
url: '/Passport/v2/UserInfo',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* GET /web-api/users
|
||||
* @param q 搜索参数,可搜索用户名、邮箱、_id
|
||||
*/
|
||||
export function getUserListApi(q?: string): Promise<UserListItem[]> {
|
||||
const params: Record<string, any> = {}
|
||||
if (q) {
|
||||
params.q = q
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/web-api/users',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -12,13 +12,13 @@
|
||||
<span>{{ item.meta?.title }}</span>
|
||||
</template>
|
||||
|
||||
<SidebarItem :routes="item.children!" />
|
||||
<SidebarItem :routes="item.children!" :parent-path="item.path" />
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 普通菜单 -->
|
||||
<el-menu-item
|
||||
v-else
|
||||
:index="item.path"
|
||||
:index="getFullPath(item)"
|
||||
>
|
||||
<el-icon v-if="item.meta?.icon">
|
||||
<component :is="icons[item.meta.icon as keyof typeof icons]" />
|
||||
@@ -32,10 +32,24 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import * as icons from '@element-plus/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
interface Props {
|
||||
routes: RouteRecordRaw[]
|
||||
}>()
|
||||
parentPath?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
parentPath: ''
|
||||
})
|
||||
|
||||
const hasChildren = (route: RouteRecordRaw) =>
|
||||
route.children && route.children.length > 0
|
||||
|
||||
const getFullPath = (route: RouteRecordRaw) => {
|
||||
// 如果是绝对路径,直接返回
|
||||
if (route.path.startsWith('/')) {
|
||||
return route.path
|
||||
}
|
||||
// 否则拼接父级路径
|
||||
return props.parentPath ? `/${props.parentPath}/${route.path}` : `/${route.path}`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,20 @@
|
||||
@change="themeStore.toggleTheme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dropdown class="user-dropdown" @command="handleCommand">
|
||||
<span class="user-info">
|
||||
<el-avatar :size="32" :icon="UserFilled" />
|
||||
<span class="username">{{ userStore.userInfo?.UserName || '用户' }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
@@ -47,14 +61,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Fold, Expand, Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { Fold, Expand, Moon, Sunny, UserFilled, ArrowDown } from '@element-plus/icons-vue'
|
||||
import SidebarItem from '../components/SidebarItem.vue'
|
||||
import { useThemeStore } from '../stores/theme'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const isCollapse = ref(false)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const themeStore = useThemeStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 处理下拉菜单命令
|
||||
const handleCommand = (command: string) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
// 跳转到个人中心
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'logout':
|
||||
// 退出登录
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = () => (isCollapse.value = !isCollapse.value)
|
||||
|
||||
@@ -171,4 +202,19 @@ const menuRoutes = computed(() => {
|
||||
background-color: var(--main-bg);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import { useThemeStore } from './stores/theme'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
@@ -16,6 +17,8 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
import '@/router/permission'
|
||||
|
||||
// 初始化主题
|
||||
const themeStore = useThemeStore(pinia)
|
||||
// 确保在应用挂载前应用正确的主题
|
||||
|
||||
@@ -19,7 +19,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
component: () => import('@/views/user/index.vue'),
|
||||
meta: { title: '用户管理', icon: 'User' },
|
||||
},
|
||||
{
|
||||
@@ -36,6 +36,11 @@ const routes = [
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '角色管理', icon: 'UserFilled' },
|
||||
},
|
||||
{
|
||||
path: 'announcement',
|
||||
component: () => import('@/views/announcement/index.vue'),
|
||||
meta: { title: '公告管理', icon: 'Bell' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
37
src/router/permission.ts
Normal file
37
src/router/permission.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import router from './index'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 未登录
|
||||
if (!userStore.token) {
|
||||
if (to.path === '/login') {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录还去 login
|
||||
if (to.path === '/login') {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有用户信息,尝试获取
|
||||
if (!userStore.userInfo) {
|
||||
try {
|
||||
await userStore.fetchUserInfo()
|
||||
} catch (error) {
|
||||
// 获取用户信息失败,可能token已过期,跳转到登录页
|
||||
console.error('获取用户信息失败:', error)
|
||||
userStore.logout()
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
31
src/stores/user.ts
Normal file
31
src/stores/user.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUserInfoApi } from '@/api/user'
|
||||
import type { UserInfo } from '@/api/user'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || '',
|
||||
userInfo: null as UserInfo | null,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setToken(token: string) {
|
||||
this.token = token
|
||||
// 持久化存储token
|
||||
localStorage.setItem('token', token)
|
||||
},
|
||||
|
||||
async fetchUserInfo() {
|
||||
const userData = await getUserInfoApi()
|
||||
this.userInfo = userData
|
||||
return userData
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.userInfo = null
|
||||
// 清除本地存储的token(如果有)
|
||||
localStorage.removeItem('token')
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,18 +1,66 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
/** 请求拦截:自动加 Token */
|
||||
request.interceptors.request.use((config) => {
|
||||
// 可加 token
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
/** 响应拦截:兼容 code / retcode */
|
||||
request.interceptors.response.use(
|
||||
(res) => res.data,
|
||||
(err) => Promise.reject(err)
|
||||
(response) => {
|
||||
const res = response.data
|
||||
|
||||
// 登录接口:code
|
||||
if ('code' in res) {
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(res.message)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
// 用户信息接口:retcode
|
||||
if ('retcode' in res) {
|
||||
if (res.retcode !== 0) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(res.message)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
// 兜底
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
// 处理401未授权错误
|
||||
if (error.response?.status === 401) {
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
|
||||
// 如果不在登录页,则跳转到登录页
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
|
||||
577
src/views/announcement/index.vue
Normal file
577
src/views/announcement/index.vue
Normal file
@@ -0,0 +1,577 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>公告管理</span>
|
||||
<div>
|
||||
<el-button type="success" @click="handleCreate">创建公告</el-button>
|
||||
<el-button type="primary" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="announcementList"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="Id" label="ID" width="80" />
|
||||
<el-table-column prop="Title" label="标题" width="200" />
|
||||
<el-table-column prop="Content" label="内容" show-overflow-tooltip />
|
||||
<el-table-column prop="Severity" label="严重等级" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getSeverityType(row.Severity)"
|
||||
size="small"
|
||||
>
|
||||
{{ getSeverityText(row.Severity) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Link" label="链接" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.Link"
|
||||
:href="row.Link"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
查看详情
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="LastUpdateTime" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.LastUpdateTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 公告详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="公告详情"
|
||||
width="50%"
|
||||
>
|
||||
<div
|
||||
v-if="currentAnnouncement"
|
||||
:class="['announcement-box', getSeverityClass(currentAnnouncement.Severity)]"
|
||||
>
|
||||
<!-- 标题 -->
|
||||
<div class="announcement-title">
|
||||
{{ currentAnnouncement.Title }}
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="announcement-content">
|
||||
<pre>{{ currentAnnouncement.Content }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="announcement-footer">
|
||||
<span class="announcement-time">
|
||||
{{ formatTime(currentAnnouncement.LastUpdateTime) }}
|
||||
</span>
|
||||
|
||||
<el-link
|
||||
v-if="currentAnnouncement.Link"
|
||||
:href="currentAnnouncement.Link"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
查看详情
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 创建公告弹窗 -->
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="创建公告"
|
||||
width="60%"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="Title">
|
||||
<el-input
|
||||
v-model="createForm.Title"
|
||||
placeholder="请输入公告标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容" prop="Content">
|
||||
<el-input
|
||||
v-model="createForm.Content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入公告内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="链接">
|
||||
<el-input
|
||||
v-model="createForm.Link"
|
||||
placeholder="可选,详细信息链接"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="严重等级">
|
||||
<el-select
|
||||
v-model="createForm.Severity"
|
||||
placeholder="请选择严重等级"
|
||||
clearable
|
||||
>
|
||||
<el-option label="信息" :value="0" />
|
||||
<el-option label="低" :value="1" />
|
||||
<el-option label="中" :value="2" />
|
||||
<el-option label="高" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="语言代码">
|
||||
<el-input
|
||||
v-model="createForm.Locale"
|
||||
placeholder="可选,如 zh-CN, en-US"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大显示版本">
|
||||
<el-input
|
||||
v-model="createForm.MaxPresentVersion"
|
||||
placeholder="可选,最大显示版本号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="createLoading"
|
||||
@click="handleCreateSubmit"
|
||||
>
|
||||
创建
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑公告弹窗 -->
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
title="编辑公告"
|
||||
width="60%"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="createRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="Title">
|
||||
<el-input
|
||||
v-model="editForm.Title"
|
||||
placeholder="请输入公告标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容" prop="Content">
|
||||
<el-input
|
||||
v-model="editForm.Content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入公告内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="链接">
|
||||
<el-input
|
||||
v-model="editForm.Link"
|
||||
placeholder="可选,详细信息链接"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="严重等级">
|
||||
<el-select
|
||||
v-model="editForm.Severity"
|
||||
placeholder="请选择严重等级"
|
||||
clearable
|
||||
>
|
||||
<el-option label="信息" :value="0" />
|
||||
<el-option label="低" :value="1" />
|
||||
<el-option label="中" :value="2" />
|
||||
<el-option label="高" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="语言代码">
|
||||
<el-input
|
||||
v-model="editForm.Locale"
|
||||
placeholder="可选,如 zh-CN, en-US"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大显示版本">
|
||||
<el-input
|
||||
v-model="editForm.MaxPresentVersion"
|
||||
placeholder="可选,最大显示版本号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="editLoading"
|
||||
@click="handleEditSubmit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getAnnouncementListApi, createAnnouncementApi, updateAnnouncementApi, deleteAnnouncementApi, type Announcement, type CreateAnnouncementRequest } from '@/api/announcement'
|
||||
|
||||
const loading = ref(false)
|
||||
const announcementList = ref<Announcement[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const currentAnnouncement = ref<Announcement | null>(null)
|
||||
|
||||
// 创建公告相关
|
||||
const createDialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
|
||||
// 编辑公告相关
|
||||
const editDialogVisible = ref(false)
|
||||
const editLoading = ref(false)
|
||||
const editFormRef = ref<FormInstance>()
|
||||
const currentEditId = ref<number | null>(null)
|
||||
|
||||
const createForm = reactive<CreateAnnouncementRequest>({
|
||||
Title: '',
|
||||
Content: '',
|
||||
Link: '',
|
||||
Locale: '',
|
||||
MaxPresentVersion: '',
|
||||
Severity: 0,
|
||||
})
|
||||
|
||||
const editForm = reactive<CreateAnnouncementRequest>({
|
||||
Title: '',
|
||||
Content: '',
|
||||
Link: '',
|
||||
Locale: '',
|
||||
MaxPresentVersion: '',
|
||||
Severity: 0,
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
Title: [
|
||||
{ required: true, message: '请输入公告标题', trigger: 'blur' },
|
||||
{ min: 1, max: 200, message: '标题长度应为 1 到 200 个字符', trigger: 'blur' },
|
||||
],
|
||||
Content: [
|
||||
{ required: true, message: '请输入公告内容', trigger: 'blur' },
|
||||
{ min: 1, max: 2000, message: '内容长度应为 1 到 2000 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
const getAnnouncementList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getAnnouncementListApi()
|
||||
announcementList.value = data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取公告列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getAnnouncementList()
|
||||
}
|
||||
|
||||
const handleView = (announcement: Announcement) => {
|
||||
currentAnnouncement.value = announcement
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const getSeverityType = (severity: number) => {
|
||||
if (severity === 0) return 'info'
|
||||
if (severity === 1) return 'success'
|
||||
if (severity === 2) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const getSeverityText = (severity: number) => {
|
||||
if (severity === 0) return '信息'
|
||||
if (severity === 1) return '低'
|
||||
if (severity === 2) return '中'
|
||||
return '高'
|
||||
}
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
return new Date(timestamp * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
const getSeverityClass = (severity: number) => {
|
||||
if (severity === 0) return 'announcement-box-info'
|
||||
if (severity === 1) return 'announcement-box-success'
|
||||
if (severity === 2) return 'announcement-box-warning'
|
||||
return 'announcement-box-danger'
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
// 重置表单
|
||||
Object.assign(createForm, {
|
||||
Title: '',
|
||||
Content: '',
|
||||
Link: '',
|
||||
Locale: '',
|
||||
MaxPresentVersion: '',
|
||||
Severity: 0,
|
||||
})
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
if (!createFormRef.value) return
|
||||
|
||||
try {
|
||||
await createFormRef.value.validate()
|
||||
createLoading.value = true
|
||||
|
||||
// 由于request.ts拦截器已经处理了code字段,这里直接返回data
|
||||
const result = await createAnnouncementApi(createForm)
|
||||
|
||||
// result 直接就是 { Id: number }
|
||||
if (result && result.Id) {
|
||||
ElMessage.success('公告创建成功')
|
||||
createDialogVisible.value = false
|
||||
// 刷新列表
|
||||
await getAnnouncementList()
|
||||
} else {
|
||||
ElMessage.error('创建公告失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('创建公告失败')
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (announcement: Announcement) => {
|
||||
currentEditId.value = announcement.Id
|
||||
// 填充表单数据
|
||||
Object.assign(editForm, {
|
||||
Title: announcement.Title,
|
||||
Content: announcement.Content,
|
||||
Link: announcement.Link || '',
|
||||
Locale: announcement.Locale || '',
|
||||
MaxPresentVersion: announcement.MaxPresentVersion || '',
|
||||
Severity: announcement.Severity,
|
||||
})
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
if (!editFormRef.value || !currentEditId.value) return
|
||||
|
||||
try {
|
||||
await editFormRef.value.validate()
|
||||
editLoading.value = true
|
||||
|
||||
await updateAnnouncementApi(currentEditId.value, editForm)
|
||||
|
||||
ElMessage.success('公告更新成功')
|
||||
editDialogVisible.value = false
|
||||
// 刷新列表
|
||||
await getAnnouncementList()
|
||||
} catch (error) {
|
||||
ElMessage.error('更新公告失败')
|
||||
} finally {
|
||||
editLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (announcement: Announcement) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除公告"${announcement.Title}"吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
await deleteAnnouncementApi(announcement.Id)
|
||||
ElMessage.success('公告删除成功')
|
||||
// 刷新列表
|
||||
await getAnnouncementList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除公告失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAnnouncementList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 公告正文容器 */
|
||||
.announcement-box {
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--announcement-bg);
|
||||
color: var(--announcement-text);
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
/* 不同严重等级的公告背景色和文字色 */
|
||||
.announcement-box-info {
|
||||
background-color: var(--announcement-bg-info);
|
||||
color: var(--announcement-text-info);
|
||||
}
|
||||
.announcement-box-success {
|
||||
background-color: var(--announcement-bg-success);
|
||||
color: var(--announcement-text-success);
|
||||
}
|
||||
.announcement-box-warning {
|
||||
background-color: var(--announcement-bg-warning);
|
||||
color: var(--announcement-text-warning);
|
||||
}
|
||||
.announcement-box-danger {
|
||||
background-color: var(--announcement-bg-danger);
|
||||
color: var(--announcement-text-danger);
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.announcement-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 内容 */
|
||||
.announcement-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.announcement-content pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.announcement-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 时间 */
|
||||
.announcement-time {
|
||||
color: var(--announcement-time);
|
||||
}
|
||||
|
||||
/* ===== 明亮模式变量 ===== */
|
||||
:root {
|
||||
--announcement-bg: #eaf3ff;
|
||||
--announcement-text: #303133;
|
||||
--announcement-time: #909399;
|
||||
|
||||
--announcement-bg-info: #e1e1e1;
|
||||
--announcement-text-info: #000000;
|
||||
|
||||
--announcement-bg-success: #f0f9eb;
|
||||
--announcement-text-success: #3a7a3a;
|
||||
|
||||
--announcement-bg-warning: #fdf6ec;
|
||||
--announcement-text-warning: #b26a00;
|
||||
|
||||
--announcement-bg-danger: #fef0f0;
|
||||
--announcement-text-danger: #a94442;
|
||||
}
|
||||
|
||||
/* ===== 暗色模式(Element Plus) ===== */
|
||||
html.dark {
|
||||
--announcement-bg: #1f1f1f;
|
||||
--announcement-text: #e5eaf3;
|
||||
--announcement-time: #a3a6ad;
|
||||
|
||||
--announcement-bg-info: #575e64;
|
||||
--announcement-text-info: #ffffff;
|
||||
|
||||
--announcement-bg-success: #1e2b22;
|
||||
--announcement-text-success: #a5d6a7;
|
||||
|
||||
--announcement-bg-warning: #2c211b;
|
||||
--announcement-text-warning: #ffd54f;
|
||||
|
||||
--announcement-bg-danger: #2a1a1a;
|
||||
--announcement-text-danger: #ef9a9a;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,128 @@
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h1>Login Page</h1>
|
||||
<!-- Login form goes here -->
|
||||
<el-card class="login-card">
|
||||
<h2 class="title">系统登录</h2>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input
|
||||
v-model="loginForm.email"
|
||||
placeholder="请输入邮箱"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { webLoginApi } from '@/api/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 表单实例
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
|
||||
// loading 状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const loginForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 校验规则
|
||||
const rules: FormRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少 6 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 登录方法
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await webLoginApi(loginForm)
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 处理响应数据结构
|
||||
const tokenData = response.data || response
|
||||
userStore.setToken(tokenData.access_token)
|
||||
|
||||
// 获取用户信息
|
||||
await userStore.fetchUserInfo()
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push({ path: '/' })
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '登录失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #409eff, #66b1ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
188
src/views/user/index.vue
Normal file
188
src/views/user/index.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<!-- 搜索栏和用户统计并排 -->
|
||||
<div class="search-statistics-row">
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入用户名、邮箱或ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 用户统计 -->
|
||||
<div class="statistics" v-if="!loading && displayUserList.length > 0">
|
||||
<el-statistic title="当前显示用户数" :value="displayUserList.length" />
|
||||
<el-statistic title="运维人员" :value="maintainerCount" />
|
||||
<el-statistic title="开发者" :value="developerCount" />
|
||||
<el-statistic v-if="isSearchMode" title="搜索模式" value="进行中" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table
|
||||
:data="displayUserList"
|
||||
style="width: 100%"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载用户数据..."
|
||||
>
|
||||
<el-table-column prop="_id" label="ID" width="120" />
|
||||
<el-table-column prop="UserName" label="用户名" min-width="150" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column label="角色" width="150">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.IsMaintainer" type="danger">运维</el-tag>
|
||||
<el-tag v-if="scope.row.IsLicensedDeveloper" type="success">开发者</el-tag>
|
||||
<el-tag v-if="!scope.row.IsMaintainer && !scope.row.IsLicensedDeveloper" type="info">普通用户</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="CreatedAt" label="注册时间" width="180" />
|
||||
<el-table-column label="权限状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.IsMaintainer || scope.row.IsLicensedDeveloper ? 'success' : 'info'">
|
||||
{{ scope.row.IsMaintainer || scope.row.IsLicensedDeveloper ? '高权限' : '普通' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && displayUserList.length === 0" description="暂无用户数据" />
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserListApi, type UserListItem } from '@/api/user'
|
||||
|
||||
interface SearchForm {
|
||||
keyword: string
|
||||
}
|
||||
|
||||
const searchForm = reactive<SearchForm>({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const userList = ref<UserListItem[]>([])
|
||||
const loading = ref(false)
|
||||
const isSearchMode = ref(false)
|
||||
|
||||
// 显示的用户列表(根据是否在搜索模式决定显示全部还是搜索结果)
|
||||
const displayUserList = computed(() => {
|
||||
return userList.value
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const maintainerCount = computed(() =>
|
||||
userList.value.filter(user => user.IsMaintainer).length
|
||||
)
|
||||
|
||||
const developerCount = computed(() =>
|
||||
userList.value.filter(user => user.IsLicensedDeveloper).length
|
||||
)
|
||||
|
||||
// 获取用户列表
|
||||
async function fetchUserList(searchKeyword?: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getUserListApi(searchKeyword)
|
||||
userList.value = data
|
||||
|
||||
if (searchKeyword) {
|
||||
ElMessage.success(`搜索完成,找到 ${data.length} 个匹配的用户`)
|
||||
isSearchMode.value = true
|
||||
} else {
|
||||
ElMessage.success('用户列表加载成功')
|
||||
isSearchMode.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败,请检查权限设置')
|
||||
userList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!searchForm.keyword.trim()) {
|
||||
ElMessage.warning('请输入搜索关键词')
|
||||
return
|
||||
}
|
||||
fetchUserList(searchForm.keyword.trim())
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
isSearchMode.value = false
|
||||
fetchUserList() // 重新获取全部用户列表
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
if (isSearchMode.value && searchForm.keyword) {
|
||||
fetchUserList(searchForm.keyword)
|
||||
} else {
|
||||
fetchUserList()
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
ElMessage.info('新增用户功能待实现')
|
||||
}
|
||||
|
||||
function handleEdit(row: UserListItem) {
|
||||
ElMessage.info(`编辑用户 ${row.UserName} 功能待实现`)
|
||||
}
|
||||
|
||||
function handleDelete(row: UserListItem) {
|
||||
ElMessage.info(`删除用户 ${row.UserName} 功能待实现`)
|
||||
}
|
||||
|
||||
// 页面加载时获取用户列表
|
||||
onMounted(() => {
|
||||
fetchUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-management {
|
||||
padding: 24px;
|
||||
}
|
||||
/* 新增flex布局 */
|
||||
.search-statistics-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.search-form {
|
||||
/* 保持原有样式,可根据需要调整宽度 */
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.statistics {
|
||||
margin-left: 32px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user