feat: 实现试用用户欢迎引导和过期检测功能

- 新增试用用户首次登录欢迎弹窗,展示使用教程
- 新增试用期过期检测服务和过期提醒弹窗
- 从 JWT token 中提取 ispluginTrial 标识判断用户类型
- 试用用户跳过邀请码验证流程
- 在消息发送前检查试用期是否过期
- 新增 ExpiredPanel 和 WelcomePanel 面板组件
- 新增 expiredModal 和 welcomeModal 视图组件
- 优化用户登录流程,根据用户类型显示不同引导
This commit is contained in:
Roe-xin
2026-02-26 15:42:18 +08:00
parent 316c784bde
commit 208c24682b
12 changed files with 924 additions and 7 deletions

View File

@ -310,6 +310,8 @@ export async function activate(context: vscode.ExtensionContext) {
logoutCommand, logoutCommand,
changeInvitationCodeCommand, changeInvitationCodeCommand,
testNotificationCommand, testNotificationCommand,
testTrialUserCommand,
testExpiredUserCommand,
// TODO: 等待重新实现这些命令 // TODO: 等待重新实现这些命令
// viewHistoryCommand, // viewHistoryCommand,
// newSessionCommand, // newSessionCommand,

View File

@ -0,0 +1,78 @@
/**
* 试用期到期提醒面板
* 功能:试用期到期时显示续费提示
* 依赖vscode
* 使用场景:试用用户到期时显示
*/
import * as vscode from 'vscode';
export class ExpiredPanel {
public static render() {
const panel = vscode.window.createWebviewPanel(
'icCoderExpired',
'试用期已到期',
vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = this.getHtmlContent();
}
private static getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body {
padding: 60px 40px;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-errorForeground);
font-size: 28px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin: 15px 0;
color: var(--vscode-descriptionForeground);
}
.button {
padding: 12px 30px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>⏰ 您的试用期已到期</h1>
<p>感谢您使用 IC Coder您的 15 天试用期已结束。</p>
<p>如需继续使用,请联系我们获取正式版本。</p>
<button class="button" onclick="contact()">联系我们</button>
<script>
function contact() {
window.open('https://iccoder.com/contact', '_blank');
}
</script>
</body>
</html>
`;
}
}

View File

@ -426,12 +426,55 @@ export async function showICHelperPanel(
case "checkInvitationCode": case "checkInvitationCode":
// 检查邀请码验证状态 // 检查邀请码验证状态
{ {
const { InvitationService } = require("../services/invitationService"); // 先检查是否是试用用户
const isVerified = await InvitationService.isVerified(context); const { getCachedUserInfo } = require("../services/userService");
panel.webview.postMessage({ const userInfo = getCachedUserInfo();
command: "invitationCodeStatus",
verified: isVerified if (userInfo?.isPluginTrial === true) {
}); // 试用用户,跳过邀请码验证,直接返回已验证
console.log('[ICHelperPanel] 试用用户,跳过邀请码验证');
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: true
});
} else {
// 正式用户,检查邀请码
const { InvitationService } = require("../services/invitationService");
const isVerified = await InvitationService.isVerified(context);
panel.webview.postMessage({
command: "invitationCodeStatus",
verified: isVerified
});
}
}
break;
case "checkWelcomeModal":
// 检查是否需要显示欢迎弹窗
{
console.log('[ICHelperPanel] 收到 checkWelcomeModal 消息');
const showWelcome = context.globalState.get('showWelcomeModal');
console.log('[ICHelperPanel] showWelcomeModal 标记值:', showWelcome);
if (showWelcome) {
// 清除标记并显示欢迎弹窗
await context.globalState.update('showWelcomeModal', undefined);
console.log('[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端');
panel.webview.postMessage({
command: "showWelcomeModal"
});
} else {
console.log('[ICHelperPanel] showWelcomeModal 标记为 false不显示弹窗');
}
}
break;
case "checkTrialExpiration":
// 检查试用期是否过期
{
console.log('[ICHelperPanel] 收到 checkTrialExpiration 消息');
const { TrialExpirationService } = require("../services/trialExpirationService");
const trialService = new TrialExpirationService(context, panel);
const isExpired = await trialService.checkExpiration();
console.log('[ICHelperPanel] 试用期过期状态:', isExpired);
} }
break; break;
case "verifyInvitationCode": case "verifyInvitationCode":

153
src/panels/WelcomePanel.ts Normal file
View File

@ -0,0 +1,153 @@
/**
* 欢迎引导面板
* 功能:插件试用用户首次登录显示使用教程
* 依赖vscode
* 使用场景:试用用户首次登录时显示
*/
import * as vscode from 'vscode';
export class WelcomePanel {
public static currentPanel: WelcomePanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel) {
this._panel = panel;
this._panel.webview.html = this.getHtmlContent();
// 监听来自 webview 的消息
this._panel.webview.onDidReceiveMessage(
(message) => {
if (message.command === 'close') {
this._panel.dispose();
}
},
null,
this._disposables
);
// 监听关闭事件
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}
public static render(context: vscode.ExtensionContext) {
// 避免重复显示
if (WelcomePanel.currentPanel) {
WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
return;
}
const panel = vscode.window.createWebviewPanel(
'icCoderWelcome',
'欢迎使用 IC Coder',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
WelcomePanel.currentPanel = new WelcomePanel(panel);
}
private getHtmlContent(): string {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>欢迎使用 IC Coder</title>
<style>
body {
padding: 40px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
h1 {
color: var(--vscode-textLink-foreground);
margin-bottom: 20px;
}
.welcome-message {
font-size: 16px;
margin-bottom: 30px;
color: var(--vscode-descriptionForeground);
}
.step {
margin: 20px 0;
padding: 20px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.step h3 {
margin-top: 0;
color: var(--vscode-textLink-foreground);
}
.step p {
margin: 10px 0;
}
.button {
padding: 12px 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 20px;
}
.button:hover {
background: var(--vscode-button-hoverBackground);
}
</style>
</head>
<body>
<h1>🎉 欢迎使用 IC Coder</h1>
<p class="welcome-message">
您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
</p>
<div class="step">
<h3>📝 步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
</div>
<div class="step">
<h3>💬 步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</div>
<div class="step">
<h3>🔬 步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
<button class="button" onclick="closePanel()">开始使用</button>
<script>
const vscode = acquireVsCodeApi();
function closePanel() {
vscode.postMessage({ command: 'close' });
}
</script>
</body>
</html>
`;
}
public dispose() {
WelcomePanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
if (disposable) {
disposable.dispose();
}
}
}
}

View File

@ -0,0 +1,62 @@
/**
* 试用期过期检测服务
* 功能:检查插件试用用户是否过期
* 依赖vscode, userService
* 使用场景:用户使用功能前检查是否过期
*/
import * as vscode from 'vscode';
import { getCachedUserInfo } from './userService';
export class TrialExpirationService {
private context: vscode.ExtensionContext;
private panel?: vscode.WebviewPanel;
constructor(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel) {
this.context = context;
this.panel = panel;
}
/**
* 检查是否过期
* @returns true=已过期false=未过期
*/
public async checkExpiration(): Promise<boolean> {
const userInfo = getCachedUserInfo();
// 不是插件试用用户,不需要检查
if (!userInfo?.isPluginTrial) {
return false;
}
// 没有过期时间,不检查
if (!userInfo.pluginTrialExpiresAt) {
return false;
}
// 检查是否过期
const now = Date.now();
if (now >= userInfo.pluginTrialExpiresAt) {
// 已过期
await this.handleExpired();
return true;
}
return false;
}
/**
* 处理过期逻辑
*/
private async handleExpired(): Promise<void> {
// 通知前端显示过期弹窗
if (this.panel) {
this.panel.webview.postMessage({
command: 'showExpiredModal'
});
console.log('[TrialExpirationService] 已通知前端显示过期弹窗');
} else {
console.warn('[TrialExpirationService] panel 未提供,无法显示过期弹窗');
}
}
}

View File

@ -9,6 +9,8 @@ import * as vscode from 'vscode';
import { getStrangeLoopApiUrl, getConfig } from '../config/settings'; import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api'; import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
import { fetchBalanceWithToken, getCachedBalance } from './creditsService'; import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
import { getIsPluginTrialFromToken } from '../utils/jwtUtils';
// 移除 WelcomePanel 导入,改用消息通知方式
/** /**
* HTTP 请求选项 * HTTP 请求选项
@ -117,6 +119,10 @@ export interface UserInfo {
}; };
// Credits 余额 // Credits 余额
credits?: number; credits?: number;
// 插件试用用户标识(从 JWT token 中提取)
isPluginTrial?: boolean;
// 试用到期时间(毫秒时间戳)
pluginTrialExpiresAt?: number;
} }
/** /**
@ -226,6 +232,10 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
try { try {
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...'); console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
// 从 token 中提取 ispluginTrial 标识
const isPluginTrial = getIsPluginTrialFromToken(token);
console.log('[UserService] 从 token 中提取 ispluginTrial:', isPluginTrial);
// 并行获取用户信息、会员信息和余额 // 并行获取用户信息、会员信息和余额
const [userInfo, membershipInfo, credits] = await Promise.all([ const [userInfo, membershipInfo, credits] = await Promise.all([
getUserInfo(token), getUserInfo(token),
@ -238,6 +248,12 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
return null; return null;
} }
// 将 token 中的 ispluginTrial 标识添加到用户信息
if (isPluginTrial !== null) {
userInfo.isPluginTrial = isPluginTrial;
console.log('[UserService] 已将 ispluginTrial 添加到用户信息:', isPluginTrial);
}
// 添加 Credits 余额到用户信息 // 添加 Credits 余额到用户信息
console.log('[UserService] 获取到的 Credits 余额:', credits); console.log('[UserService] 获取到的 Credits 余额:', credits);
if (credits !== null) { if (credits !== null) {
@ -313,6 +329,34 @@ export async function onTokenReceived(token: string): Promise<UserInfo | null> {
// 保存到持久化存储 // 保存到持久化存储
await saveUserInfo(userInfo); await saveUserInfo(userInfo);
// 判断是否是插件试用用户
console.log('[UserService] 检查用户类型isPluginTrial:', userInfo.isPluginTrial);
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
if (userInfo.isPluginTrial === true) {
// 插件试用用户:标记需要显示欢迎弹窗
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
if (!hasWelcomed && extensionContext) {
// 设置标记,让聊天面板显示欢迎弹窗
await extensionContext.globalState.update('showWelcomeModal', true);
await extensionContext.globalState.update('pluginTrialWelcomed', true);
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
// 验证标记是否设置成功
const checkMark = extensionContext.globalState.get('showWelcomeModal');
console.log('[UserService] 验证标记:', checkMark);
} else if (!extensionContext) {
console.error('[UserService] ❌ extensionContext 为 null无法设置标记');
} else {
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
}
} else {
// 正式用户:显示邀请码弹窗(现有逻辑)
console.log('[UserService] 正式用户登录,将在面板中检查邀请码');
}
return userInfo; return userInfo;
} catch (error) { } catch (error) {
console.error('[UserService] 获取用户信息失败:', error); console.error('[UserService] 获取用户信息失败:', error);

View File

@ -11,6 +11,7 @@ export interface JwtPayload {
user_id?: number; // 用户ID (下划线命名) user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间 exp?: number; // 过期时间
iat?: number; // 签发时间 iat?: number; // 签发时间
ispluginTrial?: boolean; // 是否是插件试用用户
[key: string]: unknown; [key: string]: unknown;
} }
@ -102,3 +103,24 @@ export function isTokenExpired(
return isExpired; return isExpired;
} }
/**
* 从 JWT token 中获取 ispluginTrial 标识
* @param token JWT token
* @returns true=插件试用用户false=正式用户null=无法判断
*/
export function getIsPluginTrialFromToken(token: string): boolean | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
// 检查 ispluginTrial 字段
if (payload.ispluginTrial !== undefined) {
console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
return payload.ispluginTrial === true;
}
console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
return false;
}

View File

@ -25,6 +25,7 @@ import {
} from "../services/creditsService"; } from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService"; import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService"; import { NotificationService } from "../services/notificationService";
import { TrialExpirationService } from "../services/trialExpirationService";
import type { RunMode, ServiceTier } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
@ -124,6 +125,21 @@ export async function handleUserMessage(
}); });
return; return;
} }
// 检查试用期是否过期
const trialService = new TrialExpirationService(context, panel);
const isExpired = await trialService.checkExpiration();
if (isExpired) {
console.warn("[MessageHandler] 试用期已过期,阻止发送");
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
} }
// 记录用户消息到历史(允许失败,不阻塞主流程) // 记录用户消息到历史(允许失败,不阻塞主流程)

218
src/views/expiredModal.ts Normal file
View File

@ -0,0 +1,218 @@
/**
* 试用期过期弹窗
* 功能:在聊天面板内显示过期提醒模态窗口
* 依赖:无
* 使用场景:试用用户过期时在聊天面板内显示
*/
/**
* 获取过期弹窗的 HTML 内容
*/
export function getExpiredModalContent(logoUri?: string): string {
return `
<!-- 过期弹窗 -->
<div id="expiredModal" class="expired-modal" style="display: none;">
<div class="expired-modal-overlay"></div>
<div class="expired-modal-content">
${logoUri ? `<img src="${logoUri}" class="expired-logo-corner" alt="IC Coder" />` : ""}
<div class="expired-modal-header">
<div class="expired-icon">⏰</div>
<h2>您的试用期已到期</h2>
<p class="expired-modal-subtitle">感谢您使用 IC Coder您的 15 天试用期已结束。</p>
</div>
<div class="expired-modal-body">
<p class="expired-message">如需继续使用,请联系我们获取正式版本。</p>
<button id="expiredContactBtn" class="expired-btn expired-btn-primary">
<span>联系我们</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
`;
}
/**
* 获取过期弹窗的 CSS 样式
*/
export function getExpiredModalStyles(): string {
return `
/* 过期弹窗样式 */
.expired-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
}
.expired-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease-out;
}
.expired-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 450px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.expired-logo-corner {
position: absolute;
top: 16px;
left: 24px;
height: 40px;
width: auto;
opacity: 0.9;
z-index: 10;
}
.expired-modal-header {
padding: 60px 32px 20px;
text-align: center;
}
.expired-icon {
font-size: 48px;
margin-bottom: 16px;
}
.expired-modal-header h2 {
margin: 0 0 12px;
font-size: 24px;
font-weight: 600;
color: var(--vscode-errorForeground);
}
.expired-modal-subtitle {
margin: 0;
font-size: 14px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.expired-modal-body {
padding: 0 32px 32px;
text-align: center;
}
.expired-message {
font-size: 14px;
color: var(--vscode-descriptionForeground);
margin: 20px 0;
line-height: 1.6;
}
.expired-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
transition: all 0.2s;
margin-top: 24px;
}
.expired-btn:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.expired-btn:active {
transform: translateY(0);
}
`;
}
/**
* 获取过期弹窗的 JavaScript 逻辑
*/
export function getExpiredModalScript(): string {
return `
// 过期弹窗逻辑
(function() {
const modal = document.getElementById('expiredModal');
const contactBtn = document.getElementById('expiredContactBtn');
const overlay = modal?.querySelector('.expired-modal-overlay');
// 显示过期弹窗
window.showExpiredModal = function() {
if (modal) {
modal.style.display = 'flex';
}
};
// 隐藏过期弹窗
window.hideExpiredModal = function() {
if (modal) {
modal.style.display = 'none';
}
};
// 点击"联系我们"按钮
if (contactBtn) {
contactBtn.addEventListener('click', function() {
// 可以打开联系页面
// window.open('https://iccoder.com/contact', '_blank');
hideExpiredModal();
});
}
// 点击遮罩层关闭弹窗
if (overlay) {
overlay.addEventListener('click', function() {
hideExpiredModal();
});
}
// 阻止点击弹窗内容时关闭
const content = modal?.querySelector('.expired-modal-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// 监听来自后端的消息
window.addEventListener('message', function(event) {
const message = event.data;
if (message.command === 'showExpiredModal') {
showExpiredModal();
}
});
})();
`;
}

