This commit is contained in:
jxxghp
2024-07-06 17:53:51 +08:00
parent 81fb44da80
commit b2e1fe314f
13 changed files with 373 additions and 207 deletions

View File

@@ -122,8 +122,8 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
export interface NavMenu extends NavLink {
header: string
admin: boolean
description?: string
permission?: string
}
// 👉 Vertical nav group

View File

@@ -18,18 +18,65 @@ const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// Vuex Storesuperuser
const superUser = store.state.auth.superUser
//
let superUser = store.state.auth.superUser
//
const permissions = store.state.auth.permissions
//
function hasPermission(permission: string | null = null) {
if (!permission) return true
// permission.'user.create'
const permissionList = permission.split('.')
let permissions_obj = permissions
for (const element of permissionList) {
if (!permissions_obj[element]) {
return false
} else if (typeof permissions_obj[element] === 'object') {
permissions_obj = permissions_obj[element]
} else {
return true
}
}
return false
}
//
const startMenus = ref<NavMenu[]>([])
//
const discoveryMenus = ref<NavMenu[]>([])
//
const subscribeMenus = ref<NavMenu[]>([])
//
const organizeMenus = ref<NavMenu[]>([])
//
const systemMenus = ref<NavMenu[]>([])
//
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
return SystemNavMenus.filter(
(item: NavMenu) => item.header === header && (superUser || hasPermission(item.permission)),
)
}
//
function goBack() {
history.back()
}
onMounted(() => {
//
startMenus.value = getMenuList('开始')
discoveryMenus.value = getMenuList('发现')
subscribeMenus.value = getMenuList('订阅')
organizeMenus.value = getMenuList('整理')
systemMenus.value = getMenuList('系统')
})
</script>
<template>
@@ -61,36 +108,39 @@ function goBack() {
</template>
<template #vertical-nav-content>
<VerticalNavLink v-for="item in getMenuList('开始')" :item="item" />
<VerticalNavLink v-for="item in startMenus" :item="item" />
<!-- 👉 发现 -->
<VerticalNavSectionTitle
v-if="discoveryMenus.length > 0"
:item="{
heading: '发现',
}"
/>
<VerticalNavLink v-for="item in getMenuList('发现')" :item="item" />
<VerticalNavLink v-for="item in discoveryMenus" :item="item" />
<!-- 👉 订阅 -->
<VerticalNavSectionTitle
v-if="subscribeMenus.length > 0"
:item="{
heading: '订阅',
}"
/>
<VerticalNavLink v-for="item in getMenuList('订阅')" :item="item" />
<VerticalNavLink v-for="item in subscribeMenus" :item="item" />
<!-- 👉 整理 -->
<VerticalNavSectionTitle
v-if="organizeMenus.length > 0"
:item="{
heading: '整理',
}"
/>
<VerticalNavLink v-for="item in getMenuList('整理')" :item="item" />
<VerticalNavLink v-for="item in organizeMenus" :item="item" />
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="superUser"
v-if="systemMenus.length > 0"
:item="{
heading: '系统',
}"
/>
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
<VerticalNavLink v-for="item in systemMenus" :item="item" />
</template>
<template #after-vertical-nav-items />

View File

