mirror of
https://github.com/certd/certd.git
synced 2026-08-08 15:39:32 +08:00
chore: plugin 有store page版本
This commit is contained in:
@@ -108,10 +108,10 @@ const projectStore = useProjectStore();
|
||||
<div v-if="!settingStore.isComm" class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full hidden md:block">
|
||||
<fs-button shape="circle" type="text" icon="ion:logo-github" :text="null" @click="goGithub" />
|
||||
</div>
|
||||
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<PageFooter></PageFooter>
|
||||
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
<router-view> </router-view>
|
||||
</template>
|
||||
|
||||
<!--<script lang="ts" setup>-->
|
||||
<!--import { usePageStore } from "/@/store/modules/page";-->
|
||||
<script lang="ts" setup>
|
||||
import { useSettingStore } from "../store/settings";
|
||||
|
||||
<!--const pageStore = usePageStore();-->
|
||||
<!--const keepAlive = pageStore.keepAlive;-->
|
||||
<!--</script>-->
|
||||
const settingsStore = useSettingStore();
|
||||
</script>
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
default: "Default",
|
||||
success: "Success",
|
||||
test: "Test",
|
||||
operation: "Operation",
|
||||
testButton: "Test",
|
||||
operationSuccess: "Operation successful",
|
||||
batch_delete: "Batch Delete",
|
||||
|
||||
@@ -42,6 +42,36 @@ export default {
|
||||
pluginGroup: "Plugin Group",
|
||||
pluginManagement: "Plugin Management",
|
||||
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
||||
localPlugin: "Installed",
|
||||
pluginMarket: "Plugin Market",
|
||||
installedStorePlugin: "Installed",
|
||||
onlinePluginManagement: "Online Plugins",
|
||||
onlinePluginSearch: "Search online plugins",
|
||||
onlinePluginSync: "Sync Plugin Market",
|
||||
onlinePluginSyncFirst: "Sync the plugin market list first, then search and install plugins",
|
||||
onlinePluginSyncSuccess: "Plugin market list synced successfully",
|
||||
onlinePluginLastSyncTime: "Last synced: {time}",
|
||||
onlinePluginNotSynced: "Plugin market has not been synced",
|
||||
onlinePluginRefresh: "Refresh List",
|
||||
onlinePluginInstall: "Install",
|
||||
onlinePluginUpdate: "Update",
|
||||
onlinePluginReinstall: "Reinstall",
|
||||
onlinePluginUninstall: "Uninstall",
|
||||
onlinePluginStatus: "Status",
|
||||
onlinePluginInstalled: "Installed",
|
||||
onlinePluginEnabled: "Enabled",
|
||||
onlinePluginDisabled: "Disabled",
|
||||
onlinePluginClickToEnable: "Click to enable",
|
||||
onlinePluginClickToDisable: "Click to disable",
|
||||
onlinePluginDownloadCount: "Downloads: {count}",
|
||||
onlinePluginNotInstalled: "Not Installed",
|
||||
onlinePluginUpgradeAvailable: "Upgradeable",
|
||||
onlinePluginCurrentVersion: "Current {version}",
|
||||
onlinePluginAlreadyLatest: "Already up to date",
|
||||
onlinePluginClickToUpdate: "Click to update",
|
||||
onlinePluginInstallSuccess: "Online plugin installed successfully",
|
||||
onlinePluginUninstallSuccess: "Online plugin uninstalled successfully",
|
||||
onlinePluginDeleteConfirm: 'Are you sure you want to uninstall online plugin "{name}"? Pipelines using this plugin may fail after deletion.',
|
||||
pleaseSelectRecord: "Please select records first",
|
||||
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
||||
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
||||
|
||||
@@ -50,6 +50,7 @@ export default {
|
||||
default: "默认",
|
||||
success: "成功",
|
||||
test: "测试",
|
||||
operation: "操作",
|
||||
testButton: "测试",
|
||||
operationSuccess: "操作成功",
|
||||
batch_delete: "批量删除",
|
||||
|
||||
@@ -41,6 +41,36 @@ export default {
|
||||
pluginGroup: "插件分组",
|
||||
pluginManagement: "插件管理",
|
||||
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
||||
localPlugin: "本地插件",
|
||||
pluginMarket: "插件市场",
|
||||
installedStorePlugin: "已安装",
|
||||
onlinePluginManagement: "在线插件",
|
||||
onlinePluginSearch: "搜索在线插件",
|
||||
onlinePluginSync: "同步插件市场",
|
||||
onlinePluginSyncFirst: "请先同步插件市场列表,然后再搜索和安装插件",
|
||||
onlinePluginSyncSuccess: "插件市场列表同步成功",
|
||||
onlinePluginLastSyncTime: "上次同步时间:{time}",
|
||||
onlinePluginNotSynced: "尚未同步插件市场",
|
||||
onlinePluginRefresh: "刷新列表",
|
||||
onlinePluginInstall: "安装",
|
||||
onlinePluginUpdate: "更新",
|
||||
onlinePluginReinstall: "重新安装",
|
||||
onlinePluginUninstall: "卸载",
|
||||
onlinePluginStatus: "状态",
|
||||
onlinePluginInstalled: "已安装",
|
||||
onlinePluginEnabled: "已启用",
|
||||
onlinePluginDisabled: "已禁用",
|
||||
onlinePluginClickToEnable: "点击启用",
|
||||
onlinePluginClickToDisable: "点击禁用",
|
||||
onlinePluginDownloadCount: "下载量:{count}",
|
||||
onlinePluginNotInstalled: "未安装",
|
||||
onlinePluginUpgradeAvailable: "可更新",
|
||||
onlinePluginCurrentVersion: "当前 {version}",
|
||||
onlinePluginAlreadyLatest: "当前已是最新版本",
|
||||
onlinePluginClickToUpdate: "点击更新",
|
||||
onlinePluginInstallSuccess: "在线插件安装成功",
|
||||
onlinePluginUninstallSuccess: "在线插件卸载成功",
|
||||
onlinePluginDeleteConfirm: "确定要卸载在线插件「{name}」吗?如果该插件已被流水线使用,删除可能会导致执行失败。",
|
||||
pleaseSelectRecord: "请先勾选记录",
|
||||
clearRuntimeDeps: "清理第三方依赖缓存",
|
||||
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
||||
|
||||
+57
-117
@@ -12,59 +12,21 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
||||
<a-space>
|
||||
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="currentStep">
|
||||
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
||||
<template #header>
|
||||
<a-row :gutter="10" class="mb-10">
|
||||
<a-col :span="24" style="padding-left: 20px">
|
||||
<a-input-search v-model:value="pluginSearch.keyword" placeholder="搜索插件" :allow-clear="true" :show-search="true"></a-input-search>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<div class="flex-col h-100 overflow-hidden md:ml-5 md:mr-5 step-form-body">
|
||||
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="flex-1 overflow-hidden h-full">
|
||||
<template v-for="group of computedPluginGroups" :key="group.key">
|
||||
<a-tab-pane v-if="(group.key === 'admin' && userStore.isAdmin) || group.key !== 'admin'" :key="group.key" class="scroll-y">
|
||||
<template #tab>
|
||||
<div class="cd-step-form-tab-label">
|
||||
<fs-icon :icon="group.icon" class="mr-2" />
|
||||
<div>{{ group.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-row v-if="!group.plugins || group.plugins.length === 0" :gutter="10">
|
||||
<a-col class="flex-o">
|
||||
<div class="flex-o m-10">没有找到插件</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-else :gutter="10">
|
||||
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin w-full md:w-[50%]">
|
||||
<a-card
|
||||
hoverable
|
||||
:class="{ current: item.name === currentStep.type }"
|
||||
@click="stepTypeSelected(item)"
|
||||
@dblclick="
|
||||
stepTypeSelected(item);
|
||||
stepTypeSave();
|
||||
"
|
||||
>
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<fs-icon class="plugin-icon" :icon="item.icon || 'clarity:plugin-line'"></fs-icon>
|
||||
<span class="title" :title="item.title">{{ item.title }}</span>
|
||||
<vip-button v-if="item.needPlus" mode="icon" />
|
||||
</template>
|
||||
<template #description>
|
||||
<span :title="item.desc" v-html="transformDesc(item.desc)"></span>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</template>
|
||||
<a-tabs v-model:active-key="pluginSourceActive" class="step-plugin-source-tabs flex-1 overflow-hidden h-full">
|
||||
<a-tab-pane key="local" tab="已安装插件" class="h-full">
|
||||
<LocalPluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="userStore.isAdmin" key="market" tab="插件市场" class="h-full step-market-pane">
|
||||
<OnlinePluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" @uninstalled="handleOnlinePluginUninstalled" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -117,28 +79,30 @@
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { computed, provide, ref, Ref, watch } from "vue";
|
||||
import { provide, ref, Ref } from "vue";
|
||||
import { merge, cloneDeep } from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
import { usePluginStore, PluginGroups } from "/@/store/plugin";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useCompute } from "@fast-crud/fast-crud";
|
||||
import { useReference } from "/@/use/use-refrence";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { mitter } from "/@/utils/util.mitt";
|
||||
import { utils } from "/@/utils";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import LocalPluginSelector from "./local-plugin-selector.vue";
|
||||
import OnlinePluginSelector from "./online-plugin-selector.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "PiStepForm",
|
||||
});
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
defineEmits(["update"]);
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const userStore = useUserStore();
|
||||
@@ -171,6 +135,9 @@ function useStepForm() {
|
||||
});
|
||||
|
||||
const stepTypeSelected = (item: any) => {
|
||||
if (item.__online) {
|
||||
return;
|
||||
}
|
||||
if (item.needPlus && !settingStore.isPlus) {
|
||||
message.warn("此插件需要开通Certd专业版才能使用");
|
||||
mitter.emit("openVipModal");
|
||||
@@ -339,55 +306,24 @@ function useStepForm() {
|
||||
};
|
||||
};
|
||||
|
||||
const pluginSearch = ref({
|
||||
keyword: "",
|
||||
result: [],
|
||||
});
|
||||
const pluginGroupActive = ref("all");
|
||||
const pluginGroup: Ref = ref();
|
||||
const pluginStore = usePluginStore();
|
||||
const pluginSourceActive = ref("local");
|
||||
const handlePluginConfirm = async (item: any) => {
|
||||
stepTypeSelected(item);
|
||||
await stepTypeSave();
|
||||
};
|
||||
|
||||
async function loadPluginGroups() {
|
||||
pluginGroup.value = await pluginStore.getGroups();
|
||||
}
|
||||
|
||||
loadPluginGroups();
|
||||
const computedPluginGroups: any = computed(() => {
|
||||
if (!pluginGroup.value) {
|
||||
return {};
|
||||
const handleOnlinePluginUninstalled = (plugin: any) => {
|
||||
if (currentStep.value.type === plugin.fullName) {
|
||||
currentStep.value.type = undefined;
|
||||
currentStep.value.title = "新任务";
|
||||
}
|
||||
const group = pluginGroup.value as PluginGroups;
|
||||
const groups = group.groups;
|
||||
if (pluginSearch.value.keyword) {
|
||||
const keyword = pluginSearch.value.keyword.toLowerCase();
|
||||
const list = groups.all.plugins.filter((plugin: any) => {
|
||||
return plugin.title?.toLowerCase().includes(keyword) || plugin.desc?.toLowerCase().includes(keyword) || plugin.name?.toLowerCase().includes(keyword);
|
||||
});
|
||||
return {
|
||||
search: { key: "search", title: "搜索结果", plugins: list },
|
||||
};
|
||||
} else {
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => {
|
||||
return pluginSearch.value.keyword;
|
||||
},
|
||||
(val: any) => {
|
||||
if (val) {
|
||||
pluginGroupActive.value = "search";
|
||||
} else {
|
||||
pluginGroupActive.value = "all";
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
pluginGroupActive,
|
||||
computedPluginGroups,
|
||||
pluginSearch,
|
||||
pluginSourceActive,
|
||||
stepTypeSelected,
|
||||
handlePluginConfirm,
|
||||
handleOnlinePluginUninstalled,
|
||||
stepTypeSave,
|
||||
stepFormRef,
|
||||
mode,
|
||||
@@ -436,31 +372,27 @@ const labelCol = ref({ span: 6 });
|
||||
const wrapperCol = ref({ span: 16 });
|
||||
|
||||
const stepFormRes = useStepForm();
|
||||
const { pluginGroupActive, computedPluginGroups, pluginSearch, stepTypeSelected, stepTypeSave, stepFormRef, stepDrawerVisible, currentStep, currentPlugin, stepSave, stepDelete, getScopeFunc, fullscreen } = stepFormRes;
|
||||
const {
|
||||
pluginSourceActive,
|
||||
stepTypeSelected,
|
||||
handlePluginConfirm,
|
||||
handleOnlinePluginUninstalled,
|
||||
stepTypeSave,
|
||||
stepFormRef,
|
||||
stepDrawerVisible,
|
||||
currentStep,
|
||||
currentPlugin,
|
||||
stepSave,
|
||||
stepDelete,
|
||||
getScopeFunc,
|
||||
fullscreen,
|
||||
} = stepFormRes;
|
||||
defineExpose({
|
||||
...stepFormRes,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.cd-step-form-tab-label {
|
||||
// 包括dropdown
|
||||
display: flex;
|
||||
align-items: center;
|
||||
//width: 120px;
|
||||
.fs-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #00b7ff;
|
||||
|
||||
svg {
|
||||
vertical-align: middle !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-form-drawer {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -514,6 +446,10 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
font-size: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.body {
|
||||
@@ -542,7 +478,9 @@ defineExpose({
|
||||
.ant-card-meta-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.ant-avatar {
|
||||
@@ -558,19 +496,21 @@ defineExpose({
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 14px;
|
||||
height: 100px;
|
||||
min-height: 100px;
|
||||
padding-bottom: 6px;
|
||||
|
||||
overflow-y: hidden;
|
||||
|
||||
.ant-card-meta-description {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
color: #7f7f7f;
|
||||
}
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="step-plugin-source-pane-local">
|
||||
<div class="step-plugin-search">
|
||||
<a-input-search v-model:value="localPluginSearch.keyword" placeholder="搜索本地插件" :allow-clear="true" :show-search="true"></a-input-search>
|
||||
</div>
|
||||
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="flex-1 overflow-hidden h-full">
|
||||
<a-tab-pane v-for="group of computedLocalPluginGroups" :key="group.key" class="scroll-y">
|
||||
<template #tab>
|
||||
<div class="cd-step-form-tab-label" @click="handleLocalPluginGroupChange(group.key)">
|
||||
<fs-icon :icon="group.icon" class="mr-2" />
|
||||
<div>{{ group.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!group.plugins || group.plugins.length === 0" class="step-plugin-empty">没有找到插件</div>
|
||||
<a-row v-else :gutter="10">
|
||||
<a-col v-for="item of group.plugins" :key="item.key || item.name" class="step-plugin w-full md:w-[50%]">
|
||||
<a-card hoverable :class="{ current: item.name === selectedType }" @click="emit('select', item)" @dblclick="emit('confirm', item)">
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<fs-icon class="plugin-icon" :icon="item.icon || 'clarity:plugin-line'"></fs-icon>
|
||||
<span class="title" :title="item.title">{{ item.title }}</span>
|
||||
<vip-button v-if="item.needPlus" mode="icon" />
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="plugin-card-desc" :title="item.desc" v-html="transformDesc(item.desc)"></div>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, Ref, watch } from "vue";
|
||||
import { PluginGroups, usePluginStore } from "/@/store/plugin";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import { utils } from "/@/utils";
|
||||
|
||||
defineProps({
|
||||
selectedType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select", plugin: any): void;
|
||||
(e: "confirm", plugin: any): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const pluginStore = usePluginStore();
|
||||
const pluginGroupActive = ref("all");
|
||||
const pluginGroup: Ref<PluginGroups | undefined> = ref();
|
||||
const localPluginSearch = ref({
|
||||
keyword: "",
|
||||
});
|
||||
|
||||
function transformDesc(desc: string = "") {
|
||||
return utils.transformLink(desc);
|
||||
}
|
||||
|
||||
function matchKeyword(plugin: any, keyword: string) {
|
||||
const fields = [plugin.title, plugin.desc, plugin.name, plugin.fullName, plugin.author, plugin.latest];
|
||||
return fields.some(field =>
|
||||
String(field || "")
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
async function loadPluginGroups() {
|
||||
pluginGroup.value = await pluginStore.getGroups();
|
||||
}
|
||||
|
||||
function handleLocalPluginGroupChange(groupKey: string) {
|
||||
pluginGroupActive.value = groupKey;
|
||||
localPluginSearch.value.keyword = "";
|
||||
}
|
||||
|
||||
const computedLocalPluginGroups: any = computed(() => {
|
||||
if (!pluginGroup.value) {
|
||||
return {};
|
||||
}
|
||||
const groups = pluginGroup.value.groups;
|
||||
const keyword = localPluginSearch.value.keyword.trim().toLowerCase();
|
||||
const visibleGroups: any = {};
|
||||
for (const groupKey of Object.keys(groups)) {
|
||||
if (groupKey === "admin" && !userStore.isAdmin) {
|
||||
continue;
|
||||
}
|
||||
visibleGroups[groupKey] = groups[groupKey];
|
||||
}
|
||||
if (!keyword) {
|
||||
return visibleGroups;
|
||||
}
|
||||
const filteredGroups: any = {};
|
||||
for (const groupKey of Object.keys(visibleGroups)) {
|
||||
const currentGroup = visibleGroups[groupKey];
|
||||
filteredGroups[groupKey] = {
|
||||
...currentGroup,
|
||||
plugins: currentGroup.plugins.filter((plugin: any) => {
|
||||
return matchKeyword(plugin, keyword);
|
||||
}),
|
||||
};
|
||||
}
|
||||
return filteredGroups;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return localPluginSearch.value.keyword;
|
||||
},
|
||||
val => {
|
||||
if (val) {
|
||||
pluginGroupActive.value = "all";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
loadPluginGroups();
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.step-plugin-source-pane-local {
|
||||
.cd-step-form-tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fs-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #00b7ff;
|
||||
|
||||
svg {
|
||||
vertical-align: middle !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-plugin-source-pane {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-plugin-search {
|
||||
flex: none;
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.step-plugin-empty {
|
||||
display: flex;
|
||||
min-height: 320px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7b8794;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.plugin-card-desc {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+461
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="step-plugin-source-pane-online">
|
||||
<div class="step-plugin-search">
|
||||
<a-input-search v-model:value="onlinePluginSearch.keyword" placeholder="搜索插件市场" :allow-clear="true" :show-search="true" :loading="onlineLoading" @search="handleOnlinePluginSearch"></a-input-search>
|
||||
</div>
|
||||
<a-tabs v-model:active-key="onlinePluginGroupActive" tab-position="left" class="flex-1 overflow-hidden h-full step-market-tabs">
|
||||
<a-tab-pane v-for="group of computedOnlinePluginGroups" :key="group.key" class="scroll-y">
|
||||
<template #tab>
|
||||
<div class="cd-step-form-tab-label" @click="handleOnlinePluginGroupChange(group.key)">
|
||||
<fs-icon :icon="group.icon" class="mr-2" />
|
||||
<div>{{ group.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="onlineLoading" class="step-market-state">正在加载插件市场...</div>
|
||||
<div v-else-if="!group.plugins || group.plugins.length === 0" class="step-plugin-empty">没有找到插件</div>
|
||||
<template v-else>
|
||||
<div class="step-market-content">
|
||||
<a-row :gutter="[12, 12]">
|
||||
<a-col v-for="item of group.plugins" :key="item.key || item.fullName" class="step-plugin market-plugin-col w-full md:w-[50%]">
|
||||
<PluginItemCard
|
||||
:plugin="item"
|
||||
simple
|
||||
:current="isOnlinePluginCurrent(item)"
|
||||
:install-loading="isOnlineActionLoading(item, 'install')"
|
||||
:uninstall-loading="isOnlineActionLoading(item, 'uninstall')"
|
||||
:toggle-loading="isOnlineActionLoading(item, 'toggle')"
|
||||
@click="onlinePluginSelected(item)"
|
||||
@dblclick="handleOnlinePluginCardDblclick(item)"
|
||||
@install="installOnlineStepPlugin"
|
||||
@uninstall="uninstallOnlineStepPlugin"
|
||||
@toggle-disabled="toggleOnlineStepPluginDisabled"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="online-plugin-pagination">
|
||||
<a-pagination v-model:current="onlinePage" size="small" :page-size="onlinePageSize" :total="onlineCurrentTotal" :show-size-changer="false" simple />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { computed, ref, Ref, watch } from "vue";
|
||||
import { PluginGroups, usePluginStore } from "/@/store/plugin";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import PluginItemCard from "/@/views/sys/plugin/components/plugin-item-card.vue";
|
||||
import * as pluginApi from "/@/views/sys/plugin/api";
|
||||
import { useI18n } from "/src/locales";
|
||||
|
||||
const props = defineProps({
|
||||
selectedType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select", plugin: any): void;
|
||||
(e: "confirm", plugin: any): void;
|
||||
(e: "uninstalled", plugin: any): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const pluginStore = usePluginStore();
|
||||
const pluginGroup: Ref<PluginGroups | undefined> = ref();
|
||||
const onlinePlugins: Ref<pluginApi.OnlinePluginBean[]> = ref([]);
|
||||
const onlineGroupPlugins: Ref<pluginApi.OnlinePluginBean[]> = ref([]);
|
||||
const onlineLoading: Ref<boolean> = ref(false);
|
||||
const onlineActionLoading: Ref<string> = ref("");
|
||||
const onlineGroupLoaded: Ref<boolean> = ref(false);
|
||||
const onlinePluginSearch = ref({
|
||||
keyword: "",
|
||||
});
|
||||
const onlinePluginQueryKeyword = ref("");
|
||||
const onlinePluginGroupActive = ref("all");
|
||||
const onlinePage = ref(1);
|
||||
const onlinePageSize = 8;
|
||||
let onlineRequestId = 0;
|
||||
|
||||
async function loadPluginGroups() {
|
||||
pluginGroup.value = await pluginStore.getGroups();
|
||||
}
|
||||
|
||||
function getOnlinePluginListReq(req?: { groupKey?: string; keyword?: string }) {
|
||||
const groupKey = req?.groupKey ?? onlinePluginGroupActive.value;
|
||||
const keyword = (req?.keyword ?? onlinePluginQueryKeyword.value).trim();
|
||||
const query: { pluginType: string; group?: string; keyword?: string } = {
|
||||
pluginType: "deploy",
|
||||
};
|
||||
if (groupKey && groupKey !== "all") {
|
||||
query.group = groupKey;
|
||||
}
|
||||
if (keyword) {
|
||||
query.keyword = keyword;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
async function loadOnlinePluginGroups(force = false) {
|
||||
if (!userStore.isAdmin) {
|
||||
return [];
|
||||
}
|
||||
if (onlineGroupLoaded.value && !force) {
|
||||
return onlineGroupPlugins.value;
|
||||
}
|
||||
const list = await pluginApi.OnlinePluginList({ pluginType: "deploy" });
|
||||
onlineGroupPlugins.value = list;
|
||||
onlineGroupLoaded.value = true;
|
||||
return list;
|
||||
}
|
||||
|
||||
async function loadOnlinePlugins(force = false, options?: { silent?: boolean }) {
|
||||
if (!userStore.isAdmin) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++onlineRequestId;
|
||||
if (!options?.silent) {
|
||||
onlineLoading.value = true;
|
||||
}
|
||||
try {
|
||||
const groupPlugins = await loadOnlinePluginGroups(force);
|
||||
if (requestId !== onlineRequestId) {
|
||||
return;
|
||||
}
|
||||
const query = getOnlinePluginListReq();
|
||||
const isFullQuery = !query.group && !query.keyword;
|
||||
if (isFullQuery) {
|
||||
onlinePlugins.value = groupPlugins;
|
||||
} else {
|
||||
const list = await pluginApi.OnlinePluginList(query);
|
||||
if (requestId !== onlineRequestId) {
|
||||
return;
|
||||
}
|
||||
onlinePlugins.value = list;
|
||||
}
|
||||
} finally {
|
||||
if (!options?.silent && requestId === onlineRequestId) {
|
||||
onlineLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getOnlinePluginDisplayName(plugin: pluginApi.OnlinePluginBean) {
|
||||
if (plugin.fullName) {
|
||||
return plugin.fullName;
|
||||
}
|
||||
if (plugin.author && plugin.name) {
|
||||
return `${plugin.author}/${plugin.name}`;
|
||||
}
|
||||
return plugin.name || "";
|
||||
}
|
||||
|
||||
function buildOnlinePluginCard(plugin: pluginApi.OnlinePluginBean) {
|
||||
const fullName = getOnlinePluginDisplayName(plugin);
|
||||
return {
|
||||
...plugin,
|
||||
key: `online:${fullName}`,
|
||||
name: fullName,
|
||||
fullName,
|
||||
title: plugin.title || plugin.name || fullName,
|
||||
desc: plugin.desc,
|
||||
icon: plugin.icon || "clarity:plugin-line",
|
||||
__online: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getOnlinePluginGroupKey(plugin: pluginApi.OnlinePluginBean) {
|
||||
return (plugin.group || "other").trim() || "other";
|
||||
}
|
||||
|
||||
function getOnlinePluginGroupMeta(groupKey: string) {
|
||||
const localGroups = pluginGroup.value?.groups || {};
|
||||
const localGroup = localGroups[groupKey];
|
||||
if (localGroup) {
|
||||
return {
|
||||
title: localGroup.title,
|
||||
icon: localGroup.icon,
|
||||
order: localGroup.order,
|
||||
};
|
||||
}
|
||||
if (groupKey === "other") {
|
||||
return {
|
||||
title: "其他",
|
||||
icon: "clarity:plugin-line",
|
||||
order: 9999,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: groupKey,
|
||||
icon: "clarity:plugin-line",
|
||||
order: 1000,
|
||||
};
|
||||
}
|
||||
|
||||
function getFilteredOnlinePlugins() {
|
||||
return onlinePlugins.value.map(plugin => buildOnlinePluginCard(plugin));
|
||||
}
|
||||
|
||||
function getOnlinePluginsByGroup(groupKey: string) {
|
||||
const list = getFilteredOnlinePlugins();
|
||||
if (groupKey === "all") {
|
||||
return list;
|
||||
}
|
||||
return list.filter(plugin => getOnlinePluginGroupKey(plugin) === groupKey);
|
||||
}
|
||||
|
||||
function getPagedOnlinePlugins(groupKey: string) {
|
||||
const list = getOnlinePluginsByGroup(groupKey);
|
||||
const start = (onlinePage.value - 1) * onlinePageSize;
|
||||
return list.slice(start, start + onlinePageSize);
|
||||
}
|
||||
|
||||
function getOnlinePluginActionKey(plugin: pluginApi.OnlinePluginBean, action: "install" | "uninstall" | "toggle") {
|
||||
if (!plugin.fullName) {
|
||||
return "";
|
||||
}
|
||||
return `${action}:${plugin.fullName}`;
|
||||
}
|
||||
|
||||
function isOnlineActionLoading(plugin: pluginApi.OnlinePluginBean, action: "install" | "uninstall" | "toggle") {
|
||||
return onlineActionLoading.value === getOnlinePluginActionKey(plugin, action);
|
||||
}
|
||||
|
||||
async function installOnlineStepPlugin(plugin: any) {
|
||||
if (!plugin.fullName) {
|
||||
return;
|
||||
}
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(plugin, "install");
|
||||
try {
|
||||
await pluginApi.OnlinePluginInstall({
|
||||
fullName: plugin.fullName,
|
||||
version: plugin.latest,
|
||||
});
|
||||
message.success(t("certd.onlinePluginInstallSuccess"));
|
||||
await pluginStore.reload();
|
||||
await loadPluginGroups();
|
||||
await loadOnlinePlugins(true, { silent: true });
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function uninstallOnlineStepPlugin(plugin: any) {
|
||||
if (!plugin.localPluginId || !plugin.fullName) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: t("certd.onlinePluginDeleteConfirm", { name: plugin.fullName }),
|
||||
async onOk() {
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(plugin, "uninstall");
|
||||
try {
|
||||
await pluginApi.DelObj(plugin.localPluginId);
|
||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
||||
if (props.selectedType === plugin.fullName) {
|
||||
emit("uninstalled", plugin);
|
||||
}
|
||||
await pluginStore.reload();
|
||||
await loadPluginGroups();
|
||||
await loadOnlinePlugins(true, { silent: true });
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toggleOnlineStepPluginDisabled(plugin: any) {
|
||||
if (!plugin.localPluginId || !plugin.fullName) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${!plugin.localDisabled ? t("certd.disable") : t("certd.enable")}?`,
|
||||
async onOk() {
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(plugin, "toggle");
|
||||
try {
|
||||
await pluginApi.SetDisabled({
|
||||
id: plugin.localPluginId,
|
||||
name: plugin.name,
|
||||
type: "store",
|
||||
disabled: !plugin.localDisabled,
|
||||
});
|
||||
message.success(t("certd.operationSuccess"));
|
||||
await pluginStore.reload();
|
||||
await loadPluginGroups();
|
||||
await loadOnlinePlugins(true, { silent: true });
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getInstalledPlugin(plugin: any) {
|
||||
if (!plugin.installed || !plugin.fullName) {
|
||||
return null;
|
||||
}
|
||||
const groups = pluginGroup.value;
|
||||
if (!groups) {
|
||||
return null;
|
||||
}
|
||||
const candidateNames = [plugin.fullName, plugin.name, plugin.author && plugin.name ? `${plugin.author}/${plugin.name}` : ""].filter(Boolean);
|
||||
for (const name of candidateNames) {
|
||||
const installedPlugin = groups.get(name);
|
||||
if (installedPlugin) {
|
||||
return installedPlugin;
|
||||
}
|
||||
}
|
||||
return groups.groups.all.plugins.find((item: any) => {
|
||||
if (plugin.localPluginId && item.id === plugin.localPluginId) {
|
||||
return true;
|
||||
}
|
||||
return item.author === plugin.author && item.name === plugin.name;
|
||||
});
|
||||
}
|
||||
|
||||
function isOnlinePluginCurrent(plugin: any) {
|
||||
const installedPlugin = getInstalledPlugin(plugin);
|
||||
return !!installedPlugin && installedPlugin.name === props.selectedType;
|
||||
}
|
||||
|
||||
function onlinePluginSelected(plugin: any) {
|
||||
const installedPlugin = getInstalledPlugin(plugin);
|
||||
if (!installedPlugin) {
|
||||
return;
|
||||
}
|
||||
emit("select", installedPlugin);
|
||||
}
|
||||
|
||||
function handleOnlinePluginCardDblclick(plugin: any) {
|
||||
const installedPlugin = getInstalledPlugin(plugin);
|
||||
if (!installedPlugin) {
|
||||
return;
|
||||
}
|
||||
emit("confirm", installedPlugin);
|
||||
}
|
||||
|
||||
function handleOnlinePluginGroupChange(groupKey: string) {
|
||||
onlinePluginGroupActive.value = groupKey;
|
||||
onlinePluginSearch.value.keyword = "";
|
||||
onlinePluginQueryKeyword.value = "";
|
||||
onlinePage.value = 1;
|
||||
loadOnlinePlugins();
|
||||
}
|
||||
|
||||
function handleOnlinePluginSearch(value: string) {
|
||||
onlinePluginQueryKeyword.value = (value || "").trim();
|
||||
onlinePluginGroupActive.value = "all";
|
||||
onlinePage.value = 1;
|
||||
loadOnlinePlugins();
|
||||
}
|
||||
|
||||
const computedOnlinePluginGroups: any = computed(() => {
|
||||
const allPlugins = onlineGroupPlugins.value.map(plugin => buildOnlinePluginCard(plugin));
|
||||
const groups: any = {
|
||||
all: {
|
||||
key: "all",
|
||||
title: t("certd.all"),
|
||||
order: 0,
|
||||
icon: "material-symbols:border-all-rounded",
|
||||
plugins: getPagedOnlinePlugins("all"),
|
||||
},
|
||||
};
|
||||
const groupKeys = Array.from(new Set(allPlugins.map(plugin => getOnlinePluginGroupKey(plugin))));
|
||||
groupKeys.sort((left, right) => {
|
||||
const leftMeta = getOnlinePluginGroupMeta(left);
|
||||
const rightMeta = getOnlinePluginGroupMeta(right);
|
||||
return leftMeta.order - rightMeta.order || left.localeCompare(right);
|
||||
});
|
||||
for (const groupKey of groupKeys) {
|
||||
const meta = getOnlinePluginGroupMeta(groupKey);
|
||||
groups[groupKey] = {
|
||||
key: groupKey,
|
||||
title: meta.title,
|
||||
order: meta.order,
|
||||
icon: meta.icon,
|
||||
plugins: getPagedOnlinePlugins(groupKey),
|
||||
};
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
const onlineTotalPage = computed(() => {
|
||||
const total = getOnlinePluginsByGroup(onlinePluginGroupActive.value).length;
|
||||
return Math.max(1, Math.ceil(total / onlinePageSize));
|
||||
});
|
||||
|
||||
const onlineCurrentTotal = computed(() => {
|
||||
return getOnlinePluginsByGroup(onlinePluginGroupActive.value).length;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return onlinePluginGroupActive.value;
|
||||
},
|
||||
() => {
|
||||
onlinePage.value = 1;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return onlineTotalPage.value;
|
||||
},
|
||||
totalPage => {
|
||||
if (onlinePage.value > totalPage) {
|
||||
onlinePage.value = totalPage;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
loadPluginGroups();
|
||||
loadOnlinePlugins();
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.step-plugin-source-pane-online {
|
||||
.step-market-content {
|
||||
min-height: 100%;
|
||||
padding: 2px 2px 12px 0;
|
||||
}
|
||||
|
||||
.step-market-state {
|
||||
display: flex;
|
||||
min-height: 320px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7b8794;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.market-plugin-col {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.online-plugin-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-height: 42px;
|
||||
padding: 10px 2px 0;
|
||||
color: #5f6b7a;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
.ant-pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-pagination-simple-pager {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +82,51 @@ export async function ImportPlugin(body: any) {
|
||||
});
|
||||
}
|
||||
|
||||
export type OnlinePluginBean = {
|
||||
id?: number;
|
||||
appId?: number;
|
||||
author?: string;
|
||||
pluginType?: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
group?: string;
|
||||
desc?: string;
|
||||
latest?: string;
|
||||
status?: string;
|
||||
downloadCount?: number;
|
||||
installed?: boolean;
|
||||
installedVersion?: string;
|
||||
upgradeAvailable?: boolean;
|
||||
localPluginId?: number;
|
||||
localDisabled?: boolean;
|
||||
syncTime?: number;
|
||||
};
|
||||
|
||||
export async function OnlinePluginList(body: { pluginType?: string; group?: string; keyword?: string }): Promise<OnlinePluginBean[]> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/list",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginSync(): Promise<OnlinePluginBean[]> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/sync",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginInstall(body: { fullName: string; version?: string }) {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/install",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export type PluginConfigBean = {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<a-card
|
||||
hoverable
|
||||
class="plugin-card plugin-item-card"
|
||||
:class="{
|
||||
current,
|
||||
'is-simple': simple,
|
||||
'is-local': source === 'local',
|
||||
'is-installed': isInstalled,
|
||||
'is-disabled': isDisabled,
|
||||
}"
|
||||
@click="emit('click', plugin)"
|
||||
@dblclick="emit('dblclick', plugin)"
|
||||
>
|
||||
<div class="plugin-card__head">
|
||||
<div class="plugin-card__main">
|
||||
<fs-icon class="plugin-icon plugin-card__icon" :icon="plugin.icon || 'clarity:plugin-line'" />
|
||||
<div class="plugin-card__title-wrap">
|
||||
<router-link v-if="source === 'local' && plugin.type === 'custom'" class="plugin-card__title" :title="plugin.title || plugin.name" :to="`/sys/plugin/edit?id=${plugin.id}`" @click.stop>
|
||||
{{ plugin.title || plugin.name }}
|
||||
</router-link>
|
||||
<span v-else class="plugin-card__title" :title="plugin.title || plugin.name">{{ plugin.title || plugin.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-card__actions">
|
||||
<template v-if="source === 'local'">
|
||||
<div class="plugin-card__tools">
|
||||
<a-tooltip v-if="plugin.type === 'custom'" title="编辑">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="emit('edit', plugin)">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:create-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="plugin.type === 'custom'" title="复制">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="emit('copy', plugin)">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:copy-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="plugin.type === 'custom'" :title="t('certd.export')">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="emit('export-plugin', plugin)">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:cloud-download-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="showConfig" title="配置">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="emit('config', plugin)">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:settings-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="canRemoveLocal" class="plugin-card__action-zone plugin-card__local-action-zone">
|
||||
<a-tag color="green" class="plugin-status-tag">{{ t("certd.onlinePluginInstalled") }}</a-tag>
|
||||
<a-button class="plugin-card__action-button" size="small" danger ghost @click.stop="emit('remove', plugin)">
|
||||
{{ t("certd.onlinePluginUninstall") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="plugin.installed">
|
||||
<div v-if="plugin.localPluginId" class="plugin-card__action-zone" :class="{ 'is-loading': uninstallLoading }">
|
||||
<a-tag color="green" class="plugin-status-tag">{{ t("certd.onlinePluginInstalled") }}</a-tag>
|
||||
<a-button class="plugin-card__action-button" size="small" danger ghost :loading="uninstallLoading" @click.stop="emit('uninstall', plugin)">
|
||||
{{ t("certd.onlinePluginUninstall") }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-tag v-else color="green" class="plugin-status-tag">{{ t("certd.onlinePluginInstalled") }}</a-tag>
|
||||
</template>
|
||||
<div v-else class="plugin-card__action-zone plugin-card__install-zone" :class="{ 'is-loading': installLoading }">
|
||||
<a-button class="plugin-card__action-button" size="small" type="primary" :loading="installLoading" @click.stop="emit('install', plugin)">
|
||||
{{ t("certd.onlinePluginInstall") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-card__desc" :title="plugin.desc">
|
||||
{{ plugin.desc || "暂无描述" }}
|
||||
</div>
|
||||
|
||||
<div class="plugin-card__meta">
|
||||
<template v-if="source === 'local'">
|
||||
<a-tooltip :title="toggleTitle">
|
||||
<a-tag class="plugin-status-tag" :color="plugin.disabled ? 'default' : 'green'" @click.stop="emit('toggle-disabled', plugin)">
|
||||
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tag>{{ pluginTypeLabel }}</a-tag>
|
||||
<a-tag>{{ sourceLabel }}</a-tag>
|
||||
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="!simple">
|
||||
<a-tooltip v-if="plugin.installed && plugin.localPluginId" :title="toggleTitle">
|
||||
<a-tag class="plugin-status-tag" :class="{ 'is-loading': toggleLoading }" :color="plugin.localDisabled ? 'default' : 'green'" @click.stop="emit('toggle-disabled', plugin)">
|
||||
{{ plugin.localDisabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tag>{{ pluginTypeLabel }}</a-tag>
|
||||
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
|
||||
<a-tooltip :title="t('certd.onlinePluginDownloadCount', { count: plugin.downloadCount || 0 })">
|
||||
<a-tag class="plugin-card__download-count">
|
||||
<fs-icon icon="ion:cloud-download-outline" />
|
||||
<span>{{ formatDownloadCount(plugin.downloadCount || 0) }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip :title="versionTitle">
|
||||
<span class="plugin-card__version" :class="{ 'is-upgradable': plugin.upgradeAvailable }" @click.stop="handleVersionClick">
|
||||
v{{ currentVersion }}
|
||||
<fs-icon v-if="plugin.upgradeAvailable" class="plugin-card__version-icon" icon="carbon:upgrade" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<span v-if="source !== 'local' && authorName" class="plugin-card__author" :title="`${t('certd.author')}:${authorName}`">
|
||||
<fs-icon icon="ion:person-circle-outline" />
|
||||
<span>{{ authorName }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
|
||||
defineOptions({
|
||||
name: "PluginItemCard",
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
plugin: any;
|
||||
source?: "local" | "market";
|
||||
current?: boolean;
|
||||
simple?: boolean;
|
||||
showConfig?: boolean;
|
||||
installLoading?: boolean;
|
||||
uninstallLoading?: boolean;
|
||||
toggleLoading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
source: "market",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", plugin: any): void;
|
||||
(e: "dblclick", plugin: any): void;
|
||||
(e: "edit", plugin: any): void;
|
||||
(e: "copy", plugin: any): void;
|
||||
(e: "export-plugin", plugin: any): void;
|
||||
(e: "config", plugin: any): void;
|
||||
(e: "remove", plugin: any): void;
|
||||
(e: "install", plugin: any): void;
|
||||
(e: "uninstall", plugin: any): void;
|
||||
(e: "toggle-disabled", plugin: any): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isInstalled = computed(() => {
|
||||
return props.source !== "local" && props.plugin.installed;
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return props.source === "local" ? props.plugin.disabled : props.plugin.localDisabled;
|
||||
});
|
||||
|
||||
const canRemoveLocal = computed(() => {
|
||||
return props.plugin.type === "custom" || props.plugin.type === "store";
|
||||
});
|
||||
|
||||
const fullName = computed(() => {
|
||||
if (props.plugin.fullName) {
|
||||
return props.plugin.fullName;
|
||||
}
|
||||
if (props.plugin.author && props.plugin.name) {
|
||||
return `${props.plugin.author}/${props.plugin.name}`;
|
||||
}
|
||||
return props.plugin.name || "";
|
||||
});
|
||||
|
||||
const authorName = computed(() => {
|
||||
if (props.plugin.author) {
|
||||
return props.plugin.author;
|
||||
}
|
||||
const [author] = fullName.value.split("/");
|
||||
return author || "";
|
||||
});
|
||||
|
||||
const pluginTypeLabel = computed(() => {
|
||||
const labelMap: Record<string, string> = {
|
||||
access: t("certd.auth"),
|
||||
dnsProvider: t("certd.dns"),
|
||||
deploy: t("certd.deployPlugin"),
|
||||
};
|
||||
return labelMap[props.plugin.pluginType || ""] || props.plugin.pluginType || "-";
|
||||
});
|
||||
|
||||
const sourceLabel = computed(() => {
|
||||
const labelMap: Record<string, string> = {
|
||||
builtIn: t("certd.builtIn"),
|
||||
custom: t("certd.custom"),
|
||||
store: t("certd.store"),
|
||||
};
|
||||
return labelMap[props.plugin.type || ""] || props.plugin.type || "-";
|
||||
});
|
||||
|
||||
const currentVersion = computed(() => {
|
||||
if (props.source === "local") {
|
||||
return props.plugin.version || props.plugin.latest || "-";
|
||||
}
|
||||
return props.plugin.installedVersion || props.plugin.latest || "-";
|
||||
});
|
||||
|
||||
const versionTitle = computed(() => {
|
||||
if (props.source === "local") {
|
||||
return `${t("certd.version")} v${currentVersion.value}`;
|
||||
}
|
||||
if (props.plugin.installedVersion && props.plugin.upgradeAvailable && props.plugin.latest) {
|
||||
return `${t("certd.dashboard.latestVersion", { version: `v${props.plugin.latest}` })},${t("certd.onlinePluginClickToUpdate")}`;
|
||||
}
|
||||
if (props.plugin.installedVersion) {
|
||||
return t("certd.onlinePluginAlreadyLatest");
|
||||
}
|
||||
if (props.plugin.latest) {
|
||||
return t("certd.dashboard.latestVersion", { version: `v${props.plugin.latest}` });
|
||||
}
|
||||
return `${t("certd.version")} -`;
|
||||
});
|
||||
|
||||
const toggleTitle = computed(() => {
|
||||
const disabled = props.source === "local" ? props.plugin.disabled : props.plugin.localDisabled;
|
||||
return disabled ? t("certd.onlinePluginClickToEnable") : t("certd.onlinePluginClickToDisable");
|
||||
});
|
||||
|
||||
function formatDownloadCount(count: number) {
|
||||
if (count >= 10000) {
|
||||
return `${(count / 10000).toFixed(1).replace(/\.0$/, "")}w`;
|
||||
}
|
||||
return `${count}`;
|
||||
}
|
||||
|
||||
function handleVersionClick() {
|
||||
if (props.source === "local" || !props.plugin.upgradeAvailable) {
|
||||
return;
|
||||
}
|
||||
emit("install", props.plugin);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.plugin-item-card.plugin-card {
|
||||
width: 100%;
|
||||
border-color: #e5eaf1;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
transition-duration: 160ms;
|
||||
transition-property: border-color, box-shadow, transform, opacity;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: #7dc4ff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.current {
|
||||
border-color: #00b7ff;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(0, 183, 255, 0.1),
|
||||
0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
&.is-installed {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fffb 100%);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.plugin-status-tag {
|
||||
box-sizing: border-box;
|
||||
flex: none;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
min-height: 148px;
|
||||
flex-direction: column;
|
||||
padding: 14px 16px 12px;
|
||||
}
|
||||
|
||||
.plugin-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plugin-card__main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.plugin-card__icon {
|
||||
flex: none;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.plugin-card__main .plugin-icon {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
color: #00b7ff;
|
||||
font-size: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.plugin-card__title-wrap {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.plugin-card__title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.plugin-card__title:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.plugin-card__author {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
.fs-icon {
|
||||
flex: none;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__actions {
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.plugin-card__tools {
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
opacity: 0.76;
|
||||
transition-duration: 160ms;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.plugin-card__tool {
|
||||
display: inline-flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
color: #5f6b7a;
|
||||
opacity: 0.68;
|
||||
transition-duration: 160ms;
|
||||
transition-property: color, background-color, opacity, transform;
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
.fs-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
opacity: 1;
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__action-zone {
|
||||
position: relative;
|
||||
width: 68px;
|
||||
height: 28px;
|
||||
flex: none;
|
||||
|
||||
.plugin-status-tag,
|
||||
.plugin-card__action-button {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
transition-duration: 160ms;
|
||||
transition-property: opacity, transform, filter;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.plugin-status-tag {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.plugin-card__action-button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(4px) scale(0.96);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
&.is-loading,
|
||||
&:hover {
|
||||
.plugin-status-tag {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px) scale(0.96);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.plugin-card__action-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__local-action-zone {
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
.plugin-card__install-zone {
|
||||
width: 68px;
|
||||
|
||||
.plugin-card__action-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.plugin-card__tools {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__desc {
|
||||
display: -webkit-box;
|
||||
min-height: 40px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
color: #5f6b7a;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-wrap: pretty;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.plugin-card__meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eef2f7;
|
||||
|
||||
.ant-tag {
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__version {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 22px;
|
||||
|
||||
&.is-upgradable {
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #0958d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__version-icon {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.plugin-card__download-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #6b7280;
|
||||
|
||||
.fs-icon {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-simple {
|
||||
.plugin-card__desc {
|
||||
min-height: 40px;
|
||||
margin-bottom: 4px;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.plugin-card__meta {
|
||||
min-height: 24px;
|
||||
margin-top: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.plugin-card__version {
|
||||
min-height: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.plugin-item-card.plugin-card {
|
||||
border-color: #303030;
|
||||
background: #1f1f1f;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover {
|
||||
border-color: #1668dc;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
&.current {
|
||||
border-color: #1677ff;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(22, 119, 255, 0.18),
|
||||
0 8px 24px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
&.is-installed {
|
||||
background: linear-gradient(180deg, #1f1f1f 0%, #18251e 100%);
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.plugin-card__title {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
a.plugin-card__title:hover {
|
||||
color: #69b1ff;
|
||||
}
|
||||
|
||||
.plugin-card__author,
|
||||
.plugin-card__desc,
|
||||
.plugin-card__version,
|
||||
.plugin-card__download-count {
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.plugin-card__meta {
|
||||
border-top-color: #303030;
|
||||
}
|
||||
|
||||
.plugin-card__tool {
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
|
||||
&:hover {
|
||||
color: #69b1ff;
|
||||
background: rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__version.is-upgradable {
|
||||
color: #69b1ff;
|
||||
|
||||
&:hover {
|
||||
color: #91caff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<div class="plugin-market-panel">
|
||||
<div class="plugin-market-search">
|
||||
<a-select v-model:value="onlineSearch.pluginType" class="plugin-market-search__type" allow-clear :disabled="!marketReady" :placeholder="t('certd.pluginType')" @change="handleOnlineSearch">
|
||||
<a-select-option value="deploy">{{ t("certd.deployPlugin") }}</a-select-option>
|
||||
<a-select-option value="access">{{ t("certd.auth") }}</a-select-option>
|
||||
<a-select-option value="dnsProvider">{{ t("certd.dns") }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="onlineSearch.keyword"
|
||||
class="plugin-market-search__keyword"
|
||||
:placeholder="t('certd.onlinePluginSearch')"
|
||||
allow-clear
|
||||
:disabled="!marketReady"
|
||||
:loading="onlineLoading"
|
||||
@search="handleOnlineSearch"
|
||||
/>
|
||||
<a-tooltip :title="syncButtonTitle">
|
||||
<a-button type="primary" :loading="syncLoading" @click="syncOnlinePlugins">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:sync-outline" />
|
||||
</template>
|
||||
{{ t("certd.onlinePluginSync") }}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="onlineLoading" class="plugin-market-state">正在查询插件市场...</div>
|
||||
<div v-else-if="!marketReady" class="plugin-market-state plugin-market-sync-state">
|
||||
<fs-icon icon="ion:cloud-download-outline" />
|
||||
<div>{{ t("certd.onlinePluginSyncFirst") }}</div>
|
||||
<a-tooltip :title="syncButtonTitle">
|
||||
<a-button type="primary" :loading="syncLoading" @click="syncOnlinePlugins">
|
||||
{{ t("certd.onlinePluginSync") }}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-empty v-else-if="pagedOnlinePlugins.length === 0" class="plugin-card-empty" />
|
||||
<template v-else>
|
||||
<div class="plugin-card-grid">
|
||||
<PluginItemCard
|
||||
v-for="item of pagedOnlinePlugins"
|
||||
:key="getOnlinePluginFullName(item) || item.id || item.name"
|
||||
:plugin="item"
|
||||
:install-loading="isOnlineActionLoading(item, 'install')"
|
||||
:uninstall-loading="isOnlineActionLoading(item, 'uninstall')"
|
||||
:toggle-loading="isOnlineActionLoading(item, 'toggle')"
|
||||
@install="installOnlinePlugin"
|
||||
@uninstall="uninstallOnlinePlugin"
|
||||
@toggle-disabled="toggleOnlinePluginDisabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="plugin-market-pagination">
|
||||
<a-pagination v-model:current="onlinePage" size="small" :page-size="onlinePageSize" :total="onlinePlugins.length" :show-size-changer="false" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import * as api from "../api";
|
||||
import PluginItemCard from "./plugin-item-card.vue";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useI18n } from "/src/locales";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "changed"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const pluginStore = usePluginStore();
|
||||
const onlinePlugins = ref<api.OnlinePluginBean[]>([]);
|
||||
const onlineLoading = ref(true);
|
||||
const syncLoading = ref(false);
|
||||
const onlineActionLoading = ref("");
|
||||
const onlineLoaded = ref(false);
|
||||
const hasSynced = ref(false);
|
||||
const onlinePage = ref(1);
|
||||
const onlinePageSize = 12;
|
||||
const onlineSearch = ref({
|
||||
pluginType: undefined as string | undefined,
|
||||
keyword: "",
|
||||
});
|
||||
const onlineQuery = ref({
|
||||
pluginType: undefined as string | undefined,
|
||||
keyword: "",
|
||||
});
|
||||
let onlineRequestId = 0;
|
||||
const ONLINE_PLUGIN_LAST_SYNC_TIME_KEY = "certd:plugin-market:last-sync-time";
|
||||
const lastSyncTime = ref(readLastSyncTime());
|
||||
|
||||
const pagedOnlinePlugins = computed(() => {
|
||||
const start = (onlinePage.value - 1) * onlinePageSize;
|
||||
return onlinePlugins.value.slice(start, start + onlinePageSize);
|
||||
});
|
||||
|
||||
const marketReady = computed(() => {
|
||||
return hasSynced.value || onlinePlugins.value.length > 0;
|
||||
});
|
||||
|
||||
const syncButtonTitle = computed(() => {
|
||||
if (!lastSyncTime.value) {
|
||||
return t("certd.onlinePluginNotSynced");
|
||||
}
|
||||
return t("certd.onlinePluginLastSyncTime", {
|
||||
time: formatSyncTime(lastSyncTime.value),
|
||||
});
|
||||
});
|
||||
|
||||
function readLastSyncTime() {
|
||||
const value = Number(localStorage.getItem(ONLINE_PLUGIN_LAST_SYNC_TIME_KEY) || 0);
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function saveLastSyncTime(time: number) {
|
||||
lastSyncTime.value = time;
|
||||
localStorage.setItem(ONLINE_PLUGIN_LAST_SYNC_TIME_KEY, `${time}`);
|
||||
}
|
||||
|
||||
function formatSyncTime(time: number) {
|
||||
return new Date(time).toLocaleString();
|
||||
}
|
||||
|
||||
function getOnlinePluginFullName(row: api.OnlinePluginBean) {
|
||||
if (row.fullName) {
|
||||
return row.fullName;
|
||||
}
|
||||
if (row.author && row.name) {
|
||||
return `${row.author}/${row.name}`;
|
||||
}
|
||||
return row.name || "";
|
||||
}
|
||||
|
||||
function getOnlinePluginActionKey(row: api.OnlinePluginBean, action: "install" | "uninstall" | "toggle") {
|
||||
const fullName = getOnlinePluginFullName(row);
|
||||
if (!fullName) {
|
||||
return "";
|
||||
}
|
||||
return `${action}:${fullName}`;
|
||||
}
|
||||
|
||||
function isOnlineActionLoading(row: api.OnlinePluginBean, action: "install" | "uninstall" | "toggle") {
|
||||
return onlineActionLoading.value === getOnlinePluginActionKey(row, action);
|
||||
}
|
||||
|
||||
async function loadOnlinePlugins(options?: { force?: boolean; silent?: boolean }) {
|
||||
if (onlineLoaded.value && !options?.force && !onlineQuery.value.keyword && !onlineQuery.value.pluginType) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++onlineRequestId;
|
||||
if (!options?.silent) {
|
||||
onlineLoading.value = true;
|
||||
}
|
||||
try {
|
||||
const list = await api.OnlinePluginList({
|
||||
pluginType: onlineQuery.value.pluginType,
|
||||
keyword: onlineQuery.value.keyword.trim() || undefined,
|
||||
});
|
||||
if (requestId !== onlineRequestId) {
|
||||
return;
|
||||
}
|
||||
onlinePlugins.value = list;
|
||||
if (list.length > 0) {
|
||||
hasSynced.value = true;
|
||||
}
|
||||
onlineLoaded.value = true;
|
||||
} finally {
|
||||
if (requestId === onlineRequestId && !options?.silent) {
|
||||
onlineLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncOnlinePlugins() {
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
onlineSearch.value.pluginType = undefined;
|
||||
onlineSearch.value.keyword = "";
|
||||
onlineQuery.value.pluginType = undefined;
|
||||
onlineQuery.value.keyword = "";
|
||||
onlinePage.value = 1;
|
||||
onlinePlugins.value = await api.OnlinePluginSync();
|
||||
saveLastSyncTime(Date.now());
|
||||
onlineLoaded.value = true;
|
||||
hasSynced.value = true;
|
||||
message.success(t("certd.onlinePluginSyncSuccess"));
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnlineSearch() {
|
||||
onlineQuery.value = {
|
||||
pluginType: onlineSearch.value.pluginType,
|
||||
keyword: onlineSearch.value.keyword.trim(),
|
||||
};
|
||||
onlinePage.value = 1;
|
||||
loadOnlinePlugins({ force: true });
|
||||
}
|
||||
|
||||
async function installOnlinePlugin(row: api.OnlinePluginBean) {
|
||||
const fullName = getOnlinePluginFullName(row);
|
||||
if (!fullName) {
|
||||
return;
|
||||
}
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(row, "install");
|
||||
try {
|
||||
await api.OnlinePluginInstall({
|
||||
fullName,
|
||||
version: row.latest,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
emit("changed");
|
||||
await loadOnlinePlugins({ force: true, silent: true });
|
||||
message.success(t("certd.onlinePluginInstallSuccess"));
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOnlinePluginDisabled(row: api.OnlinePluginBean) {
|
||||
const fullName = getOnlinePluginFullName(row);
|
||||
if (!row.localPluginId || !fullName) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${!row.localDisabled ? t("certd.disable") : t("certd.enable")}?`,
|
||||
async onOk() {
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(row, "toggle");
|
||||
try {
|
||||
await api.SetDisabled({
|
||||
id: row.localPluginId,
|
||||
name: row.name,
|
||||
type: "store",
|
||||
disabled: !row.localDisabled,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
emit("changed");
|
||||
await loadOnlinePlugins({ force: true, silent: true });
|
||||
message.success(t("certd.operationSuccess"));
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallOnlinePlugin(row: api.OnlinePluginBean) {
|
||||
const fullName = getOnlinePluginFullName(row);
|
||||
if (!row.localPluginId || !fullName) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: t("certd.onlinePluginDeleteConfirm", { name: fullName }),
|
||||
async onOk() {
|
||||
onlineActionLoading.value = getOnlinePluginActionKey(row, "uninstall");
|
||||
try {
|
||||
await api.DelObj(row.localPluginId);
|
||||
await pluginStore.reload();
|
||||
emit("changed");
|
||||
await loadOnlinePlugins({ force: true, silent: true });
|
||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
||||
} finally {
|
||||
onlineActionLoading.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOnlinePlugins();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.plugin-market-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plugin-market-search {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 10px 12px;
|
||||
}
|
||||
|
||||
.plugin-market-search__type {
|
||||
width: 148px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.plugin-market-search__keyword {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.plugin-market-state {
|
||||
display: flex;
|
||||
min-height: 260px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7b8794;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.plugin-market-sync-state {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
> .fs-icon {
|
||||
color: #1677ff;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-market-pagination {
|
||||
display: flex;
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
padding: 0 10px 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -110,6 +110,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
},
|
||||
},
|
||||
table: {
|
||||
show: false,
|
||||
rowKey: "name",
|
||||
remove: {
|
||||
afterRemove: async context => {
|
||||
@@ -136,7 +137,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
remove: {
|
||||
order: 999,
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
return row.type === "custom" || row.type === "store";
|
||||
}),
|
||||
},
|
||||
export: {
|
||||
@@ -347,7 +348,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
data: [
|
||||
{ label: t("certd.builtIn"), value: "builtIn" },
|
||||
{ label: t("certd.custom"), value: "custom" },
|
||||
{ label: t("certd.store"), value: "store" },
|
||||
{ label: t("certd.installedStorePlugin"), value: "store" },
|
||||
],
|
||||
}),
|
||||
column: {
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
<template>
|
||||
<fs-page class="page-cert">
|
||||
<fs-page class="page-sys-plugin">
|
||||
<template #header>
|
||||
<div class="title">
|
||||
{{ t("certd.pluginManagement") }}
|
||||
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<!-- <template #pagination-left>-->
|
||||
<!-- <a-tooltip :title="t('certd.batchDelete')">-->
|
||||
<!-- <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>-->
|
||||
<!-- </a-tooltip>-->
|
||||
<!-- </template>-->
|
||||
</fs-crud>
|
||||
<a-tabs v-model:active-key="activeTab" class="plugin-page-tabs">
|
||||
<a-tab-pane key="local">
|
||||
<template #tab>
|
||||
<span class="plugin-page-tab-label">
|
||||
<fs-icon icon="ion:cube-outline" />
|
||||
<span>{{ t("certd.localPlugin") }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" class="plugin-card-crud" v-bind="crudBinding">
|
||||
<a-empty v-if="pluginList.length === 0" class="plugin-card-empty" />
|
||||
<div v-else class="plugin-card-grid">
|
||||
<PluginItemCard
|
||||
v-for="(item, index) of pluginList"
|
||||
:key="item.id || item.name"
|
||||
source="local"
|
||||
:plugin="item"
|
||||
:show-config="settingStore.isComm"
|
||||
@edit="openEditPage"
|
||||
@copy="openCopy({ index, row: item })"
|
||||
@export-plugin="exportPlugin"
|
||||
@config="openConfig"
|
||||
@remove="removePlugin({ index, row: item })"
|
||||
@toggle-disabled="toggleDisabled"
|
||||
/>
|
||||
</div>
|
||||
</fs-crud>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="market">
|
||||
<template #tab>
|
||||
<span class="plugin-page-tab-label">
|
||||
<fs-icon icon="ion:storefront-outline" />
|
||||
<span>{{ t("certd.pluginMarket") }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<PluginMarketPanel @changed="handleMarketPluginChanged" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
@@ -20,38 +50,195 @@
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { DeleteBatch } from "./api";
|
||||
import * as api from "./api";
|
||||
import PluginMarketPanel from "./components/plugin-market-panel.vue";
|
||||
import PluginItemCard from "./components/plugin-item-card.vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { useMounted } from "/@/use/use-mounted";
|
||||
import { computed, ref } from "vue";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { usePluginConfig } from "./use-config";
|
||||
import { useSettingStore } from "/src/store/settings";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const { t } = useI18n();
|
||||
const pluginStore = usePluginStore();
|
||||
const settingStore = useSettingStore();
|
||||
const { openConfigDialog } = usePluginConfig();
|
||||
const router = useRouter();
|
||||
|
||||
defineOptions({
|
||||
name: "SysPlugin",
|
||||
});
|
||||
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const activeTab = ref("local");
|
||||
|
||||
const selectedRowKeys = context.selectedRowKeys;
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value?.length > 0) {
|
||||
Modal.confirm({
|
||||
title: t("certd.pluginManagement"),
|
||||
content: t("certd.batchDeleteConfirm", { count: selectedRowKeys.value.length }),
|
||||
async onOk() {
|
||||
await DeleteBatch(selectedRowKeys.value);
|
||||
message.info(t("certd.deleteSuccess"));
|
||||
crudExpose.doRefresh();
|
||||
selectedRowKeys.value = [];
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(t("certd.selectRecordFirst"));
|
||||
const pluginList = computed(() => {
|
||||
return crudBinding.value?.data || [];
|
||||
});
|
||||
|
||||
function openEditPage(row: any) {
|
||||
router.push(`/sys/plugin/edit?id=${row.id}`);
|
||||
}
|
||||
|
||||
async function openCopy(opts: any) {
|
||||
await crudExpose.openCopy({
|
||||
row: {
|
||||
...opts.row,
|
||||
},
|
||||
index: opts.index,
|
||||
});
|
||||
}
|
||||
|
||||
async function removePlugin(opts: any) {
|
||||
await crudExpose.doRemove(opts);
|
||||
}
|
||||
|
||||
async function openConfig(row: any) {
|
||||
await openConfigDialog({
|
||||
row,
|
||||
crudExpose,
|
||||
});
|
||||
}
|
||||
|
||||
async function exportPlugin(row: any) {
|
||||
const content = await api.ExportPlugin(row.id);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${row.name}.yaml`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function toggleDisabled(row: any) {
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${!row.disabled ? t("certd.disable") : t("certd.enable")}?`,
|
||||
async onOk() {
|
||||
await api.SetDisabled({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
disabled: !row.disabled,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
crudExpose.doRefresh();
|
||||
message.success(t("certd.operationSuccess"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleMarketPluginChanged() {
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
useMounted(async () => {
|
||||
await crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
<style lang="less">
|
||||
.page-sys-plugin {
|
||||
.fs-page-content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plugin-card-crud {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.plugin-page-tabs {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
> .ant-tabs-nav {
|
||||
flex: none;
|
||||
margin: 4px 0 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
> .ant-tabs-content-holder {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
||||
> .ant-tabs-content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
||||
> .ant-tabs-tabpane {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
> .ant-tabs-tabpane-active {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-page-tab-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 1;
|
||||
|
||||
.fs-icon {
|
||||
flex: none;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card-empty {
|
||||
padding: 72px 0;
|
||||
}
|
||||
|
||||
.plugin-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
padding: 4px 10px 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
.page-sys-plugin {
|
||||
.plugin-card-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.page-sys-plugin {
|
||||
.plugin-card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page-sys-plugin {
|
||||
.plugin-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user