View File

@ -339,12 +339,14 @@ export function getInputAreaScript(): string {
if (messageInput) { if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea); messageInput.addEventListener('input', autoResizeTextarea);
// 监听点击事件,检测工作区状态和邀请码验证状态 // 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
messageInput.addEventListener('focus', () => { messageInput.addEventListener('focus', () => {
if (!hasCheckedWorkspace) { if (!hasCheckedWorkspace) {
hasCheckedWorkspace = true; hasCheckedWorkspace = true;
vscode.postMessage({ command: 'checkWorkspace' }); vscode.postMessage({ command: 'checkWorkspace' });
} }
// 检查试用期是否过期
vscode.postMessage({ command: 'checkTrialExpiration' });
// 检查邀请码验证状态 // 检查邀请码验证状态
vscode.postMessage({ command: 'checkInvitationCode' }); vscode.postMessage({ command: 'checkInvitationCode' });
}); });

View File

@ -30,6 +30,16 @@ import {
getInvitationModalStyles, getInvitationModalStyles,
getInvitationModalScript, getInvitationModalScript,
} from "./invitationModal"; } from "./invitationModal";
import {
getWelcomeModalContent,
getWelcomeModalStyles,
getWelcomeModalScript,
} from "./welcomeModal";
import {
getExpiredModalContent,
getExpiredModalStyles,
getExpiredModalScript,
} from "./expiredModal";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
*/ */
@ -100,6 +110,8 @@ export function getWebviewContent(
${getProgressBarStyles()} ${getProgressBarStyles()}
${getInputAreaStyles()} ${getInputAreaStyles()}
${getInvitationModalStyles()} ${getInvitationModalStyles()}
${getWelcomeModalStyles()}
${getExpiredModalStyles()}
.file-editor-section { .file-editor-section {
margin-bottom: 15px; margin-bottom: 15px;
@ -466,6 +478,8 @@ export function getWebviewContent(
${getConversationHistoryBarContent()} ${getConversationHistoryBarContent()}
${getProgressBarContent()} ${getProgressBarContent()}
${getInvitationModalContent(qrCodeUri, logoUri)} ${getInvitationModalContent(qrCodeUri, logoUri)}
${getWelcomeModalContent(logoUri)}
${getExpiredModalContent(logoUri)}
<div class="header"> <div class="header">
<div style="display: flex; align-items: center; justify-content: center;"> <div style="display: flex; align-items: center; justify-content: center;">
<img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" /> <img src="${logoUri}" alt="IC Coder" style="max-width: 100%; height: auto; max-height: 80px;" />
@ -873,6 +887,8 @@ export function getWebviewContent(
${getProgressBarScript()} ${getProgressBarScript()}
${getInputAreaScript()} ${getInputAreaScript()}
${getInvitationModalScript()} ${getInvitationModalScript()}
${getWelcomeModalScript()}
${getExpiredModalScript()}
</script></body> </script></body>
</html>`; </html>`;
} }

261
src/views/welcomeModal.ts Normal file
View File

@ -0,0 +1,261 @@
/**
* 欢迎弹窗(试用用户)
* 功能:在聊天面板内显示欢迎模态窗口
* 依赖:无
* 使用场景:试用用户首次登录时在聊天面板内显示
*/
/**
* 获取欢迎弹窗的 HTML 内容
*/
export function getWelcomeModalContent(logoUri?: string): string {
return `
<!-- 欢迎弹窗 -->
<div id="welcomeModal" class="welcome-modal" style="display: none;">
<div class="welcome-modal-overlay"></div>
<div class="welcome-modal-content">
${logoUri ? `<img src="${logoUri}" class="welcome-logo-corner" alt="IC Coder" />` : ""}
<div class="welcome-modal-header">
<div class="welcome-icon">🎉</div>
<h2>欢迎使用 IC Coder</h2>
<p class="welcome-modal-subtitle">您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!</p>
</div>
<div class="welcome-modal-body">
<div class="welcome-step">
<div class="welcome-step-icon">📝</div>
<div class="welcome-step-content">
<h3>步骤 1打开聊天面板</h3>
<p>点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"</p>
</div>
</div>
<div class="welcome-step">
<div class="welcome-step-icon">💬</div>
<div class="welcome-step-content">
<h3>步骤 2输入您的需求</h3>
<p>描述您想要生成的 Verilog 代码或需要帮助的问题AI 将为您提供专业的解决方案</p>
</div>
</div>
<div class="welcome-step">
<div class="welcome-step-icon">🔬</div>
<div class="welcome-step-content">
<h3>步骤 3运行仿真</h3>
<p>使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果</p>
</div>
</div>
<button id="welcomeStartBtn" class="welcome-btn welcome-btn-primary">
<span>开始使用</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
`;
}
/**
* 获取欢迎弹窗的 CSS 样式
*/
export function getWelcomeModalStyles(): string {
return `
/* 欢迎弹窗样式 */
.welcome-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
}
.welcome-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease-out;
}
.welcome-modal-content {
position: relative;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 12px;
width: 100%;
max-width: 500px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
.welcome-logo-corner {
position: absolute;
top: 16px;
left: 24px;
height: 40px;
width: auto;
opacity: 0.9;
z-index: 10;
}
.welcome-modal-header {
padding: 60px 32px 20px;
text-align: center;
}
.welcome-icon {
font-size: 48px;
margin-bottom: 16px;
}
.welcome-modal-header h2 {
margin: 0 0 12px;
font-size: 24px;
font-weight: 600;
color: var(--vscode-foreground);
}
.welcome-modal-subtitle {
margin: 0;
font-size: 14px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.welcome-modal-body {
padding: 0 32px 32px;
}
.welcome-step {
display: flex;
gap: 16px;
margin: 20px 0;
padding: 16px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 8px;
border-left: 4px solid var(--vscode-textLink-foreground);
}
.welcome-step-icon {
font-size: 24px;
flex-shrink: 0;
}
.welcome-step-content h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
color: var(--vscode-textLink-foreground);
}
.welcome-step-content p {
margin: 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
.welcome-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
transition: all 0.2s;
margin-top: 24px;
}
.welcome-btn:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.welcome-btn:active {
transform: translateY(0);
}
`;
}
/**
* 获取欢迎弹窗的 JavaScript 逻辑
*/
export function getWelcomeModalScript(): string {
return `
// 欢迎弹窗逻辑
(function() {
const modal = document.getElementById('welcomeModal');
const startBtn = document.getElementById('welcomeStartBtn');
const overlay = modal?.querySelector('.welcome-modal-overlay');
// 显示欢迎弹窗
window.showWelcomeModal = function() {
if (modal) {
modal.style.display = 'flex';
}
};
// 隐藏欢迎弹窗
window.hideWelcomeModal = function() {
if (modal) {
modal.style.display = 'none';
}
};
// 点击"开始使用"按钮
if (startBtn) {
startBtn.addEventListener('click', function() {
hideWelcomeModal();
});
}
// 点击遮罩层关闭弹窗
if (overlay) {
overlay.addEventListener('click', function() {
hideWelcomeModal();
});
}
// 阻止点击弹窗内容时关闭
const content = modal?.querySelector('.welcome-modal-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
// 监听来自后端的消息
window.addEventListener('message', function(event) {
const message = event.data;
if (message.command === 'showWelcomeModal') {
showWelcomeModal();
}
});
// 页面加载时检查是否需要显示欢迎弹窗
vscode.postMessage({ command: 'checkWelcomeModal' });
})();
`;
}