Flex 支持CTRL-Z的TextArea

本文介绍了一个自定义组件 UndoTextArea 的实现细节,该组件继承自 TextArea,并通过监听键盘事件实现了撤销功能。文章解释了为何选择使用 KEY_UP 而不是 KEY_DOWN 事件的原因,并提到了在 IE 浏览器下 CTRL-Z 键会被浏览器截获的问题。
public class UndoTextArea extends TextArea
    {
        
        private var _undoManager:UndoManager;
        
        public function UndoTextArea()
        {
            super();
            
            _undoManager=new UndoManager();
            
            this.addEventListener(KeyboardEvent.KEY_UP,undoKeyUpHandler);
            this.addEventListener(FlexEvent.CREATION_COMPLETE,creationCompleteHandler);
        }
        
        private function creationCompleteHandler(evt:FlexEvent):void 
        { 
            
            this.textFlow.interactionManager=new EditManager(this._undoManager);
            
        }
        
        private function undoKeyUpHandler(evt:KeyboardEvent):void 
        { 
            
            if (evt.ctrlKey&&evt.keyCode == 90) 
            { 
                _undoManager.undo();
                
            } 
            
        } 
    }
 
这里使用了KEY_UP事件,其实更合理的是用KEY_DOWN,只是在IE下CTRL-Z被浏览器截获了,我们的程序捕获不到!
 

