Compare commits

...

33 Commits

Author SHA1 Message Date
jxxghp
0e46936231 fix ui 2023-09-11 12:32:20 +08:00
jxxghp
d91d3ef0ef feat TMDBID搜索 2023-09-11 11:59:07 +08:00
jxxghp
1f0dd907f9 1.1.8 2023-09-10 17:34:26 +08:00
jxxghp
3c555cbfca fix site api 2023-09-09 17:40:01 +08:00
jxxghp
9a8e4d8600 fix ui text 2023-09-09 16:35:55 +08:00
jxxghp
444aaa5cdc fix plugin ui 2023-09-09 11:38:17 +08:00
jxxghp
25669d18fc v1.1.7 2023-09-09 11:03:49 +08:00
jxxghp
03d6e46eca fix 过滤规则删除问题 2023-09-09 09:40:06 +08:00
jxxghp
c44c7ed0f0 fix filemanager 2023-09-09 08:01:00 +08:00
jxxghp
47ac7437c0 feat 优化插件界面交互 2023-09-08 15:36:54 +08:00
jxxghp
37f982e0ea add icon 2023-09-08 15:08:32 +08:00
jxxghp
139646369f fix icon 2023-09-08 10:02:43 +08:00
jxxghp
3d4b84dc09 更新 package.json 2023-09-07 17:52:20 +08:00
jxxghp
02866754e0 add icon 2023-09-07 15:50:08 +08:00
jxxghp
e4e1a75d44 fix ui 2023-09-06 21:19:30 +08:00
jxxghp
4e5dd03456 fix ui 2023-09-06 20:22:32 +08:00
jxxghp
506b6eea09 fix rss ui 2023-09-06 20:18:45 +08:00
jxxghp
403ee4b925 fix 2023-09-06 17:26:20 +08:00
jxxghp
193d1f550f fix ui 2023-09-06 15:50:05 +08:00
jxxghp
d2a02a830c fix restart ui 2023-09-06 15:30:10 +08:00
jxxghp
6cc9c1ac57 feat 内建重启 2023-09-06 12:56:49 +08:00
jxxghp
fffb1c6c02 v1.1.4 2023-09-05 19:52:01 +08:00
jxxghp
985d1baff5 feat 新增紫色主题 2023-09-05 10:15:20 +08:00
jxxghp
a70e467b69 fix ui 2023-09-05 09:53:02 +08:00
jxxghp
b07010bebd Merge pull request #35 from amtoaer/memory_percent 2023-09-04 23:18:20 +08:00
amtoaer
353bdc5989 feat: 内存占用图使用百分比 2023-09-04 23:05:16 +08:00
jxxghp
f135804c4b fix ui 2023-09-04 21:30:03 +08:00
jxxghp
ae6d0ead2c v1.1.3 2023-09-04 21:13:27 +08:00
jxxghp
6db3ad4e0d fix ui 2023-09-04 20:07:59 +08:00
jxxghp
09e42d5a08 build 2023-09-04 17:56:32 +08:00
jxxghp
b9a09fd1be fix 用户权限控制 2023-09-04 17:49:17 +08:00
jxxghp
b08235b9f6 fix ui 2023-09-04 12:37:34 +08:00
jxxghp
43e67893b4 fix ui 2023-09-04 12:36:05 +08:00
35 changed files with 475 additions and 141 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.1.3-3",
"version": "1.1.9-1",
"private": true,
"scripts": {
"dev": "vite --host",

BIN
public/plugin/brush.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/plugin/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -111,7 +111,7 @@ export default defineComponent({
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;

View File

@@ -38,7 +38,7 @@ body,
overflow: hidden;
// TODO: Use grid gutter variable here
padding-block: 1.5rem;
padding-top: calc(env(safe-area-inset-top) + 65px);
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
// display: flex;
@@ -49,7 +49,7 @@ body,
& > div:first-child {
flex: auto;
position: relative;
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
}
}
}

View File

@@ -3,7 +3,7 @@
// 👉 Vertical nav
$layout-vertical-nav-z-index: 12 !default;
$layout-vertical-nav-width: 16.25rem !default;
$layout-vertical-nav-collapsed-width: 80px !default;
$layout-vertical-nav-collapsed-width: 5rem !default;
// 👉 Horizontal nav
$layout-horizontal-nav-z-index: 11 !default;
@@ -16,7 +16,7 @@ $layout-vertical-nav-layout-navbar-z-index: 11 !default;
$layout-horizontal-nav-layout-navbar-z-index: 11 !default;
// 👉 Main content
$layout-boxed-content-width: 1440px !default;
$layout-boxed-content-width: 90rem !default;
// 👉Footer
$layout-vertical-nav-footer-height: 3.5rem !default;

View File

@@ -1,10 +1,7 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import api from './api'
import type { User } from './api/types'
import store from './store'
import avatar1 from '@images/avatars/avatar-1.png'
// 第一时间应用主题
const { global: globalTheme } = useTheme()
@@ -36,40 +33,10 @@ function startSSEMessager() {
}
}
// 当前用户信息
const accountInfo = ref<User>({
id: 0,
name: '',
password: '',
email: '',
is_active: false,
is_superuser: false,
avatar: avatar1,
})
// 调用API加载当前用户数据
async function loadAccountInfo() {
try {
const user: User = await api.get('user/current')
accountInfo.value = user
if (!accountInfo.value.avatar)
accountInfo.value.avatar = avatar1
}
catch (error) {
console.log(error)
}
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
await loadAccountInfo()
console.log('accountInfo', accountInfo.value)
startSSEMessager()
})
// 提供给所有元素复用
provide('accountInfo', accountInfo)
</script>
<template>

