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. 定时任务
- **启用定时浏览**: 是/否
- **执行时间**: 02:00 (CST时间)
- **浏览类型**: 应读/注册前未读/未读
- **浏览类型**: 应读/注册前未读
- **执行日期**: 周一到周日
### 3. 代理配置

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,22 @@ export function parseSqliteDateTime(value) {
if (!value) return null
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"
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)
if (Number.isNaN(date.getTime())) return null
return date
@@ -14,4 +27,3 @@ export function formatDateTime(value) {
if (!value) return '-'
return String(value)
}

View File

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

View File

@@ -59,7 +59,6 @@ const editForm = reactive({
const browseTypeOptions = [
{ label: '应读', value: '应读' },
{ label: '未读', value: '未读' },
{ label: '注册前未读', value: '注册前未读' },
]
@@ -80,8 +79,13 @@ function normalizeAccountPayload(acc) {
}
function replaceAccounts(list) {
for (const key of Object.keys(accountsById)) delete accountsById[key]
for (const acc of list || []) normalizeAccountPayload(acc)
const nextList = Array.isArray(list) ? list : []
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() {
@@ -138,8 +142,9 @@ function showRuntimeProgress(acc) {
return true
}
async function refreshStats() {
statsLoading.value = true
async function refreshStats(options = {}) {
const silent = Boolean(options?.silent)
if (!silent) statsLoading.value = true
try {
const data = await fetchRunStats()
stats.today_completed = Number(data?.today_completed || 0)
@@ -150,7 +155,7 @@ async function refreshStats() {
} catch (e) {
if (e?.response?.status === 401) window.location.href = '/login'
} finally {
statsLoading.value = false
if (!silent) statsLoading.value = false
}
}
@@ -456,6 +461,41 @@ function bindSocket() {
let unbindSocket = 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 () => {
if (!userStore.vipInfo) {
userStore.refreshVipInfo().catch(() => {
@@ -467,12 +507,12 @@ onMounted(async () => {
await refreshAccounts()
await refreshStats()
statsTimer = window.setInterval(refreshStats, 10_000)
syncStatsPolling()
})
onBeforeUnmount(() => {
if (unbindSocket) unbindSocket()
if (statsTimer) window.clearInterval(statsTimer)
stopStatsPolling()
})
</script>
@@ -565,7 +605,7 @@ onBeforeUnmount(() => {
</div>
</div>
<el-skeleton v-if="loading || statsLoading" :rows="5" animated />
<el-skeleton v-if="loading" :rows="5" animated />
<template v-else>
<el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" />
<div v-else class="grid">

View File

@@ -45,10 +45,14 @@ const form = reactive({
const browseTypeOptions = [
{ label: '应读', value: '应读' },
{ label: '未读', value: '未读' },
{ label: '注册前未读', value: '注册前未读' },
]
function normalizeBrowseType(value) {
if (String(value) === '注册前未读') return '注册前未读'
return '应读'
}
const weekdayOptions = [
{ label: '周一', value: '1' },
{ label: '周二', value: '2' },
@@ -92,7 +96,11 @@ async function loadAccounts() {
async function loadSchedules() {
loading.value = true
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) {
if (e?.response?.status === 401) window.location.href = '/login'
schedules.value = []
@@ -121,7 +129,7 @@ function openEdit(schedule) {
.filter(Boolean)
.map((v) => String(v))
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.account_ids = Array.isArray(schedule.account_ids) ? schedule.account_ids.slice() : []
editorOpen.value = true

View File

@@ -34,6 +34,87 @@ function openPreview(item) {
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() {
try {
await ElMessageBox.confirm('确定要清空全部截图吗?', '清空截图', {
@@ -98,14 +179,28 @@ async function copyImage(item) {
}
try {
const resp = await fetch(url, { credentials: 'include' })
if (!resp.ok) throw new Error('fetch_failed')
const blob = await resp.blob()
const mime = blob.type || ''
if (!mime.startsWith('image/')) throw new Error('not_image')
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })])
ElMessage.success('图片已复制')
// 关键点:用 Promise 形式的数据源,让 clipboard.write 在用户手势内立即发生(更稳)
try {
await navigator.clipboard.write([
new ClipboardItem({
'image/png': screenshotUrlToPngBlob(url, item.filename),
}),
])
} 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('复制图片失败:请确认允许剪贴板权限;可用“下载”。')
}
}
@@ -138,7 +233,14 @@ onMounted(load)
<div v-else class="grid">
<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-name" :title="item.display_name || item.filename">{{ item.display_name || item.filename }}</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)
# ========== 浏览类型规范化 ==========
# 当前页面仅保留:应读、注册前未读;默认应读
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()
@@ -868,7 +896,7 @@ class Account:
self.total_items = 0
self.total_attachments = 0
self.automation = None
self.last_browse_type = "注册前未读"
self.last_browse_type = BROWSE_TYPE_SHOULD_READ
self.proxy_config = None # 保存代理配置,浏览和截图共用
@property
@@ -2678,8 +2706,10 @@ def start_account(account_id):
if account.is_running:
return jsonify({"error": "任务已在运行中"}), 400
data = request.json
browse_type = data.get('browse_type', '应读')
data = request.json or {}
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) # 默认启用截图
# 确保浏览器管理器已初始化
@@ -3278,10 +3308,8 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
base = f"{parsed.scheme}://{parsed.netloc}"
if '注册前' in str(browse_type):
bz = 0
elif str(browse_type) == '已读':
bz = 1
else:
bz = 2 # 应读/未读
bz = 2 # 应读
target_url = f"{base}/admin/center.aspx?bz={bz}"
automation.main_page.goto(target_url, timeout=60000)
current_url = getattr(automation.main_page, "url", "") or ""
@@ -3461,7 +3489,14 @@ def manual_screenshot(account_id):
return jsonify({"error": "任务运行中,无法截图"}), 400
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
@@ -3911,10 +3946,12 @@ def get_run_stats():
# 获取今日任务统计
stats = database.get_user_run_stats(user_id)
# 计算当前正在运行的账号数
# 计算当前正在执行”的账号数(排队中的不计入)
current_running = 0
if user_id in user_accounts:
current_running = sum(1 for acc in user_accounts[user_id].values() if acc.is_running)
with task_status_lock:
for info in task_status.values():
if info.get('user_id') == user_id and info.get('status') == '运行中':
current_running += 1
return jsonify({
'today_completed': stats.get('completed', 0),
@@ -3973,8 +4010,10 @@ def update_system_config_api():
return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400
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
schedule_browse_type = normalized
if schedule_weekdays is not None:
# 验证星期格式,应该是逗号分隔的数字字符串 "1,2,3,4,5,6,7"
@@ -4341,7 +4380,7 @@ def run_scheduled_task(skip_weekday_check=False):
"""
try:
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:
@@ -4445,6 +4484,9 @@ def status_push_worker():
with task_status_lock:
status_items = list(task_status.items())
for account_id, status_info in status_items:
# 无任务执行时不推送;排队中的状态已通过事件推送过一次,无需周期性推送
if status_info.get('status') != '运行中':
continue
user_id = status_info.get('user_id')
if user_id:
# 获取账号对象
@@ -4618,7 +4660,7 @@ def scheduled_task_worker():
# 执行用户定时任务
user_id = schedule_config['user_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)
try:
@@ -5109,7 +5151,9 @@ def create_user_schedule_api():
name = data.get('name', '我的定时任务')
schedule_time = data.get('schedule_time', '08:00')
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)
account_ids = data.get('account_ids', [])
@@ -5170,6 +5214,11 @@ def update_schedule_api(schedule_id):
import re
if not re.match(r'^\d{2}:\d{2}$', update_data['schedule_time']):
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)
if success:
@@ -5232,7 +5281,7 @@ def run_schedule_now_api(schedule_id):
return jsonify({"error": "没有配置账号"}), 400
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']
started = []
@@ -5304,10 +5353,12 @@ def delete_schedule_logs_api(schedule_id):
def batch_start_accounts():
"""批量启动账号"""
user_id = current_user.id
data = request.json
data = request.json or {}
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)
if not account_ids:

View File

@@ -54,7 +54,7 @@ config = get_config()
DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理)
DB_VERSION = 6
DB_VERSION = 7
# ==================== 时区处理工具函数 ====================
# Bug fix: 统一时区处理,避免混用导致的问题
@@ -379,9 +379,13 @@ def migrate_database():
_migrate_to_v6(conn)
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',
(DB_VERSION,))
cursor.execute('UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1',
(DB_VERSION, get_cst_now_str()))
conn.commit()
if current_version < DB_VERSION:
@@ -587,6 +591,66 @@ def _migrate_to_v6(conn):
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():
@@ -612,8 +676,8 @@ def ensure_default_admin():
default_password_hash = hash_password_bcrypt(random_password)
cursor.execute(
'INSERT INTO admins (username, password_hash) VALUES (?, ?)',
('admin', default_password_hash)
'INSERT INTO admins (username, password_hash, created_at) VALUES (?, ?, ?)',
('admin', default_password_hash, get_cst_now_str())
)
conn.commit()
print("=" * 60)
@@ -696,10 +760,11 @@ def set_default_vip_days(days):
"""设置默认VIP天数"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute('''
INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at)
VALUES (1, ?, CURRENT_TIMESTAMP)
''', (days,))
VALUES (1, ?, ?)
''', (days, cst_time))
conn.commit()
return True
@@ -823,6 +888,7 @@ def create_user(username, password, email=''):
with db_pool.get_db() as conn:
cursor = conn.cursor()
password_hash = hash_password(password)
cst_time = get_cst_now_str()
# 获取默认VIP天数
default_vip_days = get_vip_config()['default_vip_days']
@@ -836,9 +902,9 @@ def create_user(username, password, email=''):
try:
cursor.execute('''
INSERT INTO users (username, password_hash, email, status, vip_expire_time)
VALUES (?, ?, ?, 'pending', ?)
''', (username, password_hash, email, vip_expire_time))
INSERT INTO users (username, password_hash, email, status, vip_expire_time, created_at)
VALUES (?, ?, ?, 'pending', ?, ?)
''', (username, password_hash, email, vip_expire_time, cst_time))
conn.commit()
return cursor.lastrowid
except sqlite3.IntegrityError:
@@ -978,11 +1044,12 @@ def approve_user(user_id):
"""审核通过用户"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute('''
UPDATE users
SET status = 'approved', approved_at = CURRENT_TIMESTAMP
SET status = 'approved', approved_at = ?
WHERE id = ?
''', (user_id,))
''', (cst_time, user_id))
conn.commit()
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:
cursor = conn.cursor()
cst_time = get_cst_now_str()
# 安全修复:加密存储第三方账号密码
encrypted_password = encrypt_password(password)
cursor.execute('''
INSERT INTO accounts (id, user_id, username, password, remember, remark)
VALUES (?, ?, ?, ?, ?, ?)
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark))
INSERT INTO accounts (id, user_id, username, password, remember, remark, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark, cst_time))
conn.commit()
return cursor.lastrowid
@@ -1171,7 +1239,7 @@ def get_system_stats():
cursor.execute('''
SELECT COUNT(*) as count FROM users
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']
@@ -1291,7 +1359,8 @@ def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_ti
params.append(auto_approve_vip_days)
if updates:
updates.append('updated_at = CURRENT_TIMESTAMP')
updates.append('updated_at = ?')
params.append(get_cst_now_str())
# Bug fix: 验证所有字段名都在白名单中
for update_clause in updates:
field_name = update_clause.split('=')[0].strip()
@@ -1311,7 +1380,7 @@ def get_hourly_registration_count():
cursor = conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM users
WHERE created_at >= datetime('now', '-1 hour')
WHERE created_at >= datetime('now', 'localtime', '-1 hour')
''')
return cursor.fetchone()[0]
@@ -1483,7 +1552,7 @@ def delete_old_task_logs(days=30, batch_size=1000):
DELETE FROM task_logs
WHERE rowid IN (
SELECT rowid FROM task_logs
WHERE created_at < datetime('now', '-' || ? || ' days')
WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
LIMIT ?
)
''', (days, batch_size))
@@ -1533,12 +1602,13 @@ def create_password_reset_request(user_id, new_password):
with db_pool.get_db() as conn:
cursor = conn.cursor()
password_hash = hash_password_bcrypt(new_password)
cst_time = get_cst_now_str()
try:
cursor.execute('''
INSERT INTO password_reset_requests (user_id, new_password_hash, status)
VALUES (?, ?, 'pending')
''', (user_id, password_hash))
INSERT INTO password_reset_requests (user_id, new_password_hash, status, created_at)
VALUES (?, ?, 'pending', ?)
''', (user_id, password_hash, cst_time))
conn.commit()
return cursor.lastrowid
except Exception as e:
@@ -1565,6 +1635,7 @@ def approve_password_reset(request_id):
"""批准密码重置申请"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
try:
# 获取申请信息
@@ -1588,9 +1659,9 @@ def approve_password_reset(request_id):
# 更新申请状态
cursor.execute('''
UPDATE password_reset_requests
SET status = 'approved', processed_at = CURRENT_TIMESTAMP
SET status = 'approved', processed_at = ?
WHERE id = ?
''', (request_id,))
''', (cst_time, request_id))
conn.commit()
return True
@@ -1603,13 +1674,14 @@ def reject_password_reset(request_id):
"""拒绝密码重置申请"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
try:
cursor.execute('''
UPDATE password_reset_requests
SET status = 'rejected', processed_at = CURRENT_TIMESTAMP
SET status = 'rejected', processed_at = ?
WHERE id = ? AND status = 'pending'
''', (request_id,))
''', (cst_time, request_id))
conn.commit()
return cursor.rowcount > 0
except Exception as e:
@@ -1652,7 +1724,7 @@ def clean_old_operation_logs(days=30):
try:
cursor.execute('''
DELETE FROM operation_logs
WHERE created_at < datetime('now', '-' || ? || ' days')
WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
''', (days,))
deleted_count = cursor.rowcount
conn.commit()
@@ -2146,7 +2218,7 @@ def clean_old_schedule_logs(days=30):
cursor = conn.cursor()
cursor.execute('''
DELETE FROM schedule_execution_logs
WHERE execute_time < datetime('now', '-' || ? || ' days')
WHERE execute_time < datetime('now', 'localtime', '-' || ? || ' days')
''', (days,))
conn.commit()
return cursor.rowcount

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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">
</head>
<body>

View File

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

View File

@@ -12,6 +12,13 @@ import json
from datetime import datetime
from enum import Enum
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):
"""任务执行阶段"""
@@ -108,12 +115,13 @@ class TaskCheckpoint:
task_id = f"{user_id}:{account_id}:{int(time.time())}"
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute("""
INSERT INTO task_checkpoints
(task_id, user_id, account_id, username, browse_type, stage, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
(task_id, user_id, account_id, username, browse_type, stage, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (task_id, user_id, account_id, username, browse_type,
TaskStage.QUEUED.value, 'running'))
TaskStage.QUEUED.value, 'running', cst_time, cst_time))
conn.commit()
return task_id
@@ -122,8 +130,8 @@ class TaskCheckpoint:
with db_pool.get_db() as conn:
cursor = conn.cursor()
updates = ['stage = ?', 'updated_at = CURRENT_TIMESTAMP']
params = [stage.value if isinstance(stage, TaskStage) else stage]
updates = ['stage = ?', 'updated_at = ?']
params = [stage.value if isinstance(stage, TaskStage) else stage, get_cst_now_str()]
if progress_percent is not None:
updates.append('progress_percent = ?')
@@ -155,8 +163,8 @@ class TaskCheckpoint:
with db_pool.get_db() as conn:
cursor = conn.cursor()
updates = ['updated_at = CURRENT_TIMESTAMP']
params = []
updates = ['updated_at = ?']
params = [get_cst_now_str()]
for key in ['current_page', 'total_pages', 'processed_items', 'downloaded_files']:
if key in kwargs:
@@ -182,6 +190,7 @@ class TaskCheckpoint:
"""记录错误并决定是否暂停任务"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
# 获取当前重试次数和最大重试次数
cursor.execute("""
@@ -206,10 +215,10 @@ class TaskCheckpoint:
retry_count = ?,
error_count = ?,
last_error = ?,
updated_at = CURRENT_TIMESTAMP
updated_at = ?
WHERE task_id = ?
""", (TaskStage.PAUSED.value, retry_count, error_count,
error_message, task_id))
error_message, cst_time, task_id))
conn.commit()
return 'paused'
else:
@@ -219,9 +228,9 @@ class TaskCheckpoint:
SET retry_count = ?,
error_count = ?,
last_error = ?,
updated_at = CURRENT_TIMESTAMP
updated_at = ?
WHERE task_id = ?
""", (retry_count, error_count, error_message, task_id))
""", (retry_count, error_count, error_message, cst_time, task_id))
conn.commit()
return 'retry'
@@ -231,17 +240,18 @@ class TaskCheckpoint:
"""完成任务"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute("""
UPDATE task_checkpoints
SET status = ?,
stage = ?,
progress_percent = 100,
completed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
completed_at = ?,
updated_at = ?
WHERE task_id = ?
""", ('completed' if success else 'failed',
TaskStage.COMPLETED.value if success else TaskStage.FAILED.value,
task_id))
cst_time, cst_time, task_id))
conn.commit()
def get_checkpoint(self, task_id):
@@ -327,13 +337,14 @@ class TaskCheckpoint:
"""恢复暂停的任务"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute("""
UPDATE task_checkpoints
SET status = 'running',
retry_count = 0,
updated_at = CURRENT_TIMESTAMP
updated_at = ?
WHERE task_id = ? AND status = 'paused'
""", (task_id,))
""", (cst_time, task_id))
conn.commit()
return cursor.rowcount > 0
@@ -341,14 +352,15 @@ class TaskCheckpoint:
"""放弃暂停的任务"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute("""
UPDATE task_checkpoints
SET status = 'failed',
stage = ?,
completed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
completed_at = ?,
updated_at = ?
WHERE task_id = ? AND status = 'paused'
""", (TaskStage.FAILED.value, task_id))
""", (TaskStage.FAILED.value, cst_time, cst_time, task_id))
conn.commit()
return cursor.rowcount > 0
@@ -359,7 +371,7 @@ class TaskCheckpoint:
cursor.execute("""
DELETE FROM task_checkpoints
WHERE status IN ('completed', 'failed')
AND datetime(completed_at) < datetime('now', '-' || ? || ' days')
AND datetime(completed_at) < datetime('now', 'localtime', '-' || ? || ' days')
""", (days,))
deleted = cursor.rowcount
conn.commit()

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;">
<option value="注册前未读">注册前未读</option>
<option value="应读" selected>应读</option>
<option value="未读">未读</option>
</select>
</div>
@@ -1731,10 +1730,23 @@
}
// 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) {
if (!user.vip_expire_time) return false;
const expireTime = new Date(user.vip_expire_time);
return expireTime > new Date();
const expireTime = parseBeijingDateTime(user.vip_expire_time);
return expireTime ? expireTime > new Date() : false;
}
function getVipBadge(user) {
@@ -1746,8 +1758,8 @@
function getVipExpire(user) {
if (!isVip(user)) return '';
const expireTime = new Date(user.vip_expire_time);
const daysLeft = Math.ceil((expireTime - new Date()) / (1000*60*60*24));
const expireTime = parseBeijingDateTime(user.vip_expire_time);
const daysLeft = expireTime ? Math.ceil((expireTime - new Date()) / (1000*60*60*24)) : 0;
if (user.vip_expire_time === '2099-12-31 23:59:59') {
return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
}

View File

@@ -556,7 +556,6 @@
<div class="toolbar-group">
<select class="select-inline" id="batchBrowseType">
<option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option>
</select>
<label class="switch">
@@ -718,7 +717,6 @@
<label class="form-label">浏览类型</label>
<select class="form-select" id="scheduleBrowseType">
<option value="应读">应读</option>
<option value="未读">未读</option>
<option value="注册前未读">注册前未读</option>
</select>
</div>
@@ -1313,7 +1311,7 @@
'</div>' + progressHtml +
'<div class="account-card-actions">' +
'<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>' +
'<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>' +
@@ -1868,7 +1866,6 @@
function copyScreenshotImage(imgSrc) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;