fix: 账号页闪烁/浏览类型/截图复制/时区统一

This commit is contained in:
2025-12-14 11:30:49 +08:00
parent 2ec88eac3b
commit a9c8aac48f
59 changed files with 685 additions and 339 deletions

View File

@@ -244,7 +244,7 @@ certbot renew --dry-run
### 2. 定时任务 ### 2. 定时任务
- **启用定时浏览**: 是/否 - **启用定时浏览**: 是/否
- **执行时间**: 02:00 (CST时间) - **执行时间**: 02:00 (CST时间)
- **浏览类型**: 应读/注册前未读/未读 - **浏览类型**: 应读/注册前未读
- **执行日期**: 周一到周日 - **执行日期**: 周一到周日
### 3. 代理配置 ### 3. 代理配置

View File

@@ -85,7 +85,7 @@
- 批量启动:`POST /api/accounts/batch/start` - 批量启动:`POST /api/accounts/batch/start`
- 批量停止:`POST /api/accounts/batch/stop` - 批量停止:`POST /api/accounts/batch/stop`
- 清空账号:`POST /api/accounts/clear` - 清空账号:`POST /api/accounts/clear`
- 批量参数:浏览类型(应读/未读/注册前未读)、截图开关、选中账号集合 - 批量参数:浏览类型(应读/注册前未读)、截图开关、选中账号集合
- VIP 限制/提示: - VIP 限制/提示:
- 普通用户账号数量上限、批量能力/定时能力等(以现有逻辑为准) - 普通用户账号数量上限、批量能力/定时能力等(以现有逻辑为准)

View File

@@ -63,7 +63,7 @@ let timer = null
function recordUpdatedAt() { function recordUpdatedAt() {
try { try {
lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false }) lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
} catch { } catch {
lastUpdatedAt.value = '' lastUpdatedAt.value = ''
} }

View File