转载于:https://www.cnblogs.com/kklldog/archive/2012/01/31/2332712.html

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>图片预览工具</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> :root { --primary-color: #1677ff; --border-radius: 10px; --bg-color: #f5f7fa; --text-color: #333; } body { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; background: var(--bg-color); margin: 0; } .container { height: 100vh; display: flex; padding: 20px; box-sizing: border-box; } .inner-container { background: #fff; border-radius: var(--border-radius); box-shadow: 0 4px 20px rgba(0,0,0,0.05); width: 95%; max-width: 1400px; display: flex; gap: 20px; height: 100%; } .form-section { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 30px; border-right: 1px solid #f0f0f0; } h1 { margin-bottom: 20px; font-size: 1.5rem; color: var(--text-color); } textarea { padding: 10px 14px; border: 1px solid #ddd; border-radius: var(--border-radius); font-size: 14px; outline: none; resize: vertical; min-height: 120px; width: 100%; } textarea:focus { border-color: var(--primary-color); } .preview-wrapper { flex: 2; overflow-y: auto; height: 100%; padding-right: 10px; } .preview-wrapper.empty, .preview-wrapper.single { display: flex; justify-content: center; align-items: center; } .preview-wrapper.multi { display: flex; flex-direction: column; align-items: center; } .preview-wrapper.empty .preview-container, .preview-wrapper.single .preview-container { width: 80%; display: flex; flex-direction: column; align-items: center; } .preview-container { display: flex; flex-direction: column; align-items: center; margin-bottom: 20px; } .preview-box { border: 2px dashed #ddd; border-radius: var(--border-radius); height: 300px; width: 100%; background: #fafafa; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; cursor: grab; } .preview-box.dragging { cursor: grabbing; } .preview-box img { max-width: 100%; max-height: 100%; object-fit: contain; transform-origin: center center; transition: transform 0.1s; user-select: none; -webkit-user-drag: none; } .placeholder { color: #bbb; } .preview-buttons { display: flex; gap: 12px; margin-top: 8px; } .preview-buttons button { background: var(--primary-color); color: white; border: none; padding: 8px 16px; /* 按钮更大 */ font-size: 14px; border-radius: var(--border-radius); cursor: pointer; } .preview-buttons button:hover { background: #1256c4; } .fullscreen-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 999; } .fullscreen-overlay img { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(1) rotate(0deg); max-width: 90%; max-height: 90%; object-fit: contain; user-select: none; -webkit-user-drag: none; } .fullscreen-controls { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; padding: 10px; } .fullscreen-controls button { width: 60px; height: 60px; border-radius: 50%; border: none; background: rgba(255,255,255,0.6); font-size: 24px; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 6px rgba(0,0,0,0.3); } .fullscreen-controls button:hover { background: rgba(255,255,255,0.9); transform: scale(1.1); } </style> </head> <body> <div class="container"> <div class="inner-container"> <div class="form-section"> <h1>图片预览工具</h1> <textarea id="imageURLs" placeholder="输入图片链接(每行一个)"></textarea> </div> <div id="previewWrapper" class="preview-wrapper empty"> <div class="preview-container"> <div class="preview-box"> <div class="placeholder">图片预览区</div> </div> </div> </div> </div> </div> <div class="fullscreen-overlay" id="fullscreenOverlay"> <img id="fullscreenImg"> <div class="fullscreen-controls"> <button id="fullRotateL">⟲</button> <button id="fullRotateR">⟳</button> </div> </div> <script> const previewWrapper = document.getElementById('previewWrapper'); const fullscreenOverlay = document.getElementById('fullscreenOverlay'); const fullscreenImg = document.getElementById('fullscreenImg'); let fullState = { scale:1, posX:0, posY:0, rotation:0 }; function updateTransform(img,state,isFullscreen){ if(isFullscreen) { img.style.transform = `translate(calc(-50% + ${state.posX}px), calc(-50% + ${state.posY}px)) scale(${state.scale}) rotate(${state.rotation}deg)`; } else { img.style.transform = `translate(${state.posX}px, ${state.posY}px) scale(${state.scale}) rotate(${state.rotation}deg)`; } } function bindZoomDrag(img,state,container,clickHandler,isFullscreen=false){ let isDragging=false, moved=false, startX, startY; img.onwheel = e=>{ e.preventDefault(); state.scale += (e.deltaY<0?0.05:-0.05); state.scale = Math.max(0.1, Math.min(state.scale, 5)); updateTransform(img,state,isFullscreen); }; container.onmousedown = e=>{ e.preventDefault(); isDragging=true; moved=false; startX=e.clientX - state.posX; startY=e.clientY - state.posY; container.classList.add('dragging'); }; document.addEventListener('mousemove', e=>{ if(isDragging){ const dx = e.clientX - startX; const dy = e.clientY - startY; if(Math.abs(dx-state.posX)>3 || Math.abs(dy-state.posY)>3) moved=true; state.posX = dx; state.posY = dy; updateTransform(img,state,isFullscreen); } }); document.addEventListener('mouseup', ()=>{ isDragging=false; container.classList.remove('dragging'); }); container.onclick = ()=>{ if(!moved){ clickHandler(); } }; } function rotateImg(state,deg,img,isFullscreen=false){ state.rotation += deg; updateTransform(img,state,isFullscreen); } function createPreviewBox(url){ const container=document.createElement('div'); container.className='preview-container'; const box = document.createElement('div'); box.className = 'preview-box'; const img = document.createElement('img'); img.src=url; const state = { scale:1, posX:0, posY:0, rotation:0 }; img.onload=()=>updateTransform(img,state,false); bindZoomDrag(img,state,box, ()=>{ fullscreenImg.src = img.src; fullState={scale:1,posX:0,posY:0,rotation:0}; updateTransform(fullscreenImg,fullState,true); bindZoomDrag(fullscreenImg,fullState,fullscreenOverlay,()=>{},true); fullscreenOverlay.style.display='block'; },false); box.appendChild(img); container.appendChild(box); const btnBox=document.createElement('div'); btnBox.className='preview-buttons'; const btnL=document.createElement('button'); btnL.innerText='⟲ 逆时针'; btnL.onclick=()=>rotateImg(state,-90,img,false); const btnR=document.createElement('button'); btnR.innerText='⟳ 顺时针'; btnR.onclick=()=>rotateImg(state,90,img,false); btnBox.appendChild(btnL); btnBox.appendChild(btnR); container.appendChild(btnBox); return container; } document.getElementById('imageURLs').addEventListener('input',()=>{ let urls=document.getElementById('imageURLs').value.split('\n').map(s=>s.trim()).filter(s=>s); previewWrapper.innerHTML=''; if(urls.length===0){ previewWrapper.className='preview-wrapper empty'; const emptyContainer=document.createElement('div'); emptyContainer.className='preview-container'; emptyContainer.innerHTML='<div class="preview-box"><div class="placeholder">图片预览区</div></div>'; previewWrapper.appendChild(emptyContainer); } else if(urls.length===1){ previewWrapper.className='preview-wrapper single'; previewWrapper.appendChild(createPreviewBox(urls[0])); } else { previewWrapper.className='preview-wrapper multi'; urls.forEach(url=>{ previewWrapper.appendChild(createPreviewBox(url)); }); } }); document.getElementById('fullRotateL').onclick=()=>rotateImg(fullState,-90,fullscreenImg,true); document.getElementById('fullRotateR').onclick=()=>rotateImg(fullState,90,fullscreenImg,true); fullscreenOverlay.addEventListener('click', e=>{ if(e.target===fullscreenOverlay){ fullscreenOverlay.style.display='none'; } }); </script> </body> </html>,修改,想将多个图片预览放到一个框里,实现多个图片预览,然后用ctrl c 批量复制的功能
08-23
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流程审批系统</title> <!-- 引入Element Plus样式 --> <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"> <!-- 引入LogicFlow样式 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core/lib/style/index.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/style/index.css"> <!-- 引入Vue 3和Element Plus --> <script src="https://unpkg.com/vue@3"></script> <script src="https://unpkg.com/element-plus"></script> <!-- 引入LogicFlow --> <script src="https://cdn.jsdelivr.net/npm/@logicflow/core/dist/logic-flow.js"></script> <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension/lib/index.js"></script> <style> :root { --primary-color: #409EFF; --success-color: #67C23A; --warning-color: #E6A23C; --danger-color: #F56C6C; --info-color: #909399; } body { margin: 0; padding: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f5f7fa; color: #303133; } .app-container { display: flex; flex-direction: column; min-height: 100vh; } .header { background-color: var(--primary-color); color: white; padding: 0 20px; height: 60px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .main-content { display: flex; flex: 1; } .sidebar { width: 220px; background-color: #304156; color: white; height: calc(100vh - 60px); overflow-y: auto; } .content { flex: 1; padding: 20px; overflow-y: auto; height: calc(100vh - 60px); } .page-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #ebeef5; } .card-container { background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } .toolbar { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .logicflow-container { height: 500px; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden; } .property-panel { position: absolute; right: 20px; top: 100px; width: 300px; background: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 15px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); z-index: 100; } .task-filter { margin-bottom: 20px; display: flex; gap: 10px; } .task-item { border-left: 4px solid var(--primary-color); padding: 15px; margin-bottom: 15px; background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .task-item.urgent { border-left-color: var(--danger-color); } .task-item.completed { border-left-color: var(--success-color); } .task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .task-title { font-weight: bold; font-size: 16px; } .task-meta { color: var(--info-color); font-size: 14px; margin-bottom: 10px; } .task-actions { display: flex; gap: 10px; } .process-status { display: inline-block; padding: 5px 10px; border-radius: 3px; font-size: 12px; color: white; } .status-pending { background-color: var(--warning-color); } .status-approved { background-color: var(--success-color); } .status-rejected { background-color: var(--danger-color); } .status-processing { background-color: var(--primary-color); } .menu-item { padding: 15px 20px; cursor: pointer; transition: background-color 0.3s; } .menu-item:hover, .menu-item.active { background-color: #263445; } .menu-item i { margin-right: 10px; } </style> </head> <body> <div id="app"> <div class="app-container"> <!-- 顶部导航栏 --> <div class="header"> <h2>流程审批系统</h2> <div> <el-dropdown> <span class="el-dropdown-link"> 管理员 <i class="el-icon-arrow-down el-icon--right"></i> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>个人信息</el-dropdown-item> <el-dropdown-item>退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </div> <div class="main-content"> <!-- 侧边栏菜单 --> <div class="sidebar"> <div v-for="item in menuItems" :key="item.key" :class="['menu-item', { active: activeMenu === item.key }]" @click="activeMenu = item.key" > <i :class="item.icon"></i> <span>{{ item.label }}</span> </div> </div> <!-- 主内容区域 --> <div class="content"> <!-- 流程设计器 --> <div v-if="activeMenu === 'design'"> <h2 class="page-title">流程设计器</h2> <div class="card-container"> <div class="toolbar"> <div> <el-select v-model="currentTemplate" placeholder="选择流程模板" style="width: 200px;"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" /> </el-select> <el-button type="primary" style="margin-left: 10px;" @click="saveFlow">保存流程</el-button> <el-button @click="createNewTemplate">新建模板</el-button> </div> <div> <el-button @click="importFlow">导入</el-button> <el-button @click="exportFlow">导出</el-button> </div> </div> <div class="logicflow-container" ref="container"></div> <!-- 节点属性面板 --> <div v-if="selectedNode" class="property-panel"> <h3>节点属性</h3> <el-form label-width="80px"> <el-form-item label="节点名称"> <el-input v-model="selectedNode.properties.name"></el-input> </el-form-item> <el-form-item v-if="selectedNode.type === 'approval'" label="审批人"> <el-select v-model="selectedNode.properties.approver" multiple placeholder="选择审批人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> <el-form-item v-if="selectedNode.type === 'condition'" label="条件表达式"> <el-input v-model="selectedNode.properties.condition" type="textarea" :rows="2"></el-input> </el-form-item> <el-form-item label="描述信息"> <el-input v-model="selectedNode.properties.desc" type="textarea" :rows="2"></el-input> </el-form-item> </el-form> </div> </div> </div> <!-- 我的任务 --> <div v-if="activeMenu === 'tasks'"> <h2 class="page-title">我的任务</h2> <div class="card-container"> <div class="task-filter"> <el-radio-group v-model="taskFilter"> <el-radio-button label="pending">待我审批</el-radio-button> <el-radio-button label="processed">我已审批</el-radio-button> <el-radio-button label="started">我发起的</el-radio-button> </el-radio-group> <el-input placeholder="搜索流程名称" style="width: 200px;" v-model="searchKeyword"> <template #append> <el-button icon="el-icon-search"></el-button> </template> </el-input> </div> <div v-if="filteredTasks.length > 0"> <div v-for="task in filteredTasks" :key="task.id" :class="['task-item', { urgent: task.priority === 'high', completed: task.status === 'completed' }]" > <div class="task-header"> <div class="task-title">{{ task.processName }}</div> <div> <span :class="['process-status', `status-${task.status}`]"> {{ taskStatusMap[task.status] }} </span> </div> </div> <div class="task-meta"> <div>申请人: {{ task.applicant }} | 申请时间: {{ task.applyTime }}</div> <div>当前节点: {{ task.currentNode }}</div> <div v-if="task.comment">审批意见: {{ task.comment }}</div> </div> <div class="task-actions"> <el-button v-if="task.status === 'pending'" type="primary" size="small" @click="handleApprove(task)" >审批</el-button> <el-button v-if="task.status === 'pending'" type="danger" size="small" @click="handleReject(task)" >驳回</el-button> <el-button v-if="task.status === 'pending'" type="warning" size="small" @click="handleTransfer(task)" >转办</el-button> <el-button size="small" @click="viewProcess(task)" >查看进度</el-button> </div> </div> </div> <el-empty v-else description="暂无任务"></el-empty> </div> </div> <!-- 新建流程 --> <div v-if="activeMenu === 'create'"> <h2 class="page-title">新建流程申请</h2> <div class="card-container"> <el-form :model="formData" :rules="formRules" ref="applyForm" label-width="120px"> <!-- 流程模板选择 --> <el-form-item label="流程模板" prop="templateId"> <el-select v-model="formData.templateId" placeholder="请选择流程模板" @change="handleTemplateChange"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" > <span style="float: left">{{ template.name }}</span> <span style="float: right; color: #8492a6; font-size: 13px"> {{ template.category }} </span> </el-option> </el-select> </el-form-item> <!-- 动态表单区域 --> <div v-if="selectedTemplate"> <el-divider content-position="left">申请信息</el-divider> <div v-for="field in selectedTemplate.fields" :key="field.name"> <el-form-item :label="field.label" :prop="'fields.' + field.name" :rules="field.rules" > <el-input v-if="field.type === 'text' || field.type === 'textarea'" v-model="formData.fields[field.name]" :type="field.type" :rows="field.type === 'textarea' ? 4 : 2" :placeholder="'请输入' + field.label" /> <el-input-number v-else-if="field.type === 'number'" v-model="formData.fields[field.name]" :placeholder="'请输入' + field.label" /> <el-date-picker v-else-if="field.type === 'date' || field.type === 'datetime'" v-model="formData.fields[field.name]" :type="field.type" :placeholder="'请选择' + field.label" style="width: 100%" /> <el-select v-else-if="field.type === 'select'" v-model="formData.fields[field.name]" :placeholder="'请选择' + field.label" style="width: 100%" > <el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </el-select> </el-form-item> </div> <el-divider content-position="left">附件材料</el-divider> <el-upload action="#" :auto-upload="false" :on-change="handleFileChange" :on-remove="handleFileRemove" :file-list="fileList" multiple > <el-button type="primary">点击上传</el-button> <template #tip> <div class="el-upload__tip"> 支持扩展名:.doc .docx .pdf .jpg .png,单个文件不超过10MB </div> </template> </el-upload> <div v-if="fileList.length > 0" style="margin-top: 15px;"> <div v-for="(file, index) in fileList" :key="index" style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background-color: #f5f7fa; margin-bottom: 8px; border-radius: 4px;"> <div style="display: flex; align-items: center;"> <el-icon><Document /></el-icon> <span style="margin-left: 8px">{{ file.name }}</span> </div> <el-button size="small" type="danger" text @click="handleFileRemove(file)">删除</el-button> </div> </div> <el-divider content-position="left">审批流程</el-divider> <el-timeline> <el-timeline-item v-for="(approver, index) in selectedTemplate.approvers" :key="index" :timestamp="'第' + (index + 1) + '步'" placement="top" > <el-card> <h4>{{ approver.nodeName }}</h4> <p>审批人: {{ approver.userName }}</p> <p v-if="approver.description">{{ approver.description }}</p> </el-card> </el-timeline-item> </el-timeline> </div> <!-- 操作按钮 --> <el-form-item style="margin-top: 30px;"> <el-button type="primary" @click="submitForm" :loading="submitting">提交申请</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> </div> </div> <!-- 流程监控 --> <div v-if="activeMenu === 'monitor'"> <h2 class="page-title">流程监控</h2> <div class="card-container"> <el-table :data="processInstances" style="width: 100%"> <el-table-column prop="name" label="流程名称" width="180"></el-table-column> <el-table-column prop="applicant" label="申请人" width="120"></el-table-column> <el-table-column prop="startTime" label="开始时间" width="150"></el-table-column> <el-table-column prop="currentNode" label="当前节点" width="120"></el-table-column> <el-table-column label="状态" width="100"> <template #default="scope"> <el-tag :type="statusType(scope.row.status)"> {{ scope.row.status }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="150"> <template #default="scope"> <el-button size="small" @click="viewProcessDetail(scope.row)">查看详情</el-button> </template> </el-table-column> </el-table> </div> </div> </div> </div> </div> <!-- 审批对话框 --> <el-dialog :title="dialogTitle" v-model="approvalDialogVisible" width="500px"> <el-form :model="approvalForm" label-width="80px"> <el-form-item label="审批意见"> <el-input v-model="approvalForm.comment" type="textarea" :rows="3"></el-input> </el-form-item> <el-form-item v-if="dialogType === 'transfer'" label="转办给"> <el-select v-model="approvalForm.transferTo" placeholder="选择转办人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="approvalDialogVisible = false">取消</el-button> <el-button v-if="dialogType === 'approve'" type="primary" @click="confirmApprove" >通过</el-button> <el-button v-if="dialogType === 'reject'" type="danger" @click="confirmReject" >驳回</el-button> <el-button v-if="dialogType === 'transfer'" type="warning" @click="confirmTransfer" >确认转办</el-button> </template> </el-dialog> </div> <script> const { createApp, ref, reactive, computed, onMounted } = Vue; createApp({ setup() { // 菜单项 const menuItems = reactive([ { key: 'design', label: '流程设计', icon: 'el-icon-s-promotion' }, { key: 'tasks', label: '我的任务', icon: 'el-icon-tickets' }, { key: 'create', label: '新建流程', icon: 'el-icon-circle-plus-outline' }, { key: 'monitor', label: '流程监控', icon: 'el-icon-data-line' } ]); const activeMenu = ref('tasks'); // LogicFlow相关 const container = ref(null); let lf = null; const selectedNode = ref(null); const currentTemplate = ref(''); // 模板列表 const templateList = ref([ { id: 1, name: '请假申请', category: '人力资源', description: '用于员工请假申请审批流程', fields: [ { name: 'leaveType', label: '请假类型', type: 'select', rules: [{ required: true, message: '请选择请假类型', trigger: 'change' }], options: [ { value: 'annual', label: '年假' }, { value: 'sick', label: '病假' }, { value: 'personal', label: '事假' }, { value: 'marriage', label: '婚假' }, { value: 'maternity', label: '产假' } ] }, { name: 'startDate', label: '开始时间', type: 'datetime', rules: [{ required: true, message: '请选择开始时间', trigger: 'change' }] }, { name: 'endDate', label: '结束时间', type: 'datetime', rules: [{ required: true, message: '请选择结束时间', trigger: 'change' }] }, { name: 'duration', label: '请假时长(天)', type: 'number', rules: [ { required: true, message: '请输入请假时长', trigger: 'blur' }, { type: 'number', min: 0.5, max: 365, message: '时长应在0.5到365天之间', trigger: 'blur' } ] }, { name: 'reason', label: '请假事由', type: 'textarea', rules: [{ required: true, message: '请输入请假事由', trigger: 'blur' }] } ], approvers: [ { nodeName: '直属上级审批', userName: '张经理', description: '审批请假合理性' }, { nodeName: '人事部门审核', userName: '李人事', description: '核对假期余额及合规性' } ] }, { id: 2, name: '采购申请', category: '财务', description: '用于物品采购申请审批流程', fields: [ { name: 'itemName', label: '物品名称', type: 'text', rules: [{ required: true, message: '请输入物品名称', trigger: 'blur' }] }, { name: 'specification', label: '规格型号', type: 'text', rules: [{ required: true, message: '请输入规格型号', trigger: 'blur' }] }, { name: 'quantity', label: '数量', type: 'number', rules: [ { required: true, message: '请输入数量', trigger: 'blur' }, { type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' } ] }, { name: 'estimatedPrice', label: '预估单价', type: 'number', rules: [ { required: true, message: '请输入预估单价', trigger: 'blur' }, { type: 'number', min: 0, message: '单价不能为负数', trigger: 'blur' } ] }, { name: 'purpose', label: '用途说明', type: 'textarea', rules: [{ required: true, message: '请输入用途说明', trigger: 'blur' }] } ], approvers: [ { nodeName: '部门经理审批', userName: '王经理', description: '审核采购必要性' }, { nodeName: '财务审核', userName: '赵会计', description: '审核预算及价格合理性' }, { nodeName: '总经理审批', userName: '孙总', description: '最终审批' } ] } ]); // 用户列表 const userList = ref([ { id: 1, name: '张三', department: '技术部' }, { id: 2, name: '李四', department: '人事部' }, { id: 3, name: '王五', department: '财务部' }, { id: 4, name: '赵六', department: '市场部' }, { id: 5, name: '钱七', department: '技术部' } ]); // 初始化LogicFlow const initLogicFlow = () => { if (!container.value) return; // 使用LogicFlow的BPMN扩展 LogicFlow.use(LogicFlow.BpmnElement); LogicFlow.use(LogicFlow.Snapshot); LogicFlow.use(LogicFlow.Menu); LogicFlow.use(LogicFlow.Control); lf = new LogicFlow({ container: container.value, grid: true, width: container.value.clientWidth, height: 500, keyboard: { enabled: true }, style: { rect: { width: 100, height: 60, radius: 6 } } }); // 渲染初始流程图 lf.render({ nodes: [ { id: '1', type: 'bpmn:startEvent', x: 100, y: 100, properties: { name: '开始' } } ] }); // 监听节点选择事件 lf.on('node:click', ({ data }) => { selectedNode.value = data; }); // 监听画布点击事件(点击空白处取消选择) lf.on('blank:click', () => { selectedNode.value = null; }); }; // 保存流程 const saveFlow = () => { if (!currentTemplate.value) { ElMessage.warning('请先选择或创建流程模板'); return; } const graphData = lf.getGraphData(); console.log('保存流程数据:', graphData); // 这里应该调用API保存流程数据 ElMessage.success('流程保存成功'); }; // 新建模板 const createNewTemplate = () => { ElMessageBox.prompt('请输入新模板名称', '新建模板', { confirmButtonText: '确定', cancelButtonText: '取消', }).then(({ value }) => { if (!value) return; const newTemplate = { id: templateList.value.length + 1, name: value, category: '自定义', description: '新建的流程模板', fields: [], approvers: [] }; templateList.value.push(newTemplate); currentTemplate.value = newTemplate.id; ElMessage.success('模板创建成功'); }); }; // 导入导出 const importFlow = () => { ElMessage.info('导入功能'); }; const exportFlow = () => { const graphData = lf.getGraphData(); const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(graphData, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "flow.json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); ElMessage.success('流程已导出'); }; // 任务相关 const taskFilter = ref('pending'); const searchKeyword = ref(''); // 模拟任务数据 const tasks = ref([ { id: 1, processName: '请假申请', applicant: '张三', applyTime: '2023-10-01 09:30', currentNode: '部门审批', status: 'pending', priority: 'normal', comment: '' }, { id: 2, processName: '采购申请', applicant: '李四', applyTime: '2023-10-01 10:15', currentNode: '财务审核', status: 'pending', priority: 'high', comment: '' }, { id: 3, processName: '费用报销', applicant: '王五', applyTime: '2023-09-30 14:20', currentNode: '已完成', status: 'completed', priority: 'normal', comment: '符合公司报销政策' } ]); // 任务状态映射 const taskStatusMap = { 'pending': '待处理', 'processing': '处理中', 'completed': '已完成', 'rejected': '已驳回' }; // 过滤后的任务列表 const filteredTasks = computed(() => { let result = tasks.value; // 根据筛选条件过滤 if (taskFilter.value === 'pending') { result = result.filter(task => task.status === 'pending'); } else if (taskFilter.value === 'processed') { result = result.filter(task => task.status === 'completed' || task.status === 'rejected'); } else if (taskFilter.value === 'started') { // 这里应该是用户自己发起的流程 result = result.filter(task => task.applicant === '当前用户'); } // 根据关键词搜索 if (searchKeyword.value) { const keyword = searchKeyword.value.toLowerCase(); result = result.filter(task => task.processName.toLowerCase().includes(keyword) || task.applicant.toLowerCase().includes(keyword) ); } return result; }); // 审批对话框相关 const approvalDialogVisible = ref(false); const dialogType = ref(''); // approve, reject, transfer const dialogTitle = ref(''); const currentTask = ref(null); const approvalForm = reactive({ comment: '', transferTo: '' }); // 打开审批对话框 const handleApprove = (task) => { currentTask.value = task; dialogType.value = 'approve'; dialogTitle.value = `审批通过 - ${task.processName}`; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开驳回对话框 const handleReject = (task) => { currentTask.value = task; dialogType.value = 'reject'; dialogTitle.value = `驳回申请 - ${task.processName}`; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开转办对话框 const handleTransfer = (task) => { currentTask.value = task; dialogType.value = 'transfer'; dialogTitle.value = `转办任务 - ${task.processName}`; approvalForm.comment = ''; approvalForm.transferTo = ''; approvalDialogVisible.value = true; }; // 确认审批通过 const confirmApprove = () => { if (!approvalForm.comment) { ElMessage.warning('请填写审批意见'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'completed'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.success('审批通过'); }; // 确认驳回 const confirmReject = () => { if (!approvalForm.comment) { ElMessage.warning('请填写驳回原因'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'rejected'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.warning('申请已驳回'); }; // 确认转办 const confirmTransfer = () => { if (!approvalForm.transferTo) { ElMessage.warning('请选择转办人'); return; } // 查找转办人 const transferToUser = userList.value.find(user => user.id === approvalForm.transferTo); if (!transferToUser) { ElMessage.error('选择的转办人不存在'); return; } // 更新任务 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.comment = `转办给: ${transferToUser.name}` + (approvalForm.comment ? `, 备注: ${approvalForm.comment}` : ''); // 在实际应用中,这里应该将任务转交给其他用户 } approvalDialogVisible.value = false; ElMessage.success(`任务已转办给 ${transferToUser.name}`); }; // 查看流程进度 const viewProcess = (task) => { ElMessage.info(`查看流程 ${task.processName} 的进度`); // 这里应该打开流程进度详情页面 }; // 新建流程相关 const formData = reactive({ templateId: '', fields: {} }); const fileList = ref([]); const submitting = ref(false); const applyForm = ref(null); // 当前选中的模板 const selectedTemplate = computed(() => { return templateList.value.find(t => t.id === formData.templateId) || null; }); // 表单验证规则 const formRules = reactive({ templateId: [ { required: true, message: '请选择流程模板', trigger: 'change' } ] }); // 处理模板选择变化 const handleTemplateChange = (templateId) => { // 重置表单字段 formData.fields = {}; // 为选中模板的每个字段初始化值 if (selectedTemplate.value) { selectedTemplate.value.fields.forEach(field => { formData.fields[field.name] = field.type === 'number' ? 0 : ''; }); } }; // 处理文件变化 const handleFileChange = (file, files) => { // 限制文件大小 if (file.size > 10 * 1024 * 1024) { ElMessage.error('文件大小不能超过10MB'); return false; } // 更新文件列表 fileList.value = files; }; // 处理文件移除 const handleFileRemove = (file) => { const index = fileList.value.indexOf(file); if (index !== -1) { fileList.value.splice(index, 1); } }; // 提交表单 const submitForm = () => { if (!applyForm.value) return; applyForm.value.validate((valid) => { if (valid) { submitting.value = true; // 模拟API调用 setTimeout(() => { ElMessage.success('申请提交成功!'); submitting.value = false; // 重置表单 resetForm(); // 切换到任务页面 activeMenu.value = 'tasks'; }, 1500); } else { ElMessage.error('请完善表单信息'); return false; } }); }; // 重置表单 const resetForm = () => { if (applyForm.value) { applyForm.value.resetFields(); } formData.fields = {}; fileList.value = []; }; // 流程监控相关 const processInstances = ref([ { id: 1, name: '请假申请', applicant: '张三', startTime: '2023-10-01 09:30', currentNode: '部门审批', status: '审批中' }, { id: 2, name: '采购申请', applicant: '李四', startTime: '2023-10-01 10:15', currentNode: '财务审核', status: '审批中' }, { id: 3, name: '费用报销', applicant: '王五', startTime: '2023-09-30 14:20', currentNode: '已完成', status: '已通过' } ]); // 状态标签类型 const statusType = (status) => { const map = { '审批中': 'primary', '已通过': 'success', '已拒绝': 'danger', '已撤销': 'info' }; return map[status] || 'info'; }; // 查看流程详情 const viewProcessDetail = (process) => { ElMessage.info(`查看流程 ${process.name} 的详情`); // 这里应该打开流程详情页面 }; // 初始化 onMounted(() => { initLogicFlow(); }); return { menuItems, activeMenu, container, selectedNode, currentTemplate, templateList, userList, saveFlow, createNewTemplate, importFlow, exportFlow, taskFilter, searchKeyword, filteredTasks, taskStatusMap, handleApprove, handleReject, handleTransfer, viewProcess, approvalDialogVisible, dialogType, dialogTitle, approvalForm, confirmApprove, confirmReject, confirmTransfer, formData, formRules, fileList, submitting, applyForm, selectedTemplate, handleTemplateChange, handleFileChange, handleFileRemove, submitForm, resetForm, processInstances, statusType, viewProcessDetail }; } }).use(ElementPlus).mount('#app'); </script> </body> </html>优化页面
09-03
<script setup> // import { h } from 'snabbdom' import { onBeforeUnmount, ref, shallowRef } from 'vue' // import { IButtonMenu } from '@wangeditor/core' import { Boot } from '@wangeditor/editor' import { Editor, Toolbar } from '@wangeditor/editor-for-vue' // 测试:第三方插件 // import withCtrlEnter from '@wangeditor/plugin-ctrl-enter' // Boot.registerPlugin(withCtrlEnter) // // 测试:多语言 // i18nChangeLanguage('en') // // 测试:注册 renderElem // function renderElemFn(elem, children) { // const vnode = h('div', {}, children) // type: 'paragraph' 节点,即渲染为 <p> 标签 // return vnode // } // const renderElemConf = { // type: 'paragraph', // 节点 type ,重要!!! // renderElem: renderElemFn, // } // Boot.registerRenderElem(renderElemConf) // // 测试:注册插件 // function withCtrlEnter(editor) { // const { insertBreak } = editor // setTimeout(() => { // // beforeInput 事件不能识别 ctrl+enter ,所以自己绑定 DOM 事件 // const { $textArea } = DomEditor.getTextarea(newEditor) // $textArea.on('keydown', e => { // const isCtrl = e.ctrlKey || e.metaKey // if (e.keyCode === 13 && isCtrl) { // // ctrl+enter 触发换行 // editor.insertBreak() // } // }) // }) // const newEditor = editor // newEditor.insertBreak = () => { // const e = window.event // const isCtrl = e.ctrlKey || e.metaKey // // 只有 ctrl 才能换行 // if (isCtrl) { // insertBreak() // } // } // return newEditor // } // Boot.registerPlugin(withCtrlEnter) // 测试:注册 button menu // class MyButtonMenu { // constructor() { // // this.title = '', // this.tag = 'button' // } // getValue() { return '' } // isActive() { return false } // isDisabled() { return false } // exec(editor) { // console.log(editor) // // alert('menu1 exec') // } // } // const menuConf = { // key: 'my-menu-1', // menu key ,唯一。注册之后,需通过 toolbarKeys 配置到工具栏 // factory() { // return new MyButtonMenu() // }, // } // // 检查菜单是否已注册,避免重复注册 // if (!Boot.getMenuConfig('my-menu-1')) { // Boot.registerMenu(menuConf); // console.log('菜单注册成功'); // } else { // console.log('菜单已存在,跳过注册'); // } // console.log(1111111111) // 移到组件最外层,确保模块加载时只执行一次 class MyButtonMenu { constructor() { this.title = ''; // 必须有标题,否则按钮不显示 this.tag = 'button'; } getValue() { return ''; } isActive() { return false; } isDisabled() { return false; } exec(editor) { // console.log('自定义菜单点击', editor); // editor.insertText('这是自定义菜单插入的内容'); // 示例功能 } } const menuConf = { key: 'my-menu-1', factory() { return new MyButtonMenu(); }, }; // 核心:用try-catch捕获重复注册错误 try { Boot.registerMenu(menuConf); console.log('菜单注册成功'); } catch (err) { if (err.message.includes('Duplicated key')) { console.log('菜单已注册,跳过重复注册'); } else { console.error('菜单注册失败:', err); } } // 编辑器实例,必须用 shallowRef ,重要! const editorRef = shallowRef() // 内容 HTML const valueHtml = ref('<p>hello world</p>') // 编辑器配置 const editorConfig = { placeholder: '请输入内容...', MENU_CONF: { insertImage: { checkImage(src) { console.log('image src', src) if (src.indexOf("http") !== 0) { return "图片网址必须以 http/https 开头"; } return true; }, }, } } // 工具栏配置 const toolbarConfig = { // toolbarKeys: ['headerSelect', 'bold', 'my-menu-1'], // excludeKeys: [], insertKeys: { index: 0, keys: 'my-menu-1' } } // 编辑器回调函数 const handleCreated = (editor) => { console.log("created", editor); editorRef.value = editor // 记录 editor 实例,重要! // window.editor = editor // 临时测试使用,用完删除 } const handleChange = (editor) => { console.log("change:", editor.children); } const handleDestroyed = (editor) => { console.log('destroyed', editor) } const handleFocus = (editor) => { console.log('focus', editor) } const handleBlur = (editor) => { console.log('blur', editor) } const customAlert = (info, type) => { alert(`【自定义提示】${type} - ${info}`) } const customPaste = (editor, event, callback) => { console.log('ClipboardEvent 粘贴事件对象', event) // 自定义插入内容 editor.insertText('xxx') // 返回值(注意,vue 事件的返回值,不能用 return) callback(false) // 返回 false ,阻止默认粘贴行为 // callback(true) // 返回 true ,继续默认的粘贴行为 } // 及时销毁编辑器 onBeforeUnmount(() => { const editor = editorRef.value if (editor == null) return // 销毁,并移除 editor editor.destroy() }) const getHtml = () => { const editor = editorRef.value if (editor == null) return console.log(editor.getHtml()) } </script> <template> <!-- <div>--> <!-- <button @click="getHtml">获取 html</button>--> <!-- </div>--> <!-- <div style="border: 1px solid #ccc">--> <!-- <!– 工具栏 –>--> <!-- <Toolbar--> <!-- :editor="editorRef"--> <!-- :defaultConfig="toolbarConfig"--> <!-- style="border-bottom: 1px solid #ccc"--> <!-- />--> <!-- <!– 编辑器 –>--> <!-- <Editor--> <!-- v-model="valueHtml"--> <!-- :defaultConfig="editorConfig"--> <!-- @onChange="handleChange"--> <!-- @onCreated="handleCreated"--> <!-- @onDestroyed="handleDestroyed"--> <!-- @onFocus="handleFocus"--> <!-- @onBlur="handleBlur"--> <!-- @customAlert="customAlert"--> <!-- @customPaste="customPaste"--> <!-- style="height: 500px"--> <!-- />--> <!-- </div>--> <div class="editor-container"> <!-- 添加容器类名 --> <!-- 工具栏 --> <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #e5e7eb" /> <!-- 编辑器 --> <Editor v-model="valueHtml" :defaultConfig="editorConfig" @onChange="handleChange" @onCreated="handleCreated" @onDestroyed="handleDestroyed" @onFocus="handleFocus" @onBlur="handleBlur" @customAlert="customAlert" @customPaste="customPaste" /> </div> </template> <style src="@wangeditor/editor/dist/css/style.css"> /* 编辑器整体容器 */ .editor-container { border: 1px solid #e5e7eb; /* 浅灰色边框 */ border-radius: 8px; /* 圆角 */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* 轻微阴影 */ overflow: hidden; /* 防止内部元素溢出 */ transition: box-shadow 0.2s ease; /* 阴影过渡效果 */ } .editor-container:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* hover时增强阴影 */ } /* 工具栏样式 */ .w-e-toolbar { background-color: #f9fafb !important; /* 工具栏背景色 */ border-bottom: 1px solid #e5e7eb !important; /* 底部边框 */ padding: 8px 12px !important; /* 内边距 */ } /* 工具栏按钮通用样式 */ .w-e-toolbar .w-e-menu { margin: 0 3px !important; /* 按钮间距 */ border-radius: 4px !important; /* 按钮圆角 */ transition: all 0.2s ease !important; /* 过渡效果 */ } .w-e-toolbar .w-e-menu:hover { background-color: #eef2f5 !important; /* 悬停背景色 */ transform: translateY(-1px) !important; /* 轻微上浮效果 */ } /* 自定义菜单按钮样式(my-menu-1) */ .w-e-toolbar [data-key="my-menu-1"] { background-color: #4f46e5 !important; /* 紫色背景(突出自定义按钮) */ color: white !important; /* 白色文字 */ border: none !important; } .w-e-toolbar [data-key="my-menu-1"]:hover { background-color: #4338ca !important; /* 深色hover效果 */ } /* 编辑区域样式 */ .w-e-text-container { height: 500px !important; /* 固定高度 */ padding: 16px !important; /* 内边距 */ background-color: white !important; /* 白色背景 */ } /* 编辑区域内容样式 */ .w-e-text-container p { margin: 0 0 12px 0 !important; /* 段落间距 */ line-height: 1.8 !important; /* 行高(提升可读性) */ font-size: 14px !important; /* 基础字体大小 */ color: #1f2937 !important; /* 文字颜色 */ } /* 编辑区域聚焦样式 */ .w-e-text-container:focus-within { outline: 2px solid rgba(79, 70, 229, 0.2) !important; /* 聚焦时边框高亮 */ } /* 工具栏分割线样式 */ .w-e-toolbar .w-e-separator { background-color: #e5e7eb !important; /* 分割线颜色 */ margin: 0 6px !important; /* 分割线间距 */ } /* 响应式调整(小屏幕适配) */ @media (max-width: 768px) { .editor-container { border-radius: 4px; /* 小屏幕减小圆角 */ } .w-e-toolbar { padding: 4px 8px !important; /* 小屏幕减小内边距 */ } .w-e-text-container { height: 400px !important; /* 小屏幕减小编辑区高度 */ padding: 12px !important; } } </style>改一改让工具栏悬浮在上面,不会随着页面移动,不论页面滚轮怎么移动都保持在页面上方,右侧添加滚轮使页面可以移动,工具栏上面再搞一个头部,最左边是返回键,中间是AI笔记的标题,最右边是一个头像,点击头像会跳转到个人用户管理界面,头像左边有一个历史记录按钮,点击后会显示一个侧边栏,里面有AI笔记历史,历史记录先留一个按钮就行,头像也先留一个位置就行
07-26
将以下代码保存为 接单词.txt # 互动英语单词打字游戏实现方案 我将创建一个英语单词从屏幕顶部掉落且有旋转效果的互动打字游戏,包含所有您要求的功能。 ## 设计思路 1. 使用HTML Canvas绘制游戏界面和动画 2. JavaScript实现游戏逻辑和控制 3. 单词随机从列表选取并下落 4. 输入框位于底部,有占位提示 5. 单词成功输入或到底部时消失并淡出 6. 每5秒生成新单词 7. 单词下落速度随时间增加 8. 旋转效果通过CSS 3D变换实现 下面是完整的实现代码: ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>英语单词打字游戏</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Arial Rounded MT Bold', 'Segoe UI', sans-serif; } body { background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); color: white; min-height: 100vh; padding: 20px; overflow: hidden; display: flex; flex-direction: column; align-items: center; } .header { text-align: center; margin-bottom: 20px; width: 100%; padding: 15px; background: rgba(0, 0, 0, 0.4); border-radius: 15px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(8px); } h1 { font-size: 2.8rem; margin-bottom: 10px; text-shadow: 0 4px 8px rgba(0, 0, 0, 0.6); background: linear-gradient(to right, #ff8a00, #e52e71); -webkit-background-clip: text; background-clip: text; color: transparent; } .subtitle { font-size: 1.2rem; max-width: 800px; margin: 0 auto; color: #ddd; } .stats { display: flex; justify-content: space-around; width: 100%; max-width: 800px; margin: 15px auto; padding: 15px; background: rgba(0, 0, 0, 0.3); border-radius: 12px; font-size: 1.2rem; } .stat-item { text-align: center; padding: 10px; } .stat-value { font-size: 1.8rem; font-weight: bold; color: #ffd700; } .game-container { position: relative; width: 800px; height: 500px; background: rgba(0, 0, 50, 0.6); border-radius: 15px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5); overflow: hidden; margin: 15px 0 25px; border: 2px solid #4a76ef; } canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } #gameCanvas { z-index: 1; } #effectsCanvas { z-index: 2; pointer-events: none; } .input-container { display: flex; flex-direction: column; width: 800px; padding: 15px; background: rgba(0, 0, 0, 0.5); border-radius: 15px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); margin-top: 15px; } label { margin-bottom: 10px; font-size: 1.3rem; color: #4df; } #wordInput { width: 100%; padding: 15px; font-size: 1.5rem; border: none; border-radius: 10px; background: rgba(255, 255, 255, 0.95); color: #333; outline: none; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); transition: all 0.3s ease; } #wordInput:focus { box-shadow: 0 4px 20px rgba(100, 200, 255, 0.6); transform: translateY(-2px); } .controls { display: flex; justify-content: center; gap: 20px; margin-top: 20px; width: 800px; } button { padding: 13px 35px; font-size: 1.2rem; border: none; border-radius: 50px; background: linear-gradient(to right, #3498db, #2ecc71); color: white; cursor: pointer; box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; font-weight: bold; } button:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); } button:active { transform: translateY(1px); } #startBtn { background: linear-gradient(to right, #2ecc71, #3498db); } #resetBtn { background: linear-gradient(to right, #e74c3c, #f39c12); } .instructions { width: 800px; background: rgba(0, 0, 0, 0.4); padding: 20px; border-radius: 15px; margin-top: 25px; font-size: 1.1rem; line-height: 1.6; } .instructions h3 { color: #ffd700; margin-bottom: 15px; text-align: center; font-size: 1.4rem; } .instructions ul { padding-left: 25px; } .instructions li { margin-bottom: 10px; } .word { position: absolute; font-size: 28px; font-weight: bold; color: white; text-shadow: 0 0 10px rgba(0, 200, 255, 0.8); transition: transform 0.2s ease; cursor: pointer; padding: 8px 15px; border-radius: 10px; background: rgba(0, 50, 150, 0.6); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } @keyframes rotate { 0% { transform: rotateY(0deg) rotateX(0deg); } 100% { transform: rotateY(360deg) rotateX(15deg); } } .fade-out { animation: fadeOut 0.5s forwards; } @keyframes fadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.5); } } .hitEffect { position: absolute; width: 100px; height: 100px; background: radial-gradient(circle, rgba(255, 215, 0, 0.8) 0%, transparent 70%); border-radius: 50%; animation: expand 0.8s forwards; pointer-events: none; } @keyframes expand { 0% { transform: scale(0.1); opacity: 1; } 100% { transform: scale(3); opacity: 0; } } .level-indicator { position: absolute; top: 20px; right: 20px; padding: 8px 15px; background: rgba(0, 0, 0, 0.6); border-radius: 20px; font-size: 1.2rem; color: #ffd700; z-index: 10; border: 2px solid #3498db; } .notification { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 15px 30px; border-radius: 10px; font-size: 1.3rem; z-index: 1000; opacity: 0; transition: opacity 0.3s; } .notification.show { opacity: 1; } </style> </head> <body> <div class="header"> <h1>英语单词打字游戏</h1> <p class="subtitle">输入底部文本框中显示的单词来"接住"下落的单词 - 提高你的打字速度和词汇量!</p> </div> <div class="stats"> <div class="stat-item"> <div>得分</div> <div id="score" class="stat-value">0</div> </div> <div class="stat-item"> <div>正确单词</div> <div id="correctCount" class="stat-value">0</div> </div> <div class="stat-item"> <div>速度等级</div> <div id="speedLevel" class="stat-value">1</div> </div> <div class="stat-item"> <div>剩余时间</div> <div id="timer" class="stat-value">60</div> </div> </div> <div class="game-container"> <canvas id="gameCanvas"></canvas> <canvas id="effectsCanvas"></canvas> <div class="level-indicator">等级: <span id="levelDisplay">1</span></div> </div> <div class="input-container"> <label for="wordInput">输入下方显示的单词:</label> <input type="text" id="wordInput" placeholder="在这里输入单词..." autocomplete="off"> </div> <div class="controls"> <button id="startBtn">开始游戏</button> <button id="resetBtn">重新开始</button> </div> <div class="instructions"> <h3>游戏说明</h3> <ul> <li>单词会从屏幕顶部随机位置掉落并有旋转效果</li> <li>在底部输入框中输入当前显示的单词来"接住"它</li> <li>每次正确输入单词可以获得<span style="color:#ffd700;">10分</span></li> <li>单词到达底部或输入错误会<span style="color:#ff6b6b;">减少生命值</span></li> <li>每隔5秒会生成一个新单词</li> <li>单词掉落速度会随时间增加(每30秒升一级)</li> <li>游戏时间60秒,尽可能获得高分!</li> </ul> </div> <div class="notification" id="notification"></div> <script> // 游戏配置 const config = { wordList: [ "apple", "banana", "computer", "keyboard", "programming", "javascript", "html", "css", "developer", "algorithm", "variable", "function", "object", "array", "string", "integer", "boolean", "loop", "condition", "syntax", "framework", "library", "database", "network", "animation" ], initialSpeed: 2, speedIncreaseRate: 0.25, spawnInterval: 5000, // 5秒 gameDuration: 60000, // 60秒 maxWords: 8, // 最大同时显示的单词数 basePoints: 10, lives: 5 }; // 游戏状态 const gameState = { active: false, score: 0, correctCount: 0, speed: config.initialSpeed, speedLevel: 1, timeLeft: config.gameDuration / 1000, lives: config.lives, words: [], fallingObjects: [], currentTargetWord: "", effects: [], lastSpawnTime: 0, startTime: 0 }; // DOM元素 const elements = { gameCanvas: document.getElementById('gameCanvas'), effectsCanvas: document.getElementById('effectsCanvas'), wordInput: document.getElementById('wordInput'), scoreDisplay: document.getElementById('score'), correctCountDisplay: document.getElementById('correctCount'), speedLevelDisplay: document.getElementById('speedLevel'), timerDisplay: document.getElementById('timer'), levelDisplay: document.getElementById('levelDisplay'), startBtn: document.getElementById('startBtn'), resetBtn: document.getElementById('resetBtn'), notification: document.getElementById('notification') }; // 获取Canvas上下文 const gameCtx = elements.gameCanvas.getContext('2d'); const effectsCtx = elements.effectsCanvas.getContext('2d'); // 初始化Canvas大小 function initCanvas() { const container = document.querySelector('.game-container'); elements.gameCanvas.width = container.clientWidth; elements.gameCanvas.height = container.clientHeight; elements.effectsCanvas.width = container.clientWidth; elements.effectsCanvas.height = container.clientHeight; } // 随机选择单词 function getRandomWord() { return config.wordList[Math.floor(Math.random() * config.wordList.length)]; } // 生成新单词 function spawnWord() { if (!gameState.active || gameState.fallingObjects.length >= config.maxWords) return; const word = getRandomWord(); const x = Math.random() * (elements.gameCanvas.width - 150); gameState.fallingObjects.push({ word: word, x: x, y: -50, speed: gameState.speed, width: 100, height: 50, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * 0.1, opacity: 1 }); gameState.lastSpawnTime = Date.now(); } // 设置目标单词显示 function setTargetWord() { if (gameState.fallingObjects.length > 0) { const randomObj = gameState.fallingObjects[ Math.floor(Math.random() * gameState.fallingObjects.length) ]; gameState.currentTargetWord = randomObj.word; elements.wordInput.placeholder = `输入: ${randomObj.word}`; } } // 更新游戏状态 function update() { if (!gameState.active) return; const currentTime = Date.now(); const elapsed = currentTime - gameState.startTime; // 更新时间 gameState.timeLeft = Math.max(0, Math.floor((config.gameDuration - elapsed) / 1000)); elements.timerDisplay.textContent = gameState.timeLeft; // 更新速度等级 const newSpeedLevel = Math.floor(elapsed / 30000) + 1; if (newSpeedLevel > gameState.speedLevel) { gameState.speedLevel = newSpeedLevel; gameState.speed = config.initialSpeed + (gameState.speedLevel - 1) * config.speedIncreaseRate; elements.speedLevelDisplay.textContent = gameState.speedLevel; elements.levelDisplay.textContent = gameState.speedLevel; showNotification(`速度提升到等级 ${gameState.speedLevel}!`); } // 生成新单词 if (currentTime - gameState.lastSpawnTime > config.spawnInterval) { spawnWord(); } // 更新所有下落单词 for (let i = gameState.fallingObjects.length - 1; i >= 0; i--) { const obj = gameState.fallingObjects[i]; // 更新位置 obj.y += obj.speed; obj.rotation += obj.rotationSpeed; // 检查是否到达底部 if (obj.y > elements.gameCanvas.height) { gameState.fallingObjects.splice(i, 1); gameState.lives--; showNotification(`错过 ${obj.word}! 生命值减少`); if (gameState.lives <= 0) { endGame(); } continue; } } // 重新设置目标单词 if (!gameState.currentTargetWord && gameState.fallingObjects.length > 0) { setTargetWord(); } // 检查游戏结束 if (elapsed >= config.gameDuration || gameState.lives <= 0) { endGame(); } } // 渲染游戏 function render() { // 清除画布 gameCtx.clearRect(0, 0, elements.gameCanvas.width, elements.gameCanvas.height); effectsCtx.clearRect(0, 0, elements.effectsCanvas.width, elements.effectsCanvas.height); // 绘制背景 gameCtx.fillStyle = 'rgba(10, 20, 50, 0.6)'; gameCtx.fillRect(0, 0, elements.game
10-15
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值