jcloud/dashboard/src2/objects/marketplace.js
2025-04-12 17:39:38 +08:00

658 lines
16 KiB
JavaScript

import { defineAsyncComponent, h } from 'vue';
import { Button, Badge } from 'jingrow-ui';
import { toast } from 'vue-sonner';
import ChangeAppBranchDialog from '../components/marketplace/ChangeAppBranchDialog.vue';
import { confirmDialog, icon, renderDialog } from '../utils/components';
import PlansDialog from '../components/marketplace/PlansDialog.vue';
import CodeReview from '../components/marketplace/CodeReview.vue';
import GenericDialog from '../components/GenericDialog.vue';
import ObjectList from '../components/ObjectList.vue';
import { userCurrency, currency } from '../utils/format';
import { getToastErrorMessage } from '../utils/toast';
import { isMobile } from '../utils/device';
import router from '../router';
export default {
pagetype: 'Marketplace App',
whitelistedMethods: {
removeVersion: 'remove_version',
addVersion: 'add_version',
siteInstalls: 'site_installs',
createApprovalRequest: 'create_approval_request',
cancelApprovalRequest: 'cancel_approval_request',
updateListing: 'update_listing',
markAppReadyForReview: 'mark_app_ready_for_review'
},
list: {
route: '/apps',
title: '应用市场',
fields: ['image', 'title', 'status', 'description'],
columns: [
{
label: '应用',
fieldname: 'title',
class: 'font-medium',
width: 0.3,
prefix(row) {
return row.image
? h('img', {
src: row.image,
class: 'w-6 h-6 rounded-sm',
alt: row.title
})
: h(
'div',
{
class:
'w-6 h-6 rounded bg-gray-300 text-gray-600 flex items-center justify-center'
},
row.title[0].toUpperCase()
);
}
},
{
label: '状态',
type: 'Badge',
fieldname: 'status',
width: 0.3
},
{
label: '描述',
fieldname: 'description',
width: 1.0
}
],
primaryAction() {
return {
label: '新建应用',
variant: 'solid',
slots: {
prefix: icon('plus')
},
onClick() {
const NewMarketplaceAppDialog = defineAsyncComponent(() =>
import('../components/marketplace/NewMarketplaceAppDialog.vue')
);
renderDialog(h(NewMarketplaceAppDialog));
}
};
}
},
detail: {
titleField: 'name',
route: '/apps/:name',
statusBadge({ documentResource: app }) {
return { label: app.pg.status };
},
breadcrumbs({ items, documentResource: app }) {
return [
items[0],
{
label: app.pg.title,
route: `/apps/${app.pg.name}`
}
];
},
tabs: [
{
label: '分析',
icon: icon('bar-chart-2'),
route: 'analytics',
type: 'Component',
component: defineAsyncComponent(() =>
import('../components/marketplace/MarketplaceAppAnalytics.vue')
),
props: app => {
return { app: app.pg.name };
}
},
{
label: '列表',
icon: icon('shopping-cart'),
route: 'listing',
type: 'Component',
component: defineAsyncComponent(() =>
import('../components/MarketplaceAppListing.vue')
),
props: app => {
return { app: app };
}
},
{
label: '版本',
icon: icon('package'),
route: 'versions',
type: 'list',
list: {
pagetype: 'Marketplace App Version',
filters: app => {
return { parent: app.pg.name, parenttype: 'Marketplace App' };
},
onRowClick: (row, context) => {
const { listResource: versions, documentResource: app } = context;
showReleases(row, app);
},
fields: [
'source.repository_owner as repository_owner',
'source.repository as repository',
'source.branch as branch'
],
columns: [
{
label: '版本',
fieldname: 'version',
width: 0.5
},
{
label: '来源',
fieldname: 'source',
width: 0.5
},
{
label: '仓库',
width: 0.5,
format: (value, row) => {
return `${row.repository_owner}/${row.repository}`;
}
},
{
label: '分支',
fieldname: 'branch',
type: 'Badge',
width: 0.5
}
],
primaryAction({ listResource: versions, documentResource: app }) {
return {
label: '新建版本',
slots: {
prefix: icon('plus')
},
onClick() {
renderDialog(
h(
GenericDialog,
{
options: {
title: `${app.pg.title} 添加版本支持`,
size: '4xl'
}
},
{
default: () =>
h(ObjectList, {
options: {
label: '版本',
fieldname: 'version',
fieldtype: 'ListSelection',
columns: [
{
label: '版本',
fieldname: 'version'
},
{
label: '分支',
type: 'Select',
fieldname: 'branch',
format: (value, row) => {
row.selectedOption = value[0];
return value.map(v => ({
label: v,
value: v,
onClick: () => {
row.selectedOption = v;
}
}));
}
},
{
label: '',
fieldname: '',
align: 'right',
type: 'Button',
width: '5rem',
Button({ row, listResource: versionsOptions }) {
return {
label: '添加',
onClick() {
if (app.addVersion.loading) return;
toast.promise(
app.addVersion.submit({
version: row.version,
branch: row.selectedOption
}),
{
loading: '正在添加新版本...',
success: () => {
versions.reload();
versionsOptions.reload();
return '新版本已添加';
},
error: e => getToastErrorMessage(e)
}
);
}
};
}
}
],
resource() {
return {
url: 'jcloud.api.marketplace.options_for_version',
params: {
name: app.pg.name
},
auto: true
};
}
}
})
}
)
);
}
};
},
rowActions({ row, listResource: versions, documentResource: app }) {
return [
{
label: '显示发布',
slots: {
prefix: icon('plus')
},
onClick() {
showReleases(row, app);
}
},
{
label: '更改分支',
onClick() {
renderDialog(
h(ChangeAppBranchDialog, {
app: app.pg.name,
source: row.source,
version: row.version,
activeBranch: row.branch,
onBranchChanged() {
versions.reload();
}
})
);
}
},
{
label: '移除版本',
onClick() {
toast.promise(
app.removeVersion.submit({ version: row.version }),
{
loading: '正在移除版本...',
success: () => {
versions.reload();
return '版本已成功移除';
},
error: e => getToastErrorMessage(e)
}
);
}
}
];
}
}
},
{
label: '定价',
icon: icon('dollar-sign'),
route: 'pricing',
type: 'list',
list: {
pagetype: 'Marketplace App Plan',
filters: app => {
return { app: app.pg.name };
},
fields: ['name', 'title', 'price_cny', 'price_usd', 'enabled'],
columns: [
{
label: '标题',
fieldname: 'title'
},
{
label: '启用',
type: 'Badge',
fieldname: 'enabled',
format: value => {
return value == 1 ? '已启用' : '已禁用';
}
},
{
label: '价格 (CNY)',
fieldname: 'price_cny',
format: value => {
return currency(value, 'CNY');
}
},
{
label: '价格 (USD)',
fieldname: 'price_usd',
format: value => {
return currency(value, 'USD');
}
}
],
primaryAction({ listResource: plans, documentResource: app }) {
return {
label: '新建计划',
slots: {
prefix: icon('plus')
},
onClick() {
renderDialog(
h(PlansDialog, {
app: app.pg.name,
onPlanCreated() {
plans.reload();
}
})
);
}
};
},
rowActions({ row, listResource: plans, documentResource: app }) {
return [
{
label: '编辑',
onClick() {
renderDialog(
h(PlansDialog, {
app: app.pg.name,
plan: row,
onPlanUpdated() {
plans.reload();
}
})
);
}
}
];
}
}
},
{
label: '订阅',
icon: icon('users'),
route: 'subscription',
type: 'list',
list: {
pagetype: 'Subscription',
filters: app => {
return {
document_type: 'Marketplace App',
document_name: app.name
};
},
fields: ['site', 'enabled', 'team'],
filterControls() {
return [
{
type: 'select',
label: '状态',
class: !isMobile() ? 'w-24' : '',
fieldname: 'enabled',
options: ['', '激活', '禁用']
}
];
},
columns: [
{
label: '站点',
fieldname: 'site',
width: 0.6
},
{
label: '状态',
type: 'Badge',
fieldname: 'enabled',
width: 0.3,
format: value => {
return value ? '激活' : '禁用';
}
},
{
label: '价格',
fieldname: 'price',
width: 0.3,
format: value => {
return userCurrency(value);
}
},
{
label: '已激活天数',
fieldname: 'active_for',
width: 0.3,
format: value => {
return value + (value == 1 ? ' 天' : ' 天');
}
}
]
}
}
],
actions({ documentResource: app }) {
return [
{
label: '在市场中查看',
slots: {
prefix: icon('external-link')
},
condition: () => app.pg.status === 'Published',
onClick() {
window.open(
`${window.location.origin}/marketplace/apps/${app.name}`,
'_blank'
);
}
},
{
label: '指南',
slots: {
icon: icon('help-circle')
},
condition: () => app.pg.status === 'Draft',
onClick() {
window.open(
'https://jingrow.com/docs/marketplace/marketplace-guidelines',
'_blank'
);
}
},
{
label: '完成列表',
variant: 'solid',
slots: {
prefix: icon('alert-circle')
},
condition: () => app.pg.status === 'Draft',
onClick() {
let AppListingStepsDialog = defineAsyncComponent(() =>
import('../components/marketplace/AppListingStepsDialog.vue')
);
renderDialog(
h(AppListingStepsDialog, {
app: app.pg.name
})
);
}
},
{
label: '选项',
condition: () => app.pg.status === 'Draft',
options: [
{
label: '删除',
icon: icon('trash-2'),
condition: () => app.pg.status === 'Draft',
onClick() {
confirmDialog({
title: `删除应用 ${app.pg.title}`,
message: '您确定要删除此应用吗?',
onSuccess({ hide }) {
toast.promise(app.delete.submit(), {
loading: '正在删除应用...',
success: () => {
hide();
router.push({ name: 'Marketplace App List' });
return '应用删除成功';
},
error: e => getToastErrorMessage(e)
});
}
});
}
}
]
}
];
}
}
};
function showReleases(row, app) {
renderDialog(
h(
GenericDialog,
{
options: {
title: `${app.pg.name}${row.branch} 分支的发布`,
size: '6xl'
}
},
{
default: () =>
h(ObjectList, {
options: {
label: '版本',
type: 'list',
pagetype: 'App Release',
filters: {
app: app.pg.name,
source: row.source
},
fields: ['message', 'tag', 'author', 'status'],
orderBy: 'creation desc',
columns: [
{
label: '提交信息',
fieldname: 'message',
class: 'w-64',
width: 0.5
},
{
label: '哈希值',
fieldname: 'hash',
class: 'w-24',
type: 'Badge',
width: 0.2,
format: value => {
return value.slice(0, 7);
}
},
{
label: '作者',
fieldname: 'author',
width: 0.2
},
{
label: '状态',
fieldname: 'status',
type: 'Badge',
width: 0.3
},
{
label: '代码审查',
type: 'Component',
width: 0.2,
component: ({ row, listResource: releases, app }) => {
if (
(row.status === 'Awaiting Approval' ||
row.status === 'Rejected') &&
row.screening_status === 'Complete'
) {
return h(Button, {
label: '代码审查',
variant: 'subtle',
theme: 'blue',
size: 'sm',
onClick: () =>
codeReview(row, app, window.is_system_user)
});
}
return h(Badge, {
label: row.screening_status || '未开始'
});
}
},
{
label: '',
fieldname: '',
align: 'right',
type: 'Button',
width: 0.2,
Button({ row, listResource: releases }) {
let label = '';
let successMessage = '';
let loadingMessage = '';
if (row.status === 'Awaiting Approval') {
label = '取消';
successMessage = '发布已取消';
loadingMessage = '正在取消发布...';
} else if (row.status === 'Draft') {
label = '提交';
loadingMessage = '正在提交发布以供审批...';
successMessage =
'发布已提交以供审批';
}
return {
label: label,
onClick() {
toast.promise(
row.status === 'Awaiting Approval'
? app.cancelApprovalRequest.submit({
app_release: row.name
})
: app.createApprovalRequest.submit({
app_release: row.name
}),
{
loading: loadingMessage,
success: () => {
releases.reload();
return successMessage;
},
error: e => getToastErrorMessage(e)
}
);
}
};
}
}
]
}
})
}
)
);
}
function codeReview(row, app, isSystemUser) {
renderDialog(
h(CodeReview, {
row: row,
app: app,
isSystemUser: isSystemUser
})
);
}