修改菜单名

添加设置切换动画
This commit is contained in:
thofx
2023-07-22 17:00:01 +08:00
parent 4d67126f0e
commit 4bd9bc4a1f
11 changed files with 17 additions and 17 deletions

View File

@@ -0,0 +1,467 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
const isNewPasswordVisible = ref(false)
const isConfirmPasswordVisible = ref(false)
const isPasswordVisible = ref(false)
const newPassword = ref('')
const confirmPassword = ref('')
// 提示框
const $toast = useToast()
const refInputEl = ref<HTMLElement>()
// 新增用户窗口
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: '',
})
// 所有用户信息
const allUsers = ref<User[]>([])
// changeAvatar function
function changeAvatar(file: Event) {
const fileReader = new FileReader()
const { files } = file.target as HTMLInputElement
if (files && files.length > 0) {
fileReader.readAsDataURL(files[0])
fileReader.onload = () => {
if (typeof fileReader.result === 'string') {
accountInfo.value.avatar = fileReader.result
saveAccountInfo()
}
}
}
}
// reset avatar image
function resetAvatar() {
accountInfo.value.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)
}
}
// 保存用户信息
async function saveAccountInfo() {
if (newPassword.value || confirmPassword.value) {
if (newPassword.value !== confirmPassword.value) {
$toast.error('两次输入的密码不一致')
return
}
accountInfo.value.password = newPassword.value
}
try {
const result: { [key: string]: any } = await api.put('user', accountInfo.value)
if (result.success)
$toast.success('用户信息保存成功!')
else
$toast.error(`用户信息保存失败:${result.message}`)
}
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() {
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>
<VRow>
<VCol cols="12">
<VCard title="个人信息">
<VCardText class="d-flex">
<!-- 👉 Avatar -->
<VAvatar
rounded="lg"
size="100"
class="me-6"
:image="accountInfo.avatar"
/>
<!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2">
<VBtn
color="primary"
@click="refInputEl?.click()"
>
<VIcon
icon="mdi-cloud-upload-outline"
class="d-sm-none"
/>
<span class="d-none d-sm-block">上传头像</span>
</VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
>
<VBtn
type="reset"
color="error"
variant="tonal"
@click="resetAvatar"
>
<span class="d-none d-sm-block">重置</span>
<VIcon
icon="mdi-refresh"
class="d-sm-none"
/>
</VBtn>
</div>
<p class="text-body-1 mb-0">
允许 JPGGIF PNG 格式 最大尽寸 800K
</p>
</form>
</VCardText>
<VDivider />
<VCardText>
<!-- 👉 Form -->
<VForm class="mt-6">
<VRow>
<!-- 👉 Name -->
<VCol
md="6"
cols="12"
>
<VTextField
v-model="accountInfo.name"
readonly
label="用户名"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="accountInfo.email"
label="邮箱"
type="email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="新密码"
autocomplete="new-password"
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="确认新密码"
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
/>
</VCol>
<!-- 👉 Form Actions -->
<VCol
cols="12"
class="d-flex flex-wrap gap-4"
>
<VBtn @click="saveAccountInfo">
保存
</VBtn>
</VCol>
</VRow>
</VForm>
</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="!user.is_superuser">
<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="800"
persistent
>
<!-- 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"
label="邮箱"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="addUser">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { NotificationSwitch } from '@/api/types'
const messagemTypes = ref<NotificationSwitch[]>([])
// 提示框
const $toast = useToast()
// 调用API查询消息开关
async function loadNotificationSwitchs() {
try {
const result: NotificationSwitch[] = await api.get('message/switchs')
messagemTypes.value = result
}
catch (error) {
console.log(error)
}
}
// 调用API保存消息开关
async function saveNotificationSwitchs() {
try {
const result: { [key: string]: any } = await api.post(
'message/switchs',
messagemTypes.value,
)
if (result.success)
$toast.success('保存通知消息设置成功')
else
$toast.error('保存通知消息设置失败!')
// messagemTypes.value = messagemTypes.value
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
loadNotificationSwitchs()
})
</script>
<template>
<VCard title="消息通知">
<VCardText> 对应消息类型只会发送给选中的消息渠道 </VCardText>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
消息类型
</th>
<th scope="col">
微信
</th>
<th scope="col">
Telegram
</th>
<th scope="col">
Slack
</th>
</tr>
</thead>
<tbody>
<tr
v-for="message in messagemTypes"
:key="message.mtype"
>
<td>
{{ message.mtype }}
</td>
<td>
<VCheckbox v-model="message.wechat" />
</td>
<td>
<VCheckbox v-model="message.telegram" />
</td>
<td>
<VCheckbox v-model="message.slack" />
</td>
</tr>
<tr v-if="messagemTypes.length === 0">
<td
colspan="4"
class="text-center"
>
没有设置任何通知渠道
</td>
</tr>
</tbody>
</VTable>
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn
mtype="submit"
@click="saveNotificationSwitchs"
>
保存
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,229 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
// 是否可见
visible: boolean
}
// 提示框
const $toast = useToast()
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 规则卡片列表
const filterCards = ref<FilterCard[]>([])
// 查询已设置过滤规则
async function queryCustomFilters() {
try {
const result: { [key: string]: any } = await api.get('system/setting/FilterRules')
if (result.success) {
// 保存的是个字符串,需要分割成数组
const groups = result.data?.value.split('>')
// 生成规则卡片
filterCards.value = groups?.map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
visible: true,
}
})
}
}
catch (error) {
console.log(error)
}
}
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的识别词
async function saveCustomFilters() {
try {
// 有值才处理
if (filterCards.value.length === 0)
return
// 将卡片规则接装为字符串
const value = filterCards.value
.filter(card => card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
// 保存
const result: { [key: string]: any } = await api.post(
'system/setting/FilterRules',
value,
)
if (result.success)
$toast.success('过滤规则保存成功')
else
$toast.error('过滤规则保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = filterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 移除卡片
function filterCardClose(pri: string) {
// 将卡片从列表中删除,并更新剩余卡片的序号
const index = filterCards.value.findIndex(card => card.pri === pri)
if (index !== -1) {
// 创建新的数组,然后使用 splice 方法来删除元素
const updatedCards = [...filterCards.value]
updatedCards.splice(index, 1)
// 更新剩余卡片的序号
updatedCards.forEach((card, i) => {
card.pri = (i + 1).toString()
})
// 更新 filterCards.value
filterCards.value = updatedCards
}
}
// 增加卡片
function addFilterCard() {
// 优先级
const pri = (filterCards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [], visible: true }
// 添加到列表
filterCards.value.push(newCard)
}
onMounted(() => {
queryTorrentPriority()
queryCustomFilters()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="过滤规则">
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in filterCards"
:key="index"
:pri="card.pri"
:rules="card.rules"
:visible="card.visible"
@changed="updateFilterCardValue"
@close="filterCardClose(card.pri)"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard"
>
<VIcon icon="mdi-plus" />
<span class="d-none d-sm-block">增加规则</span>
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardText>
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -0,0 +1,152 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Site } from '@/api/types'
// 提示框
const $toast = useToast()
// 选中站点
const selectedSites = ref<number[]>([])
// 所有站点
const allSites = ref<Site[]>([])
// 站点重置
const isConfirmResetSites = ref(false)
// 站点重置按钮文本
const resetSitesText = ref('重置站点数据')
// 站点重置按钮可用状态
const resetSitesDisabled = ref(false)
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
querySelectedSites()
}
catch (error) {
console.log(error)
}
}
// 查询用户选中的站点
async function querySelectedSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
selectedSites.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存用户选中的站点
async function saveSelectedSites() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/IndexerSites',
selectedSites.value,
)
if (result.success)
$toast.success('索引站点保存成功')
else
$toast.error('索引站点保存失败!')
}
catch (error) {
console.log(error)
}
}
// 重置站点
async function resetSites() {
try {
resetSitesDisabled.value = true
resetSitesText.value = '正在重置...'
const result: { [key: string]: any } = await api.get('site/reset')
if (result.success) {
$toast.success('站点重置成功请等待CookieCloud同步完成')
querySites()
}
else {
$toast.error('站点重置失败!')
}
resetSitesDisabled.value = false
resetSitesText.value = '重置站点数据'
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
querySites()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="索引站点">
<VCardSubtitle> 只有选中的站点才会在搜索和订阅中使用 </VCardSubtitle>
<VCardItem>
<VChipGroup
v-model="selectedSites"
column
multiple
>
<VChip
v-for="site in allSites"
:key="site.id"
filter
variant="outlined"
:value="site.id"
>
{{ site.name }}
</VChip>
</VChipGroup>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveSelectedSites"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="站点重置">
<VCardText>
<div>
<VCheckbox
v-model="isConfirmResetSites"
label="确认删除所有站点数据并重新同步"
/>
</div>
<VBtn
:disabled="!isConfirmResetSites || resetSitesDisabled"
color="error"
class="mt-3"
@click="resetSites"
>
{{ resetSitesText }}
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -0,0 +1,132 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
// 提示框
const $toast = useToast()
// 自定义识别词
const customIdentifiers = ref('')
// 自定义制作组
const customReleaseGroups = ref('')
// 查询已设置的识别词
async function queryCustomIdentifiers() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/CustomIdentifiers',
)
customIdentifiers.value = result.data?.value.join('\n')
}
catch (error) {
console.log(error)
}
}
// 查询已设置的制作组
async function queryCustomReleaseGroups() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/CustomReleaseGroups',
)
customReleaseGroups.value = result.data?.value.join('\n')
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的识别词
async function saveCustomIdentifiers() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/CustomIdentifiers',
customIdentifiers.value.split('\n'),
)
if (result.success)
$toast.success('自定义识别词保存成功')
else
$toast.error('自定义识别词保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存自定义制作组
async function saveCustomReleaseGroups() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/CustomReleaseGroups',
customReleaseGroups.value.split('\n'),
)
if (result.success)
$toast.success('自定义制作组/字幕组保存成功')
else
$toast.error('自定义制作组/字幕组保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryCustomIdentifiers()
queryCustomReleaseGroups()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="自定义识别词">
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customIdentifiers"
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持三种配置格式:
屏蔽词
被替换词 => 替换词
前定位词 <> 后定位词 >> 偏移量EP"
/>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveCustomIdentifiers"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="自定义制作组/字幕组">
<VCardSubtitle> 添加无法识别的制作组/字幕组 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customReleaseGroups"
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
/>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveCustomReleaseGroups"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -1,146 +0,0 @@
<script lang="ts" setup>
import api from '@/api'
import type { Plugin } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import PluginCard from '@/components/cards/PluginCard.vue'
// 数据列表
const dataList = ref<Plugin[]>([])
// 是否刷新过
const isRefreshed = ref(false)
// APP市场窗口
const PluginAppDialog = ref(false)
// 获取已安装的插件列表
const getInstalledPluginList = computed(() => {
return dataList.value.filter(item => item.installed)
})
// 获取未安装的插件列表
const getUninstalledPluginList = computed(() => {
return dataList.value.filter(item => !item.installed)
})
// 关闭插件市场窗口
function pluginDialogClose() {
PluginAppDialog.value = false
}
// 新安装了插件
function pluginInstalled() {
fetchData()
pluginDialogClose()
}
// 获取插件列表数据
async function fetchData() {
try {
dataList.value = await api.get('plugin')
isRefreshed.value = true
}
catch (error) {
console.error(error)
}
}
// 加载时获取数据
onBeforeMount(fetchData)
</script>
<template>
<VProgressCircular
v-if="!isRefreshed"
class="centered"
indeterminate
color="primary"
/>
<div
v-if="dataList.length > 0"
class="grid gap-3 grid-plugin-card"
>
<PluginCard
v-for="data in getInstalledPluginList"
:key="data.id"
:plugin="data"
@remove="fetchData"
/>
</div>
<NoDataFound
v-if="getInstalledPluginList.length === 0 && isRefreshed"
error-code="404"
error-title="没有安装插件"
error-description="点击右下角按钮前往插件市场安装插件"
/>
<!-- App市场 -->
<VDialog
v-model="PluginAppDialog"
fullscreen
:scrim="false"
transition="dialog-bottom-transition"
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<!-- Dialog Content -->
<VCard>
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbarTitle>插件市场</VToolbarTitle>
<VSpacer />
<VToolbarItems>
<VBtn
size="x-large"
@click="pluginDialogClose"
>
<VIcon
color="white"
icon="mdi-close"
/>
</VBtn>
</VToolbarItems>
</VToolbar>
</div>
<div class="pa-4">
<div class="grid gap-3 grid-plugin-card">
<PluginAppCard
v-for="data in getUninstalledPluginList"
:key="data.id"
:plugin="data"
@install="pluginInstalled"
/>
</div>
<NoDataFound
v-if="getUninstalledPluginList.length === 0 && isRefreshed"
error-code="404"
error-title="没有未安装插件"
error-description="所有可用插件均已安装"
/>
</div>
</VCard>
</VDialog>
</template>
<style lang="scss">
.grid-plugin-card {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
padding-block-end: 1rem;
}
dialog-bottom-transition-enter-active,
.dialog-bottom-transition-leave-active {
transition: transform 0.2s ease-in-out;
}
</style>