mirror of
https://github.com/certd/certd.git
synced 2026-08-09 08:35:49 +08:00
perf(plugin): 新增插件评分展示、排序与个人插件筛选功能
This commit is contained in:
@@ -1 +1 @@
|
||||
export * from './lib/iframe.client.js';
|
||||
export * from "./lib/iframe.client.js";
|
||||
|
||||
@@ -70,6 +70,9 @@ export class IframeClient {
|
||||
this.requestQueue = {};
|
||||
this.handlers = {};
|
||||
}
|
||||
public close() {
|
||||
this.destroy();
|
||||
}
|
||||
public isInFrame() {
|
||||
return window.self !== window.top;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.964.0",
|
||||
"@certd/vue-js-cron-light": "^4.0.14",
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@fast-crud/editor-code": "^1.28.3",
|
||||
"@fast-crud/fast-crud": "^1.28.3",
|
||||
"@fast-crud/fast-extends": "^1.28.3",
|
||||
"@fast-crud/ui-antdv4": "^1.28.3",
|
||||
"@fast-crud/ui-interface": "^1.28.3",
|
||||
"@fast-crud/editor-code": "^1.28.7",
|
||||
"@fast-crud/fast-crud": "^1.28.7",
|
||||
"@fast-crud/fast-extends": "^1.28.7",
|
||||
"@fast-crud/ui-antdv4": "^1.28.7",
|
||||
"@fast-crud/ui-interface": "^1.28.7",
|
||||
"@iconify/tailwind": "^1.2.0",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@manypkg/get-packages": "^2.2.2",
|
||||
|
||||
@@ -256,3 +256,9 @@ button.ant-btn.ant-btn-default.isPlus {
|
||||
.fs-form-content{display: none;}
|
||||
}
|
||||
}
|
||||
|
||||
.fs-actionbar{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap:4px;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ defineOptions({
|
||||
name: "AccountBind",
|
||||
});
|
||||
const iframeRef = ref();
|
||||
let iframeClient: IframeClient | undefined;
|
||||
let iframeClient: IframeClient = undefined;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const settingStore = useSettingStore();
|
||||
@@ -89,7 +89,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
iframeClient?.destroy();
|
||||
iframeClient?.close();
|
||||
iframeClient = undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
+1
-3
@@ -51,7 +51,7 @@ const iframeSrc = computed(() => {
|
||||
return `${baseUrl}/#/app/certd/plugin/${plugin.id || 0}?${params.toString()}`;
|
||||
});
|
||||
|
||||
let iframeClient: IframeClient;
|
||||
let iframeClient: IframeClient = undefined;
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
@@ -71,12 +71,10 @@ watch(
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
//@ts-ignore
|
||||
iframeClient?.destroy();
|
||||
});
|
||||
|
||||
function setupIframeClient() {
|
||||
//@ts-ignore
|
||||
iframeClient?.destroy();
|
||||
if (!iframeRef.value) {
|
||||
return;
|
||||
|
||||
@@ -118,6 +118,12 @@
|
||||
<span>{{ formatDownloadCount(plugin.downloadCount || 0) }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="`平均评分 ${formatScore(plugin.score)} 星`">
|
||||
<a-tag class="plugin-card__score">
|
||||
<fs-icon icon="ion:star" />
|
||||
<span>{{ formatScore(plugin.score) }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tooltip v-if="!isBuiltInPlugin" :title="versionTitle">
|
||||
<span class="plugin-card__version" :class="{ 'is-upgradable': plugin.upgradeAvailable }" @click.stop="handleVersionClick">
|
||||
@@ -506,6 +512,14 @@ function formatDownloadCount(count: number) {
|
||||
return `${count}`;
|
||||
}
|
||||
|
||||
function formatScore(score?: number) {
|
||||
const value = Number(score || 0);
|
||||
if (!Number.isFinite(value)) {
|
||||
return "0";
|
||||
}
|
||||
return value.toFixed(1).replace(/\.0$/, "");
|
||||
}
|
||||
|
||||
function handleVersionClick() {
|
||||
if (props.source === "local" || !props.plugin.upgradeAvailable) {
|
||||
return;
|
||||
@@ -856,6 +870,19 @@ function handleVersionClick() {
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__score {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #d48806;
|
||||
|
||||
.fs-icon {
|
||||
color: #faad14;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-card__ai-check-icon {
|
||||
flex: none;
|
||||
color: #52c41a;
|
||||
@@ -924,6 +951,10 @@ function handleVersionClick() {
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.plugin-card__score {
|
||||
color: #ffc53d;
|
||||
}
|
||||
|
||||
.plugin-card__meta {
|
||||
border-top-color: #303030;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
query.page.offset = 0;
|
||||
}
|
||||
lastType = query?.query?.type;
|
||||
const queryData = query.query || {};
|
||||
const sortBy = queryData.sortBy;
|
||||
delete queryData.sortBy;
|
||||
if (sortBy === "score" || sortBy === "downloadCount") {
|
||||
query.sort = {
|
||||
prop: sortBy,
|
||||
asc: false,
|
||||
};
|
||||
} else {
|
||||
delete query.sort;
|
||||
}
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
@@ -218,6 +229,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
},
|
||||
remove: {
|
||||
order: 999,
|
||||
//@ts-ignore
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom" || row.type === "store";
|
||||
}),
|
||||
@@ -227,6 +239,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
icon: "ion:cloud-download-outline",
|
||||
title: t("certd.export"),
|
||||
type: "link",
|
||||
//@ts-ignore
|
||||
show: compute(({ row }) => {
|
||||
return canEditStorePlugin(row);
|
||||
}),
|
||||
@@ -283,6 +296,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
component: {
|
||||
disabled: false,
|
||||
},
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
order: 0,
|
||||
@@ -334,6 +350,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "text",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: true,
|
||||
@@ -367,6 +386,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
vModel: "value",
|
||||
},
|
||||
show: true,
|
||||
col: {
|
||||
span: 2,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: true,
|
||||
@@ -380,9 +402,60 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
onlyMine: {
|
||||
title: "只看我的",
|
||||
type: "dict-switch",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 2,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
show: false,
|
||||
},
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "否", value: false },
|
||||
{ label: "是", value: true },
|
||||
],
|
||||
}),
|
||||
},
|
||||
sortBy: {
|
||||
title: "排序",
|
||||
type: "dict-select",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
show: false,
|
||||
},
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "默认排序", value: "" },
|
||||
{ label: "评分最高", value: "score" },
|
||||
{ label: "下载最多", value: "downloadCount" },
|
||||
],
|
||||
}),
|
||||
},
|
||||
title: {
|
||||
title: t("certd.titlea"),
|
||||
type: "text",
|
||||
search: {
|
||||
show: false,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
helper: t("certd.titleHelper"),
|
||||
rules: [{ required: true }],
|
||||
@@ -408,6 +481,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "dict-select",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
component: {
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
value: "store",
|
||||
@@ -564,6 +643,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "dict-select",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
dict: dict({
|
||||
url: "/pi/plugin/groupsList",
|
||||
|
||||
@@ -286,6 +286,49 @@ describe("PluginService online plugins", () => {
|
||||
assert.equal((service.repository as any).state.findCalls, 1);
|
||||
});
|
||||
|
||||
it("filters market plugins by the current bound developer when onlyMine is enabled", async () => {
|
||||
const service = new PluginService();
|
||||
service.sysSettingsService = {
|
||||
async getSetting() {
|
||||
return { bindUserId: 12 };
|
||||
},
|
||||
} as any;
|
||||
service.repository = createPluginRepositoryMock({
|
||||
marketRows: [
|
||||
{
|
||||
id: 1,
|
||||
type: "store",
|
||||
developerId: 12,
|
||||
fullName: "developer/owned-plugin",
|
||||
author: "developer",
|
||||
name: "owned-plugin",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "store",
|
||||
developerId: 20,
|
||||
fullName: "other/other-plugin",
|
||||
author: "other",
|
||||
name: "other-plugin",
|
||||
},
|
||||
],
|
||||
}) as any;
|
||||
|
||||
const page = await service.page({
|
||||
query: {
|
||||
type: "store",
|
||||
onlyMine: true,
|
||||
} as any,
|
||||
page: {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(page.total, 1);
|
||||
assert.equal(page.records[0].fullName, "developer/owned-plugin");
|
||||
});
|
||||
|
||||
it("does not treat uninstalled market rows as installed plugins", async () => {
|
||||
const service = new PluginService();
|
||||
service.repository = createPluginRepositoryMock({
|
||||
@@ -334,14 +377,14 @@ describe("PluginService online plugins", () => {
|
||||
total: 201,
|
||||
},
|
||||
list: Array.from({ length: 200 }, (_, index) => ({
|
||||
author: "developer",
|
||||
name: `plugin-${index + 1}`,
|
||||
pluginType: "deploy",
|
||||
title: `plugin ${index + 1}`,
|
||||
latest: "1.0.0",
|
||||
status: "published",
|
||||
aiCheckStatus: "passed",
|
||||
})),
|
||||
author: "developer",
|
||||
name: `plugin-${index + 1}`,
|
||||
pluginType: "deploy",
|
||||
title: `plugin ${index + 1}`,
|
||||
latest: "1.0.0",
|
||||
status: "published",
|
||||
aiCheckStatus: "passed",
|
||||
})),
|
||||
};
|
||||
}
|
||||
assert.equal(config.data.page.start, 200);
|
||||
@@ -624,6 +667,77 @@ describe("PluginService online plugins", () => {
|
||||
assert.equal(updatedPlugin.installed, true);
|
||||
});
|
||||
|
||||
it("reuses a legacy custom plugin when switching an online plugin version", async () => {
|
||||
const service = new PluginService();
|
||||
const findOptions: any[] = [];
|
||||
let updatedPlugin: any;
|
||||
|
||||
(service as any).findOne = async (options: any) => {
|
||||
findOptions.push(options);
|
||||
if (findOptions.length === 1) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: 7,
|
||||
type: "custom",
|
||||
author: "greper2",
|
||||
name: "BaishanUpdateCert666",
|
||||
};
|
||||
};
|
||||
service.update = async plugin => {
|
||||
updatedPlugin = plugin;
|
||||
};
|
||||
|
||||
await service.importPlugin({
|
||||
type: "store",
|
||||
override: true,
|
||||
content: "name: BaishanUpdateCert666\npluginType: deploy\nauthor: greper2\ncontent: |\n return class Demo {}\n",
|
||||
});
|
||||
|
||||
assert.equal(findOptions.length, 2);
|
||||
assert.deepEqual(findOptions[1].where, {
|
||||
author: "greper2",
|
||||
name: "BaishanUpdateCert666",
|
||||
});
|
||||
assert.equal(updatedPlugin.id, 7);
|
||||
assert.equal(updatedPlugin.type, "store");
|
||||
assert.equal(updatedPlugin.installed, true);
|
||||
});
|
||||
|
||||
it("updates a store plugin without conflicting with a legacy custom plugin of the same name", async () => {
|
||||
const service = new PluginService();
|
||||
let savedPlugin: any;
|
||||
|
||||
(service as any).findOne = async (options: any) => {
|
||||
assert.deepEqual(options.where, {
|
||||
type: "store",
|
||||
fullName: "greper2/BaishanUpdateCert666",
|
||||
});
|
||||
return {
|
||||
id: 7,
|
||||
type: "store",
|
||||
fullName: "greper2/BaishanUpdateCert666",
|
||||
};
|
||||
};
|
||||
service.unRegisterById = async () => {};
|
||||
service.registerById = async () => {};
|
||||
service.repository = {
|
||||
async save(plugin: any) {
|
||||
savedPlugin = plugin;
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.update({
|
||||
id: 7,
|
||||
type: "store",
|
||||
fullName: "greper2/BaishanUpdateCert666",
|
||||
author: "greper2",
|
||||
name: "BaishanUpdateCert666",
|
||||
});
|
||||
|
||||
assert.equal(savedPlugin.id, 7);
|
||||
});
|
||||
|
||||
it("marks an uninstalled market plugin as installed after importing it again", async () => {
|
||||
const service = new PluginService();
|
||||
let updatedPlugin: any;
|
||||
|
||||
@@ -303,12 +303,28 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
// 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";
|
||||
const query = pageReq.query as Partial<PluginEntity> & { onlyMine?: boolean };
|
||||
const onlyMine = query.onlyMine === true;
|
||||
delete query.onlyMine;
|
||||
if (onlyMine) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
if (!installInfo.bindUserId) {
|
||||
return {
|
||||
records: [],
|
||||
total: 0,
|
||||
offset: pageReq.page.offset,
|
||||
limit: pageReq.page.limit,
|
||||
};
|
||||
}
|
||||
query.type = "store";
|
||||
query.developerId = installInfo.bindUserId;
|
||||
}
|
||||
if (pageReq.query.type && pageReq.query.type !== "builtIn") {
|
||||
if (query.type === "custom") {
|
||||
query.type = "store";
|
||||
}
|
||||
if (query.type && query.type !== "builtIn") {
|
||||
const pageRes = await super.page(pageReq);
|
||||
if (pageReq.query.type === "store") {
|
||||
if (query.type === "store") {
|
||||
pageRes.records = (await this.attachOnlineInstallState(pageRes.records.map(item => this.toOnlinePluginBean(item)))) as any;
|
||||
}
|
||||
return pageRes;
|
||||
@@ -1038,12 +1054,22 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
|
||||
async update(param: any) {
|
||||
param.type = normalizePluginSourceType(param.type);
|
||||
const old = await this.findOne({
|
||||
where: {
|
||||
name: param.name,
|
||||
author: param.author,
|
||||
},
|
||||
});
|
||||
let old: PluginEntity | null;
|
||||
if (param.type === "store" && param.fullName) {
|
||||
old = await this.findOne({
|
||||
where: {
|
||||
type: "store",
|
||||
fullName: param.fullName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
old = await this.findOne({
|
||||
where: {
|
||||
name: param.name,
|
||||
author: param.author,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (old && old.id !== param.id) {
|
||||
throw new Error(`插件${param.author}/${param.name}已存在`);
|
||||
@@ -1264,6 +1290,15 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!old) {
|
||||
// 兼容旧版本仍标记为 custom 的在线插件,覆盖安装时应复用原记录。
|
||||
old = await this.findOne({
|
||||
where: {
|
||||
author: loaded.author,
|
||||
name: loaded.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
old = await this.findOne({
|
||||
where: {
|
||||
|
||||
+4
-4
@@ -2,11 +2,11 @@ packages:
|
||||
- 'packages/**' # <--------------开发插件请注释掉这一行,PR时本修改不要提交
|
||||
- 'packages/ui/**'
|
||||
- '!**/test/**'
|
||||
- '!packages/libs/libs/lib-huawei'
|
||||
- '!packages/libs/libs/lib-iframe'
|
||||
- '!packages/libs/libs/lib-jdcloud'
|
||||
# - '!packages/libs/libs/lib-huawei'
|
||||
# - '!packages/libs/libs/lib-iframe'
|
||||
# - '!packages/libs/libs/lib-jdcloud'
|
||||
# - '!packages/libs/libs/lib-k8s' # k8s引用了basic
|
||||
- '!packages/libs/libs/midway-flyway-js'
|
||||
# - '!packages/libs/libs/midway-flyway-js'
|
||||
|
||||
# 禁止放开这里的注释,不要配置allowBuilds
|
||||
# allowBuilds:
|
||||
|
||||
Reference in New Issue
Block a user