diff --git a/src/config.py b/src/config.py index 21b4024..d042d39 100644 --- a/src/config.py +++ b/src/config.py @@ -10,6 +10,7 @@ load_dotenv() # --- File Paths & Directories --- STATE_FILE = "xianyu_state.json" IMAGE_SAVE_DIR = "images" +CONFIG_FILE = "config.json" os.makedirs(IMAGE_SAVE_DIR, exist_ok=True) # 任务隔离的临时图片目录前缀 diff --git a/src/file_operator.py b/src/file_operator.py new file mode 100644 index 0000000..1f2c2d6 --- /dev/null +++ b/src/file_operator.py @@ -0,0 +1,46 @@ +import aiofiles +from pathlib import Path + + +class FileOperator: + def __init__(self, filepath: str): + self.filepath = filepath + + async def read(self) -> str | None: + """ + 读取 + """ + try: + async with aiofiles.open(self.filepath, 'r', encoding='utf-8') as f: + content_str = await f.read() + if content_str.strip(): + return content_str + else: + return None + except FileNotFoundError: + print(f"文件 {self.filepath} 不存在") + return None + except PermissionError: + print(f"错误:没有权限读取文件 {self.filepath}") + return None + except Exception as e: + print(f"读取文件 {self.filepath} 时发生错误: {e}") + return None + + async def write(self, content: str) -> bool: + """ + 写入 + """ + try: + Path(self.filepath).parent.mkdir(parents=True, exist_ok=True) + + async with aiofiles.open(self.filepath, 'w', encoding='utf-8') as f: + await f.write(content) + return True + + except PermissionError: + print(f"错误:没有权限写入文件 {self.filepath}") + return False + except Exception as e: + print(f"写入文件 {self.filepath} 时发生错误: {e}") + return False diff --git a/src/task.py b/src/task.py new file mode 100644 index 0000000..cc567ce --- /dev/null +++ b/src/task.py @@ -0,0 +1,95 @@ +import json + +from pydantic import BaseModel +from typing import Optional + +from src.config import CONFIG_FILE +from src.file_operator import FileOperator + + +class Task(BaseModel): + task_name: str + enabled: bool + keyword: str + description: str + max_pages: int + personal_only: bool + min_price: Optional[str] = None + max_price: Optional[str] = None + cron: Optional[str] = None + ai_prompt_base_file: str + ai_prompt_criteria_file: str + is_running: Optional[bool] = False + + +class TaskUpdate(BaseModel): + task_name: Optional[str] = None + enabled: Optional[bool] = None + keyword: Optional[str] = None + description: Optional[str] = None + max_pages: Optional[int] = None + personal_only: Optional[bool] = None + min_price: Optional[str] = None + max_price: Optional[str] = None + cron: Optional[str] = None + ai_prompt_base_file: Optional[str] = None + ai_prompt_criteria_file: Optional[str] = None + is_running: Optional[bool] = None + + +async def add_task(task: Task) -> bool: + config_file_op = FileOperator(CONFIG_FILE) + + config_data_str = await config_file_op.read() + config_data = json.loads(config_data_str) if config_data_str else [] + config_data.append(task) + + return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2)) + + +async def update_task(task_id: int, task: Task) -> bool: + config_file_op = FileOperator(CONFIG_FILE) + + config_data_str = await config_file_op.read() + + if not config_data_str: + return False + + config_data = json.loads(config_data_str) + + if len(config_data) <= task_id: + return False + + config_data[task_id] = task + + return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2)) + + +async def get_task(task_id: int) -> Task | None: + config_file_op = FileOperator(CONFIG_FILE) + config_data_str = await config_file_op.read() + + if not config_data_str: + return None + + config_data = json.loads(config_data_str) + if len(config_data) <= task_id: + return None + + return config_data[task_id] + + +async def remove_task(task_id: int) -> bool: + config_file_op = FileOperator(CONFIG_FILE) + config_data_str = await config_file_op.read() + if not config_data_str: + return True + + config_data = json.loads(config_data_str) + + if len(config_data) <= task_id: + return True + + config_data.pop(task_id) + + return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2)) diff --git a/static/css/style.css b/static/css/style.css index 6472596..8524a6f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -279,6 +279,21 @@ h2 { color: #ff4d4f; } +.criteria { + display: flex; + align-items: center; + justify-content: start; + gap: 10px; +} + +.refresh-criteria { + display: flex; + background: transparent; + border: none; + padding: 0; + cursor: pointer; +} + /* Toggle Switch */ .switch { position: relative; diff --git a/static/js/main.js b/static/js/main.js index 1b9d6b9..1143eb8 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -682,6 +682,8 @@ document.addEventListener('DOMContentLoaded', function() { return '

