mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-31 22:54:11 +00:00
Subscription (#6069)
* subscription api * subscription page * subscriptionAPI.js * buy quota * add currency icons --------- Co-authored-by: Michael An <2331806369@qq.com>
This commit is contained in:
523
frontend/src/components/subscription.js
Normal file
523
frontend/src/components/subscription.js
Normal file
@@ -0,0 +1,523 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import toaster from './toast';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText, Input, Button } from 'reactstrap';
|
||||
import { gettext, serviceURL } from '../utils/constants';
|
||||
import { Utils } from '../utils/utils';
|
||||
import { subscriptionAPI } from '../utils/subscription-api';
|
||||
import Loading from './loading';
|
||||
|
||||
import '../css/layout.css';
|
||||
import '../css/subscription.css';
|
||||
|
||||
const {
|
||||
isOrgContext,
|
||||
} = window.app.pageOptions;
|
||||
|
||||
|
||||
const PlansPropTypes = {
|
||||
plans: PropTypes.array.isRequired,
|
||||
onPay: PropTypes.func.isRequired,
|
||||
paymentType: PropTypes.string.isRequired,
|
||||
handleContentScroll: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class Plans extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentPlan: props.plans[0],
|
||||
assetQuotaUnitCount: 1,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
togglePlan = (plan) => {
|
||||
this.setState({currentPlan: plan}, () => {
|
||||
});
|
||||
};
|
||||
|
||||
onPay = () => {
|
||||
let { paymentType } = this.props;
|
||||
let { currentPlan, assetQuotaUnitCount, count } = this.state;
|
||||
let totalAmount, assetQuota, newUserCount;
|
||||
|
||||
// parse
|
||||
if (paymentType === 'paid') {
|
||||
newUserCount = currentPlan.count;
|
||||
totalAmount = currentPlan.total_amount;
|
||||
} else if (paymentType === 'extend_time') {
|
||||
newUserCount = currentPlan.count;
|
||||
assetQuota = currentPlan.asset_quota;
|
||||
totalAmount = currentPlan.total_amount;
|
||||
} else if (paymentType === 'add_user') {
|
||||
newUserCount = count;
|
||||
totalAmount = count * currentPlan.price_per_user;
|
||||
} else if (paymentType === 'buy_quota') {
|
||||
assetQuota = (assetQuotaUnitCount) * currentPlan.asset_quota_unit;
|
||||
totalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
|
||||
} else {
|
||||
toaster.danger(gettext('Internal Server Error.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onPay(currentPlan.plan_id, newUserCount, assetQuota, totalAmount);
|
||||
};
|
||||
|
||||
onCountInputChange = (e) => {
|
||||
let { currentPlan } = this.state;
|
||||
if (!currentPlan.can_custom_count) {
|
||||
return;
|
||||
}
|
||||
let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
|
||||
if (count < 1) {
|
||||
count = 1;
|
||||
} else if (count > 9999) {
|
||||
count = 9999;
|
||||
}
|
||||
this.setState({count: count});
|
||||
};
|
||||
|
||||
onAssetQuotaUnitCountInputChange = (e) => {
|
||||
let { currentPlan } = this.state;
|
||||
if (!currentPlan.can_custom_asset_quota) {
|
||||
return;
|
||||
}
|
||||
let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
|
||||
if (count < 1) {
|
||||
count = 1;
|
||||
} else if (count > 9999) {
|
||||
count = 9999;
|
||||
}
|
||||
this.setState({assetQuotaUnitCount: count});
|
||||
};
|
||||
|
||||
renderPaidOrExtendTime = () => {
|
||||
let { plans, paymentType } = this.props;
|
||||
let { currentPlan } = this.state;
|
||||
let boughtQuota = 0;
|
||||
if (paymentType === 'extend_time') {
|
||||
boughtQuota = currentPlan.asset_quota - 100;
|
||||
}
|
||||
let totalAmount = currentPlan.total_amount;
|
||||
let originalTotalAmount = totalAmount;
|
||||
return (
|
||||
<div className='d-flex flex-column subscription-container'>
|
||||
<span className="subscription-subtitle">{'选择方案'}</span>
|
||||
<dl className='items-dl'>
|
||||
{plans.map((item, index) => {
|
||||
let selectedCss = item.plan_id === currentPlan.plan_id ? 'plan-selected' : '';
|
||||
let countDescription = '¥' + item.price_per_user;
|
||||
if (isOrgContext) {
|
||||
countDescription += '/每用户';
|
||||
}
|
||||
return (
|
||||
<dd key={index} className={`plan-description-item ${selectedCss}`} onClick={this.togglePlan.bind(this, item)}>
|
||||
<span className='plan-name'>{item.name}</span>
|
||||
<span className='plan-description'>{countDescription}</span>
|
||||
</dd>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
|
||||
{paymentType === 'extend_time' && boughtQuota > 0 &&
|
||||
<Fragment>
|
||||
<span className="subscription-subtitle">{'增加空间'}</span>
|
||||
<dl className='items-dl'>
|
||||
<dd className='order-item order-item-top order-item-bottom subscription-list'>
|
||||
<span className='order-into'>{currentPlan.asset_quota_unit + 'GB x ' + (boughtQuota / currentPlan.asset_quota_unit)}</span>
|
||||
{/* 续费时候需要减去附赠的100GB */}
|
||||
<span className='order-value'>{'¥' + (boughtQuota / currentPlan.asset_quota_unit) * currentPlan.price_per_asset_quota_unit}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
<span className="subscription-subtitle">{'方案汇总'}</span>
|
||||
<dl className='items-dl'>
|
||||
<div>
|
||||
<dd className='order-item order-item-top'>
|
||||
<span className='order-into'>{'所选方案'}</span>
|
||||
<span className='order-value'>{currentPlan.name}</span>
|
||||
</dd>
|
||||
{isOrgContext &&
|
||||
<dd className='order-item'>
|
||||
<span className='order-into'>{'成员人数'}</span>
|
||||
<span className='order-value'>{currentPlan.count + '人'}</span>
|
||||
</dd>
|
||||
}
|
||||
<dd className='order-item'>
|
||||
<span className='order-into'>{'可用空间'}</span>
|
||||
<span className='order-value'>{'100GB(附赠)' + (boughtQuota > 0 ? '+' + boughtQuota + 'GB(扩充)' : '')}</span>
|
||||
</dd>
|
||||
<dd className='order-item order-item-bottom rounded-0'>
|
||||
<span className='order-into'>{'到期时间'}</span>
|
||||
<span className='order-value'>{currentPlan.new_term_end}</span>
|
||||
</dd>
|
||||
<dd className='order-item order-item-bottom subscription-list'>
|
||||
<span className='order-into'>{'实际支付金额'}</span>
|
||||
<span className='order-price'>
|
||||
{originalTotalAmount !== totalAmount &&
|
||||
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{'¥' + originalTotalAmount}</span>
|
||||
}
|
||||
<span>{'¥' + totalAmount + ' '}</span>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<Button className='subscription-submit' color="primary" onClick={this.onPay}>{'提交订单'}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderAddUser = () => {
|
||||
let { currentPlan, count } = this.state;
|
||||
let operationIntro = '新增用户';
|
||||
let originalTotalAmount = count * currentPlan.price_per_user;
|
||||
let totalAmount = originalTotalAmount;
|
||||
return (
|
||||
<div className='d-flex flex-column subscription-container price-version-container-header subscription-add-user'>
|
||||
<div className="price-version-container-top"></div>
|
||||
<h3 className='user-quota-plan-name py-5'>{currentPlan.name}</h3>
|
||||
<span className='py-2 mb-0 text-orange font-500 text-center'>
|
||||
{'¥ '}<span className="price-version-plan-price">{currentPlan.price}</span>{' ' + currentPlan.description}
|
||||
</span>
|
||||
<InputGroup style={{marginBottom: '5px'}} className='user-numbers'>
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>{operationIntro}</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
className="py-2"
|
||||
placeholder={operationIntro}
|
||||
title={operationIntro}
|
||||
type="number"
|
||||
value={count || 1}
|
||||
min="1"
|
||||
max="9999"
|
||||
disabled={!currentPlan.can_custom_count}
|
||||
onChange={this.onCountInputChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
<span className='py-2 text-orange mb-0 font-500 price-version-plan-whole-price text-center'>
|
||||
{'总价 ¥ ' + totalAmount}
|
||||
{originalTotalAmount !== totalAmount &&
|
||||
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{' ¥' + originalTotalAmount}</span>
|
||||
}
|
||||
</span>
|
||||
<span className='py-2 mb-0 text-lg-size font-500 price-version-plan-valid-day text-center'>{'有效期至 ' + currentPlan.new_term_end}</span>
|
||||
<span className='subscription-notice text-center py-5'>{'注:当有效期剩余天数少于计划中的时候,增加用户的价格按天来计算'}</span>
|
||||
<Button className='subscription-submit' onClick={this.onPay} color="primary">{'立即购买'}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderBuyQuota = () => {
|
||||
let { currentPlan, assetQuotaUnitCount } = this.state;
|
||||
let operationIntro = '新增空间';
|
||||
let originalTotalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
|
||||
let totalAmount = originalTotalAmount;
|
||||
return (
|
||||
<div className='d-flex flex-column subscription-container price-version-container-header subscription-add-space'>
|
||||
<div className="price-version-container-top"></div>
|
||||
<h3 className='user-quota-plan-name py-5'>{currentPlan.name}</h3>
|
||||
<span className='py-2 mb-0 text-orange font-500 text-center'>
|
||||
{'¥ '}<span className="price-version-plan-price">{currentPlan.asset_quota_price}</span>{' ' + currentPlan.asset_quota_description}
|
||||
</span>
|
||||
<InputGroup style={{marginBottom: '5px'}} className='space-quota'>
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText><span className="font-500">{operationIntro}</span></InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
className="py-2"
|
||||
placeholder={operationIntro}
|
||||
title={operationIntro}
|
||||
type="number"
|
||||
value={assetQuotaUnitCount || 1}
|
||||
min="1"
|
||||
max="9999"
|
||||
disabled={!currentPlan.can_custom_asset_quota}
|
||||
onChange={this.onAssetQuotaUnitCountInputChange}
|
||||
/>
|
||||
<InputGroupAddon addonType='append'>
|
||||
<InputGroupText><span className="font-500">{' x ' + currentPlan.asset_quota_unit + 'GB'}</span></InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<span className='py-4 text-orange mb-0 font-500 price-version-plan-whole-price text-center'>
|
||||
{'总价 ¥ ' + totalAmount}
|
||||
{originalTotalAmount !== totalAmount &&
|
||||
<span style={{fontSize: 'small', textDecoration: 'line-through', color: '#9a9a9a'}}>{' ¥' + originalTotalAmount}</span>
|
||||
}
|
||||
</span>
|
||||
<span className='py-2 mb-0 text-lg-size font-500 price-version-plan-valid-day text-center'>{'有效期至 ' + currentPlan.new_term_end}</span>
|
||||
<span className='subscription-notice text-center py-5'>{'注:当有效期剩余天数少于计划中的时候,增加空间的价格按天来计算'}</span>
|
||||
<Button className='subscription-submit' onClick={this.onPay} color="primary">{'立即购买'}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let { paymentType } = this.props;
|
||||
if (paymentType === 'paid' || paymentType === 'extend_time') {
|
||||
return this.renderPaidOrExtendTime();
|
||||
} else if (paymentType === 'add_user') {
|
||||
return this.renderAddUser();
|
||||
} else if (paymentType === 'buy_quota') {
|
||||
return this.renderBuyQuota();
|
||||
} else {
|
||||
toaster.danger(gettext('Internal Server Error.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Plans.propTypes = PlansPropTypes;
|
||||
|
||||
const PlansDialogPropTypes = {
|
||||
isOrgContext: PropTypes.bool.isRequired,
|
||||
paymentType: PropTypes.string.isRequired,
|
||||
paymentTypeTrans: PropTypes.string.isRequired,
|
||||
toggleDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class PlansDialog extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
isWaiting: false,
|
||||
planList: [],
|
||||
paymentSourceList: [],
|
||||
};
|
||||
}
|
||||
|
||||
getPlans = () => {
|
||||
subscriptionAPI.getSubscriptionPlans(this.props.paymentType).then((res) => {
|
||||
this.setState({
|
||||
planList: res.data.plan_list,
|
||||
paymentSourceList: res.data.payment_source_list,
|
||||
isLoading: false,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: errorMsg,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onPay = (planID, count, asset_quota, totalAmount) => {
|
||||
this.setState({ isWaiting: true });
|
||||
let payUrl = serviceURL + '/subscription/pay/?payment_source=' + this.state.paymentSourceList[0] +
|
||||
'&payment_type=' + this.props.paymentType + '&plan_id=' + planID +
|
||||
'&total_amount=' + totalAmount;
|
||||
if (count) {
|
||||
payUrl += '&count=' + count;
|
||||
}
|
||||
if (asset_quota) {
|
||||
payUrl += '&asset_quota=' + asset_quota;
|
||||
}
|
||||
window.open(payUrl);
|
||||
};
|
||||
|
||||
onReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getPlans();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, isWaiting, planList } = this.state;
|
||||
const { toggleDialog, paymentTypeTrans, paymentType } = this.props;
|
||||
const modalStyle = (paymentType === 'paid' || paymentType === 'extend_time') ?
|
||||
{width: '560px', maxWidth: '560px'} : {width: '560px'};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog}>
|
||||
<ModalHeader toggle={toggleDialog}>{paymentTypeTrans}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Loading />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
if (isWaiting) {
|
||||
return (
|
||||
<Modal isOpen={true} toggle={this.onReload}>
|
||||
<ModalHeader toggle={this.onReload}>{paymentTypeTrans}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>{'是否完成付款?'}</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn btn-outline-primary" onClick={this.onReload}>{'是'}</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={true} toggle={toggleDialog} style={modalStyle}>
|
||||
<ModalHeader toggle={toggleDialog}>{paymentTypeTrans}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="d-flex justify-content-between">
|
||||
<Plans
|
||||
plans={planList}
|
||||
onPay={this.onPay}
|
||||
paymentType={this.props.paymentType}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlansDialog.propTypes = PlansDialogPropTypes;
|
||||
|
||||
const propTypes = {
|
||||
isOrgContext: PropTypes.bool.isRequired,
|
||||
handleContentScroll: PropTypes.func,
|
||||
};
|
||||
|
||||
class Subscription extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.paymentTypeTransMap = {
|
||||
paid: '立即购买',
|
||||
extend_time: '立即续费',
|
||||
add_user: '增加用户',
|
||||
buy_quota: '增加空间',
|
||||
};
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
errorMsg: '',
|
||||
isDialogOpen: false,
|
||||
planName: this.props.isOrgContext ? '团队版' : '个人版',
|
||||
userLimit: 20,
|
||||
assetQuota: 1,
|
||||
termEnd: '长期',
|
||||
subscription: null,
|
||||
paymentTypeList: [],
|
||||
currentPaymentType: '',
|
||||
errorMsgCode: ''
|
||||
};
|
||||
}
|
||||
|
||||
getSubscription = () => {
|
||||
subscriptionAPI.getSubscription().then((res) => {
|
||||
const subscription = res.data.subscription;
|
||||
const paymentTypeList = res.data.payment_type_list;
|
||||
if (!subscription) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
paymentTypeList: paymentTypeList,
|
||||
});
|
||||
} else {
|
||||
let isActive = subscription.is_active;
|
||||
let plan = subscription.plan;
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
subscription,
|
||||
planName: plan.name,
|
||||
userLimit: subscription.user_limit,
|
||||
assetQuota: isActive ? subscription.asset_quota : plan.asset_quota,
|
||||
termEnd: isActive ? subscription.term_end : '已过期',
|
||||
paymentTypeList: paymentTypeList,
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
let errorMsg = Utils.getErrorMsg(error);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: errorMsg,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ isDialogOpen: !this.state.isDialogOpen });
|
||||
};
|
||||
|
||||
togglePaymentType = (paymentType) => {
|
||||
this.setState({ currentPaymentType: paymentType });
|
||||
this.toggleDialog();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getSubscription();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, errorMsg, planName, userLimit, assetQuota, termEnd,
|
||||
isDialogOpen, paymentTypeList, currentPaymentType } = this.state;
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (errorMsg) {
|
||||
return <p className="text-center mt-8 error">{errorMsg}</p>;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="content position-relative" onScroll={this.props.handleContentScroll}>
|
||||
<div id="current-plan" className="subscription-info">
|
||||
<h3 className="subscription-info-heading">{'当前版本'}</h3>
|
||||
<p className="mb-2">{planName}</p>
|
||||
</div>
|
||||
{this.props.isOrgContext &&
|
||||
<div id="user-limit" className="subscription-info">
|
||||
<h3 className="subscription-info-heading">{'用户数限制'}</h3>
|
||||
<p className="mb-2">{userLimit}</p>
|
||||
</div>
|
||||
}
|
||||
<div id="asset-quota" className="subscription-info">
|
||||
<h3 className="subscription-info-heading">{'空间'}</h3>
|
||||
<p className="mb-2">{assetQuota ? assetQuota + 'GB' : '1GB'}</p>
|
||||
</div>
|
||||
<div id="current-subscription-period" className="subscription-info">
|
||||
<h3 className="subscription-info-heading">{'订阅有效期'}</h3>
|
||||
<p className="mb-2">{termEnd}</p>
|
||||
</div>
|
||||
<div id="product-price" className="subscription-info">
|
||||
<h3 className="subscription-info-heading">{'云服务付费方案'}</h3>
|
||||
<p className="mb-2">
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://www.seafile.com/product/private_server/">{'查看详情'}</a>
|
||||
</p>
|
||||
</div>
|
||||
{paymentTypeList.map((item, index) => {
|
||||
let name = this.paymentTypeTransMap[item];
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className="btn btn-outline-primary mr-4"
|
||||
onClick={this.togglePaymentType.bind(this, item)}
|
||||
>{name}</button>
|
||||
);
|
||||
})}
|
||||
{!this.state.subscription &&
|
||||
<div id="sales-consultant" className="subscription-info mt-6">
|
||||
<h3 className="subscription-info-heading">{'销售咨询'}</h3>
|
||||
<img className="mb-2" src="/media/img/qr-sale.png" alt="" width="112"></img>
|
||||
<p className="mb-2">{'微信扫码联系销售'}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{isDialogOpen &&
|
||||
<PlansDialog
|
||||
paymentType={currentPaymentType}
|
||||
paymentTypeTrans={this.paymentTypeTransMap[currentPaymentType]}
|
||||
isOrgContext={this.props.isOrgContext}
|
||||
toggleDialog={this.toggleDialog}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Subscription.propTypes = propTypes;
|
||||
|
||||
export default Subscription;
|
Reference in New Issue
Block a user