outsystem中文教程2025
outsystem中文教程2025
outsystem中文教程2025
<!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;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: flex;
min-height: 100vh;
}
/* 左侧菜单栏样式 */
.sidebar {
width: 200px;
background-color: #f5f5f5;
padding: 20px;
border-right: 1px solid #e0e0e0;
}
.menu-item {
padding: 10px 15px;
margin-bottom: 5px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
}
.menu-item:hover {
background-color: #e0e0e0;
}
.menu-item.active {
background-color: #007bff;
color: white;
}
/* 右侧内容区样式 */
.main-content {
flex: 1;
padding: 20px;
background-color: #ffffff;
}
/* 顶部功能区样式 */
.top-actions {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
.btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #0056b3;
}
/* 内容区域样式 */
.content-section {
display: none;
}
.content-section.active {
display: block;
}
/* 备忘录样式 */
.memo-item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.memo-checkbox {
width: 20px;
height: 20px;
border: 2px solid #007bff;
border-radius: 50%;
margin-right: 10px;
cursor: pointer;
position: relative;
}
.memo-checkbox.checked {
background-color: #007bff;
}
.memo-checkbox.checked::after {
content: '✓';
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
}
.memo-content {
flex: 1;
}
.memo-content.completed {
color: #888;
text-decoration: line-through;
}
.memo-date {
color: #666;
font-size: 0.9em;
margin-right: 10px;
}
.memo-delete {
color: #dc3545;
cursor: pointer;
padding: 5px;
}
.memo-date-group {
margin-bottom: 20px;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
}
.memo-date-title {
color: #007bff;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #e0e0e0;
}
/* 待办事项区域样式 */
.todo-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-top: 20px;
}
.todo-section {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 15px;
}
.todo-section h3 {
margin-bottom: 15px;
color: #333;
}
.todo-item {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
cursor: move;
user-select: none;
}
.todo-items {
min-height: 100px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.todo-items.dragover {
background-color: #e9ecef;
border: 2px dashed #007bff;
}
.todo-item-content {
margin-bottom: 5px;
}
.todo-item-actions {
display: flex;
gap: 10px;
}
.todo-item-actions button {
font-size: 0.8em;
padding: 3px 8px;
}
.add-item {
margin-top: 10px;
}
.add-item input[type="text"] {
width: 400px;
padding: 8px;
font-size: 16px;
}
.add-item input[type="date"] {
padding: 8px;
font-size: 16px;
}
/* 添加响应式设计 */
@media (max-width: 768px) {
body {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #e0e0e0;
}
.todo-container {
grid-template-columns: 1fr;
}
}
/* 添加打印样式 */
@media print {
.sidebar, .top-actions {
display: none;
}
.main-content {
padding: 0;
}
.memo-item, .todo-item {
page-break-inside: avoid;
}
}
/* 悬浮提示框样式 */
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #28a745;
color: white;
padding: 15px 25px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
opacity: 0;
z-index: 1000;
transition: opacity 0.5s, transform 0.5s;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast.hide {
opacity: 0;
transform: translate(-50%, -20px);
}
.toast.error {
background-color: #dc3545;
}
</style>
</head>
<body>
<!-- 左侧菜单栏 -->
<div class="sidebar">
<div class="menu-item active" data-section="memo">备忘录</div>
<div class="menu-item" data-section="todo">待办事项</div>
</div>
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 顶部功能区 -->
<div class="top-actions">
<div style="display: flex; align-items: center; gap: 10px;">
<button class="btn" id="importBtn">导入数据</button>
<button class="btn" id="exportBtn">导出数据</button>
<div>
<input type="text" id="memosCsvPathInput" value="/Users/baidoufu/Desktop/memos.csv" readonly style="padding: 5px; border: 1px solid #ced4da; border-radius: 4px; width: 300px; background-color: #f8f9fa;">
</div>
<div>
<input type="text" id="todosCsvPathInput" value="/Users/baidoufu/Desktop/todos.csv" readonly style="padding: 5px; border: 1px solid #ced4da; border-radius: 4px; width: 300px; background-color: #f8f9fa;">
</div>
<div>
<input type="text" id="backupPathInput" value="/Users/baidoufu/Desktop/backup" readonly style="padding: 5px; border: 1px solid #ced4da; border-radius: 4px; width: 300px; background-color: #f8f9fa;">
</div>
<button class="btn" id="backupBtn">备份数据</button>
</div>
</div>
<!-- 备忘录区域 -->
<div class="content-section active" id="memo-section">
<h2>备忘录</h2>
<div class="add-item">
<input type="text" id="newMemoInput" placeholder="输入新的备忘录内容" style="width: 500px; padding: 10px; font-size: 16px;">
<input type="date" id="newMemoDate" value="" style="padding: 8px; font-size: 16px;">
<button class="btn" onclick="addMemo()">添加</button>
</div>
<div id="memoList"></div>
</div>
<!-- 待办事项区域 -->
<div class="content-section" id="todo-section">
<div class="todo-header">
<h2>待办事项</h2>
<div class="add-item">
<input type="text" id="newTodoInput" placeholder="输入新的待办事项" style="width: 500px; padding: 10px; font-size: 16px;">
<button class="btn" onclick="addTodoItem('pending')">创建待办</button>
</div>
</div>
<div class="todo-container">
<div class="todo-section" id="todo-pending">
<h3>待办区</h3>
<div class="todo-items" ondrop="drop(event)" ondragover="allowDrop(event)" data-section="pending"></div>
</div>
<div class="todo-section" id="todo-inProgress">
<h3>进行区</h3>
<div class="todo-items" ondrop="drop(event)" ondragover="allowDrop(event)" data-section="inProgress"></div>
</div>
<div class="todo-section" id="todo-completed">
<h3>完成区</h3>
<div class="todo-items" ondrop="drop(event)" ondragover="allowDrop(event)" data-section="completed"></div>
</div>
<div class="todo-section" id="todo-onHold">
<h3>搁置区</h3>
<div class="todo-items" ondrop="drop(event)" ondragover="allowDrop(event)" data-section="onHold"></div>
</div>
</div>
</div>
</div>
<script>
// 数据存储
let memos = [];
let todos = {
pending: [],
inProgress: [],
completed: [],
onHold: []
};
// 菜单切换功能
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
document.querySelectorAll('.content-section').forEach(i => i.classList.remove('active'));
item.classList.add('active');
const section = item.dataset.section;
document.getElementById(`${section}-section`).classList.add('active');
});
});
// 初始化日期选择器默认值为今天
document.getElementById('newMemoDate').valueAsDate = new Date();
// 备忘录功能
function addMemo() {
const input = document.getElementById('newMemoInput');
const dateInput = document.getElementById('newMemoDate');
const content = input.value.trim();
const selectedDate = dateInput.value;
if (!content) {
alert('请输入备忘录内容');
return;
}
if (!selectedDate) {
alert('请选择日期');
return;
}
// 修复日期比较逻辑
const selectedDateObj = new Date(selectedDate);
const today = new Date();
// 只比较年月日
const isPastDate = selectedDateObj.getFullYear() < today.getFullYear() ||
(selectedDateObj.getFullYear() === today.getFullYear() &&
selectedDateObj.getMonth() < today.getMonth()) ||
(selectedDateObj.getFullYear() === today.getFullYear() &&
selectedDateObj.getMonth() === today.getMonth() &&
selectedDateObj.getDate() < today.getDate());
if (isPastDate) {
if (!confirm('您选择的日期是过去日期,确定要添加吗?')) {
return;
}
}
const memo = {
id: Date.now(),
content: content,
date: selectedDate,
completed: false
};
memos.push(memo);
renderMemos();
input.value = '';
}
function renderMemos() {
const memoList = document.getElementById('memoList');
memoList.innerHTML = '';
// 按日期对备忘录进行分组
const groupedMemos = {};
memos.forEach(memo => {
if (!groupedMemos[memo.date]) {
groupedMemos[memo.date] = [];
}
groupedMemos[memo.date].push(memo);
});
// 按日期排序(从新到旧)
const sortedDates = Object.keys(groupedMemos).sort((a, b) => new Date(b) - new Date(a));
sortedDates.forEach(date => {
// 检查该日期下的所有备忘录是否都已完成
const allCompleted = groupedMemos[date].every(memo => memo.completed);
// 如果该日期下的所有备忘录都已完成,则跳过渲染
if (allCompleted) return;
// 创建日期分组标题
const dateGroup = document.createElement('div');
dateGroup.className = 'memo-date-group';
dateGroup.innerHTML = `<h3 class="memo-date-title">${date}</h3>`;
memoList.appendChild(dateGroup);
// 渲染该日期下的所有备忘录
groupedMemos[date].forEach(memo => {
const memoElement = document.createElement('div');
memoElement.className = 'memo-item';
memoElement.innerHTML = `
<div class="memo-checkbox ${memo.completed ? 'checked' : ''}" onclick="toggleMemo(${memo.id})"></div>
<span class="memo-content ${memo.completed ? 'completed' : ''}">${memo.content}</span>
<span class="memo-delete" onclick="deleteMemo(${memo.id})">×</span>
`;
dateGroup.appendChild(memoElement);
});
});
}
function toggleMemo(id) {
const memo = memos.find(m => m.id === id);
if (memo) {
memo.completed = !memo.completed;
renderMemos();
}
}
function deleteMemo(id) {
memos = memos.filter(m => m.id !== id);
renderMemos();
}
// 待办事项功能
function addTodoItem(section) {
const input = document.getElementById('newTodoInput');
if (!input) return;
const content = input.value.trim();
if (content) {
const todo = {
id: Date.now(),
content: content
};
todos[section].push(todo);
renderTodos();
input.value = '';
}
}
function deleteTodoItem(section, id) {
todos[section] = todos[section].filter(item => item.id !== id);
renderTodos();
}
function moveTodoItem(id, fromSection, toSection) {
const item = todos[fromSection].find(item => item.id === id);
if (item) {
todos[fromSection] = todos[fromSection].filter(i => i.id !== id);
todos[toSection].push(item);
renderTodos();
}
}
function renderTodos() {
Object.keys(todos).forEach(section => {
const container = document.querySelector(`#todo-${section} .todo-items`);
container.innerHTML = '';
todos[section].forEach(todo => {
const todoElement = document.createElement('div');
todoElement.className = 'todo-item';
todoElement.draggable = true;
todoElement.setAttribute('data-id', todo.id);
todoElement.setAttribute('data-section', section);
todoElement.ondragstart = drag;
todoElement.innerHTML = `
<div class="todo-item-content">${todo.content}</div>
<div class="todo-item-actions">
<button class="btn" style="background-color: #dc3545;" onclick="deleteTodoItem('${section}', ${todo.id})">删除</button>
</div>
`;
container.appendChild(todoElement);
});
});
}
function getSectionName(section) {
const names = {
pending: '待办区',
inProgress: '进行区',
completed: '完成区',
onHold: '搁置区'
};
return names[section];
}
// 拖拽功能
function allowDrop(event) {
event.preventDefault();
const dropTarget = event.target.closest('.todo-items');
if (dropTarget) {
dropTarget.classList.add('dragover');
}
}
function drag(event) {
const todoItem = event.target.closest('.todo-item');
if (todoItem) {
event.dataTransfer.setData('text/plain', JSON.stringify({
id: parseInt(todoItem.getAttribute('data-id')),
fromSection: todoItem.getAttribute('data-section')
}));
}
}
function drop(event) {
event.preventDefault();
const dropTarget = event.target.closest('.todo-items');
if (!dropTarget) return;
dropTarget.classList.remove('dragover');
const data = JSON.parse(event.dataTransfer.getData('text/plain'));
const toSection = dropTarget.dataset.section;
if (data.fromSection !== toSection) {
moveTodoItem(data.id, data.fromSection, toSection);
}
}
// 添加拖拽结束时移除视觉效果
document.querySelectorAll('.todo-items').forEach(container => {
container.addEventListener('dragleave', (event) => {
event.preventDefault();
const dropTarget = event.target.closest('.todo-items');
if (dropTarget) {
dropTarget.classList.remove('dragover');
}
});
});
// CSV导入导出功能
document.getElementById('importBtn').addEventListener('click', async () => {
try {
const memosCsvPath = document.getElementById('memosCsvPathInput').value.trim();
const todosCsvPath = document.getElementById('todosCsvPathInput').value.trim();
// 读取memos.csv
const memosResponse = await fetch('/read-files', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
memosCsvPath: memosCsvPath,
todosCsvPath: todosCsvPath
})
});
const data = await memosResponse.json();
if (data.status === 'success') {
if (data.memosContent) {
importMemosFromCSV(data.memosContent);
}
if (data.todosContent) {
importTodosFromCSV(data.todosContent);
}
showToast('数据导入成功');
} else {
throw new Error(data.error || '导入数据失败');
}
} catch (error) {
showToast('导入数据失败: ' + error.message, 'error');
}
});
document.getElementById('exportBtn').addEventListener('click', async () => {
try {
const memosCsvPath = document.getElementById('memosCsvPathInput').value.trim();
const todosCsvPath = document.getElementById('todosCsvPathInput').value.trim();
// 导出所有备忘录数据
const memosCSV = generateMemosCSV(memos);
await writeFile(memosCsvPath, memosCSV);
// 导出所有待办事项数据
const todosCSV = generateTodosCSV(todos);
await writeFile(todosCsvPath, todosCSV);
showToast('数据导出成功');
} catch (error) {
showToast('导出数据失败: ' + error.message, 'error');
}
});
// 修改备份按钮点击事件处理
document.getElementById('backupBtn').addEventListener('click', async () => {
try {
const backupPath = document.getElementById('backupPathInput').value.trim();
// 获取当前时间戳
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
// 备份备忘录数据
const memosCSV = generateMemosCSV(memos);
const memosBackupPath = `${backupPath}/memos_${timestamp}.csv`;
await writeFile(memosBackupPath, memosCSV);
// 备份待办事项数据
const todosCSV = generateTodosCSV(todos);
const todosBackupPath = `${backupPath}/todos_${timestamp}.csv`;
await writeFile(todosBackupPath, todosCSV);
showToast('数据备份成功');
} catch (error) {
showToast('备份数据失败: ' + error.message, 'error');
}
});
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(new Error('文件读取失败'));
reader.readAsText(file);
});
}
function writeFile(path, content) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/export-data', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error('文件写入失败'));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.send(JSON.stringify({
path: path,
content: content
}));
});
}
function generateMemosCSV(memosData) {
let csv = 'id,content,date,completed\n';
memosData.forEach(memo => {
const content = memo.content.replace(/"/g, '""');
csv += `${memo.id},"${content}",${memo.date},${memo.completed}\n`;
});
return csv;
}
function generateTodosCSV(todosData) {
let csv = 'section,id,content\n';
Object.entries(todosData).forEach(([section, items]) => {
items.forEach(todo => {
const content = todo.content.replace(/"/g, '""');
csv += `${section},${todo.id},"${content}"\n`;
});
});
return csv;
}
function importMemosFromCSV(csv) {
const lines = csv.split('\n');
if (lines.length < 2) {
alert('CSV文件格式不正确');
return;
}
memos = lines.slice(1).filter(line => line.trim()).map(line => {
// 使用更精确的CSV解析方法
const cols = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
if (cols.length < 4) return null;
return {
id: parseInt(cols[0]),
content: cols[1].replace(/^"|"$/g, '').replace(/""/g, '"'),
date: cols[2],
completed: cols[3] === 'true'
};
}).filter(Boolean);
renderMemos();
}
function importTodosFromCSV(csv) {
const lines = csv.split('\n');
if (lines.length < 2) return;
Object.keys(todos).forEach(key => todos[key] = []);
lines.slice(1).filter(line => line.trim()).forEach(line => {
const cols = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
if (cols.length < 3) return;
const section = cols[0];
if (todos[section]) {
todos[section].push({
id: parseInt(cols[1]),
content: cols[2].replace(/^"|"$/g, '').replace(/""/g, '"')
});
}
});
renderTodos();
}
function showToast(message, type = 'success') {
// 移除所有现有的提示框
document.querySelectorAll('.toast').forEach(toast => toast.remove());
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
// 显示提示框
setTimeout(() => {
toast.classList.add('show');
}, 10);
// 3秒后隐藏提示框
setTimeout(() => {
toast.classList.remove('show');
toast.classList.add('hide');
// 在动画结束后移除提示框
toast.addEventListener('transitionend', () => {
if (toast.classList.contains('hide')) {
toast.remove();
}
}, { once: true });
}, 2000);
}
</script>
</body>
</html>
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import os
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
try:
with open('todo-page.html', 'rb') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(content)
except:
self.send_error(404, 'File not found')
else:
self.send_error(404, 'File not found')
def do_POST(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
try:
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
if self.path == '/export-data':
path = data.get('path')
content = data.get('content')
# 检查路径和内容是否有效
if not path or not content:
raise ValueError("Invalid request data")
# 创建目录(如果不存在)
os.makedirs(os.path.dirname(path), exist_ok=True)
# 写入文件
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'success'}).encode())
except PermissionError:
self.send_response(403)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': 'Permission denied'}).encode())
except Exception as e:
self.send_response(500)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': str(e)}).encode())
elif self.path == '/read-files':
memos_csv_path = data.get('memosCsvPath')
todos_csv_path = data.get('todosCsvPath')
response_data = {'status': 'success'}
# 读取备忘录CSV文件
if os.path.exists(memos_csv_path):
with open(memos_csv_path, 'r', encoding='utf-8') as f:
response_data['memosContent'] = f.read()
# 读取待办事项CSV文件
if os.path.exists(todos_csv_path):
with open(todos_csv_path, 'r', encoding='utf-8') as f:
response_data['todosContent'] = f.read()
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode())
else:
self.send_error(404, 'Endpoint not found')
except Exception as e:
self.send_response(400)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': str(e)}).encode())
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
server = HTTPServer(('localhost', 8000), RequestHandler)
print("Server running on port 8000")
server.serve_forever()