重做套餐定价页视觉,去除效果图冗余样式
采用现代 SaaS 定价卡布局,按名称去重并铺满内容区,推荐态改为简洁胶囊标签。
This commit is contained in:
+267
-215
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Switch, Button, message, Spin, Empty, Modal, Form, Input, InputNumber, Select } from 'antd'
|
import { Switch, Button, message, Spin, Empty, Modal, Form, Input, InputNumber, Select, Tooltip } from 'antd'
|
||||||
import {
|
import {
|
||||||
CheckOutlined, CloseOutlined, PlusOutlined, EditOutlined, TeamOutlined,
|
CheckOutlined, PlusOutlined, EditOutlined, TeamOutlined,
|
||||||
CalendarOutlined, ThunderboltOutlined, CustomerServiceOutlined,
|
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { createPlan, getPlans, getTenants, updatePlan, type Plan, type Tenant } from '@/services/api'
|
import { createPlan, getPlans, getTenants, updatePlan, type Plan, type Tenant } from '@/services/api'
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ type FeatureFlags = {
|
|||||||
channels?: string[] | string
|
channels?: string[] | string
|
||||||
brand?: boolean
|
brand?: boolean
|
||||||
dedicated?: boolean | string
|
dedicated?: boolean | string
|
||||||
source?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFeatures(raw: string): FeatureFlags {
|
function parseFeatures(raw: string): FeatureFlags {
|
||||||
@@ -24,61 +22,94 @@ function parseFeatures(raw: string): FeatureFlags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatStorage(days: number) {
|
function formatStorage(days: number) {
|
||||||
if (!days || days <= 0) return '永久'
|
if (!days || days <= 0) return '永久保存'
|
||||||
return `${days} 天`
|
return `${days} 天`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatKB(limit: number) {
|
function formatKB(limit: number) {
|
||||||
if (!limit || limit <= 0) return '不限'
|
if (!limit || limit <= 0) return '不限容量'
|
||||||
return `${limit} 条`
|
return `${limit} 条`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSeats(n: number) {
|
function formatSeats(n: number) {
|
||||||
if (!n || n <= 0 || n >= 999) return '无限'
|
if (!n || n <= 0 || n >= 999) return '不限坐席'
|
||||||
return `${n} 个`
|
return `${n} 坐席`
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelLabel(features: FeatureFlags) {
|
function channelLabel(features: FeatureFlags) {
|
||||||
const ch = features.channels
|
const ch = features.channels
|
||||||
if (!ch) return '网页'
|
if (!ch) return '网页渠道'
|
||||||
if (typeof ch === 'string') {
|
if (typeof ch === 'string') {
|
||||||
if (ch === 'all') return '全渠道'
|
if (ch === 'all') return '全渠道接入'
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
if (ch.includes('all')) return '全渠道'
|
if (ch.includes('all')) return '全渠道接入'
|
||||||
const map: Record<string, string> = { web: '网页', wechat: '微信', app: 'APP', phone: '电话' }
|
const map: Record<string, string> = { web: '网页', wechat: '微信', app: 'APP', phone: '电话' }
|
||||||
return ch.map(c => map[c] || c).join(' + ') || '网页'
|
return ch.map(c => map[c] || c).join(' / ') || '网页渠道'
|
||||||
}
|
}
|
||||||
|
|
||||||
function statsLabel(features: FeatureFlags) {
|
function statsLabel(features: FeatureFlags) {
|
||||||
const s = features.stats
|
const s = features.stats
|
||||||
if (s === 'advanced') return '高级'
|
if (s === 'advanced') return '高级统计'
|
||||||
if (s === 'custom') return '自定义'
|
if (s === 'custom') return '自定义报表'
|
||||||
return '基础'
|
return '基础统计'
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureRow = { label: string; ok: boolean; detail?: string }
|
type Highlight = { text: string; muted?: boolean }
|
||||||
|
|
||||||
function featureRows(plan: Plan): FeatureRow[] {
|
function planHighlights(plan: Plan): Highlight[] {
|
||||||
const f = parseFeatures(plan.features)
|
const f = parseFeatures(plan.features)
|
||||||
return [
|
const items: Highlight[] = [
|
||||||
{ label: '坐席数量', ok: true, detail: formatSeats(plan.seats) },
|
{ text: formatSeats(plan.seats) },
|
||||||
{ label: '对话记录保存', ok: true, detail: formatStorage(plan.storage_days) },
|
{ text: formatStorage(plan.storage_days) },
|
||||||
{ label: '知识库容量', ok: true, detail: formatKB(plan.kb_limit) },
|
{ text: formatKB(plan.kb_limit) },
|
||||||
{ label: '数据统计报表', ok: true, detail: statsLabel(f) },
|
{ text: statsLabel(f) },
|
||||||
{ label: 'API 接口', ok: Boolean(f.api), detail: f.api ? undefined : undefined },
|
{ text: channelLabel(f) },
|
||||||
{ label: '多渠道接入', ok: true, detail: channelLabel(f) },
|
]
|
||||||
{ label: '自定义品牌', ok: Boolean(f.brand) },
|
if (f.api) items.push({ text: '开放 API' })
|
||||||
|
else items.push({ text: '不含 API', muted: true })
|
||||||
|
if (f.brand) items.push({ text: '自定义品牌' })
|
||||||
|
else items.push({ text: '不含品牌定制', muted: true })
|
||||||
|
if (f.dedicated) {
|
||||||
|
items.push({
|
||||||
|
text: typeof f.dedicated === 'string' && f.dedicated !== 'true'
|
||||||
|
? `专属客服 · ${f.dedicated}`
|
||||||
|
: '专属客服 · 7×24',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items.push({ text: '不含专属客服', muted: true })
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡片主题色:按价格档位 */
|
||||||
|
function planTheme(index: number, recommended: boolean) {
|
||||||
|
if (recommended) {
|
||||||
|
return {
|
||||||
|
ring: 'ring-2 ring-[#2563eb]/50',
|
||||||
|
price: 'text-[#2563eb]',
|
||||||
|
accent: 'bg-[#2563eb]',
|
||||||
|
soft: 'from-[#eff6ff] to-white',
|
||||||
|
check: 'bg-[#dbeafe] text-[#2563eb]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const themes = [
|
||||||
{
|
{
|
||||||
label: '专属客服支持',
|
ring: 'ring-1 ring-neutral-200/80',
|
||||||
ok: Boolean(f.dedicated),
|
price: 'text-neutral-900',
|
||||||
detail: f.dedicated === true || f.dedicated === 'true'
|
accent: 'bg-neutral-800',
|
||||||
? '7×24'
|
soft: 'from-neutral-50 to-white',
|
||||||
: typeof f.dedicated === 'string'
|
check: 'bg-neutral-100 text-neutral-600',
|
||||||
? f.dedicated
|
},
|
||||||
: undefined,
|
{
|
||||||
|
ring: 'ring-1 ring-neutral-200/80',
|
||||||
|
price: 'text-neutral-900',
|
||||||
|
accent: 'bg-violet-600',
|
||||||
|
soft: 'from-violet-50/40 to-white',
|
||||||
|
check: 'bg-violet-50 text-violet-600',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
return themes[index % themes.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plans = () => {
|
const Plans = () => {
|
||||||
@@ -117,16 +148,34 @@ const Plans = () => {
|
|||||||
return map
|
return map
|
||||||
}, [tenants])
|
}, [tenants])
|
||||||
|
|
||||||
const sorted = useMemo(
|
const displayPlans = useMemo(() => {
|
||||||
() => [...plans].sort((a, b) => a.price_monthly - b.price_monthly),
|
type Row = Plan & { aliasIds: number[] }
|
||||||
[plans],
|
const map = new Map<string, Row>()
|
||||||
)
|
const ordered = [...plans].sort((a, b) => a.id - b.id)
|
||||||
|
for (const p of ordered) {
|
||||||
|
const key = p.name.trim()
|
||||||
|
const existing = map.get(key)
|
||||||
|
if (!existing) {
|
||||||
|
map.set(key, { ...p, aliasIds: [p.id] })
|
||||||
|
} else {
|
||||||
|
existing.aliasIds.push(p.id)
|
||||||
|
if (p.status === 'active') existing.status = 'active'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...map.values()].sort((a, b) => a.price_monthly - b.price_monthly)
|
||||||
|
}, [plans])
|
||||||
|
|
||||||
|
const tenantCountForPlan = (plan: Plan & { aliasIds?: number[] }) => {
|
||||||
|
const ids = plan.aliasIds || [plan.id]
|
||||||
|
return ids.reduce((s, id) => s + (tenantCountByPlan[id] || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
/** 价格居中的套餐标为推荐(通常专业版) */
|
|
||||||
const recommendId = useMemo(() => {
|
const recommendId = useMemo(() => {
|
||||||
if (sorted.length < 2) return sorted[0]?.id
|
const pro = displayPlans.find(p => p.name.includes('专业'))
|
||||||
return sorted[Math.min(1, sorted.length - 1)]?.id
|
if (pro) return pro.id
|
||||||
}, [sorted])
|
if (displayPlans.length < 2) return displayPlans[0]?.id
|
||||||
|
return displayPlans[Math.floor(displayPlans.length / 2)]?.id
|
||||||
|
}, [displayPlans])
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
@@ -223,179 +272,176 @@ const Plans = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col min-h-0 overflow-hidden bg-neutral-50">
|
<div className="h-full flex flex-col min-h-0 overflow-hidden bg-[#f4f6f9]">
|
||||||
<header
|
<header
|
||||||
className="shrink-0 px-6 flex items-center justify-between border-b border-neutral-200 bg-white"
|
className="shrink-0 px-6 flex items-center justify-between border-b border-neutral-200/80 bg-white/90 backdrop-blur-sm"
|
||||||
style={{ height: 'var(--header-height)' }}
|
style={{ height: 'var(--header-height)' }}
|
||||||
>
|
>
|
||||||
<h1 className="text-base font-semibold text-neutral-900 m-0">套餐管理</h1>
|
<div className="min-w-0">
|
||||||
<Button type="primary" icon={<PlusOutlined />} className="!h-8" onClick={openCreate}>
|
<h1 className="text-[15px] font-semibold text-neutral-900 m-0 tracking-tight">套餐定价</h1>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
className="!h-8 !rounded-lg !px-3.5 !font-medium !shadow-none"
|
||||||
|
onClick={openCreate}
|
||||||
|
>
|
||||||
新建套餐
|
新建套餐
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 overflow-auto px-6 py-5">
|
<div className="flex-1 min-h-0 overflow-auto">
|
||||||
{loading ? (
|
<div className="px-6 py-6 w-full max-w-[1280px] mx-auto">
|
||||||
<div className="h-64 flex items-center justify-center"><Spin size="large" /></div>
|
{loading ? (
|
||||||
) : sorted.length === 0 ? (
|
<div className="h-64 flex items-center justify-center"><Spin size="large" /></div>
|
||||||
<Empty description="暂无套餐">
|
) : displayPlans.length === 0 ? (
|
||||||
<Button type="primary" onClick={openCreate}>新建套餐</Button>
|
<div className="bg-white rounded-2xl border border-neutral-200/80 py-16">
|
||||||
</Empty>
|
<Empty description="还没有套餐">
|
||||||
) : (
|
<Button type="primary" onClick={openCreate}>创建第一个套餐</Button>
|
||||||
<div className="flex flex-col gap-6 w-full max-w-[1200px]">
|
</Empty>
|
||||||
{/* 套餐卡片 */}
|
</div>
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
) : (
|
||||||
{sorted.map(plan => {
|
<>
|
||||||
const recommended = plan.id === recommendId
|
<p className="text-sm text-neutral-500 m-0 mb-6">
|
||||||
const active = plan.status === 'active'
|
管理订阅档位与权益。下架后新租户不可购买,已订阅租户不受影响。
|
||||||
const rows = featureRows(plan)
|
</p>
|
||||||
const checkColor = recommended ? '#2563eb' : '#16a34a'
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={plan.id}
|
|
||||||
className={`relative flex flex-col rounded-xl bg-white p-6 shadow-sm ${
|
|
||||||
recommended
|
|
||||||
? 'border-2 border-[#2563eb] shadow-md'
|
|
||||||
: 'border border-neutral-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{recommended && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="absolute top-0 right-0"
|
|
||||||
style={{
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
borderLeft: '48px solid transparent',
|
|
||||||
borderTop: '48px solid #2563eb',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="absolute top-2 right-1.5 text-[10px] font-semibold text-white whitespace-nowrap"
|
|
||||||
style={{ transform: 'rotate(45deg)' }}
|
|
||||||
>
|
|
||||||
推荐
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-4 pr-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||||
<h2 className="text-lg font-bold text-neutral-900 m-0 truncate">{plan.name}</h2>
|
{displayPlans.map((plan, index) => {
|
||||||
<span
|
const recommended = plan.id === recommendId
|
||||||
className={`inline-flex px-2 py-0.5 rounded-md text-[11px] font-medium whitespace-nowrap ${
|
const active = plan.status === 'active'
|
||||||
active ? 'bg-[#f0fdf4] text-[#16a34a]' : 'bg-neutral-100 text-neutral-500'
|
const theme = planTheme(index, recommended)
|
||||||
}`}
|
const highlights = planHighlights(plan)
|
||||||
>
|
const tenantN = tenantCountForPlan(plan)
|
||||||
{active ? '在售' : '已下架'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-5">
|
return (
|
||||||
<span
|
<article
|
||||||
className={`text-4xl font-bold tabular-nums ${
|
key={plan.id}
|
||||||
recommended ? 'text-[#2563eb]' : 'text-neutral-900'
|
className={`
|
||||||
}`}
|
relative flex flex-col rounded-2xl bg-gradient-to-b ${theme.soft}
|
||||||
>
|
${theme.ring} shadow-[0_1px_2px_rgba(15,23,42,0.04)]
|
||||||
¥{plan.price_monthly}
|
transition-shadow hover:shadow-[0_8px_30px_rgba(15,23,42,0.06)]
|
||||||
</span>
|
`}
|
||||||
<span className="text-sm text-neutral-500 ml-1">/月</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="space-y-2.5 flex-1 mb-5 m-0 p-0 list-none">
|
|
||||||
{rows.map(row => (
|
|
||||||
<li
|
|
||||||
key={row.label}
|
|
||||||
className={`flex items-start gap-2 text-sm ${
|
|
||||||
row.ok ? 'text-neutral-700' : 'text-neutral-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{row.ok ? (
|
|
||||||
<CheckOutlined className="mt-0.5 shrink-0 text-xs" style={{ color: checkColor }} />
|
|
||||||
) : (
|
|
||||||
<CloseOutlined className="mt-0.5 shrink-0 text-xs text-neutral-300" />
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
{row.label}
|
|
||||||
{row.ok && row.detail != null && row.detail !== '' && (
|
|
||||||
<>:<strong className="font-semibold">{row.detail}</strong></>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between pt-4 border-t gap-2 ${
|
|
||||||
recommended ? 'border-blue-100' : 'border-neutral-200'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="text-sm text-neutral-500 flex items-center gap-1 min-w-0">
|
{recommended && (
|
||||||
<TeamOutlined className="shrink-0" />
|
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2 z-10">
|
||||||
<span className="truncate">{tenantCountByPlan[plan.id] || 0} 个租户</span>
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-[#2563eb] text-white shadow-sm">
|
||||||
</span>
|
最受欢迎
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
</span>
|
||||||
<Switch
|
</div>
|
||||||
size="small"
|
)}
|
||||||
checked={active}
|
|
||||||
checkedChildren="上架"
|
<div className="p-6 pb-4 flex-1 flex flex-col">
|
||||||
unCheckedChildren="下架"
|
<div className="flex items-start justify-between gap-2 mb-4">
|
||||||
onChange={v => toggleStatus(plan, v)}
|
<div>
|
||||||
/>
|
<h2 className="text-[17px] font-semibold text-neutral-900 m-0 tracking-tight">
|
||||||
|
{plan.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-neutral-400 m-0 mt-1">
|
||||||
|
{active ? '当前可售' : '已对新增租户关闭'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 text-[11px] font-medium px-2 py-0.5 rounded-full ${
|
||||||
|
active
|
||||||
|
? 'bg-emerald-50 text-emerald-700'
|
||||||
|
: 'bg-neutral-100 text-neutral-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{active ? '在售' : '下架'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-end gap-1">
|
||||||
|
<span className={`text-[2.5rem] font-bold tracking-tight leading-none tabular-nums ${theme.price}`}>
|
||||||
|
¥{plan.price_monthly.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-neutral-400 mb-1.5">/ 月</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400 m-0 mt-2">
|
||||||
|
年付约 ¥{Math.round(plan.price_monthly * 12 * 0.8).toLocaleString()}(8 折)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="m-0 p-0 list-none space-y-2.5 flex-1">
|
||||||
|
{highlights.map(h => (
|
||||||
|
<li key={h.text} className="flex items-center gap-2.5">
|
||||||
|
<span
|
||||||
|
className={`w-5 h-5 rounded-full flex items-center justify-center shrink-0 text-[10px] ${
|
||||||
|
h.muted
|
||||||
|
? 'bg-neutral-100 text-neutral-300'
|
||||||
|
: theme.check
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckOutlined />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-[13px] leading-snug ${
|
||||||
|
h.muted ? 'text-neutral-400 line-through decoration-neutral-300' : 'text-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{h.text}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-4 border-t border-neutral-100/80 bg-white/60 rounded-b-2xl">
|
||||||
|
<div className="flex items-center justify-between gap-3 mb-3">
|
||||||
|
<Tooltip title="当前绑定此套餐的租户数">
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs text-neutral-500">
|
||||||
|
<TeamOutlined />
|
||||||
|
<span className="tabular-nums font-medium text-neutral-700">{tenantN}</span>
|
||||||
|
个租户
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[11px] text-neutral-400">上架</span>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={active}
|
||||||
|
onChange={v => toggleStatus(plan, v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openEdit(plan)}
|
onClick={() => openEdit(plan)}
|
||||||
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium border-0 cursor-pointer ${
|
className={`
|
||||||
recommended
|
w-full h-9 rounded-lg text-sm font-medium cursor-pointer border-0
|
||||||
|
flex items-center justify-center gap-1.5 transition-colors
|
||||||
|
${recommended
|
||||||
? 'bg-[#2563eb] text-white hover:bg-[#1d4ed8]'
|
? 'bg-[#2563eb] text-white hover:bg-[#1d4ed8]'
|
||||||
: 'bg-white text-neutral-700 border border-neutral-200 hover:bg-neutral-50'
|
: 'bg-neutral-900 text-white hover:bg-neutral-800'
|
||||||
}`}
|
}
|
||||||
style={recommended ? undefined : { border: '1px solid #e2e8f0' }}
|
`}
|
||||||
>
|
>
|
||||||
<EditOutlined className="text-xs" />
|
<EditOutlined className="text-xs" />
|
||||||
编辑
|
编辑套餐
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 定价规则 */}
|
|
||||||
<section className="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm">
|
|
||||||
<h3 className="text-base font-bold text-neutral-900 m-0 mb-4">定价规则说明</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
icon: <CalendarOutlined className="text-[#2563eb]" />,
|
|
||||||
title: '按月订阅',
|
|
||||||
desc: '标准月度计费,年付享受 8 折优惠,支持随时升降配',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <ThunderboltOutlined className="text-[#2563eb]" />,
|
|
||||||
title: '超额坐席',
|
|
||||||
desc: '超出套餐坐席数后按每坐席 ¥49/月 额外计费',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <CustomerServiceOutlined className="text-[#2563eb]" />,
|
|
||||||
title: '下架与续费',
|
|
||||||
desc: '下架后新租户不可购买,已购租户可正常续费;企业版可联系销售定制',
|
|
||||||
},
|
|
||||||
].map(item => (
|
|
||||||
<div key={item.title} className="flex items-start gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-md bg-[#dbeafe] flex items-center justify-center shrink-0">
|
|
||||||
{item.icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-sm font-semibold text-neutral-800 m-0">{item.title}</p>
|
|
||||||
<p className="text-sm text-neutral-500 m-0 mt-1 leading-relaxed">{item.desc}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
{/* 底部说明 — 紧凑一行信息条 */}
|
||||||
)}
|
<div className="mt-8 rounded-xl bg-white border border-neutral-200/80 px-5 py-4">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 text-[13px] text-neutral-500">
|
||||||
|
<span className="font-medium text-neutral-700 shrink-0">计费说明</span>
|
||||||
|
<span className="hidden sm:inline text-neutral-200">|</span>
|
||||||
|
<span>年付 8 折</span>
|
||||||
|
<span className="hidden sm:inline text-neutral-200">·</span>
|
||||||
|
<span>超额坐席 ¥49/月</span>
|
||||||
|
<span className="hidden sm:inline text-neutral-200">·</span>
|
||||||
|
<span>下架不影响已购租户续费</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -407,36 +453,40 @@ const Plans = () => {
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
width={520}
|
width={520}
|
||||||
okText="保存"
|
okText="保存"
|
||||||
|
styles={{ body: { paddingTop: 12 } }}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit} className="mt-2">
|
<Form form={form} layout="vertical" onFinish={handleSubmit} requiredMark={false}>
|
||||||
<Form.Item name="name" label="套餐名称" rules={[{ required: true, message: '请输入名称' }]}>
|
<Form.Item name="name" label="套餐名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||||
<Input maxLength={30} placeholder="如:专业版" />
|
<Input maxLength={30} placeholder="如:专业版" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-x-3">
|
||||||
<Form.Item name="price_monthly" label="月费(元)" rules={[{ required: true }]}>
|
<Form.Item name="price_monthly" label="月费(元)" rules={[{ required: true }]}>
|
||||||
<InputNumber min={0} className="!w-full" />
|
<InputNumber min={0} className="!w-full" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="seats" label="坐席数" rules={[{ required: true }]}>
|
<Form.Item name="seats" label="坐席数" rules={[{ required: true }]} extra="0 表示无限">
|
||||||
<InputNumber min={0} className="!w-full" placeholder="0 表示无限" />
|
<InputNumber min={0} className="!w-full" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="storage_days" label="记录保存(天)" rules={[{ required: true }]}>
|
<Form.Item name="storage_days" label="记录保存(天)" rules={[{ required: true }]} extra="0 = 永久">
|
||||||
<InputNumber min={0} className="!w-full" placeholder="0=永久" />
|
<InputNumber min={0} className="!w-full" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="kb_limit" label="知识库容量" rules={[{ required: true }]}>
|
<Form.Item name="kb_limit" label="知识库容量" rules={[{ required: true }]} extra="0 = 不限">
|
||||||
<InputNumber min={0} className="!w-full" placeholder="0=不限" />
|
<InputNumber min={0} className="!w-full" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<Form.Item name="stats" label="统计能力">
|
<Form.Item name="stats" label="统计能力">
|
||||||
<Select options={[
|
<Select
|
||||||
{ value: 'basic', label: '基础' },
|
size="large"
|
||||||
{ value: 'advanced', label: '高级' },
|
options={[
|
||||||
{ value: 'custom', label: '自定义' },
|
{ value: 'basic', label: '基础' },
|
||||||
]}
|
{ value: 'advanced', label: '高级' },
|
||||||
|
{ value: 'custom', label: '自定义' },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="channels" label="渠道">
|
<Form.Item name="channels" label="渠道">
|
||||||
<Select
|
<Select
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
|
size="large"
|
||||||
options={[
|
options={[
|
||||||
{ value: 'web', label: '网页' },
|
{ value: 'web', label: '网页' },
|
||||||
{ value: 'wechat', label: '微信' },
|
{ value: 'wechat', label: '微信' },
|
||||||
@@ -445,7 +495,7 @@ const Plans = () => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
<div className="grid grid-cols-3 gap-3 mb-1 p-3 rounded-xl bg-neutral-50 border border-neutral-100">
|
||||||
<Form.Item name="api" label="API" valuePropName="checked" className="!mb-0">
|
<Form.Item name="api" label="API" valuePropName="checked" className="!mb-0">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -457,11 +507,13 @@ const Plans = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
{editing && (
|
{editing && (
|
||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="销售状态" className="!mt-3">
|
||||||
<Select options={[
|
<Select
|
||||||
{ value: 'active', label: '在售' },
|
size="large"
|
||||||
{ value: 'inactive', label: '下架' },
|
options={[
|
||||||
]}
|
{ value: 'active', label: '在售' },
|
||||||
|
{ value: 'inactive', label: '下架' },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user