@@ -111,11 +111,11 @@ watch(isCompactMode, value => {
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem v-if="superUser" link @click="router.push('/setting?tab=account')">
<VListItem link @click="router.push('/profile')">
<template #prepend>
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
</template>
<VListItemTitle>设定</VListItemTitle>
<VListItemTitle>个人信息</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
@@ -123,7 +123,7 @@ watch(isCompactMode, value => {
<template #prepend>
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
</template>
<VListItemTitle>帮助</VListItemTitle>
<VListItemTitle>帮助文档</VListItemTitle>
</VListItem>
<!-- Divider -->

View File

@@ -1,18 +1,18 @@
<script lang="ts" setup>
import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue'
import DefaultLayout from './components/DefaultLayout.vue'
const route = useRoute()
</script>
<template>
<DefaultLayoutWithVerticalNav>
<DefaultLayout>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" v-if="route.meta.keepAlive" :key="route.fullPath" />
</keep-alive>
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
</router-view>
</DefaultLayoutWithVerticalNav>
</DefaultLayout>
</template>
<style lang="scss">

View File

@@ -163,9 +163,10 @@ function login() {
const avatar = response.avatar
const level = response.level
const remember = form.value.remember
const permissions = response.permissions
// 更新token和remember状态到Vuex Store
store.dispatch('auth/login', { token, remember, superUser, userName, avatar, level })
store.dispatch('auth/login', { token, remember, superUser, userName, avatar, level, permissions })
// 登录后处理
afterLogin(superUser)

9
src/pages/profile.vue Normal file
View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
import UserProfileView from '@/views/user/UserProfileView.vue'
</script>
<template>
<div>
<UserProfileView />
</div>
</template>

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router'
import router from '@/router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
@@ -38,13 +37,6 @@ function jumpTab(tab: string) {
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 用户 -->
<VWindowItem value="account">
<transition name="fade-slide" appear>
<AccountSettingAccount />
</transition>
</VWindowItem>
<!-- 连接 -->
<VWindowItem value="system">
<transition name="fade-slide" appear>

9
src/pages/user.vue Normal file
View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
import UserListView from '@/views/user/UserListView.vue'
</script>
<template>
<div>
<UserListView />
</div>
</template>

View File

@@ -90,6 +90,22 @@ const router = createRouter({
requiresAuth: true,
},
},
{
path: '/user',
component: () => import('../pages/user.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
},
},
{
path: '/profile',
component: () => import('../pages/profile.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
},
},
{
path: '/plugins',
component: () => import('../pages/plugin.vue'),

View File

@@ -5,21 +5,21 @@ export const SystemNavMenus = [
icon: 'mdi-home-outline',
to: '/dashboard',
header: '开始',
admin: false,
permission: 'dashboard',
},
{
title: '推荐',
icon: 'mdi-star-outline',
to: '/ranking',
header: '发现',
admin: false,
permission: 'ranking',
},
{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
header: '发现',
admin: false,
permission: 'resource.search',
},
{
title: '电影',
@@ -27,7 +27,7 @@ export const SystemNavMenus = [
icon: 'mdi-movie-open-outline',
to: '/subscribe/movie',
header: '订阅',
admin: false,
permission: 'subscribe.movie',
},
{
title: '电视剧',
@@ -35,7 +35,7 @@ export const SystemNavMenus = [
icon: 'mdi-television',
to: '/subscribe/tv',
header: '订阅',
admin: false,
permission: 'subscribe.tv',
},
{
title: '日历',
@@ -43,49 +43,56 @@ export const SystemNavMenus = [
icon: 'mdi-calendar',
to: '/calendar',
header: '订阅',
admin: false,
permission: 'subscribe.calendar',
},
{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
header: '整理',
admin: false,
permission: 'downloading.view',
},
{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
header: '整理',
admin: true,
permission: 'admin',
},
{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
header: '整理',
admin: true,
permission: 'admin',
},
{
title: '插件',
icon: 'mdi-apps',
to: '/plugins',
header: '系统',
admin: true,
permission: 'admin',
},
{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
header: '系统',
admin: true,
permission: 'admin',
},
{
title: '用户管理',
icon: 'mdi-account-group',
to: '/user',
header: '系统',
permission: 'usermanage',
},
{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
header: '系统',
admin: true,
permission: 'admin',
},
]
@@ -120,12 +127,6 @@ export const UserfulMenus = [
// 设定标签页
export const SettingTabs = [
{
title: '用户',
icon: 'mdi-account',
tab: 'account',
description: '个人信息、用户管理、修改密码、双重认证',
},
{
title: '连接',
icon: 'mdi-server-network',

View File

@@ -9,6 +9,7 @@ interface AuthState {
avatar: string
originalPath: string | null
level: number
permissions: { [key: string]: any }
}
// 定义根状态类型
@@ -16,17 +17,40 @@ interface RootState {
auth: AuthState
}
// 导出模块
// 用户信息模块
const authModule: Module<AuthState, RootState> = {
namespaced: true,
state: {
token: null,
remember: false,
superUser: false,
userName: '',
avatar: '',
originalPath: null,
level: 1,
token: null, // 用户令牌
remember: false, // 记住我
superUser: false, // 超级管理员
userName: '', // 用户名
avatar: '', // 头像
originalPath: null, // 原始路径
level: 1, // 用户认证等级 1-未认证 2-已认证
permissions: {
admin: false, // 管理员
usermanage: false, // 用户管理
dashboard: true, //仪表板
ranking: true, // 推荐榜单
resource: {
search: false, // 搜索站点资源
download: false, // 下载站点资源
},
subscribe: {
movie: true, // 查看电影订阅
tv: true, // 电视剧订阅
request: true, // 提交订阅请求
autopass: false, // 订阅请求自动批准
approve: false, // 审批订阅请求
calendar: true, // 查看订阅日历
manage: false, // 管理所有订阅
},
downloading: {
view: true, // 查看正在下载任务
manager: false, // 管理正在下载任务
},
},
},
mutations: {
setToken(state, token: string) {
@@ -53,15 +77,19 @@ const authModule: Module<AuthState, RootState> = {
setLevel(state, level: number) {
state.level = level
},
setPermissions(state, permissions: object) {
state.permissions = permissions
},
},
actions: {
login({ commit }, { token, remember, superUser, userName, avatar, level }) {
login({ commit }, { token, remember, superUser, userName, avatar, level, permissions }) {
commit('setToken', token)
commit('setRemember', remember)
commit('setSuperUser', superUser)
commit('setUserName', userName)
commit('setAvatar', avatar)
commit('setLevel', level)
commit('setPermissions', permissions)
},
logout({ commit }) {
commit('clearToken')
@@ -76,6 +104,7 @@ const authModule: Module<AuthState, RootState> = {
getAvatar: state => state.avatar,
getOriginalPath: state => state.originalPath,
getLevel: state => state.level,
getPermissions: state => state.permissions,
},
}

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { VForm } from 'vuetify/lib/components/index.mjs'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
// 提示框
const $toast = useToast()
// 新增用户窗口
const addUserDialog = ref(false)
// 新增用户表单
const userForm = reactive({
name: '',
password: '',
email: '',
})
// 当前用户信息
const accountInfo = ref<User>({
id: 0,
name: '',
password: '',
email: '',
is_active: false,
is_superuser: false,
avatar: '',
is_otp: false,
})
// 所有用户信息
const allUsers = ref<User[]>([])
// 调用API加载当前用户数据
async function loadAccountInfo() {
try {
const user: User = await api.get('user/current')
console.log(user)
accountInfo.value = user
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1
} catch (error) {
console.log(error)
}
}
// 调用API查询所有用户
async function loadAllUsers() {
try {
const result: User[] = await api.get('/user/')
allUsers.value = result
} catch (error) {
console.log(error)
}
}
// 删除用户
async function deleteUser(user: User) {
try {
const result: { [key: string]: any } = await api.delete(`user/${user.name}`)
if (result.success) {
$toast.success('用户删除成功!')
loadAllUsers()
} else {
$toast.error(`用户删除失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
// 冻结用户
async function deactivateUser(user: User) {
try {
user.is_active = !user.is_active
const result: { [key: string]: any } = await api.put('user/', user)
if (result.success) {
$toast.success('用户冻结成功!')
loadAllUsers()
} else {
$toast.error(`用户冻结失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
// 新增用户
async function addUser() {
if (!userForm.name || !userForm.password || !userForm.email) {
$toast.error('请填写完整信息!')
return
}
try {
const result: { [key: string]: any } = await api.post('user/', userForm)
if (result.success) {
$toast.success('用户新增成功!')
loadAllUsers()
addUserDialog.value = false
} else {
$toast.error(`用户新增失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
// 加载当前用户数据
onMounted(() => {
loadAccountInfo()
loadAllUsers()
})
</script>
<template>
<div>
<VRow>
<VCol v-if="accountInfo.is_superuser" cols="12">
<!-- 👉 Accounts -->
<VCard title="所有用户">
<template #append>
<IconBtn @click.stop="addUserDialog = true">
<VIcon icon="mdi-plus" />
</IconBtn>
</template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">用户名</th>
<th scope="col">邮箱</th>
<th scope="col">状态</th>
<th scope="col">管理员</th>
<th scope="col" class="w-5" />
</tr>
</thead>
<tbody>
<tr v-for="user in allUsers" :key="user.name">
<td>
{{ user.name }}
</td>
<td>{{ user.email }}</td>
<td>
<VChip v-if="user.is_active" color="success" text-color="white"> 激活 </VChip>
<VChip v-else color="error" text-color="white"> 冻结 </VChip>
</td>
<td>{{ user.is_superuser ? '是' : '否' }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="deactivateUser(user)">
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{ user.is_active ? '冻结' : '解冻' }}
</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="error" @click="deleteUser(user)">
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</VCol>
</VRow>
<!-- =弹窗 -->
<VDialog v-model="addUserDialog" max-width="50rem" persistent z-index="1010">
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VTextField v-model="userForm.name" label="用户名" :rules="[requiredValidator]" />
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="userForm.email" :rules="[requiredValidator]" label="邮箱" />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false"> 取消 </VBtn>
<VSpacer />
<VBtn @click="addUser"> 确定 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
</template>

View File

@@ -2,7 +2,6 @@
import { useToast } from 'vue-toast-notification'
import QrcodeVue from 'qrcode.vue'
import { VForm } from 'vuetify/lib/components/index.mjs'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
@@ -13,7 +12,6 @@ const display = useDisplay()
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
const isPasswordVisible = ref(false)
const newPassword = ref('')
const confirmPassword = ref('')
@@ -22,9 +20,6 @@ const $toast = useToast()
const refInputEl = ref<HTMLElement>()
//
const addUserDialog = ref(false)
//
const otpDialog = ref(false)
@@ -37,13 +32,6 @@ const secret = ref('')
//
const otpPassword = ref('')
//
const userForm = reactive({
name: '',
password: '',
email: '',
})
//
const accountInfo = ref<User>({
id: 0,
@@ -125,58 +113,6 @@ async function loadAllUsers() {
}
}
//
async function deleteUser(user: User) {
try {
const result: { [key: string]: any } = await api.delete(`user/${user.name}`)
if (result.success) {
$toast.success('用户删除成功!')
loadAllUsers()
} else {
$toast.error(`用户删除失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
//
async function deactivateUser(user: User) {
try {
user.is_active = !user.is_active
const result: { [key: string]: any } = await api.put('user/', user)
if (result.success) {
$toast.success('用户冻结成功!')
loadAllUsers()
} else {
$toast.error(`用户冻结失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
//
async function addUser() {
if (!userForm.name || !userForm.password || !userForm.email) {
$toast.error('请填写完整信息!')
return
}
try {
const result: { [key: string]: any } = await api.post('user/', userForm)
if (result.success) {
$toast.success('用户新增成功!')
loadAllUsers()
addUserDialog.value = false
} else {
$toast.error(`用户新增失败:${result.message}`)
}
} catch (error) {
console.log(error)
}
}
// Otp Uri
async function getOtpUri() {
try {
@@ -335,98 +271,7 @@ onMounted(() => {
</VCardText>
</VCard>
</VCol>
<VCol v-if="accountInfo.is_superuser" cols="12">
<!-- 👉 Accounts -->
<VCard title="所有用户">
<template #append>
<IconBtn @click.stop="addUserDialog = true">
<VIcon icon="mdi-plus" />
</IconBtn>
</template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">用户名</th>
<th scope="col">邮箱</th>
<th scope="col">状态</th>
<th scope="col">管理员</th>
<th scope="col" class="w-5" />
</tr>
</thead>
<tbody>
<tr v-for="user in allUsers" :key="user.name">
<td>
{{ user.name }}
</td>
<td>{{ user.email }}</td>
<td>
<VChip v-if="user.is_active" color="success" text-color="white"> 激活 </VChip>
<VChip v-else color="error" text-color="white"> 冻结 </VChip>
</td>
<td>{{ user.is_superuser ? '是' : '否' }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem variant="plain" @click="deactivateUser(user)">
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{ user.is_active ? '冻结' : '解冻' }}
</VListItemTitle>
</VListItem>
<VListItem variant="plain" base-color="error" @click="deleteUser(user)">
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</VCol>
</VRow>
<!-- =弹窗 -->
<VDialog v-model="addUserDialog" max-width="50rem" persistent z-index="1010">
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VTextField v-model="userForm.name" label="用户名" :rules="[requiredValidator]" />
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="userForm.email" :rules="[requiredValidator]" label="邮箱" />
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false"> 取消 </VBtn>
<VSpacer />
<VBtn @click="addUser"> 确定 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 双重验证弹窗 -->
<VDialog v-model="otpDialog" max-width="45rem" persistent z-index="1010">