perf(plugin): 新增插件评分展示、排序与个人插件筛选功能

This commit is contained in:
xiaojunnuo
2026-08-02 17:04:02 +08:00
parent 0bf323ab97
commit 7ab502991e
11 changed files with 302 additions and 33 deletions
+1 -1
View File
@@ -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;
}
+5 -5
View File
@@ -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>
@@ -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
View File
@@ -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: