fix: 账号页闪烁/浏览类型/截图复制/时区统一
This commit is contained in:
@@ -244,7 +244,7 @@ certbot renew --dry-run
|
|||||||
### 2. 定时任务
|
### 2. 定时任务
|
||||||
- **启用定时浏览**: 是/否
|
- **启用定时浏览**: 是/否
|
||||||
- **执行时间**: 02:00 (CST时间)
|
- **执行时间**: 02:00 (CST时间)
|
||||||
- **浏览类型**: 应读/注册前未读/未读
|
- **浏览类型**: 应读/注册前未读
|
||||||
- **执行日期**: 周一到周日
|
- **执行日期**: 周一到周日
|
||||||
|
|
||||||
### 3. 代理配置
|
### 3. 代理配置
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
- 批量启动:`POST /api/accounts/batch/start`
|
- 批量启动:`POST /api/accounts/batch/start`
|
||||||
- 批量停止:`POST /api/accounts/batch/stop`
|
- 批量停止:`POST /api/accounts/batch/stop`
|
||||||
- 清空账号:`POST /api/accounts/clear`
|
- 清空账号:`POST /api/accounts/clear`
|
||||||
- 批量参数:浏览类型(应读/未读/注册前未读)、截图开关、选中账号集合
|
- 批量参数:浏览类型(应读/注册前未读)、截图开关、选中账号集合
|
||||||
- VIP 限制/提示:
|
- VIP 限制/提示:
|
||||||
- 普通用户账号数量上限、批量能力/定时能力等(以现有逻辑为准)
|
- 普通用户账号数量上限、批量能力/定时能力等(以现有逻辑为准)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ let timer = null
|
|||||||
|
|
||||||
function recordUpdatedAt() {
|
function recordUpdatedAt() {
|
||||||
try {
|
try {
|
||||||
lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
|
||||||
} catch {
|
} catch {
|
||||||
lastUpdatedAt.value = ''
|
lastUpdatedAt.value = ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ const scheduleWeekdayDisplay = computed(() =>
|
|||||||
.join('、'),
|
.join('、'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function normalizeBrowseType(value) {
|
||||||
|
if (String(value) === '注册前未读') return '注册前未读'
|
||||||
|
return '应读'
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -65,7 +70,7 @@ async function loadAll() {
|
|||||||
|
|
||||||
scheduleEnabled.value = (system.schedule_enabled ?? 0) === 1
|
scheduleEnabled.value = (system.schedule_enabled ?? 0) === 1
|
||||||
scheduleTime.value = system.schedule_time || '02:00'
|
scheduleTime.value = system.schedule_time || '02:00'
|
||||||
scheduleBrowseType.value = system.schedule_browse_type || '应读'
|
scheduleBrowseType.value = normalizeBrowseType(system.schedule_browse_type)
|
||||||
|
|
||||||
const weekdays = String(system.schedule_weekdays || '1,2,3,4,5,6,7')
|
const weekdays = String(system.schedule_weekdays || '1,2,3,4,5,6,7')
|
||||||
.split(',')
|
.split(',')
|
||||||
@@ -279,7 +284,6 @@ onMounted(loadAll)
|
|||||||
<el-select v-model="scheduleBrowseType" style="width: 220px">
|
<el-select v-model="scheduleBrowseType" style="width: 220px">
|
||||||
<el-option label="注册前未读" value="注册前未读" />
|
<el-option label="注册前未读" value="注册前未读" />
|
||||||
<el-option label="应读" value="应读" />
|
<el-option label="应读" value="应读" />
|
||||||
<el-option label="未读" value="未读" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,22 @@ export function parseSqliteDateTime(value) {
|
|||||||
if (!value) return null
|
if (!value) return null
|
||||||
if (value instanceof Date) return value
|
if (value instanceof Date) return value
|
||||||
|
|
||||||
const str = String(value)
|
let str = String(value).trim()
|
||||||
|
if (!str) return null
|
||||||
|
|
||||||
|
// "YYYY-MM-DD" -> "YYYY-MM-DDT00:00:00"
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) str = `${str}T00:00:00`
|
||||||
|
|
||||||
// "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss"
|
// "YYYY-MM-DD HH:mm:ss" -> "YYYY-MM-DDTHH:mm:ss"
|
||||||
const iso = str.includes('T') ? str : str.replace(' ', 'T')
|
let iso = str.includes('T') ? str : str.replace(' ', 'T')
|
||||||
|
|
||||||
|
// SQLite 可能带微秒,Date 仅可靠支持到毫秒
|
||||||
|
iso = iso.replace(/\.(\d{3})\d+/, '.$1')
|
||||||
|
|
||||||
|
// 统一按北京时间解析(除非字符串本身已带时区)
|
||||||
|
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso)
|
||||||
|
if (!hasTimezone) iso = `${iso}+08:00`
|
||||||
|
|
||||||
const date = new Date(iso)
|
const date = new Date(iso)
|
||||||
if (Number.isNaN(date.getTime())) return null
|
if (Number.isNaN(date.getTime())) return null
|
||||||
return date
|
return date
|
||||||
@@ -14,4 +27,3 @@ export function formatDateTime(value) {
|
|||||||
if (!value) return '-'
|
if (!value) return '-'
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,14 +350,13 @@ class APIBrowser:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# 根据浏览类型确定 bz 参数
|
# 根据浏览类型确定 bz 参数
|
||||||
# 网页实际选项: 0=注册前未读, 1=已读, 2=应读
|
# 网页实际参数: 0=注册前未读, 2=应读(历史上曾存在 1=已读,但当前逻辑不再使用)
|
||||||
# 前端选项: 注册前未读, 应读, 未读, 已读
|
# 当前前端选项: 注册前未读、应读(默认应读)
|
||||||
if '注册前' in browse_type:
|
browse_type_text = str(browse_type or "")
|
||||||
|
if '注册前' in browse_type_text:
|
||||||
bz = 0 # 注册前未读
|
bz = 0 # 注册前未读
|
||||||
elif browse_type == '已读':
|
|
||||||
bz = 1 # 已读
|
|
||||||
else:
|
else:
|
||||||
bz = 2 # 应读、未读 都映射到 bz=2
|
bz = 2 # 应读
|
||||||
|
|
||||||
self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...")
|
self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...")
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ const editForm = reactive({
|
|||||||
|
|
||||||
const browseTypeOptions = [
|
const browseTypeOptions = [
|
||||||
{ label: '应读', value: '应读' },
|
{ label: '应读', value: '应读' },
|
||||||
{ label: '未读', value: '未读' },
|
|
||||||
{ label: '注册前未读', value: '注册前未读' },
|
{ label: '注册前未读', value: '注册前未读' },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -80,8 +79,13 @@ function normalizeAccountPayload(acc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replaceAccounts(list) {
|
function replaceAccounts(list) {
|
||||||
for (const key of Object.keys(accountsById)) delete accountsById[key]
|
const nextList = Array.isArray(list) ? list : []
|
||||||
for (const acc of list || []) normalizeAccountPayload(acc)
|
const nextIds = new Set(nextList.map((acc) => String(acc?.id || '')))
|
||||||
|
|
||||||
|
for (const existingId of Object.keys(accountsById)) {
|
||||||
|
if (!nextIds.has(existingId)) delete accountsById[existingId]
|
||||||
|
}
|
||||||
|
for (const acc of nextList) normalizeAccountPayload(acc)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBrowseTypeDefaults() {
|
function ensureBrowseTypeDefaults() {
|
||||||
@@ -138,8 +142,9 @@ function showRuntimeProgress(acc) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStats() {
|
async function refreshStats(options = {}) {
|
||||||
statsLoading.value = true
|
const silent = Boolean(options?.silent)
|
||||||
|
if (!silent) statsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await fetchRunStats()
|
const data = await fetchRunStats()
|
||||||
stats.today_completed = Number(data?.today_completed || 0)
|
stats.today_completed = Number(data?.today_completed || 0)
|
||||||
@@ -150,7 +155,7 @@ async function refreshStats() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e?.response?.status === 401) window.location.href = '/login'
|
if (e?.response?.status === 401) window.location.href = '/login'
|
||||||
} finally {
|
} finally {
|
||||||
statsLoading.value = false
|
if (!silent) statsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +461,41 @@ function bindSocket() {
|
|||||||
let unbindSocket = null
|
let unbindSocket = null
|
||||||
let statsTimer = null
|
let statsTimer = null
|
||||||
|
|
||||||
|
const shouldPollStats = computed(() => {
|
||||||
|
// 仅在“真正执行中”才轮询(排队中不轮询,避免空转导致页面闪烁)
|
||||||
|
return accounts.value.some((acc) => {
|
||||||
|
if (!acc?.is_running) return false
|
||||||
|
const statusText = String(acc.status || '')
|
||||||
|
if (statusText.includes('排队')) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function stopStatsPolling() {
|
||||||
|
if (!statsTimer) return
|
||||||
|
window.clearInterval(statsTimer)
|
||||||
|
statsTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function startStatsPolling() {
|
||||||
|
if (statsTimer) return
|
||||||
|
statsTimer = window.setInterval(() => refreshStats({ silent: true }), 10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStatsPolling(prevRunning = null) {
|
||||||
|
const running = shouldPollStats.value
|
||||||
|
|
||||||
|
// 任务从“运行中 -> 空闲”时,补拉一次统计以确保最终数据正确,再停止轮询
|
||||||
|
if (prevRunning === true && running === false) refreshStats({ silent: true }).catch(() => {})
|
||||||
|
|
||||||
|
if (running) startStatsPolling()
|
||||||
|
else stopStatsPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(shouldPollStats, (running, prevRunning) => {
|
||||||
|
syncStatsPolling(prevRunning)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!userStore.vipInfo) {
|
if (!userStore.vipInfo) {
|
||||||
userStore.refreshVipInfo().catch(() => {
|
userStore.refreshVipInfo().catch(() => {
|
||||||
@@ -467,12 +507,12 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await refreshAccounts()
|
await refreshAccounts()
|
||||||
await refreshStats()
|
await refreshStats()
|
||||||
statsTimer = window.setInterval(refreshStats, 10_000)
|
syncStatsPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (unbindSocket) unbindSocket()
|
if (unbindSocket) unbindSocket()
|
||||||
if (statsTimer) window.clearInterval(statsTimer)
|
stopStatsPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -565,7 +605,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-skeleton v-if="loading || statsLoading" :rows="5" animated />
|
<el-skeleton v-if="loading" :rows="5" animated />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" />
|
<el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" />
|
||||||
<div v-else class="grid">
|
<div v-else class="grid">
|
||||||
|
|||||||
@@ -45,10 +45,14 @@ const form = reactive({
|
|||||||
|
|
||||||
const browseTypeOptions = [
|
const browseTypeOptions = [
|
||||||
{ label: '应读', value: '应读' },
|
{ label: '应读', value: '应读' },
|
||||||
{ label: '未读', value: '未读' },
|
|
||||||
{ label: '注册前未读', value: '注册前未读' },
|
{ label: '注册前未读', value: '注册前未读' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function normalizeBrowseType(value) {
|
||||||
|
if (String(value) === '注册前未读') return '注册前未读'
|
||||||
|
return '应读'
|
||||||
|
}
|
||||||
|
|
||||||
const weekdayOptions = [
|
const weekdayOptions = [
|
||||||
{ label: '周一', value: '1' },
|
{ label: '周一', value: '1' },
|
||||||
{ label: '周二', value: '2' },
|
{ label: '周二', value: '2' },
|
||||||
@@ -92,7 +96,11 @@ async function loadAccounts() {
|
|||||||
async function loadSchedules() {
|
async function loadSchedules() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
schedules.value = await fetchSchedules()
|
const list = await fetchSchedules()
|
||||||
|
schedules.value = (Array.isArray(list) ? list : []).map((s) => ({
|
||||||
|
...s,
|
||||||
|
browse_type: normalizeBrowseType(s?.browse_type),
|
||||||
|
}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e?.response?.status === 401) window.location.href = '/login'
|
if (e?.response?.status === 401) window.location.href = '/login'
|
||||||
schedules.value = []
|
schedules.value = []
|
||||||
@@ -121,7 +129,7 @@ function openEdit(schedule) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((v) => String(v))
|
.map((v) => String(v))
|
||||||
if (form.weekdays.length === 0) form.weekdays = ['1', '2', '3', '4', '5']
|
if (form.weekdays.length === 0) form.weekdays = ['1', '2', '3', '4', '5']
|
||||||
form.browse_type = schedule.browse_type || '应读'
|
form.browse_type = normalizeBrowseType(schedule.browse_type)
|
||||||
form.enable_screenshot = Number(schedule.enable_screenshot ?? 1) !== 0
|
form.enable_screenshot = Number(schedule.enable_screenshot ?? 1) !== 0
|
||||||
form.account_ids = Array.isArray(schedule.account_ids) ? schedule.account_ids.slice() : []
|
form.account_ids = Array.isArray(schedule.account_ids) ? schedule.account_ids.slice() : []
|
||||||
editorOpen.value = true
|
editorOpen.value = true
|
||||||
|
|||||||
@@ -34,6 +34,87 @@ function openPreview(item) {
|
|||||||
previewOpen.value = true
|
previewOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findRenderedShotImage(filename) {
|
||||||
|
try {
|
||||||
|
const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(String(filename)) : String(filename)
|
||||||
|
return document.querySelector(`img[data-shot-filename="${escaped}"]`)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToPngBlob(canvas) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('toBlob_failed'))), 'image/png')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function imageElementToPngBlob(imgEl) {
|
||||||
|
if (!imgEl) throw new Error('no_image')
|
||||||
|
if (!imgEl.complete || imgEl.naturalWidth <= 0) {
|
||||||
|
if (typeof imgEl.decode === 'function') await imgEl.decode()
|
||||||
|
else {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
imgEl.addEventListener('load', resolve, { once: true })
|
||||||
|
imgEl.addEventListener('error', reject, { once: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = imgEl.naturalWidth
|
||||||
|
canvas.height = imgEl.naturalHeight
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) throw new Error('no_canvas')
|
||||||
|
ctx.drawImage(imgEl, 0, 0)
|
||||||
|
return await canvasToPngBlob(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blobToPng(blob) {
|
||||||
|
if (!blob) throw new Error('no_blob')
|
||||||
|
if (blob.type === 'image/png') return blob
|
||||||
|
|
||||||
|
if (typeof createImageBitmap === 'function') {
|
||||||
|
const bitmap = await createImageBitmap(blob)
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = bitmap.width
|
||||||
|
canvas.height = bitmap.height
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) throw new Error('no_canvas')
|
||||||
|
ctx.drawImage(bitmap, 0, 0)
|
||||||
|
return await canvasToPngBlob(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
try {
|
||||||
|
const img = new Image()
|
||||||
|
img.src = url
|
||||||
|
if (typeof img.decode === 'function') await img.decode()
|
||||||
|
return await imageElementToPngBlob(img)
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function screenshotUrlToPngBlob(url, filename) {
|
||||||
|
// 优先使用页面上已渲染完成的 <img>(避免额外请求;也更容易满足剪贴板“用户手势”限制)
|
||||||
|
const imgEl = findRenderedShotImage(filename)
|
||||||
|
if (imgEl) {
|
||||||
|
try {
|
||||||
|
return await imageElementToPngBlob(imgEl)
|
||||||
|
} catch {
|
||||||
|
// fallback to fetch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, { credentials: 'include', cache: 'no-store' })
|
||||||
|
if (!resp.ok) throw new Error('fetch_failed')
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const mime = resp.headers.get('Content-Type') || blob.type || ''
|
||||||
|
if (!mime.startsWith('image/')) throw new Error('not_image')
|
||||||
|
return await blobToPng(blob)
|
||||||
|
}
|
||||||
|
|
||||||
async function onClearAll() {
|
async function onClearAll() {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要清空全部截图吗?', '清空截图', {
|
await ElMessageBox.confirm('确定要清空全部截图吗?', '清空截图', {
|
||||||
@@ -98,14 +179,28 @@ async function copyImage(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { credentials: 'include' })
|
// 关键点:用 Promise 形式的数据源,让 clipboard.write 在用户手势内立即发生(更稳)
|
||||||
if (!resp.ok) throw new Error('fetch_failed')
|
try {
|
||||||
const blob = await resp.blob()
|
await navigator.clipboard.write([
|
||||||
const mime = blob.type || ''
|
new ClipboardItem({
|
||||||
if (!mime.startsWith('image/')) throw new Error('not_image')
|
'image/png': screenshotUrlToPngBlob(url, item.filename),
|
||||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })])
|
}),
|
||||||
ElMessage.success('图片已复制')
|
])
|
||||||
} catch {
|
} catch {
|
||||||
|
const pngBlob = await screenshotUrlToPngBlob(url, item.filename)
|
||||||
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })])
|
||||||
|
}
|
||||||
|
ElMessage.success('图片已复制到剪贴板')
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
await navigator.clipboard.writeText(`${window.location.origin}${url}`)
|
||||||
|
ElMessage.warning('复制图片失败,已复制图片链接(可直接粘贴到浏览器打开)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
ElMessage.warning('复制图片失败:请确认允许剪贴板权限;可用“下载”。')
|
ElMessage.warning('复制图片失败:请确认允许剪贴板权限;可用“下载”。')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +233,14 @@ onMounted(load)
|
|||||||
|
|
||||||
<div v-else class="grid">
|
<div v-else class="grid">
|
||||||
<el-card v-for="item in screenshots" :key="item.filename" shadow="never" class="shot-card" :body-style="{ padding: '0' }">
|
<el-card v-for="item in screenshots" :key="item.filename" shadow="never" class="shot-card" :body-style="{ padding: '0' }">
|
||||||
<img class="shot-img" :src="buildUrl(item.filename)" :alt="item.display_name || item.filename" loading="lazy" @click="openPreview(item)" />
|
<img
|
||||||
|
class="shot-img"
|
||||||
|
:src="buildUrl(item.filename)"
|
||||||
|
:alt="item.display_name || item.filename"
|
||||||
|
:data-shot-filename="item.filename"
|
||||||
|
loading="lazy"
|
||||||
|
@click="openPreview(item)"
|
||||||
|
/>
|
||||||
<div class="shot-body">
|
<div class="shot-body">
|
||||||
<div class="shot-name" :title="item.display_name || item.filename">{{ item.display_name || item.filename }}</div>
|
<div class="shot-name" :title="item.display_name || item.filename">{{ item.display_name || item.filename }}</div>
|
||||||
<div class="shot-meta app-muted">{{ item.created || '' }}</div>
|
<div class="shot-meta app-muted">{{ item.created || '' }}</div>
|
||||||
|
|||||||
85
app.py
85
app.py
@@ -76,6 +76,34 @@ def get_beijing_now():
|
|||||||
"""获取北京时间的当前时间(统一时区处理)"""
|
"""获取北京时间的当前时间(统一时区处理)"""
|
||||||
return datetime.now(BEIJING_TZ)
|
return datetime.now(BEIJING_TZ)
|
||||||
|
|
||||||
|
# ========== 浏览类型规范化 ==========
|
||||||
|
# 当前页面仅保留:应读、注册前未读;默认应读
|
||||||
|
BROWSE_TYPE_SHOULD_READ = "应读"
|
||||||
|
BROWSE_TYPE_PRE_REG_UNREAD = "注册前未读"
|
||||||
|
|
||||||
|
_BROWSE_TYPES_ALLOWED_INPUT = {
|
||||||
|
BROWSE_TYPE_SHOULD_READ,
|
||||||
|
BROWSE_TYPE_PRE_REG_UNREAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_browse_type(value, default=BROWSE_TYPE_SHOULD_READ):
|
||||||
|
"""规范化浏览类型(非法值回退到默认)。"""
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text == BROWSE_TYPE_PRE_REG_UNREAD:
|
||||||
|
return BROWSE_TYPE_PRE_REG_UNREAD
|
||||||
|
if text == BROWSE_TYPE_SHOULD_READ:
|
||||||
|
return BROWSE_TYPE_SHOULD_READ
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def validate_browse_type(value, default=BROWSE_TYPE_SHOULD_READ):
|
||||||
|
"""校验并返回规范化浏览类型;非法值返回None。"""
|
||||||
|
text = str(value if value is not None else default).strip()
|
||||||
|
if text not in _BROWSE_TYPES_ALLOWED_INPUT:
|
||||||
|
return None
|
||||||
|
return normalize_browse_type(text, default=default)
|
||||||
|
|
||||||
|
|
||||||
# ========== 初始化配置 ==========
|
# ========== 初始化配置 ==========
|
||||||
config = get_config()
|
config = get_config()
|
||||||
@@ -868,7 +896,7 @@ class Account:
|
|||||||
self.total_items = 0
|
self.total_items = 0
|
||||||
self.total_attachments = 0
|
self.total_attachments = 0
|
||||||
self.automation = None
|
self.automation = None
|
||||||
self.last_browse_type = "注册前未读"
|
self.last_browse_type = BROWSE_TYPE_SHOULD_READ
|
||||||
self.proxy_config = None # 保存代理配置,浏览和截图共用
|
self.proxy_config = None # 保存代理配置,浏览和截图共用
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -2678,8 +2706,10 @@ def start_account(account_id):
|
|||||||
if account.is_running:
|
if account.is_running:
|
||||||
return jsonify({"error": "任务已在运行中"}), 400
|
return jsonify({"error": "任务已在运行中"}), 400
|
||||||
|
|
||||||
data = request.json
|
data = request.json or {}
|
||||||
browse_type = data.get('browse_type', '应读')
|
browse_type = validate_browse_type(data.get('browse_type'), default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not browse_type:
|
||||||
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
enable_screenshot = data.get('enable_screenshot', True) # 默认启用截图
|
enable_screenshot = data.get('enable_screenshot', True) # 默认启用截图
|
||||||
|
|
||||||
# 确保浏览器管理器已初始化
|
# 确保浏览器管理器已初始化
|
||||||
@@ -3278,10 +3308,8 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
|
|||||||
base = f"{parsed.scheme}://{parsed.netloc}"
|
base = f"{parsed.scheme}://{parsed.netloc}"
|
||||||
if '注册前' in str(browse_type):
|
if '注册前' in str(browse_type):
|
||||||
bz = 0
|
bz = 0
|
||||||
elif str(browse_type) == '已读':
|
|
||||||
bz = 1
|
|
||||||
else:
|
else:
|
||||||
bz = 2 # 应读/未读
|
bz = 2 # 应读
|
||||||
target_url = f"{base}/admin/center.aspx?bz={bz}"
|
target_url = f"{base}/admin/center.aspx?bz={bz}"
|
||||||
automation.main_page.goto(target_url, timeout=60000)
|
automation.main_page.goto(target_url, timeout=60000)
|
||||||
current_url = getattr(automation.main_page, "url", "") or ""
|
current_url = getattr(automation.main_page, "url", "") or ""
|
||||||
@@ -3461,7 +3489,14 @@ def manual_screenshot(account_id):
|
|||||||
return jsonify({"error": "任务运行中,无法截图"}), 400
|
return jsonify({"error": "任务运行中,无法截图"}), 400
|
||||||
|
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
browse_type = data.get('browse_type', account.last_browse_type)
|
requested_browse_type = data.get('browse_type', None)
|
||||||
|
if requested_browse_type is None:
|
||||||
|
# 兼容历史遗留值:内存中的 last_browse_type 可能为旧枚举,直接规范化回退到默认(应读)
|
||||||
|
browse_type = normalize_browse_type(account.last_browse_type)
|
||||||
|
else:
|
||||||
|
browse_type = validate_browse_type(requested_browse_type, default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not browse_type:
|
||||||
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
|
|
||||||
account.last_browse_type = browse_type
|
account.last_browse_type = browse_type
|
||||||
|
|
||||||
@@ -3911,10 +3946,12 @@ def get_run_stats():
|
|||||||
# 获取今日任务统计
|
# 获取今日任务统计
|
||||||
stats = database.get_user_run_stats(user_id)
|
stats = database.get_user_run_stats(user_id)
|
||||||
|
|
||||||
# 计算当前正在运行的账号数
|
# 计算当前“正在执行”的账号数(排队中的不计入)
|
||||||
current_running = 0
|
current_running = 0
|
||||||
if user_id in user_accounts:
|
with task_status_lock:
|
||||||
current_running = sum(1 for acc in user_accounts[user_id].values() if acc.is_running)
|
for info in task_status.values():
|
||||||
|
if info.get('user_id') == user_id and info.get('status') == '运行中':
|
||||||
|
current_running += 1
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'today_completed': stats.get('completed', 0),
|
'today_completed': stats.get('completed', 0),
|
||||||
@@ -3973,8 +4010,10 @@ def update_system_config_api():
|
|||||||
return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400
|
return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400
|
||||||
|
|
||||||
if schedule_browse_type is not None:
|
if schedule_browse_type is not None:
|
||||||
if schedule_browse_type not in ['注册前未读', '应读', '未读']:
|
normalized = validate_browse_type(schedule_browse_type, default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not normalized:
|
||||||
return jsonify({"error": "浏览类型无效"}), 400
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
|
schedule_browse_type = normalized
|
||||||
|
|
||||||
if schedule_weekdays is not None:
|
if schedule_weekdays is not None:
|
||||||
# 验证星期格式,应该是逗号分隔的数字字符串 "1,2,3,4,5,6,7"
|
# 验证星期格式,应该是逗号分隔的数字字符串 "1,2,3,4,5,6,7"
|
||||||
@@ -4341,7 +4380,7 @@ def run_scheduled_task(skip_weekday_check=False):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
config = database.get_system_config()
|
config = database.get_system_config()
|
||||||
browse_type = config.get('schedule_browse_type', '应读')
|
browse_type = normalize_browse_type(config.get('schedule_browse_type', BROWSE_TYPE_SHOULD_READ))
|
||||||
|
|
||||||
# 检查今天是否在允许执行的星期列表中(立即执行时跳过此检查)
|
# 检查今天是否在允许执行的星期列表中(立即执行时跳过此检查)
|
||||||
if not skip_weekday_check:
|
if not skip_weekday_check:
|
||||||
@@ -4445,6 +4484,9 @@ def status_push_worker():
|
|||||||
with task_status_lock:
|
with task_status_lock:
|
||||||
status_items = list(task_status.items())
|
status_items = list(task_status.items())
|
||||||
for account_id, status_info in status_items:
|
for account_id, status_info in status_items:
|
||||||
|
# 无任务执行时不推送;排队中的状态已通过事件推送过一次,无需周期性推送
|
||||||
|
if status_info.get('status') != '运行中':
|
||||||
|
continue
|
||||||
user_id = status_info.get('user_id')
|
user_id = status_info.get('user_id')
|
||||||
if user_id:
|
if user_id:
|
||||||
# 获取账号对象
|
# 获取账号对象
|
||||||
@@ -4618,7 +4660,7 @@ def scheduled_task_worker():
|
|||||||
# 执行用户定时任务
|
# 执行用户定时任务
|
||||||
user_id = schedule_config['user_id']
|
user_id = schedule_config['user_id']
|
||||||
schedule_id = schedule_config['id']
|
schedule_id = schedule_config['id']
|
||||||
browse_type = schedule_config.get('browse_type', '应读')
|
browse_type = normalize_browse_type(schedule_config.get('browse_type', BROWSE_TYPE_SHOULD_READ))
|
||||||
enable_screenshot = schedule_config.get('enable_screenshot', 1)
|
enable_screenshot = schedule_config.get('enable_screenshot', 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -5109,7 +5151,9 @@ def create_user_schedule_api():
|
|||||||
name = data.get('name', '我的定时任务')
|
name = data.get('name', '我的定时任务')
|
||||||
schedule_time = data.get('schedule_time', '08:00')
|
schedule_time = data.get('schedule_time', '08:00')
|
||||||
weekdays = data.get('weekdays', '1,2,3,4,5')
|
weekdays = data.get('weekdays', '1,2,3,4,5')
|
||||||
browse_type = data.get('browse_type', '应读')
|
browse_type = validate_browse_type(data.get('browse_type', BROWSE_TYPE_SHOULD_READ), default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not browse_type:
|
||||||
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
enable_screenshot = data.get('enable_screenshot', 1)
|
enable_screenshot = data.get('enable_screenshot', 1)
|
||||||
account_ids = data.get('account_ids', [])
|
account_ids = data.get('account_ids', [])
|
||||||
|
|
||||||
@@ -5170,6 +5214,11 @@ def update_schedule_api(schedule_id):
|
|||||||
import re
|
import re
|
||||||
if not re.match(r'^\d{2}:\d{2}$', update_data['schedule_time']):
|
if not re.match(r'^\d{2}:\d{2}$', update_data['schedule_time']):
|
||||||
return jsonify({"error": "时间格式不正确"}), 400
|
return jsonify({"error": "时间格式不正确"}), 400
|
||||||
|
if 'browse_type' in update_data:
|
||||||
|
normalized = validate_browse_type(update_data.get('browse_type'), default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not normalized:
|
||||||
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
|
update_data['browse_type'] = normalized
|
||||||
|
|
||||||
success = database.update_user_schedule(schedule_id, **update_data)
|
success = database.update_user_schedule(schedule_id, **update_data)
|
||||||
if success:
|
if success:
|
||||||
@@ -5232,7 +5281,7 @@ def run_schedule_now_api(schedule_id):
|
|||||||
return jsonify({"error": "没有配置账号"}), 400
|
return jsonify({"error": "没有配置账号"}), 400
|
||||||
|
|
||||||
user_id = current_user.id
|
user_id = current_user.id
|
||||||
browse_type = schedule['browse_type']
|
browse_type = normalize_browse_type(schedule.get('browse_type', BROWSE_TYPE_SHOULD_READ))
|
||||||
enable_screenshot = schedule['enable_screenshot']
|
enable_screenshot = schedule['enable_screenshot']
|
||||||
|
|
||||||
started = []
|
started = []
|
||||||
@@ -5304,10 +5353,12 @@ def delete_schedule_logs_api(schedule_id):
|
|||||||
def batch_start_accounts():
|
def batch_start_accounts():
|
||||||
"""批量启动账号"""
|
"""批量启动账号"""
|
||||||
user_id = current_user.id
|
user_id = current_user.id
|
||||||
data = request.json
|
data = request.json or {}
|
||||||
|
|
||||||
account_ids = data.get('account_ids', [])
|
account_ids = data.get('account_ids', [])
|
||||||
browse_type = data.get('browse_type', '应读')
|
browse_type = validate_browse_type(data.get('browse_type', BROWSE_TYPE_SHOULD_READ), default=BROWSE_TYPE_SHOULD_READ)
|
||||||
|
if not browse_type:
|
||||||
|
return jsonify({"error": "浏览类型无效"}), 400
|
||||||
enable_screenshot = data.get('enable_screenshot', True)
|
enable_screenshot = data.get('enable_screenshot', True)
|
||||||
|
|
||||||
if not account_ids:
|
if not account_ids:
|
||||||
|
|||||||
128
database.py
128
database.py
@@ -54,7 +54,7 @@ config = get_config()
|
|||||||
DB_FILE = config.DB_FILE
|
DB_FILE = config.DB_FILE
|
||||||
|
|
||||||
# 数据库版本 (用于迁移管理)
|
# 数据库版本 (用于迁移管理)
|
||||||
DB_VERSION = 6
|
DB_VERSION = 7
|
||||||
|
|
||||||
# ==================== 时区处理工具函数 ====================
|
# ==================== 时区处理工具函数 ====================
|
||||||
# Bug fix: 统一时区处理,避免混用导致的问题
|
# Bug fix: 统一时区处理,避免混用导致的问题
|
||||||
@@ -379,9 +379,13 @@ def migrate_database():
|
|||||||
_migrate_to_v6(conn)
|
_migrate_to_v6(conn)
|
||||||
current_version = 6
|
current_version = 6
|
||||||
|
|
||||||
|
if current_version < 7:
|
||||||
|
_migrate_to_v7(conn)
|
||||||
|
current_version = 7
|
||||||
|
|
||||||
# 更新版本号
|
# 更新版本号
|
||||||
cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
cursor.execute('UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1',
|
||||||
(DB_VERSION,))
|
(DB_VERSION, get_cst_now_str()))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
if current_version < DB_VERSION:
|
if current_version < DB_VERSION:
|
||||||
@@ -587,6 +591,66 @@ def _migrate_to_v6(conn):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_to_v7(conn):
|
||||||
|
"""迁移到版本7 - 统一存储北京时间(将历史UTC时间字段整体+8小时)"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
def table_exists(table_name: str) -> bool:
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
def column_exists(table_name: str, column_name: str) -> bool:
|
||||||
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
|
return any(row[1] == column_name for row in cursor.fetchall())
|
||||||
|
|
||||||
|
def shift_utc_to_cst(table_name: str, column_name: str) -> None:
|
||||||
|
if not table_exists(table_name):
|
||||||
|
return
|
||||||
|
if not column_exists(table_name, column_name):
|
||||||
|
return
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {column_name} = datetime({column_name}, '+8 hours')
|
||||||
|
WHERE {column_name} IS NOT NULL AND {column_name} != ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 主库(这些字段历史上主要由 CURRENT_TIMESTAMP 产生,为UTC)
|
||||||
|
for table, col in [
|
||||||
|
("users", "created_at"),
|
||||||
|
("users", "approved_at"),
|
||||||
|
("admins", "created_at"),
|
||||||
|
("accounts", "created_at"),
|
||||||
|
("password_reset_requests", "created_at"),
|
||||||
|
("password_reset_requests", "processed_at"),
|
||||||
|
]:
|
||||||
|
shift_utc_to_cst(table, col)
|
||||||
|
|
||||||
|
# 邮件模块(同库,不一定启用,但表可能存在)
|
||||||
|
for table, col in [
|
||||||
|
("smtp_configs", "created_at"),
|
||||||
|
("smtp_configs", "updated_at"),
|
||||||
|
("smtp_configs", "last_success_at"),
|
||||||
|
("email_settings", "updated_at"),
|
||||||
|
("email_tokens", "created_at"),
|
||||||
|
("email_logs", "created_at"),
|
||||||
|
("email_stats", "last_updated"),
|
||||||
|
]:
|
||||||
|
shift_utc_to_cst(table, col)
|
||||||
|
|
||||||
|
# 断点续传(同库,表由 task_checkpoint.py 创建)
|
||||||
|
for table, col in [
|
||||||
|
("task_checkpoints", "created_at"),
|
||||||
|
("task_checkpoints", "updated_at"),
|
||||||
|
("task_checkpoints", "completed_at"),
|
||||||
|
]:
|
||||||
|
shift_utc_to_cst(table, col)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(" ✓ 时区迁移:历史UTC时间已转换为北京时间")
|
||||||
|
|
||||||
|
|
||||||
# ==================== 管理员相关 ====================
|
# ==================== 管理员相关 ====================
|
||||||
|
|
||||||
def ensure_default_admin():
|
def ensure_default_admin():
|
||||||
@@ -612,8 +676,8 @@ def ensure_default_admin():
|
|||||||
|
|
||||||
default_password_hash = hash_password_bcrypt(random_password)
|
default_password_hash = hash_password_bcrypt(random_password)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'INSERT INTO admins (username, password_hash) VALUES (?, ?)',
|
'INSERT INTO admins (username, password_hash, created_at) VALUES (?, ?, ?)',
|
||||||
('admin', default_password_hash)
|
('admin', default_password_hash, get_cst_now_str())
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@@ -696,10 +760,11 @@ def set_default_vip_days(days):
|
|||||||
"""设置默认VIP天数"""
|
"""设置默认VIP天数"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at)
|
INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at)
|
||||||
VALUES (1, ?, CURRENT_TIMESTAMP)
|
VALUES (1, ?, ?)
|
||||||
''', (days,))
|
''', (days, cst_time))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -823,6 +888,7 @@ def create_user(username, password, email=''):
|
|||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
password_hash = hash_password(password)
|
password_hash = hash_password(password)
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
|
|
||||||
# 获取默认VIP天数
|
# 获取默认VIP天数
|
||||||
default_vip_days = get_vip_config()['default_vip_days']
|
default_vip_days = get_vip_config()['default_vip_days']
|
||||||
@@ -836,9 +902,9 @@ def create_user(username, password, email=''):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO users (username, password_hash, email, status, vip_expire_time)
|
INSERT INTO users (username, password_hash, email, status, vip_expire_time, created_at)
|
||||||
VALUES (?, ?, ?, 'pending', ?)
|
VALUES (?, ?, ?, 'pending', ?, ?)
|
||||||
''', (username, password_hash, email, vip_expire_time))
|
''', (username, password_hash, email, vip_expire_time, cst_time))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
@@ -978,11 +1044,12 @@ def approve_user(user_id):
|
|||||||
"""审核通过用户"""
|
"""审核通过用户"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET status = 'approved', approved_at = CURRENT_TIMESTAMP
|
SET status = 'approved', approved_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (user_id,))
|
''', (cst_time, user_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
@@ -1011,12 +1078,13 @@ def create_account(user_id, account_id, username, password, remember=True, remar
|
|||||||
"""创建账号(密码加密存储)"""
|
"""创建账号(密码加密存储)"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
# 安全修复:加密存储第三方账号密码
|
# 安全修复:加密存储第三方账号密码
|
||||||
encrypted_password = encrypt_password(password)
|
encrypted_password = encrypt_password(password)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO accounts (id, user_id, username, password, remember, remark)
|
INSERT INTO accounts (id, user_id, username, password, remember, remark, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark))
|
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark, cst_time))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
|
|
||||||
@@ -1171,7 +1239,7 @@ def get_system_stats():
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT COUNT(*) as count FROM users
|
SELECT COUNT(*) as count FROM users
|
||||||
WHERE vip_expire_time IS NOT NULL
|
WHERE vip_expire_time IS NOT NULL
|
||||||
AND datetime(vip_expire_time) > datetime('now')
|
AND datetime(vip_expire_time) > datetime('now', 'localtime')
|
||||||
''')
|
''')
|
||||||
vip_users = cursor.fetchone()['count']
|
vip_users = cursor.fetchone()['count']
|
||||||
|
|
||||||
@@ -1291,7 +1359,8 @@ def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_ti
|
|||||||
params.append(auto_approve_vip_days)
|
params.append(auto_approve_vip_days)
|
||||||
|
|
||||||
if updates:
|
if updates:
|
||||||
updates.append('updated_at = CURRENT_TIMESTAMP')
|
updates.append('updated_at = ?')
|
||||||
|
params.append(get_cst_now_str())
|
||||||
# Bug fix: 验证所有字段名都在白名单中
|
# Bug fix: 验证所有字段名都在白名单中
|
||||||
for update_clause in updates:
|
for update_clause in updates:
|
||||||
field_name = update_clause.split('=')[0].strip()
|
field_name = update_clause.split('=')[0].strip()
|
||||||
@@ -1311,7 +1380,7 @@ def get_hourly_registration_count():
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT COUNT(*) FROM users
|
SELECT COUNT(*) FROM users
|
||||||
WHERE created_at >= datetime('now', '-1 hour')
|
WHERE created_at >= datetime('now', 'localtime', '-1 hour')
|
||||||
''')
|
''')
|
||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
@@ -1483,7 +1552,7 @@ def delete_old_task_logs(days=30, batch_size=1000):
|
|||||||
DELETE FROM task_logs
|
DELETE FROM task_logs
|
||||||
WHERE rowid IN (
|
WHERE rowid IN (
|
||||||
SELECT rowid FROM task_logs
|
SELECT rowid FROM task_logs
|
||||||
WHERE created_at < datetime('now', '-' || ? || ' days')
|
WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
)
|
)
|
||||||
''', (days, batch_size))
|
''', (days, batch_size))
|
||||||
@@ -1533,12 +1602,13 @@ def create_password_reset_request(user_id, new_password):
|
|||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
password_hash = hash_password_bcrypt(new_password)
|
password_hash = hash_password_bcrypt(new_password)
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO password_reset_requests (user_id, new_password_hash, status)
|
INSERT INTO password_reset_requests (user_id, new_password_hash, status, created_at)
|
||||||
VALUES (?, ?, 'pending')
|
VALUES (?, ?, 'pending', ?)
|
||||||
''', (user_id, password_hash))
|
''', (user_id, password_hash, cst_time))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1565,6 +1635,7 @@ def approve_password_reset(request_id):
|
|||||||
"""批准密码重置申请"""
|
"""批准密码重置申请"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取申请信息
|
# 获取申请信息
|
||||||
@@ -1588,9 +1659,9 @@ def approve_password_reset(request_id):
|
|||||||
# 更新申请状态
|
# 更新申请状态
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE password_reset_requests
|
UPDATE password_reset_requests
|
||||||
SET status = 'approved', processed_at = CURRENT_TIMESTAMP
|
SET status = 'approved', processed_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (request_id,))
|
''', (cst_time, request_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
@@ -1603,13 +1674,14 @@ def reject_password_reset(request_id):
|
|||||||
"""拒绝密码重置申请"""
|
"""拒绝密码重置申请"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE password_reset_requests
|
UPDATE password_reset_requests
|
||||||
SET status = 'rejected', processed_at = CURRENT_TIMESTAMP
|
SET status = 'rejected', processed_at = ?
|
||||||
WHERE id = ? AND status = 'pending'
|
WHERE id = ? AND status = 'pending'
|
||||||
''', (request_id,))
|
''', (cst_time, request_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1652,7 +1724,7 @@ def clean_old_operation_logs(days=30):
|
|||||||
try:
|
try:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
DELETE FROM operation_logs
|
DELETE FROM operation_logs
|
||||||
WHERE created_at < datetime('now', '-' || ? || ' days')
|
WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
|
||||||
''', (days,))
|
''', (days,))
|
||||||
deleted_count = cursor.rowcount
|
deleted_count = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -2146,7 +2218,7 @@ def clean_old_schedule_logs(days=30):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
DELETE FROM schedule_execution_logs
|
DELETE FROM schedule_execution_logs
|
||||||
WHERE execute_time < datetime('now', '-' || ? || ' days')
|
WHERE execute_time < datetime('now', 'localtime', '-' || ? || ' days')
|
||||||
''', (days,))
|
''', (days,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|||||||
139
email_service.py
139
email_service.py
@@ -27,6 +27,10 @@ BEIJING_TZ = pytz.timezone('Asia/Shanghai')
|
|||||||
def get_beijing_today():
|
def get_beijing_today():
|
||||||
"""获取北京时间的今天日期字符串"""
|
"""获取北京时间的今天日期字符串"""
|
||||||
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d')
|
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
def get_beijing_now_str():
|
||||||
|
"""获取北京时间的当前时间字符串"""
|
||||||
|
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
@@ -44,7 +48,13 @@ from crypto_utils import encrypt_password, decrypt_password, is_encrypted
|
|||||||
def parse_datetime(dt_str: str) -> datetime:
|
def parse_datetime(dt_str: str) -> datetime:
|
||||||
"""解析数据库中的时间字符串,支持带微秒和不带微秒的格式"""
|
"""解析数据库中的时间字符串,支持带微秒和不带微秒的格式"""
|
||||||
if not dt_str:
|
if not dt_str:
|
||||||
return datetime.min
|
return BEIJING_TZ.localize(datetime(1970, 1, 1))
|
||||||
|
|
||||||
|
# 兼容 sqlite3 可能返回 datetime 对象的情况
|
||||||
|
if isinstance(dt_str, datetime):
|
||||||
|
return dt_str.astimezone(BEIJING_TZ) if dt_str.tzinfo else BEIJING_TZ.localize(dt_str)
|
||||||
|
|
||||||
|
text = str(dt_str)
|
||||||
# 尝试多种格式
|
# 尝试多种格式
|
||||||
formats = [
|
formats = [
|
||||||
'%Y-%m-%d %H:%M:%S.%f', # 带微秒
|
'%Y-%m-%d %H:%M:%S.%f', # 带微秒
|
||||||
@@ -52,11 +62,12 @@ def parse_datetime(dt_str: str) -> datetime:
|
|||||||
]
|
]
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(dt_str, fmt)
|
naive = datetime.strptime(text, fmt)
|
||||||
|
return BEIJING_TZ.localize(naive)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
# 如果都失败,返回最小时间(视为过期)
|
# 如果都失败,返回最小时间(视为过期)
|
||||||
return datetime.min
|
return BEIJING_TZ.localize(datetime(1970, 1, 1))
|
||||||
|
|
||||||
|
|
||||||
# ============ 常量配置 ============
|
# ============ 常量配置 ============
|
||||||
@@ -146,10 +157,13 @@ def init_email_tables():
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# 初始化默认设置
|
# 初始化默认设置
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled)
|
"""
|
||||||
VALUES (1, 0, 1)
|
INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled, updated_at)
|
||||||
""")
|
VALUES (1, 0, 1, ?)
|
||||||
|
""",
|
||||||
|
(get_beijing_now_str(),),
|
||||||
|
)
|
||||||
|
|
||||||
# 3. 邮件验证Token表
|
# 3. 邮件验证Token表
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@@ -208,9 +222,12 @@ def init_email_tables():
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# 初始化统计记录
|
# 初始化统计记录
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
INSERT OR IGNORE INTO email_stats (id) VALUES (1)
|
"""
|
||||||
""")
|
INSERT OR IGNORE INTO email_stats (id, last_updated) VALUES (1, ?)
|
||||||
|
""",
|
||||||
|
(get_beijing_now_str(),),
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("[邮件服务] 数据库表初始化完成")
|
print("[邮件服务] 数据库表初始化完成")
|
||||||
@@ -276,8 +293,8 @@ def update_email_settings(
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 构建动态更新语句
|
# 构建动态更新语句
|
||||||
updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = CURRENT_TIMESTAMP']
|
updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = ?']
|
||||||
params = [int(enabled), int(failover_enabled)]
|
params = [int(enabled), int(failover_enabled), get_beijing_now_str()]
|
||||||
|
|
||||||
if register_verify_enabled is not None:
|
if register_verify_enabled is not None:
|
||||||
updates.append('register_verify_enabled = ?')
|
updates.append('register_verify_enabled = ?')
|
||||||
@@ -416,6 +433,7 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
|
|||||||
"""创建新的SMTP配置"""
|
"""创建新的SMTP配置"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
now_str = get_beijing_now_str()
|
||||||
|
|
||||||
# 加密密码
|
# 加密密码
|
||||||
password = encrypt_password(data.get('password', '')) if data.get('password') else ''
|
password = encrypt_password(data.get('password', '')) if data.get('password') else ''
|
||||||
@@ -423,8 +441,8 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO smtp_configs
|
INSERT INTO smtp_configs
|
||||||
(name, enabled, is_primary, priority, host, port, username, password,
|
(name, enabled, is_primary, priority, host, port, username, password,
|
||||||
use_ssl, use_tls, sender_name, sender_email, daily_limit)
|
use_ssl, use_tls, sender_name, sender_email, daily_limit, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (
|
""", (
|
||||||
data.get('name', '默认配置'),
|
data.get('name', '默认配置'),
|
||||||
int(data.get('enabled', True)),
|
int(data.get('enabled', True)),
|
||||||
@@ -438,7 +456,9 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
|
|||||||
int(data.get('use_tls', False)),
|
int(data.get('use_tls', False)),
|
||||||
data.get('sender_name', '自动化学习'),
|
data.get('sender_name', '自动化学习'),
|
||||||
data.get('sender_email', ''),
|
data.get('sender_email', ''),
|
||||||
data.get('daily_limit', 0)
|
data.get('daily_limit', 0),
|
||||||
|
now_str,
|
||||||
|
now_str,
|
||||||
))
|
))
|
||||||
|
|
||||||
config_id = cursor.lastrowid
|
config_id = cursor.lastrowid
|
||||||
@@ -484,7 +504,8 @@ def update_smtp_config(config_id: int, data: Dict[str, Any]) -> bool:
|
|||||||
if not updates:
|
if not updates:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
updates.append("updated_at = CURRENT_TIMESTAMP")
|
updates.append("updated_at = ?")
|
||||||
|
params.append(get_beijing_now_str())
|
||||||
params.append(config_id)
|
params.append(config_id)
|
||||||
|
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
@@ -515,9 +536,9 @@ def set_primary_smtp_config(config_id: int) -> bool:
|
|||||||
# 设置新的主配置
|
# 设置新的主配置
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE smtp_configs
|
UPDATE smtp_configs
|
||||||
SET is_primary = 1, updated_at = CURRENT_TIMESTAMP
|
SET is_primary = 1, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""", (config_id,))
|
""", (get_beijing_now_str(), config_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
@@ -584,25 +605,26 @@ def _update_smtp_stats(config_id: int, success: bool, error: str = ''):
|
|||||||
"""更新SMTP配置的统计信息"""
|
"""更新SMTP配置的统计信息"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
now_str = get_beijing_now_str()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE smtp_configs
|
UPDATE smtp_configs
|
||||||
SET daily_sent = daily_sent + 1,
|
SET daily_sent = daily_sent + 1,
|
||||||
success_count = success_count + 1,
|
success_count = success_count + 1,
|
||||||
last_success_at = CURRENT_TIMESTAMP,
|
last_success_at = ?,
|
||||||
last_error = '',
|
last_error = '',
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""", (config_id,))
|
""", (now_str, now_str, config_id))
|
||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE smtp_configs
|
UPDATE smtp_configs
|
||||||
SET fail_count = fail_count + 1,
|
SET fail_count = fail_count + 1,
|
||||||
last_error = ?,
|
last_error = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""", (error[:500], config_id))
|
""", (error[:500], now_str, config_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -864,11 +886,12 @@ def test_smtp_config(config_id: int, test_email: str) -> Dict[str, Any]:
|
|||||||
# 更新最后成功时间
|
# 更新最后成功时间
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
now_str = get_beijing_now_str()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE smtp_configs
|
UPDATE smtp_configs
|
||||||
SET last_success_at = CURRENT_TIMESTAMP, last_error = ''
|
SET last_success_at = ?, last_error = '', updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""", (config_id,))
|
""", (now_str, now_str, config_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return {'success': True, 'error': ''}
|
return {'success': True, 'error': ''}
|
||||||
@@ -899,10 +922,10 @@ def _log_email_send(user_id: int, smtp_config_id: int, email_to: str,
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO email_logs
|
INSERT INTO email_logs
|
||||||
(user_id, smtp_config_id, email_to, email_type, subject, status,
|
(user_id, smtp_config_id, email_to, email_type, subject, status,
|
||||||
error_message, attachment_count, attachment_size)
|
error_message, attachment_count, attachment_size, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (user_id, smtp_config_id, email_to, email_type, subject, status,
|
""", (user_id, smtp_config_id, email_to, email_type, subject, status,
|
||||||
error_message, attachment_count, attachment_size))
|
error_message, attachment_count, attachment_size, get_beijing_now_str()))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -910,6 +933,7 @@ def _update_email_stats(email_type: str, success: bool):
|
|||||||
"""更新邮件统计"""
|
"""更新邮件统计"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
now_str = get_beijing_now_str()
|
||||||
|
|
||||||
type_field_map = {
|
type_field_map = {
|
||||||
EMAIL_TYPE_REGISTER: 'register_sent',
|
EMAIL_TYPE_REGISTER: 'register_sent',
|
||||||
@@ -927,25 +951,25 @@ def _update_email_stats(email_type: str, success: bool):
|
|||||||
SET total_sent = total_sent + 1,
|
SET total_sent = total_sent + 1,
|
||||||
total_success = total_success + 1,
|
total_success = total_success + 1,
|
||||||
{type_field} = {type_field} + 1,
|
{type_field} = {type_field} + 1,
|
||||||
last_updated = CURRENT_TIMESTAMP
|
last_updated = ?
|
||||||
WHERE id = 1
|
WHERE id = 1
|
||||||
""")
|
""", (now_str,))
|
||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE email_stats
|
UPDATE email_stats
|
||||||
SET total_sent = total_sent + 1,
|
SET total_sent = total_sent + 1,
|
||||||
total_success = total_success + 1,
|
total_success = total_success + 1,
|
||||||
last_updated = CURRENT_TIMESTAMP
|
last_updated = ?
|
||||||
WHERE id = 1
|
WHERE id = 1
|
||||||
""")
|
""", (now_str,))
|
||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE email_stats
|
UPDATE email_stats
|
||||||
SET total_sent = total_sent + 1,
|
SET total_sent = total_sent + 1,
|
||||||
total_failed = total_failed + 1,
|
total_failed = total_failed + 1,
|
||||||
last_updated = CURRENT_TIMESTAMP
|
last_updated = ?
|
||||||
WHERE id = 1
|
WHERE id = 1
|
||||||
""")
|
""", (now_str,))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -1061,10 +1085,14 @@ def cleanup_email_logs(days: int = 30) -> int:
|
|||||||
"""清理过期邮件日志"""
|
"""清理过期邮件日志"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cutoff = (datetime.now(BEIJING_TZ) - timedelta(days=int(days))).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
DELETE FROM email_logs
|
DELETE FROM email_logs
|
||||||
WHERE datetime(created_at) < datetime('now', '-' || ? || ' days')
|
WHERE datetime(created_at) < datetime(?)
|
||||||
""", (days,))
|
""",
|
||||||
|
(cutoff,),
|
||||||
|
)
|
||||||
deleted = cursor.rowcount
|
deleted = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return deleted
|
return deleted
|
||||||
@@ -1083,14 +1111,19 @@ def generate_email_token(email: str, token_type: str, user_id: int = None) -> st
|
|||||||
EMAIL_TYPE_BIND: TOKEN_EXPIRE_BIND
|
EMAIL_TYPE_BIND: TOKEN_EXPIRE_BIND
|
||||||
}.get(token_type, TOKEN_EXPIRE_REGISTER)
|
}.get(token_type, TOKEN_EXPIRE_REGISTER)
|
||||||
|
|
||||||
expires_at = datetime.now() + timedelta(seconds=expire_seconds)
|
now = datetime.now(BEIJING_TZ)
|
||||||
|
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
expires_at_str = (now + timedelta(seconds=expire_seconds)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
INSERT INTO email_tokens (user_id, email, token, token_type, expires_at)
|
"""
|
||||||
VALUES (?, ?, ?, ?, ?)
|
INSERT INTO email_tokens (user_id, email, token, token_type, expires_at, created_at)
|
||||||
""", (user_id, email, token, token_type, expires_at))
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(user_id, email, token, token_type, expires_at_str, now_str),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return token
|
return token
|
||||||
@@ -1122,7 +1155,7 @@ def verify_email_token(token: str, token_type: str) -> Optional[Dict[str, Any]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 检查是否过期
|
# 检查是否过期
|
||||||
if parse_datetime(expires_at) < datetime.now():
|
if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 标记为已使用
|
# 标记为已使用
|
||||||
@@ -1161,7 +1194,7 @@ def check_rate_limit(email: str, token_type: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
last_sent = parse_datetime(row[0])
|
last_sent = parse_datetime(row[0])
|
||||||
elapsed = (datetime.now() - last_sent).total_seconds()
|
elapsed = (datetime.now(BEIJING_TZ) - last_sent).total_seconds()
|
||||||
|
|
||||||
return elapsed >= limit_seconds
|
return elapsed >= limit_seconds
|
||||||
|
|
||||||
@@ -1170,10 +1203,14 @@ def cleanup_expired_tokens() -> int:
|
|||||||
"""清理过期Token"""
|
"""清理过期Token"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
now_str = get_beijing_now_str()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
DELETE FROM email_tokens
|
DELETE FROM email_tokens
|
||||||
WHERE datetime(expires_at) < datetime('now')
|
WHERE datetime(expires_at) < datetime(?)
|
||||||
""")
|
""",
|
||||||
|
(now_str,),
|
||||||
|
)
|
||||||
deleted = cursor.rowcount
|
deleted = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return deleted
|
return deleted
|
||||||
@@ -1436,7 +1473,7 @@ def verify_password_reset_token(token: str) -> Optional[Dict[str, Any]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 检查是否过期
|
# 检查是否过期
|
||||||
if parse_datetime(expires_at) < datetime.now():
|
if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return {'user_id': user_id, 'email': email, 'token_id': token_id}
|
return {'user_id': user_id, 'email': email, 'token_id': token_id}
|
||||||
@@ -1827,7 +1864,7 @@ def send_task_complete_email(
|
|||||||
return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0}
|
return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0}
|
||||||
|
|
||||||
# 获取完成时间
|
# 获取完成时间
|
||||||
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
complete_time = get_beijing_now_str()
|
||||||
|
|
||||||
# 读取截图文件
|
# 读取截图文件
|
||||||
screenshot_data = None
|
screenshot_data = None
|
||||||
@@ -2045,7 +2082,7 @@ def send_batch_task_complete_email(
|
|||||||
return {'success': False, 'error': '没有截图需要发送'}
|
return {'success': False, 'error': '没有截图需要发送'}
|
||||||
|
|
||||||
# 获取完成时间
|
# 获取完成时间
|
||||||
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
complete_time = get_beijing_now_str()
|
||||||
|
|
||||||
# 统计信息
|
# 统计信息
|
||||||
total_items_sum = sum(s.get('items', 0) for s in screenshots)
|
total_items_sum = sum(s.get('items', 0) for s in screenshots)
|
||||||
@@ -2131,7 +2168,7 @@ def send_batch_task_complete_email(
|
|||||||
else:
|
else:
|
||||||
with open(zip_path, 'rb') as f:
|
with open(zip_path, 'rb') as f:
|
||||||
zip_data = f.read()
|
zip_data = f.read()
|
||||||
zip_filename = f"screenshots_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
zip_filename = f"screenshots_{datetime.now(BEIJING_TZ).strftime('%Y%m%d_%H%M%S')}.zip"
|
||||||
attachment_note = "截图已打包为ZIP附件,请查收。"
|
attachment_note = "截图已打包为ZIP附件,请查收。"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[邮件] 打包截图失败: {e}")
|
print(f"[邮件] 打包截图失败: {e}")
|
||||||
|
|||||||
@@ -680,7 +680,7 @@ class PlaywrightAutomation:
|
|||||||
切换浏览类型(带重试机制)
|
切换浏览类型(带重试机制)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
browse_type: 浏览类型(注册前未读/应读/已读)
|
browse_type: 浏览类型(注册前未读/应读)
|
||||||
max_retries: 最大重试次数(默认2次)
|
max_retries: 最大重试次数(默认2次)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"_datetime-CpkTDmvr.js": {
|
"_datetime-ZCuLLiQt.js": {
|
||||||
"file": "assets/datetime-CpkTDmvr.js",
|
"file": "assets/datetime-ZCuLLiQt.js",
|
||||||
"name": "datetime"
|
"name": "datetime"
|
||||||
},
|
},
|
||||||
"_tasks-C2mQL6Tj.js": {
|
"_tasks-BYcXDffp.js": {
|
||||||
"file": "assets/tasks-C2mQL6Tj.js",
|
"file": "assets/tasks-BYcXDffp.js",
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_users-5QCWoNsI.js": {
|
"_users-Du3tLSHt.js": {
|
||||||
"file": "assets/users-5QCWoNsI.js",
|
"file": "assets/users-Du3tLSHt.js",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-CrrNPCqw.js",
|
"file": "assets/index-C5w7EVNo.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AnnouncementsPage.vue": {
|
"src/pages/AnnouncementsPage.vue": {
|
||||||
"file": "assets/AnnouncementsPage-9I91QH6T.js",
|
"file": "assets/AnnouncementsPage-C63j6LV5.js",
|
||||||
"name": "AnnouncementsPage",
|
"name": "AnnouncementsPage",
|
||||||
"src": "src/pages/AnnouncementsPage.vue",
|
"src": "src/pages/AnnouncementsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/EmailPage.vue": {
|
"src/pages/EmailPage.vue": {
|
||||||
"file": "assets/EmailPage-C0sjJZrc.js",
|
"file": "assets/EmailPage-Bf6BbYPD.js",
|
||||||
"name": "EmailPage",
|
"name": "EmailPage",
|
||||||
"src": "src/pages/EmailPage.vue",
|
"src": "src/pages/EmailPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/FeedbacksPage.vue": {
|
"src/pages/FeedbacksPage.vue": {
|
||||||
"file": "assets/FeedbacksPage-B_IMe7WI.js",
|
"file": "assets/FeedbacksPage-mOUifait.js",
|
||||||
"name": "FeedbacksPage",
|
"name": "FeedbacksPage",
|
||||||
"src": "src/pages/FeedbacksPage.vue",
|
"src": "src/pages/FeedbacksPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LogsPage.vue": {
|
"src/pages/LogsPage.vue": {
|
||||||
"file": "assets/LogsPage-Bvt31x2D.js",
|
"file": "assets/LogsPage-DVfeUO3d.js",
|
||||||
"name": "LogsPage",
|
"name": "LogsPage",
|
||||||
"src": "src/pages/LogsPage.vue",
|
"src": "src/pages/LogsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-5QCWoNsI.js",
|
"_users-Du3tLSHt.js",
|
||||||
"_tasks-C2mQL6Tj.js",
|
"_tasks-BYcXDffp.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -88,21 +88,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/PendingPage.vue": {
|
"src/pages/PendingPage.vue": {
|
||||||
"file": "assets/PendingPage-Bs4BEacx.js",
|
"file": "assets/PendingPage-BALptdIG.js",
|
||||||
"name": "PendingPage",
|
"name": "PendingPage",
|
||||||
"src": "src/pages/PendingPage.vue",
|
"src": "src/pages/PendingPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-5QCWoNsI.js",
|
"_users-Du3tLSHt.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_datetime-CpkTDmvr.js"
|
"_datetime-ZCuLLiQt.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/PendingPage-C_mZDlzP.css"
|
"assets/PendingPage-C_mZDlzP.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SettingsPage.vue": {
|
"src/pages/SettingsPage.vue": {
|
||||||
"file": "assets/SettingsPage-BA0VS3mc.js",
|
"file": "assets/SettingsPage-BKf3hQvU.js",
|
||||||
"name": "SettingsPage",
|
"name": "SettingsPage",
|
||||||
"src": "src/pages/SettingsPage.vue",
|
"src": "src/pages/SettingsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -114,20 +114,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/StatsPage.vue": {
|
"src/pages/StatsPage.vue": {
|
||||||
"file": "assets/StatsPage-CyjgHApe.js",
|
"file": "assets/StatsPage-DjylIGTc.js",
|
||||||
"name": "StatsPage",
|
"name": "StatsPage",
|
||||||
"src": "src/pages/StatsPage.vue",
|
"src": "src/pages/StatsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_tasks-C2mQL6Tj.js",
|
"_tasks-BYcXDffp.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/StatsPage-B4w_lWWU.css"
|
"assets/StatsPage-Bjh6A42Y.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SystemPage.vue": {
|
"src/pages/SystemPage.vue": {
|
||||||
"file": "assets/SystemPage-DMxUhCvv.js",
|
"file": "assets/SystemPage-k6FhqNid.js",
|
||||||
"name": "SystemPage",
|
"name": "SystemPage",
|
||||||
"src": "src/pages/SystemPage.vue",
|
"src": "src/pages/SystemPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -135,17 +135,17 @@
|
|||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SystemPage-DC1VKbLw.css"
|
"assets/SystemPage-b-S9OiVi.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/UsersPage.vue": {
|
"src/pages/UsersPage.vue": {
|
||||||
"file": "assets/UsersPage-DDSa1S98.js",
|
"file": "assets/UsersPage-XRSMHsqH.js",
|
||||||
"name": "UsersPage",
|
"name": "UsersPage",
|
||||||
"src": "src/pages/UsersPage.vue",
|
"src": "src/pages/UsersPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-5QCWoNsI.js",
|
"_users-Du3tLSHt.js",
|
||||||
"_datetime-CpkTDmvr.js",
|
"_datetime-ZCuLLiQt.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{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};
|
||||||
@@ -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};
|
||||||
@@ -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}
|
|
||||||
1
static/admin/assets/StatsPage-Bjh6A42Y.css
Normal file
1
static/admin/assets/StatsPage-Bjh6A42Y.css
Normal 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
1
static/admin/assets/StatsPage-DjylIGTc.js
Normal file
1
static/admin/assets/StatsPage-DjylIGTc.js
Normal file
File diff suppressed because one or more lines are too long
@@ -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
1
static/admin/assets/SystemPage-b-S9OiVi.css
Normal file
1
static/admin/assets/SystemPage-b-S9OiVi.css
Normal 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}
|
||||||
16
static/admin/assets/SystemPage-k6FhqNid.js
Normal file
16
static/admin/assets/SystemPage-k6FhqNid.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +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};
|
|
||||||
1
static/admin/assets/datetime-ZCuLLiQt.js
Normal file
1
static/admin/assets/datetime-ZCuLLiQt.js
Normal 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
@@ -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};
|
||||||
@@ -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};
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>后台管理 - 知识管理平台</title>
|
<title>后台管理 - 知识管理平台</title>
|
||||||
<script type="module" crossorigin src="./assets/index-CrrNPCqw.js"></script>
|
<script type="module" crossorigin src="./assets/index-C5w7EVNo.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"_accounts-B4fckN3X.js": {
|
"_accounts-DuQjqW8V.js": {
|
||||||
"file": "assets/accounts-B4fckN3X.js",
|
"file": "assets/accounts-DuQjqW8V.js",
|
||||||
"name": "accounts",
|
"name": "accounts",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_auth-DhZn_bRf.js": {
|
"_auth-C__02fQ5.js": {
|
||||||
"file": "assets/auth-DhZn_bRf.js",
|
"file": "assets/auth-C__02fQ5.js",
|
||||||
"name": "auth",
|
"name": "auth",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"name": "password"
|
"name": "password"
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-XYID9gNS.js",
|
"file": "assets/index-2JnZbEa5.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -36,26 +36,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AccountsPage.vue": {
|
"src/pages/AccountsPage.vue": {
|
||||||
"file": "assets/AccountsPage-D6CPzxlM.js",
|
"file": "assets/AccountsPage-CugNoiiP.js",
|
||||||
"name": "AccountsPage",
|
"name": "AccountsPage",
|
||||||
"src": "src/pages/AccountsPage.vue",
|
"src": "src/pages/AccountsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_accounts-B4fckN3X.js",
|
"_accounts-DuQjqW8V.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/AccountsPage-OUqse8pw.css"
|
"assets/AccountsPage-DAtYSxiO.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LoginPage.vue": {
|
"src/pages/LoginPage.vue": {
|
||||||
"file": "assets/LoginPage-DY0hfuOv.js",
|
"file": "assets/LoginPage-8hxar7WW.js",
|
||||||
"name": "LoginPage",
|
"name": "LoginPage",
|
||||||
"src": "src/pages/LoginPage.vue",
|
"src": "src/pages/LoginPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_auth-DhZn_bRf.js",
|
"_auth-C__02fQ5.js",
|
||||||
"_password-7ryi82gE.js"
|
"_password-7ryi82gE.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -63,26 +63,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/RegisterPage.vue": {
|
"src/pages/RegisterPage.vue": {
|
||||||
"file": "assets/RegisterPage-CXbQlCO0.js",
|
"file": "assets/RegisterPage-B_EUWOuP.js",
|
||||||
"name": "RegisterPage",
|
"name": "RegisterPage",
|
||||||
"src": "src/pages/RegisterPage.vue",
|
"src": "src/pages/RegisterPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_auth-DhZn_bRf.js"
|
"_auth-C__02fQ5.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/RegisterPage-CVjBOq6i.css"
|
"assets/RegisterPage-CVjBOq6i.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ResetPasswordPage.vue": {
|
"src/pages/ResetPasswordPage.vue": {
|
||||||
"file": "assets/ResetPasswordPage-CQpLXPca.js",
|
"file": "assets/ResetPasswordPage-BLEflhaq.js",
|
||||||
"name": "ResetPasswordPage",
|
"name": "ResetPasswordPage",
|
||||||
"src": "src/pages/ResetPasswordPage.vue",
|
"src": "src/pages/ResetPasswordPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_auth-DhZn_bRf.js",
|
"_auth-C__02fQ5.js",
|
||||||
"_password-7ryi82gE.js"
|
"_password-7ryi82gE.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -90,20 +90,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SchedulesPage.vue": {
|
"src/pages/SchedulesPage.vue": {
|
||||||
"file": "assets/SchedulesPage-DIc25kqH.js",
|
"file": "assets/SchedulesPage-BbA-BJek.js",
|
||||||
"name": "SchedulesPage",
|
"name": "SchedulesPage",
|
||||||
"src": "src/pages/SchedulesPage.vue",
|
"src": "src/pages/SchedulesPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_accounts-B4fckN3X.js",
|
"_accounts-DuQjqW8V.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SchedulesPage-Gu_OsDUd.css"
|
"assets/SchedulesPage-s5SCMMmz.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ScreenshotsPage.vue": {
|
"src/pages/ScreenshotsPage.vue": {
|
||||||
"file": "assets/ScreenshotsPage-BawNJkO2.js",
|
"file": "assets/ScreenshotsPage-DCIxup8x.js",
|
||||||
"name": "ScreenshotsPage",
|
"name": "ScreenshotsPage",
|
||||||
"src": "src/pages/ScreenshotsPage.vue",
|
"src": "src/pages/ScreenshotsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -111,11 +111,11 @@
|
|||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/ScreenshotsPage-CRn_Qd8Q.css"
|
"assets/ScreenshotsPage-DgLR6Xlu.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/VerifyResultPage.vue": {
|
"src/pages/VerifyResultPage.vue": {
|
||||||
"file": "assets/VerifyResultPage-C-GZLPnL.js",
|
"file": "assets/VerifyResultPage-QQKdIo1L.js",
|
||||||
"name": "VerifyResultPage",
|
"name": "VerifyResultPage",
|
||||||
"src": "src/pages/VerifyResultPage.vue",
|
"src": "src/pages/VerifyResultPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
|
|||||||
1
static/app/assets/AccountsPage-CugNoiiP.js
Normal file
1
static/app/assets/AccountsPage-CugNoiiP.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/app/assets/AccountsPage-DAtYSxiO.css
Normal file
1
static/app/assets/AccountsPage-DAtYSxiO.css
Normal 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}}
|
||||||
@@ -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
@@ -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};
|
||||||
@@ -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};
|
||||||
1
static/app/assets/SchedulesPage-BbA-BJek.js
Normal file
1
static/app/assets/SchedulesPage-BbA-BJek.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +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}}
|
|
||||||
1
static/app/assets/SchedulesPage-s5SCMMmz.css
Normal file
1
static/app/assets/SchedulesPage-s5SCMMmz.css
Normal 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}}
|
||||||
@@ -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};
|
|
||||||
@@ -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}}
|
|
||||||
1
static/app/assets/ScreenshotsPage-DCIxup8x.js
Normal file
1
static/app/assets/ScreenshotsPage-DCIxup8x.js
Normal file
File diff suppressed because one or more lines are too long
1
static/app/assets/ScreenshotsPage-DgLR6Xlu.css
Normal file
1
static/app/assets/ScreenshotsPage-DgLR6Xlu.css
Normal 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}}
|
||||||
@@ -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};
|
||||||
@@ -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};
|
||||||
@@ -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
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||||
<title>知识管理平台</title>
|
<title>知识管理平台</title>
|
||||||
<script type="module" crossorigin src="./assets/index-XYID9gNS.js"></script>
|
<script type="module" crossorigin src="./assets/index-2JnZbEa5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import db_pool
|
import db_pool
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
# 北京时区(统一)
|
||||||
|
CST_TZ = pytz.timezone("Asia/Shanghai")
|
||||||
|
|
||||||
|
def get_cst_now_str():
|
||||||
|
return datetime.now(CST_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
class TaskStage(Enum):
|
class TaskStage(Enum):
|
||||||
"""任务执行阶段"""
|
"""任务执行阶段"""
|
||||||
@@ -108,12 +115,13 @@ class TaskCheckpoint:
|
|||||||
task_id = f"{user_id}:{account_id}:{int(time.time())}"
|
task_id = f"{user_id}:{account_id}:{int(time.time())}"
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO task_checkpoints
|
INSERT INTO task_checkpoints
|
||||||
(task_id, user_id, account_id, username, browse_type, stage, status)
|
(task_id, user_id, account_id, username, browse_type, stage, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (task_id, user_id, account_id, username, browse_type,
|
""", (task_id, user_id, account_id, username, browse_type,
|
||||||
TaskStage.QUEUED.value, 'running'))
|
TaskStage.QUEUED.value, 'running', cst_time, cst_time))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
@@ -122,8 +130,8 @@ class TaskCheckpoint:
|
|||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
updates = ['stage = ?', 'updated_at = CURRENT_TIMESTAMP']
|
updates = ['stage = ?', 'updated_at = ?']
|
||||||
params = [stage.value if isinstance(stage, TaskStage) else stage]
|
params = [stage.value if isinstance(stage, TaskStage) else stage, get_cst_now_str()]
|
||||||
|
|
||||||
if progress_percent is not None:
|
if progress_percent is not None:
|
||||||
updates.append('progress_percent = ?')
|
updates.append('progress_percent = ?')
|
||||||
@@ -155,8 +163,8 @@ class TaskCheckpoint:
|
|||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
updates = ['updated_at = CURRENT_TIMESTAMP']
|
updates = ['updated_at = ?']
|
||||||
params = []
|
params = [get_cst_now_str()]
|
||||||
|
|
||||||
for key in ['current_page', 'total_pages', 'processed_items', 'downloaded_files']:
|
for key in ['current_page', 'total_pages', 'processed_items', 'downloaded_files']:
|
||||||
if key in kwargs:
|
if key in kwargs:
|
||||||
@@ -182,6 +190,7 @@ class TaskCheckpoint:
|
|||||||
"""记录错误并决定是否暂停任务"""
|
"""记录错误并决定是否暂停任务"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
|
|
||||||
# 获取当前重试次数和最大重试次数
|
# 获取当前重试次数和最大重试次数
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@@ -206,10 +215,10 @@ class TaskCheckpoint:
|
|||||||
retry_count = ?,
|
retry_count = ?,
|
||||||
error_count = ?,
|
error_count = ?,
|
||||||
last_error = ?,
|
last_error = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
""", (TaskStage.PAUSED.value, retry_count, error_count,
|
""", (TaskStage.PAUSED.value, retry_count, error_count,
|
||||||
error_message, task_id))
|
error_message, cst_time, task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return 'paused'
|
return 'paused'
|
||||||
else:
|
else:
|
||||||
@@ -219,9 +228,9 @@ class TaskCheckpoint:
|
|||||||
SET retry_count = ?,
|
SET retry_count = ?,
|
||||||
error_count = ?,
|
error_count = ?,
|
||||||
last_error = ?,
|
last_error = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
""", (retry_count, error_count, error_message, task_id))
|
""", (retry_count, error_count, error_message, cst_time, task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return 'retry'
|
return 'retry'
|
||||||
|
|
||||||
@@ -231,17 +240,18 @@ class TaskCheckpoint:
|
|||||||
"""完成任务"""
|
"""完成任务"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE task_checkpoints
|
UPDATE task_checkpoints
|
||||||
SET status = ?,
|
SET status = ?,
|
||||||
stage = ?,
|
stage = ?,
|
||||||
progress_percent = 100,
|
progress_percent = 100,
|
||||||
completed_at = CURRENT_TIMESTAMP,
|
completed_at = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
""", ('completed' if success else 'failed',
|
""", ('completed' if success else 'failed',
|
||||||
TaskStage.COMPLETED.value if success else TaskStage.FAILED.value,
|
TaskStage.COMPLETED.value if success else TaskStage.FAILED.value,
|
||||||
task_id))
|
cst_time, cst_time, task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def get_checkpoint(self, task_id):
|
def get_checkpoint(self, task_id):
|
||||||
@@ -327,13 +337,14 @@ class TaskCheckpoint:
|
|||||||
"""恢复暂停的任务"""
|
"""恢复暂停的任务"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE task_checkpoints
|
UPDATE task_checkpoints
|
||||||
SET status = 'running',
|
SET status = 'running',
|
||||||
retry_count = 0,
|
retry_count = 0,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE task_id = ? AND status = 'paused'
|
WHERE task_id = ? AND status = 'paused'
|
||||||
""", (task_id,))
|
""", (cst_time, task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
@@ -341,14 +352,15 @@ class TaskCheckpoint:
|
|||||||
"""放弃暂停的任务"""
|
"""放弃暂停的任务"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
cst_time = get_cst_now_str()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE task_checkpoints
|
UPDATE task_checkpoints
|
||||||
SET status = 'failed',
|
SET status = 'failed',
|
||||||
stage = ?,
|
stage = ?,
|
||||||
completed_at = CURRENT_TIMESTAMP,
|
completed_at = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = ?
|
||||||
WHERE task_id = ? AND status = 'paused'
|
WHERE task_id = ? AND status = 'paused'
|
||||||
""", (TaskStage.FAILED.value, task_id))
|
""", (TaskStage.FAILED.value, cst_time, cst_time, task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
@@ -359,7 +371,7 @@ class TaskCheckpoint:
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
DELETE FROM task_checkpoints
|
DELETE FROM task_checkpoints
|
||||||
WHERE status IN ('completed', 'failed')
|
WHERE status IN ('completed', 'failed')
|
||||||
AND datetime(completed_at) < datetime('now', '-' || ? || ' days')
|
AND datetime(completed_at) < datetime('now', 'localtime', '-' || ? || ' days')
|
||||||
""", (days,))
|
""", (days,))
|
||||||
deleted = cursor.rowcount
|
deleted = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -842,7 +842,6 @@
|
|||||||
<select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
<select id="scheduleBrowseType" style="max-width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;">
|
||||||
<option value="注册前未读">注册前未读</option>
|
<option value="注册前未读">注册前未读</option>
|
||||||
<option value="应读" selected>应读</option>
|
<option value="应读" selected>应读</option>
|
||||||
<option value="未读">未读</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1731,10 +1730,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VIP functions
|
// VIP functions
|
||||||
|
function parseBeijingDateTime(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const str = String(value).trim();
|
||||||
|
if (!str) return null;
|
||||||
|
let iso = str.includes('T') ? str : str.replace(' ', 'T');
|
||||||
|
// 统一按北京时间解析(除非字符串本身已带时区)
|
||||||
|
const hasTimezone = /([zZ]|[+-]\d{2}:\d{2})$/.test(iso);
|
||||||
|
if (!hasTimezone) iso = iso + '+08:00';
|
||||||
|
const dt = new Date(iso);
|
||||||
|
if (Number.isNaN(dt.getTime())) return null;
|
||||||
|
return dt;
|
||||||
|
}
|
||||||
|
|
||||||
function isVip(user) {
|
function isVip(user) {
|
||||||
if (!user.vip_expire_time) return false;
|
if (!user.vip_expire_time) return false;
|
||||||
const expireTime = new Date(user.vip_expire_time);
|
const expireTime = parseBeijingDateTime(user.vip_expire_time);
|
||||||
return expireTime > new Date();
|
return expireTime ? expireTime > new Date() : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVipBadge(user) {
|
function getVipBadge(user) {
|
||||||
@@ -1746,8 +1758,8 @@
|
|||||||
|
|
||||||
function getVipExpire(user) {
|
function getVipExpire(user) {
|
||||||
if (!isVip(user)) return '';
|
if (!isVip(user)) return '';
|
||||||
const expireTime = new Date(user.vip_expire_time);
|
const expireTime = parseBeijingDateTime(user.vip_expire_time);
|
||||||
const daysLeft = Math.ceil((expireTime - new Date()) / (1000*60*60*24));
|
const daysLeft = expireTime ? Math.ceil((expireTime - new Date()) / (1000*60*60*24)) : 0;
|
||||||
if (user.vip_expire_time === '2099-12-31 23:59:59') {
|
if (user.vip_expire_time === '2099-12-31 23:59:59') {
|
||||||
return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
|
return '<div class="user-info" style="color:#667eea;">永久VIP</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -556,7 +556,6 @@
|
|||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<select class="select-inline" id="batchBrowseType">
|
<select class="select-inline" id="batchBrowseType">
|
||||||
<option value="应读">应读</option>
|
<option value="应读">应读</option>
|
||||||
<option value="未读">未读</option>
|
|
||||||
<option value="注册前未读">注册前未读</option>
|
<option value="注册前未读">注册前未读</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
@@ -718,7 +717,6 @@
|
|||||||
<label class="form-label">浏览类型</label>
|
<label class="form-label">浏览类型</label>
|
||||||
<select class="form-select" id="scheduleBrowseType">
|
<select class="form-select" id="scheduleBrowseType">
|
||||||
<option value="应读">应读</option>
|
<option value="应读">应读</option>
|
||||||
<option value="未读">未读</option>
|
|
||||||
<option value="注册前未读">注册前未读</option>
|
<option value="注册前未读">注册前未读</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1313,7 +1311,7 @@
|
|||||||
'</div>' + progressHtml +
|
'</div>' + progressHtml +
|
||||||
'<div class="account-card-actions">' +
|
'<div class="account-card-actions">' +
|
||||||
'<select class="select-inline browse-type" data-id="' + acc.id + '">' +
|
'<select class="select-inline browse-type" data-id="' + acc.id + '">' +
|
||||||
'<option value="应读">应读</option><option value="未读">未读</option><option value="注册前未读">注册前未读</option>' +
|
'<option value="应读">应读</option><option value="注册前未读">注册前未读</option>' +
|
||||||
'</select>' +
|
'</select>' +
|
||||||
'<button class="btn btn-primary btn-small" onclick="startAccount(\'' + acc.id + '\')" ' + (isRunning ? 'disabled' : '') + '>启动</button>' +
|
'<button class="btn btn-primary btn-small" onclick="startAccount(\'' + acc.id + '\')" ' + (isRunning ? 'disabled' : '') + '>启动</button>' +
|
||||||
'<button class="btn btn-outlined btn-small" onclick="stopAccount(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + '>停止</button>' +
|
'<button class="btn btn-outlined btn-small" onclick="stopAccount(\'' + acc.id + '\')" ' + (!isRunning ? 'disabled' : '') + '>停止</button>' +
|
||||||
@@ -1868,7 +1866,6 @@
|
|||||||
|
|
||||||
function copyScreenshotImage(imgSrc) {
|
function copyScreenshotImage(imgSrc) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
|
||||||
img.onload = function() {
|
img.onload = function() {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
|
|||||||
Reference in New Issue
Block a user