mirror of
https://github.com/certd/certd.git
synced 2026-08-11 09:45:50 +08:00
chore: plugin 有store page版本
This commit is contained in:
@@ -389,6 +389,9 @@ export class Executor {
|
|||||||
};
|
};
|
||||||
await instance.setCtx(taskCtx);
|
await instance.setCtx(taskCtx);
|
||||||
|
|
||||||
|
if (!(instance instanceof AbstractTaskPlugin)) {
|
||||||
|
throw new Error(`插件类型错误:${step.type}不是AbstractTaskPlugin的实例`);
|
||||||
|
}
|
||||||
await instance.onInstance();
|
await instance.onInstance();
|
||||||
const result = await instance.execute();
|
const result = await instance.execute();
|
||||||
//执行结果处理
|
//执行结果处理
|
||||||
@@ -398,6 +401,7 @@ export class Executor {
|
|||||||
}
|
}
|
||||||
//输出上下文变量到output context
|
//输出上下文变量到output context
|
||||||
forEach(define.output, (item: any, key: any) => {
|
forEach(define.output, (item: any, key: any) => {
|
||||||
|
// @ts-ignore
|
||||||
step.status!.output[key] = instance[key];
|
step.status!.output[key] = instance[key];
|
||||||
// const stepOutputKey = `step.${step.id}.${key}`;
|
// const stepOutputKey = `step.${step.id}.${key}`;
|
||||||
// this.runtime.context[stepOutputKey] = instance[key];
|
// this.runtime.context[stepOutputKey] = instance[key];
|
||||||
@@ -411,7 +415,8 @@ export class Executor {
|
|||||||
merge(vars, instance._result.pipelineVars);
|
merge(vars, instance._result.pipelineVars);
|
||||||
await this.pipelineContext.setObj("vars", vars);
|
await this.pipelineContext.setObj("vars", vars);
|
||||||
}
|
}
|
||||||
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
|
// @ts-ignore
|
||||||
|
if (Object.keys(instance._result?.pipelinePrivateVars).length > 0) {
|
||||||
// 判断 pipelineVars 有值时更新
|
// 判断 pipelineVars 有值时更新
|
||||||
let vars = await this.pipelineContext.getObj("privateVars");
|
let vars = await this.pipelineContext.getObj("privateVars");
|
||||||
vars = vars || {};
|
vars = vars || {};
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export class PlusService {
|
|||||||
baseURL: plusRequestService.getBaseURL(),
|
baseURL: plusRequestService.getBaseURL(),
|
||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Berear ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await http.request(config);
|
const res = await http.request(config);
|
||||||
@@ -173,4 +173,9 @@ export class PlusService {
|
|||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async request(config: HttpRequestConfig) {
|
||||||
|
const plusRequestService = await this.getPlusRequestService();
|
||||||
|
return await plusRequestService.request(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
<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" />
|
<fs-button shape="circle" type="text" icon="ion:logo-github" :text="null" @click="goGithub" />
|
||||||
</div>
|
</div>
|
||||||
|
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<PageFooter></PageFooter>
|
<PageFooter></PageFooter>
|
||||||
<MaxKBChat v-if="settingsStore.sysPublic.aiChatEnabled !== false" ref="chatBox" />
|
|
||||||
</template>
|
</template>
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
<router-view> </router-view>
|
<router-view> </router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!--<script lang="ts" setup>-->
|
<script lang="ts" setup>
|
||||||
<!--import { usePageStore } from "/@/store/modules/page";-->
|
import { useSettingStore } from "../store/settings";
|
||||||
|
|
||||||
<!--const pageStore = usePageStore();-->
|
const settingsStore = useSettingStore();
|
||||||
<!--const keepAlive = pageStore.keepAlive;-->
|
</script>
|
||||||
<!--</script>-->
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default {
|
|||||||
default: "Default",
|
default: "Default",
|
||||||
success: "Success",
|
success: "Success",
|
||||||
test: "Test",
|
test: "Test",
|
||||||
|
operation: "Operation",
|
||||||
testButton: "Test",
|
testButton: "Test",
|
||||||
operationSuccess: "Operation successful",
|
operationSuccess: "Operation successful",
|
||||||
batch_delete: "Batch Delete",
|
batch_delete: "Batch Delete",
|
||||||
|
|||||||
@@ -42,6 +42,36 @@ export default {
|
|||||||
pluginGroup: "Plugin Group",
|
pluginGroup: "Plugin Group",
|
||||||
pluginManagement: "Plugin Management",
|
pluginManagement: "Plugin Management",
|
||||||
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
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",
|
pleaseSelectRecord: "Please select records first",
|
||||||
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
||||||
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default {
|
|||||||
default: "默认",
|
default: "默认",
|
||||||
success: "成功",
|
success: "成功",
|
||||||
test: "测试",
|
test: "测试",
|
||||||
|
operation: "操作",
|
||||||
testButton: "测试",
|
testButton: "测试",
|
||||||
operationSuccess: "操作成功",
|
operationSuccess: "操作成功",
|
||||||
batch_delete: "批量删除",
|
batch_delete: "批量删除",
|
||||||
|
|||||||
@@ -41,6 +41,36 @@ export default {
|
|||||||
pluginGroup: "插件分组",
|
pluginGroup: "插件分组",
|
||||||
pluginManagement: "插件管理",
|
pluginManagement: "插件管理",
|
||||||
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
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: "请先勾选记录",
|
pleaseSelectRecord: "请先勾选记录",
|
||||||
clearRuntimeDeps: "清理第三方依赖缓存",
|
clearRuntimeDeps: "清理第三方依赖缓存",
|
||||||
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
||||||
|
|||||||
+55
-115
@@ -12,59 +12,21 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
|
<a-space>
|
||||||
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
|
||||||
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="currentStep">
|
<template v-if="currentStep">
|
||||||
<pi-container v-if="currentStep._isAdd" class="pi-step-form">
|
<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">
|
<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">
|
<a-tabs v-model:active-key="pluginSourceActive" class="step-plugin-source-tabs flex-1 overflow-hidden h-full">
|
||||||
<template v-for="group of computedPluginGroups" :key="group.key">
|
<a-tab-pane key="local" tab="已安装插件" class="h-full">
|
||||||
<a-tab-pane v-if="(group.key === 'admin' && userStore.isAdmin) || group.key !== 'admin'" :key="group.key" class="scroll-y">
|
<LocalPluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" />
|
||||||
<template #tab>
|
</a-tab-pane>
|
||||||
<div class="cd-step-form-tab-label">
|
<a-tab-pane v-if="userStore.isAdmin" key="market" tab="插件市场" class="h-full step-market-pane">
|
||||||
<fs-icon :icon="group.icon" class="mr-2" />
|
<OnlinePluginSelector :selected-type="currentStep.type" @select="stepTypeSelected" @confirm="handlePluginConfirm" @uninstalled="handleOnlinePluginUninstalled" />
|
||||||
<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>
|
</a-tab-pane>
|
||||||
</template>
|
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -117,28 +79,30 @@
|
|||||||
|
|
||||||
<script lang="tsx" setup>
|
<script lang="tsx" setup>
|
||||||
import { message, Modal } from "ant-design-vue";
|
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 { merge, cloneDeep } from "lodash-es";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { usePluginStore, PluginGroups } from "/@/store/plugin";
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
import { useCompute } from "@fast-crud/fast-crud";
|
import { useCompute } from "@fast-crud/fast-crud";
|
||||||
import { useReference } from "/@/use/use-refrence";
|
import { useReference } from "/@/use/use-refrence";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import { mitter } from "/@/utils/util.mitt";
|
import { mitter } from "/@/utils/util.mitt";
|
||||||
import { utils } from "/@/utils";
|
import { utils } from "/@/utils";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
|
import LocalPluginSelector from "./local-plugin-selector.vue";
|
||||||
|
import OnlinePluginSelector from "./online-plugin-selector.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PiStepForm",
|
name: "PiStepForm",
|
||||||
});
|
});
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update"]);
|
defineEmits(["update"]);
|
||||||
|
|
||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -171,6 +135,9 @@ function useStepForm() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stepTypeSelected = (item: any) => {
|
const stepTypeSelected = (item: any) => {
|
||||||
|
if (item.__online) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (item.needPlus && !settingStore.isPlus) {
|
if (item.needPlus && !settingStore.isPlus) {
|
||||||
message.warn("此插件需要开通Certd专业版才能使用");
|
message.warn("此插件需要开通Certd专业版才能使用");
|
||||||
mitter.emit("openVipModal");
|
mitter.emit("openVipModal");
|
||||||
@@ -339,55 +306,24 @@ function useStepForm() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const pluginSearch = ref({
|
const pluginSourceActive = ref("local");
|
||||||
keyword: "",
|
const handlePluginConfirm = async (item: any) => {
|
||||||
result: [],
|
stepTypeSelected(item);
|
||||||
});
|
await stepTypeSave();
|
||||||
const pluginGroupActive = ref("all");
|
|
||||||
const pluginGroup: Ref = ref();
|
|
||||||
const pluginStore = usePluginStore();
|
|
||||||
|
|
||||||
async function loadPluginGroups() {
|
|
||||||
pluginGroup.value = await pluginStore.getGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPluginGroups();
|
|
||||||
const computedPluginGroups: any = computed(() => {
|
|
||||||
if (!pluginGroup.value) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
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;
|
const handleOnlinePluginUninstalled = (plugin: any) => {
|
||||||
|
if (currentStep.value.type === plugin.fullName) {
|
||||||
|
currentStep.value.type = undefined;
|
||||||
|
currentStep.value.title = "新任务";
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
return pluginSearch.value.keyword;
|
|
||||||
},
|
|
||||||
(val: any) => {
|
|
||||||
if (val) {
|
|
||||||
pluginGroupActive.value = "search";
|
|
||||||
} else {
|
|
||||||
pluginGroupActive.value = "all";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pluginGroupActive,
|
pluginSourceActive,
|
||||||
computedPluginGroups,
|
|
||||||
pluginSearch,
|
|
||||||
stepTypeSelected,
|
stepTypeSelected,
|
||||||
|
handlePluginConfirm,
|
||||||
|
handleOnlinePluginUninstalled,
|
||||||
stepTypeSave,
|
stepTypeSave,
|
||||||
stepFormRef,
|
stepFormRef,
|
||||||
mode,
|
mode,
|
||||||
@@ -436,31 +372,27 @@ const labelCol = ref({ span: 6 });
|
|||||||
const wrapperCol = ref({ span: 16 });
|
const wrapperCol = ref({ span: 16 });
|
||||||
|
|
||||||
const stepFormRes = useStepForm();
|
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({
|
defineExpose({
|
||||||
...stepFormRes,
|
...stepFormRes,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<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 {
|
.step-form-drawer {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
@@ -514,6 +446,10 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 22px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
@@ -542,7 +478,9 @@ defineExpose({
|
|||||||
.ant-card-meta-title {
|
.ant-card-meta-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-avatar {
|
.ant-avatar {
|
||||||
@@ -558,19 +496,21 @@ defineExpose({
|
|||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
height: 100px;
|
min-height: 100px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
.ant-card-meta-description {
|
.ant-card-meta-description {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
color: #7f7f7f;
|
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 = {
|
export type PluginConfigBean = {
|
||||||
name: string;
|
name: string;
|
||||||
disabled: boolean;
|
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: {
|
table: {
|
||||||
|
show: false,
|
||||||
rowKey: "name",
|
rowKey: "name",
|
||||||
remove: {
|
remove: {
|
||||||
afterRemove: async context => {
|
afterRemove: async context => {
|
||||||
@@ -136,7 +137,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
remove: {
|
remove: {
|
||||||
order: 999,
|
order: 999,
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return row.type === "custom";
|
return row.type === "custom" || row.type === "store";
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
@@ -347,7 +348,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
data: [
|
data: [
|
||||||
{ label: t("certd.builtIn"), value: "builtIn" },
|
{ label: t("certd.builtIn"), value: "builtIn" },
|
||||||
{ label: t("certd.custom"), value: "custom" },
|
{ label: t("certd.custom"), value: "custom" },
|
||||||
{ label: t("certd.store"), value: "store" },
|
{ label: t("certd.installedStorePlugin"), value: "store" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
column: {
|
column: {
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<fs-page class="page-cert">
|
<fs-page class="page-sys-plugin">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ t("certd.pluginManagement") }}
|
{{ t("certd.pluginManagement") }}
|
||||||
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
<a-tabs v-model:active-key="activeTab" class="plugin-page-tabs">
|
||||||
<!-- <template #pagination-left>-->
|
<a-tab-pane key="local">
|
||||||
<!-- <a-tooltip :title="t('certd.batchDelete')">-->
|
<template #tab>
|
||||||
<!-- <fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>-->
|
<span class="plugin-page-tab-label">
|
||||||
<!-- </a-tooltip>-->
|
<fs-icon icon="ion:cube-outline" />
|
||||||
<!-- </template>-->
|
<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>
|
</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>
|
</fs-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,38 +50,195 @@
|
|||||||
import { useFs } from "@fast-crud/fast-crud";
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
import createCrudOptions from "./crud";
|
import createCrudOptions from "./crud";
|
||||||
import { message, Modal } from "ant-design-vue";
|
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 { useI18n } from "/src/locales";
|
||||||
import { useMounted } from "/@/use/use-mounted";
|
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 { t } = useI18n();
|
||||||
|
const pluginStore = usePluginStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { openConfigDialog } = usePluginConfig();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "SysPlugin",
|
name: "SysPlugin",
|
||||||
});
|
});
|
||||||
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
|
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||||
|
const activeTab = ref("local");
|
||||||
|
|
||||||
const selectedRowKeys = context.selectedRowKeys;
|
const pluginList = computed(() => {
|
||||||
const handleBatchDelete = () => {
|
return crudBinding.value?.data || [];
|
||||||
if (selectedRowKeys.value?.length > 0) {
|
});
|
||||||
|
|
||||||
|
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({
|
Modal.confirm({
|
||||||
title: t("certd.pluginManagement"),
|
title: t("certd.confirm"),
|
||||||
content: t("certd.batchDeleteConfirm", { count: selectedRowKeys.value.length }),
|
content: `${t("certd.confirmToggle")} ${!row.disabled ? t("certd.disable") : t("certd.enable")}?`,
|
||||||
async onOk() {
|
async onOk() {
|
||||||
await DeleteBatch(selectedRowKeys.value);
|
await api.SetDisabled({
|
||||||
message.info(t("certd.deleteSuccess"));
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
disabled: !row.disabled,
|
||||||
|
});
|
||||||
|
await pluginStore.reload();
|
||||||
crudExpose.doRefresh();
|
crudExpose.doRefresh();
|
||||||
selectedRowKeys.value = [];
|
message.success(t("certd.operationSuccess"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
message.error(t("certd.selectRecordFirst"));
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
function handleMarketPluginChanged() {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
// 页面打开后获取列表数据
|
// 页面打开后获取列表数据
|
||||||
useMounted(async () => {
|
useMounted(async () => {
|
||||||
await crudExpose.doRefresh();
|
await crudExpose.doRefresh();
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ ALTER TABLE "cd_audit_log" ADD COLUMN "scope" varchar(32) NOT NULL DEFAULT ('use
|
|||||||
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
CREATE INDEX "index_audit_log_scope" ON "cd_audit_log" ("scope");
|
||||||
|
|
||||||
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT (1);
|
ALTER TABLE "cd_audit_log" ADD COLUMN "success" boolean NOT NULL DEFAULT (1);
|
||||||
|
-- 删除project_name字段
|
||||||
|
ALTER TABLE "cd_audit_log" DROP COLUMN "project_name";
|
||||||
|
|
||||||
-- 审计日志表索引
|
-- 审计日志表索引
|
||||||
CREATE INDEX "index_audit_log_type" ON "cd_audit_log" ("type");
|
CREATE INDEX "index_audit_log_type" ON "cd_audit_log" ("type");
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE "pi_plugin_market_item"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"app_id" integer,
|
||||||
|
"full_name" varchar(200) NOT NULL,
|
||||||
|
"author" varchar(100),
|
||||||
|
"name" varchar(100),
|
||||||
|
"plugin_type" varchar(100),
|
||||||
|
"title" varchar(200),
|
||||||
|
"icon" varchar(200),
|
||||||
|
"group" varchar(100),
|
||||||
|
"desc" varchar(1000),
|
||||||
|
"latest" varchar(100),
|
||||||
|
"status" varchar(100),
|
||||||
|
"download_count" integer,
|
||||||
|
"sync_time" integer,
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_plugin_market_item_full_name" ON "pi_plugin_market_item" ("full_name");
|
||||||
|
CREATE INDEX "index_plugin_market_item_plugin_type" ON "pi_plugin_market_item" ("plugin_type");
|
||||||
|
CREATE INDEX "index_plugin_market_item_group" ON "pi_plugin_market_item" ("group");
|
||||||
|
CREATE INDEX "index_plugin_market_item_sync_time" ON "pi_plugin_market_item" ("sync_time");
|
||||||
@@ -43,7 +43,6 @@ process.on("uncaughtException", error => {
|
|||||||
// setInterval(log, 200);
|
// setInterval(log, 200);
|
||||||
// log()
|
// log()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// startHeapLog();
|
// startHeapLog();
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||||
import { merge } from "lodash-es";
|
import { merge } from "lodash-es";
|
||||||
import { CrudController } from "@certd/lib-server";
|
import { CrudController } from "@certd/lib-server";
|
||||||
import { PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
|
import { OnlinePluginInstallReq, OnlinePluginListReq, PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
|
||||||
import { CommPluginConfig, PluginConfig, PluginConfigService } from "../../../modules/plugin/service/plugin-config-service.js";
|
import { CommPluginConfig, PluginConfig, PluginConfigService } from "../../../modules/plugin/service/plugin-config-service.js";
|
||||||
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
||||||
/**
|
/**
|
||||||
@@ -115,6 +115,26 @@ export class PluginController extends CrudController<PluginService> {
|
|||||||
return this.ok(res);
|
return this.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("/online/list", { description: "sys:settings:view", summary: "查询在线插件" })
|
||||||
|
async onlineList(@Body(ALL) body: OnlinePluginListReq) {
|
||||||
|
const res = await this.service.onlinePluginList(body);
|
||||||
|
return this.ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/online/sync", { description: "sys:settings:edit", summary: "同步在线插件" })
|
||||||
|
async onlineSync() {
|
||||||
|
const res = await this.service.syncOnlinePluginList();
|
||||||
|
await this.auditLog({ content: "同步了在线插件市场" });
|
||||||
|
return this.ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/online/install", { description: "sys:settings:edit", summary: "安装在线插件" })
|
||||||
|
async onlineInstall(@Body(ALL) body: OnlinePluginInstallReq) {
|
||||||
|
const res = await this.service.installOnlinePlugin(body);
|
||||||
|
await this.auditLog({ content: `安装了在线插件「${body.fullName}」` });
|
||||||
|
return this.ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/export", { description: "sys:settings:edit", summary: "导出插件" })
|
@Post("/export", { description: "sys:settings:edit", summary: "导出插件" })
|
||||||
async export(@Body("id") id: number) {
|
async export(@Body("id") id: number) {
|
||||||
const res = await this.service.exportPlugin(id);
|
const res = await this.service.exportPlugin(id);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity("pi_plugin_market_item")
|
||||||
|
export class PluginMarketItemEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: "app_id", comment: "平台应用ID", nullable: true })
|
||||||
|
appId: number;
|
||||||
|
|
||||||
|
@Column({ name: "full_name", comment: "插件完整名称", length: 200 })
|
||||||
|
fullName: string;
|
||||||
|
|
||||||
|
@Column({ name: "author", comment: "作者", length: 100, nullable: true })
|
||||||
|
author: string;
|
||||||
|
|
||||||
|
@Column({ name: "name", comment: "插件名称", length: 100, nullable: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: "plugin_type", comment: "插件类型", length: 100, nullable: true })
|
||||||
|
pluginType: string;
|
||||||
|
|
||||||
|
@Column({ name: "title", comment: "标题", length: 200, nullable: true })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ name: "icon", comment: "图标", length: 200, nullable: true })
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
@Column({ name: "group", comment: "分组", length: 100, nullable: true })
|
||||||
|
group: string;
|
||||||
|
|
||||||
|
@Column({ name: "desc", comment: "描述", length: 1000, nullable: true })
|
||||||
|
desc: string;
|
||||||
|
|
||||||
|
@Column({ name: "latest", comment: "最新版本", length: 100, nullable: true })
|
||||||
|
latest: string;
|
||||||
|
|
||||||
|
@Column({ name: "status", comment: "状态", length: 100, nullable: true })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Column({ name: "download_count", comment: "下载次数", nullable: true })
|
||||||
|
downloadCount: number;
|
||||||
|
|
||||||
|
@Column({ name: "sync_time", comment: "同步时间", nullable: true })
|
||||||
|
syncTime: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: "create_time",
|
||||||
|
comment: "创建时间",
|
||||||
|
default: () => "CURRENT_TIMESTAMP",
|
||||||
|
})
|
||||||
|
createTime: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: "update_time",
|
||||||
|
comment: "修改时间",
|
||||||
|
default: () => "CURRENT_TIMESTAMP",
|
||||||
|
})
|
||||||
|
updateTime: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { PluginService, parseOnlinePluginFullName, isOnlinePluginUpgradeAvailable } from "./plugin-service.js";
|
||||||
|
|
||||||
|
function createMarketRepositoryMock(list: any[]) {
|
||||||
|
return {
|
||||||
|
createQueryBuilder() {
|
||||||
|
return {
|
||||||
|
andWhere() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
orderBy() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
addOrderBy() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
getMany() {
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PluginService online plugins", () => {
|
||||||
|
it("parses online plugin full name", () => {
|
||||||
|
assert.deepEqual(parseOnlinePluginFullName("greper/DeployToDemo"), {
|
||||||
|
author: "greper",
|
||||||
|
name: "DeployToDemo",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => parseOnlinePluginFullName("greper/deploy:DeployToDemo"), /插件标识格式错误/);
|
||||||
|
assert.throws(() => parseOnlinePluginFullName("DeployToDemo"), /插件标识格式错误/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects when an online plugin can upgrade the installed version", () => {
|
||||||
|
assert.equal(isOnlinePluginUpgradeAvailable("1.0.0", "1.0.1"), true);
|
||||||
|
assert.equal(isOnlinePluginUpgradeAvailable("v1.2.0", "1.2.0"), false);
|
||||||
|
assert.equal(isOnlinePluginUpgradeAvailable("2.0.0", "1.9.9"), false);
|
||||||
|
assert.equal(isOnlinePluginUpgradeAvailable("beta", "beta2"), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks online plugins with local installation state", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
service.pluginMarketItemRepository = createMarketRepositoryMock([
|
||||||
|
{
|
||||||
|
fullName: "greper/DeployToDemo",
|
||||||
|
author: "greper",
|
||||||
|
name: "DeployToDemo",
|
||||||
|
latest: "1.0.1",
|
||||||
|
syncTime: 1784566000000,
|
||||||
|
},
|
||||||
|
]) as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne() {
|
||||||
|
return {
|
||||||
|
id: 3,
|
||||||
|
version: "1.0.0",
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const list = await service.onlinePluginList({});
|
||||||
|
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].installed, true);
|
||||||
|
assert.equal(list[0].installedVersion, "1.0.0");
|
||||||
|
assert.equal(list[0].upgradeAvailable, true);
|
||||||
|
assert.equal(list[0].localPluginId, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches installed online dnsProvider plugins saved with full name", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
service.pluginMarketItemRepository = createMarketRepositoryMock([
|
||||||
|
{
|
||||||
|
fullName: "developer/testtest3",
|
||||||
|
author: "developer",
|
||||||
|
name: "testtest3",
|
||||||
|
pluginType: "dnsProvider",
|
||||||
|
latest: "1.0.0",
|
||||||
|
},
|
||||||
|
]) as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne(options: any) {
|
||||||
|
const where = options.where;
|
||||||
|
if (Array.isArray(where) && where.some(item => item.name === "developer/testtest3")) {
|
||||||
|
return {
|
||||||
|
id: 4,
|
||||||
|
version: "1.0.0",
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const list = await service.onlinePluginList({});
|
||||||
|
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].installed, true);
|
||||||
|
assert.equal(list[0].upgradeAvailable, false);
|
||||||
|
assert.equal(list[0].localPluginId, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches legacy installed dnsProvider plugins saved without author", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
service.pluginMarketItemRepository = createMarketRepositoryMock([
|
||||||
|
{
|
||||||
|
fullName: "developer/testtest3",
|
||||||
|
author: "developer",
|
||||||
|
name: "testtest3",
|
||||||
|
pluginType: "dnsProvider",
|
||||||
|
latest: "1.0.0",
|
||||||
|
},
|
||||||
|
]) as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne(options: any) {
|
||||||
|
const where = options.where;
|
||||||
|
if (Array.isArray(where) && where.some(item => item.name === "testtest3" && item.pluginType === "dnsProvider")) {
|
||||||
|
return {
|
||||||
|
id: 5,
|
||||||
|
version: "1.0.0",
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const list = await service.onlinePluginList({});
|
||||||
|
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].installed, true);
|
||||||
|
assert.equal(list[0].upgradeAvailable, false);
|
||||||
|
assert.equal(list[0].localPluginId, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mark uninstalled online plugins as upgradeable", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
service.pluginMarketItemRepository = createMarketRepositoryMock([
|
||||||
|
{
|
||||||
|
fullName: "developer/new-plugin",
|
||||||
|
author: "developer",
|
||||||
|
name: "new-plugin",
|
||||||
|
latest: "1.0.0",
|
||||||
|
},
|
||||||
|
]) as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const list = await service.onlinePluginList({});
|
||||||
|
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].installed, false);
|
||||||
|
assert.equal(list[0].upgradeAvailable, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mark installed online plugins without local version as upgradeable", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
service.pluginMarketItemRepository = createMarketRepositoryMock([
|
||||||
|
{
|
||||||
|
fullName: "developer/testtest2",
|
||||||
|
author: "developer",
|
||||||
|
name: "testtest2",
|
||||||
|
pluginType: "access",
|
||||||
|
latest: "1.0.0",
|
||||||
|
},
|
||||||
|
]) as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne() {
|
||||||
|
return {
|
||||||
|
id: 8,
|
||||||
|
version: null,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const list = await service.onlinePluginList({});
|
||||||
|
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].installed, true);
|
||||||
|
assert.equal(list[0].installedVersion, null);
|
||||||
|
assert.equal(list[0].upgradeAvailable, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("syncs online plugin list metadata to local market table without yaml content", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
const savedRecords: any[] = [];
|
||||||
|
const existingRecords: any[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
fullName: "developer/old-plugin",
|
||||||
|
author: "developer",
|
||||||
|
name: "old-plugin",
|
||||||
|
pluginType: "deploy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const requestCalls: any[] = [];
|
||||||
|
const buildItem = (index: number) => {
|
||||||
|
return {
|
||||||
|
fullName: `developer/plugin-${index}`,
|
||||||
|
author: "developer",
|
||||||
|
name: `plugin-${index}`,
|
||||||
|
pluginType: index % 2 === 0 ? "deploy" : "access",
|
||||||
|
title: `plugin ${index}`,
|
||||||
|
latest: "1.0.0",
|
||||||
|
status: "published",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
service.plusService = {
|
||||||
|
async register() {},
|
||||||
|
async request(config: any) {
|
||||||
|
requestCalls.push(config);
|
||||||
|
assert.equal(config.url, "/activation/plugin/list");
|
||||||
|
assert.equal(config.data.pluginType, undefined);
|
||||||
|
assert.equal(config.data.page.limit, 200);
|
||||||
|
if (config.data.page.start === 0) {
|
||||||
|
return {
|
||||||
|
list: Array.from({ length: 200 }, (_, index) => {
|
||||||
|
return buildItem(index + 1);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (config.data.page.start === 200) {
|
||||||
|
return {
|
||||||
|
list: [buildItem(201)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
service.pluginMarketItemRepository = {
|
||||||
|
async find() {
|
||||||
|
return existingRecords;
|
||||||
|
},
|
||||||
|
create(record: any) {
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
async save(records: any[]) {
|
||||||
|
savedRecords.push(...records);
|
||||||
|
return records;
|
||||||
|
},
|
||||||
|
createQueryBuilder() {
|
||||||
|
return {
|
||||||
|
andWhere() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
orderBy() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
addOrderBy() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
getMany() {
|
||||||
|
return savedRecords;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
service.repository = {
|
||||||
|
async findOne() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const res = await service.syncOnlinePluginList();
|
||||||
|
|
||||||
|
assert.equal(requestCalls.length, 2);
|
||||||
|
assert.deepEqual(
|
||||||
|
requestCalls.map(item => item.data.page.start),
|
||||||
|
[0, 200]
|
||||||
|
);
|
||||||
|
assert.equal(res.length, 201);
|
||||||
|
assert.equal(savedRecords.length, 201);
|
||||||
|
assert.equal(
|
||||||
|
savedRecords.some(record => record.content),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(savedRecords[0].fullName, "developer/plugin-1");
|
||||||
|
assert.equal(savedRecords[200].fullName, "developer/plugin-201");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("downloads and imports online plugin content as store plugin", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
let importReq: any;
|
||||||
|
|
||||||
|
service.plusService = {
|
||||||
|
async register() {},
|
||||||
|
async request(config: any) {
|
||||||
|
assert.equal(config.url, "/activation/plugin/download");
|
||||||
|
assert.deepEqual(config.data, { fullName: "greper/DeployToDemo", version: "1.0.1" });
|
||||||
|
return {
|
||||||
|
fullName: "greper/DeployToDemo",
|
||||||
|
content: "name: DeployToDemo\npluginType: deploy\nauthor: greper\ncontent: |\n return class Demo {}\n",
|
||||||
|
plugin: {
|
||||||
|
fullName: "greper/DeployToDemo",
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
version: "1.0.1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
service.importPlugin = async req => {
|
||||||
|
importReq = req;
|
||||||
|
return { id: 9 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await service.installOnlinePlugin({ fullName: "greper/DeployToDemo", version: "1.0.1" });
|
||||||
|
|
||||||
|
assert.equal(res.id, 9);
|
||||||
|
assert.equal(res.fullName, "greper/DeployToDemo");
|
||||||
|
assert.equal(importReq.override, true);
|
||||||
|
assert.equal(importReq.type, "store");
|
||||||
|
assert.match(importReq.content, /DeployToDemo/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fills missing yaml version from online plugin version before import", async () => {
|
||||||
|
const service = new PluginService();
|
||||||
|
let importReq: any;
|
||||||
|
|
||||||
|
service.plusService = {
|
||||||
|
async register() {},
|
||||||
|
async request() {
|
||||||
|
return {
|
||||||
|
fullName: "developer/testtest2",
|
||||||
|
content: "name: testtest2\npluginType: access\nauthor: developer\ncontent: |\n return class DemoAccess {}\n",
|
||||||
|
plugin: {
|
||||||
|
fullName: "developer/testtest2",
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
service.importPlugin = async req => {
|
||||||
|
importReq = req;
|
||||||
|
return { id: 10 };
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.installOnlinePlugin({ fullName: "developer/testtest2" });
|
||||||
|
|
||||||
|
assert.match(importReq.content, /version: 1\.0\.0/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
import { addonRegistry, BaseService, PageReq } from "@certd/lib-server";
|
import { addonRegistry, BaseService, PageReq, PlusService } from "@certd/lib-server";
|
||||||
import { PluginEntity } from "../entity/plugin.js";
|
import { PluginEntity } from "../entity/plugin.js";
|
||||||
|
import { PluginMarketItemEntity } from "../entity/plugin-market-item.js";
|
||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
import { IsNull, Not, Repository } from "typeorm";
|
import { Brackets, IsNull, Not, Repository } from "typeorm";
|
||||||
import { isComm } from "@certd/plus-core";
|
import { isComm } from "@certd/plus-core";
|
||||||
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
|
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
|
||||||
import { merge } from "lodash-es";
|
import { merge } from "lodash-es";
|
||||||
@@ -17,8 +18,115 @@ import { importRuntime as importRuntimeDirect, getRuntimeDepsService, pluginRegi
|
|||||||
export type PluginImportReq = {
|
export type PluginImportReq = {
|
||||||
content: string;
|
content: string;
|
||||||
override?: boolean;
|
override?: boolean;
|
||||||
|
type?: "custom" | "store";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OnlinePluginListReq = {
|
||||||
|
pluginType?: string;
|
||||||
|
group?: string;
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnlinePluginInstallReq = {
|
||||||
|
fullName: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ONLINE_PLUGIN_SYNC_PAGE_SIZE = 200;
|
||||||
|
|
||||||
|
export function parseOnlinePluginFullName(fullName: string) {
|
||||||
|
const parts = (fullName || "")
|
||||||
|
.trim()
|
||||||
|
.split("/")
|
||||||
|
.map(item => item.trim());
|
||||||
|
if (parts.length !== 2 || !parts[0] || !parts[1] || parts[1].includes(":") || parts[1].includes("/")) {
|
||||||
|
throw new Error("插件标识格式错误");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
author: parts[0],
|
||||||
|
name: parts[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareOnlinePluginVersion(current: string, latest: string) {
|
||||||
|
const normalize = (version: string) => {
|
||||||
|
return (version || "").trim().replace(/^[vV]/, "");
|
||||||
|
};
|
||||||
|
const currentText = normalize(current);
|
||||||
|
const latestText = normalize(latest);
|
||||||
|
const currentParts = currentText.split(".");
|
||||||
|
const latestParts = latestText.split(".");
|
||||||
|
const maxLength = Math.max(currentParts.length, latestParts.length);
|
||||||
|
|
||||||
|
for (let index = 0; index < maxLength; index++) {
|
||||||
|
const currentPart = Number(currentParts[index] || 0);
|
||||||
|
const latestPart = Number(latestParts[index] || 0);
|
||||||
|
if (!Number.isInteger(currentPart) || !Number.isInteger(latestPart)) {
|
||||||
|
return currentText.localeCompare(latestText);
|
||||||
|
}
|
||||||
|
if (currentPart > latestPart) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (currentPart < latestPart) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOnlinePluginUpgradeAvailable(installedVersion?: string, latestVersion?: string) {
|
||||||
|
const installed = (installedVersion || "").trim();
|
||||||
|
const latest = (latestVersion || "").trim();
|
||||||
|
if (!installed || !latest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return compareOnlinePluginVersion(installed, latest) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillOnlinePluginYamlVersion(content: string, version?: string) {
|
||||||
|
const versionText = (version || "").trim();
|
||||||
|
if (!versionText) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
const loaded = yaml.load(content);
|
||||||
|
if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) {
|
||||||
|
throw new Error("插件 YAML 内容格式错误");
|
||||||
|
}
|
||||||
|
return yaml.dump(
|
||||||
|
{
|
||||||
|
...(loaded as Record<string, any>),
|
||||||
|
version: versionText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
sortKeys: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isBareModuleSpecifier(modulePath: string) {
|
function isBareModuleSpecifier(modulePath: string) {
|
||||||
if (modulePath.startsWith(".") || modulePath.startsWith("/") || modulePath.startsWith("file:") || modulePath.startsWith("node:")) {
|
if (modulePath.startsWith(".") || modulePath.startsWith("/") || modulePath.startsWith("file:") || modulePath.startsWith("node:")) {
|
||||||
return false;
|
return false;
|
||||||
@@ -45,9 +153,15 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
@InjectEntityModel(PluginEntity)
|
@InjectEntityModel(PluginEntity)
|
||||||
repository: Repository<PluginEntity>;
|
repository: Repository<PluginEntity>;
|
||||||
|
|
||||||
|
@InjectEntityModel(PluginMarketItemEntity)
|
||||||
|
pluginMarketItemRepository: Repository<PluginMarketItemEntity>;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
builtInPluginService: BuiltInPluginService;
|
builtInPluginService: BuiltInPluginService;
|
||||||
|
|
||||||
|
@Inject("plusService")
|
||||||
|
plusService: PlusService;
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
getRepository() {
|
getRepository() {
|
||||||
return this.repository;
|
return this.repository;
|
||||||
@@ -269,6 +383,234 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
await this.refreshPluginDeps();
|
await this.refreshPluginDeps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOnlinePluginFullName(item: OnlinePluginBean) {
|
||||||
|
if (item.fullName) {
|
||||||
|
return item.fullName;
|
||||||
|
}
|
||||||
|
if (item.author && item.name) {
|
||||||
|
return `${item.author}/${item.name}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeOnlinePluginMarketItem(item: OnlinePluginBean, syncTime: number, old?: PluginMarketItemEntity) {
|
||||||
|
return this.pluginMarketItemRepository.create({
|
||||||
|
id: old?.id,
|
||||||
|
appId: item.appId,
|
||||||
|
fullName: this.getOnlinePluginFullName(item),
|
||||||
|
author: item.author,
|
||||||
|
name: item.name,
|
||||||
|
pluginType: item.pluginType,
|
||||||
|
title: item.title,
|
||||||
|
icon: item.icon,
|
||||||
|
group: item.group,
|
||||||
|
desc: item.desc,
|
||||||
|
latest: item.latest,
|
||||||
|
status: item.status,
|
||||||
|
downloadCount: item.downloadCount,
|
||||||
|
syncTime,
|
||||||
|
updateTime: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toOnlinePluginBean(item: PluginMarketItemEntity): OnlinePluginBean {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
appId: item.appId,
|
||||||
|
fullName: item.fullName,
|
||||||
|
author: item.author,
|
||||||
|
name: item.name,
|
||||||
|
pluginType: item.pluginType,
|
||||||
|
title: item.title,
|
||||||
|
icon: item.icon,
|
||||||
|
group: item.group,
|
||||||
|
desc: item.desc,
|
||||||
|
latest: item.latest,
|
||||||
|
status: item.status,
|
||||||
|
downloadCount: item.downloadCount,
|
||||||
|
syncTime: item.syncTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findOnlinePluginMarketItems(req: OnlinePluginListReq) {
|
||||||
|
const query = req || {};
|
||||||
|
const keyword = (query.keyword || "").trim().toLowerCase();
|
||||||
|
const builder = this.pluginMarketItemRepository.createQueryBuilder("item");
|
||||||
|
if (query.pluginType) {
|
||||||
|
builder.andWhere("item.pluginType = :pluginType", {
|
||||||
|
pluginType: query.pluginType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (query.group) {
|
||||||
|
builder.andWhere("item.group = :group", {
|
||||||
|
group: query.group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (keyword) {
|
||||||
|
builder.andWhere(
|
||||||
|
new Brackets(qb => {
|
||||||
|
qb.where("LOWER(item.fullName) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.author) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.name) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.title) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.desc) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.group) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||||
|
.orWhere("LOWER(item.pluginType) LIKE :keyword", { keyword: `%${keyword}%` });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await builder.orderBy("item.pluginType", "ASC").addOrderBy("item.group", "ASC").addOrderBy("item.title", "ASC").addOrderBy("item.fullName", "ASC").addOrderBy("item.id", "ASC").getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findInstalledOnlinePlugin(parsed: { author: string; name: string }, record: OnlinePluginBean) {
|
||||||
|
const fullName = record.fullName || `${parsed.author}/${parsed.name}`;
|
||||||
|
const where: any[] = [
|
||||||
|
{
|
||||||
|
author: parsed.author,
|
||||||
|
name: parsed.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fullName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (record.pluginType) {
|
||||||
|
where.push({
|
||||||
|
pluginType: record.pluginType,
|
||||||
|
name: parsed.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await this.repository.findOne({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attachOnlineInstallState(list: OnlinePluginBean[]) {
|
||||||
|
const records: OnlinePluginBean[] = [];
|
||||||
|
for (const item of list) {
|
||||||
|
const record: OnlinePluginBean = { ...item };
|
||||||
|
const fullName = this.getOnlinePluginFullName(record);
|
||||||
|
record.fullName = fullName;
|
||||||
|
|
||||||
|
let parsed: { author: string; name: string };
|
||||||
|
try {
|
||||||
|
parsed = parseOnlinePluginFullName(fullName);
|
||||||
|
} catch (e) {
|
||||||
|
record.installed = false;
|
||||||
|
record.upgradeAvailable = false;
|
||||||
|
records.push(record);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPlugin = await this.findInstalledOnlinePlugin(parsed, record);
|
||||||
|
record.installed = !!localPlugin;
|
||||||
|
record.localPluginId = localPlugin?.id;
|
||||||
|
record.localDisabled = localPlugin?.disabled;
|
||||||
|
record.installedVersion = localPlugin?.version;
|
||||||
|
record.upgradeAvailable = localPlugin ? isOnlinePluginUpgradeAvailable(localPlugin.version, record.latest) : false;
|
||||||
|
records.push(record);
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onlinePluginList(req: OnlinePluginListReq) {
|
||||||
|
const list = await this.findOnlinePluginMarketItems(req);
|
||||||
|
return await this.attachOnlineInstallState(list.map(item => this.toOnlinePluginBean(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncOnlinePluginList() {
|
||||||
|
await this.plusService.register();
|
||||||
|
|
||||||
|
// 只同步市场列表元数据,插件 YAML 内容仍然在安装时按版本下载。
|
||||||
|
const pluginMap = new Map<string, OnlinePluginBean>();
|
||||||
|
let pageStart = 0;
|
||||||
|
while (true) {
|
||||||
|
const res = await this.plusService.request({
|
||||||
|
url: "/activation/plugin/list",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
page: {
|
||||||
|
start: pageStart,
|
||||||
|
limit: ONLINE_PLUGIN_SYNC_PAGE_SIZE,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
asc: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = res?.list || [];
|
||||||
|
for (const item of list) {
|
||||||
|
const record = { ...item };
|
||||||
|
const fullName = this.getOnlinePluginFullName(record);
|
||||||
|
if (!fullName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
record.fullName = fullName;
|
||||||
|
pluginMap.set(fullName, record);
|
||||||
|
}
|
||||||
|
if (list.length < ONLINE_PLUGIN_SYNC_PAGE_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
const existingList = await this.pluginMarketItemRepository.find();
|
||||||
|
const existingMap = new Map(existingList.map(item => [item.fullName, item]));
|
||||||
|
const syncTime = Date.now();
|
||||||
|
const records = Array.from(pluginMap.values()).map(item => {
|
||||||
|
return this.normalizeOnlinePluginMarketItem(item, syncTime, existingMap.get(item.fullName));
|
||||||
|
});
|
||||||
|
await this.pluginMarketItemRepository.save(records);
|
||||||
|
return await this.onlinePluginList({});
|
||||||
|
}
|
||||||
|
|
||||||
|
async installOnlinePlugin(req: OnlinePluginInstallReq) {
|
||||||
|
const fullName = (req.fullName || "").trim();
|
||||||
|
parseOnlinePluginFullName(fullName);
|
||||||
|
|
||||||
|
await this.plusService.register();
|
||||||
|
const res = await this.plusService.request({
|
||||||
|
url: "/activation/plugin/download",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
fullName,
|
||||||
|
version: req.version || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res?.content) {
|
||||||
|
throw new Error("插件内容为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fillOnlinePluginYamlVersion(res.content, res.version?.version || req.version);
|
||||||
|
const importRes = await this.importPlugin({
|
||||||
|
content,
|
||||||
|
override: true,
|
||||||
|
type: "store",
|
||||||
|
});
|
||||||
|
await this.refreshOnlinePluginDownloadCount(res.fullName || fullName, res.plugin?.downloadCount);
|
||||||
|
return {
|
||||||
|
...importRes,
|
||||||
|
fullName: res.fullName || fullName,
|
||||||
|
plugin: res.plugin,
|
||||||
|
version: res.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) {
|
||||||
|
if (!this.pluginMarketItemRepository || !fullName || downloadCount == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.pluginMarketItemRepository.update(
|
||||||
|
{
|
||||||
|
fullName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
downloadCount,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async unRegisterById(id: any) {
|
async unRegisterById(id: any) {
|
||||||
const item = await this.info(id);
|
const item = await this.info(id);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@@ -533,6 +875,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
|
|
||||||
const pluginEntity = {
|
const pluginEntity = {
|
||||||
...loaded,
|
...loaded,
|
||||||
|
type: req.type || loaded.type || "custom",
|
||||||
metadata: yaml.dump(metadata),
|
metadata: yaml.dump(metadata),
|
||||||
extra: yaml.dump(extra),
|
extra: yaml.dump(extra),
|
||||||
content: loaded.content,
|
content: loaded.content,
|
||||||
|
|||||||
Reference in New Issue
Block a user