mirror of
https://github.com/certd/certd.git
synced 2026-08-03 20:05:54 +08:00
feat: 支持在线插件下载安装审核等
This commit is contained in:
@@ -40,7 +40,6 @@ export default {
|
||||
headerMenus: "Top Menu Settings",
|
||||
sysAccess: "System-level Authorization",
|
||||
sysPlugin: "Plugin Management",
|
||||
sysPluginEdit: "Edit Plugin",
|
||||
sysPluginConfig: "Certificate Plugin Configuration",
|
||||
accountBind: "Account Binding",
|
||||
permissionManager: "Permission Management",
|
||||
|
||||
@@ -59,6 +59,7 @@ export default {
|
||||
onlinePluginUninstall: "Uninstall",
|
||||
onlinePluginStatus: "Status",
|
||||
onlinePluginInstalled: "Installed",
|
||||
onlinePluginAiReviewPassed: "AI reviewed",
|
||||
onlinePluginEnabled: "Enabled",
|
||||
onlinePluginDisabled: "Disabled",
|
||||
onlinePluginClickToEnable: "Click to enable",
|
||||
@@ -69,9 +70,44 @@ export default {
|
||||
onlinePluginCurrentVersion: "Current {version}",
|
||||
onlinePluginAlreadyLatest: "Already up to date",
|
||||
onlinePluginClickToUpdate: "Click to update",
|
||||
onlinePluginSelfAuthored: "Mine",
|
||||
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.',
|
||||
onlinePluginPublish: "Publish to Plugin Market",
|
||||
onlinePluginPublishManage: "Plugin Publish Management",
|
||||
onlinePluginPublishConfirm: 'Publish "{name}" to the plugin market? It will be checked by AI and then wait for administrator review.',
|
||||
onlinePluginPublishConfirmTip: "Please confirm the plugin information. It will be checked by AI and then wait for administrator review.",
|
||||
onlinePluginPublishManageTip: "Confirm the current plugin information. If it has been submitted before, versions, publish status, and rejection reasons are shown here.",
|
||||
onlinePluginPublishSubmit: "Submit Publish",
|
||||
onlinePluginPublishSuccess: "Submitted, waiting for AI security review",
|
||||
onlinePluginPublishVersionRequired: "Please set the plugin version before publishing",
|
||||
onlinePluginPublishStatus: "Publish Status",
|
||||
onlinePluginNotSubmitted: "Not Submitted",
|
||||
onlinePluginVersionManagement: "Versions",
|
||||
onlinePluginLatestVersion: "Latest Version",
|
||||
onlinePluginCurrentRelease: "This Release",
|
||||
onlinePluginPublishPrompt: "Release Notes",
|
||||
onlinePluginVersionRejectedReason: "Rejection Reason",
|
||||
onlinePluginNoSubmittedVersion: "No release has been submitted yet",
|
||||
onlinePluginVersionFormatError: "Use a dotted numeric version, for example 1.0.0",
|
||||
onlinePluginVersionBaselineError: "Enter a version greater than v{version}",
|
||||
onlinePluginStatusDraft: "Draft",
|
||||
onlinePluginStatusReviewing: "Reviewing",
|
||||
onlinePluginStatusPublished: "Published",
|
||||
onlinePluginStatusRejected: "Rejected",
|
||||
onlinePluginStatusOffline: "Offline",
|
||||
onlinePluginReviewAiPending: "Pending AI Review",
|
||||
onlinePluginReviewPending: "Pending Manual Review",
|
||||
onlinePluginReviewPassed: "Passed",
|
||||
onlinePluginReviewRejected: "Rejected",
|
||||
onlinePluginAuthorRegister: "Register Plugin Author",
|
||||
onlinePluginAuthorName: "Author Name",
|
||||
onlinePluginAuthorDisplayName: "Display Name",
|
||||
onlinePluginAuthorNotRegistered: "Not Registered",
|
||||
onlinePluginAuthorNameRequired: "Please enter an author name",
|
||||
onlinePluginAuthorNameHelper: "Used as the plugin market author prefix. Each bound user can register only one author.",
|
||||
onlinePluginAuthorNameRuleMsg: "Must start with a letter and contain only letters, digits, hyphens, or underscores",
|
||||
pleaseSelectRecord: "Please select records first",
|
||||
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
||||
clearRuntimeDepsTooltip: "Restart the certd container after clearing, otherwise cached modules will not be reloaded",
|
||||
|
||||
@@ -41,7 +41,6 @@ export default {
|
||||
headerMenus: "顶部菜单设置",
|
||||
sysAccess: "系统级授权",
|
||||
sysPlugin: "插件管理",
|
||||
sysPluginEdit: "编辑插件",
|
||||
sysPluginConfig: "证书插件配置",
|
||||
accountBind: "账号绑定",
|
||||
permissionManager: "权限管理",
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
||||
localPlugin: "本地插件",
|
||||
pluginMarket: "插件市场",
|
||||
installedStorePlugin: "已安装",
|
||||
installedStorePlugin: "插件市场",
|
||||
onlinePluginManagement: "在线插件",
|
||||
onlinePluginSearch: "搜索在线插件",
|
||||
onlinePluginSync: "同步插件市场",
|
||||
@@ -58,6 +58,7 @@ export default {
|
||||
onlinePluginUninstall: "卸载",
|
||||
onlinePluginStatus: "状态",
|
||||
onlinePluginInstalled: "已安装",
|
||||
onlinePluginAiReviewPassed: "AI审核通过",
|
||||
onlinePluginEnabled: "已启用",
|
||||
onlinePluginDisabled: "已禁用",
|
||||
onlinePluginClickToEnable: "点击启用",
|
||||
@@ -68,9 +69,44 @@ export default {
|
||||
onlinePluginCurrentVersion: "当前 {version}",
|
||||
onlinePluginAlreadyLatest: "当前已是最新版本",
|
||||
onlinePluginClickToUpdate: "点击更新",
|
||||
onlinePluginSelfAuthored: "我的",
|
||||
onlinePluginInstallSuccess: "在线插件安装成功",
|
||||
onlinePluginUninstallSuccess: "在线插件卸载成功",
|
||||
onlinePluginDeleteConfirm: "确定要卸载在线插件「{name}」吗?如果该插件已被流水线使用,删除可能会导致执行失败。",
|
||||
onlinePluginPublish: "发布到插件市场",
|
||||
onlinePluginPublishManage: "插件发布管理",
|
||||
onlinePluginPublishConfirm: "确定要将「{name}」发布到插件市场吗?提交后会进行 AI 安全审查,通过后等待管理员审核。",
|
||||
onlinePluginPublishConfirmTip: "请确认以下插件信息,提交后会进行 AI 安全审查,通过后等待管理员审核。",
|
||||
onlinePluginPublishManageTip: "请确认当前插件信息;如已提交过发布,可以在这里查看版本、发布状态和拒绝原因。",
|
||||
onlinePluginPublishSubmit: "提交发布",
|
||||
onlinePluginPublishSuccess: "已提交,等待 AI 安全审查",
|
||||
onlinePluginPublishVersionRequired: "请先设置插件版本号后再发布",
|
||||
onlinePluginPublishStatus: "发布状态",
|
||||
onlinePluginNotSubmitted: "未提交",
|
||||
onlinePluginVersionManagement: "版本管理",
|
||||
onlinePluginLatestVersion: "最新版本",
|
||||
onlinePluginCurrentRelease: "本次发布",
|
||||
onlinePluginPublishPrompt: "发布提示",
|
||||
onlinePluginVersionRejectedReason: "拒绝原因",
|
||||
onlinePluginNoSubmittedVersion: "尚未提交过发布版本",
|
||||
onlinePluginVersionFormatError: "版本号只能使用数字点分格式,例如 1.0.0",
|
||||
onlinePluginVersionBaselineError: "请输入大于 v{version} 的版本号",
|
||||
onlinePluginStatusDraft: "草稿",
|
||||
onlinePluginStatusReviewing: "审核中",
|
||||
onlinePluginStatusPublished: "已发布",
|
||||
onlinePluginStatusRejected: "已拒绝",
|
||||
onlinePluginStatusOffline: "已下架",
|
||||
onlinePluginReviewAiPending: "待 AI 审查",
|
||||
onlinePluginReviewPending: "待人工审核",
|
||||
onlinePluginReviewPassed: "已通过",
|
||||
onlinePluginReviewRejected: "已拒绝",
|
||||
onlinePluginAuthorRegister: "注册插件作者",
|
||||
onlinePluginAuthorName: "作者名称",
|
||||
onlinePluginAuthorDisplayName: "显示名称",
|
||||
onlinePluginAuthorNotRegistered: "未注册",
|
||||
onlinePluginAuthorNameRequired: "请输入作者名称",
|
||||
onlinePluginAuthorNameHelper: "作为插件市场作者前缀,同一个绑定用户只能注册一个作者",
|
||||
onlinePluginAuthorNameRuleMsg: "必须以英文字母开头,只能包含英文字母、数字、中划线或下划线",
|
||||
pleaseSelectRecord: "请先勾选记录",
|
||||
clearRuntimeDeps: "清理第三方依赖缓存",
|
||||
clearRuntimeDepsTooltip: "清除后需重启 certd 容器,否则已缓存模块不会重新读取",
|
||||
|
||||
@@ -154,19 +154,6 @@ export const sysResources = [
|
||||
auth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "certd.sysResources.sysPluginEdit",
|
||||
name: "SysPluginEdit",
|
||||
path: "/sys/plugin/edit",
|
||||
component: "/sys/plugin/edit.vue",
|
||||
meta: {
|
||||
isMenu: false,
|
||||
icon: "ion:extension-puzzle",
|
||||
permission: "sys:settings:view",
|
||||
keepAlive: true,
|
||||
auth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "certd.sysResources.sysPluginConfig",
|
||||
name: "SysPluginConfig",
|
||||
|
||||
@@ -153,6 +153,22 @@ export class PluginGroups {
|
||||
}
|
||||
}
|
||||
|
||||
function filterInstalledPluginGroups(groups: { [key: string]: PluginGroup }) {
|
||||
const filteredGroups: { [key: string]: PluginGroup } = {};
|
||||
for (const [key, group] of Object.entries(groups)) {
|
||||
filteredGroups[key] = {
|
||||
...group,
|
||||
plugins: group.plugins.filter(plugin => {
|
||||
if (plugin.type !== "store") {
|
||||
return true;
|
||||
}
|
||||
return plugin.installed === true;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return filteredGroups;
|
||||
}
|
||||
|
||||
export const usePluginStore = defineStore({
|
||||
id: "app.plugin",
|
||||
state: (): PluginState => ({
|
||||
@@ -162,8 +178,9 @@ export const usePluginStore = defineStore({
|
||||
actions: {
|
||||
async reload() {
|
||||
const groups = await api.GetGroups({});
|
||||
this.group = new PluginGroups(groups, { mergeSetting: true });
|
||||
this.originGroup = new PluginGroups(cloneDeep(groups));
|
||||
const installedGroups = filterInstalledPluginGroups(groups);
|
||||
this.group = new PluginGroups(installedGroups, { mergeSetting: true });
|
||||
this.originGroup = new PluginGroups(cloneDeep(installedGroups));
|
||||
console.log("group", this.group);
|
||||
console.log("originGroup", this.originGroup);
|
||||
},
|
||||
|
||||
@@ -75,3 +75,9 @@ footer {
|
||||
.ant-input-number {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-title-content{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
@@ -36,6 +36,116 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cd-card-section {
|
||||
padding: 14px;
|
||||
border: 1px solid #e8edf3;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.cd-card-section-title {
|
||||
margin-bottom: 12px;
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
|
||||
&.cd-card-section-title--compact {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cd-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 14px;
|
||||
}
|
||||
|
||||
.cd-meta-item {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cd-meta-label {
|
||||
color: #7b8794;
|
||||
}
|
||||
|
||||
.cd-meta-value {
|
||||
min-width: 0;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cd-tip-box {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e8edf3;
|
||||
border-radius: 8px;
|
||||
background: #f6f8fb;
|
||||
color: #526172;
|
||||
line-height: 22px;
|
||||
|
||||
&.cd-tip-box-warning {
|
||||
border-color: #ffd591;
|
||||
background: #fff7e6;
|
||||
color: #ad6800;
|
||||
}
|
||||
|
||||
&.cd-tip-box-danger {
|
||||
border-color: #ffccc7;
|
||||
background: #fff2f0;
|
||||
color: #cf1322;
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
.cd-tip-title {
|
||||
margin-bottom: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cd-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 82px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cd-form-label {
|
||||
padding-top: 5px;
|
||||
color: #5f6b7a;
|
||||
}
|
||||
|
||||
.cd-text-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 4px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
|
||||
&.cd-text-input-error {
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.cd-field-error {
|
||||
margin-top: 6px;
|
||||
color: #cf1322;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cd-text-danger {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.cd-text-muted {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.ant-drawer-content {
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
@@ -140,3 +250,9 @@ button.ant-btn.ant-btn-default.isPlus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-form-none-content {
|
||||
.fs-form-wrapper{
|
||||
.fs-form-content{display: none;}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ export type FormOptionReq = {
|
||||
initialForm?: any;
|
||||
zIndex?: number;
|
||||
wrapper?: any;
|
||||
noneForm?: boolean; //是否隐藏表单,只显示注入的body
|
||||
};
|
||||
|
||||
export function useFormDialog() {
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
|
||||
async function openFormDialog(req: FormOptionReq) {
|
||||
const noneForm = req.noneForm ?? Object.keys(req.columns).length === 0;
|
||||
function createCrudOptions() {
|
||||
const warpper = merge(
|
||||
{
|
||||
@@ -24,6 +26,7 @@ export function useFormDialog() {
|
||||
slots: {
|
||||
"form-body-top": req.body,
|
||||
},
|
||||
wrapClassName: noneForm ? "fs-form-none-content" : "",
|
||||
},
|
||||
req.wrapper
|
||||
);
|
||||
|
||||
+83
-2
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="stepDrawerVisible" :wrap-style="{ maxWidth: '100vw' }" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
|
||||
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
|
||||
<template #title>
|
||||
<div>
|
||||
编辑步骤
|
||||
@@ -30,7 +30,7 @@
|
||||
</a-tabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="padding: 20px; margin-left: 100px">
|
||||
<div class="bottom-button">
|
||||
<a-button v-if="editMode" type="primary" @click="stepTypeSave"> 确定</a-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -396,6 +396,10 @@ defineExpose({
|
||||
.step-form-drawer {
|
||||
max-width: 100%;
|
||||
|
||||
.ant-drawer-content-wrapper {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin-top: 10px !important;
|
||||
padding: 8px 14px !important;
|
||||
@@ -434,6 +438,83 @@ defineExpose({
|
||||
}
|
||||
|
||||
.pi-step-form {
|
||||
.step-plugin-source-pane-local,
|
||||
.step-plugin-source-pane-online {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-plugin-search {
|
||||
flex: none;
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.step-plugin-selector-tabs {
|
||||
min-height: 0;
|
||||
|
||||
> .ant-tabs-nav {
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .ant-tabs-nav .ant-tabs-nav-wrap,
|
||||
> .ant-tabs-nav .ant-tabs-nav-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .ant-tabs-nav .ant-tabs-tab {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cd-step-form-tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.fs-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #00b7ff;
|
||||
|
||||
svg {
|
||||
vertical-align: middle !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
> .ant-tabs-content-holder {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
> .ant-tabs-content-holder > .ant-tabs-content {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
> .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane {
|
||||
padding-right: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-button {
|
||||
padding: 20px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
<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">
|
||||
<a-tabs v-model:active-key="pluginGroupActive" tab-position="left" class="step-plugin-selector-tabs flex-1 overflow-hidden h-full">
|
||||
<a-tab-pane v-for="group of computedLocalPluginGroups" :key="group.key" class="step-plugin-list-pane">
|
||||
<template #tab>
|
||||
<div class="cd-step-form-tab-label" @click="handleLocalPluginGroupChange(group.key)">
|
||||
<fs-icon :icon="group.icon" class="mr-2" />
|
||||
|
||||
+9
-94
@@ -3,8 +3,8 @@
|
||||
<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">
|
||||
<a-tabs v-model:active-key="onlinePluginGroupActive" tab-position="left" class="step-plugin-selector-tabs step-market-tabs flex-1 overflow-hidden h-full">
|
||||
<a-tab-pane v-for="group of computedOnlinePluginGroups" :key="group.key" class="step-plugin-list-pane">
|
||||
<template #tab>
|
||||
<div class="cd-step-form-tab-label" @click="handleOnlinePluginGroupChange(group.key)">
|
||||
<fs-icon :icon="group.icon" class="mr-2" />
|
||||
@@ -17,19 +17,7 @@
|
||||
<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"
|
||||
/>
|
||||
<PluginItemCard :plugin="item" simple :current="isOnlinePluginCurrent(item)" @click="onlinePluginSelected(item)" @dblclick="handleOnlinePluginCardDblclick(item)" @changed="handleOnlinePluginChanged" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="online-plugin-pagination">
|
||||
@@ -43,7 +31,6 @@
|
||||
</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";
|
||||
@@ -71,7 +58,6 @@ 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: "",
|
||||
@@ -215,86 +201,15 @@ function getPagedOnlinePlugins(groupKey: string) {
|
||||
return list.slice(start, start + onlinePageSize);
|
||||
}
|
||||
|
||||
function getOnlinePluginActionKey(plugin: pluginApi.OnlinePluginBean, action: "install" | "uninstall" | "toggle") {
|
||||
if (!plugin.fullName) {
|
||||
return "";
|
||||
async function handleOnlinePluginChanged(payload: { plugin: any; action: string }) {
|
||||
if (payload.action === "uninstall" && props.selectedType === payload.plugin.fullName) {
|
||||
emit("uninstalled", payload.plugin);
|
||||
}
|
||||
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"));
|
||||
if (payload.action === "install" || payload.action === "uninstall") {
|
||||
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 = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
await loadPluginGroups();
|
||||
await loadOnlinePlugins(true, { silent: true });
|
||||
}
|
||||
|
||||
function getInstalledPlugin(plugin: any) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { IframeClient } from "@certd/lib-iframe";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import * as api from "./api";
|
||||
@@ -23,6 +23,7 @@ defineOptions({
|
||||
name: "AccountBind",
|
||||
});
|
||||
const iframeRef = ref();
|
||||
let iframeClient: IframeClient | undefined;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const settingStore = useSettingStore();
|
||||
@@ -40,9 +41,10 @@ type SubjectInfo = {
|
||||
installAt?: number;
|
||||
vipType?: string;
|
||||
expiresAt?: number;
|
||||
userId?: number;
|
||||
};
|
||||
onMounted(() => {
|
||||
const iframeClient = new IframeClient(iframeRef.value, (e: any) => {
|
||||
iframeClient = new IframeClient(iframeRef.value, (e: any) => {
|
||||
notification.error({
|
||||
message: " error",
|
||||
description: e.message,
|
||||
@@ -54,6 +56,7 @@ onMounted(() => {
|
||||
installAt: settingStore.installInfo.installTime,
|
||||
vipType: settingStore.plusInfo.vipType || "free",
|
||||
expiresAt: settingStore.plusInfo.expireTime,
|
||||
userId: settingStore.installInfo.bindUserId || undefined,
|
||||
};
|
||||
return subjectInfo;
|
||||
});
|
||||
@@ -84,6 +87,11 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
iframeClient?.destroy();
|
||||
iframeClient = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -85,7 +85,9 @@ export async function ImportPlugin(body: any) {
|
||||
export type OnlinePluginBean = {
|
||||
id?: number;
|
||||
appId?: number;
|
||||
developerId?: number;
|
||||
author?: string;
|
||||
type?: string;
|
||||
pluginType?: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
@@ -96,14 +98,42 @@ export type OnlinePluginBean = {
|
||||
latest?: string;
|
||||
status?: string;
|
||||
downloadCount?: number;
|
||||
score?: number;
|
||||
aiCheckStatus?: string;
|
||||
selfAuthored?: boolean;
|
||||
installed?: boolean;
|
||||
installedVersion?: string;
|
||||
upgradeAvailable?: boolean;
|
||||
localPluginId?: number;
|
||||
localDisabled?: boolean;
|
||||
localEditable?: boolean;
|
||||
syncTime?: number;
|
||||
};
|
||||
|
||||
export type OnlinePluginVersionBean = {
|
||||
id?: number;
|
||||
pluginId?: number;
|
||||
version?: string;
|
||||
minAppVersion?: string;
|
||||
maxAppVersion?: string;
|
||||
status?: string;
|
||||
publishedAt?: number;
|
||||
reviewStatus?: string;
|
||||
reviewReason?: string;
|
||||
reviewedAt?: number;
|
||||
aiCheckStatus?: string;
|
||||
aiCheckResult?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginCommentBean = {
|
||||
id?: number;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
userId?: number;
|
||||
score?: number;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export async function OnlinePluginList(body: { pluginType?: string; group?: string; keyword?: string }): Promise<OnlinePluginBean[]> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/list",
|
||||
@@ -119,6 +149,13 @@ export async function OnlinePluginSync(): Promise<OnlinePluginBean[]> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginSetting(): Promise<{ lastSyncTime?: number }> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/setting",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginInstall(body: { fullName: string; version?: string }) {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/install",
|
||||
@@ -127,6 +164,94 @@ export async function OnlinePluginInstall(body: { fullName: string; version?: st
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginDetail(body: { pluginId?: number; fullName?: string; commentPageNo?: number; commentPageSize?: number }): Promise<{
|
||||
plugin: OnlinePluginBean;
|
||||
versions: OnlinePluginVersionBean[];
|
||||
myScore?: number;
|
||||
myComment?: string;
|
||||
comments?: OnlinePluginCommentBean[];
|
||||
commentsTotal?: number;
|
||||
}> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/detail",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginSource(body: { pluginId?: number; fullName?: string; version?: string }): Promise<{ plugin: OnlinePluginBean; version: OnlinePluginVersionBean; content: string }> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/source",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginRate(body: { pluginId?: number; fullName?: string; score: number; comment: string }): Promise<{ plugin: OnlinePluginBean; myScore?: number; myComment?: string }> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/rate",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginSubmitVersion(body: { fullName: string; version: string; content: string; minAppVersion?: string; maxAppVersion?: string }) {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/version/submit",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginPublish(body: { id: number; version?: string; minAppVersion?: string; maxAppVersion?: string }) {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/publish",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginPublishInfo(body: { id: number }): Promise<{
|
||||
localPlugin: OnlinePluginBean;
|
||||
authorRegistered?: boolean;
|
||||
author?: OnlinePluginAuthorBean;
|
||||
marketPlugin?: OnlinePluginBean;
|
||||
versions: OnlinePluginVersionBean[];
|
||||
}> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/publish/info",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export type OnlinePluginAuthorBean = {
|
||||
id?: number;
|
||||
appId?: number;
|
||||
appOwnerId?: number;
|
||||
developerId?: number;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
desc?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export async function OnlinePluginAuthorGet(): Promise<{ registered?: boolean; author?: OnlinePluginAuthorBean }> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/author/get",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export async function OnlinePluginAuthorAdd(body: { name: string; displayName?: string; avatar?: string; desc?: string }): Promise<OnlinePluginAuthorBean> {
|
||||
return await request({
|
||||
url: apiPrefix + "/online/author/add",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export type PluginConfigBean = {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
@@ -139,6 +264,9 @@ export type CertApplyPluginSysInput = {
|
||||
googleCommonEabAccessId?: number;
|
||||
zerosslCommonEabAccessId?: number;
|
||||
litesslCommonEabAccessId?: number;
|
||||
googleCommonAcmeAccountAccessId?: number;
|
||||
zerosslCommonAcmeAccountAccessId?: number;
|
||||
litesslCommonAcmeAccountAccessId?: number;
|
||||
};
|
||||
export type PluginSysSetting<T> = {
|
||||
sysSetting: {
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<a-modal v-model:open="visible" :width="'min(1540px, calc(100vw - 48px))'" :footer="null" destroy-on-close class="online-plugin-detail-modal">
|
||||
<a-spin :spinning="loading">
|
||||
<iframe v-if="iframeSrc" ref="iframeRef" class="online-plugin-detail-modal__iframe" :src="iframeSrc" @load="handleIframeLoad" />
|
||||
<a-empty v-else description="插件详情地址未配置" />
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { IframeClient } from "@certd/lib-iframe";
|
||||
import * as api from "../api";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useSettingStore } from "/src/store/settings";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
plugin?: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "installed", value: { plugin: any; version?: string }): void;
|
||||
}>();
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const settingStore = useSettingStore();
|
||||
const iframeRef = ref<HTMLIFrameElement>();
|
||||
const loading = ref(false);
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: value => emit("update:open", value),
|
||||
});
|
||||
const iframeSrc = computed(() => {
|
||||
const plugin = props.plugin;
|
||||
if (!plugin) {
|
||||
return "";
|
||||
}
|
||||
const baseUrl = String(settingStore.installInfo?.accountServerBaseUrl || "").replace(/\/$/, "");
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
embedded: "true",
|
||||
fullName: plugin.fullName || "",
|
||||
installedVersion: plugin.installedVersion || "",
|
||||
t: `${Date.now()}`,
|
||||
});
|
||||
return `${baseUrl}/#/app/certd/plugin/${plugin.id || 0}?${params.toString()}`;
|
||||
});
|
||||
|
||||
let iframeClient: IframeClient;
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
async open => {
|
||||
if (!open || !props.plugin) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
await nextTick();
|
||||
setupIframeClient();
|
||||
if (!iframeSrc.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
//@ts-ignore
|
||||
iframeClient?.destroy();
|
||||
});
|
||||
|
||||
function setupIframeClient() {
|
||||
//@ts-ignore
|
||||
iframeClient?.destroy();
|
||||
if (!iframeRef.value) {
|
||||
return;
|
||||
}
|
||||
iframeClient = new IframeClient(iframeRef.value, (error: any) => {
|
||||
message.error(error?.message || "插件操作失败");
|
||||
});
|
||||
iframeClient.register("installPlugin", async req => {
|
||||
return await installPluginVersion(req.data);
|
||||
});
|
||||
}
|
||||
|
||||
function handleIframeLoad() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function installPluginVersion(data: { fullName?: string; version?: string }) {
|
||||
const fullName = data?.fullName || props.plugin?.fullName;
|
||||
if (!fullName || !data?.version) {
|
||||
throw new Error("插件版本信息不完整");
|
||||
}
|
||||
await api.OnlinePluginInstall({
|
||||
fullName,
|
||||
version: data.version,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
emit("installed", {
|
||||
plugin: props.plugin,
|
||||
version: data.version,
|
||||
});
|
||||
return {
|
||||
version: data.version,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.online-plugin-detail-modal {
|
||||
.ant-modal-body {
|
||||
height: 80vh;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading,
|
||||
.ant-spin-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="plugin-author-field">
|
||||
<span v-if="authorName" class="plugin-author-field__name">{{ authorName }}</span>
|
||||
<a-button v-else type="link" size="small" :loading="loading" @click="registerAuthor">注册作者</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import * as api from "../api";
|
||||
import { usePluginPublish } from "../use-publish";
|
||||
|
||||
defineOptions({ name: "PluginAuthorField" });
|
||||
|
||||
const props = defineProps<{ modelValue?: string }>();
|
||||
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
|
||||
const loading = ref(false);
|
||||
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
|
||||
const { registerPluginAuthor } = usePluginPublish();
|
||||
const authorName = computed(() => props.modelValue || registeredAuthor.value?.name || "");
|
||||
|
||||
async function loadAuthor() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await api.OnlinePluginAuthorGet();
|
||||
registeredAuthor.value = result.author;
|
||||
if (!props.modelValue && result.author?.name) {
|
||||
emit("update:modelValue", result.author.name);
|
||||
}
|
||||
} catch {
|
||||
// 作者为非必填项,未绑定账号或尚未注册作者时保持为空。
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerAuthor() {
|
||||
const author = await registerPluginAuthor();
|
||||
if (!author?.name) {
|
||||
return;
|
||||
}
|
||||
registeredAuthor.value = author;
|
||||
emit("update:modelValue", author.name);
|
||||
}
|
||||
|
||||
onMounted(loadAuthor);
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.plugin-author-field {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
|
||||
&__name {
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="plugin-edit-dialog-body">
|
||||
<div class="plugin-edit-dialog-body__header">
|
||||
<span>插件名称:</span>
|
||||
<fs-copyable :model-value="pluginName" />
|
||||
<a-button v-if="canEditPlugin" class="plugin-edit-dialog-body__publish" :loading="isPublishingPlugin(plugin)" @click="doPublish">发布到插件市场</a-button>
|
||||
</div>
|
||||
<div class="plugin-edit-dialog-body__content">
|
||||
<section class="plugin-edit-dialog-body__base">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="base" tab="插件信息" />
|
||||
</a-tabs>
|
||||
<div class="plugin-edit-dialog-body__base-content">
|
||||
<fs-form ref="baseFormRef" v-bind="formOptionsRef" />
|
||||
</div>
|
||||
</section>
|
||||
<section class="plugin-edit-dialog-body__editor plugin-edit-dialog-body__metadata">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="metadata" tab="元数据" />
|
||||
</a-tabs>
|
||||
<code-editor :id="`plugin-metadata-${pluginId}`" v-model:model-value="plugin.metadata" language="yaml" @save="doSave" />
|
||||
</section>
|
||||
<section class="plugin-edit-dialog-body__editor plugin-edit-dialog-body__script">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="script" tab="脚本" />
|
||||
</a-tabs>
|
||||
<code-editor :id="`plugin-content-${pluginId}`" v-model:model-value="plugin.content" language="javascript" @save="doSave" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, provide, ref, type Ref } from "vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { useColumns } from "@fast-crud/fast-crud";
|
||||
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
|
||||
import yaml from "js-yaml";
|
||||
import * as api from "../api";
|
||||
import createCrudOptions from "../crud";
|
||||
import { usePluginPublish } from "../use-publish";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
|
||||
defineOptions({
|
||||
name: "PluginEditDialogBody",
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
pluginId: number | string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "saved"): void;
|
||||
}>();
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const settingStore = useSettingStore();
|
||||
const plugin = ref<any>({});
|
||||
const formOptionsRef: Ref = ref();
|
||||
const baseFormRef: Ref = ref({});
|
||||
const saveLoading = ref(false);
|
||||
const { isPublishingPlugin, publishLocalPlugin } = usePluginPublish();
|
||||
|
||||
function initFormOptions() {
|
||||
const formCrudOptions = createCrudOptions({
|
||||
// 编辑弹框只复用插件表单字段,不需要 CRUD 实例。
|
||||
crudExpose: {},
|
||||
context: {},
|
||||
});
|
||||
const { buildFormOptions } = useColumns();
|
||||
const formOptions = buildFormOptions(formCrudOptions.crudOptions, {});
|
||||
formOptions.mode = "edit";
|
||||
formOptions.col = { span: 24 };
|
||||
formOptions.labelCol = { style: { width: "100px" } };
|
||||
formOptionsRef.value = formOptions;
|
||||
}
|
||||
|
||||
initFormOptions();
|
||||
|
||||
const pluginName = computed(() => {
|
||||
if (plugin.value.author) {
|
||||
return `${plugin.value.author}/${plugin.value.name}`;
|
||||
}
|
||||
return plugin.value.name || "";
|
||||
});
|
||||
|
||||
const canEditPlugin = computed(() => {
|
||||
const current = plugin.value || {};
|
||||
if (current.type === "custom") {
|
||||
return true;
|
||||
}
|
||||
if (current.type !== "store") {
|
||||
return false;
|
||||
}
|
||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
||||
return !current.developerId || (!!bindUserId && Number(current.developerId) === bindUserId);
|
||||
});
|
||||
|
||||
provide("get:plugin", () => plugin);
|
||||
|
||||
async function loadPlugin() {
|
||||
const pluginObj = await api.GetObj(props.pluginId);
|
||||
plugin.value = pluginObj;
|
||||
const baseForm = { ...pluginObj };
|
||||
if (baseForm.extra) {
|
||||
baseForm.extra = yaml.load(baseForm.extra);
|
||||
}
|
||||
delete baseForm.metadata;
|
||||
delete baseForm.content;
|
||||
baseFormRef.value.setFormData(baseForm);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
try {
|
||||
yaml.load(plugin.value.metadata);
|
||||
} catch (error: any) {
|
||||
const message = `元数据校验失败:${error.message}`;
|
||||
notification.error({ message });
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSubmitForm() {
|
||||
const baseForm = baseFormRef.value.getFormData();
|
||||
const form = { ...plugin.value, ...baseForm };
|
||||
if (form.extra) {
|
||||
form.extra = yaml.dump(form.extra);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
async function doSave() {
|
||||
if (!canEditPlugin.value) {
|
||||
notification.warning({ message: "当前绑定账号无权编辑该插件" });
|
||||
return;
|
||||
}
|
||||
validate();
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
const form = buildSubmitForm();
|
||||
await api.UpdateObj(form);
|
||||
plugin.value = form;
|
||||
pluginStore.clear();
|
||||
notification.success({ message: "保存成功" });
|
||||
emit("saved");
|
||||
return form;
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doPublish() {
|
||||
const baseForm = baseFormRef.value.getFormData();
|
||||
const currentPlugin = { ...plugin.value, ...baseForm };
|
||||
await publishLocalPlugin(currentPlugin, {
|
||||
beforePublish: doSave,
|
||||
async afterPublish() {
|
||||
await loadPlugin();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadPlugin);
|
||||
|
||||
defineExpose({
|
||||
save: doSave,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.plugin-edit-dialog-body {
|
||||
display: flex;
|
||||
min-height: 680px;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__publish {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
grid-template-columns: minmax(260px, 0.72fr) minmax(330px, 1fr) minmax(430px, 1.35fr);
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__base,
|
||||
&__editor {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__base {
|
||||
padding-right: 16px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
&__base-content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fs-editor-code {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .plugin-edit-dialog-body {
|
||||
&__base {
|
||||
border-right-color: #303030;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -41,8 +41,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, Ref, inject, toRef } from "vue";
|
||||
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import yaml from "js-yaml";
|
||||
const activeKey = ref([]);
|
||||
|
||||
const getPlugin: any = inject("get:plugin");
|
||||
|
||||
@@ -9,69 +9,80 @@
|
||||
'is-installed': isInstalled,
|
||||
'is-disabled': isDisabled,
|
||||
}"
|
||||
@click="emit('click', plugin)"
|
||||
@dblclick="emit('dblclick', plugin)"
|
||||
@click="handleCardClick"
|
||||
@dblclick="handleCardDoubleClick"
|
||||
>
|
||||
<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>
|
||||
<a v-if="showEditButton" class="plugin-card__title" :title="plugin.title || plugin.name" href="#" @click.stop.prevent="editPlugin">
|
||||
{{ plugin.title || plugin.name }}
|
||||
</router-link>
|
||||
</a>
|
||||
<span v-else class="plugin-card__title" :title="plugin.title || plugin.name">{{ plugin.title || plugin.name }}</span>
|
||||
<a-tooltip v-if="plugin.aiCheckStatus === 'passed'" :title="t('certd.onlinePluginAiReviewPassed')">
|
||||
<fs-icon class="plugin-card__ai-check-icon" icon="ion:shield-checkmark-outline" />
|
||||
</a-tooltip>
|
||||
</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") }}
|
||||
<div v-if="showEditButton || showConfigButton || showToolMenu" class="plugin-card__tools">
|
||||
<a-tooltip v-if="showEditButton" title="编辑">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="editPlugin">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:create-outline" />
|
||||
</template>
|
||||
</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-tooltip>
|
||||
<a-tooltip v-if="copyHandler && canCopyPlugin" title="复制">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="copyPlugin">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:copy-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="isOwnImportedPlugin" :title="t('certd.export')">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="exportPlugin">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:download-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="canPublishPlugin" :title="t('certd.onlinePluginPublish')">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" :loading="isPublishingPlugin(plugin)" @click.stop="publishPlugin">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:cloud-upload-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="showConfigButton" title="配置">
|
||||
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="openConfig">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:settings-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="source === 'local' && canRemoveLocal" class="plugin-card__action-zone plugin-card__local-action-zone">
|
||||
<a-tooltip title="删除">
|
||||
<a-button class="plugin-card__action-button" type="text" size="small" danger :loading="isActionLoading('remove')" @click.stop="removePlugin">
|
||||
<template #icon>
|
||||
<fs-icon icon="ion:trash-outline" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<template v-else-if="plugin.installed && source !== 'local'">
|
||||
<div v-if="plugin.localPluginId" class="plugin-card__action-zone" :class="{ 'is-loading': isActionLoading('uninstall') }">
|
||||
<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)">
|
||||
<a-button class="plugin-card__action-button" size="small" danger ghost :loading="isActionLoading('uninstall')" @click.stop="uninstallPlugin">
|
||||
{{ 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)">
|
||||
<div v-else-if="source !== 'local'" class="plugin-card__action-zone plugin-card__install-zone" :class="{ 'is-loading': isActionLoading('install') }">
|
||||
<a-button class="plugin-card__action-button" size="small" type="primary" :loading="isActionLoading('install')" @click.stop="installPlugin">
|
||||
{{ t("certd.onlinePluginInstall") }}
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -85,7 +96,7 @@
|
||||
<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)">
|
||||
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
|
||||
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
@@ -95,7 +106,7 @@
|
||||
</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)">
|
||||
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.localDisabled ? 'default' : 'green'" @click.stop="togglePlugin">
|
||||
{{ plugin.localDisabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
@@ -108,23 +119,32 @@
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip :title="versionTitle">
|
||||
<a-tooltip v-if="!isBuiltInPlugin" :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>
|
||||
<a-tooltip v-if="source !== 'local' && authorName" :title="plugin.selfAuthored ? '这是我自己提交的插件' : `${t('certd.author')}:${authorName}`">
|
||||
<span class="plugin-card__author" :class="{ 'is-self-authored': plugin.selfAuthored }">
|
||||
<fs-icon :icon="plugin.selfAuthored ? 'ion:person-circle' : 'ion:person-circle-outline'" />
|
||||
<span>{{ authorName }}</span>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import * as api from "../api";
|
||||
import { usePluginConfig } from "../use-config";
|
||||
import { usePluginPublish } from "../use-publish";
|
||||
import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "PluginItemCard",
|
||||
@@ -137,29 +157,61 @@ const props = withDefaults(
|
||||
current?: boolean;
|
||||
simple?: boolean;
|
||||
showConfig?: boolean;
|
||||
installLoading?: boolean;
|
||||
uninstallLoading?: boolean;
|
||||
toggleLoading?: boolean;
|
||||
editable?: boolean;
|
||||
copyHandler?: (plugin: any) => void | Promise<void>;
|
||||
}>(),
|
||||
{
|
||||
source: "market",
|
||||
copyHandler: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
(e: "changed", payload: { plugin: any; action: PluginCardAction }): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const pluginStore = usePluginStore();
|
||||
const { openConfigDialog } = usePluginConfig();
|
||||
const { isPublishingPlugin, publishLocalPlugin } = usePluginPublish();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
const actionLoading = ref<PluginCardAction | "">("");
|
||||
const editDialogBodyRef = ref();
|
||||
|
||||
type PluginCardAction = "edit" | "copy" | "export" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
|
||||
|
||||
const editPluginId = computed(() => {
|
||||
return props.plugin.localPluginId || props.plugin.id;
|
||||
});
|
||||
|
||||
const canEditPlugin = computed(() => {
|
||||
if (props.editable != null) {
|
||||
return props.editable;
|
||||
}
|
||||
return props.plugin.type === "custom";
|
||||
});
|
||||
|
||||
const isOwnImportedPlugin = computed(() => {
|
||||
return props.source === "local" && canEditPlugin.value;
|
||||
});
|
||||
|
||||
const isAvailableLocally = computed(() => {
|
||||
return props.source === "local" || !!props.plugin.installed;
|
||||
});
|
||||
|
||||
const canCopyPlugin = computed(() => {
|
||||
return isOwnImportedPlugin.value || (props.source === "market" && !!props.plugin.selfAuthored && isAvailableLocally.value);
|
||||
});
|
||||
|
||||
const showEditButton = computed(() => {
|
||||
return canEditPlugin.value && isAvailableLocally.value;
|
||||
});
|
||||
|
||||
const showConfigButton = computed(() => {
|
||||
return !!props.showConfig && isAvailableLocally.value;
|
||||
});
|
||||
|
||||
const isInstalled = computed(() => {
|
||||
return props.source !== "local" && props.plugin.installed;
|
||||
@@ -170,7 +222,19 @@ const isDisabled = computed(() => {
|
||||
});
|
||||
|
||||
const canRemoveLocal = computed(() => {
|
||||
return props.plugin.type === "custom" || props.plugin.type === "store";
|
||||
return isOwnImportedPlugin.value;
|
||||
});
|
||||
|
||||
const isBuiltInPlugin = computed(() => {
|
||||
return props.source === "local" && props.plugin.type === "builtIn";
|
||||
});
|
||||
|
||||
const canPublishPlugin = computed(() => {
|
||||
return canEditPlugin.value && isAvailableLocally.value && (!props.plugin.status || !!props.plugin.selfAuthored);
|
||||
});
|
||||
|
||||
const showToolMenu = computed(() => {
|
||||
return !isBuiltInPlugin.value && (isOwnImportedPlugin.value || canPublishPlugin.value);
|
||||
});
|
||||
|
||||
const fullName = computed(() => {
|
||||
@@ -201,6 +265,9 @@ const pluginTypeLabel = computed(() => {
|
||||
});
|
||||
|
||||
const sourceLabel = computed(() => {
|
||||
if (props.source === "local" && props.plugin.type !== "builtIn") {
|
||||
return t("certd.localPlugin");
|
||||
}
|
||||
const labelMap: Record<string, string> = {
|
||||
builtIn: t("certd.builtIn"),
|
||||
custom: t("certd.custom"),
|
||||
@@ -237,6 +304,201 @@ const toggleTitle = computed(() => {
|
||||
return disabled ? t("certd.onlinePluginClickToEnable") : t("certd.onlinePluginClickToDisable");
|
||||
});
|
||||
|
||||
function isActionLoading(action: PluginCardAction) {
|
||||
return actionLoading.value === action;
|
||||
}
|
||||
|
||||
function handleCardClick() {
|
||||
if (props.source === "local") {
|
||||
if (showEditButton.value) {
|
||||
void editPlugin();
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit("click", props.plugin);
|
||||
}
|
||||
|
||||
function handleCardDoubleClick() {
|
||||
if (props.source !== "local") {
|
||||
emit("dblclick", props.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
function emitChanged(action: PluginCardAction) {
|
||||
emit("changed", {
|
||||
plugin: props.plugin,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
async function runAction(action: PluginCardAction, callback: () => Promise<void>, options?: { emitChanged?: boolean }) {
|
||||
if (actionLoading.value) {
|
||||
return;
|
||||
}
|
||||
actionLoading.value = action;
|
||||
try {
|
||||
await callback();
|
||||
if (options?.emitChanged !== false) {
|
||||
emitChanged(action);
|
||||
}
|
||||
} finally {
|
||||
actionLoading.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function editPlugin() {
|
||||
await openFormDialog({
|
||||
title: `编辑插件 ${props.plugin.title || props.plugin.name}`,
|
||||
columns: {},
|
||||
noneForm: true,
|
||||
wrapper: {
|
||||
width: 1480,
|
||||
destroyOnClose: true,
|
||||
maskClosable: false,
|
||||
okText: "保存",
|
||||
cancelText: "关闭",
|
||||
class: "plugin-edit-dialog",
|
||||
},
|
||||
body: () =>
|
||||
h(PluginEditDialogBody, {
|
||||
ref: editDialogBodyRef,
|
||||
pluginId: editPluginId.value,
|
||||
}),
|
||||
async onSubmit() {
|
||||
await editDialogBodyRef.value?.save?.();
|
||||
emitChanged("edit");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function copyPlugin() {
|
||||
if (!props.copyHandler) {
|
||||
return;
|
||||
}
|
||||
await runAction(
|
||||
"copy",
|
||||
async () => {
|
||||
await props.copyHandler?.(props.plugin);
|
||||
},
|
||||
{ emitChanged: true }
|
||||
);
|
||||
}
|
||||
|
||||
async function exportPlugin() {
|
||||
await runAction(
|
||||
"export",
|
||||
async () => {
|
||||
const content = await api.ExportPlugin(props.plugin.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 = `${props.plugin.name}.yaml`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
{ emitChanged: false }
|
||||
);
|
||||
}
|
||||
|
||||
async function publishPlugin() {
|
||||
await publishLocalPlugin(
|
||||
{ ...props.plugin, id: editPluginId.value },
|
||||
{
|
||||
async afterPublish() {
|
||||
emitChanged("publish");
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function openConfig() {
|
||||
await openConfigDialog({
|
||||
row: props.plugin,
|
||||
onSuccess: async () => {
|
||||
emitChanged("config");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function installPlugin() {
|
||||
const fullName = fullNameValue();
|
||||
if (!fullName) {
|
||||
return;
|
||||
}
|
||||
await runAction("install", async () => {
|
||||
await api.OnlinePluginInstall({
|
||||
fullName,
|
||||
version: props.plugin.latest,
|
||||
});
|
||||
message.success(t("certd.onlinePluginInstallSuccess"));
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallPlugin() {
|
||||
if (!props.plugin.localPluginId) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: t("certd.onlinePluginDeleteConfirm", { name: fullNameValue() || props.plugin.title || props.plugin.name }),
|
||||
async onOk() {
|
||||
await runAction("uninstall", async () => {
|
||||
await api.DelObj(props.plugin.localPluginId);
|
||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function removePlugin() {
|
||||
if (!props.plugin.id) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: "确定要删除吗?如果该插件已被使用,删除可能会导致流水线执行失败!",
|
||||
async onOk() {
|
||||
await runAction("remove", async () => {
|
||||
await api.DelObj(props.plugin.id);
|
||||
message.success(t("certd.onlinePluginUninstallSuccess"));
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function togglePlugin() {
|
||||
const id = props.source === "local" ? props.plugin.id : props.plugin.localPluginId;
|
||||
const canToggleBuiltInPlugin = props.source === "local" && props.plugin.type === "builtIn" && !!props.plugin.name;
|
||||
if (!id && !canToggleBuiltInPlugin) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${isDisabled.value ? t("certd.enable") : t("certd.disable")}?`,
|
||||
maskClosable: true,
|
||||
async onOk() {
|
||||
await runAction("toggle", async () => {
|
||||
await api.SetDisabled({
|
||||
id: id || undefined,
|
||||
name: props.plugin.name,
|
||||
type: props.source === "local" ? props.plugin.type : "store",
|
||||
disabled: !isDisabled.value,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
message.success(t("certd.operationSuccess"));
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function fullNameValue() {
|
||||
return fullName.value;
|
||||
}
|
||||
|
||||
function formatDownloadCount(count: number) {
|
||||
if (count >= 10000) {
|
||||
return `${(count / 10000).toFixed(1).replace(/\.0$/, "")}w`;
|
||||
@@ -248,7 +510,15 @@ function handleVersionClick() {
|
||||
if (props.source === "local" || !props.plugin.upgradeAvailable) {
|
||||
return;
|
||||
}
|
||||
emit("install", props.plugin);
|
||||
Modal.confirm({
|
||||
title: "升级插件",
|
||||
content: `确认将 ${props.plugin.title || props.plugin.name} 升级到 v${props.plugin.latest} 吗?`,
|
||||
okText: "确认升级",
|
||||
cancelText: "取消",
|
||||
async onOk() {
|
||||
await installPlugin();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -304,7 +574,7 @@ function handleVersionClick() {
|
||||
|
||||
.plugin-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -313,17 +583,17 @@ function handleVersionClick() {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plugin-card__icon {
|
||||
flex: none;
|
||||
margin-top: 1px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.plugin-card__main .plugin-icon {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
@@ -334,14 +604,19 @@ function handleVersionClick() {
|
||||
}
|
||||
|
||||
.plugin-card__title-wrap {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.plugin-card__title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
@@ -378,6 +653,14 @@ function handleVersionClick() {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.is-self-authored {
|
||||
color: #1677ff;
|
||||
|
||||
.fs-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__actions {
|
||||
@@ -392,14 +675,9 @@ function handleVersionClick() {
|
||||
|
||||
.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 {
|
||||
@@ -417,7 +695,14 @@ function handleVersionClick() {
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
.fs-icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -482,7 +767,14 @@ function handleVersionClick() {
|
||||
}
|
||||
|
||||
.plugin-card__local-action-zone {
|
||||
width: 76px;
|
||||
width: 28px;
|
||||
|
||||
.plugin-card__action-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__install-zone {
|
||||
@@ -496,12 +788,6 @@ function handleVersionClick() {
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.plugin-card__tools {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__desc {
|
||||
display: -webkit-box;
|
||||
min-height: 40px;
|
||||
@@ -570,6 +856,13 @@ function handleVersionClick() {
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__ai-check-icon {
|
||||
flex: none;
|
||||
color: #52c41a;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.is-simple {
|
||||
.plugin-card__desc {
|
||||
min-height: 40px;
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
<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>
|
||||
@@ -114,7 +114,12 @@ const { openConfigDialog } = usePluginConfig();
|
||||
|
||||
async function doPluginConfig() {
|
||||
const certApplyInfo = await GetPluginByName("CertApply");
|
||||
await openConfigDialog({ row: certApplyInfo, crudExpose: null });
|
||||
await openConfigDialog({
|
||||
row: certApplyInfo,
|
||||
onSuccess: async () => {
|
||||
await loadForm();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
|
||||
@@ -49,6 +49,9 @@ import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import Rollbackable from "./rollbackable.vue";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import * as api from "./api";
|
||||
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
|
||||
import yaml from "js-yaml";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const pluginStore = usePluginStore();
|
||||
@@ -264,12 +267,35 @@ function clearFormValue(key: string) {
|
||||
console.log(key, configForm);
|
||||
}
|
||||
|
||||
function getPluginOriginName() {
|
||||
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;
|
||||
}
|
||||
|
||||
async function loadPluginSetting() {
|
||||
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
|
||||
for (const key in currentPlugin.value.input) {
|
||||
const originName = getPluginOriginName();
|
||||
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName);
|
||||
if (!currentPlugin.value?.input && originName !== props.plugin.name) {
|
||||
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
|
||||
}
|
||||
if (!currentPlugin.value?.input && props.plugin.id) {
|
||||
const plugin = await api.GetObj(props.plugin.localPluginId || props.plugin.id);
|
||||
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
|
||||
currentPlugin.value = {
|
||||
...plugin,
|
||||
...(metadata || {}),
|
||||
};
|
||||
}
|
||||
const input = currentPlugin.value?.input || {};
|
||||
for (const key in input) {
|
||||
configForm[key] = {};
|
||||
}
|
||||
const setting = props.plugin.sysSetting;
|
||||
const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting;
|
||||
if (setting) {
|
||||
const settingJson = JSON.parse(setting);
|
||||
merge(configForm, settingJson.metadata?.input || {});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as api from "./api";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { Ref, ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
import { Modal, message } from "ant-design-vue";
|
||||
//@ts-ignore
|
||||
@@ -11,9 +10,9 @@ import KvInput from "/@/components/plugins/common/kv-input.vue";
|
||||
import { usePluginConfig } from "./use-config";
|
||||
import { useSettingStore } from "/src/store/settings/index";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import PluginAuthorField from "./components/plugin-author-field.vue";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
let lastType = "";
|
||||
@@ -50,6 +49,70 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const pluginStore = usePluginStore();
|
||||
const syncLoading = ref(false);
|
||||
const lastSyncTime = ref(0);
|
||||
const autoSyncInterval = 30 * 24 * 60 * 60 * 1000;
|
||||
const syncButtonTitle = computed(() => {
|
||||
if (!lastSyncTime.value) {
|
||||
return t("certd.onlinePluginNotSynced");
|
||||
}
|
||||
return t("certd.onlinePluginLastSyncTime", {
|
||||
time: formatSyncTime(lastSyncTime.value),
|
||||
});
|
||||
});
|
||||
|
||||
function formatSyncTime(time: number) {
|
||||
return new Date(time).toLocaleString();
|
||||
}
|
||||
|
||||
function needAutoSync(time: number) {
|
||||
if (!time) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() - time > autoSyncInterval;
|
||||
}
|
||||
|
||||
function canEditStorePlugin(row: any) {
|
||||
if (row.type !== "store") {
|
||||
return false;
|
||||
}
|
||||
if (typeof row.localEditable === "boolean") {
|
||||
return row.localEditable;
|
||||
}
|
||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
||||
return !row.developerId || (!!bindUserId && Number(row.developerId) === bindUserId);
|
||||
}
|
||||
|
||||
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
|
||||
if (syncLoading.value) {
|
||||
return;
|
||||
}
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
await api.OnlinePluginSync();
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || Date.now();
|
||||
await pluginStore.reload();
|
||||
crudExpose.doRefresh();
|
||||
if (options?.showSuccess !== false) {
|
||||
message.success(t("certd.onlinePluginSyncSuccess"));
|
||||
}
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOnlinePluginSetting() {
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || 0;
|
||||
if (needAutoSync(lastSyncTime.value)) {
|
||||
await syncOnlinePlugins({ showSuccess: false });
|
||||
}
|
||||
}
|
||||
|
||||
loadOnlinePluginSetting().catch(e => {
|
||||
console.warn("load online plugin setting failed", e);
|
||||
});
|
||||
return {
|
||||
crudOptions: {
|
||||
settings: {
|
||||
@@ -89,6 +152,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
await openImportDialog({ crudExpose });
|
||||
},
|
||||
},
|
||||
syncOnline: {
|
||||
show: true,
|
||||
icon: "ion:sync-outline",
|
||||
type: "primary",
|
||||
text: t("certd.onlinePluginSync"),
|
||||
tooltip: { title: syncButtonTitle },
|
||||
loading: syncLoading,
|
||||
async click() {
|
||||
await syncOnlinePlugins();
|
||||
},
|
||||
},
|
||||
clearRuntimeDeps: {
|
||||
show: true,
|
||||
icon: "ion:trash-outline",
|
||||
@@ -126,13 +200,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
buttons: {
|
||||
edit: {
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
return canEditStorePlugin(row);
|
||||
}),
|
||||
},
|
||||
copy: {
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
return canEditStorePlugin(row);
|
||||
}),
|
||||
async click({ row }) {
|
||||
const copyRow = { ...row };
|
||||
delete copyRow.fullName;
|
||||
delete copyRow.id;
|
||||
crudExpose.openCopy({
|
||||
row: copyRow,
|
||||
});
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
order: 999,
|
||||
@@ -146,7 +228,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("certd.export"),
|
||||
type: "link",
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
return canEditStorePlugin(row);
|
||||
}),
|
||||
async click({ row }) {
|
||||
const content = await api.ExportPlugin(row.id);
|
||||
@@ -172,7 +254,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
async click({ row }) {
|
||||
await openConfigDialog({
|
||||
row,
|
||||
crudExpose,
|
||||
onSuccess: async () => {
|
||||
crudExpose.doRefresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -187,16 +271,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
},
|
||||
form: {
|
||||
onSuccess(opts: any) {
|
||||
if (opts.res?.id) {
|
||||
router.push({
|
||||
name: "SysPluginEdit",
|
||||
query: {
|
||||
id: opts.res.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
crudExpose.doRefresh();
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
@@ -287,19 +362,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("certd.author"),
|
||||
type: "text",
|
||||
search: {
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
form: {
|
||||
show: true,
|
||||
helper: t("certd.authorHelper"),
|
||||
rules: [
|
||||
{ required: true },
|
||||
{
|
||||
type: "pattern",
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||
message: t("certd.authorRuleMsg"),
|
||||
},
|
||||
],
|
||||
component: {
|
||||
name: PluginAuthorField,
|
||||
vModel: "modelValue",
|
||||
},
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
@@ -316,9 +390,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
column: {
|
||||
width: 300,
|
||||
cellRender({ row }) {
|
||||
if (row.type === "custom") {
|
||||
return <router-link to={`/sys/plugin/edit?id=${row.id}`}>{row.title}</router-link>;
|
||||
}
|
||||
return <div>{row.title}</div>;
|
||||
},
|
||||
},
|
||||
@@ -339,7 +410,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: true,
|
||||
},
|
||||
form: {
|
||||
value: "custom",
|
||||
value: "store",
|
||||
component: {
|
||||
disabled: true,
|
||||
},
|
||||
@@ -347,8 +418,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: t("certd.builtIn"), value: "builtIn" },
|
||||
{ label: t("certd.custom"), value: "custom" },
|
||||
{ label: t("certd.installedStorePlugin"), value: "store" },
|
||||
{ label: t("certd.store"), value: "store" },
|
||||
],
|
||||
}),
|
||||
column: {
|
||||
@@ -473,6 +543,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${!value ? t("certd.disable") : t("certd.enable")}?`,
|
||||
maskClosable: true,
|
||||
onOk: async () => {
|
||||
await api.SetDisabled({
|
||||
id: row.id,
|
||||
@@ -480,7 +551,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: row.type,
|
||||
disabled: !value,
|
||||
});
|
||||
await crudExpose.doRefresh();
|
||||
crudExpose.doRefresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -491,6 +562,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
group: {
|
||||
title: t("certd.pluginGroup"),
|
||||
type: "dict-select",
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
dict: dict({
|
||||
url: "/pi/plugin/groupsList",
|
||||
label: "title",
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
<template>
|
||||
<fs-page class="page-plugin-edit">
|
||||
<template #header>
|
||||
<div class="title">
|
||||
插件编辑
|
||||
<span class="sub">
|
||||
<span class="name flex-inline"> 插件名称:<fs-copyable :model-value="pluginName"></fs-copyable> </span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="more">
|
||||
<a-button class="mr-1" type="primary" :loading="saveLoading" @click="doSave">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pi-plugin-editor">
|
||||
<div class="base">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="base" tab="插件信息"> </a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="base-body">
|
||||
<fs-form ref="baseFormRef" v-bind="formOptionsRef"></fs-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="editor" tab="元数据"> </a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="metadata-body">
|
||||
<code-editor :id="`metadata_${idRef}`" v-model:model-value="plugin.metadata" language="yaml" @save="doSave"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="script">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="script" tab="脚本"> </a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="script-body">
|
||||
<code-editor :id="`content_${idRef}`" v-model:model-value="plugin.content" language="javascript" @save="doSave"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fs-page>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, Ref, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import * as api from "./api";
|
||||
import { notification } from "ant-design-vue";
|
||||
import createCrudOptions from "./crud";
|
||||
import { useColumns } from "@fast-crud/fast-crud";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
//@ts-ignore
|
||||
import yaml from "js-yaml";
|
||||
|
||||
defineOptions({
|
||||
name: "SysPluginEdit",
|
||||
});
|
||||
const route = useRoute();
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const plugin = ref<any>({});
|
||||
const formOptionsRef: Ref = ref();
|
||||
const baseFormRef: Ref = ref({});
|
||||
function initFormOptions() {
|
||||
const formCrudOptions = createCrudOptions({
|
||||
//@ts-ignore
|
||||
crudExpose: {},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const { buildFormOptions } = useColumns();
|
||||
|
||||
const formOptions = buildFormOptions(formCrudOptions.crudOptions, {});
|
||||
|
||||
formOptions.mode = "edit";
|
||||
formOptions.col = {
|
||||
span: 24,
|
||||
};
|
||||
formOptions.labelCol = {
|
||||
style: {
|
||||
width: "100px",
|
||||
},
|
||||
};
|
||||
formOptionsRef.value = formOptions;
|
||||
}
|
||||
initFormOptions();
|
||||
|
||||
const idRef = ref(route.query.id);
|
||||
async function getPlugin() {
|
||||
const id = route.query.id;
|
||||
const pluginObj = await api.GetObj(id);
|
||||
plugin.value = pluginObj;
|
||||
|
||||
const baseFrom = {
|
||||
...pluginObj,
|
||||
};
|
||||
if (baseFrom.extra) {
|
||||
baseFrom.extra = yaml.load(baseFrom.extra);
|
||||
}
|
||||
delete baseFrom.metadata;
|
||||
delete baseFrom.content;
|
||||
baseFormRef.value.setFormData(baseFrom);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getPlugin();
|
||||
});
|
||||
|
||||
const pluginName = computed(() => {
|
||||
if (!plugin.value) {
|
||||
return "";
|
||||
}
|
||||
if (plugin.value.author) {
|
||||
return `${plugin.value.author}/${plugin.value.name}`;
|
||||
}
|
||||
return plugin.value.name;
|
||||
});
|
||||
|
||||
provide("get:plugin", () => {
|
||||
return plugin;
|
||||
});
|
||||
|
||||
function validate() {
|
||||
try {
|
||||
yaml.load(plugin.value.metadata);
|
||||
} catch (e: any) {
|
||||
const message = `元数据校验失败:${e.message}`;
|
||||
notification.error({
|
||||
message,
|
||||
});
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const saveLoading = ref(false);
|
||||
async function doSave() {
|
||||
validate();
|
||||
saveLoading.value = true;
|
||||
const baseForm = baseFormRef.value.getFormData();
|
||||
const form = {
|
||||
...plugin.value,
|
||||
...baseForm,
|
||||
};
|
||||
if (form.extra) {
|
||||
form.extra = yaml.dump(form.extra);
|
||||
}
|
||||
try {
|
||||
await api.UpdateObj(form);
|
||||
notification.success({
|
||||
message: "保存成功",
|
||||
});
|
||||
pluginStore.clear();
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doTest() {
|
||||
await doSave();
|
||||
const result = await api.DoTest({
|
||||
id: plugin.value.id,
|
||||
input: {},
|
||||
});
|
||||
notification.success({
|
||||
message: "测试已开始",
|
||||
description: result,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.page-plugin-edit {
|
||||
.pi-plugin-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
.fs-editor-code {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 400px;
|
||||
max-width: 30%;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.base-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata {
|
||||
width: 600px;
|
||||
max-width: 30%;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.metadata-body {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metadata-editor {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.metadata-source {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.script {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.script-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,134 +6,108 @@
|
||||
<span class="sub">{{ t("certd.pluginBetaWarning") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<a-tabs v-model:active-key="activeTab" class="plugin-page-tabs">
|
||||
<a-tab-pane key="local">
|
||||
<template #tab>
|
||||
<span class="plugin-page-tab-label">
|
||||
<fs-icon icon="ion:cube-outline" />
|
||||
<span>{{ t("certd.localPlugin") }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" class="plugin-card-crud" v-bind="crudBinding">
|
||||
<a-empty v-if="pluginList.length === 0" class="plugin-card-empty" />
|
||||
<div v-else class="plugin-card-grid">
|
||||
<PluginItemCard
|
||||
v-for="(item, index) of pluginList"
|
||||
:key="item.id || item.name"
|
||||
source="local"
|
||||
:plugin="item"
|
||||
:show-config="settingStore.isComm"
|
||||
@edit="openEditPage"
|
||||
@copy="openCopy({ index, row: item })"
|
||||
@export-plugin="exportPlugin"
|
||||
@config="openConfig"
|
||||
@remove="removePlugin({ index, row: item })"
|
||||
@toggle-disabled="toggleDisabled"
|
||||
/>
|
||||
</div>
|
||||
</fs-crud>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="market">
|
||||
<template #tab>
|
||||
<span class="plugin-page-tab-label">
|
||||
<fs-icon icon="ion:storefront-outline" />
|
||||
<span>{{ t("certd.pluginMarket") }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<PluginMarketPanel @changed="handleMarketPluginChanged" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<fs-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 of pluginList"
|
||||
:key="item.id || item.name"
|
||||
:source="getPluginCardSource(item)"
|
||||
:plugin="item"
|
||||
:show-config="settingStore.isComm"
|
||||
:editable="canEditPlugin(item)"
|
||||
:copy-handler="copyPlugin"
|
||||
@changed="handlePluginChanged"
|
||||
@click="openPluginDetail"
|
||||
/>
|
||||
</div>
|
||||
</fs-crud>
|
||||
<OnlinePluginDetailModal v-model:open="detailVisible" :plugin="detailPlugin" @installed="handleDetailInstalled" />
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import * as api from "./api";
|
||||
import PluginMarketPanel from "./components/plugin-market-panel.vue";
|
||||
import PluginItemCard from "./components/plugin-item-card.vue";
|
||||
import OnlinePluginDetailModal from "./components/online-plugin-detail-modal.vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { useMounted } from "/@/use/use-mounted";
|
||||
import { computed, ref } from "vue";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { usePluginConfig } from "./use-config";
|
||||
import { useSettingStore } from "/src/store/settings";
|
||||
import { useRouter } from "vue-router";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
|
||||
const { t } = useI18n();
|
||||
const pluginStore = usePluginStore();
|
||||
const settingStore = useSettingStore();
|
||||
const { openConfigDialog } = usePluginConfig();
|
||||
const router = useRouter();
|
||||
const pluginStore = usePluginStore();
|
||||
|
||||
defineOptions({
|
||||
name: "SysPlugin",
|
||||
});
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const activeTab = ref("local");
|
||||
const detailVisible = ref(false);
|
||||
const detailPlugin = ref<any>();
|
||||
|
||||
const pluginList = computed(() => {
|
||||
return crudBinding.value?.data || [];
|
||||
});
|
||||
|
||||
function openEditPage(row: any) {
|
||||
router.push(`/sys/plugin/edit?id=${row.id}`);
|
||||
function getPluginCardSource(row: any) {
|
||||
return row.type === "store" && (row.appId || row.developerId) ? "market" : "local";
|
||||
}
|
||||
|
||||
async function openCopy(opts: any) {
|
||||
await crudExpose.openCopy({
|
||||
row: {
|
||||
...opts.row,
|
||||
},
|
||||
index: opts.index,
|
||||
});
|
||||
function canEditPlugin(row: any) {
|
||||
if (row.type === "custom") {
|
||||
return true;
|
||||
}
|
||||
if (row.type !== "store") {
|
||||
return false;
|
||||
}
|
||||
if (typeof row.localEditable === "boolean") {
|
||||
return row.localEditable;
|
||||
}
|
||||
const bindUserId = Number(settingStore.installInfo?.bindUserId || 0);
|
||||
return !row.developerId || (!!bindUserId && Number(row.developerId) === bindUserId);
|
||||
}
|
||||
|
||||
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) {
|
||||
function openPluginDetail(row: any) {
|
||||
if (row.type !== "store" || !(row.fullName || (row.author && row.name))) {
|
||||
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);
|
||||
detailPlugin.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
async function toggleDisabled(row: any) {
|
||||
Modal.confirm({
|
||||
title: t("certd.confirm"),
|
||||
content: `${t("certd.confirmToggle")} ${!row.disabled ? t("certd.disable") : t("certd.enable")}?`,
|
||||
async onOk() {
|
||||
await api.SetDisabled({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
disabled: !row.disabled,
|
||||
});
|
||||
await pluginStore.reload();
|
||||
crudExpose.doRefresh();
|
||||
message.success(t("certd.operationSuccess"));
|
||||
async function handleDetailInstalled() {
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
|
||||
async function copyPlugin(row: any) {
|
||||
const copyRow = { ...row };
|
||||
delete copyRow.fullName;
|
||||
delete copyRow.id;
|
||||
delete copyRow.developerId;
|
||||
delete copyRow.appId;
|
||||
delete copyRow.latest;
|
||||
delete copyRow.status;
|
||||
delete copyRow.downloadCount;
|
||||
delete copyRow.score;
|
||||
await crudExpose.openCopy(
|
||||
{
|
||||
row: copyRow,
|
||||
},
|
||||
});
|
||||
{
|
||||
async onSuccess() {
|
||||
crudExpose.doRefresh();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleMarketPluginChanged() {
|
||||
async function handlePluginChanged(payload: { action: string }) {
|
||||
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove" || payload.action === "copy") {
|
||||
await pluginStore.reload();
|
||||
}
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
|
||||
@@ -158,55 +132,6 @@ useMounted(async () => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function usePluginConfig() {
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
// @ts-ignore
|
||||
async function openConfigDialog({ row, crudExpose }) {
|
||||
async function openConfigDialog({ row, onSuccess }) {
|
||||
const configEditorRef = ref();
|
||||
function createCrudOptions() {
|
||||
return {
|
||||
@@ -33,10 +33,10 @@ export function usePluginConfig() {
|
||||
},
|
||||
},
|
||||
},
|
||||
afterSubmit() {
|
||||
async afterSubmit() {
|
||||
notification.success({ message: t("certd.operationSuccess") });
|
||||
if (crudExpose) {
|
||||
crudExpose.doRefresh();
|
||||
if (onSuccess) {
|
||||
await onSuccess();
|
||||
}
|
||||
},
|
||||
async doSubmit({}: any) {
|
||||
|
||||
@@ -66,6 +66,7 @@ export function usePluginImport() {
|
||||
async doSubmit({ form }: any) {
|
||||
return await api.ImportPlugin({
|
||||
...form,
|
||||
type: "store",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
.sys-plugin-publish {
|
||||
margin: 2px;
|
||||
|
||||
&__hero {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid #e4ecf7;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #f7fbff 0%, #eef5ff 55%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
display: flex;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(64, 120, 192, 0.16);
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 22px rgba(31, 77, 132, 0.08);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #2f6fbb;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
&__hero-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title-line,
|
||||
&__version-line,
|
||||
&__author-register,
|
||||
&__review-icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__title-line {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #172033;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 4px;
|
||||
color: #526172;
|
||||
line-height: 22px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__sections,
|
||||
&__latest-version {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__sections {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__latest-version {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__meta-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__version {
|
||||
color: #1f2937;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__review-icons {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__review-icon {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&.is-ai {
|
||||
&.is-passed {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
&.is-rejected {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-admin {
|
||||
color: #1677ff;
|
||||
|
||||
&.is-rejected {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__rejected-reasons {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
border-left: 3px solid #ff7875;
|
||||
border-radius: 4px;
|
||||
background: #fff2f0;
|
||||
color: #a61d24;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__prompt {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
import { reactive, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import * as api from "./api";
|
||||
import "./use-publish.less";
|
||||
|
||||
type PublishManagerResult =
|
||||
| {
|
||||
action: "publish";
|
||||
version: string;
|
||||
}
|
||||
| {
|
||||
action: "registered";
|
||||
}
|
||||
| {
|
||||
action: "cancel";
|
||||
};
|
||||
|
||||
export function usePluginPublish() {
|
||||
const { t } = useI18n();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
const pluginStore = usePluginStore();
|
||||
const publishingPluginId = ref<number | string>("");
|
||||
|
||||
const authorNamePattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
||||
|
||||
function getPluginTypeLabel(pluginType: string) {
|
||||
const labelMap: Record<string, string> = {
|
||||
access: t("certd.auth"),
|
||||
dnsProvider: t("certd.dns"),
|
||||
deploy: t("certd.deployPlugin"),
|
||||
};
|
||||
return labelMap[pluginType] || pluginType || "-";
|
||||
}
|
||||
|
||||
function isPublishingPlugin(row: any) {
|
||||
return !!row?.id && publishingPluginId.value === row.id;
|
||||
}
|
||||
|
||||
function registerPluginAuthor() {
|
||||
return new Promise<api.OnlinePluginAuthorBean | undefined>(resolve => {
|
||||
let resolved = false;
|
||||
const resolveAuthor = (author?: api.OnlinePluginAuthorBean) => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
resolved = true;
|
||||
resolve(author);
|
||||
};
|
||||
|
||||
void openFormDialog({
|
||||
title: t("certd.onlinePluginAuthorRegister"),
|
||||
wrapper: {
|
||||
width: 560,
|
||||
onClosed() {
|
||||
resolveAuthor();
|
||||
},
|
||||
},
|
||||
initialForm: {
|
||||
name: "",
|
||||
nameConfirm: "",
|
||||
desc: "",
|
||||
},
|
||||
async onSubmit(form: any) {
|
||||
if (form.name?.trim() !== form.nameConfirm?.trim()) {
|
||||
throw new Error("两次输入的作者名称不一致");
|
||||
}
|
||||
const author = await api.OnlinePluginAuthorAdd({
|
||||
name: form.name,
|
||||
displayName: form.displayName,
|
||||
desc: form.desc,
|
||||
});
|
||||
resolveAuthor(author);
|
||||
},
|
||||
columns: {
|
||||
name: {
|
||||
title: t("certd.onlinePluginAuthorName"),
|
||||
type: "text",
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
helper: t("certd.onlinePluginAuthorNameHelper"),
|
||||
rules: [
|
||||
{ required: true, message: t("certd.onlinePluginAuthorNameRequired") },
|
||||
{
|
||||
pattern: authorNamePattern,
|
||||
message: t("certd.onlinePluginAuthorNameRuleMsg"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
nameConfirm: {
|
||||
title: "确认作者名称",
|
||||
type: "text",
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
helper: "请再次输入相同名称。作者名称注册后不允许修改。",
|
||||
rules: [{ required: true, message: "请再次输入作者名称" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string) {
|
||||
const labelMap: Record<string, string> = {
|
||||
draft: t("certd.onlinePluginStatusDraft"),
|
||||
reviewing: t("certd.onlinePluginStatusReviewing"),
|
||||
published: t("certd.onlinePluginStatusPublished"),
|
||||
rejected: t("certd.onlinePluginStatusRejected"),
|
||||
offline: t("certd.onlinePluginStatusOffline"),
|
||||
};
|
||||
return labelMap[status || ""] || status || "-";
|
||||
}
|
||||
|
||||
function normalizeVersion(version?: string) {
|
||||
return (version || "").trim();
|
||||
}
|
||||
|
||||
function isReviewingVersion(version: api.OnlinePluginVersionBean) {
|
||||
return version.status === "reviewing" || version.reviewStatus === "ai_pending" || version.reviewStatus === "pending";
|
||||
}
|
||||
|
||||
function compareVersion(current: string, last: string) {
|
||||
const currentParts = normalizeVersion(current)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map(item => Number.parseInt(item, 10));
|
||||
const lastParts = normalizeVersion(last)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map(item => Number.parseInt(item, 10));
|
||||
const maxLength = Math.max(currentParts.length, lastParts.length);
|
||||
for (let index = 0; index < maxLength; index++) {
|
||||
const currentPart = currentParts[index] || 0;
|
||||
const lastPart = lastParts[index] || 0;
|
||||
if (currentPart > lastPart) {
|
||||
return 1;
|
||||
}
|
||||
if (currentPart < lastPart) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findLatestPublishedVersion(versions: api.OnlinePluginVersionBean[], latest?: string) {
|
||||
const publishedVersions = versions.filter(item => item.status === "published");
|
||||
const latestText = normalizeVersion(latest);
|
||||
if (latestText) {
|
||||
const matched = publishedVersions.find(item => normalizeVersion(item.version) === latestText);
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
return publishedVersions[0];
|
||||
}
|
||||
|
||||
function getDefaultPublishVersion(row: any, localPlugin: any, reviewingVersion?: api.OnlinePluginVersionBean) {
|
||||
const localVersion = normalizeVersion(row.version || localPlugin.version);
|
||||
if (reviewingVersion) {
|
||||
return localVersion || normalizeVersion(reviewingVersion.version);
|
||||
}
|
||||
return localVersion || "1.0.0";
|
||||
}
|
||||
|
||||
function getVersionError(version: string, baselineVersion?: api.OnlinePluginVersionBean) {
|
||||
const value = normalizeVersion(version);
|
||||
if (!value) {
|
||||
return t("certd.onlinePluginPublishVersionRequired");
|
||||
}
|
||||
if (!/^v?\d+(\.\d+)*$/i.test(value)) {
|
||||
return t("certd.onlinePluginVersionFormatError");
|
||||
}
|
||||
if (baselineVersion?.version && compareVersion(value, baselineVersion.version) <= 0) {
|
||||
return t("certd.onlinePluginVersionBaselineError", { version: baselineVersion.version });
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getStatusColor(status?: string) {
|
||||
const colorMap: Record<string, string> = {
|
||||
draft: "default",
|
||||
reviewing: "processing",
|
||||
published: "success",
|
||||
rejected: "error",
|
||||
offline: "warning",
|
||||
};
|
||||
return colorMap[status || ""] || "default";
|
||||
}
|
||||
|
||||
function getAiCheckIcon(version: api.OnlinePluginVersionBean) {
|
||||
if (version.aiCheckStatus === "rejected") {
|
||||
return "lucide:shield-x";
|
||||
}
|
||||
if (version.aiCheckStatus === "pending") {
|
||||
return "lucide:shield-alert";
|
||||
}
|
||||
return "lucide:shield-check";
|
||||
}
|
||||
|
||||
function getAiCheckTooltip(version: api.OnlinePluginVersionBean) {
|
||||
const result = `${version.aiCheckResult || ""}`.trim();
|
||||
if (version.aiCheckStatus === "rejected") {
|
||||
return result ? `AI 安全审查未通过:${result}` : "AI 安全审查未通过";
|
||||
}
|
||||
if (version.aiCheckStatus === "pending" || version.reviewStatus === "ai_pending") {
|
||||
return result ? `AI 安全审查中:${result}` : "AI 安全审查中";
|
||||
}
|
||||
return result ? `AI 安全审查已通过:${result}` : "AI 安全审查已通过";
|
||||
}
|
||||
|
||||
function isAdminRejected(version: api.OnlinePluginVersionBean) {
|
||||
return version.reviewStatus === "rejected" && !!`${version.reviewReason || ""}`.trim();
|
||||
}
|
||||
|
||||
function hasAdminReview(version: api.OnlinePluginVersionBean) {
|
||||
return version.reviewStatus === "passed" || isAdminRejected(version);
|
||||
}
|
||||
|
||||
function getAdminReviewTooltip(version: api.OnlinePluginVersionBean) {
|
||||
if (isAdminRejected(version)) {
|
||||
return `管理员审核已拒绝:${version.reviewReason}`;
|
||||
}
|
||||
return "管理员审核已通过";
|
||||
}
|
||||
|
||||
function renderReviewIndicators(version: api.OnlinePluginVersionBean) {
|
||||
const aiCheckStatus = version.aiCheckStatus || (version.reviewStatus === "ai_pending" ? "pending" : "");
|
||||
return (
|
||||
<span class="sys-plugin-publish__review-icons">
|
||||
{aiCheckStatus ? (
|
||||
<a-tooltip title={getAiCheckTooltip(version)}>
|
||||
<fs-icon class={["sys-plugin-publish__review-icon", "is-ai", `is-${aiCheckStatus}`]} icon={getAiCheckIcon({ ...version, aiCheckStatus })} />
|
||||
</a-tooltip>
|
||||
) : null}
|
||||
{hasAdminReview(version) ? (
|
||||
<a-tooltip title={getAdminReviewTooltip(version)}>
|
||||
<fs-icon class={["sys-plugin-publish__review-icon", "is-admin", { "is-rejected": isAdminRejected(version) }]} icon={isAdminRejected(version) ? "lucide:user-x" : "lucide:user-check"} />
|
||||
</a-tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRejectedReasons(version: api.OnlinePluginVersionBean) {
|
||||
const aiReason = version.aiCheckStatus === "rejected" ? `${version.aiCheckResult || ""}`.trim() : "";
|
||||
if (!aiReason) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div class="sys-plugin-publish__rejected-reasons">
|
||||
<div class="sys-plugin-publish__rejected-reason is-ai">AI 审查未通过:{aiReason}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function openPublishManager(row: any, info: Awaited<ReturnType<typeof api.OnlinePluginPublishInfo>>) {
|
||||
const author = info.author;
|
||||
const marketPlugin = info.marketPlugin;
|
||||
const versions = info.versions || [];
|
||||
const localPlugin = info.localPlugin || row;
|
||||
const reviewingVersion = versions.find(isReviewingVersion);
|
||||
const rejectedVersion = versions.find(item => item.status === "rejected" || item.reviewStatus === "rejected");
|
||||
const replaceableVersion = reviewingVersion || rejectedVersion;
|
||||
const latestPublished = findLatestPublishedVersion(versions, marketPlugin?.latest);
|
||||
const latestVersion = versions[0];
|
||||
const isFirstPublish = !marketPlugin && versions.length === 0;
|
||||
const publishMode = isFirstPublish ? "first" : replaceableVersion ? "cover" : "new";
|
||||
const defaultVersion = getDefaultPublishVersion(row, localPlugin, replaceableVersion);
|
||||
const versionBaseline = publishMode === "cover" ? latestPublished : latestVersion;
|
||||
const state = reactive({
|
||||
version: defaultVersion,
|
||||
error: getVersionError(defaultVersion, versionBaseline),
|
||||
});
|
||||
|
||||
let formWrapper: any;
|
||||
let resolved = false;
|
||||
let resolveResult: (result: PublishManagerResult) => void;
|
||||
|
||||
function getPublishModeTitle() {
|
||||
if (publishMode === "first") {
|
||||
return "首次发布";
|
||||
}
|
||||
if (publishMode === "cover") {
|
||||
return "覆盖待审核版本";
|
||||
}
|
||||
return "发布新版本";
|
||||
}
|
||||
|
||||
function getPublishModeTip() {
|
||||
if (publishMode === "first") {
|
||||
return "插件还没有提交到在线市场,确认后会创建插件并提交发布审核。";
|
||||
}
|
||||
if (publishMode === "cover") {
|
||||
if (rejectedVersion && rejectedVersion === replaceableVersion) {
|
||||
return `当前 v${rejectedVersion.version || "-"} 已被拒绝,本次提交可覆盖该版本并重新提交审核。`;
|
||||
}
|
||||
return `当前已有 v${reviewingVersion?.version || "-"} 正在审核中,本次提交会撤回并覆盖该审核版本。`;
|
||||
}
|
||||
return `当前最新版本为 v${latestVersion?.version || latestPublished?.version || marketPlugin?.latest || "-"},新提交的版本号必须更高。`;
|
||||
}
|
||||
|
||||
function renderMetaItem(label: string, value: any) {
|
||||
return (
|
||||
<div class="cd-meta-item">
|
||||
<span class="cd-meta-label">{label}</span>
|
||||
<span class="cd-meta-value">{value || "-"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAuthor() {
|
||||
if (info.authorRegistered) {
|
||||
return author?.displayName || author?.name || "-";
|
||||
}
|
||||
return (
|
||||
<span class="sys-plugin-publish__author-register">
|
||||
<span class="cd-text-danger">{t("certd.onlinePluginAuthorNotRegistered")}</span>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={async (event: any) => {
|
||||
event.stopPropagation();
|
||||
const createdAuthor = await registerPluginAuthor();
|
||||
if (!createdAuthor?.id) {
|
||||
return;
|
||||
}
|
||||
resolveResult({ action: "registered" });
|
||||
formWrapper?.close?.();
|
||||
}}
|
||||
>
|
||||
{t("certd.onlinePluginAuthorRegister")}
|
||||
</a-button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLatestVersion() {
|
||||
if (!latestVersion) {
|
||||
return <div class="cd-text-muted">{t("certd.onlinePluginNoSubmittedVersion")}</div>;
|
||||
}
|
||||
return (
|
||||
<div class="sys-plugin-publish__latest-version">
|
||||
<div class="sys-plugin-publish__version-line">
|
||||
<strong class="sys-plugin-publish__version">v{latestVersion.version || "-"}</strong>
|
||||
<a-tag color={getStatusColor(latestVersion.status)}>{getStatusLabel(latestVersion.status)}</a-tag>
|
||||
{renderReviewIndicators(latestVersion)}
|
||||
</div>
|
||||
{renderRejectedReasons(latestVersion)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
return (
|
||||
<div class="sys-plugin-publish">
|
||||
<div class="sys-plugin-publish__hero">
|
||||
<div class="sys-plugin-publish__icon-wrap">
|
||||
<fs-icon class="sys-plugin-publish__icon" icon={localPlugin.icon || row.icon || "clarity:plugin-line"} />
|
||||
</div>
|
||||
<div class="sys-plugin-publish__hero-main">
|
||||
<div class="sys-plugin-publish__title-line">
|
||||
<div class="sys-plugin-publish__title">{localPlugin.title || row.title || localPlugin.name || row.name || "-"}</div>
|
||||
<a-tag color="blue">{getPublishModeTitle()}</a-tag>
|
||||
</div>
|
||||
<div class="sys-plugin-publish__desc">{localPlugin.desc || row.desc || "暂无描述"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sys-plugin-publish__sections">
|
||||
<div class="cd-card-section">
|
||||
<div class="cd-card-section-title">插件信息</div>
|
||||
<div class="cd-meta-grid">
|
||||
{renderMetaItem(t("certd.pluginName"), localPlugin.name || row.name)}
|
||||
{renderMetaItem(t("certd.pluginType"), getPluginTypeLabel(localPlugin.pluginType || row.pluginType))}
|
||||
{renderMetaItem(t("certd.pluginGroup"), localPlugin.group || row.group)}
|
||||
{renderMetaItem(t("certd.onlinePluginPublishStatus"), marketPlugin ? getStatusLabel(marketPlugin.status) : t("certd.onlinePluginNotSubmitted"))}
|
||||
<div class="sys-plugin-publish__meta-full">{renderMetaItem(t("certd.author"), renderAuthor())}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cd-card-section">
|
||||
<div class="cd-card-section-title">{t("certd.onlinePluginLatestVersion")}</div>
|
||||
{renderLatestVersion()}
|
||||
</div>
|
||||
|
||||
<div class="cd-card-section">
|
||||
<div class="cd-card-section-title cd-card-section-title--compact">{t("certd.onlinePluginCurrentRelease")}</div>
|
||||
<div class={["cd-tip-box sys-plugin-publish__prompt", { "cd-tip-box-warning": publishMode === "cover" }]}>
|
||||
<div class="cd-tip-title">{t("certd.onlinePluginPublishPrompt")}</div>
|
||||
<div>{getPublishModeTip()}</div>
|
||||
</div>
|
||||
<div class="cd-form-row">
|
||||
<label class="cd-form-label">{t("certd.version")}</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={state.version}
|
||||
placeholder="请输入版本号,例如 1.0.1"
|
||||
class={["cd-text-input", { "cd-text-input-error": state.error }]}
|
||||
onInput={(event: any) => {
|
||||
state.version = event?.target?.value || "";
|
||||
state.error = getVersionError(state.version, versionBaseline);
|
||||
}}
|
||||
/>
|
||||
{state.error ? <div class="cd-field-error">{state.error}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<PublishManagerResult>(resolve => {
|
||||
resolveResult = (result: PublishManagerResult) => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
resolved = true;
|
||||
resolve(result);
|
||||
};
|
||||
void openFormDialog({
|
||||
title: marketPlugin ? t("certd.onlinePluginPublishManage") : t("certd.onlinePluginPublish"),
|
||||
columns: {},
|
||||
body: () => renderContent(),
|
||||
wrapper: {
|
||||
width: 920,
|
||||
buttons: {
|
||||
reset: {
|
||||
show: false,
|
||||
},
|
||||
ok: {
|
||||
show: true,
|
||||
text: t("certd.onlinePluginPublishSubmit"),
|
||||
disabled: !info.authorRegistered,
|
||||
},
|
||||
cancel: {
|
||||
show: true,
|
||||
text: "取消",
|
||||
},
|
||||
},
|
||||
onClosed() {
|
||||
resolveResult({ action: "cancel" });
|
||||
},
|
||||
},
|
||||
async onSubmit() {
|
||||
if (!info.authorRegistered) {
|
||||
throw new Error(t("certd.onlinePluginAuthorNotRegistered"));
|
||||
}
|
||||
state.error = getVersionError(state.version, versionBaseline);
|
||||
if (state.error) {
|
||||
throw new Error(state.error);
|
||||
}
|
||||
resolveResult({ action: "publish", version: normalizeVersion(state.version) });
|
||||
},
|
||||
}).then(wrapper => {
|
||||
formWrapper = wrapper;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function publishLocalPlugin(row: any, options?: { beforePublish?: () => Promise<any>; afterPublish?: () => Promise<void> }) {
|
||||
if (!row?.id || (row.type !== "store" && row.type !== "custom")) {
|
||||
return;
|
||||
}
|
||||
const info = await api.OnlinePluginPublishInfo({
|
||||
id: row.id,
|
||||
});
|
||||
const action = await openPublishManager(row, info);
|
||||
if (action.action === "cancel") {
|
||||
return;
|
||||
}
|
||||
if (action.action === "registered") {
|
||||
await publishLocalPlugin(row, options);
|
||||
return;
|
||||
}
|
||||
if (action.action !== "publish") {
|
||||
return;
|
||||
}
|
||||
|
||||
publishingPluginId.value = row.id;
|
||||
try {
|
||||
const publishRow = (await options?.beforePublish?.()) || row;
|
||||
await api.OnlinePluginPublish({
|
||||
id: publishRow.id,
|
||||
version: action.version,
|
||||
});
|
||||
if (normalizeVersion(publishRow.version) !== action.version) {
|
||||
await api.UpdateObj({
|
||||
...publishRow,
|
||||
version: action.version,
|
||||
});
|
||||
}
|
||||
await pluginStore.reload();
|
||||
await options?.afterPublish?.();
|
||||
message.success(t("certd.onlinePluginPublishSuccess"));
|
||||
} finally {
|
||||
publishingPluginId.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isPublishingPlugin,
|
||||
publishLocalPlugin,
|
||||
registerPluginAuthor,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user