mirror of
https://github.com/certd/certd.git
synced 2026-08-02 02:44:49 +08:00
feat: 支持在线插件下载安装审核等
This commit is contained in:
Vendored
+14
@@ -87,6 +87,20 @@
|
||||
"plus_use_prod": "false",
|
||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server-local-comm",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/packages/ui/certd-server",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev-localcomm"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"plus_use_prod": "false",
|
||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
|
||||
@@ -139,6 +139,9 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
|
||||
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
|
||||
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
||||
- 前端组件样式统一写在 `<style>` / Less / CSS 文件里,通过样式名映射到元素;尽量不要在元素上直接写 `style`。
|
||||
- 每个组件都要有一个稳定的根样式名,并把组件下方样式全部包在该根样式名内;尽量不要使用 `scoped`。
|
||||
- 可复用的公共样式名放在 `packages/ui/certd-client/src/style` 下维护,优先使用 `cd-` 前缀,避免散落在业务组件里重复定义。
|
||||
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
|
||||
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`。
|
||||
|
||||
|
||||
@@ -568,6 +568,7 @@ export class RuntimeDepsService {
|
||||
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
|
||||
if (result.code === 0) {
|
||||
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
||||
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
|
||||
log.info("第三方依赖安装完成");
|
||||
return { registryUrl: tryUrl, packageJsonPath };
|
||||
}
|
||||
@@ -575,9 +576,9 @@ export class RuntimeDepsService {
|
||||
const outOutput = (result.stdout || "").trim();
|
||||
lastError = errOutput || outOutput || "unknown error";
|
||||
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
|
||||
log.info(` pnpm stderr: ${(errOutput || "(空)").slice(0, 2000)}`);
|
||||
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
|
||||
if (outOutput) {
|
||||
log.info(` pnpm stdout: ${outOutput.slice(0, 2000)}`);
|
||||
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
|
||||
}
|
||||
}
|
||||
this.writeInstallState(statePath, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export type IframeMessageData<T> = {
|
||||
action: string;
|
||||
@@ -29,10 +29,12 @@ export class IframeClient {
|
||||
onError?: any;
|
||||
|
||||
handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {};
|
||||
private messageHandler: (event: MessageEvent<IframeMessageData<any>>) => Promise<void>;
|
||||
|
||||
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
|
||||
this.iframe = iframe;
|
||||
this.onError = onError;
|
||||
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
|
||||
this.messageHandler = async (event: MessageEvent<IframeMessageData<any>>) => {
|
||||
const data = event.data;
|
||||
if (data.action) {
|
||||
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
|
||||
@@ -40,20 +42,21 @@ export class IframeClient {
|
||||
const handler = this.handlers[data.action];
|
||||
if (handler) {
|
||||
const res = await handler(data);
|
||||
if (data.id && data.action !== 'reply') {
|
||||
await this.send('reply', res, data.id);
|
||||
if (data.id && data.action !== "reply") {
|
||||
await this.send("reply", res, data.id);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
await this.send('reply', {}, data.id, 500, e.message);
|
||||
await this.send("reply", {}, data.id, 500, e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
|
||||
this.register('reply', async data => {
|
||||
this.register("reply", async data => {
|
||||
const req = this.requestQueue[data.replyId!];
|
||||
if (req) {
|
||||
req.onReply(data);
|
||||
@@ -61,11 +64,17 @@ export class IframeClient {
|
||||
}
|
||||
});
|
||||
}
|
||||
isInFrame() {
|
||||
|
||||
public destroy() {
|
||||
window.removeEventListener("message", this.messageHandler);
|
||||
this.requestQueue = {};
|
||||
this.handlers = {};
|
||||
}
|
||||
public isInFrame() {
|
||||
return window.self !== window.top;
|
||||
}
|
||||
|
||||
register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
|
||||
public register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
|
||||
this.handlers[action] = handler;
|
||||
}
|
||||
|
||||
@@ -106,12 +115,12 @@ export class IframeClient {
|
||||
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
|
||||
if (!this.iframe) {
|
||||
if (!window.parent) {
|
||||
reject('当前页面不在 iframe 中');
|
||||
reject("当前页面不在 iframe 中");
|
||||
}
|
||||
window.parent.postMessage(reqMessageData, '*');
|
||||
window.parent.postMessage(reqMessageData, "*");
|
||||
} else {
|
||||
//子页面
|
||||
this.iframe.contentWindow?.postMessage(reqMessageData, '*');
|
||||
this.iframe.contentWindow?.postMessage(reqMessageData, "*");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -279,3 +279,11 @@ export class SysSafeSetting extends BaseSettings {
|
||||
autoHiddenTimes: 5,
|
||||
};
|
||||
}
|
||||
|
||||
export class SysPluginSetting extends BaseSettings {
|
||||
static __title__ = "系统插件设置";
|
||||
static __key__ = "sys.plugin";
|
||||
static __access__ = "private";
|
||||
|
||||
lastSyncTime?: number;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -26,8 +26,9 @@ typeorm:
|
||||
#
|
||||
#plus:
|
||||
# server:
|
||||
# baseUrls: ['http://127.0.0.1:11007']
|
||||
# baseUrls: ['http://localhost:11007']
|
||||
#
|
||||
#account:
|
||||
# server:
|
||||
# baseUrl: 'http://127.0.0.1:1017/subject'
|
||||
# baseUrl: 'http://localhost:1017/subject'
|
||||
|
||||
+1
-1
@@ -30,4 +30,4 @@ typeorm:
|
||||
|
||||
account:
|
||||
server:
|
||||
baseUrl: 'http://127.0.0.1:1017/subject'
|
||||
baseUrl: 'http://localhost:1017/subject'
|
||||
@@ -17,4 +17,4 @@ typeorm:
|
||||
#
|
||||
#account:
|
||||
# server:
|
||||
# baseUrl: 'http://127.0.0.1:1017/subject'
|
||||
# baseUrl: 'http://localhost:1017/subject'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "app_id" integer;
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "developer_id" integer;
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "full_name" varchar(200);
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "latest" varchar(100);
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "status" varchar(100);
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "download_count" integer;
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "score" real;
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "sync_time" integer;
|
||||
|
||||
CREATE UNIQUE INDEX "index_plugin_full_name" ON "pi_plugin" ("full_name");
|
||||
CREATE INDEX "index_plugin_plugin_type" ON "pi_plugin" ("pluginType");
|
||||
CREATE INDEX "index_plugin_group" ON "pi_plugin" ("group");
|
||||
CREATE INDEX "index_plugin_developer_id" ON "pi_plugin" ("developer_id");
|
||||
CREATE INDEX "index_plugin_sync_time" ON "pi_plugin" ("sync_time");
|
||||
|
||||
UPDATE "pi_plugin" SET "type" = 'store' WHERE "type" = 'custom';
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE "pi_plugin_market_item"
|
||||
(
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"app_id" integer,
|
||||
"full_name" varchar(200) NOT NULL,
|
||||
"author" varchar(100),
|
||||
"name" varchar(100),
|
||||
"plugin_type" varchar(100),
|
||||
"title" varchar(200),
|
||||
"icon" varchar(200),
|
||||
"group" varchar(100),
|
||||
"desc" varchar(1000),
|
||||
"latest" varchar(100),
|
||||
"status" varchar(100),
|
||||
"download_count" integer,
|
||||
"sync_time" integer,
|
||||
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_plugin_market_item_full_name" ON "pi_plugin_market_item" ("full_name");
|
||||
CREATE INDEX "index_plugin_market_item_plugin_type" ON "pi_plugin_market_item" ("plugin_type");
|
||||
CREATE INDEX "index_plugin_market_item_group" ON "pi_plugin_market_item" ("group");
|
||||
CREATE INDEX "index_plugin_market_item_sync_time" ON "pi_plugin_market_item" ("sync_time");
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "installed" boolean NOT NULL DEFAULT true;
|
||||
|
||||
UPDATE "pi_plugin" SET "installed" = true;
|
||||
|
||||
ALTER TABLE "pi_plugin" ADD COLUMN "ai_check_status" varchar(32) NOT NULL DEFAULT '';
|
||||
@@ -9,7 +9,7 @@
|
||||
"dev-start": "cross-env NODE_ENV=dev node --optimize-for-size ./bootstrap.js",
|
||||
"dc": "cd ../../../ && pnpm run dev",
|
||||
"dev": "cross-env NODE_ENV=dev mwtsc --watch --run @midwayjs/mock/app",
|
||||
"dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app",
|
||||
"dev-localcomm": "cross-env NODE_ENV=dev-localcomm mwtsc --watch --run @midwayjs/mock/app",
|
||||
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
|
||||
"dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
|
||||
"dev-mysql": "cross-env NODE_ENV=dev-mysql mwtsc --watch --run @midwayjs/mock/app",
|
||||
|
||||
@@ -42,9 +42,10 @@ process.on("uncaughtException", error => {
|
||||
// }
|
||||
// setInterval(log, 200);
|
||||
// log()
|
||||
// }
|
||||
// startHeapLog();
|
||||
// }127.0.0.1
|
||||
|
||||
|
||||
// startHeapLog();
|
||||
@Configuration({
|
||||
detectorOptions: {
|
||||
ignore: ["**/plugins/**"],
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { merge } from "lodash-es";
|
||||
import { CrudController } from "@certd/lib-server";
|
||||
import { OnlinePluginInstallReq, OnlinePluginListReq, PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
|
||||
import {
|
||||
OnlinePluginAuthorAddReq,
|
||||
OnlinePluginDetailReq,
|
||||
OnlinePluginInstallReq,
|
||||
OnlinePluginListReq,
|
||||
OnlinePluginPublishInfoReq,
|
||||
OnlinePluginPublishReq,
|
||||
OnlinePluginRateReq,
|
||||
OnlinePluginSourceReq,
|
||||
OnlinePluginVersionSubmitReq,
|
||||
PluginImportReq,
|
||||
PluginService,
|
||||
} from "../../../modules/plugin/service/plugin-service.js";
|
||||
import { CommPluginConfig, PluginConfig, PluginConfigService } from "../../../modules/plugin/service/plugin-config-service.js";
|
||||
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
||||
/**
|
||||
@@ -40,8 +52,13 @@ export class PluginController extends CrudController<PluginService> {
|
||||
const def: any = {
|
||||
isDefault: false,
|
||||
disabled: false,
|
||||
type: "store",
|
||||
};
|
||||
merge(bean, def);
|
||||
bean.fullName = bean.name;
|
||||
if (bean.author){
|
||||
bean.fullName = bean.author + "/" + bean.name;
|
||||
}
|
||||
const res = await super.add(bean);
|
||||
await this.auditLog({ content: `新增了插件「${bean.name}」(ID:${res.data}, 类型:${bean.type})` });
|
||||
return res;
|
||||
@@ -49,6 +66,7 @@ export class PluginController extends CrudController<PluginService> {
|
||||
|
||||
@Post("/update", { description: "sys:settings:edit", summary: "更新插件" })
|
||||
async update(@Body(ALL) bean: any) {
|
||||
await this.service.ensurePluginEditable(bean.id);
|
||||
const res = await super.update(bean);
|
||||
await this.auditLog({ content: `修改了插件「${bean.name}」(ID:${bean.id})` });
|
||||
return res;
|
||||
@@ -121,6 +139,12 @@ export class PluginController extends CrudController<PluginService> {
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/setting", { description: "sys:settings:view", summary: "查询在线插件同步设置" })
|
||||
async onlineSetting() {
|
||||
const res = await this.service.getOnlinePluginSetting();
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/sync", { description: "sys:settings:edit", summary: "同步在线插件" })
|
||||
async onlineSync() {
|
||||
const res = await this.service.syncOnlinePluginList();
|
||||
@@ -135,6 +159,58 @@ export class PluginController extends CrudController<PluginService> {
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/detail", { description: "sys:settings:view", summary: "查看在线插件详情" })
|
||||
async onlineDetail(@Body(ALL) body: OnlinePluginDetailReq) {
|
||||
const res = await this.service.onlinePluginDetail(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/source", { description: "sys:settings:view", summary: "查看在线插件源代码" })
|
||||
async onlineSource(@Body(ALL) body: OnlinePluginSourceReq) {
|
||||
const res = await this.service.onlinePluginSource(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/rate", { description: "sys:settings:edit", summary: "在线插件评分" })
|
||||
async onlineRate(@Body(ALL) body: OnlinePluginRateReq) {
|
||||
const res = await this.service.rateOnlinePlugin(body);
|
||||
await this.auditLog({ content: `给在线插件「${body.fullName || body.pluginId}」评分` });
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/version/submit", { description: "sys:settings:edit", summary: "提交在线插件版本" })
|
||||
async onlineVersionSubmit(@Body(ALL) body: OnlinePluginVersionSubmitReq) {
|
||||
const res = await this.service.submitOnlinePluginVersion(body);
|
||||
await this.auditLog({ content: `提交了在线插件「${body.fullName}」的新版本` });
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/publish", { description: "sys:settings:edit", summary: "发布本地插件到市场" })
|
||||
async onlinePublish(@Body(ALL) body: OnlinePluginPublishReq) {
|
||||
const res = await this.service.publishLocalPlugin(body);
|
||||
await this.auditLog({ content: `发布了本地插件(ID:${body.id})到插件市场` });
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/author/get", { description: "sys:settings:view", summary: "查询在线插件作者" })
|
||||
async onlineAuthorGet() {
|
||||
const res = await this.service.getOnlinePluginAuthor();
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/author/add", { description: "sys:settings:edit", summary: "注册在线插件作者" })
|
||||
async onlineAuthorAdd(@Body(ALL) body: OnlinePluginAuthorAddReq) {
|
||||
const res = await this.service.addOnlinePluginAuthor(body);
|
||||
await this.auditLog({ content: `注册了在线插件作者「${body.name}」` });
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/online/publish/info", { description: "sys:settings:view", summary: "查询本地插件发布信息" })
|
||||
async onlinePublishInfo(@Body(ALL) body: OnlinePluginPublishInfoReq) {
|
||||
const res = await this.service.getOnlinePluginPublishInfo(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/export", { description: "sys:settings:edit", summary: "导出插件" })
|
||||
async export(@Body("id") id: number) {
|
||||
const res = await this.service.exportPlugin(id);
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("pi_plugin_market_item")
|
||||
export class PluginMarketItemEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: "app_id", comment: "平台应用ID", nullable: true })
|
||||
appId: number;
|
||||
|
||||
@Column({ name: "full_name", comment: "插件完整名称", length: 200 })
|
||||
fullName: string;
|
||||
|
||||
@Column({ name: "author", comment: "作者", length: 100, nullable: true })
|
||||
author: string;
|
||||
|
||||
@Column({ name: "name", comment: "插件名称", length: 100, nullable: true })
|
||||
name: string;
|
||||
|
||||
@Column({ name: "plugin_type", comment: "插件类型", length: 100, nullable: true })
|
||||
pluginType: string;
|
||||
|
||||
@Column({ name: "title", comment: "标题", length: 200, nullable: true })
|
||||
title: string;
|
||||
|
||||
@Column({ name: "icon", comment: "图标", length: 200, nullable: true })
|
||||
icon: string;
|
||||
|
||||
@Column({ name: "group", comment: "分组", length: 100, nullable: true })
|
||||
group: string;
|
||||
|
||||
@Column({ name: "desc", comment: "描述", length: 1000, nullable: true })
|
||||
desc: string;
|
||||
|
||||
@Column({ name: "latest", comment: "最新版本", length: 100, nullable: true })
|
||||
latest: string;
|
||||
|
||||
@Column({ name: "status", comment: "状态", length: 100, nullable: true })
|
||||
status: string;
|
||||
|
||||
@Column({ name: "download_count", comment: "下载次数", nullable: true })
|
||||
downloadCount: number;
|
||||
|
||||
@Column({ name: "sync_time", comment: "同步时间", nullable: true })
|
||||
syncTime: number;
|
||||
|
||||
@Column({
|
||||
name: "create_time",
|
||||
comment: "创建时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@Column({
|
||||
name: "update_time",
|
||||
comment: "修改时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export class PluginEntity {
|
||||
content: string;
|
||||
|
||||
@Column({ comment: "类型", length: 100, nullable: true })
|
||||
type: string; // builtIn | local | download
|
||||
type: string; // builtIn | store
|
||||
|
||||
@Column({ comment: "启用/禁用", default: false })
|
||||
disabled: boolean;
|
||||
@@ -41,6 +41,9 @@ export class PluginEntity {
|
||||
@Column({ comment: "插件类型", length: 100, nullable: true })
|
||||
pluginType: string;
|
||||
|
||||
@Column({ comment: "是否已安装", default: true })
|
||||
installed: boolean;
|
||||
|
||||
@Column({ comment: "元数据", length: 40960, nullable: true })
|
||||
metadata: string;
|
||||
|
||||
@@ -50,6 +53,33 @@ export class PluginEntity {
|
||||
@Column({ comment: "作者", length: 100, nullable: true })
|
||||
author: string;
|
||||
|
||||
@Column({ name: "app_id", comment: "平台应用ID", nullable: true })
|
||||
appId: number;
|
||||
|
||||
@Column({ name: "developer_id", comment: "平台开发者ID", nullable: true })
|
||||
developerId: number;
|
||||
|
||||
@Column({ name: "full_name", comment: "插件完整名称", length: 200, nullable: true })
|
||||
fullName: string;
|
||||
|
||||
@Column({ comment: "最新版本", length: 100, nullable: true })
|
||||
latest: string;
|
||||
|
||||
@Column({ comment: "市场状态", length: 100, nullable: true })
|
||||
status: string;
|
||||
|
||||
@Column({ name: "download_count", comment: "下载次数", nullable: true })
|
||||
downloadCount: number;
|
||||
|
||||
@Column({ comment: "评分", nullable: true })
|
||||
score: number;
|
||||
|
||||
@Column({ name: "ai_check_status", comment: "最新版本AI审核状态", length: 32, default: "" })
|
||||
aiCheckStatus: string;
|
||||
|
||||
@Column({ name: "sync_time", comment: "同步时间", nullable: true })
|
||||
syncTime: number;
|
||||
|
||||
@Column({
|
||||
name: "create_time",
|
||||
comment: "创建时间",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { addonRegistry, BaseService, PageReq, PlusService } from "@certd/lib-server";
|
||||
import { addonRegistry, BaseService, PageReq, PlusService, SysInstallInfo, SysPluginSetting, SysSettingsService } from "@certd/lib-server";
|
||||
import { PluginEntity } from "../entity/plugin.js";
|
||||
import { PluginMarketItemEntity } from "../entity/plugin-market-item.js";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { Brackets, IsNull, Not, Repository } from "typeorm";
|
||||
import { isComm } from "@certd/plus-core";
|
||||
@@ -32,10 +31,71 @@ export type OnlinePluginInstallReq = {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginDetailReq = {
|
||||
pluginId?: number;
|
||||
fullName?: string;
|
||||
commentPageNo?: number;
|
||||
commentPageSize?: number;
|
||||
};
|
||||
|
||||
export type OnlinePluginSourceReq = OnlinePluginDetailReq & {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginRateReq = OnlinePluginDetailReq & {
|
||||
score: number;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginVersionSubmitReq = {
|
||||
fullName: string;
|
||||
version: string;
|
||||
content: string;
|
||||
minAppVersion?: string;
|
||||
maxAppVersion?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginPublishReq = {
|
||||
id: number;
|
||||
version?: string;
|
||||
minAppVersion?: string;
|
||||
maxAppVersion?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginPublishInfoReq = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type OnlinePluginAuthorBean = {
|
||||
id?: number;
|
||||
appId?: number;
|
||||
appOwnerId?: number;
|
||||
developerId?: number;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
desc?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginAuthorGetReply = {
|
||||
registered?: boolean;
|
||||
author?: OnlinePluginAuthorBean;
|
||||
};
|
||||
|
||||
export type OnlinePluginAuthorAddReq = {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
desc?: string;
|
||||
};
|
||||
|
||||
export type OnlinePluginBean = {
|
||||
id?: number;
|
||||
appId?: number;
|
||||
developerId?: number;
|
||||
author?: string;
|
||||
type?: string;
|
||||
pluginType?: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
@@ -46,14 +106,41 @@ 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 OnlinePluginPublishInfo = {
|
||||
localPlugin: Partial<PluginEntity>;
|
||||
authorRegistered: boolean;
|
||||
author?: OnlinePluginAuthorBean;
|
||||
marketPlugin?: OnlinePluginBean;
|
||||
versions: OnlinePluginVersionBean[];
|
||||
};
|
||||
|
||||
const ONLINE_PLUGIN_SYNC_PAGE_SIZE = 200;
|
||||
|
||||
export function parseOnlinePluginFullName(fullName: string) {
|
||||
@@ -134,6 +221,39 @@ function isBareModuleSpecifier(modulePath: string) {
|
||||
return !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(modulePath);
|
||||
}
|
||||
|
||||
function normalizePluginSourceType(type?: string) {
|
||||
if (!type || type === "custom") {
|
||||
return "store";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
export function canEditStorePlugin(developerId?: number, bindUserId?: number) {
|
||||
if (!developerId) {
|
||||
return true;
|
||||
}
|
||||
return !!bindUserId && developerId === bindUserId;
|
||||
}
|
||||
|
||||
export function getPluginFullName(plugin: { fullName?: string; author?: string; name?: string }) {
|
||||
const fullName = `${plugin.fullName || ""}`.trim();
|
||||
if (fullName) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
const name = `${plugin.name || ""}`.trim();
|
||||
const author = `${plugin.author || ""}`.trim();
|
||||
if (!author || !name || name.startsWith(`${author}/`)) {
|
||||
return name;
|
||||
}
|
||||
return `${author}/${name}`;
|
||||
}
|
||||
|
||||
function getPluginRegistryName(plugin: { fullName?: string; author?: string; name?: string; addonType?: string }) {
|
||||
const fullName = getPluginFullName(plugin);
|
||||
return plugin.addonType ? `${plugin.addonType}:${fullName}` : fullName;
|
||||
}
|
||||
|
||||
async function importLocalModule(modulePath: string) {
|
||||
if (!modulePath) {
|
||||
throw new Error("modules path 不能为空");
|
||||
@@ -153,30 +273,61 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
@InjectEntityModel(PluginEntity)
|
||||
repository: Repository<PluginEntity>;
|
||||
|
||||
@InjectEntityModel(PluginMarketItemEntity)
|
||||
pluginMarketItemRepository: Repository<PluginMarketItemEntity>;
|
||||
|
||||
@Inject()
|
||||
builtInPluginService: BuiltInPluginService;
|
||||
|
||||
@Inject("plusService")
|
||||
plusService: PlusService;
|
||||
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
private addInstalledPluginOnlyQuery(bq: any) {
|
||||
bq.andWhere("(main.type != :storeType OR main.installed = :installed)", {
|
||||
storeType: "store",
|
||||
installed: true,
|
||||
});
|
||||
}
|
||||
|
||||
private filterBuiltInList(list: PluginEntity[], query: Partial<PluginEntity>) {
|
||||
let records = list;
|
||||
if (query.group) {
|
||||
records = records.filter(item => item.group === query.group);
|
||||
}
|
||||
if (query.pluginType) {
|
||||
records = records.filter(item => item.pluginType === query.pluginType);
|
||||
}
|
||||
if (query.name) {
|
||||
const keyword = `${query.name}`.trim().toLowerCase();
|
||||
records = records.filter(item => (item.name || "").toLowerCase().includes(keyword));
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async page(pageReq: PageReq<PluginEntity>) {
|
||||
pageReq.query = pageReq.query || {};
|
||||
if (pageReq.query.type === "custom") {
|
||||
pageReq.query.type = "store";
|
||||
}
|
||||
if (pageReq.query.type && pageReq.query.type !== "builtIn") {
|
||||
return await super.page(pageReq);
|
||||
const pageRes = await super.page(pageReq);
|
||||
if (pageReq.query.type === "store") {
|
||||
pageRes.records = (await this.attachOnlineInstallState(pageRes.records.map(item => this.toOnlinePluginBean(item)))) as any;
|
||||
}
|
||||
return pageRes;
|
||||
}
|
||||
//仅查询内置插件
|
||||
const offset = pageReq.page.offset;
|
||||
const limit = pageReq.page.limit;
|
||||
|
||||
const builtInList = await this.getBuiltInEntityList();
|
||||
let builtInList = await this.getBuiltInEntityList();
|
||||
builtInList = this.filterBuiltInList(builtInList, pageReq.query || {});
|
||||
|
||||
//获取分页数据
|
||||
const data = builtInList.slice(offset, offset + limit);
|
||||
@@ -332,6 +483,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
* @param param 数据
|
||||
*/
|
||||
async add(param: any) {
|
||||
param.type = normalizePluginSourceType(param.type);
|
||||
const old = await this.repository.findOne({
|
||||
where: {
|
||||
name: param.name,
|
||||
@@ -393,11 +545,13 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
return "";
|
||||
}
|
||||
|
||||
private normalizeOnlinePluginMarketItem(item: OnlinePluginBean, syncTime: number, old?: PluginMarketItemEntity) {
|
||||
return this.pluginMarketItemRepository.create({
|
||||
private normalizeOnlinePluginRecord(item: OnlinePluginBean, syncTime: number, old?: PluginEntity) {
|
||||
const fullName = this.getOnlinePluginFullName(item);
|
||||
return this.repository.create({
|
||||
id: old?.id,
|
||||
appId: item.appId,
|
||||
fullName: this.getOnlinePluginFullName(item),
|
||||
developerId: item.developerId,
|
||||
fullName,
|
||||
author: item.author,
|
||||
name: item.name,
|
||||
pluginType: item.pluginType,
|
||||
@@ -408,17 +562,31 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
latest: item.latest,
|
||||
status: item.status,
|
||||
downloadCount: item.downloadCount,
|
||||
score: item.score,
|
||||
aiCheckStatus: item.aiCheckStatus,
|
||||
syncTime,
|
||||
type: old?.type || "store",
|
||||
disabled: old?.disabled ?? false,
|
||||
installed: old?.installed ?? false,
|
||||
setting: old?.setting,
|
||||
sysSetting: old?.sysSetting,
|
||||
content: old?.content,
|
||||
metadata: old?.metadata,
|
||||
extra: old?.extra,
|
||||
version: old?.version,
|
||||
updateTime: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
private toOnlinePluginBean(item: PluginMarketItemEntity): OnlinePluginBean {
|
||||
private toOnlinePluginBean(item: PluginEntity): OnlinePluginBean {
|
||||
const isInstalled = item.installed === true;
|
||||
return {
|
||||
id: item.id,
|
||||
appId: item.appId,
|
||||
developerId: item.developerId,
|
||||
fullName: item.fullName,
|
||||
author: item.author,
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
pluginType: item.pluginType,
|
||||
title: item.title,
|
||||
@@ -428,14 +596,23 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
latest: item.latest,
|
||||
status: item.status,
|
||||
downloadCount: item.downloadCount,
|
||||
score: item.score,
|
||||
aiCheckStatus: item.aiCheckStatus,
|
||||
syncTime: item.syncTime,
|
||||
installed: isInstalled,
|
||||
localPluginId: isInstalled ? item.id : undefined,
|
||||
localDisabled: isInstalled ? item.disabled : undefined,
|
||||
installedVersion: isInstalled ? item.version : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async findOnlinePluginMarketItems(req: OnlinePluginListReq) {
|
||||
private async findOnlinePluginRecords(req: OnlinePluginListReq) {
|
||||
const query = req || {};
|
||||
const keyword = (query.keyword || "").trim().toLowerCase();
|
||||
const builder = this.pluginMarketItemRepository.createQueryBuilder("item");
|
||||
const builder = this.repository.createQueryBuilder("item");
|
||||
builder.where("item.type = :type", {
|
||||
type: "store",
|
||||
});
|
||||
if (query.pluginType) {
|
||||
builder.andWhere("item.pluginType = :pluginType", {
|
||||
pluginType: query.pluginType,
|
||||
@@ -449,7 +626,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
if (keyword) {
|
||||
builder.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("LOWER(item.fullName) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||
qb.where("LOWER(COALESCE(item.fullName, '')) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||
.orWhere("LOWER(item.author) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||
.orWhere("LOWER(item.name) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||
.orWhere("LOWER(item.title) LIKE :keyword", { keyword: `%${keyword}%` })
|
||||
@@ -464,38 +641,46 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
|
||||
private async findInstalledOnlinePlugin(parsed: { author: string; name: string }, record: OnlinePluginBean) {
|
||||
const fullName = record.fullName || `${parsed.author}/${parsed.name}`;
|
||||
const where: any[] = [
|
||||
{
|
||||
author: parsed.author,
|
||||
name: parsed.name,
|
||||
},
|
||||
{
|
||||
name: fullName,
|
||||
},
|
||||
];
|
||||
if (record.pluginType) {
|
||||
where.push({
|
||||
pluginType: record.pluginType,
|
||||
name: parsed.name,
|
||||
});
|
||||
}
|
||||
return await this.repository.findOne({
|
||||
where,
|
||||
});
|
||||
const builder = this.repository
|
||||
.createQueryBuilder("plugin")
|
||||
.where("plugin.type = :type", { type: "store" })
|
||||
.andWhere("plugin.installed = :installed", { installed: true })
|
||||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("plugin.fullName = :fullName", { fullName }).orWhere("plugin.name = :fullName", { fullName }).orWhere("plugin.author = :author AND plugin.name = :name", {
|
||||
author: parsed.author,
|
||||
name: parsed.name,
|
||||
});
|
||||
if (record.pluginType) {
|
||||
qb.orWhere("plugin.pluginType = :pluginType AND plugin.name = :name", {
|
||||
pluginType: record.pluginType,
|
||||
name: parsed.name,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return await builder.getOne();
|
||||
}
|
||||
|
||||
private async attachOnlineInstallState(list: OnlinePluginBean[]) {
|
||||
const records: OnlinePluginBean[] = [];
|
||||
let bindUserId = 0;
|
||||
if (this.sysSettingsService) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
bindUserId = installInfo.bindUserId || 0;
|
||||
}
|
||||
for (const item of list) {
|
||||
const record: OnlinePluginBean = { ...item };
|
||||
const fullName = this.getOnlinePluginFullName(record);
|
||||
record.fullName = fullName;
|
||||
record.selfAuthored = !!bindUserId && record.developerId === bindUserId;
|
||||
record.localEditable = canEditStorePlugin(record.developerId, bindUserId);
|
||||
|
||||
let parsed: { author: string; name: string };
|
||||
try {
|
||||
parsed = parseOnlinePluginFullName(fullName);
|
||||
} catch (e) {
|
||||
record.installed = false;
|
||||
record.installed = !!record.localPluginId;
|
||||
record.upgradeAvailable = false;
|
||||
records.push(record);
|
||||
continue;
|
||||
@@ -507,13 +692,14 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
record.localDisabled = localPlugin?.disabled;
|
||||
record.installedVersion = localPlugin?.version;
|
||||
record.upgradeAvailable = localPlugin ? isOnlinePluginUpgradeAvailable(localPlugin.version, record.latest) : false;
|
||||
record.localEditable = canEditStorePlugin(localPlugin?.developerId ?? record.developerId, bindUserId);
|
||||
records.push(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
async onlinePluginList(req: OnlinePluginListReq) {
|
||||
const list = await this.findOnlinePluginMarketItems(req);
|
||||
const list = await this.findOnlinePluginRecords(req);
|
||||
return await this.attachOnlineInstallState(list.map(item => this.toOnlinePluginBean(item)));
|
||||
}
|
||||
|
||||
@@ -522,12 +708,14 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
|
||||
// 只同步市场列表元数据,插件 YAML 内容仍然在安装时按版本下载。
|
||||
const pluginMap = new Map<string, OnlinePluginBean>();
|
||||
const createdAtLt = Date.now();
|
||||
let pageStart = 0;
|
||||
while (true) {
|
||||
const res = await this.plusService.request({
|
||||
url: "/activation/plugin/list",
|
||||
url: "/activation/plugin/page",
|
||||
method: "post",
|
||||
data: {
|
||||
createdAtLt,
|
||||
page: {
|
||||
start: pageStart,
|
||||
limit: ONLINE_PLUGIN_SYNC_PAGE_SIZE,
|
||||
@@ -550,21 +738,44 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
record.fullName = fullName;
|
||||
pluginMap.set(fullName, record);
|
||||
}
|
||||
const total = Number(res?.page?.total || 0);
|
||||
if (total > 0) {
|
||||
pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE;
|
||||
if (pageStart >= total) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (list.length < ONLINE_PLUGIN_SYNC_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE;
|
||||
}
|
||||
const existingList = await this.pluginMarketItemRepository.find();
|
||||
const existingMap = new Map(existingList.map(item => [item.fullName, item]));
|
||||
const existingList = await this.repository.find({
|
||||
where: {
|
||||
type: "store",
|
||||
},
|
||||
});
|
||||
const existingMap = new Map<string, PluginEntity>();
|
||||
for (const item of existingList) {
|
||||
const fullName = item.fullName || this.getOnlinePluginFullName(item as any);
|
||||
if (fullName) {
|
||||
existingMap.set(fullName, item);
|
||||
}
|
||||
}
|
||||
const syncTime = Date.now();
|
||||
const records = Array.from(pluginMap.values()).map(item => {
|
||||
return this.normalizeOnlinePluginMarketItem(item, syncTime, existingMap.get(item.fullName));
|
||||
return this.normalizeOnlinePluginRecord(item, syncTime, existingMap.get(item.fullName));
|
||||
});
|
||||
await this.pluginMarketItemRepository.save(records);
|
||||
await this.repository.save(records);
|
||||
await this.saveOnlinePluginSyncTime(syncTime);
|
||||
return await this.onlinePluginList({});
|
||||
}
|
||||
|
||||
async getOnlinePluginSetting() {
|
||||
return await this.sysSettingsService.getSetting<SysPluginSetting>(SysPluginSetting);
|
||||
}
|
||||
|
||||
async installOnlinePlugin(req: OnlinePluginInstallReq) {
|
||||
const fullName = (req.fullName || "").trim();
|
||||
parseOnlinePluginFullName(fullName);
|
||||
@@ -597,12 +808,243 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
};
|
||||
}
|
||||
|
||||
async onlinePluginDetail(req: OnlinePluginDetailReq) {
|
||||
await this.plusService.register();
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
const res = await this.plusService.request({
|
||||
url: "/activation/plugin/detail",
|
||||
method: "post",
|
||||
data: {
|
||||
pluginId: req.pluginId || 0,
|
||||
fullName: req.fullName || "",
|
||||
userId: installInfo.bindUserId || 0,
|
||||
commentPageNo: req.commentPageNo || 1,
|
||||
commentPageSize: req.commentPageSize || 5,
|
||||
},
|
||||
});
|
||||
if (res?.plugin) {
|
||||
const [plugin] = await this.attachOnlineInstallState([res.plugin]);
|
||||
res.plugin = plugin;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async onlinePluginSource(req: OnlinePluginSourceReq) {
|
||||
await this.plusService.register();
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
return await this.plusService.request({
|
||||
url: "/activation/plugin/source",
|
||||
method: "post",
|
||||
data: {
|
||||
pluginId: req.pluginId || 0,
|
||||
fullName: req.fullName || "",
|
||||
version: req.version || "",
|
||||
userId: installInfo.bindUserId || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async rateOnlinePlugin(req: OnlinePluginRateReq) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!installInfo.bindUserId) {
|
||||
throw new Error("请先绑定账号后再评分");
|
||||
}
|
||||
await this.plusService.register();
|
||||
const res = await this.plusService.request({
|
||||
url: "/activation/plugin/rate",
|
||||
method: "post",
|
||||
data: {
|
||||
pluginId: req.pluginId || 0,
|
||||
fullName: req.fullName || "",
|
||||
userId: installInfo.bindUserId,
|
||||
score: req.score,
|
||||
comment: req.comment,
|
||||
},
|
||||
});
|
||||
if (res?.plugin?.score != null) {
|
||||
await this.repository.update(
|
||||
{
|
||||
type: "store",
|
||||
fullName: res.plugin.fullName || req.fullName,
|
||||
},
|
||||
{
|
||||
score: res.plugin.score,
|
||||
}
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async submitOnlinePluginVersion(req: OnlinePluginVersionSubmitReq) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!installInfo.bindUserId) {
|
||||
throw new Error("请先绑定账号后再发布版本");
|
||||
}
|
||||
await this.plusService.register();
|
||||
return await this.plusService.request({
|
||||
url: "/activation/plugin/version/submit",
|
||||
method: "post",
|
||||
data: {
|
||||
userId: installInfo.bindUserId,
|
||||
fullName: req.fullName,
|
||||
version: req.version,
|
||||
content: req.content,
|
||||
minAppVersion: req.minAppVersion || "",
|
||||
maxAppVersion: req.maxAppVersion || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async publishLocalPlugin(req: OnlinePluginPublishReq) {
|
||||
if (!req.id) {
|
||||
throw new Error("插件ID不能为空");
|
||||
}
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!installInfo.bindUserId) {
|
||||
throw new Error("请先绑定账号后再发布插件");
|
||||
}
|
||||
const plugin = await this.info(req.id);
|
||||
if (!plugin) {
|
||||
throw new Error("插件不存在");
|
||||
}
|
||||
if (normalizePluginSourceType(plugin.type) !== "store") {
|
||||
throw new Error("只有商店插件可以发布到插件市场");
|
||||
}
|
||||
if (!canEditStorePlugin(plugin.developerId, installInfo.bindUserId)) {
|
||||
throw new Error("当前绑定账号无权编辑该插件");
|
||||
}
|
||||
const content = await this.exportPlugin(req.id);
|
||||
await this.plusService.register();
|
||||
return await this.plusService.request({
|
||||
url: "/activation/plugin/publish",
|
||||
method: "post",
|
||||
data: {
|
||||
userId: installInfo.bindUserId,
|
||||
content,
|
||||
version: req.version || plugin.version || "",
|
||||
minAppVersion: req.minAppVersion || "",
|
||||
maxAppVersion: req.maxAppVersion || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async ensurePluginEditable(id: number) {
|
||||
const plugin = await this.info(id);
|
||||
if (!plugin) {
|
||||
throw new Error("插件不存在");
|
||||
}
|
||||
if (plugin.type === "custom") {
|
||||
return plugin;
|
||||
}
|
||||
if (plugin.type !== "store") {
|
||||
throw new Error("该插件不允许编辑");
|
||||
}
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!canEditStorePlugin(plugin.developerId, installInfo.bindUserId)) {
|
||||
throw new Error("当前绑定账号无权编辑该插件");
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private async getBindUserId(action: string) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!installInfo.bindUserId) {
|
||||
throw new Error(`请先绑定账号后再${action}`);
|
||||
}
|
||||
return installInfo.bindUserId;
|
||||
}
|
||||
|
||||
async getOnlinePluginAuthor(): Promise<OnlinePluginAuthorGetReply> {
|
||||
const bindUserId = await this.getBindUserId("发布插件");
|
||||
await this.plusService.register();
|
||||
return await this.plusService.request({
|
||||
url: "/activation/plugin/author/get",
|
||||
method: "post",
|
||||
data: {
|
||||
userId: bindUserId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addOnlinePluginAuthor(req: OnlinePluginAuthorAddReq): Promise<OnlinePluginAuthorBean> {
|
||||
const bindUserId = await this.getBindUserId("注册插件作者");
|
||||
await this.plusService.register();
|
||||
return await this.plusService.request({
|
||||
url: "/activation/plugin/author/add",
|
||||
method: "post",
|
||||
data: {
|
||||
userId: bindUserId,
|
||||
name: req.name,
|
||||
displayName: req.displayName || "",
|
||||
avatar: req.avatar || "",
|
||||
desc: req.desc || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOnlinePluginPublishInfo(req: OnlinePluginPublishInfoReq): Promise<OnlinePluginPublishInfo> {
|
||||
if (!req.id) {
|
||||
throw new Error("插件ID不能为空");
|
||||
}
|
||||
const plugin = await this.info(req.id);
|
||||
if (!plugin) {
|
||||
throw new Error("插件不存在");
|
||||
}
|
||||
if (normalizePluginSourceType(plugin.type) !== "store") {
|
||||
throw new Error("只有商店插件可以发布到插件市场");
|
||||
}
|
||||
const bindUserId = await this.getBindUserId("发布插件");
|
||||
if (!canEditStorePlugin(plugin.developerId, bindUserId)) {
|
||||
throw new Error("当前绑定账号无权编辑该插件");
|
||||
}
|
||||
|
||||
const authorReply = await this.getOnlinePluginAuthor();
|
||||
const author = authorReply?.author;
|
||||
const reply: OnlinePluginPublishInfo = {
|
||||
localPlugin: {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
title: plugin.title,
|
||||
pluginType: plugin.pluginType,
|
||||
group: plugin.group,
|
||||
version: plugin.version,
|
||||
desc: plugin.desc,
|
||||
icon: plugin.icon,
|
||||
},
|
||||
authorRegistered: !!authorReply?.registered && !!author?.id,
|
||||
author,
|
||||
versions: [],
|
||||
};
|
||||
if (!reply.authorRegistered || !author?.name) {
|
||||
return reply;
|
||||
}
|
||||
|
||||
const fullName = `${author.name}/${plugin.name}`;
|
||||
try {
|
||||
const detail = await this.onlinePluginDetail({
|
||||
fullName,
|
||||
});
|
||||
reply.marketPlugin = detail?.plugin;
|
||||
reply.versions = detail?.versions || [];
|
||||
} catch (e) {
|
||||
logger.warn("get online plugin detail failed", e);
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
private async saveOnlinePluginSyncTime(syncTime: number) {
|
||||
const setting = await this.sysSettingsService.getSetting<SysPluginSetting>(SysPluginSetting);
|
||||
setting.lastSyncTime = syncTime;
|
||||
await this.sysSettingsService.saveSetting(setting);
|
||||
}
|
||||
|
||||
private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) {
|
||||
if (!this.pluginMarketItemRepository || !fullName || downloadCount == null) {
|
||||
if (!fullName || downloadCount == null) {
|
||||
return;
|
||||
}
|
||||
await this.pluginMarketItemRepository.update(
|
||||
await this.repository.update(
|
||||
{
|
||||
type: "store",
|
||||
fullName,
|
||||
},
|
||||
{
|
||||
@@ -619,10 +1061,14 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
if (item.type === "builtIn") {
|
||||
return;
|
||||
}
|
||||
let name = item.name;
|
||||
if (item.author && !item.name.startsWith(`${item.author}/`)) {
|
||||
name = `${item.author}/${item.name}`;
|
||||
}
|
||||
const metadata = item.metadata ? yaml.load(item.metadata) : {};
|
||||
const extra = item.extra ? yaml.load(item.extra) : {};
|
||||
const plugin = {
|
||||
...item,
|
||||
...metadata,
|
||||
...extra,
|
||||
};
|
||||
const name = getPluginRegistryName(plugin);
|
||||
if (item.pluginType === "access") {
|
||||
accessRegistry.unRegister(name);
|
||||
} else if (item.pluginType === "deploy") {
|
||||
@@ -645,6 +1091,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
}
|
||||
|
||||
async update(param: any) {
|
||||
param.type = normalizePluginSourceType(param.type);
|
||||
const old = await this.repository.findOne({
|
||||
where: {
|
||||
name: param.name,
|
||||
@@ -730,6 +1177,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
bq.andWhere("type != :type", {
|
||||
type: "builtIn",
|
||||
});
|
||||
this.addInstalledPluginOnlyQuery(bq);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -771,13 +1219,9 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
delete item.metadata;
|
||||
delete item.content;
|
||||
delete item.extra;
|
||||
if (item.author && !item.name.startsWith(`${item.author}/`)) {
|
||||
item.name = item.author + "/" + item.name;
|
||||
}
|
||||
let name = item.name;
|
||||
if (item.addonType) {
|
||||
name = item.addonType + ":" + name;
|
||||
}
|
||||
const fullName = getPluginFullName(item);
|
||||
item.name = fullName;
|
||||
const name = getPluginRegistryName(item);
|
||||
let registry = null;
|
||||
if (item.pluginType === "access") {
|
||||
registry = accessRegistry;
|
||||
@@ -800,7 +1244,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
if (item.type === "builtIn") {
|
||||
return await this.getPluginClassFromFile(item);
|
||||
} else {
|
||||
return await this.getPluginClassFromDb(name);
|
||||
return await this.getPluginClassFromDb(fullName);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -813,6 +1257,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
bq.andWhere("type != :type", {
|
||||
type: "builtIn",
|
||||
});
|
||||
this.addInstalledPluginOnlyQuery(bq);
|
||||
},
|
||||
});
|
||||
const list = [...builtInList];
|
||||
@@ -855,12 +1300,30 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
}
|
||||
delete loaded.id;
|
||||
|
||||
const old = await this.repository.findOne({
|
||||
where: {
|
||||
name: loaded.name,
|
||||
author: loaded.author,
|
||||
},
|
||||
});
|
||||
const fullName = loaded.fullName || (loaded.author && loaded.name ? `${loaded.author}/${loaded.name}` : "");
|
||||
const entityType = normalizePluginSourceType(req.type || loaded.type);
|
||||
|
||||
const old =
|
||||
entityType === "store"
|
||||
? await this.repository.findOne({
|
||||
where: [
|
||||
{
|
||||
type: "store",
|
||||
fullName,
|
||||
},
|
||||
{
|
||||
type: "store",
|
||||
author: loaded.author,
|
||||
name: loaded.name,
|
||||
},
|
||||
],
|
||||
})
|
||||
: await this.repository.findOne({
|
||||
where: {
|
||||
name: loaded.name,
|
||||
author: loaded.author,
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = {
|
||||
input: loaded.input,
|
||||
@@ -875,11 +1338,19 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
|
||||
const pluginEntity = {
|
||||
...loaded,
|
||||
type: req.type || loaded.type || "custom",
|
||||
appId: entityType === "store" ? (old?.appId ?? loaded.appId) : loaded.appId,
|
||||
developerId: entityType === "store" ? (old?.developerId ?? loaded.developerId) : loaded.developerId,
|
||||
fullName: entityType === "store" ? fullName || old?.fullName : old?.fullName,
|
||||
type: entityType,
|
||||
metadata: yaml.dump(metadata),
|
||||
extra: yaml.dump(extra),
|
||||
content: loaded.content,
|
||||
disabled: false,
|
||||
installed: true,
|
||||
latest: old?.latest ?? loaded.latest,
|
||||
status: old?.status ?? loaded.status,
|
||||
downloadCount: old?.downloadCount ?? loaded.downloadCount,
|
||||
syncTime: old?.syncTime ?? loaded.syncTime,
|
||||
disabled: old?.disabled ?? false,
|
||||
};
|
||||
if (!pluginEntity.pluginType) {
|
||||
throw new Error(`插件类型不能为空`);
|
||||
@@ -904,7 +1375,21 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
async deleteByIds(ids: any[]) {
|
||||
ids = this.filterIds(ids);
|
||||
for (const id of ids) {
|
||||
const item = await this.info(id);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
await this.unRegisterById(id);
|
||||
if (item.type === "store" && item.developerId) {
|
||||
await this.repository.update(
|
||||
{ id },
|
||||
{
|
||||
installed: false,
|
||||
disabled: false,
|
||||
}
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await this.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user