新增账号,超管体系
This commit is contained in:
@@ -10,3 +10,14 @@ export function login(data) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function changePassword(data) {
|
||||
return request({
|
||||
url: '/auth/password',
|
||||
method: 'put',
|
||||
data: {
|
||||
old_password: data.oldPassword,
|
||||
new_password: data.newPassword
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getUsers(params) {
|
||||
return request({
|
||||
url: '/users/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: '/users/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/users/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/users/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCurrentUser() {
|
||||
return request({
|
||||
url: '/auth/me',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="修改密码"
|
||||
width="480px"
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="原密码" prop="oldPassword">
|
||||
<el-input
|
||||
v-model="form.oldPassword"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input
|
||||
v-model="form.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
placeholder="至少 6 位"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
placeholder="再次输入新密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { changePassword } from '@/api/auth'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const validateConfirm = (_rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请再次输入新密码'))
|
||||
return
|
||||
}
|
||||
if (value !== form.newPassword) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const rules = {
|
||||
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '新密码至少 6 位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validateConfirm, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.oldPassword = ''
|
||||
form.newPassword = ''
|
||||
form.confirmPassword = ''
|
||||
formRef.value?.resetFields?.()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await changePassword({
|
||||
oldPassword: form.oldPassword,
|
||||
newPassword: form.newPassword
|
||||
})
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('密码已修改')
|
||||
visible.value = false
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -55,10 +55,8 @@
|
||||
<script setup>
|
||||
import { computed, markRaw, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Document, List, Upload, Collection, House } from '@element-plus/icons-vue'
|
||||
|
||||
const STORAGE_KEY = 'dashboard_tabs_v1'
|
||||
const HOME_PATH = '/brand'
|
||||
import { Document, List, Upload, Collection, House, User } from '@element-plus/icons-vue'
|
||||
import { TABS_STORAGE_KEY, HOME_PATH } from '@/utils/tabs'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -68,7 +66,8 @@ const iconMap = markRaw({
|
||||
list: List,
|
||||
upload: Upload,
|
||||
collection: Collection,
|
||||
home: House
|
||||
home: House,
|
||||
user: User
|
||||
})
|
||||
|
||||
function resolveTabIcon(iconKey) {
|
||||
@@ -108,7 +107,7 @@ function defaultHomeTab() {
|
||||
key: HOME_PATH,
|
||||
fullPath: HOME_PATH,
|
||||
path: HOME_PATH,
|
||||
name: '',
|
||||
name: 'Home',
|
||||
title: '首页',
|
||||
icon: 'home',
|
||||
closable: false
|
||||
@@ -137,13 +136,13 @@ function persist() {
|
||||
closable: t.closable
|
||||
}))
|
||||
}
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||
sessionStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(data))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function restore() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY)
|
||||
const raw = sessionStorage.getItem(TABS_STORAGE_KEY)
|
||||
if (!raw) return false
|
||||
const parsed = JSON.parse(raw)
|
||||
const list = Array.isArray(parsed?.tabs) ? parsed.tabs : null
|
||||
|
||||
@@ -92,16 +92,41 @@
|
||||
<el-breadcrumb-item v-if="breadcrumbSection">{{ breadcrumbSection }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="breadcrumbTitle">{{ breadcrumbTitle }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<el-button
|
||||
circle
|
||||
type="primary"
|
||||
class="lux-header-power-btn"
|
||||
title="退出登录"
|
||||
aria-label="退出登录"
|
||||
@click="handleLogout"
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
popper-class="lux-user-dropdown"
|
||||
@command="handleUserMenuCommand"
|
||||
>
|
||||
<el-icon :size="18"><SwitchButton /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
circle
|
||||
type="primary"
|
||||
class="lux-header-user-btn"
|
||||
title="用户菜单"
|
||||
aria-label="用户菜单"
|
||||
>
|
||||
<el-icon :size="18"><User /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="currentUsername" disabled class="lux-user-dropdown-name">
|
||||
{{ currentUsername }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="isSuperAdminUser" command="users" :divided="!!currentUsername">
|
||||
账号管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
command="password"
|
||||
:divided="!!currentUsername && !isSuperAdminUser"
|
||||
>
|
||||
修改密码
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-header>
|
||||
<el-main class="lux-main">
|
||||
<TabsView />
|
||||
@@ -111,6 +136,8 @@
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<ChangePasswordDialog v-model="changePasswordVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,10 +145,11 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Headset, Collection, List, Upload, Document, SwitchButton, Share } from '@element-plus/icons-vue'
|
||||
import { clearToken } from '@/utils/auth'
|
||||
import { Headset, Collection, List, Upload, Document, Share, User } from '@element-plus/icons-vue'
|
||||
import { clearAuth, getUser, isSuperAdmin } from '@/utils/auth'
|
||||
import TabsView from '@/components/TabsView.vue'
|
||||
import SidebarLogo from '@/components/SidebarLogo.vue'
|
||||
import ChangePasswordDialog from '@/components/ChangePasswordDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -131,6 +159,10 @@ const isSidebarCollapsed = ref(true)
|
||||
|
||||
const openedMenus = ref([])
|
||||
|
||||
const isSuperAdminUser = computed(() => isSuperAdmin())
|
||||
const currentUsername = computed(() => getUser()?.username || '')
|
||||
const changePasswordVisible = ref(false)
|
||||
|
||||
function resolveOpenedMenus(path) {
|
||||
if (path.startsWith('/brand') || path.startsWith('/model')) return ['1']
|
||||
if (path.startsWith('/ota')) return ['upgrade']
|
||||
@@ -154,6 +186,7 @@ const railNavItems = [
|
||||
]
|
||||
|
||||
const breadcrumbTitle = computed(() => {
|
||||
if (route.path === '/home') return ''
|
||||
const t = route.meta?.title
|
||||
return typeof t === 'string' ? t : ''
|
||||
})
|
||||
@@ -163,14 +196,29 @@ const breadcrumbSection = computed(() => {
|
||||
if (path.startsWith('/brand') || path.startsWith('/model')) return '耳机管理'
|
||||
if (path.startsWith('/ota')) return '升级管理'
|
||||
if (path.startsWith('/share-code')) return '分享码'
|
||||
if (path.startsWith('/system')) return '系统管理'
|
||||
return ''
|
||||
})
|
||||
|
||||
const handleLogout = () => {
|
||||
clearToken()
|
||||
clearAuth()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function handleUserMenuCommand(command) {
|
||||
if (command === 'logout') {
|
||||
handleLogout()
|
||||
return
|
||||
}
|
||||
if (command === 'users') {
|
||||
router.push('/system/users')
|
||||
return
|
||||
}
|
||||
if (command === 'password') {
|
||||
changePasswordVisible.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import { getToken, clearToken, isTokenExpired } from '@/utils/auth'
|
||||
import { getToken, clearAuth, isTokenExpired, isSuperAdmin } from '@/utils/auth'
|
||||
import { HOME_PATH } from '@/utils/tabs'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -13,8 +14,14 @@ const routes = [
|
||||
path: '/',
|
||||
component: Layout,
|
||||
meta: { requiresAuth: true },
|
||||
redirect: '/brand',
|
||||
redirect: HOME_PATH,
|
||||
children: [
|
||||
{
|
||||
path: HOME_PATH,
|
||||
name: 'Home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: { title: '首页', icon: 'home' }
|
||||
},
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'Brand',
|
||||
@@ -38,6 +45,12 @@ const routes = [
|
||||
name: 'ShareCodeLog',
|
||||
component: () => import('@/views/share-code/log.vue'),
|
||||
meta: { title: '分享日志', icon: 'document' }
|
||||
},
|
||||
{
|
||||
path: '/system/users',
|
||||
name: 'SystemUsers',
|
||||
component: () => import('@/views/system/users/index.vue'),
|
||||
meta: { title: '账号管理', icon: 'user', requiresSuperAdmin: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -54,15 +67,20 @@ router.beforeEach((to, _from, next) => {
|
||||
|
||||
if (needAuth) {
|
||||
if (!token || isTokenExpired(token)) {
|
||||
if (token) clearToken()
|
||||
if (token) clearAuth()
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
const needSuperAdmin = to.matched.some((r) => r.meta && r.meta.requiresSuperAdmin === true)
|
||||
if (needSuperAdmin && !isSuperAdmin()) {
|
||||
next({ path: HOME_PATH })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (to.path === '/login' && token && !isTokenExpired(token)) {
|
||||
const redirect = to.query.redirect
|
||||
next(typeof redirect === 'string' && redirect ? redirect : '/')
|
||||
next({ path: HOME_PATH })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -208,19 +208,19 @@ body {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-power-btn {
|
||||
.lux-shell .lux-header-user-btn {
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.22) !important;
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-power-btn .el-icon {
|
||||
.lux-shell .lux-header-user-btn .el-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-power-btn:hover,
|
||||
.lux-shell .lux-header-power-btn:focus {
|
||||
.lux-shell .lux-header-user-btn:hover,
|
||||
.lux-shell .lux-header-user-btn:focus {
|
||||
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3) !important;
|
||||
}
|
||||
|
||||
@@ -428,11 +428,27 @@ body {
|
||||
}
|
||||
|
||||
/* 下拉菜单深色 */
|
||||
.lux-shell .el-dropdown-menu {
|
||||
.lux-shell .el-dropdown-menu,
|
||||
.lux-user-dropdown.el-popper {
|
||||
background: #1e293b !important;
|
||||
border-color: rgba(148, 163, 184, 0.12) !important;
|
||||
}
|
||||
|
||||
.lux-user-dropdown .el-dropdown-menu__item {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.lux-user-dropdown .el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
color: #e0f2fe;
|
||||
}
|
||||
|
||||
.lux-user-dropdown .lux-user-dropdown-name {
|
||||
color: #94a3b8 !important;
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 消息框深色 */
|
||||
.el-message-box {
|
||||
background: #1e293b !important;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const TOKEN_KEY = 'dashboard_access_token'
|
||||
const USER_KEY = 'dashboard_user'
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
@@ -16,6 +17,63 @@ export function clearToken() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function setUser(user) {
|
||||
if (user) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user))
|
||||
} else {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
const raw = localStorage.getItem(USER_KEY)
|
||||
if (raw) {
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
}
|
||||
return getUserFromToken()
|
||||
}
|
||||
|
||||
export function clearUser() {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
|
||||
import { clearTabsStorage } from '@/utils/tabs'
|
||||
|
||||
export function clearAuth() {
|
||||
clearToken()
|
||||
clearUser()
|
||||
clearTabsStorage()
|
||||
}
|
||||
|
||||
/** 从 JWT 解析用户信息(兜底) */
|
||||
export function getUserFromToken(token = getToken()) {
|
||||
if (!token) return null
|
||||
try {
|
||||
const part = token.split('.')[1]
|
||||
if (!part) return null
|
||||
let base64 = part.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const pad = base64.length % 4
|
||||
if (pad) base64 += '='.repeat(4 - pad)
|
||||
const payload = JSON.parse(atob(base64))
|
||||
if (payload.sub == null) return null
|
||||
return {
|
||||
id: payload.sub,
|
||||
username: payload.username || '',
|
||||
is_super_admin: !!payload.is_super_admin
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isSuperAdmin(user = getUser()) {
|
||||
return !!user?.is_super_admin
|
||||
}
|
||||
|
||||
/** 解析 JWT 过期时间(毫秒时间戳),失败返回 null */
|
||||
export function getTokenExpiresAt(token) {
|
||||
if (!token) return null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getToken, clearToken } from '@/utils/auth'
|
||||
import { getToken, clearAuth } from '@/utils/auth'
|
||||
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
@@ -44,7 +44,7 @@ request.interceptors.response.use(
|
||||
error => {
|
||||
const status = error.response?.status
|
||||
if (status === 401) {
|
||||
clearToken()
|
||||
clearAuth()
|
||||
const detail = error.response?.data?.detail
|
||||
if (detail) {
|
||||
ElMessage.warning(typeof detail === 'string' ? detail : '登录已失效')
|
||||
@@ -57,6 +57,11 @@ request.interceptors.response.use(
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (status === 403) {
|
||||
const detail = error.response?.data?.detail
|
||||
ElMessage.warning(typeof detail === 'string' ? detail : '无权限执行此操作')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
console.error('响应错误:', error)
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
return Promise.reject(error)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const TABS_STORAGE_KEY = 'dashboard_tabs_v1'
|
||||
export const HOME_PATH = '/home'
|
||||
|
||||
export function clearTabsStorage() {
|
||||
try {
|
||||
sessionStorage.removeItem(TABS_STORAGE_KEY)
|
||||
} catch {}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<el-card class="home-hero">
|
||||
<div class="home-hero-inner">
|
||||
<div class="home-hero-icon">
|
||||
<el-icon :size="28"><House /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="home-title">欢迎回来{{ username ? `,${username}` : '' }}</h1>
|
||||
<p class="home-subtitle">Luxsin CMS · 从左侧菜单或下方快捷入口开始操作</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="home-links">
|
||||
<el-card
|
||||
v-for="item in quickLinks"
|
||||
:key="item.path"
|
||||
class="home-link-card"
|
||||
shadow="hover"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<div class="home-link-inner">
|
||||
<el-icon class="home-link-icon"><component :is="item.icon" /></el-icon>
|
||||
<div>
|
||||
<div class="home-link-title">{{ item.title }}</div>
|
||||
<div class="home-link-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { House, Collection, List, Document, Share } from '@element-plus/icons-vue'
|
||||
import { getUser } from '@/utils/auth'
|
||||
|
||||
defineOptions({ name: 'Home' })
|
||||
|
||||
const router = useRouter()
|
||||
const username = computed(() => getUser()?.username || '')
|
||||
|
||||
const quickLinks = [
|
||||
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Collection },
|
||||
{ path: '/model', title: '耳机型号管理', desc: '维护耳机型号与配置', icon: List },
|
||||
{ path: '/ota', title: 'OTA 管理', desc: '固件升级包管理', icon: Document },
|
||||
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share }
|
||||
]
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.home-hero :deep(.el-card__body) {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.home-hero-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.home-hero-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #38bdf8;
|
||||
background: linear-gradient(145deg, rgba(15, 23, 42, 0.9), rgba(30, 41, 59, 0.9));
|
||||
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.2) inset;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.home-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-link-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.home-link-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.home-link-inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.home-link-icon {
|
||||
font-size: 22px;
|
||||
color: #7dd3fc;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.home-link-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-link-desc {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -33,10 +33,14 @@
|
||||
|
||||
<div class="login-card">
|
||||
<div class="logo-ring">
|
||||
<el-icon :size="26"><Headset /></el-icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 26 26">
|
||||
<path d="M0 0h26v26H0z" fill="none" />
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.607 4C21 4.617 21 5.413 21 7.004v9.994c0 1.591 0 2.387-.393 3.002q-.105.165-.235.312c-.483.546-1.249.765-2.78 1.202c-1.533.438-2.3.657-2.856.329a1.5 1.5 0 0 1-.267-.202C14 21.196 14 20.4 14 18.803V5.197c0-1.596 0-2.393.469-2.837q.121-.116.267-.202c.555-.328 1.323-.11 2.857.329c1.53.437 2.296.656 2.78 1.202q.129.147.234.312M14 4h-2.017C10.08 4 9.13 4 8.54 4.586C8.21 4.912 8.064 5.35 8 6m6 14h-2.017c-1.902 0-2.853 0-3.443-.586c-.33-.326-.476-.764-.54-1.414m2-6H3m5.5-2.5S11 11.34 11 12s-2.5 2.5-2.5 2.5" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<h1 class="title">欢迎回来</h1>
|
||||
<p class="subtitle">耳机管理平台 · 控制台登录</p>
|
||||
<p class="subtitle">Luxsin CMS</p>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -95,13 +99,13 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Headset, Key } from '@element-plus/icons-vue'
|
||||
import { login } from '@/api/auth'
|
||||
import { setToken } from '@/utils/auth'
|
||||
import { setToken, setUser } from '@/utils/auth'
|
||||
import { clearTabsStorage, HOME_PATH } from '@/utils/tabs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
@@ -128,9 +132,12 @@ const handleSubmit = async () => {
|
||||
})
|
||||
if (res.code === 1 && res.data?.access_token) {
|
||||
setToken(res.data.access_token)
|
||||
if (res.data.user) {
|
||||
setUser(res.data.user)
|
||||
}
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = route.query.redirect
|
||||
router.replace(typeof redirect === 'string' && redirect ? redirect : '/')
|
||||
clearTabsStorage()
|
||||
router.replace(HOME_PATH)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '登录失败')
|
||||
}
|
||||
@@ -217,11 +224,47 @@ const handleSubmit = async () => {
|
||||
max-width: 420px;
|
||||
padding: 44px 40px 36px;
|
||||
border-radius: 24px;
|
||||
background: #1e293b;
|
||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||
overflow: hidden;
|
||||
background: rgba(30, 41, 59, 0.38);
|
||||
backdrop-filter: blur(24px) saturate(1.5);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
0 8px 32px rgba(0, 0, 0, 0.28),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.1) inset;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 255, 255, 0.14) 0%,
|
||||
rgba(255, 255, 255, 0.04) 28%,
|
||||
transparent 52%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
border-radius: 23px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(56, 189, 248, 0.06) 0%,
|
||||
transparent 36%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-card > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo-ring {
|
||||
@@ -233,10 +276,13 @@ const handleSubmit = async () => {
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
color: #38bdf8;
|
||||
background: linear-gradient(145deg, #0f172a 0%, #1e293b 100%);
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow:
|
||||
0 2px 8px rgba(56, 189, 248, 0.15),
|
||||
0 0 0 1px rgba(56, 189, 248, 0.2) inset;
|
||||
0 2px 12px rgba(56, 189, 248, 0.18),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset,
|
||||
0 0 0 1px rgba(56, 189, 248, 0.22);
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -283,9 +329,13 @@ const handleSubmit = async () => {
|
||||
|
||||
.glass-input :deep(.el-input__wrapper) {
|
||||
border-radius: 14px;
|
||||
background-color: #0f172a !important;
|
||||
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.15) inset !important;
|
||||
border: 1px solid transparent;
|
||||
background-color: rgba(15, 23, 42, 0.32) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.04) inset !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
@@ -293,14 +343,19 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
.glass-input :deep(.el-input__wrapper:hover) {
|
||||
background-color: #1e293b !important;
|
||||
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.25) inset !important;
|
||||
background-color: rgba(30, 41, 59, 0.42) !important;
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.12) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.06) inset !important;
|
||||
}
|
||||
|
||||
.glass-input :deep(.el-input__wrapper.is-focus) {
|
||||
background-color: #0f172a !important;
|
||||
border-color: rgba(56, 189, 248, 0.45);
|
||||
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.22) !important;
|
||||
background-color: rgba(15, 23, 42, 0.45) !important;
|
||||
border-color: rgba(56, 189, 248, 0.5);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(56, 189, 248, 0.18),
|
||||
0 0 0 1px rgba(56, 189, 248, 0.35) inset !important;
|
||||
}
|
||||
|
||||
.glass-input :deep(.el-input__inner) {
|
||||
@@ -328,14 +383,22 @@ const handleSubmit = async () => {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
margin-top: 4px;
|
||||
border: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: #fff !important;
|
||||
background: #38bdf8 !important;
|
||||
box-shadow: 0 4px 16px rgba(56, 189, 248, 0.3);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(56, 189, 248, 0.92) 0%,
|
||||
rgba(34, 211, 238, 0.88) 100%
|
||||
) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
box-shadow:
|
||||
0 4px 20px rgba(56, 189, 248, 0.32),
|
||||
0 1px 0 rgba(255, 255, 255, 0.2) inset;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.2s ease,
|
||||
@@ -343,8 +406,14 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: #22d3ee !important;
|
||||
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(34, 211, 238, 0.95) 0%,
|
||||
rgba(56, 189, 248, 0.92) 100%
|
||||
) !important;
|
||||
box-shadow:
|
||||
0 6px 24px rgba(56, 189, 248, 0.42),
|
||||
0 1px 0 rgba(255, 255, 255, 0.25) inset;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<div class="users-container">
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="searchForm.username"
|
||||
placeholder="模糊查询"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<div class="card-header">
|
||||
<span class="title">账号列表</span>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建账号
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column prop="is_super_admin" label="超级管理员" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_super_admin" type="warning" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success" size="small">启用</el-tag>
|
||||
<el-tag v-else type="danger" size="small">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_login_at" label="最近登录" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.last_login_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_at" label="创建时间" min-width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.create_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleToggleStatus(row, 0)"
|
||||
>
|
||||
禁用
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleToggleStatus(row, 1)"
|
||||
>
|
||||
启用
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleChangePassword(row)">
|
||||
改密
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="新建账号"
|
||||
width="480px"
|
||||
@close="resetCreateForm"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="createForm.username" maxlength="64" placeholder="登录账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="至少 6 位"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="超级管理员">
|
||||
<el-switch v-model="createForm.is_super_admin" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleCreateSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="passwordDialogVisible"
|
||||
:title="`修改密码 - ${passwordForm.username}`"
|
||||
width="480px"
|
||||
@close="resetPasswordForm"
|
||||
>
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
v-model="passwordForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="至少 6 位"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handlePasswordSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { getUsers, createUser, updateUser, deleteUser } from '@/api/user'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const tableData = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
username: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
const createFormRef = ref(null)
|
||||
const createForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
is_super_admin: false
|
||||
})
|
||||
|
||||
const passwordDialogVisible = ref(false)
|
||||
const passwordFormRef = ref(null)
|
||||
const passwordForm = reactive({
|
||||
id: null,
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const createRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 1, max: 64, message: '长度 1-64 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少 6 位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const passwordRules = {
|
||||
password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少 6 位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUsers({
|
||||
skip: (pagination.page - 1) * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
username: searchForm.username || undefined
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else if (res.code === 2) {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.username = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handlePageChange() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetCreateForm() {
|
||||
createForm.username = ''
|
||||
createForm.password = ''
|
||||
createForm.is_super_admin = false
|
||||
createFormRef.value?.resetFields?.()
|
||||
}
|
||||
|
||||
async function handleCreateSubmit() {
|
||||
if (!createFormRef.value) return
|
||||
await createFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await createUser({
|
||||
username: createForm.username.trim(),
|
||||
password: createForm.password,
|
||||
is_super_admin: createForm.is_super_admin
|
||||
})
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('创建成功')
|
||||
createDialogVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '创建失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleChangePassword(row) {
|
||||
passwordForm.id = row.id
|
||||
passwordForm.username = row.username
|
||||
passwordForm.password = ''
|
||||
passwordDialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetPasswordForm() {
|
||||
passwordForm.id = null
|
||||
passwordForm.username = ''
|
||||
passwordForm.password = ''
|
||||
passwordFormRef.value?.resetFields?.()
|
||||
}
|
||||
|
||||
async function handlePasswordSubmit() {
|
||||
if (!passwordFormRef.value) return
|
||||
await passwordFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await updateUser(passwordForm.id, { password: passwordForm.password })
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('密码已更新')
|
||||
passwordDialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg || '更新失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleToggleStatus(row, status) {
|
||||
const action = status === 1 ? '启用' : '禁用'
|
||||
ElMessageBox.confirm(`确定要${action}账号「${row.username}」吗?`, '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await updateUser(row.id, { status })
|
||||
if (res.code === 1) {
|
||||
ElMessage.success(`${action}成功`)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || `${action}失败`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm(`确定要删除账号「${row.username}」吗?此操作不可恢复。`, '警告', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await deleteUser(row.id)
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user