fix: unify light/dark primary color, fix doc management API, remove compliance restriction
Docker Build / Build and Push Docker Image (push) Successful in 4m23s

This commit is contained in:
2026-06-14 21:20:47 +08:00
parent a4c069f88d
commit 113b9c8ecb
24 changed files with 90 additions and 485 deletions
+14 -8
View File
@@ -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
+25
View File
@@ -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
View File
@@ -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>
</> </>
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}
@@ -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')
@@ -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}
-10
View File
@@ -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
@@ -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
View File
@@ -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>
+1
View File
@@ -23,6 +23,7 @@ export const STATIC_I18N_KEYS = [
'Home', 'Home',
'Console', 'Console',
'Model Square', 'Model Square',
'Pricing',
'Rankings', 'Rankings',
'Docs', 'Docs',
'About', 'About',
+5 -5
View File
@@ -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);
} }