658 lines
16 KiB
JavaScript
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
|
|
})
|
|
);
|
|
} |