@@ -54,6 +54,11 @@ const scheduleWeekdayDisplay = computed(() =>
.join('、'), .join('、'),
) )
function normalizeBrowseType(value) {
if (String(value) === '注册前未读') return '注册前未读'
return '应读'
}
async function loadAll() { async function loadAll() {
loading.value = true loading.value = true
try { try {
@@ -65,7 +70,7 @@ async function loadAll() {
scheduleEnabled.value = (system.schedule_enabled ?? 0) === 1 scheduleEnabled.value = (system.schedule_enabled ?? 0) === 1
scheduleTime.value = system.schedule_time || '02:00' scheduleTime.value = system.schedule_time || '02:00'
scheduleBrowseType.value = system.schedule_browse_type || '应读' scheduleBrowseType.value = normalizeBrowseType(system.schedule_browse_type)
const weekdays = String(system.schedule_weekdays || '1,2,3,4,5,6,7') const weekdays = String(system.schedule_weekdays || '1,2,3,4,5,6,7')
.split(',') .split(',')
@@ -279,7 +284,6 @@ onMounted(loadAll)
<el-select v-model="scheduleBrowseType" style="width: 220px"> <el-select v-model="scheduleBrowseType" style="width: 220px">
<el-option label="注册前未读" value="注册前未读" /> <el-option label="注册前未读" value="注册前未读" />
<el-option label="应读" value="应读" /> <el-option label="应读" value="应读" />
<el-option label="未读" value="未读" />
</el-select> </el-select>
</el-form-item> </el-form-item>

View File

@@ -2,9 +2,22 @@ export function parseSqliteDateTime(value) {
if (!value) return null if (!value) return null
if (value instanceof Date) return value if (value instanceof Date) return value
const str = String(value) let str = String(value).trim()
if (!str) return null
// "YYYY-MM-DD" -> "YYYY-MM-DDT00:00:00"
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) str = `${str}T00:00:00`
// "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss" // "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss"
const iso = str.includes('T') ? str : str.replace(' ', 'T') let iso = str.includes('T') ? str : str.replace(' ', 'T')
// SQLite 可能带微秒Date 仅可靠支持到毫秒
iso = iso.replace(/\.(\d{3})\d+/, '.$1')
// 统一按北京时间解析(除非字符串本身已带时区)
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso)
if (!hasTimezone) iso = `${iso}+08:00`
const date = new Date(iso) const date = new Date(iso)
if (Number.isNaN(date.getTime())) return null if (Number.isNaN(date.getTime())) return null
return date return date
@@ -14,4 +27,3 @@ export function formatDateTime(value) {
if (!value) return '-' if (!value) return '-'
return String(value) return String(value)
} }

View File

@@ -350,14 +350,13 @@ class APIBrowser:
return result return result
# 根据浏览类型确定 bz 参数 # 根据浏览类型确定 bz 参数
# 网页实际选项: 0=注册前未读, 1=已读, 2=应读 # 网页实际参数: 0=注册前未读, 2=应读(历史上曾存在 1=已读,但当前逻辑不再使用)
# 前端选项: 注册前未读, 应读, 未读, 已读 # 当前前端选项: 注册前未读、应读(默认应读)
if '注册前' in browse_type: browse_type_text = str(browse_type or "")
if '注册前' in browse_type_text:
bz = 0 # 注册前未读 bz = 0 # 注册前未读
elif browse_type == '已读':
bz = 1 # 已读
else: else:
bz = 2 # 应读、未读 都映射到 bz=2 bz = 2 # 应读
self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...") self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...")

View File

@@ -59,7 +59,6 @@ const editForm = reactive({
const browseTypeOptions = [ const browseTypeOptions = [
{ label: '应读', value: '应读' }, { label: '应读', value: '应读' },
{ label: '未读', value: '未读' },
{ label: '注册前未读', value: '注册前未读' }, { label: '注册前未读', value: '注册前未读' },
] ]
@@ -80,8 +79,13 @@ function normalizeAccountPayload(acc) {
} }
function replaceAccounts(list) { function replaceAccounts(list) {
for (const key of Object.keys(accountsById)) delete accountsById[key] const nextList = Array.isArray(list) ? list : []
for (const acc of list || []) normalizeAccountPayload(acc) const nextIds = new Set(nextList.map((acc) => String(acc?.id || '')))
for (const existingId of Object.keys(accountsById)) {
if (!nextIds.has(existingId)) delete accountsById[existingId]
}
for (const acc of nextList) normalizeAccountPayload(acc)
} }
function ensureBrowseTypeDefaults() { function ensureBrowseTypeDefaults() {
@@ -138,8 +142,9 @@ function showRuntimeProgress(acc) {
return true return true
} }
async function refreshStats() { async function refreshStats(options = {}) {
statsLoading.value = true const silent = Boolean(options?.silent)
if (!silent) statsLoading.value = true
try { try {
const data = await fetchRunStats() const data = await fetchRunStats()
stats.today_completed = Number(data?.today_completed || 0) stats.today_completed = Number(data?.today_completed || 0)
@@ -150,7 +155,7 @@ async function refreshStats() {
} catch (e) { } catch (e) {
if (e?.response?.status === 401) window.location.href = '/login' if (e?.response?.status === 401) window.location.href = '/login'
} finally { } finally {
statsLoading.value = false if (!silent) statsLoading.value = false
} }
} }
@@ -456,6 +461,41 @@ function bindSocket() {
let unbindSocket = null let unbindSocket = null
let statsTimer = null let statsTimer = null
const shouldPollStats = computed(() => {
// 仅在“真正执行中”才轮询(排队中不轮询,避免空转导致页面闪烁)
return accounts.value.some((acc) => {
if (!acc?.is_running) return false
const statusText = String(acc.status || '')
if (statusText.includes('排队')) return false
return true
})
})
function stopStatsPolling() {
if (!statsTimer) return
window.clearInterval(statsTimer)
statsTimer = null
}
function startStatsPolling() {
if (statsTimer) return
statsTimer = window.setInterval(() => refreshStats({ silent: true }), 10_000)
}
function syncStatsPolling(prevRunning = null) {
const running = shouldPollStats.value
// 任务从“运行中 -> 空闲”时,补拉一次统计以确保最终数据正确,再停止轮询
if (prevRunning === true && running === false) refreshStats({ silent: true }).catch(() => {})
if (running) startStatsPolling()
else stopStatsPolling()
}
watch(shouldPollStats, (running, prevRunning) => {
syncStatsPolling(prevRunning)
})
onMounted(async () => { onMounted(async () => {
if (!userStore.vipInfo) { if (!userStore.vipInfo) {
userStore.refreshVipInfo().catch(() => { userStore.refreshVipInfo().catch(() => {
@@ -467,12 +507,12 @@ onMounted(async () => {
await refreshAccounts() await refreshAccounts()
await refreshStats() await refreshStats()
statsTimer = window.setInterval(refreshStats, 10_000) syncStatsPolling()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (unbindSocket) unbindSocket() if (unbindSocket) unbindSocket()
if (statsTimer) window.clearInterval(statsTimer) stopStatsPolling()
}) })
</script> </script>
@@ -565,7 +605,7 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
<el-skeleton v-if="loading || statsLoading" :rows="5" animated /> <el-skeleton v-if="loading" :rows="5" animated />
<template v-else> <template v-else>
<el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" /> <el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" />
<div v-else class="grid"> <div v-else class="grid">

View File

@@ -45,10 +45,14 @@ const form = reactive({
const browseTypeOptions = [ const browseTypeOptions = [
{ label: '应读', value: '应读' }, { label: '应读', value: '应读' },
{ label: '未读', value: '未读' },
{ label: '注册前未读', value: '注册前未读' }, { label: '注册前未读', value: '注册前未读' },
] ]
function normalizeBrowseType(value) {
if (String(value) === '注册前未读') return '注册前未读'
return '应读'
}
const weekdayOptions = [ const weekdayOptions = [
{ label: '周一', value: '1' }, { label: '周一', value: '1' },
{ label: '周二', value: '2' }, { label: '周二', value: '2' },
@@ -92,7 +96,11 @@ async function loadAccounts() {
async function loadSchedules() { async function loadSchedules() {
loading.value = true loading.value = true
try { try {
schedules.value = await fetchSchedules() const list = await fetchSchedules()
schedules.value = (Array.isArray(list) ? list : []).map((s) => ({
...s,
browse_type: normalizeBrowseType(s?.browse_type),
}))
} catch (e) { } catch (e) {
if (e?.response?.status === 401) window.location.href = '/login' if (e?.response?.status === 401) window.location.href = '/login'
schedules.value = [] schedules.value = []
@@ -121,7 +129,7 @@ function openEdit(schedule) {
.filter(Boolean) .filter(Boolean)
.map((v) => String(v)) .map((v) => String(v))
if (form.weekdays.length === 0) form.weekdays = ['1', '2', '3', '4', '5'] if (form.weekdays.length === 0) form.weekdays = ['1', '2', '3', '4', '5']
form.browse_type = schedule.browse_type || '应读' form.browse_type = normalizeBrowseType(schedule.browse_type)
form.enable_screenshot = Number(schedule.enable_screenshot ?? 1) !== 0 form.enable_screenshot = Number(schedule.enable_screenshot ?? 1) !== 0
form.account_ids = Array.isArray(schedule.account_ids) ? schedule.account_ids.slice() : [] form.account_ids = Array.isArray(schedule.account_ids) ? schedule.account_ids.slice() : []
editorOpen.value = true editorOpen.value = true

View File

@@ -34,6 +34,87 @@ function openPreview(item) {
previewOpen.value = true previewOpen.value = true
} }
function findRenderedShotImage(filename) {
try {
const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(String(filename)) : String(filename)
return document.querySelector(`img[data-shot-filename="${escaped}"]`)
} catch {
return null
}
}
function canvasToPngBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('toBlob_failed'))), 'image/png')
})
}
async function imageElementToPngBlob(imgEl) {
if (!imgEl) throw new Error('no_image')
if (!imgEl.complete || imgEl.naturalWidth <= 0) {
if (typeof imgEl.decode === 'function') await imgEl.decode()
else {
await new Promise((resolve, reject) => {
imgEl.addEventListener('load', resolve, { once: true })
imgEl.addEventListener('error', reject, { once: true })
})
}
}
const canvas = document.createElement('canvas')
canvas.width = imgEl.naturalWidth
canvas.height = imgEl.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('no_canvas')
ctx.drawImage(imgEl, 0, 0)
return await canvasToPngBlob(canvas)
}
async function blobToPng(blob) {
if (!blob) throw new Error('no_blob')
if (blob.type === 'image/png') return blob
if (typeof createImageBitmap === 'function') {
const bitmap = await createImageBitmap(blob)
const canvas = document.createElement('canvas')
canvas.width = bitmap.width
canvas.height = bitmap.height
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('no_canvas')
ctx.drawImage(bitmap, 0, 0)
return await canvasToPngBlob(canvas)
}
const url = URL.createObjectURL(blob)
try {
const img = new Image()
img.src = url
if (typeof img.decode === 'function') await img.decode()
return await imageElementToPngBlob(img)
} finally {
URL.revokeObjectURL(url)
}
}
async function screenshotUrlToPngBlob(url, filename) {
// 优先使用页面上已渲染完成的 <img>(避免额外请求;也更容易满足剪贴板“用户手势”限制)
const imgEl = findRenderedShotImage(filename)
if (imgEl) {
try {
return await imageElementToPngBlob(imgEl)
} catch {
// fallback to fetch
}
}
const resp = await fetch(url, { credentials: 'include', cache: 'no-store' })
if (!resp.ok) throw new Error('fetch_failed')
const blob = await resp.blob()
const mime = resp.headers.get('Content-Type') || blob.type || ''
if (!mime.startsWith('image/')) throw new Error('not_image')
return await blobToPng(blob)
}
async function onClearAll() { async function onClearAll() {
try { try {
await ElMessageBox.confirm('确定要清空全部截图吗?', '清空截图', { await ElMessageBox.confirm('确定要清空全部截图吗?', '清空截图', {
@@ -98,14 +179,28 @@ async function copyImage(item) {
} }
try { try {
const resp = await fetch(url, { credentials: 'include' }) // 关键点:用 Promise 形式的数据源,让 clipboard.write 在用户手势内立即发生(更稳)
if (!resp.ok) throw new Error('fetch_failed') try {
const blob = await resp.blob() await navigator.clipboard.write([
const mime = blob.type || '' new ClipboardItem({
if (!mime.startsWith('image/')) throw new Error('not_image') 'image/png': screenshotUrlToPngBlob(url, item.filename),
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]) }),
ElMessage.success('图片已复制') ])
} catch { } catch {
const pngBlob = await screenshotUrlToPngBlob(url, item.filename)
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })])
}
ElMessage.success('图片已复制到剪贴板')
} catch (e) {
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(`${window.location.origin}${url}`)
ElMessage.warning('复制图片失败,已复制图片链接(可直接粘贴到浏览器打开)')
return
}
} catch {
// ignore
}
ElMessage.warning('复制图片失败:请确认允许剪贴板权限;可用“下载”。') ElMessage.warning('复制图片失败:请确认允许剪贴板权限;可用“下载”。')
} }
} }
@@ -138,7 +233,14 @@ onMounted(load)
<div v-else class="grid"> <div v-else class="grid">
<el-card v-for="item in screenshots" :key="item.filename" shadow="never" class="shot-card" :body-style="{ padding: '0' }"> <el-card v-for="item in screenshots" :key="item.filename" shadow="never" class="shot-card" :body-style="{ padding: '0' }">
<img class="shot-img" :src="buildUrl(item.filename)" :alt="item.display_name || item.filename" loading="lazy" @click="openPreview(item)" /> <img
class="shot-img"
:src="buildUrl(item.filename)"
:alt="item.display_name || item.filename"
:data-shot-filename="item.filename"
loading="lazy"
@click="openPreview(item)"
/>
<div class="shot-body"> <div class="shot-body">
<div class="shot-name" :title="item.display_name || item.filename">{{ item.display_name || item.filename }}</div> <div class="shot-name" :title="item.display_name || item.filename">{{ item.display_name || item.filename }}</div>
<div class="shot-meta app-muted">{{ item.created || '' }}</div> <div class="shot-meta app-muted">{{ item.created || '' }}</div>

85
app.py
View File

@@ -76,6 +76,34 @@ def get_beijing_now():
"""获取北京时间的当前时间(统一时区处理)""" """获取北京时间的当前时间(统一时区处理)"""
return datetime.now(BEIJING_TZ) return datetime.now(BEIJING_TZ)
# ========== 浏览类型规范化 ==========
# 当前页面仅保留:应读、注册前未读;默认应读
BROWSE_TYPE_SHOULD_READ = "应读"
BROWSE_TYPE_PRE_REG_UNREAD = "注册前未读"
_BROWSE_TYPES_ALLOWED_INPUT = {
BROWSE_TYPE_SHOULD_READ,
BROWSE_TYPE_PRE_REG_UNREAD,
}
def normalize_browse_type(value, default=BROWSE_TYPE_SHOULD_READ):
"""规范化浏览类型(非法值回退到默认)。"""
text = str(value or "").strip()
if text == BROWSE_TYPE_PRE_REG_UNREAD:
return BROWSE_TYPE_PRE_REG_UNREAD
if text == BROWSE_TYPE_SHOULD_READ:
return BROWSE_TYPE_SHOULD_READ
return default
def validate_browse_type(value, default=BROWSE_TYPE_SHOULD_READ):
"""校验并返回规范化浏览类型非法值返回None。"""
text = str(value if value is not None else default).strip()
if text not in _BROWSE_TYPES_ALLOWED_INPUT:
return None
return normalize_browse_type(text, default=default)
# ========== 初始化配置 ========== # ========== 初始化配置 ==========
config = get_config() config = get_config()
@@ -868,7 +896,7 @@ class Account:
self.total_items = 0 self.total_items = 0
self.total_attachments = 0 self.total_attachments = 0
self.automation = None self.automation = None
self.last_browse_type = "注册前未读" self.last_browse_type = BROWSE_TYPE_SHOULD_READ
self.proxy_config = None # 保存代理配置,浏览和截图共用 self.proxy_config = None # 保存代理配置,浏览和截图共用
@property @property
@@ -2678,8 +2706,10 @@ def start_account(account_id):
if account.is_running: if account.is_running:
return jsonify({"error": "任务已在运行中"}), 400 return jsonify({"error": "任务已在运行中"}), 400
data = request.json data = request.json or {}
browse_type = data.get('browse_type', '应读') browse_type = validate_browse_type(data.get('browse_type'), default=BROWSE_TYPE_SHOULD_READ)
if not browse_type:
return jsonify({"error": "浏览类型无效"}), 400
enable_screenshot = data.get('enable_screenshot', True) # 默认启用截图 enable_screenshot = data.get('enable_screenshot', True) # 默认启用截图
# 确保浏览器管理器已初始化 # 确保浏览器管理器已初始化
@@ -3278,10 +3308,8 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
base = f"{parsed.scheme}://{parsed.netloc}" base = f"{parsed.scheme}://{parsed.netloc}"
if '注册前' in str(browse_type): if '注册前' in str(browse_type):
bz = 0 bz = 0
elif str(browse_type) == '已读':
bz = 1
else: else:
bz = 2 # 应读/未读 bz = 2 # 应读
target_url = f"{base}/admin/center.aspx?bz={bz}" target_url = f"{base}/admin/center.aspx?bz={bz}"
automation.main_page.goto(target_url, timeout=60000) automation.main_page.goto(target_url, timeout=60000)
current_url = getattr(automation.main_page, "url", "") or "" current_url = getattr(automation.main_page, "url", "") or ""
@@ -3461,7 +3489,14 @@ def manual_screenshot(account_id):
return jsonify({"error": "任务运行中,无法截图"}), 400 return jsonify({"error": "任务运行中,无法截图"}), 400
data = request.json or {} data = request.json or {}
browse_type = data.get('browse_type', account.last_browse_type) requested_browse_type = data.get('browse_type', None)
if requested_browse_type is None:
# 兼容历史遗留值:内存中的 last_browse_type 可能为旧枚举,直接规范化回退到默认(应读)
browse_type = normalize_browse_type(account.last_browse_type)
else:
browse_type = validate_browse_type(requested_browse_type, default=BROWSE_TYPE_SHOULD_READ)
if not browse_type:
return jsonify({"error": "浏览类型无效"}), 400
account.last_browse_type = browse_type account.last_browse_type = browse_type
@@ -3911,10 +3946,12 @@ def get_run_stats():
# 获取今日任务统计 # 获取今日任务统计
stats = database.get_user_run_stats(user_id) stats = database.get_user_run_stats(user_id)
# 计算当前正在运行的账号数 # 计算当前正在执行”的账号数(排队中的不计入)
current_running = 0 current_running = 0
if user_id in user_accounts: with task_status_lock:
current_running = sum(1 for acc in user_accounts[user_id].values() if acc.is_running) for info in task_status.values():
if info.get('user_id') == user_id and info.get('status') == '运行中':
current_running += 1
return jsonify({ return jsonify({
'today_completed': stats.get('completed', 0), 'today_completed': stats.get('completed', 0),
@@ -3973,8 +4010,10 @@ def update_system_config_api():
return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400 return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400
if schedule_browse_type is not None: if schedule_browse_type is not None:
if schedule_browse_type not in ['注册前未读', '应读', '未读']: normalized = validate_browse_type(schedule_browse_type, default=BROWSE_TYPE_SHOULD_READ)
if not normalized:
return jsonify({"error": "浏览类型无效"}), 400 return jsonify({"error": "浏览类型无效"}), 400
schedule_browse_type = normalized
if schedule_weekdays is not None: if schedule_weekdays is not None:
# 验证星期格式,应该是逗号分隔的数字字符串 "1,2,3,4,5,6,7" # 验证星期格式,应该是逗号分隔的数字字符串 "1,2,3,4,5,6,7"
@@ -4341,7 +4380,7 @@ def run_scheduled_task(skip_weekday_check=False):
""" """
try: try:
config = database.get_system_config() config = database.get_system_config()
browse_type = config.get('schedule_browse_type', '应读') browse_type = normalize_browse_type(config.get('schedule_browse_type', BROWSE_TYPE_SHOULD_READ))
# 检查今天是否在允许执行的星期列表中(立即执行时跳过此检查) # 检查今天是否在允许执行的星期列表中(立即执行时跳过此检查)
if not skip_weekday_check: if not skip_weekday_check:
@@ -4445,6 +4484,9 @@ def status_push_worker():
with task_status_lock: with task_status_lock:
status_items = list(task_status.items()) status_items = list(task_status.items())
for account_id, status_info in status_items: for account_id, status_info in status_items:
# 无任务执行时不推送;排队中的状态已通过事件推送过一次,无需周期性推送
if status_info.get('status') != '运行中':
continue
user_id = status_info.get('user_id') user_id = status_info.get('user_id')
if user_id: if user_id:
# 获取账号对象 # 获取账号对象
@@ -4618,7 +4660,7 @@ def scheduled_task_worker():
# 执行用户定时任务 # 执行用户定时任务
user_id = schedule_config['user_id'] user_id = schedule_config['user_id']
schedule_id = schedule_config['id'] schedule_id = schedule_config['id']
browse_type = schedule_config.get('browse_type', '应读') browse_type = normalize_browse_type(schedule_config.get('browse_type', BROWSE_TYPE_SHOULD_READ))
enable_screenshot = schedule_config.get('enable_screenshot', 1) enable_screenshot = schedule_config.get('enable_screenshot', 1)
try: try:
@@ -5109,7 +5151,9 @@ def create_user_schedule_api():
name = data.get('name', '我的定时任务') name = data.get('name', '我的定时任务')
schedule_time = data.get('schedule_time', '08:00') schedule_time = data.get('schedule_time', '08:00')
weekdays = data.get('weekdays', '1,2,3,4,5') weekdays = data.get('weekdays', '1,2,3,4,5')
browse_type = data.get('browse_type', '应读') browse_type = validate_browse_type(data.get('browse_type', BROWSE_TYPE_SHOULD_READ), default=BROWSE_TYPE_SHOULD_READ)
if not browse_type:
return jsonify({"error": "浏览类型无效"}), 400
enable_screenshot = data.get('enable_screenshot', 1) enable_screenshot = data.get('enable_screenshot', 1)
account_ids = data.get('account_ids', []) account_ids = data.get('account_ids', [])
@@ -5170,6 +5214,11 @@ def update_schedule_api(schedule_id):
import re import re
if not re.match(r'^\d{2}:\d{2}$', update_data['schedule_time']): if not re.match(r'^\d{2}:\d{2}$', update_data['schedule_time']):
return jsonify({"error": "时间格式不正确"}), 400 return jsonify({"error": "时间格式不正确"}), 400
if 'browse_type' in update_data:
normalized = validate_browse_type(update_data.get('browse_type'), default=BROWSE_TYPE_SHOULD_READ)
if not normalized:
return jsonify({"error": "浏览类型无效"}), 400
update_data['browse_type'] = normalized
success = database.update_user_schedule(schedule_id, **update_data) success = database.update_user_schedule(schedule_id, **update_data)
if success: if success:
@@ -5232,7 +5281,7 @@ def run_schedule_now_api(schedule_id):
return jsonify({"error": "没有配置账号"}), 400 return jsonify({"error": "没有配置账号"}), 400
user_id = current_user.id user_id = current_user.id
browse_type = schedule['browse_type'] browse_type = normalize_browse_type(schedule.get('browse_type', BROWSE_TYPE_SHOULD_READ))
enable_screenshot = schedule['enable_screenshot'] enable_screenshot = schedule['enable_screenshot']
started = [] started = []
@@ -5304,10 +5353,12 @@ def delete_schedule_logs_api(schedule_id):
def batch_start_accounts(): def batch_start_accounts():
"""批量启动账号""" """批量启动账号"""
user_id = current_user.id user_id = current_user.id
data = request.json data = request.json or {}
account_ids = data.get('account_ids', []) account_ids = data.get('account_ids', [])
browse_type = data.get('browse_type', '应读') browse_type = validate_browse_type(data.get('browse_type', BROWSE_TYPE_SHOULD_READ), default=BROWSE_TYPE_SHOULD_READ)
if not browse_type:
return jsonify({"error": "浏览类型无效"}), 400
enable_screenshot = data.get('enable_screenshot', True) enable_screenshot = data.get('enable_screenshot', True)
if not account_ids: if not account_ids:

View File

@@ -54,7 +54,7 @@ config = get_config()
DB_FILE = config.DB_FILE DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理) # 数据库版本 (用于迁移管理)
DB_VERSION = 6 DB_VERSION = 7
# ==================== 时区处理工具函数 ==================== # ==================== 时区处理工具函数 ====================
# Bug fix: 统一时区处理,避免混用导致的问题 # Bug fix: 统一时区处理,避免混用导致的问题
@@ -379,9 +379,13 @@ def migrate_database():
_migrate_to_v6(conn) _migrate_to_v6(conn)
current_version = 6 current_version = 6
if current_version < 7:
_migrate_to_v7(conn)
current_version = 7
# 更新版本号 # 更新版本号
cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1', cursor.execute('UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1',
(DB_VERSION,)) (DB_VERSION, get_cst_now_str()))
conn.commit() conn.commit()
if current_version < DB_VERSION: if current_version < DB_VERSION:
@@ -587,6 +591,66 @@ def _migrate_to_v6(conn):
conn.commit() conn.commit()
def _migrate_to_v7(conn):
"""迁移到版本7 - 统一存储北京时间将历史UTC时间字段整体+8小时"""
cursor = conn.cursor()
def table_exists(table_name: str) -> bool:
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
return cursor.fetchone() is not None
def column_exists(table_name: str, column_name: str) -> bool:
cursor.execute(f"PRAGMA table_info({table_name})")
return any(row[1] == column_name for row in cursor.fetchall())
def shift_utc_to_cst(table_name: str, column_name: str) -> None:
if not table_exists(table_name):
return
if not column_exists(table_name, column_name):
return
cursor.execute(
f"""
UPDATE {table_name}
SET {column_name} = datetime({column_name}, '+8 hours')
WHERE {column_name} IS NOT NULL AND {column_name} != ''
"""
)
# 主库(这些字段历史上主要由 CURRENT_TIMESTAMP 产生为UTC
for table, col in [
("users", "created_at"),
("users", "approved_at"),
("admins", "created_at"),
("accounts", "created_at"),
("password_reset_requests", "created_at"),
("password_reset_requests", "processed_at"),
]:
shift_utc_to_cst(table, col)
# 邮件模块(同库,不一定启用,但表可能存在)
for table, col in [
("smtp_configs", "created_at"),
("smtp_configs", "updated_at"),
("smtp_configs", "last_success_at"),
("email_settings", "updated_at"),
("email_tokens", "created_at"),
("email_logs", "created_at"),
("email_stats", "last_updated"),
]:
shift_utc_to_cst(table, col)
# 断点续传(同库,表由 task_checkpoint.py 创建)
for table, col in [
("task_checkpoints", "created_at"),
("task_checkpoints", "updated_at"),
("task_checkpoints", "completed_at"),
]:
shift_utc_to_cst(table, col)
conn.commit()
print(" ✓ 时区迁移历史UTC时间已转换为北京时间")
# ==================== 管理员相关 ==================== # ==================== 管理员相关 ====================
def ensure_default_admin(): def ensure_default_admin():
@@ -612,8 +676,8 @@ def ensure_default_admin():
default_password_hash = hash_password_bcrypt(random_password) default_password_hash = hash_password_bcrypt(random_password)
cursor.execute( cursor.execute(
'INSERT INTO admins (username, password_hash) VALUES (?, ?)', 'INSERT INTO admins (username, password_hash, created_at) VALUES (?, ?, ?)',
('admin', default_password_hash) ('admin', default_password_hash, get_cst_now_str())
) )
conn.commit() conn.commit()
print("=" * 60) print("=" * 60)
@@ -696,10 +760,11 @@ def set_default_vip_days(days):
"""设置默认VIP天数""" """设置默认VIP天数"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute(''' cursor.execute('''
INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at) INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at)
VALUES (1, ?, CURRENT_TIMESTAMP) VALUES (1, ?, ?)
''', (days,)) ''', (days, cst_time))
conn.commit() conn.commit()
return True return True
@@ -823,6 +888,7 @@ def create_user(username, password, email=''):
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
password_hash = hash_password(password) password_hash = hash_password(password)
cst_time = get_cst_now_str()
# 获取默认VIP天数 # 获取默认VIP天数
default_vip_days = get_vip_config()['default_vip_days'] default_vip_days = get_vip_config()['default_vip_days']
@@ -836,9 +902,9 @@ def create_user(username, password, email=''):
try: try:
cursor.execute(''' cursor.execute('''
INSERT INTO users (username, password_hash, email, status, vip_expire_time) INSERT INTO users (username, password_hash, email, status, vip_expire_time, created_at)
VALUES (?, ?, ?, 'pending', ?) VALUES (?, ?, ?, 'pending', ?, ?)
''', (username, password_hash, email, vip_expire_time)) ''', (username, password_hash, email, vip_expire_time, cst_time))
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
@@ -978,11 +1044,12 @@ def approve_user(user_id):
"""审核通过用户""" """审核通过用户"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute(''' cursor.execute('''
UPDATE users UPDATE users
SET status = 'approved', approved_at = CURRENT_TIMESTAMP SET status = 'approved', approved_at = ?
WHERE id = ? WHERE id = ?
''', (user_id,)) ''', (cst_time, user_id))
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
@@ -1011,12 +1078,13 @@ def create_account(user_id, account_id, username, password, remember=True, remar
"""创建账号(密码加密存储)""" """创建账号(密码加密存储)"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
# 安全修复:加密存储第三方账号密码 # 安全修复:加密存储第三方账号密码
encrypted_password = encrypt_password(password) encrypted_password = encrypt_password(password)
cursor.execute(''' cursor.execute('''
INSERT INTO accounts (id, user_id, username, password, remember, remark) INSERT INTO accounts (id, user_id, username, password, remember, remark, created_at)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark)) ''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark, cst_time))
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
@@ -1171,7 +1239,7 @@ def get_system_stats():
cursor.execute(''' cursor.execute('''
SELECT COUNT(*) as count FROM users SELECT COUNT(*) as count FROM users
WHERE vip_expire_time IS NOT NULL WHERE vip_expire_time IS NOT NULL
AND datetime(vip_expire_time) > datetime('now') AND datetime(vip_expire_time) > datetime('now', 'localtime')
''') ''')
vip_users = cursor.fetchone()['count'] vip_users = cursor.fetchone()['count']
@@ -1291,7 +1359,8 @@ def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_ti
params.append(auto_approve_vip_days) params.append(auto_approve_vip_days)
if updates: if updates:
updates.append('updated_at = CURRENT_TIMESTAMP') updates.append('updated_at = ?')
params.append(get_cst_now_str())
# Bug fix: 验证所有字段名都在白名单中 # Bug fix: 验证所有字段名都在白名单中
for update_clause in updates: for update_clause in updates:
field_name = update_clause.split('=')[0].strip() field_name = update_clause.split('=')[0].strip()
@@ -1311,7 +1380,7 @@ def get_hourly_registration_count():
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
SELECT COUNT(*) FROM users SELECT COUNT(*) FROM users
WHERE created_at >= datetime('now', '-1 hour') WHERE created_at >= datetime('now', 'localtime', '-1 hour')
''') ''')
return cursor.fetchone()[0] return cursor.fetchone()[0]
@@ -1483,7 +1552,7 @@ def delete_old_task_logs(days=30, batch_size=1000):
DELETE FROM task_logs DELETE FROM task_logs
WHERE rowid IN ( WHERE rowid IN (
SELECT rowid FROM task_logs SELECT rowid FROM task_logs
WHERE created_at < datetime('now', '-' || ? || ' days') WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
LIMIT ? LIMIT ?
) )
''', (days, batch_size)) ''', (days, batch_size))
@@ -1533,12 +1602,13 @@ def create_password_reset_request(user_id, new_password):
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
password_hash = hash_password_bcrypt(new_password) password_hash = hash_password_bcrypt(new_password)
cst_time = get_cst_now_str()
try: try:
cursor.execute(''' cursor.execute('''
INSERT INTO password_reset_requests (user_id, new_password_hash, status) INSERT INTO password_reset_requests (user_id, new_password_hash, status, created_at)
VALUES (?, ?, 'pending') VALUES (?, ?, 'pending', ?)
''', (user_id, password_hash)) ''', (user_id, password_hash, cst_time))
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid
except Exception as e: except Exception as e:
@@ -1565,6 +1635,7 @@ def approve_password_reset(request_id):
"""批准密码重置申请""" """批准密码重置申请"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
try: try:
# 获取申请信息 # 获取申请信息
@@ -1588,9 +1659,9 @@ def approve_password_reset(request_id):
# 更新申请状态 # 更新申请状态
cursor.execute(''' cursor.execute('''
UPDATE password_reset_requests UPDATE password_reset_requests
SET status = 'approved', processed_at = CURRENT_TIMESTAMP SET status = 'approved', processed_at = ?
WHERE id = ? WHERE id = ?
''', (request_id,)) ''', (cst_time, request_id))
conn.commit() conn.commit()
return True return True
@@ -1603,13 +1674,14 @@ def reject_password_reset(request_id):
"""拒绝密码重置申请""" """拒绝密码重置申请"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
try: try:
cursor.execute(''' cursor.execute('''
UPDATE password_reset_requests UPDATE password_reset_requests
SET status = 'rejected', processed_at = CURRENT_TIMESTAMP SET status = 'rejected', processed_at = ?
WHERE id = ? AND status = 'pending' WHERE id = ? AND status = 'pending'
''', (request_id,)) ''', (cst_time, request_id))
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
except Exception as e: except Exception as e:
@@ -1652,7 +1724,7 @@ def clean_old_operation_logs(days=30):
try: try:
cursor.execute(''' cursor.execute('''
DELETE FROM operation_logs DELETE FROM operation_logs
WHERE created_at < datetime('now', '-' || ? || ' days') WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
''', (days,)) ''', (days,))
deleted_count = cursor.rowcount deleted_count = cursor.rowcount
conn.commit() conn.commit()
@@ -2146,7 +2218,7 @@ def clean_old_schedule_logs(days=30):
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
DELETE FROM schedule_execution_logs DELETE FROM schedule_execution_logs
WHERE execute_time < datetime('now', '-' || ? || ' days') WHERE execute_time < datetime('now', 'localtime', '-' || ? || ' days')
''', (days,)) ''', (days,))
conn.commit() conn.commit()
return cursor.rowcount return cursor.rowcount

View File

@@ -27,6 +27,10 @@ BEIJING_TZ = pytz.timezone('Asia/Shanghai')
def get_beijing_today(): def get_beijing_today():
"""获取北京时间的今天日期字符串""" """获取北京时间的今天日期字符串"""
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d') return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d')
def get_beijing_now_str():
"""获取北京时间的当前时间字符串"""
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d %H:%M:%S')
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
@@ -44,7 +48,13 @@ from crypto_utils import encrypt_password, decrypt_password, is_encrypted
def parse_datetime(dt_str: str) -> datetime: def parse_datetime(dt_str: str) -> datetime:
"""解析数据库中的时间字符串,支持带微秒和不带微秒的格式""" """解析数据库中的时间字符串,支持带微秒和不带微秒的格式"""
if not dt_str: if not dt_str:
return datetime.min return BEIJING_TZ.localize(datetime(1970, 1, 1))
# 兼容 sqlite3 可能返回 datetime 对象的情况
if isinstance(dt_str, datetime):
return dt_str.astimezone(BEIJING_TZ) if dt_str.tzinfo else BEIJING_TZ.localize(dt_str)
text = str(dt_str)
# 尝试多种格式 # 尝试多种格式
formats = [ formats = [
'%Y-%m-%d %H:%M:%S.%f', # 带微秒 '%Y-%m-%d %H:%M:%S.%f', # 带微秒
@@ -52,11 +62,12 @@ def parse_datetime(dt_str: str) -> datetime:
] ]
for fmt in formats: for fmt in formats:
try: try:
return datetime.strptime(dt_str, fmt) naive = datetime.strptime(text, fmt)
return BEIJING_TZ.localize(naive)
except ValueError: except ValueError:
continue continue
# 如果都失败,返回最小时间(视为过期) # 如果都失败,返回最小时间(视为过期)
return datetime.min return BEIJING_TZ.localize(datetime(1970, 1, 1))
# ============ 常量配置 ============ # ============ 常量配置 ============
@@ -146,10 +157,13 @@ def init_email_tables():
""") """)
# 初始化默认设置 # 初始化默认设置
cursor.execute(""" cursor.execute(
INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled) """
VALUES (1, 0, 1) INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled, updated_at)
""") VALUES (1, 0, 1, ?)
""",
(get_beijing_now_str(),),
)
# 3. 邮件验证Token表 # 3. 邮件验证Token表
cursor.execute(""" cursor.execute("""
@@ -208,9 +222,12 @@ def init_email_tables():
""") """)
# 初始化统计记录 # 初始化统计记录
cursor.execute(""" cursor.execute(
INSERT OR IGNORE INTO email_stats (id) VALUES (1) """
""") INSERT OR IGNORE INTO email_stats (id, last_updated) VALUES (1, ?)
""",
(get_beijing_now_str(),),
)
conn.commit() conn.commit()
print("[邮件服务] 数据库表初始化完成") print("[邮件服务] 数据库表初始化完成")
@@ -276,8 +293,8 @@ def update_email_settings(
cursor = conn.cursor() cursor = conn.cursor()
# 构建动态更新语句 # 构建动态更新语句
updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = CURRENT_TIMESTAMP'] updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = ?']
params = [int(enabled), int(failover_enabled)] params = [int(enabled), int(failover_enabled), get_beijing_now_str()]
if register_verify_enabled is not None: if register_verify_enabled is not None:
updates.append('register_verify_enabled = ?') updates.append('register_verify_enabled = ?')
@@ -416,6 +433,7 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
"""创建新的SMTP配置""" """创建新的SMTP配置"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
now_str = get_beijing_now_str()
# 加密密码 # 加密密码
password = encrypt_password(data.get('password', '')) if data.get('password') else '' password = encrypt_password(data.get('password', '')) if data.get('password') else ''
@@ -423,8 +441,8 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
cursor.execute(""" cursor.execute("""
INSERT INTO smtp_configs INSERT INTO smtp_configs
(name, enabled, is_primary, priority, host, port, username, password, (name, enabled, is_primary, priority, host, port, username, password,
use_ssl, use_tls, sender_name, sender_email, daily_limit) use_ssl, use_tls, sender_name, sender_email, daily_limit, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", ( """, (
data.get('name', '默认配置'), data.get('name', '默认配置'),
int(data.get('enabled', True)), int(data.get('enabled', True)),
@@ -438,7 +456,9 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
int(data.get('use_tls', False)), int(data.get('use_tls', False)),
data.get('sender_name', '自动化学习'), data.get('sender_name', '自动化学习'),
data.get('sender_email', ''), data.get('sender_email', ''),
data.get('daily_limit', 0) data.get('daily_limit', 0),
now_str,
now_str,
)) ))
config_id = cursor.lastrowid config_id = cursor.lastrowid
@@ -484,7 +504,8 @@ def update_smtp_config(config_id: int, data: Dict[str, Any]) -> bool:
if not updates: if not updates:
return False return False
updates.append("updated_at = CURRENT_TIMESTAMP") updates.append("updated_at = ?")
params.append(get_beijing_now_str())
params.append(config_id) params.append(config_id)
cursor.execute(f""" cursor.execute(f"""
@@ -515,9 +536,9 @@ def set_primary_smtp_config(config_id: int) -> bool:
# 设置新的主配置 # 设置新的主配置
cursor.execute(""" cursor.execute("""
UPDATE smtp_configs UPDATE smtp_configs
SET is_primary = 1, updated_at = CURRENT_TIMESTAMP SET is_primary = 1, updated_at = ?
WHERE id = ? WHERE id = ?
""", (config_id,)) """, (get_beijing_now_str(), config_id))
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
@@ -584,25 +605,26 @@ def _update_smtp_stats(config_id: int, success: bool, error: str = ''):
"""更新SMTP配置的统计信息""" """更新SMTP配置的统计信息"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
now_str = get_beijing_now_str()
if success: if success:
cursor.execute(""" cursor.execute("""
UPDATE smtp_configs UPDATE smtp_configs
SET daily_sent = daily_sent + 1, SET daily_sent = daily_sent + 1,
success_count = success_count + 1, success_count = success_count + 1,
last_success_at = CURRENT_TIMESTAMP, last_success_at = ?,
last_error = '', last_error = '',
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE id = ? WHERE id = ?
""", (config_id,)) """, (now_str, now_str, config_id))
else: else:
cursor.execute(""" cursor.execute("""
UPDATE smtp_configs UPDATE smtp_configs
SET fail_count = fail_count + 1, SET fail_count = fail_count + 1,
last_error = ?, last_error = ?,
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE id = ? WHERE id = ?
""", (error[:500], config_id)) """, (error[:500], now_str, config_id))
conn.commit() conn.commit()
@@ -864,11 +886,12 @@ def test_smtp_config(config_id: int, test_email: str) -> Dict[str, Any]:
# 更新最后成功时间 # 更新最后成功时间
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
now_str = get_beijing_now_str()
cursor.execute(""" cursor.execute("""
UPDATE smtp_configs UPDATE smtp_configs
SET last_success_at = CURRENT_TIMESTAMP, last_error = '' SET last_success_at = ?, last_error = '', updated_at = ?
WHERE id = ? WHERE id = ?
""", (config_id,)) """, (now_str, now_str, config_id))
conn.commit() conn.commit()
return {'success': True, 'error': ''} return {'success': True, 'error': ''}
@@ -899,10 +922,10 @@ def _log_email_send(user_id: int, smtp_config_id: int, email_to: str,
cursor.execute(""" cursor.execute("""
INSERT INTO email_logs INSERT INTO email_logs
(user_id, smtp_config_id, email_to, email_type, subject, status, (user_id, smtp_config_id, email_to, email_type, subject, status,
error_message, attachment_count, attachment_size) error_message, attachment_count, attachment_size, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (user_id, smtp_config_id, email_to, email_type, subject, status, """, (user_id, smtp_config_id, email_to, email_type, subject, status,
error_message, attachment_count, attachment_size)) error_message, attachment_count, attachment_size, get_beijing_now_str()))
conn.commit() conn.commit()
@@ -910,6 +933,7 @@ def _update_email_stats(email_type: str, success: bool):
"""更新邮件统计""" """更新邮件统计"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
now_str = get_beijing_now_str()
type_field_map = { type_field_map = {
EMAIL_TYPE_REGISTER: 'register_sent', EMAIL_TYPE_REGISTER: 'register_sent',
@@ -927,25 +951,25 @@ def _update_email_stats(email_type: str, success: bool):
SET total_sent = total_sent + 1, SET total_sent = total_sent + 1,
total_success = total_success + 1, total_success = total_success + 1,
{type_field} = {type_field} + 1, {type_field} = {type_field} + 1,
last_updated = CURRENT_TIMESTAMP last_updated = ?
WHERE id = 1 WHERE id = 1
""") """, (now_str,))
else: else:
cursor.execute(""" cursor.execute("""
UPDATE email_stats UPDATE email_stats
SET total_sent = total_sent + 1, SET total_sent = total_sent + 1,
total_success = total_success + 1, total_success = total_success + 1,
last_updated = CURRENT_TIMESTAMP last_updated = ?
WHERE id = 1 WHERE id = 1
""") """, (now_str,))
else: else:
cursor.execute(""" cursor.execute("""
UPDATE email_stats UPDATE email_stats
SET total_sent = total_sent + 1, SET total_sent = total_sent + 1,
total_failed = total_failed + 1, total_failed = total_failed + 1,
last_updated = CURRENT_TIMESTAMP last_updated = ?
WHERE id = 1 WHERE id = 1
""") """, (now_str,))
conn.commit() conn.commit()
@@ -1061,10 +1085,14 @@ def cleanup_email_logs(days: int = 30) -> int:
"""清理过期邮件日志""" """清理过期邮件日志"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cutoff = (datetime.now(BEIJING_TZ) - timedelta(days=int(days))).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute(
"""
DELETE FROM email_logs DELETE FROM email_logs
WHERE datetime(created_at) < datetime('now', '-' || ? || ' days') WHERE datetime(created_at) < datetime(?)
""", (days,)) """,
(cutoff,),
)
deleted = cursor.rowcount deleted = cursor.rowcount
conn.commit() conn.commit()
return deleted return deleted
@@ -1083,14 +1111,19 @@ def generate_email_token(email: str, token_type: str, user_id: int = None) -> st
EMAIL_TYPE_BIND: TOKEN_EXPIRE_BIND EMAIL_TYPE_BIND: TOKEN_EXPIRE_BIND
}.get(token_type, TOKEN_EXPIRE_REGISTER) }.get(token_type, TOKEN_EXPIRE_REGISTER)
expires_at = datetime.now() + timedelta(seconds=expire_seconds) now = datetime.now(BEIJING_TZ)
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
expires_at_str = (now + timedelta(seconds=expire_seconds)).strftime('%Y-%m-%d %H:%M:%S')
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(
INSERT INTO email_tokens (user_id, email, token, token_type, expires_at) """
VALUES (?, ?, ?, ?, ?) INSERT INTO email_tokens (user_id, email, token, token_type, expires_at, created_at)
""", (user_id, email, token, token_type, expires_at)) VALUES (?, ?, ?, ?, ?, ?)
""",
(user_id, email, token, token_type, expires_at_str, now_str),
)
conn.commit() conn.commit()
return token return token
@@ -1122,7 +1155,7 @@ def verify_email_token(token: str, token_type: str) -> Optional[Dict[str, Any]]:
return None return None
# 检查是否过期 # 检查是否过期
if parse_datetime(expires_at) < datetime.now(): if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
return None return None
# 标记为已使用 # 标记为已使用
@@ -1161,7 +1194,7 @@ def check_rate_limit(email: str, token_type: str) -> bool:
return True return True
last_sent = parse_datetime(row[0]) last_sent = parse_datetime(row[0])
elapsed = (datetime.now() - last_sent).total_seconds() elapsed = (datetime.now(BEIJING_TZ) - last_sent).total_seconds()
return elapsed >= limit_seconds return elapsed >= limit_seconds
@@ -1170,10 +1203,14 @@ def cleanup_expired_tokens() -> int:
"""清理过期Token""" """清理过期Token"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" now_str = get_beijing_now_str()
cursor.execute(
"""
DELETE FROM email_tokens DELETE FROM email_tokens
WHERE datetime(expires_at) < datetime('now') WHERE datetime(expires_at) < datetime(?)
""") """,
(now_str,),
)
deleted = cursor.rowcount deleted = cursor.rowcount
conn.commit() conn.commit()
return deleted return deleted
@@ -1436,7 +1473,7 @@ def verify_password_reset_token(token: str) -> Optional[Dict[str, Any]]:
return None return None
# 检查是否过期 # 检查是否过期
if parse_datetime(expires_at) < datetime.now(): if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
return None return None
return {'user_id': user_id, 'email': email, 'token_id': token_id} return {'user_id': user_id, 'email': email, 'token_id': token_id}
@@ -1827,7 +1864,7 @@ def send_task_complete_email(
return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0} return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0}
# 获取完成时间 # 获取完成时间
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') complete_time = get_beijing_now_str()
# 读取截图文件 # 读取截图文件
screenshot_data = None screenshot_data = None
@@ -2045,7 +2082,7 @@ def send_batch_task_complete_email(
return {'success': False, 'error': '没有截图需要发送'} return {'success': False, 'error': '没有截图需要发送'}
# 获取完成时间 # 获取完成时间
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') complete_time = get_beijing_now_str()
# 统计信息 # 统计信息
total_items_sum = sum(s.get('items', 0) for s in screenshots) total_items_sum = sum(s.get('items', 0) for s in screenshots)
@@ -2131,7 +2168,7 @@ def send_batch_task_complete_email(
else: else:
with open(zip_path, 'rb') as f: with open(zip_path, 'rb') as f:
zip_data = f.read() zip_data = f.read()
zip_filename = f"screenshots_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" zip_filename = f"screenshots_{datetime.now(BEIJING_TZ).strftime('%Y%m%d_%H%M%S')}.zip"
attachment_note = "截图已打包为ZIP附件请查收。" attachment_note = "截图已打包为ZIP附件请查收。"
except Exception as e: except Exception as e:
print(f"[邮件] 打包截图失败: {e}") print(f"[邮件] 打包截图失败: {e}")

View File

@@ -680,7 +680,7 @@ class PlaywrightAutomation:
切换浏览类型(带重试机制) 切换浏览类型(带重试机制)
Args: Args:
browse_type: 浏览类型(注册前未读/应读/已读 browse_type: 浏览类型(注册前未读/应读)
max_retries: 最大重试次数(默认2次) max_retries: 最大重试次数(默认2次)
Returns: Returns:

View File

@@ -1,24 +1,24 @@
{ {
"_datetime-CpkTDmvr.js": { "_datetime-ZCuLLiQt.js": {
"file": "assets/datetime-CpkTDmvr.js", "file": "assets/datetime-ZCuLLiQt.js",
"name": "datetime" "name": "datetime"
}, },
"_tasks-C2mQL6Tj.js": { "_tasks-BYcXDffp.js": {
"file": "assets/tasks-C2mQL6Tj.js", "file": "assets/tasks-BYcXDffp.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-5QCWoNsI.js": { "_users-Du3tLSHt.js": {
"file": "assets/users-5QCWoNsI.js", "file": "assets/users-Du3tLSHt.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"index.html": { "index.html": {
"file": "assets/index-CrrNPCqw.js", "file": "assets/index-C5w7EVNo.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -38,7 +38,7 @@
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-9I91QH6T.js", "file": "assets/AnnouncementsPage-C63j6LV5.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -50,7 +50,7 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-C0sjJZrc.js", "file": "assets/EmailPage-Bf6BbYPD.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -62,7 +62,7 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-B_IMe7WI.js", "file": "assets/FeedbacksPage-mOUifait.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -74,13 +74,13 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-Bvt31x2D.js", "file": "assets/LogsPage-DVfeUO3d.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-5QCWoNsI.js", "_users-Du3tLSHt.js",
"_tasks-C2mQL6Tj.js", "_tasks-BYcXDffp.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -88,21 +88,21 @@
] ]
}, },
"src/pages/PendingPage.vue": { "src/pages/PendingPage.vue": {
"file": "assets/PendingPage-Bs4BEacx.js", "file": "assets/PendingPage-BALptdIG.js",
"name": "PendingPage", "name": "PendingPage",
"src": "src/pages/PendingPage.vue", "src": "src/pages/PendingPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-5QCWoNsI.js", "_users-Du3tLSHt.js",
"index.html", "index.html",
"_datetime-CpkTDmvr.js" "_datetime-ZCuLLiQt.js"
], ],
"css": [ "css": [
"assets/PendingPage-C_mZDlzP.css" "assets/PendingPage-C_mZDlzP.css"
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-BA0VS3mc.js", "file": "assets/SettingsPage-BKf3hQvU.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -114,20 +114,20 @@
] ]
}, },
"src/pages/StatsPage.vue": { "src/pages/StatsPage.vue": {
"file": "assets/StatsPage-CyjgHApe.js", "file": "assets/StatsPage-DjylIGTc.js",
"name": "StatsPage", "name": "StatsPage",
"src": "src/pages/StatsPage.vue", "src": "src/pages/StatsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_tasks-C2mQL6Tj.js", "_tasks-BYcXDffp.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/StatsPage-B4w_lWWU.css" "assets/StatsPage-Bjh6A42Y.css"
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DMxUhCvv.js", "file": "assets/SystemPage-k6FhqNid.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -135,17 +135,17 @@
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/SystemPage-DC1VKbLw.css" "assets/SystemPage-b-S9OiVi.css"
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-DDSa1S98.js", "file": "assets/UsersPage-XRSMHsqH.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-5QCWoNsI.js", "_users-Du3tLSHt.js",
"_datetime-CpkTDmvr.js", "_datetime-ZCuLLiQt.js",
"index.html" "index.html"
], ],
"css": [ "css": [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{f as E,a as I,r as A}from"./users-5QCWoNsI.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-CrrNPCqw.js";import{p as L}from"./datetime-CpkTDmvr.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default}; import{f as E,a as I,r as A}from"./users-Du3tLSHt.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-C5w7EVNo.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};

View File

@@ -1 +1 @@
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-CrrNPCqw.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default}; import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-C5w7EVNo.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};

View File

@@ -1 +0,0 @@
.page-stack[data-v-50c34c25]{display:flex;flex-direction:column;gap:12px}.metric-card[data-v-50c34c25],.card[data-v-50c34c25]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.metric-label[data-v-50c34c25]{font-size:12px;color:var(--app-muted)}.metric-value[data-v-50c34c25]{margin-top:6px;font-size:18px;font-weight:800}.metric-sub[data-v-50c34c25]{margin-top:4px;font-size:12px}.section-head[data-v-50c34c25]{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}.section-title[data-v-50c34c25]{margin:0;font-size:14px;font-weight:800}.count-row[data-v-50c34c25]{margin-bottom:10px}.count-card[data-v-50c34c25]{border-radius:10px;border:1px solid var(--app-border)}.count-card.ok[data-v-50c34c25]{background:#10b98114}.count-card.warn[data-v-50c34c25]{background:#f59e0b14}.count-value[data-v-50c34c25]{font-size:22px;font-weight:900;line-height:1.1}.count-label[data-v-50c34c25]{margin-top:4px;font-size:12px;color:var(--app-muted)}.sub-title[data-v-50c34c25]{margin-top:14px;margin-bottom:8px;font-size:13px;font-weight:800}.empty[data-v-50c34c25]{padding:10px 0}.task-list[data-v-50c34c25]{display:flex;flex-direction:column;gap:8px}.task-item[data-v-50c34c25]{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:10px 12px;border-radius:10px;border:1px solid var(--app-border);background:#fff}.task-item.queue[data-v-50c34c25]{background:#f59e0b0f}.task-line[data-v-50c34c25]{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.task-line2[data-v-50c34c25]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:12px}.task-user[data-v-50c34c25]{font-weight:600}.task-account[data-v-50c34c25]{font-weight:700;color:#2563eb}.dot[data-v-50c34c25]{width:8px;height:8px;border-radius:999px;display:inline-block}.task-status[data-v-50c34c25]{font-weight:700}.task-right[data-v-50c34c25]{font-size:12px;font-weight:700;color:#10b981;white-space:nowrap}.task-right.warn[data-v-50c34c25]{color:#f59e0b}@media(max-width:768px){.task-item[data-v-50c34c25]{flex-direction:column}.task-right[data-v-50c34c25]{align-self:flex-end}}.stat-grid[data-v-50c34c25]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stat-box[data-v-50c34c25]{border-radius:12px;border:1px solid var(--app-border);padding:12px}.stat-box.ok[data-v-50c34c25]{background:#10b98114}.stat-box.err[data-v-50c34c25]{background:#ef444414}.stat-box.info[data-v-50c34c25]{background:#3b82f614}.stat-box.info2[data-v-50c34c25]{background:#06b6d414}.stat-name[data-v-50c34c25]{font-size:12px;font-weight:800;margin-bottom:6px}.stat-row[data-v-50c34c25]{display:flex;align-items:baseline;gap:8px}.stat-big[data-v-50c34c25]{font-size:20px;font-weight:900}.stat-row2[data-v-50c34c25]{margin-top:6px;font-size:12px}

View File

@@ -0,0 +1 @@
.page-stack[data-v-56211e34]{display:flex;flex-direction:column;gap:12px}.metric-card[data-v-56211e34],.card[data-v-56211e34]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.metric-label[data-v-56211e34]{font-size:12px;color:var(--app-muted)}.metric-value[data-v-56211e34]{margin-top:6px;font-size:18px;font-weight:800}.metric-sub[data-v-56211e34]{margin-top:4px;font-size:12px}.section-head[data-v-56211e34]{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}.section-title[data-v-56211e34]{margin:0;font-size:14px;font-weight:800}.count-row[data-v-56211e34]{margin-bottom:10px}.count-card[data-v-56211e34]{border-radius:10px;border:1px solid var(--app-border)}.count-card.ok[data-v-56211e34]{background:#10b98114}.count-card.warn[data-v-56211e34]{background:#f59e0b14}.count-value[data-v-56211e34]{font-size:22px;font-weight:900;line-height:1.1}.count-label[data-v-56211e34]{margin-top:4px;font-size:12px;color:var(--app-muted)}.sub-title[data-v-56211e34]{margin-top:14px;margin-bottom:8px;font-size:13px;font-weight:800}.empty[data-v-56211e34]{padding:10px 0}.task-list[data-v-56211e34]{display:flex;flex-direction:column;gap:8px}.task-item[data-v-56211e34]{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:10px 12px;border-radius:10px;border:1px solid var(--app-border);background:#fff}.task-item.queue[data-v-56211e34]{background:#f59e0b0f}.task-line[data-v-56211e34]{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.task-line2[data-v-56211e34]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:12px}.task-user[data-v-56211e34]{font-weight:600}.task-account[data-v-56211e34]{font-weight:700;color:#2563eb}.dot[data-v-56211e34]{width:8px;height:8px;border-radius:999px;display:inline-block}.task-status[data-v-56211e34]{font-weight:700}.task-right[data-v-56211e34]{font-size:12px;font-weight:700;color:#10b981;white-space:nowrap}.task-right.warn[data-v-56211e34]{color:#f59e0b}@media(max-width:768px){.task-item[data-v-56211e34]{flex-direction:column}.task-right[data-v-56211e34]{align-self:flex-end}}.stat-grid[data-v-56211e34]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stat-box[data-v-56211e34]{border-radius:12px;border:1px solid var(--app-border);padding:12px}.stat-box.ok[data-v-56211e34]{background:#10b98114}.stat-box.err[data-v-56211e34]{background:#ef444414}.stat-box.info[data-v-56211e34]{background:#3b82f614}.stat-box.info2[data-v-56211e34]{background:#06b6d414}.stat-name[data-v-56211e34]{font-size:12px;font-weight:800;margin-bottom:6px}.stat-row[data-v-56211e34]{display:flex;align-items:baseline;gap:8px}.stat-big[data-v-56211e34]{font-size:20px;font-weight:900}.stat-row2[data-v-56211e34]{margin-top:6px;font-size:12px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-6af756b3]{display:flex;flex-direction:column;gap:12px}.card[data-v-6af756b3]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-6af756b3]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-6af756b3]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-6af756b3]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-3a1fcdf4]{display:flex;flex-direction:column;gap:12px}.card[data-v-3a1fcdf4]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-3a1fcdf4]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-3a1fcdf4]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-3a1fcdf4]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
function i(t){if(!t)return null;if(t instanceof Date)return t;const e=String(t),r=e.includes("T")?e:e.replace(" ","T"),n=new Date(r);return Number.isNaN(n.getTime())?null:n}export{i as p};

View File

@@ -0,0 +1 @@
function s(n){if(!n)return null;if(n instanceof Date)return n;let e=String(n).trim();if(!e)return null;/^\d{4}-\d{2}-\d{2}$/.test(e)&&(e=`${e}T00:00:00`);let t=e.includes("T")?e:e.replace(" ","T");t=t.replace(/\.(\d{3})\d+/,".$1"),/([zZ]|[+-]\d{2}:\d{2})$/.test(t)||(t=`${t}+08:00`);const i=new Date(t);return Number.isNaN(i.getTime())?null:i}export{s as p};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{B as a}from"./index-CrrNPCqw.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{e as a,o as b,r as c,i as d,f as e,c as f}; import{B as a}from"./index-C5w7EVNo.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{e as a,o as b,r as c,i as d,f as e,c as f};

View File

@@ -1 +1 @@
import{B as a}from"./index-CrrNPCqw.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s}; import{B as a}from"./index-C5w7EVNo.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" /> <link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title> <title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-CrrNPCqw.js"></script> <script type="module" crossorigin src="./assets/index-C5w7EVNo.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css"> <link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
</head> </head>
<body> <body>

View File

@@ -1,13 +1,13 @@
{ {
"_accounts-B4fckN3X.js": { "_accounts-DuQjqW8V.js": {
"file": "assets/accounts-B4fckN3X.js", "file": "assets/accounts-DuQjqW8V.js",
"name": "accounts", "name": "accounts",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_auth-DhZn_bRf.js": { "_auth-C__02fQ5.js": {
"file": "assets/auth-DhZn_bRf.js", "file": "assets/auth-C__02fQ5.js",
"name": "auth", "name": "auth",
"imports": [ "imports": [
"index.html" "index.html"
@@ -18,7 +18,7 @@
"name": "password" "name": "password"
}, },
"index.html": { "index.html": {
"file": "assets/index-XYID9gNS.js", "file": "assets/index-2JnZbEa5.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -36,26 +36,26 @@
] ]
}, },
"src/pages/AccountsPage.vue": { "src/pages/AccountsPage.vue": {
"file": "assets/AccountsPage-D6CPzxlM.js", "file": "assets/AccountsPage-CugNoiiP.js",
"name": "AccountsPage", "name": "AccountsPage",
"src": "src/pages/AccountsPage.vue", "src": "src/pages/AccountsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-B4fckN3X.js", "_accounts-DuQjqW8V.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/AccountsPage-OUqse8pw.css" "assets/AccountsPage-DAtYSxiO.css"
] ]
}, },
"src/pages/LoginPage.vue": { "src/pages/LoginPage.vue": {
"file": "assets/LoginPage-DY0hfuOv.js", "file": "assets/LoginPage-8hxar7WW.js",
"name": "LoginPage", "name": "LoginPage",
"src": "src/pages/LoginPage.vue", "src": "src/pages/LoginPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-DhZn_bRf.js", "_auth-C__02fQ5.js",
"_password-7ryi82gE.js" "_password-7ryi82gE.js"
], ],
"css": [ "css": [
@@ -63,26 +63,26 @@
] ]
}, },
"src/pages/RegisterPage.vue": { "src/pages/RegisterPage.vue": {
"file": "assets/RegisterPage-CXbQlCO0.js", "file": "assets/RegisterPage-B_EUWOuP.js",
"name": "RegisterPage", "name": "RegisterPage",
"src": "src/pages/RegisterPage.vue", "src": "src/pages/RegisterPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-DhZn_bRf.js" "_auth-C__02fQ5.js"
], ],
"css": [ "css": [
"assets/RegisterPage-CVjBOq6i.css" "assets/RegisterPage-CVjBOq6i.css"
] ]
}, },
"src/pages/ResetPasswordPage.vue": { "src/pages/ResetPasswordPage.vue": {
"file": "assets/ResetPasswordPage-CQpLXPca.js", "file": "assets/ResetPasswordPage-BLEflhaq.js",
"name": "ResetPasswordPage", "name": "ResetPasswordPage",
"src": "src/pages/ResetPasswordPage.vue", "src": "src/pages/ResetPasswordPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-DhZn_bRf.js", "_auth-C__02fQ5.js",
"_password-7ryi82gE.js" "_password-7ryi82gE.js"
], ],
"css": [ "css": [
@@ -90,20 +90,20 @@
] ]
}, },
"src/pages/SchedulesPage.vue": { "src/pages/SchedulesPage.vue": {
"file": "assets/SchedulesPage-DIc25kqH.js", "file": "assets/SchedulesPage-BbA-BJek.js",
"name": "SchedulesPage", "name": "SchedulesPage",
"src": "src/pages/SchedulesPage.vue", "src": "src/pages/SchedulesPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-B4fckN3X.js", "_accounts-DuQjqW8V.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/SchedulesPage-Gu_OsDUd.css" "assets/SchedulesPage-s5SCMMmz.css"
] ]
}, },
"src/pages/ScreenshotsPage.vue": { "src/pages/ScreenshotsPage.vue": {
"file": "assets/ScreenshotsPage-BawNJkO2.js", "file": "assets/ScreenshotsPage-DCIxup8x.js",
"name": "ScreenshotsPage", "name": "ScreenshotsPage",
"src": "src/pages/ScreenshotsPage.vue", "src": "src/pages/ScreenshotsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -111,11 +111,11 @@
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/ScreenshotsPage-CRn_Qd8Q.css" "assets/ScreenshotsPage-DgLR6Xlu.css"
] ]
}, },
"src/pages/VerifyResultPage.vue": { "src/pages/VerifyResultPage.vue": {
"file": "assets/VerifyResultPage-C-GZLPnL.js", "file": "assets/VerifyResultPage-QQKdIo1L.js",
"name": "VerifyResultPage", "name": "VerifyResultPage",
"src": "src/pages/VerifyResultPage.vue", "src": "src/pages/VerifyResultPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page[data-v-374b34a5]{display:flex;flex-direction:column;gap:12px}.stat-card[data-v-374b34a5],.panel[data-v-374b34a5]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-label[data-v-374b34a5]{font-size:12px}.stat-value[data-v-374b34a5]{margin-top:6px;font-size:22px;font-weight:900;letter-spacing:.2px}.stat-suffix[data-v-374b34a5]{margin-left:6px;font-size:12px;font-weight:600}.upgrade-banner[data-v-374b34a5]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.upgrade-actions[data-v-374b34a5]{margin-top:10px}.panel-head[data-v-374b34a5]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-374b34a5]{font-size:16px;font-weight:900}.panel-actions[data-v-374b34a5]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.toolbar[data-v-374b34a5]{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:10px;border:1px dashed rgba(17,24,39,.14);border-radius:12px;background:#f6f7fb99}.toolbar-left[data-v-374b34a5],.toolbar-middle[data-v-374b34a5],.toolbar-right[data-v-374b34a5]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.toolbar-right[data-v-374b34a5]{margin-left:auto;justify-content:flex-end}.grid[data-v-374b34a5]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.account-card[data-v-374b34a5]{border-radius:14px;border:1px solid var(--app-border)}.card-top[data-v-374b34a5]{display:flex;gap:10px}.card-check[data-v-374b34a5]{padding-top:2px}.card-main[data-v-374b34a5]{min-width:0;flex:1}.card-title[data-v-374b34a5]{display:flex;align-items:center;justify-content:space-between;gap:10px}.card-name[data-v-374b34a5]{font-size:14px;font-weight:900;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-sub[data-v-374b34a5]{margin-top:6px;font-size:12px;line-height:1.4;word-break:break-word}.progress[data-v-374b34a5]{margin-top:12px}.progress-meta[data-v-374b34a5]{margin-top:6px;display:flex;justify-content:space-between;gap:10px;font-size:12px}.card-controls[data-v-374b34a5]{margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.card-buttons[data-v-374b34a5]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}.vip-body[data-v-374b34a5]{padding:12px 0 0}.vip-tip[data-v-374b34a5]{margin-top:10px;font-size:13px;line-height:1.6}@media(max-width:480px){.grid[data-v-374b34a5]{grid-template-columns:1fr}}

View File

@@ -1 +0,0 @@
.page[data-v-cd2eb01a]{display:flex;flex-direction:column;gap:12px}.stat-card[data-v-cd2eb01a],.panel[data-v-cd2eb01a]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-label[data-v-cd2eb01a]{font-size:12px}.stat-value[data-v-cd2eb01a]{margin-top:6px;font-size:22px;font-weight:900;letter-spacing:.2px}.stat-suffix[data-v-cd2eb01a]{margin-left:6px;font-size:12px;font-weight:600}.upgrade-banner[data-v-cd2eb01a]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.upgrade-actions[data-v-cd2eb01a]{margin-top:10px}.panel-head[data-v-cd2eb01a]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-cd2eb01a]{font-size:16px;font-weight:900}.panel-actions[data-v-cd2eb01a]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.toolbar[data-v-cd2eb01a]{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:10px;border:1px dashed rgba(17,24,39,.14);border-radius:12px;background:#f6f7fb99}.toolbar-left[data-v-cd2eb01a],.toolbar-middle[data-v-cd2eb01a],.toolbar-right[data-v-cd2eb01a]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.toolbar-right[data-v-cd2eb01a]{margin-left:auto;justify-content:flex-end}.grid[data-v-cd2eb01a]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.account-card[data-v-cd2eb01a]{border-radius:14px;border:1px solid var(--app-border)}.card-top[data-v-cd2eb01a]{display:flex;gap:10px}.card-check[data-v-cd2eb01a]{padding-top:2px}.card-main[data-v-cd2eb01a]{min-width:0;flex:1}.card-title[data-v-cd2eb01a]{display:flex;align-items:center;justify-content:space-between;gap:10px}.card-name[data-v-cd2eb01a]{font-size:14px;font-weight:900;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-sub[data-v-cd2eb01a]{margin-top:6px;font-size:12px;line-height:1.4;word-break:break-word}.progress[data-v-cd2eb01a]{margin-top:12px}.progress-meta[data-v-cd2eb01a]{margin-top:6px;display:flex;justify-content:space-between;gap:10px;font-size:12px}.card-controls[data-v-cd2eb01a]{margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.card-buttons[data-v-cd2eb01a]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}.vip-body[data-v-cd2eb01a]{padding:12px 0 0}.vip-tip[data-v-cd2eb01a]{margin-top:10px;font-size:13px;line-height:1.6}@media(max-width:480px){.grid[data-v-cd2eb01a]{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-XYID9gNS.js";import{g as z,f as F,c as G}from"./auth-DhZn_bRf.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),b=p(""),h=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于接收审核通知");async function w(){try{const u=await z();b.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{b.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}h.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:b.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{h.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:h.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-32684b4d"]]);export{ae as default}; import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-2JnZbEa5.js";import{g as z,f as F,c as G}from"./auth-C__02fQ5.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),b=p(""),h=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于接收审核通知");async function w(){try{const u=await z();b.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{b.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}h.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:b.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{h.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:h.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-32684b4d"]]);export{ae as default};

View File

@@ -1 +1 @@
import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-XYID9gNS.js";import{d as H}from"./auth-DhZn_bRf.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default}; import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-2JnZbEa5.js";import{d as H}from"./auth-C__02fQ5.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page[data-v-ee36bae1]{display:flex;flex-direction:column;gap:12px}.vip-alert[data-v-ee36bae1]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.vip-actions[data-v-ee36bae1]{margin-top:10px}.panel[data-v-ee36bae1]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.panel-head[data-v-ee36bae1]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-ee36bae1]{font-size:16px;font-weight:900}.panel-actions[data-v-ee36bae1]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.grid[data-v-ee36bae1]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.schedule-card[data-v-ee36bae1]{border-radius:14px;border:1px solid var(--app-border)}.schedule-top[data-v-ee36bae1]{display:flex;justify-content:space-between;gap:12px}.schedule-main[data-v-ee36bae1]{min-width:0;flex:1}.schedule-title[data-v-ee36bae1]{display:flex;align-items:center;justify-content:space-between;gap:10px}.schedule-name[data-v-ee36bae1]{font-size:14px;font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.schedule-meta[data-v-ee36bae1]{margin-top:6px;display:flex;gap:10px;flex-wrap:wrap;font-size:12px}.schedule-actions[data-v-ee36bae1]{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}.logs[data-v-ee36bae1]{display:flex;flex-direction:column;gap:10px}.log-card[data-v-ee36bae1]{border-radius:12px;border:1px solid var(--app-border)}.log-head[data-v-ee36bae1]{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px}.log-body[data-v-ee36bae1]{margin-top:8px;font-size:13px;line-height:1.6}.log-error[data-v-ee36bae1]{margin-top:6px;color:#b91c1c}.vip-body[data-v-ee36bae1]{padding:12px 0 0}.vip-tip[data-v-ee36bae1]{margin-top:10px;font-size:13px;line-height:1.6}@media(max-width:480px){.grid[data-v-ee36bae1]{grid-template-columns:1fr}}

View File

@@ -0,0 +1 @@
.page[data-v-bfae6f4f]{display:flex;flex-direction:column;gap:12px}.vip-alert[data-v-bfae6f4f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.vip-actions[data-v-bfae6f4f]{margin-top:10px}.panel[data-v-bfae6f4f]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.panel-head[data-v-bfae6f4f]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-bfae6f4f]{font-size:16px;font-weight:900}.panel-actions[data-v-bfae6f4f]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.grid[data-v-bfae6f4f]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.schedule-card[data-v-bfae6f4f]{border-radius:14px;border:1px solid var(--app-border)}.schedule-top[data-v-bfae6f4f]{display:flex;justify-content:space-between;gap:12px}.schedule-main[data-v-bfae6f4f]{min-width:0;flex:1}.schedule-title[data-v-bfae6f4f]{display:flex;align-items:center;justify-content:space-between;gap:10px}.schedule-name[data-v-bfae6f4f]{font-size:14px;font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.schedule-meta[data-v-bfae6f4f]{margin-top:6px;display:flex;gap:10px;flex-wrap:wrap;font-size:12px}.schedule-actions[data-v-bfae6f4f]{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}.logs[data-v-bfae6f4f]{display:flex;flex-direction:column;gap:10px}.log-card[data-v-bfae6f4f]{border-radius:12px;border:1px solid var(--app-border)}.log-head[data-v-bfae6f4f]{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px}.log-body[data-v-bfae6f4f]{margin-top:8px;font-size:13px;line-height:1.6}.log-error[data-v-bfae6f4f]{margin-top:6px;color:#b91c1c}.vip-body[data-v-bfae6f4f]{padding:12px 0 0}.vip-tip[data-v-bfae6f4f]{margin-top:10px;font-size:13px;line-height:1.6}@media(max-width:480px){.grid[data-v-bfae6f4f]{grid-template-columns:1fr}}

View File

@@ -1 +0,0 @@
import{p as b,_ as M,a as m,o as N,h as w,w as o,e as y,f as i,g as s,b as C,d as c,k as f,F as E,v as R,t as T,x as I,E as l}from"./index-XYID9gNS.js";async function D(){const{data:d}=await b.get("/screenshots");return d}async function F(d){const{data:u}=await b.delete(`/screenshots/${encodeURIComponent(d)}`);return u}async function H(){const{data:d}=await b.post("/screenshots/clear",{});return d}const L={class:"panel-head"},O={class:"panel-actions"},W={key:1,class:"grid"},j=["src","alt","onClick"],q={class:"shot-body"},G=["title"],J={class:"shot-meta app-muted"},K={class:"shot-actions"},Q={class:"preview"},X=["src","alt"],Y={__name:"ScreenshotsPage",setup(d){const u=m(!1),r=m([]),p=m(!1),g=m(""),h=m("");function _(t){return`/screenshots/${encodeURIComponent(t)}`}async function x(){u.value=!0;try{const t=await D();r.value=Array.isArray(t)?t:[]}catch(t){t?.response?.status===401&&(window.location.href="/login"),r.value=[]}finally{u.value=!1}}function S(t){h.value=t.display_name||t.filename||"截图预览",g.value=_(t.filename),p.value=!0}async function U(){try{await I.confirm("确定要清空全部截图吗?","清空截图",{confirmButtonText:"清空",cancelButtonText:"取消",type:"warning"})}catch{return}try{const t=await H();if(t?.success){l.success(`已清空(删除 ${t?.deleted||0} 张)`),r.value=[],p.value=!1;return}l.error(t?.error||"操作失败")}catch(t){const e=t?.response?.data;l.error(e?.error||"操作失败")}}async function V(t){try{await I.confirm(`确定要删除截图「${t.display_name||t.filename}」吗?`,"删除截图",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await F(t.filename);if(e?.success){r.value=r.value.filter(a=>a.filename!==t.filename),g.value.includes(encodeURIComponent(t.filename))&&(p.value=!1),l.success("已删除");return}l.error(e?.error||"删除失败")}catch(e){const a=e?.response?.data;l.error(a?.error||"删除失败")}}async function z(t){const e=_(t.filename);if(!navigator.clipboard||typeof navigator.clipboard.write!="function"||typeof window.ClipboardItem>"u"){l.warning("当前环境不支持复制图片(建议使用 Chrome/Edge 并通过 HTTPS 访问);可用“下载”。");return}try{const a=await fetch(e,{credentials:"include"});if(!a.ok)throw new Error("fetch_failed");const v=await a.blob();if(!(v.type||"").startsWith("image/"))throw new Error("not_image");await navigator.clipboard.write([new ClipboardItem({[v.type]:v})]),l.success("图片已复制")}catch{l.warning("复制图片失败:请确认允许剪贴板权限;可用“下载”。")}}function A(t){const e=document.createElement("a");e.href=_(t.filename),e.download=t.display_name||t.filename,document.body.appendChild(e),e.click(),e.remove()}return N(x),(t,e)=>{const a=y("el-button"),v=y("el-skeleton"),B=y("el-empty"),$=y("el-card"),P=y("el-dialog");return i(),w($,{shadow:"never",class:"panel","body-style":{padding:"14px"}},{default:o(()=>[s("div",L,[e[4]||(e[4]=s("div",{class:"panel-title"},"截图管理",-1)),s("div",O,[c(a,{loading:u.value,onClick:x},{default:o(()=>[...e[2]||(e[2]=[f("刷新",-1)])]),_:1},8,["loading"]),c(a,{type:"danger",plain:"",disabled:r.value.length===0,onClick:U},{default:o(()=>[...e[3]||(e[3]=[f("清空全部",-1)])]),_:1},8,["disabled"])])]),u.value?(i(),w(v,{key:0,rows:6,animated:""})):(i(),C(E,{key:1},[r.value.length===0?(i(),w(B,{key:0,description:"暂无截图"})):(i(),C("div",W,[(i(!0),C(E,null,R(r.value,n=>(i(),w($,{key:n.filename,shadow:"never",class:"shot-card","body-style":{padding:"0"}},{default:o(()=>[s("img",{class:"shot-img",src:_(n.filename),alt:n.display_name||n.filename,loading:"lazy",onClick:k=>S(n)},null,8,j),s("div",q,[s("div",{class:"shot-name",title:n.display_name||n.filename},T(n.display_name||n.filename),9,G),s("div",J,T(n.created||""),1),s("div",K,[c(a,{size:"small",text:"",type:"primary",onClick:k=>z(n)},{default:o(()=>[...e[5]||(e[5]=[f("复制图片",-1)])]),_:1},8,["onClick"]),c(a,{size:"small",text:"",onClick:k=>A(n)},{default:o(()=>[...e[6]||(e[6]=[f("下载",-1)])]),_:1},8,["onClick"]),c(a,{size:"small",text:"",type:"danger",onClick:k=>V(n)},{default:o(()=>[...e[7]||(e[7]=[f("删除",-1)])]),_:1},8,["onClick"])])])]),_:2},1024))),128))]))],64)),c(P,{modelValue:p.value,"onUpdate:modelValue":e[1]||(e[1]=n=>p.value=n),title:h.value,width:"min(920px, 94vw)"},{footer:o(()=>[c(a,{onClick:e[0]||(e[0]=n=>p.value=!1)},{default:o(()=>[...e[8]||(e[8]=[f("关闭",-1)])]),_:1})]),default:o(()=>[s("div",Q,[s("img",{src:g.value,alt:h.value,class:"preview-img"},null,8,X)])]),_:1},8,["modelValue","title"])]),_:1})}}},ee=M(Y,[["__scopeId","data-v-239daac5"]]);export{ee as default};

View File

@@ -1 +0,0 @@
.panel[data-v-239daac5]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.panel-head[data-v-239daac5]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-239daac5]{font-size:16px;font-weight:900}.panel-actions[data-v-239daac5]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.grid[data-v-239daac5]{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}.shot-card[data-v-239daac5]{border-radius:14px;border:1px solid var(--app-border);overflow:hidden}.shot-img[data-v-239daac5]{width:100%;aspect-ratio:16/9;object-fit:cover;cursor:pointer;display:block}.shot-body[data-v-239daac5]{padding:12px}.shot-name[data-v-239daac5]{font-size:13px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shot-meta[data-v-239daac5]{margin-top:4px;font-size:12px}.shot-actions[data-v-239daac5]{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px}.preview[data-v-239daac5]{display:flex;justify-content:center}.preview-img[data-v-239daac5]{max-width:100%;max-height:78vh;object-fit:contain;border-radius:10px;border:1px solid var(--app-border);background:#fff}@media(max-width:480px){.grid[data-v-239daac5]{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.panel[data-v-4871f4ca]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.panel-head[data-v-4871f4ca]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-4871f4ca]{font-size:16px;font-weight:900}.panel-actions[data-v-4871f4ca]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.grid[data-v-4871f4ca]{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}.shot-card[data-v-4871f4ca]{border-radius:14px;border:1px solid var(--app-border);overflow:hidden}.shot-img[data-v-4871f4ca]{width:100%;aspect-ratio:16/9;object-fit:cover;cursor:pointer;display:block}.shot-body[data-v-4871f4ca]{padding:12px}.shot-name[data-v-4871f4ca]{font-size:13px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shot-meta[data-v-4871f4ca]{margin-top:4px;font-size:12px}.shot-actions[data-v-4871f4ca]{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px}.preview[data-v-4871f4ca]{display:flex;justify-content:center}.preview-img[data-v-4871f4ca]{max-width:100%;max-height:78vh;object-fit:contain;border-radius:10px;border:1px solid var(--app-border);background:#fff}@media(max-width:480px){.grid[data-v-4871f4ca]{grid-template-columns:1fr}}

View File

@@ -1 +1 @@
import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-XYID9gNS.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default}; import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-2JnZbEa5.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default};

View File

@@ -1 +1 @@
import{p as c}from"./index-XYID9gNS.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u}; import{p as c}from"./index-2JnZbEa5.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u};

View File

@@ -1 +1 @@
import{p as s}from"./index-XYID9gNS.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r}; import{p as s}from"./index-2JnZbEa5.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r};

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>知识管理平台</title> <title>知识管理平台</title>
<script type="module" crossorigin src="./assets/index-XYID9gNS.js"></script> <script type="module" crossorigin src="./assets/index-2JnZbEa5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css"> <link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css">
</head> </head>
<body> <body>

View File

@@ -7,11 +7,18 @@
4. 智能重试机制 4. 智能重试机制
""" """
import time import time
import json import json
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
import db_pool import db_pool
import pytz
# 北京时区(统一)
CST_TZ = pytz.timezone("Asia/Shanghai")
def get_cst_now_str():
return datetime.now(CST_TZ).strftime('%Y-%m-%d %H:%M:%S')
class TaskStage(Enum): class TaskStage(Enum):
"""任务执行阶段""" """任务执行阶段"""
@@ -103,27 +110,28 @@ class TaskCheckpoint:
conn.commit() conn.commit()
def create_checkpoint(self, user_id, account_id, username, browse_type): def create_checkpoint(self, user_id, account_id, username, browse_type):
"""创建新的任务断点""" """创建新的任务断点"""
task_id = f"{user_id}:{account_id}:{int(time.time())}" task_id = f"{user_id}:{account_id}:{int(time.time())}"
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cst_time = get_cst_now_str()
INSERT INTO task_checkpoints cursor.execute("""
(task_id, user_id, account_id, username, browse_type, stage, status) INSERT INTO task_checkpoints
VALUES (?, ?, ?, ?, ?, ?, ?) (task_id, user_id, account_id, username, browse_type, stage, status, created_at, updated_at)
""", (task_id, user_id, account_id, username, browse_type, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
TaskStage.QUEUED.value, 'running')) """, (task_id, user_id, account_id, username, browse_type,
conn.commit() TaskStage.QUEUED.value, 'running', cst_time, cst_time))
return task_id conn.commit()
return task_id
def update_stage(self, task_id, stage, progress_percent=None, checkpoint_data=None): def update_stage(self, task_id, stage, progress_percent=None, checkpoint_data=None):
"""更新任务阶段""" """更新任务阶段"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
updates = ['stage = ?', 'updated_at = CURRENT_TIMESTAMP'] updates = ['stage = ?', 'updated_at = ?']
params = [stage.value if isinstance(stage, TaskStage) else stage] params = [stage.value if isinstance(stage, TaskStage) else stage, get_cst_now_str()]
if progress_percent is not None: if progress_percent is not None:
updates.append('progress_percent = ?') updates.append('progress_percent = ?')
@@ -155,8 +163,8 @@ class TaskCheckpoint:
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
updates = ['updated_at = CURRENT_TIMESTAMP'] updates = ['updated_at = ?']
params = [] params = [get_cst_now_str()]
for key in ['current_page', 'total_pages', 'processed_items', 'downloaded_files']: for key in ['current_page', 'total_pages', 'processed_items', 'downloaded_files']:
if key in kwargs: if key in kwargs:
@@ -178,10 +186,11 @@ class TaskCheckpoint:
""", params) """, params)
conn.commit() conn.commit()
def record_error(self, task_id, error_message, pause=False): def record_error(self, task_id, error_message, pause=False):
"""记录错误并决定是否暂停任务""" """记录错误并决定是否暂停任务"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cst_time = get_cst_now_str()
# 获取当前重试次数和最大重试次数 # 获取当前重试次数和最大重试次数
cursor.execute(""" cursor.execute("""
@@ -199,50 +208,51 @@ class TaskCheckpoint:
# 判断是否超过最大重试次数 # 判断是否超过最大重试次数
if retry_count >= max_retries or pause: if retry_count >= max_retries or pause:
# 超过重试次数,暂停任务等待人工处理 # 超过重试次数,暂停任务等待人工处理
cursor.execute(""" cursor.execute("""
UPDATE task_checkpoints UPDATE task_checkpoints
SET status = 'paused', SET status = 'paused',
stage = ?, stage = ?,
retry_count = ?, retry_count = ?,
error_count = ?, error_count = ?,
last_error = ?, last_error = ?,
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE task_id = ? WHERE task_id = ?
""", (TaskStage.PAUSED.value, retry_count, error_count, """, (TaskStage.PAUSED.value, retry_count, error_count,
error_message, task_id)) error_message, cst_time, task_id))
conn.commit() conn.commit()
return 'paused' return 'paused'
else: else:
# 还可以重试 # 还可以重试
cursor.execute(""" cursor.execute("""
UPDATE task_checkpoints UPDATE task_checkpoints
SET retry_count = ?, SET retry_count = ?,
error_count = ?, error_count = ?,
last_error = ?, last_error = ?,
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE task_id = ? WHERE task_id = ?
""", (retry_count, error_count, error_message, task_id)) """, (retry_count, error_count, error_message, cst_time, task_id))
conn.commit() conn.commit()
return 'retry' return 'retry'
return 'unknown' return 'unknown'
def complete_task(self, task_id, success=True): def complete_task(self, task_id, success=True):
"""完成任务""" """完成任务"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cst_time = get_cst_now_str()
UPDATE task_checkpoints cursor.execute("""
SET status = ?, UPDATE task_checkpoints
stage = ?, SET status = ?,
progress_percent = 100, stage = ?,
completed_at = CURRENT_TIMESTAMP, progress_percent = 100,
updated_at = CURRENT_TIMESTAMP completed_at = ?,
WHERE task_id = ? updated_at = ?
""", ('completed' if success else 'failed', WHERE task_id = ?
TaskStage.COMPLETED.value if success else TaskStage.FAILED.value, """, ('completed' if success else 'failed',
task_id)) TaskStage.COMPLETED.value if success else TaskStage.FAILED.value,
conn.commit() cst_time, cst_time, task_id))
conn.commit()
def get_checkpoint(self, task_id): def get_checkpoint(self, task_id):
"""获取任务断点信息""" """获取任务断点信息"""
@@ -323,47 +333,49 @@ class TaskCheckpoint:
}) })
return tasks return tasks
def resume_task(self, task_id): def resume_task(self, task_id):
"""恢复暂停的任务""" """恢复暂停的任务"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cst_time = get_cst_now_str()
UPDATE task_checkpoints cursor.execute("""
SET status = 'running', UPDATE task_checkpoints
retry_count = 0, SET status = 'running',
updated_at = CURRENT_TIMESTAMP retry_count = 0,
WHERE task_id = ? AND status = 'paused' updated_at = ?
""", (task_id,)) WHERE task_id = ? AND status = 'paused'
conn.commit() """, (cst_time, task_id))
return cursor.rowcount > 0 conn.commit()
return cursor.rowcount > 0
def abandon_task(self, task_id): def abandon_task(self, task_id):
"""放弃暂停的任务""" """放弃暂停的任务"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cst_time = get_cst_now_str()
UPDATE task_checkpoints cursor.execute("""
SET status = 'failed', UPDATE task_checkpoints
stage = ?, SET status = 'failed',
completed_at = CURRENT_TIMESTAMP, stage = ?,
updated_at = CURRENT_TIMESTAMP completed_at = ?,
WHERE task_id = ? AND status = 'paused' updated_at = ?
""", (TaskStage.FAILED.value, task_id)) WHERE task_id = ? AND status = 'paused'
conn.commit() """, (TaskStage.FAILED.value, cst_time, cst_time, task_id))
return cursor.rowcount > 0 conn.commit()
return cursor.rowcount > 0
def cleanup_old_checkpoints(self, days=7): def cleanup_old_checkpoints(self, days=7):
"""清理旧的断点数据(保留最近N天)""" """清理旧的断点数据(保留最近N天)"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
DELETE FROM task_checkpoints DELETE FROM task_checkpoints
WHERE status IN ('completed', 'failed') WHERE status IN ('completed', 'failed')
AND datetime(completed_at) < datetime('now', '-' || ? || ' days') AND datetime(completed_at) < datetime('now', 'localtime', '-' || ? || ' days')
""", (days,)) """, (days,))
deleted = cursor.rowcount deleted = cursor.rowcount
conn.commit() conn.commit()
return deleted return deleted
# 全局单例 # 全局单例

View File

@@ -842,7 +842,6 @@
<select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;"> <select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
<option value="注册前未读">注册前未读</option> <option value="注册前未读">注册前未读</option>
<option value="应读" selected>应读</option> <option value="应读" selected>应读</option>
<option value="未读">未读</option>
</select> </select>
</div> </div>
@@ -1731,10 +1730,23 @@
} }
// VIP functions // VIP functions
function parseBeijingDateTime(value) {
if (!value) return null;
const str = String(value).trim();
if (!str) return null;
let iso = str.includes('T') ? str : str.replace(' ', 'T');
// 统一按北京时间解析(除非字符串本身已带时区)
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso);
if (!hasTimezone) iso = iso + '+08:00';
const dt = new Date(iso);
if (Number.isNaN(dt.getTime())) return null;
return dt;
}
function isVip(user) { function isVip(user) {
if (!user.vip_expire_time) return false; if (!user.vip_expire_time) return false;
const expireTime = new Date(user.vip_expire_time); const expireTime = parseBeijingDateTime(user.vip_expire_time);
return expireTime > new Date(); return expireTime ? expireTime > new Date() : false;
} }
function getVipBadge(user) { function getVipBadge(user) {
@@ -1746,8 +1758,8 @@
function getVipExpire(user) { function getVipExpire(user) {
if (!isVip(user)) return ''; if (!isVip(user)) return '';
const expireTime = new Date(user.vip_expire_time); const expireTime = parseBeijingDateTime(user.vip_expire_time);
const daysLeft = Math.ceil((expireTime - new Date()) / (1000*60*60*24)); const daysLeft = expireTime ? Math.ceil((expireTime - new Date()) / (1000*60*60*24)) : 0;
if (user.vip_expire_time === '2099-12-31 23:59:59') { if (user.vip_expire_time === '2099-12-31 23:59:59') {
return '<div class="user-info" style="color:#667eea;">永久VIP</div>'; return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
} }

View File

@@ -556,7 +556,6 @@
<div class="toolbar-group"> <div class="toolbar-group">
<select class="select-inline" id="batchBrowseType"> <select class="select-inline" id="batchBrowseType">
<option value="应读">应读</option> <option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option> <option value="注册前未读">注册前未读</option>
</select> </select>
<label class="switch"> <label class="switch">
@@ -718,7 +717,6 @@
<label class="form-label">浏览类型</label> <label class="form-label">浏览类型</label>
<select class="form-select" id="scheduleBrowseType"> <select class="form-select" id="scheduleBrowseType">
<option value="应读">应读</option> <option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option> <option value="注册前未读">注册前未读</option>
</select> </select>
</div> </div>
@@ -1313,7 +1311,7 @@
'</div>' + progressHtml + '</div>' + progressHtml +
'<div class="account-card-actions">' + '<div class="account-card-actions">' +
'<select class="select-inline browse-type" data-id="' + acc.id + '">' + '<select class="select-inline browse-type" data-id="' + acc.id + '">' +
'<option value="应读">应读</option><option value="未读">未读</option><option value="注册前未读">注册前未读</option>' + '<option value="应读">应读</option><option value="注册前未读">注册前未读</option>' +
'</select>' + '</select>' +
'<button class="btn btn-primary btn-small" onclick="startAccount(\'' + acc.id + '\')" ' + (isRunning ? 'disabled' : '') + '>启动</button>' + '<button class="btn btn-primary btn-small" onclick="startAccount(\'' + acc.id + '\')" ' + (isRunning ? 'disabled' : '') + '>启动</button>' +
'<button class="btn btn-outlined btn-small" onclick="stopAccount(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + '>停止</button>' + '<button class="btn btn-outlined btn-small" onclick="stopAccount(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + '>停止</button>' +
@@ -1868,7 +1866,6 @@
function copyScreenshotImage(imgSrc) { function copyScreenshotImage(imgSrc) {
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() { img.onload = function() {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth; canvas.width = img.naturalWidth;