From bf80d3d8017db1770a7bde0a3c28fa5022665eb1 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Thu, 28 May 2026 18:18:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=99=BB=E5=BD=95=E6=BF=80?= =?UTF-8?q?=E6=B4=BB=E7=A7=81=E6=9C=89=E5=8C=96=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/activate/__tests__/page.test.tsx | 67 +++++++++++----------- app/activate/page.tsx | 74 +++++++++++++++---------- app/api/system-settings/route.ts | 1 + app/login/page.tsx | 30 +++++++--- docs/商业化地基排查问题清单_20260524.md | 5 +- 5 files changed, 107 insertions(+), 70 deletions(-) diff --git a/app/activate/__tests__/page.test.tsx b/app/activate/__tests__/page.test.tsx index 5876f7f..d7ae4c4 100644 --- a/app/activate/__tests__/page.test.tsx +++ b/app/activate/__tests__/page.test.tsx @@ -3,7 +3,7 @@ */ /** * 激活页面测试 - * 测试许可证激活和免费试用功能 + * 测试许可证激活和自助试用功能 */ import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import ActivatePage from '../page'; @@ -60,6 +60,7 @@ describe('ActivatePage', () => { beforeEach(() => { jest.clearAllMocks(); (global.fetch as jest.Mock).mockReset(); + delete process.env.NEXT_PUBLIC_ENABLE_FREE_TRIAL_ACTIVATION; mockFetchRoutes({ '/api/license/check': createJsonResponse({ activated: false }), '*': createJsonResponse({ success: true }), @@ -71,20 +72,20 @@ describe('ActivatePage', () => { await renderPage(); expect(screen.getByText('FLNXI 素材系统')).toBeInTheDocument(); - expect(screen.getByText('请激活您的许可证')).toBeInTheDocument(); + expect(screen.getByText('请输入交付给您的许可证密钥')).toBeInTheDocument(); expect(screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument(); expect(screen.getByText('激活许可证')).toBeInTheDocument(); - expect(screen.getByText('免费试用')).toBeInTheDocument(); + expect(screen.queryByText('自助试用')).not.toBeInTheDocument(); }); - it('应该显示免费版限制说明', async () => { + it('默认不显示自助试用说明', async () => { await renderPage(); - expect(screen.getByText('免费版限制:10 个用户、100 个素材')).toBeInTheDocument(); + expect(screen.queryByText('自助试用会按当前授权服务规则生成受限许可证')).not.toBeInTheDocument(); }); it('应该显示联系信息', async () => { await renderPage(); - expect(screen.getByText('需要购买许可证?请联系我们')).toBeInTheDocument(); + expect(screen.getByText('如需许可证,请联系系统交付人员或管理员')).toBeInTheDocument(); }); }); @@ -285,24 +286,28 @@ describe('ActivatePage', () => { }); }); - describe('免费试用', () => { - it('点击免费试用按钮应该显示客户名称输入弹窗', async () => { + describe('自助试用(显式开关开启时)', () => { + beforeEach(() => { + process.env.NEXT_PUBLIC_ENABLE_FREE_TRIAL_ACTIVATION = 'true'; + }); + + it('点击自助试用按钮应该显示交付单位输入弹窗', async () => { await renderPage(); - const button = screen.getByText('免费试用'); + const button = screen.getByText('自助试用'); await act(async () => { fireEvent.click(button); }); - expect(screen.getByText('申请免费试用')).toBeInTheDocument(); - expect(screen.getByPlaceholderText('请输入您的公司或个人名称')).toBeInTheDocument(); + expect(screen.getByText('申请自助试用')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('请输入单位或团队名称')).toBeInTheDocument(); expect(screen.getByText('确认申请')).toBeInTheDocument(); expect(screen.getByText('取消')).toBeInTheDocument(); }); it('未输入客户名称时确认申请按钮应该禁用', async () => { await renderPage(); - const freeTrialButton = screen.getByText('免费试用'); + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); @@ -314,13 +319,13 @@ describe('ActivatePage', () => { it('只输入客户名称未输入联系方式时确认申请按钮应该禁用', async () => { await renderPage(); - const freeTrialButton = screen.getByText('免费试用'); + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); - const nameInput = screen.getByPlaceholderText('请输入您的公司或个人名称'); + const nameInput = screen.getByPlaceholderText('请输入单位或团队名称'); fireEvent.change(nameInput, { target: { value: '测试公司' } }); const confirmButton = screen.getByText('确认申请'); @@ -329,13 +334,13 @@ describe('ActivatePage', () => { it('输入客户名称和联系方式后确认申请按钮应该启用', async () => { await renderPage(); - const freeTrialButton = screen.getByText('免费试用'); + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); - const nameInput = screen.getByPlaceholderText('请输入您的公司或个人名称'); + const nameInput = screen.getByPlaceholderText('请输入单位或团队名称'); const contactInput = screen.getByPlaceholderText('手机号/微信/邮箱'); fireEvent.change(nameInput, { target: { value: '测试公司' } }); fireEvent.change(contactInput, { target: { value: '13800138000' } }); @@ -346,23 +351,23 @@ describe('ActivatePage', () => { it('点击取消按钮应该关闭弹窗', async () => { await renderPage(); - const freeTrialButton = screen.getByText('免费试用'); + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); - expect(screen.getByText('申请免费试用')).toBeInTheDocument(); + expect(screen.getByText('申请自助试用')).toBeInTheDocument(); const cancelButton = screen.getByText('取消'); await act(async () => { fireEvent.click(cancelButton); }); - expect(screen.queryByText('申请免费试用')).not.toBeInTheDocument(); + expect(screen.queryByText('申请自助试用')).not.toBeInTheDocument(); }); - it('免费试用成功后应该显示成功页面', async () => { + it('自助试用成功后应该显示成功页面', async () => { mockFetchRoutes({ '/api/license/check': createJsonResponse({ activated: false }), '/api/license/activate-free': createJsonResponse({ @@ -378,14 +383,14 @@ describe('ActivatePage', () => { await renderPage(); - // 点击免费试用按钮打开弹窗 - const freeTrialButton = screen.getByText('免费试用'); + // 点击自助试用按钮打开弹窗 + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); // 输入客户名称和联系方式 - const nameInput = screen.getByPlaceholderText('请输入您的公司或个人名称'); + const nameInput = screen.getByPlaceholderText('请输入单位或团队名称'); const contactInput = screen.getByPlaceholderText('手机号/微信/邮箱'); fireEvent.change(nameInput, { target: { value: '测试公司' } }); fireEvent.change(contactInput, { target: { value: '13800138000' } }); @@ -408,7 +413,7 @@ describe('ActivatePage', () => { }); }); - it('免费试用失败应该显示错误信息', async () => { + it('自助试用失败应该显示错误信息', async () => { mockFetchRoutes({ '/api/license/check': createJsonResponse({ activated: false }), '/api/license/activate-free': createJsonResponse({ success: false, error: '试用已过期' }), @@ -416,14 +421,14 @@ describe('ActivatePage', () => { await renderPage(); - // 点击免费试用按钮打开弹窗 - const freeTrialButton = screen.getByText('免费试用'); + // 点击自助试用按钮打开弹窗 + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); // 输入客户名称和联系方式 - const nameInput = screen.getByPlaceholderText('请输入您的公司或个人名称'); + const nameInput = screen.getByPlaceholderText('请输入单位或团队名称'); const contactInput = screen.getByPlaceholderText('手机号/微信/邮箱'); fireEvent.change(nameInput, { target: { value: '测试公司' } }); fireEvent.change(contactInput, { target: { value: '13800138000' } }); @@ -447,14 +452,14 @@ describe('ActivatePage', () => { await renderPage(); - // 点击免费试用按钮打开弹窗 - const freeTrialButton = screen.getByText('免费试用'); + // 点击自助试用按钮打开弹窗 + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); }); // 输入客户名称和联系方式 - const nameInput = screen.getByPlaceholderText('请输入您的公司或个人名称'); + const nameInput = screen.getByPlaceholderText('请输入单位或团队名称'); const contactInput = screen.getByPlaceholderText('手机号/微信/邮箱'); fireEvent.change(nameInput, { target: { value: '测试公司' } }); fireEvent.change(contactInput, { target: { value: '13800138000' } }); @@ -472,7 +477,7 @@ describe('ActivatePage', () => { it('应该显示联系方式输入框', async () => { await renderPage(); - const freeTrialButton = screen.getByText('免费试用'); + const freeTrialButton = screen.getByText('自助试用'); await act(async () => { fireEvent.click(freeTrialButton); diff --git a/app/activate/page.tsx b/app/activate/page.tsx index 0036529..74919d4 100644 --- a/app/activate/page.tsx +++ b/app/activate/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; export default function ActivatePage() { const router = useRouter(); + const freeTrialActivationEnabled = process.env.NEXT_PUBLIC_ENABLE_FREE_TRIAL_ACTIVATION === 'true'; const [initialAdmin, setInitialAdmin] = useState<{ email: string; username: string; @@ -90,15 +91,25 @@ export default function ActivatePage() { } }; - // 打开免费试用弹窗 + // 打开自助试用弹窗 const openFreeTrialModal = () => { + if (!freeTrialActivationEnabled) { + setError('当前部署未开放自助试用,请使用许可证密钥或联系系统交付人员'); + return; + } setCustomerName(''); setContactInfo(''); setShowFreeTrialModal(true); }; - // 免费试用 + // 自助试用 const handleFreeTrial = async () => { + if (!freeTrialActivationEnabled) { + setError('当前部署未开放自助试用,请使用许可证密钥或联系系统交付人员'); + setShowFreeTrialModal(false); + return; + } + const trimmedName = customerName.trim(); const trimmedContact = contactInfo.trim(); @@ -238,7 +249,7 @@ export default function ActivatePage() { {/* Logo */}

FLNXI 素材系统

-

请激活您的许可证

+

请输入交付给您的许可证密钥

{/* 许可证输入 */} @@ -273,36 +284,40 @@ export default function ActivatePage() { {loading ? '激活中...' : '激活许可证'} - {/* 分隔线 */} -
-
- 或 -
-
+ {freeTrialActivationEnabled && ( + <> + {/* 分隔线 */} +
+
+ 或 +
+
- {/* 免费试用 */} - + {/* 自助试用 */} + - {/* 免费版说明 */} -

- 免费版限制:10 个用户、100 个素材 -

+ {/* 免费版说明 */} +

+ 自助试用会按当前授权服务规则生成受限许可证 +

+ + )} {/* 联系信息 */}

- 需要购买许可证?请联系我们 + 如需许可证,请联系系统交付人员或管理员

- {/* 免费试用弹窗 */} - {showFreeTrialModal && ( + {/* 自助试用弹窗 */} + {freeTrialActivationEnabled && showFreeTrialModal && (
-

申请免费试用

+

申请自助试用

setCustomerName(e.target.value)} - placeholder="请输入您的公司或个人名称" + placeholder="请输入单位或团队名称" className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white" autoFocus required @@ -338,17 +353,18 @@ export default function ActivatePage() { className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white" required /> -

用于许可证管理和售后服务

+

用于授权联系和交付核验

diff --git a/docs/商业化地基排查问题清单_20260524.md b/docs/商业化地基排查问题清单_20260524.md index 9836252..3e20737 100644 --- a/docs/商业化地基排查问题清单_20260524.md +++ b/docs/商业化地基排查问题清单_20260524.md @@ -1137,19 +1137,20 @@ NAS 私有化系统可能在平板或窄屏浏览器里使用。模板编辑器 证据: - `app/login/page.tsx:11`:默认登录 Logo 不再指向不存在的 `/logo/logo2.png`,没有配置时直接走文字占位。 +- `app/api/system-settings/route.ts`、`app/login/page.tsx`:公共系统设置现在返回注册开关;登录页不再固定展示“立即注册”,注册开启时改为“申请加入”,关闭时提示联系管理员开通。 - `app/login/page.tsx:295`:登录页已改为浅灰背景和中性表单,不再使用浅蓝渐变背景。 - `app/register/page.tsx:21`:注册页已改为和登录页一致的浅灰背景。 - `app/register/page.tsx:88`:注册页已增加默认 Logo 兜底,没有配置图片时显示文字占位,不再显示破图。 - `app/complete-profile/page.tsx:25`:完善资料页已改为和登录页一致的浅灰背景。 - `app/complete-profile/page.tsx:205`:完善资料页已增加默认 Logo 兜底。 -- `app/activate/page.tsx`:激活页已完成第一轮收口,激活表单、激活成功页和免费试用弹窗统一为浅灰背景、普通边框、`rounded-lg` 和轻阴影;许可证输入去掉大字距和偏展示页的大号字体,蓝色主按钮改为中性深色按钮。 +- `app/activate/page.tsx`:激活页已完成第二轮收口,默认只展示许可证密钥激活;“免费试用/购买许可证”入口已改为显式环境开关下的“自助试用”,客户默认安装现场不再露出公网获客口径。 - `app/dingtalk-callback/page.tsx`:钉钉回调页已完成第一轮收口,蓝色渐变整屏背景改为浅灰背景,`rounded-2xl shadow-xl` 大卡片改为 `rounded-lg border shadow-sm` 普通状态卡片,加载/成功/失败状态复用同一个中性外壳。 - 代码复扫:`app/dingtalk-callback/page.tsx` 已无 `bg-gradient`、`rounded-2xl`、`shadow-xl`、大尺寸状态图标等命中。 - 验证限制:本轮尝试用 Codex 内置浏览器打开钉钉回调页时返回“没有可用活动面板”,因此该页暂未补截图;已完成 `npm run type-check` 和 `npm run lint` 验证。 建议动作: -- 登录、激活、回调、完善资料页统一成同一套私有化安装/访问风格;钉钉回调页和激活页已完成第一轮视觉收口。 +- 登录、激活、回调、完善资料页统一成同一套私有化安装/访问风格;钉钉回调页和激活页已完成第一轮视觉收口,激活页公网试用/购买口径已进一步隐藏。 - 背景用纯色或轻灰,不用大面积渐变。 - 登录页可以保留品牌感,但减少泛 SaaS 视觉:小 Logo、清晰表单、明确客户系统名称即可。