View File

@@ -526,6 +526,9 @@ export interface Plugin {
// 运行状态
state?: boolean
// 是否有详情页面
has_page?: boolean
}
// 种子信息
@@ -905,4 +908,5 @@ export interface FileItem {
extension: string
size: number
children: FileItem[]
modify_time: number
}

View File

@@ -57,6 +57,8 @@ const loading = ref(0)
const activeStorage = ref('local')
// 刷新
const refreshPending = ref(false)
// 排序
const sort = ref('name')
// axios实例
const axiosInstance = ref<Axios>()
@@ -83,6 +85,12 @@ function pathChanged(_path: string) {
emit('pathchanged', _path)
}
// 排序变化
function sortChanged(s: string) {
sort.value = s
refreshPending.value = true
}
// 初始化
onBeforeMount(() => {
activeStorage.value = props.storage ?? 'local'
@@ -101,6 +109,7 @@ onBeforeMount(() => {
@storagechanged="storageChanged"
@pathchanged="pathChanged"
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<VRow no-gutters>
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
@@ -125,6 +134,7 @@ onBeforeMount(() => {
:endpoints="endpoints"
:axios="axiosInstance"
:refreshpending="refreshPending"
:sort="sort"
@pathchanged="pathChanged"
@loading="loadingChanged"
@refreshed="refreshPending = false"

View File

@@ -50,9 +50,6 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
])
// 已选择的过滤规则
const selectedFilters = ref<string[]>(props.rules ?? [])
</script>
<template>
@@ -64,7 +61,7 @@ const selectedFilters = ref<string[]>(props.rules ?? [])
<VCol>
<VSelect
:key="props.pri"
v-model="selectedFilters"
v-model="props.rules"
variant="underlined"
:items="selectFilterOptions"
chips

View File

@@ -52,23 +52,23 @@ function openTmdbPage(type: string, tmdbId: number) {
<div>
<VCardItem class="pb-1">
<VCardTitle>
<VCardTitle class="text-center text-md-left">
{{ context?.media_info?.title || context?.meta_info?.name }}
{{ context?.meta_info?.season_episode }}
</VCardTitle>
<VCardSubtitle>
<VCardSubtitle class="text-center text-md-left">
{{ context?.media_info?.year || context?.meta_info?.year }}
</VCardSubtitle>
</VCardItem>
<VCardText
v-if="context?.media_info?.overview"
class="line-clamp-4 overflow-hidden text-ellipsis ..."
class="line-clamp-4 overflow-hidden text-ellipsis text-center text-md-left ..."
>
{{ context?.media_info?.overview }}
</VCardText>
<VCardItem>
<VCardItem class="text-center text-md-left">
<!-- 类型 -->
<VChip
v-if="context?.media_info?.type || context?.meta_info?.type"

View File

@@ -119,6 +119,8 @@ async function savePluginConf() {
// 显示插件详情
async function showPluginInfo() {
// 加载详情
await loadPluginPage()
pluginConfigDialog.value = false
pluginInfoDialog.value = true
}
@@ -129,17 +131,35 @@ async function showPluginConfig() {
await loadPluginForm()
// 加载配置
await loadPluginConf()
// 加载详情
await loadPluginPage()
// 显示对话框
pluginInfoDialog.value = false
pluginConfigDialog.value = true
}
// 弹出菜单
const dropdownItems = ref([
{
title: '卸载',
title: '查看详情',
value: 1,
show: props.plugin?.has_page,
props: {
prependIcon: 'mdi-information-outline',
click: showPluginInfo,
},
},
{
title: '配置',
value: 2,
show: true,
props: {
prependIcon: 'mdi-cog-outline',
click: showPluginConfig,
},
},
{
title: '卸载',
value: 3,
show: true,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -155,7 +175,12 @@ const dropdownItems = ref([
v-if="isVisible"
:width="props.width"
:height="props.height"
@click="showPluginConfig"
@click="() => {
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
}"
>
<div
class="relative pa-4 text-center card-cover-blurred"
@@ -171,6 +196,7 @@ const dropdownItems = ref([
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
v-show="item.show"
:key="i"
variant="plain"
:base-color="item.props.color"
@@ -214,7 +240,7 @@ const dropdownItems = ref([
scrollable
persistent
>
<VCard :title="props.plugin?.plugin_name">
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VCardText>
<FormRender
@@ -226,7 +252,7 @@ const dropdownItems = ref([
</VCardText>
<VCardActions>
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
详情
查看详情
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf">
@@ -243,7 +269,7 @@ const dropdownItems = ref([
scrollable
persistent
>
<VCard :title="`${props.plugin?.plugin_name} - 详情`">
<VCard :title="`${props.plugin?.plugin_name}`">
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText>
<PageRender
@@ -253,6 +279,9 @@ const dropdownItems = ref([
/>
</VCardText>
<VCardActions>
<VBtn @click="showPluginConfig">
配置
</VBtn>
<VSpacer />
<VBtn @click="pluginInfoDialog = false">
关闭

View File

@@ -368,6 +368,7 @@ onMounted(() => {
>
<!-- Dialog Content -->
<VCard :title="`订阅 - ${props.media?.name}`">
<DialogCloseBtn @click="rssInfoDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
@@ -425,6 +426,7 @@ onMounted(() => {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.include"
@@ -433,6 +435,7 @@ onMounted(() => {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.exclude"
@@ -441,6 +444,7 @@ onMounted(() => {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.save_path"

View File

@@ -244,13 +244,7 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
async function getResourceList() {
resourceLoading.value = true
try {
resourceDataList.value = await api.get('search/title', {
params: {
keyword: resourceSearch.value,
page: resourceCurrentPage.value,
site: cardProps.site?.id,
},
})
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
resourceLoading.value = false
}
catch (error) {

View File

@@ -0,0 +1,93 @@
<script lang="ts" setup>
import api from '@/api'
import type { MediaInfo } from '@/api/types'
// update:modelValue 事件
const emit = defineEmits(['update:modelValue', 'close'])
const items = ref<{}[]>([])
// 搜索词
const keyword = ref('')
// 加载中
const loading = ref(false)
// 选中条目
function selectMedia(item: MediaInfo) {
console.log(item)
emit('update:modelValue', item.tmdb_id)
emit('close')
}
// 搜索词条
async function searchMedias() {
if (!keyword)
return
// 调用API搜索词条
try {
loading.value = true
const result: MediaInfo[] = await api.get('media/search', {
params: {
title: keyword.value,
page: 1,
count: 20,
},
})
// 清空
items.value = []
// 赋值
for (const item of result) {
if (items.value.length > 0)
items.value.push({ type: 'divider', inset: true })
items.value.push({
prependAvatar: item.poster_path,
title: `${item.title}${item.year}`,
subtitle: `<span class="text-primary">${item.type}</span> ${item.overview}`,
onClick: () => selectMedia(item),
})
}
loading.value = false
}
catch (e) {
console.error(e)
}
}
</script>
<template>
<VCard
class="mx-auto"
width="100%"
>
<VToolbar flat dense>
<VTextField
v-model="keyword"
density="compact"
label="输入名称搜索"
single-line
hide-details
flat
class="mx-3"
append-inner-icon="mdi-magnify"
:loading="loading"
@click:append-inner="searchMedias"
@keydown.enter="searchMedias"
/>
</VToolbar>
<VList
v-if="items.length > 0"
:items="items"
item-props
lines="three"
>
<template #subtitle="{ subtitle }">
<div v-html="subtitle" />
</template>
</VList>
</VCard>
</template>

View File

@@ -10,6 +10,7 @@ import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数
const inProps = defineProps({
@@ -19,6 +20,7 @@ const inProps = defineProps({
endpoints: Object as PropType<EndPoints>,
axios: Object as PropType<Axios>,
refreshpending: Boolean,
sort: String,
})
// 对外事件
@@ -91,6 +93,9 @@ const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
@@ -125,18 +130,18 @@ const isImage = computed(() => {
async function load() {
loading.value = true
emit('loading', true)
if (isDir.value) {
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
// 参数
const url = inProps.endpoints?.list.url
.replace(/{storage}/g, storage.value)
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
.replace(/{sort}/g, inProps.sort || 'name')
const config = {
url,
method: inProps.endpoints?.list.method || 'get',
}
// 加载数据
items.value = await axiosInstance.value.request(config) ?? []
const config = {
url,
method: inProps.endpoints?.list.method || 'get',
}
// 加载数据
items.value = await axiosInstance.value.request(config) ?? []
emit('loading', false)
loading.value = false
}
@@ -261,7 +266,7 @@ async function transfer() {
stopLoadingProgress()
// 显示结果
if (res.success) {
$toast.success(`${currentItem.value?.name} 整理成`)
$toast.success(`${currentItem.value?.name} 整理成!`)
// 重新加载
load()
}
@@ -275,6 +280,11 @@ async function transfer() {
}
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
}
// 监听path变化
watch(
() => inProps.path,
@@ -408,7 +418,9 @@ onMounted(() => {
v-else-if="isFile && !isImage"
class="text-center break-all"
>
文件: {{ path }}
<strong>{{ items[0]?.name }}</strong><br>
大小{{ formatBytes(items[0]?.size || 0) }}<br>
修改时间{{ formatTime(items[0]?.modify_time || 0) }}
</VCardText>
<VCardText
v-else-if="isFile && isImage"
@@ -659,6 +671,8 @@ onMounted(() => {
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
@@ -714,10 +728,10 @@ onMounted(() => {
</VForm>
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="transferPopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
@click="transfer"
>
@@ -749,7 +763,6 @@ onMounted(() => {
<!-- 识别结果对话框 -->
<vDialog
v-model="nameTestDialog"
:scrim="false"
width="800"
>
<vCard>
@@ -759,6 +772,17 @@ onMounted(() => {
</VCardItem>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</template>
<style lang="scss" scoped>

View File

@@ -12,7 +12,7 @@ const inProps = defineProps({
})
// 对外事件
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated'])
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated', 'sortchanged'])
// 新建文件夹名称
const newFolderPopper = ref(false)
@@ -20,6 +20,19 @@ const newFolderPopper = ref(false)
// 新建文件名称
const newFolderName = ref('')
// 排序方式
const sort = ref('name')
// 调整排序方式
function changeSort() {
if (sort.value === 'name')
sort.value = 'time'
else
sort.value = 'name'
emit('sortchanged', sort.value)
}
// 计算PATH面包屑
const pathSegments = computed(() => {
let path_str = ''
@@ -81,6 +94,14 @@ async function mkdir() {
// 通知重新加载
emit('foldercreated')
}
// 计算排序图标
const sortIcon = computed(() => {
if (sort.value === 'time')
return 'mdi-sort-clock-ascending-outline'
else
return 'mdi-sort-alphabetical-ascending'
})
</script>
<template>
@@ -123,6 +144,9 @@ async function mkdir() {
</template>
</VToolbarItems>
<div class="flex-grow-1" />
<IconBtn @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
@@ -156,9 +180,3 @@ async function mkdir() {
</VDialog>
</VToolbar>
</template>
<style lang="scss" scoped>
.v-toolbar{
background: rgb(var(--v-table-header-background));
}
</style>

View File

@@ -35,6 +35,7 @@ function init() {
name: 'root',
children: [],
size: 0,
modify_time: 0,
}]
}

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup>
import type { User } from '@/api/types'
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
@@ -10,9 +9,10 @@ import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
// 获取当前用户信息
const accountInfo: User = inject('accountInfo') as User
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
</script>
<template>
@@ -91,7 +91,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
@@ -99,7 +99,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '电视剧',
icon: 'mdi-television-classic',
@@ -107,7 +107,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '自定义',
icon: 'mdi-rss',
@@ -135,7 +135,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '历史记录',
icon: 'mdi-history',
@@ -143,7 +143,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '文件管理',
icon: 'mdi-folder-multiple-outline',
@@ -153,13 +153,13 @@ const accountInfo: User = inject('accountInfo') as User
<!-- 👉 系统 -->
<VerticalNavSectionTitle
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
heading: '系统',
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '插件',
icon: 'mdi-apps',
@@ -167,7 +167,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '站点管理',
icon: 'mdi-web',
@@ -175,7 +175,7 @@ const accountInfo: User = inject('accountInfo') as User
}"
/>
<VerticalNavLink
v-if="accountInfo.is_superuser"
v-if="superUser"
:item="{
title: '设定',
icon: 'mdi-cog',

View File

@@ -10,6 +10,10 @@ const themes: ThemeSwitcherTheme[] = [
name: 'dark',
icon: 'mdi-weather-night',
},
{
name: 'purple',
icon: 'mdi-brightness-4',
},
]
</script>

View File

@@ -1,11 +1,23 @@
<script setup lang="ts">
import { useStore } from 'vuex'
import { useConfirm } from 'vuetify-use-dialog'
import { useToast } from 'vue-toast-notification'
import router from '@/router'
import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
// Vuex Store
const store = useStore()
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
// 进度框
const progressDialog = ref(false)
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -15,8 +27,45 @@ function logout() {
router.push('/login')
}
// 获取当前用户信息
const accountInfo: User = inject('accountInfo') as User
// 执行重启操作
async function restart() {
// 弹出提示
const confirmed = await createConfirm({
title: '确认',
content: '确认重启系统吗?',
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '30rem',
},
})
if (confirmed) {
// 调用API重启
try {
// 显示等待框
progressDialog.value = true
const result: { [key: string]: any } = await api.get('system/restart')
if (!result?.success) {
// 隐藏等待框
progressDialog.value = false
// 重启不成功
$toast.error(result.message)
return
}
}
catch (error) {
console.error(error)
}
// 注销
logout()
}
}
// 从Vuex Store中获取信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
const avatar = store.state.auth.avatar
</script>
<template>
@@ -25,7 +74,7 @@ const accountInfo: User = inject('accountInfo') as User
color="primary"
variant="tonal"
>
<VImg :src="accountInfo.avatar" />
<VImg :src="avatar ?? avatar1" />
<!-- SECTION Menu -->
<VMenu
@@ -43,21 +92,21 @@ const accountInfo: User = inject('accountInfo') as User
color="primary"
variant="tonal"
>
<VImg :src="accountInfo.avatar" />
<VImg :src="avatar ?? avatar1" />
</VAvatar>
</VListItemAction>
</template>
<VListItemTitle class="font-weight-semibold">
{{ accountInfo.is_superuser ? "管理员" : "普通用户" }}
{{ superUser ? "管理员" : "普通用户" }}
</VListItemTitle>
<VListItemSubtitle>{{ accountInfo.name }}</VListItemSubtitle>
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem
v-if="accountInfo.is_superuser"
v-if="superUser"
link
to="setting"
>
@@ -91,6 +140,19 @@ const accountInfo: User = inject('accountInfo') as User
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem @click="restart">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
@@ -107,4 +169,22 @@ const accountInfo: User = inject('accountInfo') as User
</VMenu>
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<vDialog
v-model="progressDialog"
width="400"
>
<vCard
color="primary"
>
<vCardText class="text-center">
正在重启 ...
<vProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</vCardText>
</vCard>
</vDialog>
</template>

View File

@@ -66,10 +66,16 @@ function login() {
.then((response: any) => {
// 获取token
const token = response.access_token
const superuser = response.super_user
const username = response.user_name
const avatar = response.avatar
// 更新token和remember状态到Vuex Store
store.dispatch('auth/updateToken', token)
store.dispatch('auth/updateRemember', form.value.remember)
store.dispatch('auth/updateSuperUser', superuser)
store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页
router.push('/')

View File

@@ -57,7 +57,7 @@ const theme: VuetifyOptions['theme'] = {
dark: {
dark: true,
colors: {
'primary': '#9155FD',
'primary': '#6E66ED',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'success': '#56CA00',
@@ -104,6 +104,57 @@ const theme: VuetifyOptions['theme'] = {
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
purple: {
dark: true,
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
'error': '#FF4C51',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#28243D',
'on-background': '#E7E3FC',
'surface': '#312D4B',
'on-surface': '#E7E3FC',
'grey-50': '#2A2E42',
'grey-100': '#474360',
'grey-200': '#4A5072',
'grey-300': '#5E6692',
'grey-400': '#7983BB',
'grey-500': '#8692D0',
'grey-600': '#AAB3DE',
'grey-700': '#B6BEE3',
'grey-800': '#CFD3EC',
'grey-900': '#E7E9F6',
'perfect-scrollbar-thumb': '#4A5072',
'skin-bordered-background': '#312d4b',
'skin-bordered-surface': '#312d4b',
},
variables: {
'code-color': '#d400ff',
'overlay-scrim-background': '#2C2942',
'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#E7E3FC',
'table-header-background': '#3D3759',
'custom-background': '#373452',
// Shadows
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
},
}

View File

@@ -4,6 +4,9 @@ import type { Module } from 'vuex'
interface AuthState {
token: string | null
remember: boolean
superUser: boolean
userName: string
avatar: string
}
// 定义根状态类型
@@ -17,6 +20,9 @@ const authModule: Module<AuthState, RootState> = {
state: {
token: null,
remember: false,
superUser: false,
userName: '',
avatar: '',
},
mutations: {
setToken(state, token: string) {
@@ -28,6 +34,15 @@ const authModule: Module<AuthState, RootState> = {
setRemember(state, remember: boolean) {
state.remember = remember
},
setSuperUser(state, superUser: boolean) {
state.superUser = superUser
},
setUserName(state, userName: string) {
state.userName = userName
},
setAvatar(state, avatar: string) {
state.avatar = avatar
},
},
actions: {
updateToken({ commit }, token: string) {
@@ -39,10 +54,22 @@ const authModule: Module<AuthState, RootState> = {
updateRemember({ commit }, remember: boolean) {
commit('setRemember', remember)
},
updateSuperUser({ commit }, superUser: boolean) {
commit('setSuperUser', superUser)
},
updateUserName({ commit }, userName: string) {
commit('setUserName', userName)
},
updateAvatar({ commit }, avatar: string) {
commit('setAvatar', avatar)
},
},
getters: {
getToken: state => state.token,
getRemember: state => state.remember,
getSuperUser: state => state.superUser,
getUserName: state => state.userName,
getAvatar: state => state.avatar,
},
}

View File

@@ -3,14 +3,13 @@
@tailwind components;
@tailwind utilities;
#nprogress .bar {
background: #7D34FD !important;
background: rgb(var(--v-theme-primary)) !important;
top: env(safe-area-inset-top) !important;
}
#nprogress .peg {
box-shadow: 0 0 10px #7D34FD, 0 0 5px #7D34FD !important;
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
-webkit-transform: rotate(0deg) translate(0px, -1px);
-ms-transform: rotate(0deg) translate(0px, -1px);
transform: rotate(0deg) translate(0px, -1px);
@@ -123,3 +122,7 @@
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
}
.v-toolbar{
background: rgb(var(--v-table-header-background));
}

View File

@@ -27,6 +27,7 @@ const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: false },
},
tooltip: { enabled: false },
grid: {
@@ -75,6 +76,7 @@ const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
},
yaxis: {
labels: { show: false },
max: 100,
},
}
})

View File

@@ -20,14 +20,17 @@ const series = ref([
},
])
// 当前值
const current = ref(0)
// 占用的内存
const usedMemory = ref(0)
// 内存使用百分比
const memoryUsage = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: false },
},
tooltip: { enabled: false },
grid: {
@@ -79,6 +82,7 @@ const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
},
yaxis: {
labels: { show: false },
max: 100,
},
}
})
@@ -87,9 +91,8 @@ const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
async function getMemorgUsage() {
try {
// 请求数据
current.value = await api.get('dashboard/memory') ?? 0
// 添加到序列
series.value[0].data.push(current.value / 1024 / 1024 / 1024)
[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
series.value[0].data.push(memoryUsage.value)
// 序列超过30条记录时清掉前面的
if (series.value[0].data.length > 30)
series.value[0].data.shift()
@@ -130,7 +133,7 @@ onUnmounted(() => {
/>
<p class="text-center font-weight-medium mb-0">
当前{{ formatBytes(current) }}
当前{{ formatBytes(usedMemory) }}
</p>
</VCardText>
</VCard>

View File

@@ -430,9 +430,13 @@ onBeforeMount(() => {
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
</span>
</div>
<h1 class="flex flex-row items-baseline justify-start lg:justify-center">
<span>{{ mediaDetail.title }}</span>
<span v-if="mediaDetail.year" class="text-lg">{{ mediaDetail.year }}</span>
<h1 class="d-flex flex-column flex-lg-row align-baseline justify-center justify-lg-start">
<div class="align-self-center align-self-lg-end">
{{ mediaDetail.title }}
</div>
<div v-if="mediaDetail.year" class="text-lg align-self-center align-self-lg-end">
{{ mediaDetail.year }}
</div>
</h1>
<span class="media-attributes">
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]">{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span>

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import FileBrowser from '@/components/FileBrowser.vue'
const endpoints = {
list: { url: '/filebrowser/list?path={path}', method: 'get' },
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
download: { url: '/filebrowser/download?path={path}', method: 'get' },

View File

@@ -5,6 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 确认框
const createConfirm = useConfirm()
@@ -71,6 +72,9 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 获取订阅列表数据
async function fetchData({
page,
@@ -412,6 +416,8 @@ const dropdownItems = ref([
v-model="redoTmdbId"
label="TMDB编号"
:rules="[requiredValidator, numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
</VRow>
@@ -455,6 +461,17 @@ const dropdownItems = ref([
</vCardText>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="redoTmdbId"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</template>
<style lang="scss">

View File

@@ -5,15 +5,10 @@ import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
// 是否可见
visible: boolean
}
// 提示框
@@ -48,7 +43,6 @@ async function queryCustomFilters(ruleType: string) {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
visible: true,
}
})
}
@@ -138,23 +132,18 @@ function updateFilterCardValue2(pri: string, rules: string[]) {
// 移除卡片
function filterCardClose(ruleType: string, pri: string) {
// 将卡片从列表中删除,并更新剩余卡片的序号
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
const index = cards.value.findIndex(card => card.pri === pri)
if (index !== -1) {
// 创建新的数组,然后使用 splice 方法来删除元素
const updatedCards = [...cards.value]
updatedCards.splice(index, 1)
// 更新剩余卡片的序号
updatedCards.forEach((card, i) => {
card.pri = (i + 1).toString()
// 将pri对应的卡片从列表中删除,并更新剩余卡片的序号
const updatedCards = (ruleType === 'FilterRules' ? filterCards.value : filterCards2.value)
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 filterCards.value
cards.value = updatedCards
}
// 更新 filterCards.value
if (ruleType === 'FilterRules')
filterCards.value = updatedCards
else
filterCards2.value = updatedCards
}
// 增加卡片
@@ -164,7 +153,7 @@ function addFilterCard(ruleType: string) {
const pri = (cards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [], visible: true }
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
cards.value.push(newCard)
@@ -189,7 +178,6 @@ onMounted(() => {
:key="index"
:pri="card.pri"
:rules="card.rules"
:visible="card.visible"
@changed="updateFilterCardValue"
@close="filterCardClose('FilterRules', card.pri)"
/>
@@ -224,7 +212,6 @@ onMounted(() => {
:key="index"
:pri="card.pri"
:rules="card.rules"
:visible="card.visible"
@changed="updateFilterCardValue2"
@close="filterCardClose('FilterRules2', card.pri)"
/>

View File

@@ -176,6 +176,7 @@ function onRefresh() {
<!-- Dialog Content -->
<VCard title="新增自定义订阅">
<DialogCloseBtn @click="rssAddDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
@@ -232,6 +233,7 @@ function onRefresh() {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.include"
@@ -241,6 +243,7 @@ function onRefresh() {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.exclude"
@@ -250,6 +253,7 @@ function onRefresh() {
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="rssForm.save_path"

View File

@@ -1836,6 +1836,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.14.197":
version "4.14.198"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.198.tgz#4d27465257011aedc741a809f1269941fa2c5d4c"
integrity sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==
"@types/mdast@^3.0.0":
version "3.0.11"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"