From 130f78108f81044f10624537cf60a7cd6ec90fae Mon Sep 17 00:00:00 2001 From: liailing1026 <1815388873@qq.com> Date: Thu, 12 Mar 2026 13:35:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=88=86=E4=BA=AB=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/db/__init__.py | 6 +- backend/db/crud.py | 77 ++- backend/db/init_db.py | 2 +- backend/db/models.py | 31 + backend/db/schema.sql | 36 ++ backend/server.py | 358 ++++++++++++ .../src/components/SharePlanDialog/index.vue | 471 +++++++++++++++ .../Main/TaskTemplate/HistoryList/index.vue | 52 +- frontend/src/views/Share.vue | 547 +++++++++++++++--- 9 files changed, 1500 insertions(+), 80 deletions(-) create mode 100644 frontend/src/components/SharePlanDialog/index.vue diff --git a/backend/db/__init__.py b/backend/db/__init__.py index 8ab49cc..a8e1de1 100644 --- a/backend/db/__init__.py +++ b/backend/db/__init__.py @@ -5,8 +5,8 @@ AgentCoord 数据库模块 """ from .database import get_db, get_db_context, test_connection, engine, text -from .models import MultiAgentTask, UserAgent, ExportRecord, TaskStatus -from .crud import MultiAgentTaskCRUD, UserAgentCRUD, ExportRecordCRUD +from .models import MultiAgentTask, UserAgent, ExportRecord, PlanShare, TaskStatus +from .crud import MultiAgentTaskCRUD, UserAgentCRUD, ExportRecordCRUD, PlanShareCRUD __all__ = [ # 连接管理 @@ -19,9 +19,11 @@ __all__ = [ "MultiAgentTask", "UserAgent", "ExportRecord", + "PlanShare", "TaskStatus", # CRUD "MultiAgentTaskCRUD", "UserAgentCRUD", "ExportRecordCRUD", + "PlanShareCRUD", ] diff --git a/backend/db/crud.py b/backend/db/crud.py index e560621..8da7388 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone from typing import List, Optional from sqlalchemy.orm import Session -from .models import MultiAgentTask, UserAgent, ExportRecord +from .models import MultiAgentTask, UserAgent, ExportRecord, PlanShare class MultiAgentTaskCRUD: @@ -500,3 +500,78 @@ class ExportRecordCRUD: db.commit() return True return False + + +class PlanShareCRUD: + """任务分享 CRUD 操作""" + + @staticmethod + def create( + db: Session, + share_token: str, + task_id: str, + task_data: dict, + expires_at: Optional[datetime] = None, + extraction_code: Optional[str] = None, + ) -> PlanShare: + """创建分享记录""" + share = PlanShare( + share_token=share_token, + extraction_code=extraction_code, + task_id=task_id, + task_data=task_data, + expires_at=expires_at, + ) + db.add(share) + db.commit() + db.refresh(share) + return share + + @staticmethod + def get_by_token(db: Session, share_token: str) -> Optional[PlanShare]: + """根据 token 获取分享记录""" + return db.query(PlanShare).filter(PlanShare.share_token == share_token).first() + + @staticmethod + def get_by_task_id( + db: Session, task_id: str, limit: int = 10 + ) -> List[PlanShare]: + """根据任务 ID 获取分享记录列表""" + return ( + db.query(PlanShare) + .filter(PlanShare.task_id == task_id) + .order_by(PlanShare.created_at.desc()) + .limit(limit) + .all() + ) + + @staticmethod + def increment_view_count(db: Session, share_token: str) -> Optional[PlanShare]: + """增加查看次数""" + share = db.query(PlanShare).filter(PlanShare.share_token == share_token).first() + if share: + share.view_count = (share.view_count or 0) + 1 + db.commit() + db.refresh(share) + return share + + @staticmethod + def delete(db: Session, share_token: str) -> bool: + """删除分享记录""" + share = db.query(PlanShare).filter(PlanShare.share_token == share_token).first() + if share: + db.delete(share) + db.commit() + return True + return False + + @staticmethod + def delete_by_task_id(db: Session, task_id: str) -> bool: + """删除任务的所有分享记录""" + shares = db.query(PlanShare).filter(PlanShare.task_id == task_id).all() + if shares: + for share in shares: + db.delete(share) + db.commit() + return True + return False diff --git a/backend/db/init_db.py b/backend/db/init_db.py index 9f150c3..0618aea 100644 --- a/backend/db/init_db.py +++ b/backend/db/init_db.py @@ -10,7 +10,7 @@ import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from db.database import engine, Base -from db.models import MultiAgentTask, UserAgent +from db.models import MultiAgentTask, UserAgent, PlanShare def init_database(): diff --git a/backend/db/models.py b/backend/db/models.py index 33968d8..38ec11d 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -137,3 +137,34 @@ class UserAgent(Base): "agent_config": self.agent_config, "created_at": self.created_at.isoformat() if self.created_at else None, } + + +class PlanShare(Base): + """任务分享记录模型""" + __tablename__ = "plan_shares" + + id = Column(Integer, primary_key=True, autoincrement=True) + share_token = Column(String(64), unique=True, index=True, nullable=False) # 唯一分享码 + extraction_code = Column(String(8), nullable=True) # 提取码(4位字母数字) + task_id = Column(String(64), nullable=False, index=True) # 关联的任务ID + task_data = Column(JSONB, nullable=False) # 完整的任务数据(脱敏后) + created_at = Column(DateTime(timezone=True), default=utc_now) + expires_at = Column(DateTime(timezone=True), nullable=True) # 过期时间 + view_count = Column(Integer, default=0) # 查看次数 + + __table_args__ = ( + Index("idx_plan_shares_token", "share_token"), + ) + + def to_dict(self) -> dict: + """转换为字典""" + return { + "id": self.id, + "share_token": self.share_token, + "extraction_code": self.extraction_code, + "task_id": self.task_id, + "task_data": self.task_data, + "created_at": self.created_at.isoformat() if self.created_at else None, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "view_count": self.view_count, + } diff --git a/backend/db/schema.sql b/backend/db/schema.sql index 10ede25..b22f7c3 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -62,9 +62,45 @@ CREATE TRIGGER update_multi_agent_tasks_updated_at FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +-- ============================================================================= +-- 表3: export_records (导出记录) +-- ============================================================================= +CREATE TABLE IF NOT EXISTS export_records ( + id SERIAL PRIMARY KEY, + task_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL, + export_type VARCHAR(32) NOT NULL, + file_name VARCHAR(256) NOT NULL, + file_path VARCHAR(512) NOT NULL, + file_url VARCHAR(512), + file_size INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_export_records_task_user ON export_records(task_id, user_id); + +-- ============================================================================= +-- 表4: plan_shares (任务分享记录) +-- ============================================================================= +CREATE TABLE IF NOT EXISTS plan_shares ( + id SERIAL PRIMARY KEY, + share_token VARCHAR(64) NOT NULL UNIQUE, + extraction_code VARCHAR(8), + task_id VARCHAR(64) NOT NULL, + task_data JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP WITH TIME ZONE, + view_count INTEGER DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_plan_shares_token ON plan_shares(share_token); +CREATE INDEX IF NOT EXISTS idx_plan_shares_task_id ON plan_shares(task_id); + DO $$ BEGIN RAISE NOTICE '✅ PostgreSQL 数据库表结构创建完成!'; RAISE NOTICE '表: multi_agent_tasks (多智能体任务记录)'; RAISE NOTICE '表: user_agents (用户智能体配置)'; + RAISE NOTICE '表: export_records (导出记录)'; + RAISE NOTICE '表: plan_shares (任务分享记录)'; END $$; diff --git a/backend/server.py b/backend/server.py index e7986c7..ab42685 100644 --- a/backend/server.py +++ b/backend/server.py @@ -23,6 +23,7 @@ import uuid import copy import base64 from typing import List, Dict, Optional +from datetime import datetime, timezone, timedelta # 数据库模块导入 from db import ( @@ -30,6 +31,7 @@ from db import ( MultiAgentTaskCRUD, UserAgentCRUD, ExportRecordCRUD, + PlanShareCRUD, TaskStatus, text, ) @@ -2239,6 +2241,200 @@ def handle_pin_plan(data): }) +import secrets + + +@socketio.on('share_plan') +def handle_share_plan(data): + """ + WebSocket版本:分享任务 + """ + # socketio 包装: data = { id: 'share_plan-xxx', action: 'share_plan', data: { id: 'ws_req_xxx', data: {...} } } + request_id = data.get('id') # socketio 包装的 id + incoming_data = data.get('data', {}).get('data', {}) # 真正的请求数据 + plan_id = incoming_data.get('plan_id') + expiration_days = incoming_data.get('expiration_days', 7) # 默认为7天,0表示永久 + extraction_code = incoming_data.get('extraction_code', '') # 提取码 + auto_fill_code = incoming_data.get('auto_fill_code', True) # 是否在链接中自动填充提取码 + + if not plan_id: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '缺少 plan_id(task_id)' + }) + return + + try: + with get_db_context() as db: + # 获取任务详情 + task = MultiAgentTaskCRUD.get_by_id(db, plan_id) + if not task: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': f'任务不存在: {plan_id}' + }) + return + + # 生成唯一分享 token + share_token = secrets.token_urlsafe(16) + + # 设置过期时间 + if expiration_days == 0: + # 永久有效 + expires_at = None + else: + expires_at = datetime.now(timezone.utc) + timedelta(days=expiration_days) + + # 准备分享数据(脱敏处理,移除敏感信息) + task_data = { + "general_goal": task.query, + "task_outline": task.task_outline, + "assigned_agents": task.assigned_agents, + "agent_scores": task.agent_scores, + "agents_info": task.agents_info, + "branches": task.branches, + "result": task.result, + "rehearsal_log": task.rehearsal_log, + "status": task.status.value if task.status else None, + } + + # 创建分享记录 + share = PlanShareCRUD.create( + db=db, + share_token=share_token, + task_id=plan_id, + task_data=task_data, + expires_at=expires_at, + extraction_code=extraction_code.upper() if extraction_code else None, + ) + + # 生成分享链接(根据auto_fill_code决定是否在URL中携带提取码) + if extraction_code and auto_fill_code: + share_url = f"/share/{share_token}?code={extraction_code.upper()}" + else: + share_url = f"/share/{share_token}" + + emit('response', { + 'id': request_id, + 'status': 'success', + 'data': { + "share_url": share_url, + "share_token": share_token, + "extraction_code": extraction_code.upper() if extraction_code else None, + "auto_fill_code": auto_fill_code, + "task_id": plan_id, + } + }) + + except Exception as e: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': str(e) + }) + + +@socketio.on('import_shared_plan') +def handle_import_shared_plan(data): + """ + WebSocket版本:导入分享的任务到自己的历史记录 + """ + # socketio 包装: data = { id: 'import_shared_plan-xxx', action: 'import_shared_plan', data: { id: 'ws_req_xxx', data: {...} } } + request_id = data.get('id') # socketio 包装的 id + incoming_data = data.get('data', {}).get('data', {}) # 真正的请求数据 + share_token = incoming_data.get('share_token') + user_id = incoming_data.get('user_id') + + if not share_token: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '缺少 share_token' + }) + return + + if not user_id: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '缺少 user_id' + }) + return + + try: + with get_db_context() as db: + # 获取分享记录 + share = PlanShareCRUD.get_by_token(db, share_token) + if not share: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '分享链接无效或已失效' + }) + return + + # 检查是否过期 + if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now(): + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': '分享链接已过期' + }) + return + + # 获取分享的任务数据 + task_data = share.task_data + + # 生成新的 task_id(因为是导入到自己的账号) + import uuid + new_task_id = str(uuid.uuid4()) + + # 创建新的任务记录 + task = MultiAgentTaskCRUD.create( + db=db, + task_id=new_task_id, + user_id=user_id, + query=task_data.get("general_goal", ""), + agents_info=task_data.get("agents_info", []), + task_outline=task_data.get("task_outline"), + assigned_agents=task_data.get("assigned_agents"), + agent_scores=task_data.get("agent_scores"), + result=task_data.get("result"), + ) + + # 如果有分支数据,也保存 + if task_data.get("branches"): + MultiAgentTaskCRUD.update_branches(db, new_task_id, task_data["branches"]) + + # 如果有执行日志,也保存 + if task_data.get("rehearsal_log"): + MultiAgentTaskCRUD.update_rehearsal_log(db, new_task_id, task_data["rehearsal_log"]) + + # 增加分享的查看次数 + PlanShareCRUD.increment_view_count(db, share_token) + + # 通知所有客户端刷新历史列表 + socketio.emit('history_updated', {'user_id': user_id}) + + emit('response', { + 'id': request_id, + 'status': 'success', + 'data': { + "message": "导入成功", + "task_id": new_task_id, + } + }) + + except Exception as e: + emit('response', { + 'id': request_id, + 'status': 'error', + 'error': str(e) + }) + + @socketio.on('save_branches') def handle_save_branches(data): """ @@ -3157,6 +3353,168 @@ def get_share_info(record_id: int): return jsonify({'error': str(e)}), 500 +# ==================== 任务分享页面 ==================== + +@app.route('/share/', methods=['GET']) +def get_shared_plan_page(share_token: str): + """获取分享任务页面(无需登录验证)""" + try: + with get_db_context() as db: + share = PlanShareCRUD.get_by_token(db, share_token) + if not share: + return jsonify({'error': '分享链接无效或已失效'}), 404 + + # 检查是否过期 + if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now(): + return jsonify({'error': '分享链接已过期'}), 404 + + # 增加查看次数 + PlanShareCRUD.increment_view_count(db, share_token) + + # 返回分享数据 + task_data = share.task_data + return jsonify({ + 'share_token': share_token, + 'task_id': share.task_id, + 'task_data': task_data, + 'created_at': share.created_at.isoformat() if share.created_at else None, + 'view_count': share.view_count, + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/share//check', methods=['GET']) +def check_share_code(share_token: str): + """检查分享链接是否需要提取码""" + try: + with get_db_context() as db: + share = PlanShareCRUD.get_by_token(db, share_token) + if not share: + return jsonify({'error': '分享链接无效或已失效'}), 404 + + # 检查是否过期 + if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now(): + return jsonify({'error': '分享链接已过期'}), 404 + + # 如果有提取码,则需要提取码 + need_code = bool(share.extraction_code) + return jsonify({ + 'need_code': need_code, + 'has_extraction_code': bool(share.extraction_code) + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/share/', methods=['GET']) +def get_shared_plan_info(share_token: str): + """获取分享任务详情(API 接口,无需登录验证)""" + # 获取URL参数中的提取码 + code = request.args.get('code', '').upper() + + try: + with get_db_context() as db: + share = PlanShareCRUD.get_by_token(db, share_token) + if not share: + return jsonify({'error': '分享链接无效或已失效'}), 404 + + # 检查是否过期 + if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now(): + return jsonify({'error': '分享链接已过期'}), 404 + + # 验证提取码 + if share.extraction_code: + if not code: + return jsonify({'error': '请输入提取码'}), 403 + if code != share.extraction_code: + return jsonify({'error': '提取码错误'}), 403 + + # 增加查看次数 + PlanShareCRUD.increment_view_count(db, share_token) + + # 返回分享数据 + task_data = share.task_data + return jsonify({ + 'share_token': share_token, + 'task_id': share.task_id, + 'task_data': task_data, + 'created_at': share.created_at.isoformat() if share.created_at else None, + 'expires_at': share.expires_at.isoformat() if share.expires_at else None, + 'view_count': share.view_count, + 'extraction_code': share.extraction_code, + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/share/import', methods=['POST']) +def import_shared_plan(): + """导入分享的任务到自己的历史记录(HTTP API,无需 WebSocket)""" + try: + data = request.get_json() + share_token = data.get('share_token') + user_id = data.get('user_id') + + if not share_token: + return jsonify({'error': '缺少 share_token'}), 400 + + if not user_id: + return jsonify({'error': '缺少 user_id,请先登录'}), 401 + + with get_db_context() as db: + # 获取分享记录 + share = PlanShareCRUD.get_by_token(db, share_token) + if not share: + return jsonify({'error': '分享链接无效或已失效'}), 404 + + # 检查是否过期 + if share.expires_at and share.expires_at.replace(tzinfo=None) < datetime.now(): + return jsonify({'error': '分享链接已过期'}), 404 + + # 获取分享的任务数据 + task_data = share.task_data + + # 生成新的 task_id(因为是导入到自己的账号) + new_task_id = str(uuid.uuid4()) + + # 创建新的任务记录 + task = MultiAgentTaskCRUD.create( + db=db, + task_id=new_task_id, + user_id=user_id, + query=task_data.get("general_goal", ""), + agents_info=task_data.get("agents_info", []), + task_outline=task_data.get("task_outline"), + assigned_agents=task_data.get("assigned_agents"), + agent_scores=task_data.get("agent_scores"), + result=task_data.get("result"), + ) + + # 如果有分支数据,也保存 + if task_data.get("branches"): + MultiAgentTaskCRUD.update_branches(db, new_task_id, task_data["branches"]) + + # 如果有执行日志,也保存 + if task_data.get("rehearsal_log"): + MultiAgentTaskCRUD.update_rehearsal_log(db, new_task_id, task_data["rehearsal_log"]) + + # 增加分享的查看次数 + PlanShareCRUD.increment_view_count(db, share_token) + + return jsonify({ + 'success': True, + 'message': '导入成功', + 'task_id': new_task_id, + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + @app.route('/api/export/', methods=['DELETE']) def delete_export(record_id: int): """删除导出记录""" diff --git a/frontend/src/components/SharePlanDialog/index.vue b/frontend/src/components/SharePlanDialog/index.vue new file mode 100644 index 0000000..3f6b138 --- /dev/null +++ b/frontend/src/components/SharePlanDialog/index.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/frontend/src/layout/components/Main/TaskTemplate/HistoryList/index.vue b/frontend/src/layout/components/Main/TaskTemplate/HistoryList/index.vue index 871a423..c84202a 100644 --- a/frontend/src/layout/components/Main/TaskTemplate/HistoryList/index.vue +++ b/frontend/src/layout/components/Main/TaskTemplate/HistoryList/index.vue @@ -26,6 +26,7 @@
-
+
{{ plan.is_pinned ? '取消置顶' : '置顶' }}
-
+
分享
-
+
删除
@@ -62,6 +63,12 @@ content="删除后,该任务无法恢复 !" @confirm="confirmDelete" /> + + +
@@ -71,6 +78,7 @@ import { ElMessage } from 'element-plus' import { Loading } from '@element-plus/icons-vue' import SvgIcon from '@/components/SvgIcon/index.vue' import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue' +import SharePlanDialog from '@/components/SharePlanDialog/index.vue' import websocket from '@/utils/websocket' // 事件定义 @@ -101,6 +109,44 @@ const dialogVisible = ref(false) const planToDelete = ref(null) const deleting = ref(false) +// 分享对话框相关 +const shareDialogVisible = ref(false) +const sharePlanId = ref('') + +// Popover 引用管理 +const popoverRefs = new Map() +const setPopoverRef = (planId: string, el: any) => { + if (el) { + popoverRefs.set(planId, el) + } +} + +// 统一处理操作点击 +const handleAction = (plan: PlanInfo, action: 'pin' | 'share' | 'delete') => { + // 关闭 popover + const popover = popoverRefs.get(plan.id) + if (popover) { + popover.hide() + } + + // 延迟执行操作,让 popover 有时间关闭 + setTimeout(() => { + if (action === 'pin') { + pinPlan(plan) + } else if (action === 'share') { + openShareDialog(plan) + } else if (action === 'delete') { + deletePlan(plan) + } + }, 100) +} + +// 打开分享弹窗 +const openShareDialog = (plan: PlanInfo) => { + sharePlanId.value = plan.id + shareDialogVisible.value = true +} + // 生成唯一请求ID let requestIdCounter = 0 const generateRequestId = () => `ws_req_${Date.now()}_${++requestIdCounter}` diff --git a/frontend/src/views/Share.vue b/frontend/src/views/Share.vue index c6074c8..a00c227 100644 --- a/frontend/src/views/Share.vue +++ b/frontend/src/views/Share.vue @@ -1,19 +1,36 @@ @@ -183,32 +388,135 @@ onMounted(() => {

{{ error }}

- -
-
+ +
+
- +
-

{{ shareInfo.file_name }}

+

请输入提取码

+
+
+ + 确认 +
+
+

{{ codeError }}

+

请输入分享者提供给您的提取码

+
+ + +
+
+ + + +
+

{{ exportInfo.file_name }}

文件类型: - {{ getFileTypeName(shareInfo.export_type) }} + {{ getFileTypeName(exportInfo.export_type) }}
创建时间: - {{ formatDate(shareInfo.created_at) }} + {{ formatDate(exportInfo.created_at) }}
文件大小: - {{ formatFileSize(shareInfo.file_size) }} + {{ formatFileSize(exportInfo.file_size) }}
- + 下载文件
+ + +
+
+ + + +
+ +
+ + + +

导入成功

+

任务已添加到您的历史记录

+ 返回首页 +
+ +
+

{{ planInfo.task_data.general_goal || '任务分享' }}

+
+
+ + 任务状态: + {{ taskStatusText }} +
+
+ + 智能体数量: + {{ agentCount }} 个 +
+
+ + 分享时间: + {{ formatDate(planInfo.created_at) }} +
+
+ + 过期时间: + {{ formatDate(planInfo.expires_at) }} +
+
+ 查看次数: + {{ planInfo.view_count }} 次 +
+
+ + + {{ importing ? '导入中...' : '导入到我的历史记录' }} + + +

导入后,您可以在历史记录中查看和恢复此任务

+
+
@@ -245,8 +553,12 @@ onMounted(() => { } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } } @@ -275,12 +587,66 @@ onMounted(() => { } } +.code-input-state { + .code-icon { + color: #667eea; + margin-bottom: 20px; + } + + h2 { + color: #333; + margin-bottom: 24px; + } + + .code-input-wrapper { + width: 100%; + display: flex; + justify-content: center; + } + + .code-input-box { + display: flex; + gap: 12px; + margin-bottom: 16px; + + .code-input { + width: 160px; + + :deep(.el-input__inner) { + text-align: center; + letter-spacing: 4px; + font-size: 18px; + font-weight: bold; + + &::placeholder { + font-size: 12px; + letter-spacing: 0; + } + } + } + } + + .tip-text { + font-size: 13px; + color: #999; + text-align: center; + + &.error { + color: #f56c6c; + } + } +} + .success-state { .success-icon { color: #667eea; margin-bottom: 20px; } + &.plan-share .success-icon.plan-icon { + color: #409eff; + } + h2 { color: #333; margin-bottom: 24px; @@ -288,7 +654,8 @@ onMounted(() => { font-size: 20px; } - .file-info { + .file-info, + .plan-info { background: #f8f9fa; border-radius: 12px; padding: 20px; @@ -297,7 +664,8 @@ onMounted(() => { .info-item { display: flex; - justify-content: space-between; + align-items: center; + gap: 8px; padding: 8px 0; border-bottom: 1px solid #eee; @@ -305,6 +673,11 @@ onMounted(() => { border-bottom: none; } + .el-icon { + color: #666; + font-size: 16px; + } + .label { color: #666; } @@ -316,10 +689,38 @@ onMounted(() => { } } - .download-btn { + .plan-info .info-item { + flex-wrap: wrap; + } + + .action-btn { width: 100%; height: 48px; font-size: 16px; } + + .tip-text { + margin-top: 16px; + font-size: 13px; + color: #999; + } +} + +.import-success { + padding: 20px 0; + + .success-check { + margin-bottom: 16px; + } + + h2 { + color: #67c23a; + margin-bottom: 8px; + } + + p { + color: #666; + margin-bottom: 24px; + } }