没有找到任何任务。请点击右上角“创建新任务”来添加一个。

'; } + const refreshBtn = '' + const tableHeader = ` @@ -722,7 +724,7 @@ document.addEventListener('DOMContentLoaded', function() { ${task.min_price || '不限'} - ${task.max_price || '不限'} ${task.personal_only ? '个人闲置' : ''} ${task.max_pages || 3} - ${(task.ai_prompt_criteria_file || 'N/A').replace('prompts/', '')} +
${(task.ai_prompt_criteria_file || 'N/A').replace('prompts/', '')}
${task.cron || '未设置'} ${actionButton} @@ -1306,6 +1308,14 @@ document.addEventListener('DOMContentLoaded', function() { const container = document.getElementById('tasks-table-container'); const tasks = await fetchTasks(); container.innerHTML = renderTasksTable(tasks); + } else if (button.matches('.refresh-criteria')) { + const task = JSON.parse(row.dataset.task); + const modal = document.getElementById('refresh-criteria-modal'); + const textarea = document.getElementById('refresh-criteria-description'); + textarea.value = task['description'] || ''; + modal.dataset.taskId = taskId; + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('visible'), 10); } }); @@ -1398,6 +1408,57 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // --- refresh criteria Modal Logic --- + const refreshCriteriaModal = document.getElementById('refresh-criteria-modal'); + if (refreshCriteriaModal) { + const form = document.getElementById('refresh-criteria-form'); + const closeModalBtn = document.getElementById('close-refresh-criteria-btn'); + const cancelBtn = document.getElementById('cancel-refresh-criteria-btn'); + const refreshBtn = document.getElementById('refresh-criteria-btn'); + + const closeModal = () => { + refreshCriteriaModal.classList.remove('visible'); + setTimeout(() => { + refreshCriteriaModal.style.display = 'none'; + form.reset(); // Reset form on close + }, 300); + }; + + closeModalBtn.addEventListener('click', closeModal); + cancelBtn.addEventListener('click', closeModal); + + let canClose = false; + refreshCriteriaModal.addEventListener('mousedown', event => { + canClose = event.target === refreshCriteriaModal; + }); + refreshCriteriaModal.addEventListener('mouseup', (event) => { + // Close if clicked on the overlay background + if (canClose && event.target === refreshCriteriaModal) { + closeModal(); + } + }); + + refreshBtn.addEventListener('click', async () => { + if (form.checkValidity() === false) { + form.reportValidity(); + return; + } + const btnText = refreshBtn.querySelector('.btn-text'); + const spinner = refreshBtn.querySelector('.spinner'); + btnText.style.display = 'none'; + spinner.style.display = 'inline-block'; + refreshBtn.disabled = true; + + const taskId = refreshCriteriaModal.dataset.taskId + const formData = new FormData(form); + const result = await updateTask(taskId, {description: formData.get('description')}); + if (result && result.task) { + closeModal(); + } + }) + + } + // Initial load refreshLoginStatusWidget(); diff --git a/templates/index.html b/templates/index.html index aadb1ea..eafc11e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -235,6 +235,34 @@ + + + +