feat: add Footer component and integrate it into Pricing layout; refactor Header styles and update Plans component structure

This commit is contained in:
twwu
2025-08-15 14:16:35 +08:00
parent 153d5e8f03
commit 89c7f71199
11 changed files with 80 additions and 118 deletions

View File

@@ -0,0 +1,32 @@
import React from 'react'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine } from '@remixicon/react'
type FooterProps = {
pricingPageURL: string
}
const Footer = ({
pricingPageURL,
}: FooterProps) => {
const { t } = useTranslation()
return (
<div className='flex min-h-16 w-full justify-center border-t border-divider-accent px-10'>
<div className='flex max-w-[1680px] grow justify-end border-x border-divider-accent p-6'>
<span className='flex h-fit items-center gap-x-1 text-saas-dify-blue-accessible'>
<Link
href={pricingPageURL}
className='system-md-regular'
>
{t('billing.plansCommon.comparePlanAndFeatures')}
</Link>
<RiArrowRightUpLine className='size-4' />
</span>
</div>
</div>
)
}
export default React.memo(Footer)

View File

@@ -20,7 +20,7 @@ const Header = ({
<div className='py-[5px]'>
<DifyLogo className='h-[27px] w-[60px]' />
</div>
<span className='overflow-visible bg-billing-plan-title-bg bg-clip-text px-1.5 font-instrument text-[37px] italic leading-[1.2] text-transparent'>
<span className='bg-billing-plan-title-bg bg-clip-text px-1.5 font-instrument text-[37px] italic leading-[1.2] text-transparent'>
{t('billing.plansCommon.title.plans')}
</span>
</div>

View File

@@ -4,21 +4,13 @@ import React, { useState } from 'react'
import { createPortal } from 'react-dom'
import Header from './header'
import PlanSwitcher from './plan-switcher'
import Plans from './plans'
import Footer from './footer'
import { PlanRange } from './plan-switcher/plan-range-switcher'
import { useKeyPress } from 'ahooks'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import Plans from './plans'
// import { useTranslation } from 'react-i18next'
// import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react'
// import Link from 'next/link'
// import { Plan, SelfHostedPlan } from '../type'
// import TabSlider from '../../base/tab-slider'
// import PlanItem from './plan-item'
// import SelfHostedPlanItem from './self-hosted-plan-item'
// import GridMask from '@/app/components/base/grid-mask'
// import classNames from '@/utils/classnames'
// import { useGetPricingPageLanguage } from '@/context/i18n'
import { useGetPricingPageLanguage } from '@/context/i18n'
export type Category = 'cloud' | 'self'
@@ -29,27 +21,25 @@ type PricingProps = {
const Pricing: FC<PricingProps> = ({
onCancel,
}) => {
// const { t } = useTranslation()
const { plan } = useProviderContext()
const { isCurrentWorkspaceManager } = useAppContext()
const canPay = isCurrentWorkspaceManager
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
const [currentCategory, setCurrentCategory] = useState<Category>('cloud')
const canPay = isCurrentWorkspaceManager
useKeyPress(['esc'], onCancel)
// const pricingPageLanguage = useGetPricingPageLanguage()
// const pricingPageURL = pricingPageLanguage
// ? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features`
// : 'https://dify.ai/pricing#plans-and-features'
const pricingPageLanguage = useGetPricingPageLanguage()
const pricingPageURL = pricingPageLanguage
? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features`
: 'https://dify.ai/pricing#plans-and-features'
return createPortal(
<div
className='fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] overflow-auto bg-saas-background'
onClick={e => e.stopPropagation()}
>
<div className='relative h-full min-w-[1200px]'>
<div className='relative grid min-h-[1140px] min-w-[1200px] grid-rows-[1fr_auto_auto_1fr]'>
<Header onClose={onCancel} />
<PlanSwitcher
currentCategory={currentCategory}
@@ -63,16 +53,9 @@ const Pricing: FC<PricingProps> = ({
planRange={planRange}
canPay={canPay}
/>
{/* <GridMask wrapperClassName='w-full min-h-full' canvasClassName='min-h-full'>
<div className='flex items-center justify-center py-4'>
<div className='flex items-center justify-center gap-x-0.5 rounded-lg px-3 py-2 text-components-button-secondary-accent-text hover:cursor-pointer hover:bg-state-accent-hover'>
<Link href={pricingPageURL} className='system-sm-medium'>{t('billing.plansCommon.comparePlanAndFeatures')}</Link>
<RiArrowRightUpLine className='size-4' />
</div>
</div>
</GridMask> */}
<Footer pricingPageURL={pricingPageURL} />
</div>
</div >,
</div>,
document.body,
)
}

View File

@@ -6,7 +6,7 @@ import type { BasicPlan } from '../../../type'
import { Plan } from '../../../type'
import { ALL_PLANS } from '../../../config'
import Toast from '../../../../base/toast'
import { PlanRange } from '../../select-plan-range'
import { PlanRange } from '../../plan-switcher/plan-range-switcher'
import { useAppContext } from '@/context/app-context'
import { fetchSubscriptionUrls } from '@/service/billing'
import List from './list'
@@ -18,14 +18,14 @@ const ICON_MAP = {
[Plan.team]: <div className='size-[60px] bg-black' />,
}
type PlanItemProps = {
type CloudPlanItemProps = {
currentPlan: BasicPlan
plan: BasicPlan
planRange: PlanRange
canPay: boolean
}
const PlanItem: FC<PlanItemProps> = ({
const CloudPlanItem: FC<CloudPlanItemProps> = ({
plan,
currentPlan,
planRange,
@@ -82,7 +82,7 @@ const PlanItem: FC<PlanItemProps> = ({
}
}
return (
<div className='flex min-w-0 grow flex-col pb-3'>
<div className='flex min-w-0 flex-1 flex-col pb-3'>
<div className='flex flex-col px-5 py-4'>
<div className='flex flex-col gap-y-6 px-1 pt-10'>
{ICON_MAP[plan]}
@@ -129,4 +129,4 @@ const PlanItem: FC<PlanItemProps> = ({
</div>
)
}
export default React.memo(PlanItem)
export default React.memo(CloudPlanItem)

View File

@@ -1,7 +1,8 @@
import Divider from '@/app/components/base/divider'
import { type BasicPlan, Plan, type UsagePlanInfo } from '../../type'
import PlanItem from './plan-item'
import { type BasicPlan, Plan, SelfHostedPlan, type UsagePlanInfo } from '../../type'
import CloudPlanItem from './cloud-plan-item'
import type { PlanRange } from '../plan-switcher/plan-range-switcher'
import SelfHostedPlanItem from './self-hosted-plan-item'
type PlansProps = {
plan: {
@@ -26,21 +27,21 @@ const Plans = ({
{
currentPlan === 'cloud' && (
<>
<PlanItem
<CloudPlanItem
currentPlan={plan.type}
plan={Plan.sandbox}
planRange={planRange}
canPay={canPay}
/>
<Divider type='vertical' className='mx-0 shrink-0 bg-divider-accent' />
<PlanItem
<CloudPlanItem
currentPlan={plan.type}
plan={Plan.professional}
planRange={planRange}
canPay={canPay}
/>
<Divider type='vertical' className='mx-0 shrink-0 bg-divider-accent' />
<PlanItem
<CloudPlanItem
currentPlan={plan.type}
plan={Plan.team}
planRange={planRange}
@@ -50,23 +51,23 @@ const Plans = ({
)
}
{
// currentPlan === 'self' && <>
// <SelfHostedPlanItem
// plan={SelfHostedPlan.community}
// planRange={planRange}
// canPay={canPay}
// />
// <SelfHostedPlanItem
// plan={SelfHostedPlan.premium}
// planRange={planRange}
// canPay={canPay}
// />
// <SelfHostedPlanItem
// plan={SelfHostedPlan.enterprise}
// planRange={planRange}
// canPay={canPay}
// />
// </>
currentPlan === 'self' && <>
<SelfHostedPlanItem
plan={SelfHostedPlan.community}
planRange={planRange}
canPay={canPay}
/>
<SelfHostedPlanItem
plan={SelfHostedPlan.premium}
planRange={planRange}
canPay={canPay}
/>
<SelfHostedPlanItem
plan={SelfHostedPlan.enterprise}
planRange={planRange}
canPay={canPay}
/>
</>
}
</div>
</div>

View File

@@ -3,12 +3,12 @@ import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiBrain2Line, RiCheckLine, RiQuestionLine } from '@remixicon/react'
import { SelfHostedPlan } from '../type'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../config'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
import { Asterisk, AwsMarketplace, Azure, Buildings, Diamond, GoogleCloud } from '../../base/icons/src/public/billing'
import type { PlanRange } from './select-plan-range'
import { SelfHostedPlan } from '../../../type'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../config'
import Toast from '../../../../base/toast'
import Tooltip from '../../../../base/tooltip'
import { Asterisk, AwsMarketplace, Azure, Buildings, Diamond, GoogleCloud } from '../../../../base/icons/src/public/billing'
import type { PlanRange } from '../../plan-switcher/plan-range-switcher'
import cn from '@/utils/classnames'
import { useAppContext } from '@/context/app-context'
@@ -113,8 +113,8 @@ const SelfHostedPlanItem: FC<Props> = ({
<div>
<div className={cn(isEnterprisePlan ? 'z-1 absolute bottom-0 left-0 right-0 top-0 bg-price-enterprise-background' : '')} >
</div>
{isEnterprisePlan && <div className='z-15 absolute -left-[90px] -top-[104px] size-[341px] rounded-full bg-[#09328c] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
{isEnterprisePlan && <div className='z-15 absolute -bottom-[72px] -right-[40px] size-[341px] rounded-full bg-[#e2eafb] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
{isEnterprisePlan && <div className='z-15 absolute left-[-90px] top-[-104px] size-[341px] rounded-full bg-[#09328c] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
{isEnterprisePlan && <div className='z-15 absolute bottom-[-72px] right-[-40px] size-[341px] rounded-full bg-[#e2eafb] opacity-15 mix-blend-plus-darker blur-[80px]'></div>}
</div>
<div className='relative z-10 min-h-[559px] w-full p-6'>
<div className=' flex min-h-[108px] flex-col gap-y-1'>

View File

@@ -1,54 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Switch from '../../base/switch'
export enum PlanRange {
monthly = 'monthly',
yearly = 'yearly',
}
type Props = {
value: PlanRange
onChange: (value: PlanRange) => void
}
const ArrowIcon = (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="29" viewBox="0 0 22 29" fill="none">
<g clipPath="url(#clip0_394_43518)">
<path d="M2.11312 1.64777C2.11312 1.64777 2.10178 1.64849 2.09045 1.6492C2.06211 1.65099 2.08478 1.64956 2.11312 1.64777ZM9.047 20.493C9.43106 19.9965 8.97268 19.2232 8.35639 19.2848C7.72208 19.4215 6.27243 20.3435 5.13995 20.8814C4.2724 21.3798 3.245 21.6892 2.54015 22.4221C1.87751 23.2831 2.70599 23.9706 3.47833 24.3088C4.73679 24.9578 6.00624 25.6004 7.25975 26.2611C8.4424 26.8807 9.57833 27.5715 10.7355 28.2383C10.9236 28.3345 11.1464 28.3489 11.3469 28.2794C11.9886 28.0796 12.0586 27.1137 11.4432 26.8282C9.83391 25.8485 8.17365 24.9631 6.50314 24.0955C8.93023 24.2384 11.3968 24.1058 13.5161 22.7945C16.6626 20.8097 19.0246 17.5714 20.2615 14.0854C22.0267 8.96164 18.9313 4.08153 13.9897 2.40722C10.5285 1.20289 6.76599 0.996166 3.14837 1.46306C2.50624 1.56611 2.68616 1.53201 2.10178 1.64849C2.12445 1.64706 2.14712 1.64563 2.16979 1.6442C2.01182 1.66553 1.86203 1.72618 1.75582 1.84666C1.48961 2.13654 1.58903 2.63096 1.9412 2.80222C2.19381 2.92854 2.4835 2.83063 2.74986 2.81385C3.7267 2.69541 4.70711 2.63364 5.69109 2.62853C8.30015 2.58932 10.5052 2.82021 13.2684 3.693C21.4149 6.65607 20.7135 14.2162 14.6733 20.0304C12.4961 22.2272 9.31209 22.8944 6.11128 22.4816C5.92391 22.4877 5.72342 22.4662 5.52257 22.439C6.35474 22.011 7.20002 21.6107 8.01305 21.1498C8.35227 20.935 8.81233 20.8321 9.05266 20.4926L9.047 20.493Z" fill="url(#paint0_linear_394_43518)" />
</g>
<defs>
<linearGradient id="paint0_linear_394_43518" x1="11" y1="-48.5001" x2="12.2401" y2="28.2518" gradientUnits="userSpaceOnUse">
<stop stopColor="#FDB022" />
<stop offset="1" stopColor="#F79009" />
</linearGradient>
<clipPath id="clip0_394_43518">
<rect width="19.1928" height="27.3696" fill="white" transform="translate(21.8271 27.6475) rotate(176.395)" />
</clipPath>
</defs>
</svg>
)
const SelectPlanRange: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<div className='relative flex flex-col items-end pr-6'>
<div className='bg-premium-yearly-tip-text-background bg-clip-text text-sm italic text-transparent'>{t('billing.plansCommon.yearlyTip')}</div>
<div className='flex items-center py-1'>
<span className='mr-2 text-[13px]'>{t('billing.plansCommon.annualBilling')}</span>
<Switch size='l' defaultValue={value === PlanRange.yearly} onChange={(v) => {
onChange(v ? PlanRange.yearly : PlanRange.monthly)
}} />
</div>
<div className='absolute right-0 top-2'>
{ArrowIcon}
</div>
</div>
)
}
export default React.memo(SelectPlanRange)