Merge pull request #1 from jxxghp/v2

pr
This commit is contained in:
boeto
2024-10-25 17:36:10 +08:00
committed by GitHub
23 changed files with 174 additions and 123 deletions

View File

@@ -1,10 +1,12 @@
name: Build Moviepilot-Frontend
name: Build Moviepilot-Frontend v2
on:
workflow_dispatch:
push:
branches:
- dev
- v2
paths:
- 'package.json'
jobs:
build:
@@ -43,14 +45,14 @@ jobs:
- name: Delete Release
uses: dev-drprasad/delete-tag-and-release@v1.1
with:
tag_name: dev_${{ env.frontend_version }}
tag_name: ${{ env.frontend_version }}
delete_release: true
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Release
uses: softprops/action-gh-release@v2
with:
tag_name: dev_${{ env.frontend_version }}
tag_name: ${{ env.frontend_version }}
name: ${{ env.frontend_version }}
draft: false
prerelease: false

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.0.0-alpha",
"version": "2.0.0-beta",
"private": true,
"bin": "dist/service.js",
"scripts": {

View File

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

View File

@@ -1042,6 +1042,8 @@ export interface TransferDirectoryConf {
download_category_folder?: boolean
// 监控方式 downloader/monitorNone为不监控
monitor_type?: string
// 监控模式 fast/compatibility
monitor_mode?: string
// 整理方式 move/copy/link/softlink
transfer_type?: string
// 文件覆盖模式 always/size/never/latest
@@ -1058,6 +1060,8 @@ export interface TransferDirectoryConf {
library_type_folder?: boolean
// 媒体库类别子目录
library_category_folder?: boolean
// 是否发送通知
notify?: boolean
}
// 自定义规则项

View File

@@ -93,6 +93,11 @@ function onClose() {
<template>
<div>
<VCard variant="tonal" @click="openRuleInfoDialog">
<span class="absolute top-3 right-12">
<IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start">

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { TransferDirectoryConf } from '@/api/types'
import { VTextField } from 'vuetify/lib/components/index.mjs'
import { VDivider, VSpacer, VTextField } from 'vuetify/lib/components/index.mjs'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import { nextTick } from 'vue'
@@ -26,6 +26,9 @@ const downloadPath = ref<string>('')
// 媒体库路径
const libraryPath = ref<string>('')
// 卡版是否折叠状态
const isCollapsed = ref(true)
// 类型下拉字典
const typeItems = [
{ title: '全部', value: '' },
@@ -48,6 +51,12 @@ const transferSourceItems = [
{ title: '目录监控', value: 'monitor' },
]
// 监控模式下拉字典
const MonitorModeItems = [
{ title: '性能模式', value: 'fast' },
{ title: '兼容模式', value: 'compatibility' },
]
// 整理方式下拉字典
const transferTypeItems = ref<{ title: string; value: string }[]>([])
@@ -182,7 +191,7 @@ watch(
</IconBtn>
</span>
</VCardItem>
<VCardText>
<VCardText v-if="!isCollapsed">
<VForm>
<VRow>
<VCol cols="6">
@@ -236,6 +245,14 @@ watch(
</VCol>
</VRow>
<VRow v-if="$props.directory.monitor_type">
<VCol cols="12" v-if="$props.directory.monitor_type == 'monitor'">
<VSelect
v-model="props.directory.monitor_mode"
variant="underlined"
:items="MonitorModeItems"
label="监控模式"
/>
</VCol>
<VCol cols="4">
<VSelect
v-model="props.directory.library_storage"
@@ -285,8 +302,16 @@ watch(
<VCol cols="6">
<VSwitch v-model="props.directory.scraping" label="刮削元数据"></VSwitch>
</VCol>
<VCol cols="6">
<VSwitch v-model="props.directory.notify" label="发送通知"></VSwitch>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="text-center py-0">
<VSpacer />
<VBtn :icon="isCollapsed ? 'mdi-chevron-down' : 'mdi-chevron-up'" @click.stop="isCollapsed = !isCollapsed" />
<VSpacer />
</VCardActions>
</VCard>
</template>

View File

@@ -209,6 +209,11 @@ function onClose() {
<template>
<div>
<VCard variant="tonal" @click="opengroupInfoDialog">
<span class="absolute top-3 right-12">
<IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start">

View File

@@ -117,6 +117,11 @@ function onClose() {
<template>
<div>
<VCard variant="tonal" @click="openNotificationInfoDialog">
<span class="absolute top-3 right-12">
<IconBtn>
<VIcon class="cursor-move" icon="mdi-drag" />
</IconBtn>
</span>
<DialogCloseBtn @click="onClose" />
<VCardText class="flex justify-space-between align-center gap-3">
<div class="align-self-start">

View File

@@ -134,8 +134,8 @@ async function forkSubscribe() {
</template>
</VImg>
</div>
<div class="flex flex-col justify-center pl-2 xl:pl-4 line-clamp-2 overflow-hidden text-ellipsis ...">
<div class="mr-2 min-w-0 text-lg font-bold text-white">
<div class="flex flex-col justify-center pl-2 xl:pl-4">
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.share_title }}
</div>
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">

View File

@@ -21,8 +21,11 @@ const props = defineProps({
},
})
// 当前用户名称
const currentLoginUser = store.state.auth.userName
// 当前用户的ID
const currentLoginUserId = computed(() => store.state.auth.userID)
// 当前用户是否是管理员
const currentUserIsSuperuser = computed(() => store.state.auth.superUser)
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
@@ -57,7 +60,7 @@ async function fetchSubscriptions() {
// 删除用户
async function removeUser() {
if (props.user.name == currentLoginUser) {
if (props.user.id === currentLoginUserId.value) {
$toast.error('不能删除当前登录用户!')
return
}
@@ -84,19 +87,6 @@ function editUser() {
userEditDialog.value = true
}
// 计算是否有用户编辑权限
const canEditUser = computed(() => {
if (store.state.auth.superUser && props.user.name !== currentLoginUser) return true
return false
})
// 计算是否有用户管理权限
const canManageUser = computed(() => {
if (props.user.name == currentLoginUser) return false
if (store.state.auth.superUser) return true
return false
})
// 用户重新完成时
function onUserUpdate() {
userEditDialog.value = false
@@ -139,8 +129,9 @@ onMounted(() => {
</div>
</VCardText>
<VCardText class="pb-6">
<h5 class="text-h6">详情</h5>
<VDivider class="my-2" />
<VDivider class="my-2">
<h5 class="text-h6">详情</h5>
</VDivider>
<VList lines="one">
<VListItem>
<VListItemTitle class="text-sm">
@@ -170,8 +161,22 @@ onMounted(() => {
</VList>
</VCardText>
<VCardText class="flex flex-row justify-center">
<VBtn v-if="canEditUser" color="primary" class="me-4" @click="editUser">编辑</VBtn>
<VBtn v-if="canManageUser" color="error" variant="outlined" @click="removeUser"> 删除 </VBtn>
<VBtn
v-if="currentUserIsSuperuser"
color="primary"
class="me-4"
@click="editUser"
>
编辑
</VBtn>
<VBtn
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
color="error"
variant="outlined"
@click="removeUser"
>
删除
</VBtn>
</VCardText>
</VCard>
<!-- 用户编辑弹窗 -->

View File

@@ -36,7 +36,7 @@ const systemMenus = ref<NavMenu[]>([])
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && superUser)
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
}
// 返回上一页

View File

@@ -143,8 +143,8 @@ onMounted(() => {
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-filter-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">优先级</h6>
<span class="text-sm">优先级规则测试</span>
<h6 class="text-base font-weight-medium mt-2 mb-0">规则</h6>
<span class="text-sm">规则测试</span>
</VListItem>
</VCol>
</VRow>
@@ -241,7 +241,7 @@ onMounted(() => {
</VDialog>
<!-- 规则测试弹窗 -->
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="50rem" scrollable>
<VCard title="优先级测试">
<VCard title="规则测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VCardText>
<RuleTestView />

View File

@@ -6,9 +6,6 @@ import router from '@/router'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// Vuex Store
const store = useStore()

View File

@@ -1,14 +1,16 @@
import '@/@core/utils/compatibility'
import './ace-config'
import '@/@iconify/icons-bundle'
import '@/plugins/webfontloader'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
import router from '@/router'
import store from '@/store'
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import { removeEl } from './@core/utils/dom'
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
import './ace-config'
import { VAceEditor } from 'vue3-ace-editor'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import { VTreeview } from 'vuetify/labs/VTreeview'
import ToastPlugin from 'vue-toast-notification'
@@ -23,8 +25,6 @@ import MediaInfoCard from './components/cards/MediaInfoCard.vue'
import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import PathField from './components/input/PathField.vue'
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'

View File

@@ -12,7 +12,7 @@ const appOrder = ref<string[]>([])
// 根据分类获取菜单列表
const getMenuList = () => {
return SystemNavMenus.filter((item: NavMenu) => !item.admin || superUser)
return SystemNavMenus.filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
}
// APP列表
@@ -48,7 +48,7 @@ onMounted(() => {
:component-data="{ 'class': 'ma-0 mt-n1' }"
>
<template #item="{ element }">
<VCol cols="6" md="4" lg="3" class="text-center cursor-pointer shortcut-icon select-none">
<VCol cols="6" md="3" lg="2" class="text-center cursor-pointer shortcut-icon select-none">
<VCard class="pa-4" :to="element.to" variant="flat">
<VAvatar size="64" variant="text">
<VIcon size="48" :icon="element.icon" color="primary" />

View File

@@ -15,9 +15,6 @@ const { global: globalTheme } = useTheme()
// Vuex Store
const store = useStore()
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 表单
const form = ref({
username: '',
@@ -55,15 +52,8 @@ let intervalTimer: NodeJS.Timeout | null = null
// 获取背景图片
async function fetchBackgroundImage() {
try {
const results: string[] = await api.get('/login/wallpapers')
if (results && results.length > 0) {
results.map((url: string) => {
if (globalSettings.GLOBAL_IMAGE_CACHE)
backgroundImages.value.push(
`${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`,
)
else backgroundImages.value.push(url)
})
backgroundImages.value = await api.get('/login/wallpapers')
if (backgroundImages.value && backgroundImages.value.length > 0) {
// 随机打乱排序
backgroundImages.value.sort(() => Math.random() - 0.5)
backgroundImageUrl.value = backgroundImages.value[0]
@@ -171,6 +161,7 @@ function login() {
// 获取token
const token = response.access_token
const superUser = response.super_user
const userID = response.user_id
const userName = response.user_name
const avatar = response.avatar
const level = response.level
@@ -178,7 +169,16 @@ function login() {
const permissions = response.permissions
// 更新token和remember状态到Vuex Store
store.dispatch('auth/login', { token, remember, superUser, userName, avatar, level, permissions })
store.dispatch('auth/login', {
token,
remember,
superUser,
userID,
userName,
avatar,
level,
permissions,
})
// 登录后处理
afterLogin(superUser)

View File

@@ -5,21 +5,23 @@ export const SystemNavMenus = [
icon: 'mdi-home-outline',
to: '/dashboard',
header: '开始',
permission: 'dashboard',
admin: false,
footer: true,
},
{
title: '推荐',
icon: 'mdi-star-outline',
to: '/ranking',
header: '发现',
permission: 'ranking',
admin: false,
footer: true,
},
{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
header: '发现',
permission: 'resource.search',
admin: false,
},
{
title: '电影',
@@ -27,7 +29,8 @@ export const SystemNavMenus = [
icon: 'mdi-movie-open-outline',
to: '/subscribe/movie',
header: '订阅',
permission: 'subscribe.movie',
admin: false,
footer: true,
},
{
title: '电视剧',
@@ -35,7 +38,8 @@ export const SystemNavMenus = [
icon: 'mdi-television',
to: '/subscribe/tv',
header: '订阅',
permission: 'subscribe.tv',
admin: false,
footer: true,
},
{
title: '日历',
@@ -43,56 +47,56 @@ export const SystemNavMenus = [
icon: 'mdi-calendar',
to: '/calendar',
header: '订阅',
permission: 'subscribe.calendar',
admin: false,
},
{
title: '正在下载',
icon: 'mdi-download-outline',
to: '/downloading',
header: '整理',
permission: 'downloading.view',
admin: false,
},
{
title: '历史记录',
icon: 'mdi-history',
to: '/history',
header: '整理',
permission: 'admin',
admin: true,
},
{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
to: '/filemanager',
header: '整理',
permission: 'admin',
admin: true,
},
{
title: '插件',
icon: 'mdi-apps',
to: '/plugins',
header: '系统',
permission: 'admin',
admin: true,
},
{
title: '站点管理',
icon: 'mdi-web',
to: '/site',
header: '系统',
permission: 'admin',
admin: true,
},
{
title: '用户管理',
icon: 'mdi-account-group',
to: '/user',
header: '系统',
permission: 'usermanage',
admin: true,
},
{
title: '设定',
icon: 'mdi-cog',
to: '/setting',
header: '系统',
permission: 'admin',
admin: true,
},
]

View File

@@ -5,6 +5,7 @@ interface AuthState {
token: string | null
remember: boolean
superUser: boolean
userID: number
userName: string
avatar: string
originalPath: string | null
@@ -24,6 +25,7 @@ const authModule: Module<AuthState, RootState> = {
token: null, // 用户令牌
remember: false, // 记住我
superUser: false, // 超级管理员
userID: 999, // 用户ID
userName: '', // 用户名
avatar: '', // 头像
originalPath: null, // 原始路径
@@ -43,6 +45,9 @@ const authModule: Module<AuthState, RootState> = {
setSuperUser(state, superUser: boolean) {
state.superUser = superUser
},
setUserID(state, userID: number) {
state.userID = userID
},
setUserName(state, userName: string) {
state.userName = userName
},
@@ -60,10 +65,11 @@ const authModule: Module<AuthState, RootState> = {
},
},
actions: {
login({ commit }, { token, remember, superUser, userName, avatar, level, permissions }) {
login({ commit }, { token, remember, superUser, userID, userName, avatar, level, permissions }) {
commit('setToken', token)
commit('setRemember', remember)
commit('setSuperUser', superUser)
commit('setUserID', userID)
commit('setUserName', userName)
commit('setAvatar', avatar)
commit('setLevel', level)
@@ -78,6 +84,7 @@ const authModule: Module<AuthState, RootState> = {
getToken: state => state.token,
getRemember: state => state.remember,
getSuperUser: state => state.superUser,
getUserID: state => state.userID,
getUserName: state => state.userName,
getAvatar: state => state.avatar,
getOriginalPath: state => state.originalPath,

View File

@@ -166,7 +166,7 @@
}
.grid-directory-card {
grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}

View File

@@ -35,7 +35,8 @@ async function loadLatest(server: string) {
onMounted(async () => {
await loadMediaServerSetting()
for (const server of mediaServers.value) {
const enabledServers = mediaServers.value.filter(server => server.enabled)
for (const server of enabledServers) {
loadLatest(server.name)
}
})

View File

@@ -35,7 +35,8 @@ async function loadLibrary(server: string) {
onMounted(async () => {
await loadMediaServerSetting()
for (const server of mediaServers.value) {
const enabledServers = mediaServers.value.filter(server => server.enabled)
for (const server of enabledServers) {
loadLibrary(server.name)
}
})

View File

@@ -33,7 +33,8 @@ async function loadPlayingList(server: string) {
onMounted(async () => {
await loadMediaServerSetting()
for (const server of mediaServers.value) {
const enabledServers = mediaServers.value.filter(server => server.enabled)
for (const server of enabledServers) {
loadPlayingList(server.name)
}
})

View File

@@ -2,15 +2,16 @@
import { reactive, ref } from 'vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import { FilterRuleGroup } from '@/api/types'
// 识别结果
const ruleTestResult = ref('')
// 名称识别表单
const ruleTestForm = reactive({
title: '',
subtitle: '',
ruletype: '1',
title: null,
subtitle: null,
rulegroup: null,
})
// 识别按钮状态
@@ -22,10 +23,27 @@ const ruleTestText = ref('测试')
// 是否显示结果
const showResult = ref(false)
// 所有规则组列表
const filterRuleGroups = ref<FilterRuleGroup[]>([])
// 规则组选项
const filterRuleGroupItems = computed(() => {
return filterRuleGroups.value.map(item => ({ title: item.name, value: item.name }))
})
// 加载规则组
async function queryFilterRuleGroups() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
filterRuleGroups.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API识别
async function ruleTest() {
if (!ruleTestForm.title)
return
if (!ruleTestForm.title) return
try {
ruleTestLoading.value = true
@@ -35,69 +53,41 @@ async function ruleTest() {
params: {
title: ruleTestForm.title,
subtitle: ruleTestForm.subtitle,
ruletype: ruleTestForm.ruletype,
rulegroup_name: ruleTestForm.rulegroup,
},
})
if (result.success)
ruleTestResult.value = `优先级:${result.data.priority}`
else
ruleTestResult.value = '未命中任何优先级规则!'
if (result.success) ruleTestResult.value = `优先级:${result.data.priority}`
else ruleTestResult.value = '未命中任何优先级规则!'
ruleTestLoading.value = false
ruleTestText.value = '重新测试'
showResult.value = true
}
catch (error) {
} catch (error) {
console.error(error)
}
}
onMounted(() => {
queryFilterRuleGroups()
})
</script>
<template>
<VForm @submit.prevent="() => {}">
<VRow class="pt-2">
<VCol cols="12" md="8">
<VTextField
v-model="ruleTestForm.title"
label="标题"
:rules="[requiredValidator]"
/>
<VTextField v-model="ruleTestForm.title" label="标题" :rules="[requiredValidator]" />
</VCol>
<VCol cols="12" md="4">
<VSelect
v-model="ruleTestForm.ruletype"
label="规则类型"
:items="[{
title: '订阅优先级',
value: '1',
}, {
title: '洗版优先级',
value: '2',
}, {
title: '搜索优先级',
value: '3',
}]"
/>
<VSelect v-model="ruleTestForm.rulegroup" label="规则组" :items="filterRuleGroupItems" />
</VCol>
<VCol cols="12">
<VTextarea
v-model="ruleTestForm.subtitle"
label="副标题"
rows="2"
auto-grow
/>
<VTextarea v-model="ruleTestForm.subtitle" label="副标题" rows="2" auto-grow />
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
class="text-center"
>
<VBtn
:disabled="ruleTestLoading"
@click="ruleTest"
>
<VCol cols="12" class="text-center">
<VBtn :disabled="ruleTestLoading" @click="ruleTest">
<template #prepend>
<VIcon icon="mdi-filter-check-outline" />
</template>
@@ -109,9 +99,7 @@ async function ruleTest() {
<VExpandTransition>
<div v-show="showResult">
<VCol>
<VAlert
icon="mdi-alert-circle-outline"
>
<VAlert icon="mdi-alert-circle-outline">
{{ ruleTestResult }}
</VAlert>
</VCol>