fix: unify light/dark primary color, fix doc management API, remove compliance restriction
Docker Build / Build and Push Docker Image (push) Successful in 4m23s
Docker Build / Build and Push Docker Image (push) Successful in 4m23s
This commit is contained in:
+14
-8
@@ -89,25 +89,31 @@ func GetDocuments(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageInfo := common.GetPageQuery(c)
|
||||||
|
|
||||||
// 根据用户认证状态决定可见性过滤
|
// 根据用户认证状态决定可见性过滤
|
||||||
visibility := c.Query("visibility")
|
visibility := c.Query("visibility")
|
||||||
role := c.GetInt("role")
|
role := c.GetInt("role")
|
||||||
|
|
||||||
|
var documents []*model.Document
|
||||||
|
var total int64
|
||||||
|
var err error
|
||||||
|
|
||||||
if role >= common.RoleAdminUser {
|
if role >= common.RoleAdminUser {
|
||||||
// 管理员可看所有,如果指定了 visibility 则按指定值过滤
|
// 管理员可看所有
|
||||||
// visibility 保持原值
|
documents, total, err = model.GetDocuments(keyword, visibility, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||||
} else if role >= common.RoleCommonUser {
|
} else if role >= common.RoleCommonUser {
|
||||||
// 普通用户只能看 public 和 auth
|
// 普通用户只能看 public 和 auth
|
||||||
if visibility == "admin" {
|
if visibility == "public" || visibility == "auth" {
|
||||||
visibility = "" // 不允许看 admin
|
documents, total, err = model.GetDocuments(keyword, visibility, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||||
|
} else {
|
||||||
|
documents, total, err = model.GetDocumentsByVisibility(keyword, []string{"public", "auth"}, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||||
}
|
}
|
||||||
// 如果没有指定 visibility,则过滤出 public 和 auth
|
|
||||||
} else {
|
} else {
|
||||||
// 未登录用户只能看 public
|
// 未登录用户只能看 public
|
||||||
visibility = "public"
|
documents, total, err = model.GetDocuments(keyword, "public", categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
pageInfo := common.GetPageQuery(c)
|
|
||||||
documents, total, err := model.GetDocuments(keyword, visibility, categoryId, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -40,6 +40,29 @@ func GetDocuments(keyword string, visibility string, categoryId *int, startIdx i
|
|||||||
return documents, total, nil
|
return documents, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDocumentsByVisibility(keyword string, visibilities []string, categoryId *int, startIdx int, num int) ([]*Document, int64, error) {
|
||||||
|
query := DB.Model(&Document{})
|
||||||
|
if keyword != "" {
|
||||||
|
like := "%" + keyword + "%"
|
||||||
|
query = query.Where("title LIKE ? OR content LIKE ?", like, like)
|
||||||
|
}
|
||||||
|
if len(visibilities) > 0 {
|
||||||
|
query = query.Where("visibility IN ?", visibilities)
|
||||||
|
}
|
||||||
|
if categoryId != nil {
|
||||||
|
query = query.Where("category_id = ?", *categoryId)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var documents []*Document
|
||||||
|
if err := query.Order("sort_order ASC, id DESC").Offset(startIdx).Limit(num).Find(&documents).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return documents, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetDocumentBySlug(slug string) (*Document, error) {
|
func GetDocumentBySlug(slug string) (*Document, error) {
|
||||||
var doc Document
|
var doc Document
|
||||||
err := DB.Where("slug = ?", slug).First(&doc).Error
|
err := DB.Where("slug = ?", slug).First(&doc).Error
|
||||||
@@ -67,5 +90,7 @@ func UpdateDocument(doc *Document) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDocument(id int) error {
|
func DeleteDocument(id int) error {
|
||||||
|
// Delete associated versions first
|
||||||
|
DB.Where("document_id = ?", id).Delete(&DocumentVersion{})
|
||||||
return DB.Delete(&Document{}, id).Error
|
return DB.Delete(&Document{}, id).Error
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-147
@@ -26,9 +26,6 @@ import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPay
|
|||||||
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
|
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
|
||||||
import { API, showError, showSuccess, toBoolean } from '../../helpers';
|
import { API, showError, showSuccess, toBoolean } from '../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RiskAcknowledgementModal from '../common/modals/RiskAcknowledgementModal';
|
|
||||||
|
|
||||||
const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1';
|
|
||||||
|
|
||||||
const PaymentSetting = () => {
|
const PaymentSetting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -51,60 +48,9 @@ const PaymentSetting = () => {
|
|||||||
StripeUnitPrice: 8.0,
|
StripeUnitPrice: 8.0,
|
||||||
StripeMinTopUp: 1,
|
StripeMinTopUp: 1,
|
||||||
StripePromotionCodesEnabled: false,
|
StripePromotionCodesEnabled: false,
|
||||||
|
|
||||||
'payment_setting.compliance_confirmed': false,
|
|
||||||
'payment_setting.compliance_terms_version': '',
|
|
||||||
'payment_setting.compliance_confirmed_at': 0,
|
|
||||||
'payment_setting.compliance_confirmed_by': 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
const [complianceVisible, setComplianceVisible] = useState(false);
|
|
||||||
|
|
||||||
const complianceStatements = [
|
|
||||||
t('你已合法取得所接入模型 API、账号、密钥和额度的授权;'),
|
|
||||||
t(
|
|
||||||
'你承诺仅在已取得上游服务商、模型服务提供方或相关权利方合法授权的范围内使用其 API、账号、密钥、额度及服务能力,不进行未经授权的转售、倒卖、分销或其他违规商业化使用。',
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;',
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'你承诺不会利用本系统实施、协助实施或变相实施违反适用法律法规、监管要求、平台规则、社会公共利益或第三方合法权益的行为。',
|
|
||||||
),
|
|
||||||
t('你理解并自行承担部署、运营和收费行为产生的法律责任。'),
|
|
||||||
t(
|
|
||||||
'你理解本合规提醒仅用于风险提示,不构成法律意见、合规审查结论或对你使用本系统行为合法性的保证;你应根据实际业务场景自行咨询专业法律或合规顾问。',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const requiredComplianceText = t(
|
|
||||||
'我已阅读并理解上述合规提醒,知悉相关法律风险,并确认自行承担部署、运营和收费行为产生的法律责任',
|
|
||||||
);
|
|
||||||
const requiredComplianceTextParts = [
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
text: t('我已阅读并理解上述合规提醒'),
|
|
||||||
},
|
|
||||||
{ type: 'static', text: t(',') },
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
text: t('知悉相关法律风险'),
|
|
||||||
},
|
|
||||||
{ type: 'static', text: t(',') },
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
text: t('并确认自行承担部署'),
|
|
||||||
},
|
|
||||||
{ type: 'static', text: t('、') },
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
text: t('运营和收费行为产生的法律责任'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const complianceConfirmed =
|
|
||||||
inputs['payment_setting.compliance_confirmed'] &&
|
|
||||||
inputs['payment_setting.compliance_terms_version'] ===
|
|
||||||
CURRENT_COMPLIANCE_TERMS_VERSION;
|
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/option/');
|
||||||
@@ -146,16 +92,6 @@ const PaymentSetting = () => {
|
|||||||
newInputs['AmountDiscount'] = item.value;
|
newInputs['AmountDiscount'] = item.value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'payment_setting.compliance_confirmed':
|
|
||||||
newInputs[item.key] = toBoolean(item.value);
|
|
||||||
break;
|
|
||||||
case 'payment_setting.compliance_confirmed_at':
|
|
||||||
case 'payment_setting.compliance_confirmed_by':
|
|
||||||
newInputs[item.key] = parseInt(item.value) || 0;
|
|
||||||
break;
|
|
||||||
case 'payment_setting.compliance_terms_version':
|
|
||||||
newInputs[item.key] = item.value;
|
|
||||||
break;
|
|
||||||
case 'Price':
|
case 'Price':
|
||||||
case 'MinTopUp':
|
case 'MinTopUp':
|
||||||
case 'StripeUnitPrice':
|
case 'StripeUnitPrice':
|
||||||
@@ -193,76 +129,11 @@ const PaymentSetting = () => {
|
|||||||
onRefresh();
|
onRefresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmCompliance = async () => {
|
|
||||||
try {
|
|
||||||
const res = await API.post('/api/option/payment_compliance', {
|
|
||||||
confirmed: true,
|
|
||||||
});
|
|
||||||
if (res.data.success) {
|
|
||||||
showSuccess(t('合规声明确认成功'));
|
|
||||||
setComplianceVisible(false);
|
|
||||||
await onRefresh();
|
|
||||||
} else {
|
|
||||||
showError(res.data.message || t('确认失败'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError(t('确认失败'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
{!complianceConfirmed ? (
|
<div>
|
||||||
<Banner
|
|
||||||
type='warning'
|
|
||||||
title={t('需要确认合规声明')}
|
|
||||||
description={
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<span>
|
|
||||||
{t(
|
|
||||||
'确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。',
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
type='warning'
|
|
||||||
theme='solid'
|
|
||||||
onClick={() => setComplianceVisible(true)}
|
|
||||||
>
|
|
||||||
{t('确认合规声明')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
closeIcon={null}
|
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
fullMode={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Banner
|
|
||||||
type='success'
|
|
||||||
title={t('合规声明已确认')}
|
|
||||||
description={t('确认时间:{{time}},确认用户:#{{userId}}', {
|
|
||||||
time: inputs['payment_setting.compliance_confirmed_at']
|
|
||||||
? new Date(
|
|
||||||
inputs['payment_setting.compliance_confirmed_at'] * 1000,
|
|
||||||
).toLocaleString()
|
|
||||||
: '-',
|
|
||||||
userId:
|
|
||||||
inputs['payment_setting.compliance_confirmed_by'] || '-',
|
|
||||||
})}
|
|
||||||
closeIcon={null}
|
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
fullMode={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
complianceConfirmed
|
|
||||||
? undefined
|
|
||||||
: { opacity: 0.4, pointerEvents: 'none' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
type='card'
|
type='card'
|
||||||
defaultActiveKey='general'
|
defaultActiveKey='general'
|
||||||
@@ -306,23 +177,6 @@ const PaymentSetting = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<RiskAcknowledgementModal
|
|
||||||
visible={complianceVisible}
|
|
||||||
title={t('确认合规声明')}
|
|
||||||
markdownContent={t(
|
|
||||||
'该操作将启用支付、兑换码、订阅计划和邀请返利相关功能。请仔细阅读并确认以下声明。',
|
|
||||||
)}
|
|
||||||
checklist={complianceStatements}
|
|
||||||
inputPrompt={t('请输入以下文字以确认:')}
|
|
||||||
requiredText={requiredComplianceText}
|
|
||||||
requiredTextParts={requiredComplianceTextParts}
|
|
||||||
inputPlaceholder={t('请输入确认文案')}
|
|
||||||
mismatchText={t('输入内容与要求文案不一致')}
|
|
||||||
cancelText={t('取消')}
|
|
||||||
confirmText={t('确认并启用')}
|
|
||||||
onCancel={() => setComplianceVisible(false)}
|
|
||||||
onConfirm={confirmCompliance}
|
|
||||||
/>
|
|
||||||
</Spin>
|
</Spin>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ const renderPaymentConfig = (text, record, t, enableEpay) => {
|
|||||||
const renderOperations = (
|
const renderOperations = (
|
||||||
text,
|
text,
|
||||||
record,
|
record,
|
||||||
{ openEdit, setPlanEnabled, t, complianceConfirmed },
|
{ openEdit, setPlanEnabled, t },
|
||||||
) => {
|
) => {
|
||||||
const isEnabled = record?.plan?.enabled;
|
const isEnabled = record?.plan?.enabled;
|
||||||
|
|
||||||
@@ -260,7 +260,6 @@ const renderOperations = (
|
|||||||
type='tertiary'
|
type='tertiary'
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() => openEdit(record)}
|
onClick={() => openEdit(record)}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
>
|
>
|
||||||
{t('编辑')}
|
{t('编辑')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -270,7 +269,6 @@ const renderOperations = (
|
|||||||
type='danger'
|
type='danger'
|
||||||
size='small'
|
size='small'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
>
|
>
|
||||||
{t('禁用')}
|
{t('禁用')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -280,7 +278,6 @@ const renderOperations = (
|
|||||||
type='primary'
|
type='primary'
|
||||||
size='small'
|
size='small'
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
>
|
>
|
||||||
{t('启用')}
|
{t('启用')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -294,7 +291,6 @@ export const getSubscriptionsColumns = ({
|
|||||||
openEdit,
|
openEdit,
|
||||||
setPlanEnabled,
|
setPlanEnabled,
|
||||||
enableEpay,
|
enableEpay,
|
||||||
complianceConfirmed = true,
|
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -368,7 +364,6 @@ export const getSubscriptionsColumns = ({
|
|||||||
openEdit,
|
openEdit,
|
||||||
setPlanEnabled,
|
setPlanEnabled,
|
||||||
t,
|
t,
|
||||||
complianceConfirmed,
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const SubscriptionsTable = (subscriptionsData) => {
|
|||||||
setPlanEnabled,
|
setPlanEnabled,
|
||||||
t,
|
t,
|
||||||
enableEpay,
|
enableEpay,
|
||||||
complianceConfirmed,
|
|
||||||
} = subscriptionsData;
|
} = subscriptionsData;
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
@@ -44,9 +43,8 @@ const SubscriptionsTable = (subscriptionsData) => {
|
|||||||
openEdit,
|
openEdit,
|
||||||
setPlanEnabled,
|
setPlanEnabled,
|
||||||
enableEpay,
|
enableEpay,
|
||||||
complianceConfirmed,
|
|
||||||
});
|
});
|
||||||
}, [t, openEdit, setPlanEnabled, enableEpay, complianceConfirmed]);
|
}, [t, openEdit, setPlanEnabled, enableEpay]);
|
||||||
|
|
||||||
const tableColumns = useMemo(() => {
|
const tableColumns = useMemo(() => {
|
||||||
return compactMode
|
return compactMode
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const SubscriptionsPage = () => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [statusState] = useContext(StatusContext);
|
const [statusState] = useContext(StatusContext);
|
||||||
const enableEpay = !!statusState?.status?.enable_online_topup;
|
const enableEpay = !!statusState?.status?.enable_online_topup;
|
||||||
const [complianceConfirmed, setComplianceConfirmed] = useState(true);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showEdit,
|
showEdit,
|
||||||
@@ -49,22 +48,6 @@ const SubscriptionsPage = () => {
|
|||||||
t,
|
t,
|
||||||
} = subscriptionsData;
|
} = subscriptionsData;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadComplianceStatus = async () => {
|
|
||||||
try {
|
|
||||||
const res = await API.get('/api/user/topup/info');
|
|
||||||
if (res.data?.success) {
|
|
||||||
setComplianceConfirmed(
|
|
||||||
res.data.data?.payment_compliance_confirmed !== false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Keep the page usable if status loading fails; backend still enforces.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadComplianceStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AddEditSubscriptionModal
|
<AddEditSubscriptionModal
|
||||||
@@ -92,7 +75,6 @@ const SubscriptionsPage = () => {
|
|||||||
<SubscriptionsActions
|
<SubscriptionsActions
|
||||||
openCreate={openCreate}
|
openCreate={openCreate}
|
||||||
t={t}
|
t={t}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Banner
|
<Banner
|
||||||
@@ -116,20 +98,9 @@ const SubscriptionsPage = () => {
|
|||||||
})}
|
})}
|
||||||
t={t}
|
t={t}
|
||||||
>
|
>
|
||||||
{!complianceConfirmed && (
|
|
||||||
<Banner
|
|
||||||
type='warning'
|
|
||||||
description={t(
|
|
||||||
'订阅套餐创建和变更已锁定,管理员需先在支付设置中确认合规声明。',
|
|
||||||
)}
|
|
||||||
closeIcon={null}
|
|
||||||
className='!rounded-lg mb-3'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SubscriptionsTable
|
<SubscriptionsTable
|
||||||
{...subscriptionsData}
|
{...subscriptionsData}
|
||||||
enableEpay={enableEpay}
|
enableEpay={enableEpay}
|
||||||
complianceConfirmed={complianceConfirmed}
|
|
||||||
/>
|
/>
|
||||||
</CardPro>
|
</CardPro>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const InvitationCard = ({
|
|||||||
setOpenTransfer,
|
setOpenTransfer,
|
||||||
affLink,
|
affLink,
|
||||||
handleAffLinkClick,
|
handleAffLinkClick,
|
||||||
complianceConfirmed = true,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||||
@@ -82,7 +81,6 @@ const InvitationCard = ({
|
|||||||
theme='solid'
|
theme='solid'
|
||||||
size='small'
|
size='small'
|
||||||
disabled={
|
disabled={
|
||||||
!complianceConfirmed ||
|
|
||||||
!userState?.user?.aff_quota ||
|
!userState?.user?.aff_quota ||
|
||||||
userState?.user?.aff_quota <= 0
|
userState?.user?.aff_quota <= 0
|
||||||
}
|
}
|
||||||
@@ -93,16 +91,6 @@ const InvitationCard = ({
|
|||||||
{t('划转到余额')}
|
{t('划转到余额')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!complianceConfirmed && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: 'rgba(255,255,255,0.8)',
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('邀请奖励划转已禁用,管理员需先确认合规声明。')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 统计数据 */}
|
{/* 统计数据 */}
|
||||||
<div className='grid grid-cols-3 gap-6 mt-4'>
|
<div className='grid grid-cols-3 gap-6 mt-4'>
|
||||||
|
|||||||
-1
@@ -1024,7 +1024,6 @@ const TopUp = () => {
|
|||||||
setOpenTransfer={setOpenTransfer}
|
setOpenTransfer={setOpenTransfer}
|
||||||
affLink={affLink}
|
affLink={affLink}
|
||||||
handleAffLinkClick={handleAffLinkClick}
|
handleAffLinkClick={handleAffLinkClick}
|
||||||
complianceConfirmed={topupInfo.payment_compliance_confirmed !== false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ export default function SettingsCreditLimit(props) {
|
|||||||
});
|
});
|
||||||
const refForm = useRef();
|
const refForm = useRef();
|
||||||
const [inputsRow, setInputsRow] = useState(inputs);
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
const complianceConfirmed =
|
|
||||||
props.options?.['payment_setting.compliance_confirmed'] === true ||
|
|
||||||
props.options?.['payment_setting.compliance_confirmed'] === 'true';
|
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
const updateArray = compareObjects(inputs, inputsRow);
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
@@ -93,16 +90,6 @@ export default function SettingsCreditLimit(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{!complianceConfirmed && (
|
|
||||||
<Banner
|
|
||||||
type='warning'
|
|
||||||
description={t(
|
|
||||||
'设置非零邀请奖励额度前,需要先在支付设置中确认合规声明。',
|
|
||||||
)}
|
|
||||||
closeIcon={null}
|
|
||||||
className='!rounded-lg mb-3'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Form
|
<Form
|
||||||
values={inputs}
|
values={inputs}
|
||||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
@@ -150,9 +137,6 @@ export default function SettingsCreditLimit(props) {
|
|||||||
step={1}
|
step={1}
|
||||||
min={0}
|
min={0}
|
||||||
suffix={'Token'}
|
suffix={'Token'}
|
||||||
extraText={
|
|
||||||
!complianceConfirmed ? t('非零值需先确认合规声明') : ''
|
|
||||||
}
|
|
||||||
placeholder={t('例如:2000')}
|
placeholder={t('例如:2000')}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
@@ -171,9 +155,6 @@ export default function SettingsCreditLimit(props) {
|
|||||||
step={1}
|
step={1}
|
||||||
min={0}
|
min={0}
|
||||||
suffix={'Token'}
|
suffix={'Token'}
|
||||||
extraText={
|
|
||||||
!complianceConfirmed ? t('非零值需先确认合规声明') : ''
|
|
||||||
}
|
|
||||||
placeholder={t('例如:1000')}
|
placeholder={t('例如:1000')}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ export function DocCategories() {
|
|||||||
const fetchCategories = useCallback(async () => {
|
const fetchCategories = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const res = await api.get('/api/docs/admin/categories')
|
const res = await api.get('/api/docs/categories')
|
||||||
setCategories(res.data?.data || [])
|
setCategories(res.data?.data || [])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch categories:', err)
|
console.error('Failed to fetch categories:', err)
|
||||||
|
|||||||
+2
-2
@@ -69,8 +69,8 @@ export function DocsManagement() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const [docsRes, catsRes] = await Promise.all([
|
const [docsRes, catsRes] = await Promise.all([
|
||||||
api.get('/api/docs/admin/'),
|
api.get('/api/docs/'),
|
||||||
api.get('/api/docs/admin/categories'),
|
api.get('/api/docs/categories'),
|
||||||
])
|
])
|
||||||
setDocs(docsRes.data?.data || [])
|
setDocs(docsRes.data?.data || [])
|
||||||
setCategories(catsRes.data?.data || [])
|
setCategories(catsRes.data?.data || [])
|
||||||
|
|||||||
+34
-15
@@ -148,8 +148,19 @@ export function Pricing() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<PublicLayout showMainContainer={false}>
|
<PublicLayout showMainContainer={false}>
|
||||||
<div className='mx-auto w-full max-w-6xl px-4 pt-20 pb-8'>
|
<div className='relative'>
|
||||||
<LoadingSkeleton viewMode={viewMode} />
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className='pointer-events-none absolute inset-0 opacity-[0.03] dark:opacity-[0.04]'
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle, var(--primary) 0.5px, transparent 0.5px)',
|
||||||
|
backgroundSize: '24px 24px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='relative mx-auto w-full max-w-6xl px-4 pt-20 pb-8'>
|
||||||
|
<LoadingSkeleton viewMode={viewMode} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PublicLayout>
|
</PublicLayout>
|
||||||
)
|
)
|
||||||
@@ -158,25 +169,33 @@ export function Pricing() {
|
|||||||
return (
|
return (
|
||||||
<PublicLayout showMainContainer={false}>
|
<PublicLayout showMainContainer={false}>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
|
{/* Background effects matching home page */}
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className='pointer-events-none absolute inset-x-0 top-0 h-[400px] opacity-15 dark:opacity-[0.08]'
|
className='pointer-events-none absolute inset-0 opacity-[0.03] dark:opacity-[0.04]'
|
||||||
style={{
|
style={{
|
||||||
background:
|
backgroundImage:
|
||||||
'radial-gradient(ellipse 60% 50% at 50% 0%, oklch(0.72 0.18 250 / 80%) 0%, transparent 70%)',
|
'radial-gradient(circle, var(--primary) 0.5px, transparent 0.5px)',
|
||||||
maskImage:
|
backgroundSize: '24px 24px',
|
||||||
'linear-gradient(to bottom, black 30%, transparent 100%)',
|
|
||||||
WebkitMaskImage:
|
|
||||||
'linear-gradient(to bottom, black 30%, transparent 100%)',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className='pointer-events-none absolute inset-0'
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'radial-gradient(ellipse 60% 50% at 50% -10%, oklch(0.55 0.15 240 / 0.06) 0%, transparent 70%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageTransition className='relative mx-auto w-full max-w-6xl px-4 pt-20 pb-12'>
|
<PageTransition className='relative mx-auto w-full max-w-6xl px-4 pt-20 pb-12'>
|
||||||
{/* Header */}
|
{/* Header - matching home page hero style */}
|
||||||
<header className='mb-6'>
|
<header className='mb-8 text-center'>
|
||||||
<h1 className='text-2xl font-bold tracking-tight sm:text-3xl'>
|
<h1 className='text-[clamp(1.75rem,4vw,2.5rem)] leading-tight font-bold tracking-[-0.025em] text-foreground'>
|
||||||
{t('Model Square')}
|
{t('Model Square')}{' '}
|
||||||
|
<span className='text-primary'>{t('Pricing')}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-muted-foreground mt-1.5 text-sm'>
|
<p className='mx-auto mt-2 max-w-lg text-[15px] leading-relaxed text-muted-foreground'>
|
||||||
{t('This site currently has {{count}} models enabled', {
|
{t('This site currently has {{count}} models enabled', {
|
||||||
count: models?.length || 0,
|
count: models?.length || 0,
|
||||||
})}
|
})}
|
||||||
@@ -184,7 +203,7 @@ export function Pricing() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Search + Filters */}
|
{/* Search + Filters */}
|
||||||
<div className='mb-4 space-y-3'>
|
<div className='mb-5 space-y-3'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={setSearchInput}
|
onChange={setSearchInput}
|
||||||
|
|||||||
+1
-3
@@ -35,7 +35,7 @@ interface DataTableRowActionsProps {
|
|||||||
|
|
||||||
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setOpen, setCurrentRow, complianceConfirmed } = useSubscriptions()
|
const { setOpen, setCurrentRow } = useSubscriptions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -46,7 +46,6 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align='end'>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('update')
|
setOpen('update')
|
||||||
@@ -56,7 +55,6 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
{t('Edit')}
|
{t('Edit')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('toggle-status')
|
setOpen('toggle-status')
|
||||||
|
|||||||
+1
-2
@@ -23,13 +23,12 @@ import { useSubscriptions } from './subscriptions-provider'
|
|||||||
|
|
||||||
export function SubscriptionsPrimaryButtons() {
|
export function SubscriptionsPrimaryButtons() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setOpen, complianceConfirmed } = useSubscriptions()
|
const { setOpen } = useSubscriptions()
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => setOpen('create')}
|
onClick={() => setOpen('create')}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
>
|
>
|
||||||
<Plus className='h-4 w-4' />
|
<Plus className='h-4 w-4' />
|
||||||
{t('Create Plan')}
|
{t('Create Plan')}
|
||||||
|
|||||||
@@ -18,14 +18,8 @@ For commercial licensing, please contact admin@modelstoken.com
|
|||||||
*/
|
*/
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import useDialogState from '@/hooks/use-dialog'
|
import useDialogState from '@/hooks/use-dialog'
|
||||||
import {
|
|
||||||
getOptionValue,
|
|
||||||
useSystemOptions,
|
|
||||||
} from '@/features/system-settings/hooks/use-system-options'
|
|
||||||
import { type PlanRecord, type SubscriptionsDialogType } from '../types'
|
import { type PlanRecord, type SubscriptionsDialogType } from '../types'
|
||||||
|
|
||||||
const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1'
|
|
||||||
|
|
||||||
type SubscriptionsContextType = {
|
type SubscriptionsContextType = {
|
||||||
open: SubscriptionsDialogType | null
|
open: SubscriptionsDialogType | null
|
||||||
setOpen: (str: SubscriptionsDialogType | null) => void
|
setOpen: (str: SubscriptionsDialogType | null) => void
|
||||||
@@ -33,7 +27,6 @@ type SubscriptionsContextType = {
|
|||||||
setCurrentRow: React.Dispatch<React.SetStateAction<PlanRecord | null>>
|
setCurrentRow: React.Dispatch<React.SetStateAction<PlanRecord | null>>
|
||||||
refreshTrigger: number
|
refreshTrigger: number
|
||||||
triggerRefresh: () => void
|
triggerRefresh: () => void
|
||||||
complianceConfirmed: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubscriptionsContext =
|
const SubscriptionsContext =
|
||||||
@@ -47,15 +40,6 @@ export function SubscriptionsProvider({
|
|||||||
const [open, setOpen] = useDialogState<SubscriptionsDialogType>(null)
|
const [open, setOpen] = useDialogState<SubscriptionsDialogType>(null)
|
||||||
const [currentRow, setCurrentRow] = useState<PlanRecord | null>(null)
|
const [currentRow, setCurrentRow] = useState<PlanRecord | null>(null)
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
const { data } = useSystemOptions()
|
|
||||||
const complianceOptions = getOptionValue(data?.data, {
|
|
||||||
'payment_setting.compliance_confirmed': false,
|
|
||||||
'payment_setting.compliance_terms_version': '',
|
|
||||||
})
|
|
||||||
const complianceConfirmed =
|
|
||||||
complianceOptions['payment_setting.compliance_confirmed'] &&
|
|
||||||
complianceOptions['payment_setting.compliance_terms_version'] ===
|
|
||||||
CURRENT_COMPLIANCE_TERMS_VERSION
|
|
||||||
|
|
||||||
const triggerRefresh = () => setRefreshTrigger((prev) => prev + 1)
|
const triggerRefresh = () => setRefreshTrigger((prev) => prev + 1)
|
||||||
|
|
||||||
@@ -68,7 +52,6 @@ export function SubscriptionsProvider({
|
|||||||
setCurrentRow,
|
setCurrentRow,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
triggerRefresh,
|
triggerRefresh,
|
||||||
complianceConfirmed,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { SubscriptionsTable } from './components/subscriptions-table'
|
|||||||
|
|
||||||
function SubscriptionsContent() {
|
function SubscriptionsContent() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { complianceConfirmed } = useSubscriptions()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -53,15 +52,6 @@ function SubscriptionsContent() {
|
|||||||
</SectionPageLayout.Actions>
|
</SectionPageLayout.Actions>
|
||||||
<SectionPageLayout.Content>
|
<SectionPageLayout.Content>
|
||||||
<div className='flex h-full min-h-0 flex-col gap-4'>
|
<div className='flex h-full min-h-0 flex-col gap-4'>
|
||||||
{!complianceConfirmed ? (
|
|
||||||
<Alert variant='destructive' className='shrink-0'>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
'Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.'
|
|
||||||
)}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
<div className='min-h-0 flex-1'>
|
<div className='min-h-0 flex-1'>
|
||||||
<SubscriptionsTable />
|
<SubscriptionsTable />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,11 +67,6 @@ const defaultBillingSettings: BillingSettings = {
|
|||||||
PayMethods: '',
|
PayMethods: '',
|
||||||
'payment_setting.amount_options': '',
|
'payment_setting.amount_options': '',
|
||||||
'payment_setting.amount_discount': '',
|
'payment_setting.amount_discount': '',
|
||||||
'payment_setting.compliance_confirmed': false,
|
|
||||||
'payment_setting.compliance_terms_version': '',
|
|
||||||
'payment_setting.compliance_confirmed_at': 0,
|
|
||||||
'payment_setting.compliance_confirmed_by': 0,
|
|
||||||
'payment_setting.compliance_confirmed_ip': '',
|
|
||||||
StripeApiSecret: '',
|
StripeApiSecret: '',
|
||||||
StripeWebhookSecret: '',
|
StripeWebhookSecret: '',
|
||||||
StripePriceId: '',
|
StripePriceId: '',
|
||||||
|
|||||||
@@ -70,10 +70,6 @@ const BILLING_SECTIONS = [
|
|||||||
settings['quota_setting.enable_free_model_pre_consume'],
|
settings['quota_setting.enable_free_model_pre_consume'],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
complianceConfirmed={
|
|
||||||
(settings['payment_setting.compliance_confirmed'] ?? false) &&
|
|
||||||
settings['payment_setting.compliance_terms_version'] === 'v1'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -176,13 +172,6 @@ const BILLING_SECTIONS = [
|
|||||||
}}
|
}}
|
||||||
waffoPancakeProvisionedStoreID={settings.WaffoPancakeStoreID ?? ''}
|
waffoPancakeProvisionedStoreID={settings.WaffoPancakeStoreID ?? ''}
|
||||||
waffoPancakeProvisionedProductID={settings.WaffoPancakeProductID ?? ''}
|
waffoPancakeProvisionedProductID={settings.WaffoPancakeProductID ?? ''}
|
||||||
complianceDefaults={{
|
|
||||||
confirmed: settings['payment_setting.compliance_confirmed'] ?? false,
|
|
||||||
termsVersion:
|
|
||||||
settings['payment_setting.compliance_terms_version'] ?? '',
|
|
||||||
confirmedAt: settings['payment_setting.compliance_confirmed_at'] ?? 0,
|
|
||||||
confirmedBy: settings['payment_setting.compliance_confirmed_by'] ?? 0,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,12 +65,10 @@ type QuotaFormValues = z.infer<typeof quotaSchema>
|
|||||||
|
|
||||||
type QuotaSettingsSectionProps = {
|
type QuotaSettingsSectionProps = {
|
||||||
defaultValues: QuotaFormValues
|
defaultValues: QuotaFormValues
|
||||||
complianceConfirmed?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuotaSettingsSection({
|
export function QuotaSettingsSection({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
complianceConfirmed = true,
|
|
||||||
}: QuotaSettingsSectionProps) {
|
}: QuotaSettingsSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
@@ -104,16 +102,6 @@ export function QuotaSettingsSection({
|
|||||||
<SettingsSection title={t('Quota Settings')}>
|
<SettingsSection title={t('Quota Settings')}>
|
||||||
<FormNavigationGuard when={isDirty} />
|
<FormNavigationGuard when={isDirty} />
|
||||||
|
|
||||||
{!complianceConfirmed ? (
|
|
||||||
<Alert variant='destructive'>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
'Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.'
|
|
||||||
)}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<SettingsForm onSubmit={handleSubmit}>
|
<SettingsForm onSubmit={handleSubmit}>
|
||||||
<SettingsPageFormActions
|
<SettingsPageFormActions
|
||||||
|
|||||||
+3
-164
@@ -20,17 +20,10 @@ import * as React from 'react'
|
|||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { useForm, type Resolver } from 'react-hook-form'
|
import { useForm, type Resolver } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { Code2, Eye, ShieldAlert } from 'lucide-react'
|
import { Code2, Eye } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
AlertAction,
|
|
||||||
AlertDescription,
|
|
||||||
AlertTitle,
|
|
||||||
} from '@/components/ui/alert'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -45,8 +38,6 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { RiskAcknowledgementDialog } from '@/components/risk-acknowledgement-dialog'
|
|
||||||
import { confirmPaymentCompliance } from '../api'
|
|
||||||
import {
|
import {
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
SettingsSwitchContent,
|
SettingsSwitchContent,
|
||||||
@@ -168,22 +159,12 @@ type PaymentBaseFormValues = Omit<
|
|||||||
keyof WaffoFormFieldValues | keyof WaffoPancakeSettingsValues
|
keyof WaffoFormFieldValues | keyof WaffoPancakeSettingsValues
|
||||||
>
|
>
|
||||||
|
|
||||||
const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1'
|
|
||||||
|
|
||||||
type PaymentComplianceDefaults = {
|
|
||||||
confirmed: boolean
|
|
||||||
termsVersion: string
|
|
||||||
confirmedAt: number
|
|
||||||
confirmedBy: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaymentSettingsSectionProps = {
|
type PaymentSettingsSectionProps = {
|
||||||
defaultValues: PaymentBaseFormValues
|
defaultValues: PaymentBaseFormValues
|
||||||
waffoDefaultValues: WaffoSettingsValues
|
waffoDefaultValues: WaffoSettingsValues
|
||||||
waffoPancakeDefaultValues: WaffoPancakeSettingsValues
|
waffoPancakeDefaultValues: WaffoPancakeSettingsValues
|
||||||
waffoPancakeProvisionedStoreID?: string
|
waffoPancakeProvisionedStoreID?: string
|
||||||
waffoPancakeProvisionedProductID?: string
|
waffoPancakeProvisionedProductID?: string
|
||||||
complianceDefaults: PaymentComplianceDefaults
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseWaffoPayMethods(value: string): PayMethod[] {
|
function parseWaffoPayMethods(value: string): PayMethod[] {
|
||||||
@@ -201,7 +182,6 @@ export function PaymentSettingsSection({
|
|||||||
waffoPancakeDefaultValues,
|
waffoPancakeDefaultValues,
|
||||||
waffoPancakeProvisionedStoreID,
|
waffoPancakeProvisionedStoreID,
|
||||||
waffoPancakeProvisionedProductID,
|
waffoPancakeProvisionedProductID,
|
||||||
complianceDefaults,
|
|
||||||
}: PaymentSettingsSectionProps) {
|
}: PaymentSettingsSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -227,7 +207,6 @@ export function PaymentSettingsSection({
|
|||||||
React.useState(true)
|
React.useState(true)
|
||||||
const [creemProductsVisualMode, setCreemProductsVisualMode] =
|
const [creemProductsVisualMode, setCreemProductsVisualMode] =
|
||||||
React.useState(true)
|
React.useState(true)
|
||||||
const [showComplianceDialog, setShowComplianceDialog] = React.useState(false)
|
|
||||||
const [waffoPayMethods, setWaffoPayMethods] = React.useState<PayMethod[]>(
|
const [waffoPayMethods, setWaffoPayMethods] = React.useState<PayMethod[]>(
|
||||||
() => parseWaffoPayMethods(waffoDefaultValues.WaffoPayMethods)
|
() => parseWaffoPayMethods(waffoDefaultValues.WaffoPayMethods)
|
||||||
)
|
)
|
||||||
@@ -255,80 +234,6 @@ export function PaymentSettingsSection({
|
|||||||
setWaffoPancakeSavedBinding(nextBinding)
|
setWaffoPancakeSavedBinding(nextBinding)
|
||||||
}, [waffoPancakeProvisionedProductID, waffoPancakeProvisionedStoreID])
|
}, [waffoPancakeProvisionedProductID, waffoPancakeProvisionedStoreID])
|
||||||
|
|
||||||
const complianceStatements = React.useMemo(
|
|
||||||
() => [
|
|
||||||
t(
|
|
||||||
'You have legally obtained authorization for the connected model APIs, accounts, keys, and quotas.'
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'You commit to using upstream APIs, accounts, keys, quotas, and service capabilities only within the scope of lawful authorization obtained from upstream service providers, model service providers, or relevant rights holders, and will not conduct unauthorized resale, trafficking, distribution, or other non-compliant commercialization.'
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.'
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'You commit not to use this system to implement, assist with, or indirectly implement acts that violate applicable laws and regulations, regulatory requirements, platform rules, public interests, or the lawful rights and interests of third parties.'
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'You understand and independently bear legal responsibility arising from deployment, operation, and charging behavior.'
|
|
||||||
),
|
|
||||||
t(
|
|
||||||
'You understand this compliance reminder is only for risk notice and does not constitute legal advice, a compliance review conclusion, or a guarantee of the legality of your use of this system; you should consult professional legal or compliance advisors based on your actual business scenario.'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const complianceRequiredText = t(
|
|
||||||
'I have read and understood the above compliance reminder, acknowledge the related legal risks, and confirm that I bear legal responsibility arising from deployment, operation, and charging behavior.'
|
|
||||||
)
|
|
||||||
const complianceRequiredTextParts = React.useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
type: 'input' as const,
|
|
||||||
text: t('I have read and understood the above compliance reminder'),
|
|
||||||
},
|
|
||||||
{ type: 'static' as const, text: t(',') },
|
|
||||||
{
|
|
||||||
type: 'input' as const,
|
|
||||||
text: t('acknowledge the related legal risks'),
|
|
||||||
},
|
|
||||||
{ type: 'static' as const, text: t(',and ') },
|
|
||||||
{
|
|
||||||
type: 'input' as const,
|
|
||||||
text: t(
|
|
||||||
'confirm that I bear legal responsibility arising from deployment'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ type: 'static' as const, text: t('、') },
|
|
||||||
{
|
|
||||||
type: 'input' as const,
|
|
||||||
text: t('operation and charging behavior'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const complianceConfirmed =
|
|
||||||
complianceDefaults.confirmed &&
|
|
||||||
complianceDefaults.termsVersion === CURRENT_COMPLIANCE_TERMS_VERSION
|
|
||||||
|
|
||||||
const confirmComplianceMutation = useMutation({
|
|
||||||
mutationFn: confirmPaymentCompliance,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data.success) {
|
|
||||||
toast.success(t('Compliance confirmed successfully'))
|
|
||||||
setShowComplianceDialog(false)
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['system-options'] })
|
|
||||||
} else {
|
|
||||||
toast.error(data.message || t('Failed to confirm compliance'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
toast.error(error.message || t('Failed to confirm compliance'))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = useForm<PaymentFormValues>({
|
const form = useForm<PaymentFormValues>({
|
||||||
resolver: zodResolver(paymentSchema) as Resolver<PaymentFormValues>,
|
resolver: zodResolver(paymentSchema) as Resolver<PaymentFormValues>,
|
||||||
mode: 'onChange', // Enable real-time validation
|
mode: 'onChange', // Enable real-time validation
|
||||||
@@ -780,76 +685,10 @@ export function PaymentSettingsSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('Payment Gateway')}>
|
<SettingsSection title={t('Payment Gateway')}>
|
||||||
{!complianceConfirmed ? (
|
|
||||||
<Alert variant='destructive' className='mb-6'>
|
|
||||||
<ShieldAlert className='h-4 w-4' />
|
|
||||||
<AlertTitle>{t('Compliance confirmation required')}</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<div className='space-y-3'>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<ol className='list-decimal space-y-1 pl-5'>
|
|
||||||
{complianceStatements.map((statement) => (
|
|
||||||
<li key={statement}>{statement}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</AlertDescription>
|
|
||||||
<AlertAction>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
size='sm'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={() => setShowComplianceDialog(true)}
|
|
||||||
>
|
|
||||||
{t('Confirm compliance')}
|
|
||||||
</Button>
|
|
||||||
</AlertAction>
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert className='mb-6'>
|
|
||||||
<AlertTitle>{t('Compliance confirmed')}</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t('Confirmed at {{time}} by user #{{userId}}', {
|
|
||||||
time: complianceDefaults.confirmedAt
|
|
||||||
? new Date(
|
|
||||||
complianceDefaults.confirmedAt * 1000
|
|
||||||
).toLocaleString()
|
|
||||||
: '-',
|
|
||||||
userId: complianceDefaults.confirmedBy || '-',
|
|
||||||
})}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RiskAcknowledgementDialog
|
|
||||||
open={showComplianceDialog}
|
|
||||||
onOpenChange={setShowComplianceDialog}
|
|
||||||
title={t('Confirm compliance terms')}
|
|
||||||
description={t(
|
|
||||||
'This confirmation unlocks payment, redemption code, subscription plan, and invitation reward features. Please read the statements carefully.'
|
|
||||||
)}
|
|
||||||
items={complianceStatements}
|
|
||||||
requiredText={complianceRequiredText}
|
|
||||||
requiredTextParts={complianceRequiredTextParts}
|
|
||||||
inputPrompt={t('Please type the following text to confirm:')}
|
|
||||||
inputPlaceholder={t('Type the confirmation text here')}
|
|
||||||
mismatchHint={t('The entered text does not match the required text.')}
|
|
||||||
confirmText={t('Confirm and enable')}
|
|
||||||
isLoading={confirmComplianceMutation.isPending}
|
|
||||||
onConfirm={() => confirmComplianceMutation.mutate()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<SettingsForm
|
<SettingsForm
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className={cn(
|
className='gap-y-8'
|
||||||
'gap-y-8',
|
|
||||||
!complianceConfirmed && 'pointer-events-none opacity-40'
|
|
||||||
)}
|
|
||||||
data-no-autosubmit='true'
|
data-no-autosubmit='true'
|
||||||
>
|
>
|
||||||
<SettingsPageFormActions
|
<SettingsPageFormActions
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ interface AffiliateRewardsCardProps {
|
|||||||
user: UserWalletData | null
|
user: UserWalletData | null
|
||||||
affiliateLink: string
|
affiliateLink: string
|
||||||
onTransfer: () => void
|
onTransfer: () => void
|
||||||
complianceConfirmed?: boolean
|
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ export function AffiliateRewardsCard({
|
|||||||
user,
|
user,
|
||||||
affiliateLink,
|
affiliateLink,
|
||||||
onTransfer,
|
onTransfer,
|
||||||
complianceConfirmed = true,
|
|
||||||
loading,
|
loading,
|
||||||
}: AffiliateRewardsCardProps) {
|
}: AffiliateRewardsCardProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -112,7 +110,6 @@ export function AffiliateRewardsCard({
|
|||||||
{hasRewards && (
|
{hasRewards && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onTransfer}
|
onClick={onTransfer}
|
||||||
disabled={!complianceConfirmed}
|
|
||||||
className='h-9 shrink-0 px-3'
|
className='h-9 shrink-0 px-3'
|
||||||
size='sm'
|
size='sm'
|
||||||
>
|
>
|
||||||
@@ -120,13 +117,6 @@ export function AffiliateRewardsCard({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!complianceConfirmed ? (
|
|
||||||
<p className='text-muted-foreground text-xs lg:col-span-3'>
|
|
||||||
{t(
|
|
||||||
'Referral reward transfer is disabled until the administrator confirms compliance terms.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
-3
@@ -318,9 +318,6 @@ export function Wallet(props: WalletProps) {
|
|||||||
user={user}
|
user={user}
|
||||||
affiliateLink={affiliateLink}
|
affiliateLink={affiliateLink}
|
||||||
onTransfer={() => setTransferDialogOpen(true)}
|
onTransfer={() => setTransferDialogOpen(true)}
|
||||||
complianceConfirmed={
|
|
||||||
topupInfo?.payment_compliance_confirmed !== false
|
|
||||||
}
|
|
||||||
loading={affiliateLoading}
|
loading={affiliateLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Vendored
+1
@@ -23,6 +23,7 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Home',
|
'Home',
|
||||||
'Console',
|
'Console',
|
||||||
'Model Square',
|
'Model Square',
|
||||||
|
'Pricing',
|
||||||
'Rankings',
|
'Rankings',
|
||||||
'Docs',
|
'Docs',
|
||||||
'About',
|
'About',
|
||||||
|
|||||||
Vendored
+5
-5
@@ -107,7 +107,7 @@ For commercial licensing, please contact admin@modelstoken.com
|
|||||||
--card-foreground: oklch(0.13 0.028 270);
|
--card-foreground: oklch(0.13 0.028 270);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.13 0.028 270);
|
--popover-foreground: oklch(0.13 0.028 270);
|
||||||
--primary: oklch(0.55 0.15 240);
|
--primary: oklch(0.78 0.15 210);
|
||||||
--primary-foreground: oklch(0.995 0 0);
|
--primary-foreground: oklch(0.995 0 0);
|
||||||
--secondary: oklch(0.96 0.005 260);
|
--secondary: oklch(0.96 0.005 260);
|
||||||
--secondary-foreground: oklch(0.18 0.02 270);
|
--secondary-foreground: oklch(0.18 0.02 270);
|
||||||
@@ -127,20 +127,20 @@ For commercial licensing, please contact admin@modelstoken.com
|
|||||||
--neutral-foreground: oklch(0.995 0 0);
|
--neutral-foreground: oklch(0.995 0 0);
|
||||||
--border: oklch(0.92 0.003 260);
|
--border: oklch(0.92 0.003 260);
|
||||||
--input: oklch(0.92 0.003 260);
|
--input: oklch(0.92 0.003 260);
|
||||||
--ring: oklch(0.55 0.15 240);
|
--ring: oklch(0.78 0.15 210);
|
||||||
--chart-1: oklch(0.55 0.15 240);
|
--chart-1: oklch(0.78 0.15 210);
|
||||||
--chart-2: oklch(0.596 0.145 163.225);
|
--chart-2: oklch(0.596 0.145 163.225);
|
||||||
--chart-3: oklch(0.681 0.162 75.834);
|
--chart-3: oklch(0.681 0.162 75.834);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.975 0.003 260);
|
--sidebar: oklch(0.975 0.003 260);
|
||||||
--sidebar-foreground: oklch(0.13 0.028 270);
|
--sidebar-foreground: oklch(0.13 0.028 270);
|
||||||
--sidebar-primary: oklch(0.55 0.15 240);
|
--sidebar-primary: oklch(0.78 0.15 210);
|
||||||
--sidebar-primary-foreground: oklch(0.995 0 0);
|
--sidebar-primary-foreground: oklch(0.995 0 0);
|
||||||
--sidebar-accent: oklch(0.94 0.008 260);
|
--sidebar-accent: oklch(0.94 0.008 260);
|
||||||
--sidebar-accent-foreground: oklch(0.13 0.028 270);
|
--sidebar-accent-foreground: oklch(0.13 0.028 270);
|
||||||
--sidebar-border: oklch(0.92 0.003 260);
|
--sidebar-border: oklch(0.92 0.003 260);
|
||||||
--sidebar-ring: oklch(0.55 0.15 240);
|
--sidebar-ring: oklch(0.78 0.15 210);
|
||||||
--skeleton-base: oklch(0.96 0.005 260);
|
--skeleton-base: oklch(0.96 0.005 260);
|
||||||
--skeleton-highlight: oklch(0.995 0 0);
|
--skeleton-highlight: oklch(0.995 0 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user