feat: KDocs 上传增强 + 离线监控 + Bug修复
KDocs 上传功能增强: - 搜索优化:只用姓名搜索 + C列验证,避免匹配到错误单元格 - 有效行范围:支持配置起始行/结束行,限制上传区域 - 图片覆盖:支持覆盖单元格已有图片(Escape + Delete) - 配置持久化:kdocs_row_start/row_end 保存到数据库(v18迁移) 二次登录功能: - 登录后立即再次登录,让"上次登录时间"显示为刚刚 KDocs 离线监控: - 每5分钟检测金山文档登录状态 - 离线时发送邮件通知管理员(每次掉线只通知一次) - 恢复在线后重置通知状态 Bug 修复: - 任务日志搜索账号关键词报错500:添加异常处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,8 @@ const kdocsSheetName = ref('')
|
||||
const kdocsSheetIndex = ref(0)
|
||||
const kdocsUnitColumn = ref('A')
|
||||
const kdocsImageColumn = ref('D')
|
||||
const kdocsRowStart = ref(0)
|
||||
const kdocsRowEnd = ref(0)
|
||||
const kdocsAdminNotifyEnabled = ref(false)
|
||||
const kdocsAdminNotifyEmail = ref('')
|
||||
const kdocsStatus = ref({})
|
||||
@@ -132,6 +134,8 @@ async function loadAll() {
|
||||
kdocsSheetIndex.value = system.kdocs_sheet_index ?? 0
|
||||
kdocsUnitColumn.value = (system.kdocs_unit_column || 'A').toUpperCase()
|
||||
kdocsImageColumn.value = (system.kdocs_image_column || 'D').toUpperCase()
|
||||
kdocsRowStart.value = system.kdocs_row_start ?? 0
|
||||
kdocsRowEnd.value = system.kdocs_row_end ?? 0
|
||||
kdocsAdminNotifyEnabled.value = (system.kdocs_admin_notify_enabled ?? 0) === 1
|
||||
kdocsAdminNotifyEmail.value = system.kdocs_admin_notify_email || ''
|
||||
kdocsStatus.value = kdocsInfo || {}
|
||||
@@ -253,6 +257,8 @@ async function saveKdocsConfig() {
|
||||
kdocs_sheet_index: Number(kdocsSheetIndex.value) || 0,
|
||||
kdocs_unit_column: kdocsUnitColumn.value.trim().toUpperCase(),
|
||||
kdocs_image_column: kdocsImageColumn.value.trim().toUpperCase(),
|
||||
kdocs_row_start: Number(kdocsRowStart.value) || 0,
|
||||
kdocs_row_end: Number(kdocsRowEnd.value) || 0,
|
||||
kdocs_admin_notify_enabled: kdocsAdminNotifyEnabled.value ? 1 : 0,
|
||||
kdocs_admin_notify_email: kdocsAdminNotifyEmail.value.trim(),
|
||||
}
|
||||
@@ -565,6 +571,15 @@ onMounted(loadAll)
|
||||
<el-input v-model="kdocsImageColumn" placeholder="D" style="max-width: 120px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效行范围">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<el-input-number v-model="kdocsRowStart" :min="0" :max="10000" placeholder="起始行" style="width: 120px" />
|
||||
<span>至</span>
|
||||
<el-input-number v-model="kdocsRowEnd" :min="0" :max="10000" placeholder="结束行" style="width: 120px" />
|
||||
</div>
|
||||
<div class="help">限制上传的行范围(如 50-100),0 表示不限制。用于防止重名导致误传到其他县区。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理员通知">
|
||||
<el-switch v-model="kdocsAdminNotifyEnabled" />
|
||||
</el-form-item>
|
||||
|
||||
3
app.py
3
app.py
@@ -34,7 +34,7 @@ from realtime.status_push import status_push_worker
|
||||
from routes import register_blueprints
|
||||
from security import init_security_middleware
|
||||
from services.checkpoints import init_checkpoint_manager
|
||||
from services.maintenance import start_cleanup_scheduler
|
||||
from services.maintenance import start_cleanup_scheduler, start_kdocs_monitor
|
||||
from services.models import User
|
||||
from services.runtime import init_runtime
|
||||
from services.scheduler import scheduled_task_worker
|
||||
@@ -267,6 +267,7 @@ if __name__ == "__main__":
|
||||
logger.warning(f"警告: 邮件服务初始化失败: {e}")
|
||||
|
||||
start_cleanup_scheduler()
|
||||
start_kdocs_monitor()
|
||||
|
||||
try:
|
||||
system_config = database.get_system_config() or {}
|
||||
|
||||
12
db/admin.py
12
db/admin.py
@@ -181,6 +181,8 @@ def get_system_config_raw() -> dict:
|
||||
"kdocs_image_column": "D",
|
||||
"kdocs_admin_notify_enabled": 0,
|
||||
"kdocs_admin_notify_email": "",
|
||||
"kdocs_row_start": 0,
|
||||
"kdocs_row_end": 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +211,8 @@ def update_system_config(
|
||||
kdocs_image_column=None,
|
||||
kdocs_admin_notify_enabled=None,
|
||||
kdocs_admin_notify_email=None,
|
||||
kdocs_row_start=None,
|
||||
kdocs_row_end=None,
|
||||
) -> bool:
|
||||
"""更新系统配置(仅更新DB,不做缓存处理)。"""
|
||||
allowed_fields = {
|
||||
@@ -235,6 +239,8 @@ def update_system_config(
|
||||
"kdocs_image_column",
|
||||
"kdocs_admin_notify_enabled",
|
||||
"kdocs_admin_notify_email",
|
||||
"kdocs_row_start",
|
||||
"kdocs_row_end",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
@@ -312,6 +318,12 @@ def update_system_config(
|
||||
if kdocs_admin_notify_email is not None:
|
||||
updates.append("kdocs_admin_notify_email = ?")
|
||||
params.append(kdocs_admin_notify_email)
|
||||
if kdocs_row_start is not None:
|
||||
updates.append("kdocs_row_start = ?")
|
||||
params.append(kdocs_row_start)
|
||||
if kdocs_row_end is not None:
|
||||
updates.append("kdocs_row_end = ?")
|
||||
params.append(kdocs_row_end)
|
||||
|
||||
if not updates:
|
||||
return False
|
||||
|
||||
@@ -87,6 +87,9 @@ def migrate_database(conn, target_version: int) -> None:
|
||||
if current_version < 17:
|
||||
_migrate_to_v17(conn)
|
||||
current_version = 17
|
||||
if current_version < 18:
|
||||
_migrate_to_v18(conn)
|
||||
current_version = 18
|
||||
|
||||
if current_version != int(target_version):
|
||||
set_current_version(conn, int(target_version))
|
||||
@@ -728,3 +731,21 @@ def _migrate_to_v17(conn):
|
||||
print(f" ✓ 添加 users.{field} 字段")
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _migrate_to_v18(conn):
|
||||
"""迁移到版本18 - 金山文档上传:有效行范围配置"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("PRAGMA table_info(system_config)")
|
||||
columns = [col[1] for col in cursor.fetchall()]
|
||||
|
||||
if "kdocs_row_start" not in columns:
|
||||
cursor.execute("ALTER TABLE system_config ADD COLUMN kdocs_row_start INTEGER DEFAULT 0")
|
||||
print(" ✓ 添加 system_config.kdocs_row_start 字段")
|
||||
|
||||
if "kdocs_row_end" not in columns:
|
||||
cursor.execute("ALTER TABLE system_config ADD COLUMN kdocs_row_end INTEGER DEFAULT 0")
|
||||
print(" ✓ 添加 system_config.kdocs_row_end 字段")
|
||||
|
||||
conn.commit()
|
||||
|
||||
@@ -224,6 +224,8 @@ def ensure_schema(conn) -> None:
|
||||
kdocs_image_column TEXT DEFAULT 'D',
|
||||
kdocs_admin_notify_enabled INTEGER DEFAULT 0,
|
||||
kdocs_admin_notify_email TEXT DEFAULT '',
|
||||
kdocs_row_start INTEGER DEFAULT 0,
|
||||
kdocs_row_end INTEGER DEFAULT 0,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -680,6 +680,8 @@ def update_system_config_api():
|
||||
kdocs_image_column = data.get("kdocs_image_column")
|
||||
kdocs_admin_notify_enabled = data.get("kdocs_admin_notify_enabled")
|
||||
kdocs_admin_notify_email = data.get("kdocs_admin_notify_email")
|
||||
kdocs_row_start = data.get("kdocs_row_start")
|
||||
kdocs_row_end = data.get("kdocs_row_end")
|
||||
|
||||
if max_concurrent is not None:
|
||||
if not isinstance(max_concurrent, int) or max_concurrent < 1:
|
||||
@@ -787,6 +789,22 @@ def update_system_config_api():
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
if kdocs_row_start is not None:
|
||||
try:
|
||||
kdocs_row_start = int(kdocs_row_start)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "起始行必须是数字"}), 400
|
||||
if kdocs_row_start < 0:
|
||||
return jsonify({"error": "起始行不能为负数"}), 400
|
||||
|
||||
if kdocs_row_end is not None:
|
||||
try:
|
||||
kdocs_row_end = int(kdocs_row_end)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "结束行必须是数字"}), 400
|
||||
if kdocs_row_end < 0:
|
||||
return jsonify({"error": "结束行不能为负数"}), 400
|
||||
|
||||
old_config = database.get_system_config() or {}
|
||||
|
||||
if not database.update_system_config(
|
||||
@@ -810,6 +828,8 @@ def update_system_config_api():
|
||||
kdocs_image_column=kdocs_image_column,
|
||||
kdocs_admin_notify_enabled=kdocs_admin_notify_enabled,
|
||||
kdocs_admin_notify_email=kdocs_admin_notify_email,
|
||||
kdocs_row_start=kdocs_row_start,
|
||||
kdocs_row_end=kdocs_row_end,
|
||||
):
|
||||
return jsonify({"error": "更新失败"}), 400
|
||||
|
||||
@@ -1091,30 +1111,44 @@ def get_running_tasks_api():
|
||||
@admin_required
|
||||
def get_task_logs_api():
|
||||
"""获取任务日志列表(支持分页和多种筛选)"""
|
||||
limit = int(request.args.get("limit", 20))
|
||||
offset = int(request.args.get("offset", 0))
|
||||
try:
|
||||
limit = int(request.args.get("limit", 20))
|
||||
limit = max(1, min(limit, 200)) # 限制 1-200 条
|
||||
except (ValueError, TypeError):
|
||||
limit = 20
|
||||
|
||||
try:
|
||||
offset = int(request.args.get("offset", 0))
|
||||
offset = max(0, offset)
|
||||
except (ValueError, TypeError):
|
||||
offset = 0
|
||||
|
||||
date_filter = request.args.get("date")
|
||||
status_filter = request.args.get("status")
|
||||
source_filter = request.args.get("source")
|
||||
user_id_filter = request.args.get("user_id")
|
||||
account_filter = request.args.get("account")
|
||||
account_filter = (request.args.get("account") or "").strip()
|
||||
|
||||
if user_id_filter:
|
||||
try:
|
||||
user_id_filter = int(user_id_filter)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
user_id_filter = None
|
||||
|
||||
result = database.get_task_logs(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
date_filter=date_filter,
|
||||
status_filter=status_filter,
|
||||
source_filter=source_filter,
|
||||
user_id_filter=user_id_filter,
|
||||
account_filter=account_filter,
|
||||
)
|
||||
return jsonify(result)
|
||||
try:
|
||||
result = database.get_task_logs(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
date_filter=date_filter,
|
||||
status_filter=status_filter,
|
||||
source_filter=source_filter,
|
||||
user_id_filter=user_id_filter,
|
||||
account_filter=account_filter if account_filter else None,
|
||||
)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"获取任务日志失败: {e}")
|
||||
return jsonify({"logs": [], "total": 0, "error": "查询失败"})
|
||||
|
||||
|
||||
@admin_api_bp.route("/task/logs/clear", methods=["POST"])
|
||||
|
||||
@@ -860,6 +860,8 @@ class KDocsUploader:
|
||||
sheet_index = int(cfg.get("kdocs_sheet_index") or 0)
|
||||
unit_col = (cfg.get("kdocs_unit_column") or "A").strip().upper()
|
||||
image_col = (cfg.get("kdocs_image_column") or "D").strip().upper()
|
||||
row_start = int(cfg.get("kdocs_row_start") or 0)
|
||||
row_end = int(cfg.get("kdocs_row_end") or 0)
|
||||
|
||||
success = False
|
||||
error_msg = ""
|
||||
@@ -867,7 +869,7 @@ class KDocsUploader:
|
||||
try:
|
||||
if sheet_name or sheet_index:
|
||||
self._select_sheet(sheet_name, sheet_index)
|
||||
row_num = self._find_person_with_unit(unit, name, unit_col)
|
||||
row_num = self._find_person_with_unit(unit, name, unit_col, row_start=row_start, row_end=row_end)
|
||||
if row_num < 0:
|
||||
error_msg = f"未找到人员: {unit}-{name}"
|
||||
break
|
||||
@@ -975,12 +977,35 @@ class KDocsUploader:
|
||||
continue
|
||||
|
||||
def _get_current_cell_address(self) -> str:
|
||||
try:
|
||||
name_box = self._page.locator("input.edit-box").first
|
||||
return name_box.input_value()
|
||||
except Exception:
|
||||
name_box = self._page.locator('#root input[type="text"]').first
|
||||
return name_box.input_value()
|
||||
"""获取当前选中的单元格地址(如 A1, C66 等)"""
|
||||
import re
|
||||
# 等待一小段时间让名称框稳定
|
||||
time.sleep(0.1)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
name_box = self._page.locator("input.edit-box").first
|
||||
value = name_box.input_value()
|
||||
# 验证是否为有效的单元格地址格式(如 A1, C66, AA100 等)
|
||||
if value and re.match(r"^[A-Z]+\d+$", value.upper()):
|
||||
return value.upper()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
name_box = self._page.locator('#root input[type="text"]').first
|
||||
value = name_box.input_value()
|
||||
if value and re.match(r"^[A-Z]+\d+$", value.upper()):
|
||||
return value.upper()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 等待一下再重试
|
||||
time.sleep(0.2)
|
||||
|
||||
# 如果无法获取有效地址,返回空字符串
|
||||
logger.warning("[KDocs调试] 无法获取有效的单元格地址")
|
||||
return ""
|
||||
|
||||
def _navigate_to_cell(self, cell_address: str) -> None:
|
||||
try:
|
||||
@@ -1033,6 +1058,48 @@ class KDocsUploader:
|
||||
except Exception:
|
||||
pass
|
||||
self._focus_grid()
|
||||
|
||||
# 尝试方法1: 读取金山文档编辑栏/公式栏的内容
|
||||
try:
|
||||
# 金山文档的编辑栏选择器(可能需要调整)
|
||||
formula_bar_selectors = [
|
||||
".formula-bar-input",
|
||||
".cell-editor-input",
|
||||
"[class*='formulaBar'] input",
|
||||
"[class*='formula'] textarea",
|
||||
".formula-editor",
|
||||
"#formulaInput",
|
||||
]
|
||||
for selector in formula_bar_selectors:
|
||||
try:
|
||||
el = self._page.query_selector(selector)
|
||||
if el:
|
||||
value = el.input_value() if hasattr(el, 'input_value') else el.inner_text()
|
||||
if value and not value.startswith("=DISPIMG"):
|
||||
logger.info(f"[KDocs调试] 从编辑栏读取到: '{value[:50]}...' (selector={selector})")
|
||||
return value.strip()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 尝试方法2: F2进入编辑模式,全选复制
|
||||
try:
|
||||
self._page.keyboard.press("F2")
|
||||
time.sleep(0.2)
|
||||
self._page.keyboard.press("Control+a")
|
||||
time.sleep(0.1)
|
||||
self._page.keyboard.press("Control+c")
|
||||
time.sleep(0.2)
|
||||
self._page.keyboard.press("Escape")
|
||||
time.sleep(0.1)
|
||||
value = self._read_clipboard_text()
|
||||
if value and not value.startswith("=DISPIMG"):
|
||||
return value.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 尝试方法3: 直接复制单元格(备选)
|
||||
try:
|
||||
self._page.keyboard.press("Control+c")
|
||||
time.sleep(0.2)
|
||||
@@ -1141,38 +1208,225 @@ class KDocsUploader:
|
||||
def _extract_row_number(self, cell_address: str) -> int:
|
||||
import re
|
||||
|
||||
match = re.search(r"(\\d+)$", cell_address)
|
||||
match = re.search(r"(\d+)$", cell_address)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return -1
|
||||
|
||||
def _verify_unit_by_navigation(self, row_num: int, unit: str, unit_col: str) -> bool:
|
||||
cell_address = f"{unit_col}{row_num}"
|
||||
cell_value = self._get_cell_value(cell_address)
|
||||
if cell_value:
|
||||
return self._unit_matches(cell_value, unit)
|
||||
return False
|
||||
"""验证县区 - 从目标行开始搜索县区"""
|
||||
logger.info(f"[KDocs调试] 验证县区: 期望行={row_num}, 期望值='{unit}'")
|
||||
|
||||
def _find_person_with_unit(self, unit: str, name: str, unit_col: str, max_attempts: int = 50) -> int:
|
||||
self._search_person(name)
|
||||
found_rows = set()
|
||||
for _ in range(max_attempts):
|
||||
self._close_search()
|
||||
current_address = self._get_current_cell_address()
|
||||
row_num = self._extract_row_number(current_address)
|
||||
if row_num == -1:
|
||||
return -1
|
||||
if row_num in found_rows:
|
||||
return -1
|
||||
found_rows.add(row_num)
|
||||
|
||||
if self._verify_unit_by_navigation(row_num, unit, unit_col):
|
||||
return row_num
|
||||
# 方法: 先导航到目标行的A列,然后从那里搜索县区
|
||||
try:
|
||||
# 1. 先导航到目标行的 A 列
|
||||
start_cell = f"{unit_col}{row_num}"
|
||||
self._navigate_to_cell(start_cell)
|
||||
time.sleep(0.3)
|
||||
logger.info(f"[KDocs调试] 已导航到 {start_cell}")
|
||||
|
||||
# 2. 从当前位置搜索县区
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
time.sleep(0.3)
|
||||
|
||||
# 找到搜索框并输入
|
||||
try:
|
||||
search_input = self._page.locator("input[placeholder*='查找'], input[placeholder*='搜索'], input[type='text']").first
|
||||
search_input.fill(unit)
|
||||
time.sleep(0.2)
|
||||
self._page.keyboard.press("Enter")
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.warning(f"[KDocs调试] 填写搜索框失败: {e}")
|
||||
self._page.keyboard.press("Escape")
|
||||
return False
|
||||
|
||||
# 3. 关闭搜索框,检查当前位置
|
||||
self._page.keyboard.press("Escape")
|
||||
time.sleep(0.3)
|
||||
|
||||
current_address = self._get_current_cell_address()
|
||||
found_row = self._extract_row_number(current_address)
|
||||
logger.info(f"[KDocs调试] 搜索'{unit}'后: 当前单元格={current_address}, 行号={found_row}")
|
||||
|
||||
# 4. 检查是否在同一行(允许在目标行或之后的几行内,因为搜索可能从当前位置向下)
|
||||
if found_row == row_num:
|
||||
logger.info(f"[KDocs调试] ✓ 验证成功! 县区'{unit}'在第{row_num}行")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"[KDocs调试] 验证失败: 期望行{row_num}, 实际找到行{found_row}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[KDocs调试] 验证异常: {e}")
|
||||
return False
|
||||
|
||||
def _debug_dump_page_elements(self) -> None:
|
||||
"""调试: 输出页面上可能包含单元格值的元素"""
|
||||
logger.info("[KDocs调试] ========== 页面元素分析 ==========")
|
||||
try:
|
||||
# 查找可能的编辑栏元素
|
||||
selectors_to_check = [
|
||||
"input", "textarea",
|
||||
"[class*='formula']", "[class*='Formula']",
|
||||
"[class*='editor']", "[class*='Editor']",
|
||||
"[class*='cell']", "[class*='Cell']",
|
||||
"[class*='input']", "[class*='Input']",
|
||||
]
|
||||
for selector in selectors_to_check:
|
||||
try:
|
||||
elements = self._page.query_selector_all(selector)
|
||||
for i, el in enumerate(elements[:3]): # 只看前3个
|
||||
try:
|
||||
class_name = el.get_attribute("class") or ""
|
||||
value = ""
|
||||
try:
|
||||
value = el.input_value()
|
||||
except:
|
||||
try:
|
||||
value = el.inner_text()
|
||||
except:
|
||||
pass
|
||||
if value:
|
||||
logger.info(f"[KDocs调试] 元素 {selector}[{i}] class='{class_name[:50]}' value='{value[:30]}'")
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"[KDocs调试] 页面元素分析失败: {e}")
|
||||
logger.info("[KDocs调试] ====================================")
|
||||
|
||||
def _debug_dump_table_structure(self, target_row: int = 66) -> None:
|
||||
"""调试: 输出表格结构"""
|
||||
self._debug_dump_page_elements() # 先分析页面元素
|
||||
logger.info("[KDocs调试] ========== 表格结构分析 ==========")
|
||||
cols = ['A', 'B', 'C', 'D', 'E']
|
||||
for row in [1, 2, 3, target_row]:
|
||||
row_data = []
|
||||
for col in cols:
|
||||
val = self._get_cell_value(f"{col}{row}")
|
||||
# 截断太长的值
|
||||
if len(val) > 30:
|
||||
val = val[:30] + "..."
|
||||
row_data.append(f"{col}{row}='{val}'")
|
||||
logger.info(f"[KDocs调试] 第{row}行: {' | '.join(row_data)}")
|
||||
logger.info("[KDocs调试] ====================================")
|
||||
|
||||
def _find_person_with_unit(self, unit: str, name: str, unit_col: str, max_attempts: int = 50,
|
||||
row_start: int = 0, row_end: int = 0) -> int:
|
||||
"""
|
||||
查找人员所在行号。
|
||||
策略:只搜索姓名,找到姓名列(C列)的匹配项
|
||||
注意:组合搜索会匹配到图片列的错误位置,已放弃该方案
|
||||
|
||||
:param row_start: 有效行范围起始(0表示不限制)
|
||||
:param row_end: 有效行范围结束(0表示不限制)
|
||||
"""
|
||||
logger.info(f"[KDocs调试] 开始搜索人员: name='{name}', unit='{unit}'")
|
||||
if row_start > 0 or row_end > 0:
|
||||
logger.info(f"[KDocs调试] 有效行范围: {row_start}-{row_end}")
|
||||
|
||||
# 只搜索姓名 - 这是目前唯一可靠的方式
|
||||
logger.info(f"[KDocs调试] 搜索姓名: '{name}'")
|
||||
row_num = self._search_and_get_row(name, max_attempts=max_attempts, expected_col='C',
|
||||
row_start=row_start, row_end=row_end)
|
||||
if row_num > 0:
|
||||
logger.info(f"[KDocs调试] ✓ 姓名搜索成功! 找到行号={row_num}")
|
||||
return row_num
|
||||
|
||||
logger.warning(f"[KDocs调试] 搜索失败,未找到人员 '{name}'")
|
||||
return -1
|
||||
|
||||
def _search_and_get_row(self, search_text: str, max_attempts: int = 10, expected_col: str = None,
|
||||
row_start: int = 0, row_end: int = 0) -> int:
|
||||
"""
|
||||
执行搜索并获取找到的行号
|
||||
:param search_text: 要搜索的文本
|
||||
:param max_attempts: 最大尝试次数
|
||||
:param expected_col: 期望的列(如 'C'),如果指定则只接受该列的结果
|
||||
:param row_start: 有效行范围起始(0表示不限制)
|
||||
:param row_end: 有效行范围结束(0表示不限制)
|
||||
"""
|
||||
self._focus_grid()
|
||||
self._search_person(search_text)
|
||||
found_positions = set() # 记录已找到的位置(列+行)
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
self._close_search()
|
||||
time.sleep(0.3) # 等待名称框更新
|
||||
|
||||
current_address = self._get_current_cell_address()
|
||||
if not current_address:
|
||||
logger.warning(f"[KDocs调试] 第{attempt+1}次: 无法获取单元格地址")
|
||||
# 继续尝试下一个
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
continue
|
||||
|
||||
row_num = self._extract_row_number(current_address)
|
||||
# 提取列字母(A, B, C, D 等)
|
||||
col_letter = ''.join(c for c in current_address if c.isalpha()).upper()
|
||||
|
||||
logger.info(f"[KDocs调试] 第{attempt+1}次搜索'{search_text}': 单元格={current_address}, 列={col_letter}, 行号={row_num}")
|
||||
|
||||
if row_num <= 0:
|
||||
logger.warning(f"[KDocs调试] 无法提取行号,搜索可能没有结果")
|
||||
return -1
|
||||
|
||||
# 检查是否已经访问过这个位置
|
||||
position_key = f"{col_letter}{row_num}"
|
||||
if position_key in found_positions:
|
||||
logger.info(f"[KDocs调试] 位置{position_key}已搜索过,循环结束")
|
||||
# 检查是否有任何有效结果
|
||||
valid_results = [pos for pos in found_positions
|
||||
if (not expected_col or pos.startswith(expected_col))
|
||||
and self._extract_row_number(pos) > 2]
|
||||
if valid_results:
|
||||
# 返回第一个有效结果的行号
|
||||
return self._extract_row_number(valid_results[0])
|
||||
return -1
|
||||
|
||||
found_positions.add(position_key)
|
||||
|
||||
# 跳过标题行和表头行(通常是第1-2行)
|
||||
if row_num <= 2:
|
||||
logger.info(f"[KDocs调试] 跳过标题/表头行: {row_num}")
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
continue
|
||||
|
||||
# 如果指定了期望的列,检查是否匹配
|
||||
if expected_col and col_letter != expected_col.upper():
|
||||
logger.info(f"[KDocs调试] 列不匹配: 期望={expected_col}, 实际={col_letter},继续搜索下一个")
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
continue
|
||||
|
||||
# 检查行号是否在有效范围内
|
||||
if row_start > 0 and row_num < row_start:
|
||||
logger.info(f"[KDocs调试] 行号{row_num}小于起始行{row_start},继续搜索下一个")
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
continue
|
||||
|
||||
if row_end > 0 and row_num > row_end:
|
||||
logger.info(f"[KDocs调试] 行号{row_num}大于结束行{row_end},继续搜索下一个")
|
||||
self._page.keyboard.press("Control+f")
|
||||
time.sleep(0.2)
|
||||
self._find_next()
|
||||
continue
|
||||
|
||||
# 找到有效的数据行,列匹配且在行范围内
|
||||
logger.info(f"[KDocs调试] ✓ 找到有效位置: {current_address} (在有效范围内)")
|
||||
return row_num
|
||||
|
||||
self._close_search()
|
||||
logger.warning(f"[KDocs调试] 达到最大尝试次数{max_attempts},未找到有效结果")
|
||||
return -1
|
||||
|
||||
def _upload_image_to_cell(self, row_num: int, image_path: str, image_col: str) -> bool:
|
||||
@@ -1180,12 +1434,24 @@ class KDocsUploader:
|
||||
self._navigate_to_cell(cell_address)
|
||||
time.sleep(0.3)
|
||||
|
||||
# 清除单元格现有内容
|
||||
try:
|
||||
# 1. 导航到单元格(名称框输入地址+Enter,会跳转并可能进入编辑模式)
|
||||
self._navigate_to_cell(cell_address)
|
||||
time.sleep(0.3)
|
||||
|
||||
# 2. 按 Escape 退出可能的编辑模式,回到选中状态
|
||||
self._page.keyboard.press("Escape")
|
||||
time.sleep(0.3)
|
||||
|
||||
# 3. 按 Delete 删除选中单元格的内容
|
||||
self._page.keyboard.press("Delete")
|
||||
time.sleep(0.1)
|
||||
self._page.keyboard.press("Backspace")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
logger.info(f"[KDocs] 已删除 {cell_address} 的内容")
|
||||
except Exception as e:
|
||||
logger.warning(f"[KDocs] 清除单元格内容时出错: {e}")
|
||||
|
||||
logger.info(f"[KDocs] 准备上传图片到 {cell_address},已清除旧内容")
|
||||
|
||||
try:
|
||||
insert_btn = self._page.get_by_role("button", name="插入")
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from app_config import get_config
|
||||
from app_logger import get_logger
|
||||
@@ -26,6 +27,9 @@ USER_ACCOUNTS_EXPIRE_SECONDS = int(getattr(config, "USER_ACCOUNTS_EXPIRE_SECONDS
|
||||
BATCH_TASK_EXPIRE_SECONDS = int(getattr(config, "BATCH_TASK_EXPIRE_SECONDS", 21600))
|
||||
PENDING_RANDOM_EXPIRE_SECONDS = int(getattr(config, "PENDING_RANDOM_EXPIRE_SECONDS", 7200))
|
||||
|
||||
# 金山文档离线通知状态:每次掉线只通知一次,恢复在线后重置
|
||||
_kdocs_offline_notified: bool = False
|
||||
|
||||
|
||||
def cleanup_expired_data() -> None:
|
||||
"""定期清理过期数据,防止内存泄漏(逻辑保持不变)。"""
|
||||
@@ -91,6 +95,87 @@ def cleanup_expired_data() -> None:
|
||||
logger.debug(f"已清理 {deleted_random} 个过期随机延迟任务")
|
||||
|
||||
|
||||
def check_kdocs_online_status() -> None:
|
||||
"""检测金山文档登录状态,如果离线则发送邮件通知管理员(每次掉线只通知一次)"""
|
||||
global _kdocs_offline_notified
|
||||
|
||||
try:
|
||||
import database
|
||||
from services.kdocs_uploader import get_kdocs_uploader
|
||||
|
||||
# 获取系统配置
|
||||
cfg = database.get_system_config()
|
||||
if not cfg:
|
||||
return
|
||||
|
||||
# 检查是否启用了金山文档功能
|
||||
kdocs_enabled = int(cfg.get("kdocs_enabled") or 0)
|
||||
if not kdocs_enabled:
|
||||
return
|
||||
|
||||
# 检查是否启用了管理员通知
|
||||
admin_notify_enabled = int(cfg.get("kdocs_admin_notify_enabled") or 0)
|
||||
admin_notify_email = (cfg.get("kdocs_admin_notify_email") or "").strip()
|
||||
if not admin_notify_enabled or not admin_notify_email:
|
||||
return
|
||||
|
||||
# 获取金山文档状态
|
||||
kdocs = get_kdocs_uploader()
|
||||
status = kdocs.get_status()
|
||||
login_required = status.get("login_required", False)
|
||||
last_login_ok = status.get("last_login_ok")
|
||||
|
||||
# 如果需要登录或最后登录状态不是成功
|
||||
is_offline = login_required or (last_login_ok is False)
|
||||
|
||||
if is_offline:
|
||||
# 已经通知过了,不再重复通知
|
||||
if _kdocs_offline_notified:
|
||||
logger.debug("[KDocs监控] 金山文档离线,已通知过,跳过重复通知")
|
||||
return
|
||||
|
||||
# 发送邮件通知
|
||||
try:
|
||||
import email_service
|
||||
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
subject = "【金山文档离线告警】需要重新登录"
|
||||
body = f"""
|
||||
您好,
|
||||
|
||||
系统检测到金山文档上传功能已离线,需要重新扫码登录。
|
||||
|
||||
检测时间:{now_str}
|
||||
状态详情:
|
||||
- 需要登录:{login_required}
|
||||
- 上次登录状态:{last_login_ok}
|
||||
|
||||
请尽快登录后台,在"系统配置"→"金山文档上传"中点击"获取登录二维码"重新登录。
|
||||
|
||||
---
|
||||
此邮件由系统自动发送,请勿直接回复。
|
||||
"""
|
||||
email_service.send_email_async(
|
||||
to_email=admin_notify_email,
|
||||
subject=subject,
|
||||
body=body,
|
||||
email_type="kdocs_offline_alert",
|
||||
)
|
||||
_kdocs_offline_notified = True # 标记为已通知
|
||||
logger.warning(f"[KDocs监控] 金山文档离线,已发送通知邮件到 {admin_notify_email}")
|
||||
except Exception as e:
|
||||
logger.error(f"[KDocs监控] 发送离线通知邮件失败: {e}")
|
||||
else:
|
||||
# 恢复在线,重置通知状态
|
||||
if _kdocs_offline_notified:
|
||||
logger.info("[KDocs监控] 金山文档已恢复在线,重置通知状态")
|
||||
_kdocs_offline_notified = False
|
||||
logger.debug("[KDocs监控] 金山文档状态正常")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KDocs监控] 检测失败: {e}")
|
||||
|
||||
|
||||
def start_cleanup_scheduler() -> None:
|
||||
"""启动定期清理调度器"""
|
||||
|
||||
@@ -106,3 +191,22 @@ def start_cleanup_scheduler() -> None:
|
||||
cleanup_thread.start()
|
||||
logger.info("内存清理调度器已启动")
|
||||
|
||||
|
||||
def start_kdocs_monitor() -> None:
|
||||
"""启动金山文档状态监控"""
|
||||
|
||||
def monitor_loop():
|
||||
# 启动后等待 60 秒再开始检测(给系统初始化的时间)
|
||||
time.sleep(60)
|
||||
while True:
|
||||
try:
|
||||
check_kdocs_online_status()
|
||||
time.sleep(300) # 每5分钟检测一次
|
||||
except Exception as e:
|
||||
logger.error(f"[KDocs监控] 监控任务执行失败: {e}")
|
||||
time.sleep(60)
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_loop, daemon=True, name="kdocs-monitor")
|
||||
monitor_thread.start()
|
||||
logger.info("[KDocs监控] 金山文档状态监控已启动(每5分钟检测一次)")
|
||||
|
||||
|
||||
@@ -573,7 +573,15 @@ def run_task(user_id, account_id, browse_type, enable_screenshot=True, source="m
|
||||
|
||||
with APIBrowser(log_callback=custom_log, proxy_config=proxy_config) as api_browser:
|
||||
if api_browser.login(account.username, account.password):
|
||||
log_to_client("✓ 登录成功!", user_id, account_id)
|
||||
log_to_client("✓ 首次登录成功,刷新登录时间...", user_id, account_id)
|
||||
|
||||
# 二次登录:让"上次登录时间"变成刚才首次登录的时间
|
||||
# 这样截图时显示的"上次登录时间"就是几秒前而不是昨天
|
||||
if api_browser.login(account.username, account.password):
|
||||
log_to_client("✓ 二次登录成功!", user_id, account_id)
|
||||
else:
|
||||
log_to_client("⚠ 二次登录失败,继续使用首次登录状态", user_id, account_id)
|
||||
|
||||
api_browser.save_cookies_for_screenshot(account.username)
|
||||
database.reset_account_login_status(account_id)
|
||||
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"_email-BBYDqLKN.js": {
|
||||
"file": "assets/email-BBYDqLKN.js",
|
||||
"_email-C4xyG93p.js": {
|
||||
"file": "assets/email-C4xyG93p.js",
|
||||
"name": "email",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_system-BDTy0cf_.js": {
|
||||
"file": "assets/system-BDTy0cf_.js",
|
||||
"_system-C6kBIFhi.js": {
|
||||
"file": "assets/system-C6kBIFhi.js",
|
||||
"name": "system",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_tasks-CeZTKYlS.js": {
|
||||
"file": "assets/tasks-CeZTKYlS.js",
|
||||
"_tasks-dxahzB_w.js": {
|
||||
"file": "assets/tasks-dxahzB_w.js",
|
||||
"name": "tasks",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"_users-CWMQV7em.js": {
|
||||
"file": "assets/users-CWMQV7em.js",
|
||||
"_users-ecMaaAFD.js": {
|
||||
"file": "assets/users-ecMaaAFD.js",
|
||||
"name": "users",
|
||||
"imports": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-BDEpmftQ.js",
|
||||
"file": "assets/index-DKH_HvPt.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
@@ -44,11 +44,11 @@
|
||||
"src/pages/SettingsPage.vue"
|
||||
],
|
||||
"css": [
|
||||
"assets/index-DxTKnDeo.css"
|
||||
"assets/index-_5Ec1Hmd.css"
|
||||
]
|
||||
},
|
||||
"src/pages/AnnouncementsPage.vue": {
|
||||
"file": "assets/AnnouncementsPage-D9Qeb7XP.js",
|
||||
"file": "assets/AnnouncementsPage-kpoSCxEP.js",
|
||||
"name": "AnnouncementsPage",
|
||||
"src": "src/pages/AnnouncementsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -60,12 +60,12 @@
|
||||
]
|
||||
},
|
||||
"src/pages/EmailPage.vue": {
|
||||
"file": "assets/EmailPage-wPg_LfpQ.js",
|
||||
"file": "assets/EmailPage-CEtsoP5P.js",
|
||||
"name": "EmailPage",
|
||||
"src": "src/pages/EmailPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_email-BBYDqLKN.js",
|
||||
"_email-C4xyG93p.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -73,7 +73,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/FeedbacksPage.vue": {
|
||||
"file": "assets/FeedbacksPage-DZkgKcO5.js",
|
||||
"file": "assets/FeedbacksPage-ByHln3Ce.js",
|
||||
"name": "FeedbacksPage",
|
||||
"src": "src/pages/FeedbacksPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -85,13 +85,13 @@
|
||||
]
|
||||
},
|
||||
"src/pages/LogsPage.vue": {
|
||||
"file": "assets/LogsPage-CjnNneeo.js",
|
||||
"file": "assets/LogsPage-vZFAwgb-.js",
|
||||
"name": "LogsPage",
|
||||
"src": "src/pages/LogsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_users-CWMQV7em.js",
|
||||
"_tasks-CeZTKYlS.js",
|
||||
"_users-ecMaaAFD.js",
|
||||
"_tasks-dxahzB_w.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
@@ -99,22 +99,22 @@
|
||||
]
|
||||
},
|
||||
"src/pages/ReportPage.vue": {
|
||||
"file": "assets/ReportPage-CuHgaMnw.js",
|
||||
"file": "assets/ReportPage--ClMBhif.js",
|
||||
"name": "ReportPage",
|
||||
"src": "src/pages/ReportPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html",
|
||||
"_email-BBYDqLKN.js",
|
||||
"_tasks-CeZTKYlS.js",
|
||||
"_system-BDTy0cf_.js"
|
||||
"_email-C4xyG93p.js",
|
||||
"_tasks-dxahzB_w.js",
|
||||
"_system-C6kBIFhi.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/ReportPage-Q8rCsG8A.css"
|
||||
]
|
||||
},
|
||||
"src/pages/SecurityPage.vue": {
|
||||
"file": "assets/SecurityPage-hp7dnVAc.js",
|
||||
"file": "assets/SecurityPage-DBhX0IuO.js",
|
||||
"name": "SecurityPage",
|
||||
"src": "src/pages/SecurityPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -126,7 +126,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/SettingsPage.vue": {
|
||||
"file": "assets/SettingsPage-DfeoFZRa.js",
|
||||
"file": "assets/SettingsPage-D91FOriC.js",
|
||||
"name": "SettingsPage",
|
||||
"src": "src/pages/SettingsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -138,25 +138,25 @@
|
||||
]
|
||||
},
|
||||
"src/pages/SystemPage.vue": {
|
||||
"file": "assets/SystemPage-CMSxrPFY.js",
|
||||
"file": "assets/SystemPage-DVj-4Lnp.js",
|
||||
"name": "SystemPage",
|
||||
"src": "src/pages/SystemPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_system-BDTy0cf_.js",
|
||||
"_system-C6kBIFhi.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
"assets/SystemPage-DHDS5_BP.css"
|
||||
"assets/SystemPage-C8GQyKcD.css"
|
||||
]
|
||||
},
|
||||
"src/pages/UsersPage.vue": {
|
||||
"file": "assets/UsersPage-CPdD81iA.js",
|
||||
"file": "assets/UsersPage-C_vL5-r3.js",
|
||||
"name": "UsersPage",
|
||||
"src": "src/pages/UsersPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"_users-CWMQV7em.js",
|
||||
"_users-ecMaaAFD.js",
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{a as m,_ as B,r as p,f as u,g as T,h as P,j as r,m as a,w as l,q as x,L as i,K as b}from"./index-BDEpmftQ.js";async function C(o){const{data:s}=await m.put("/admin/username",{new_username:o});return s}async function S(o){const{data:s}=await m.put("/admin/password",{new_password:o});return s}async function U(){const{data:o}=await m.post("/logout");return o}const A={class:"page-stack"},E={__name:"SettingsPage",setup(o){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function h(){const t=d.value;if(!t){i.error("请输入新密码");return}const e=k(t);if(!e.ok){i.error(e.message);return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await S(t),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=u("el-input"),w=u("el-form-item"),v=u("el-form"),y=u("el-button"),_=u("el-card");return P(),T("div",A,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新用户名"},{default:l(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:l(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新密码"},{default:l(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:h},{default:l(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},M=B(E,[["__scopeId","data-v-12a26d11"]]);export{M as default};
|
||||
import{a as m,_ as B,r as p,f as u,g as T,h as P,j as r,m as a,w as l,q as x,L as i,K as b}from"./index-DKH_HvPt.js";async function C(o){const{data:s}=await m.put("/admin/username",{new_username:o});return s}async function S(o){const{data:s}=await m.put("/admin/password",{new_password:o});return s}async function U(){const{data:o}=await m.post("/logout");return o}const A={class:"page-stack"},E={__name:"SettingsPage",setup(o){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function h(){const t=d.value;if(!t){i.error("请输入新密码");return}const e=k(t);if(!e.ok){i.error(e.message);return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await S(t),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=u("el-input"),w=u("el-form-item"),v=u("el-form"),y=u("el-button"),_=u("el-card");return P(),T("div",A,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新用户名"},{default:l(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:l(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:l(()=>[a(w,{label:"新密码"},{default:l(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:h},{default:l(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},M=B(E,[["__scopeId","data-v-12a26d11"]]);export{M as default};
|
||||
1
static/admin/assets/SystemPage-C8GQyKcD.css
Normal file
1
static/admin/assets/SystemPage-C8GQyKcD.css
Normal file
@@ -0,0 +1 @@
|
||||
.page-stack[data-v-b359577d]{display:flex;flex-direction:column;gap:12px}.card[data-v-b359577d]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-b359577d]{margin:0 0 12px;font-size:14px;font-weight:800}.kdocs-qr[data-v-b359577d]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-b359577d]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-b359577d]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-b359577d]{display:flex;flex-wrap:wrap;gap:10px}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.page-stack[data-v-682b6c42]{display:flex;flex-direction:column;gap:12px}.card[data-v-682b6c42]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-682b6c42]{margin:0 0 12px;font-size:14px;font-weight:800}.kdocs-qr[data-v-682b6c42]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-682b6c42]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-682b6c42]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-682b6c42]{display:flex;flex-wrap:wrap;gap:10px}
|
||||
17
static/admin/assets/SystemPage-DVj-4Lnp.js
Normal file
17
static/admin/assets/SystemPage-DVj-4Lnp.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{a as n}from"./index-BDEpmftQ.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
||||
import{a as n}from"./index-DKH_HvPt.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
||||
File diff suppressed because one or more lines are too long
30
static/admin/assets/index-DKH_HvPt.js
Normal file
30
static/admin/assets/index-DKH_HvPt.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/index-_5Ec1Hmd.css
Normal file
1
static/admin/assets/index-_5Ec1Hmd.css
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{a}from"./index-BDEpmftQ.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function o(){const{data:t}=await a.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};
|
||||
import{a}from"./index-DKH_HvPt.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function o(){const{data:t}=await a.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};
|
||||
@@ -1 +1 @@
|
||||
import{a}from"./index-BDEpmftQ.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||
import{a}from"./index-DKH_HvPt.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||
@@ -1 +1 @@
|
||||
import{a as t}from"./index-BDEpmftQ.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||
import{a as t}from"./index-DKH_HvPt.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理 - 知识管理平台</title>
|
||||
<script type="module" crossorigin src="./assets/index-BDEpmftQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DxTKnDeo.css">
|
||||
<script type="module" crossorigin src="./assets/index-DKH_HvPt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-_5Ec1Hmd.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user