Integrate KDocs auto-upload

This commit is contained in:
2026-01-07 12:32:41 +08:00
parent 5137addacc
commit 3bae759afc
52 changed files with 1348 additions and 106 deletions

View File

@@ -22,6 +22,9 @@ COPY requirements.txt .
# 安装Python依赖 # 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# 安装 Playwright 浏览器依赖与 Chromium
RUN python -m playwright install --with-deps chromium
# 复制应用程序文件 # 复制应用程序文件
COPY app.py . COPY app.py .
COPY database.py . COPY database.py .

View File

@@ -0,0 +1,16 @@
import { api } from './client'
export async function fetchKdocsStatus() {
const { data } = await api.get('/kdocs/status')
return data
}
export async function fetchKdocsQr() {
const { data } = await api.post('/kdocs/qr', {})
return data
}
export async function clearKdocsLogin() {
const { data } = await api.post('/kdocs/clear-login', {})
return data
}

View File

@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system' import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system'
import { fetchKdocsQr, fetchKdocsStatus, clearKdocsLogin } from '../api/kdocs'
import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy' import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy'
const loading = ref(false) const loading = ref(false)
@@ -29,6 +30,20 @@ const autoApproveEnabled = ref(false)
const autoApproveHourlyLimit = ref(10) const autoApproveHourlyLimit = ref(10)
const autoApproveVipDays = ref(7) const autoApproveVipDays = ref(7)
// 金山文档上传
const kdocsEnabled = ref(false)
const kdocsDocUrl = ref('')
const kdocsDefaultUnit = ref('')
const kdocsSheetName = ref('')
const kdocsSheetIndex = ref(0)
const kdocsUnitColumn = ref('A')
const kdocsImageColumn = ref('D')
const kdocsAdminNotifyEnabled = ref(false)
const kdocsAdminNotifyEmail = ref('')
const kdocsStatus = ref({})
const kdocsQrOpen = ref(false)
const kdocsQrImage = ref('')
const weekdaysOptions = [ const weekdaysOptions = [
{ label: '周一', value: '1' }, { label: '周一', value: '1' },
{ label: '周二', value: '2' }, { label: '周二', value: '2' },
@@ -63,7 +78,11 @@ function normalizeBrowseType(value) {
async function loadAll() { async function loadAll() {
loading.value = true loading.value = true
try { try {
const [system, proxy] = await Promise.all([fetchSystemConfig(), fetchProxyConfig()]) const [system, proxy, kdocsInfo] = await Promise.all([
fetchSystemConfig(),
fetchProxyConfig(),
fetchKdocsStatus().catch(() => ({})),
])
maxConcurrentGlobal.value = system.max_concurrent_global ?? 2 maxConcurrentGlobal.value = system.max_concurrent_global ?? 2
maxConcurrentPerAccount.value = system.max_concurrent_per_account ?? 1 maxConcurrentPerAccount.value = system.max_concurrent_per_account ?? 1
@@ -87,6 +106,17 @@ async function loadAll() {
proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1 proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1
proxyApiUrl.value = proxy.proxy_api_url || '' proxyApiUrl.value = proxy.proxy_api_url || ''
proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3 proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3
kdocsEnabled.value = (system.kdocs_enabled ?? 0) === 1
kdocsDocUrl.value = system.kdocs_doc_url || ''
kdocsDefaultUnit.value = system.kdocs_default_unit || ''
kdocsSheetName.value = system.kdocs_sheet_name || ''
kdocsSheetIndex.value = system.kdocs_sheet_index ?? 0
kdocsUnitColumn.value = (system.kdocs_unit_column || 'A').toUpperCase()
kdocsImageColumn.value = (system.kdocs_image_column || 'D').toUpperCase()
kdocsAdminNotifyEnabled.value = (system.kdocs_admin_notify_enabled ?? 0) === 1
kdocsAdminNotifyEmail.value = system.kdocs_admin_notify_email || ''
kdocsStatus.value = kdocsInfo || {}
} catch { } catch {
// handled by interceptor // handled by interceptor
} finally { } finally {
@@ -196,6 +226,64 @@ async function saveProxy() {
} }
} }
async function saveKdocsConfig() {
const payload = {
kdocs_enabled: kdocsEnabled.value ? 1 : 0,
kdocs_doc_url: kdocsDocUrl.value.trim(),
kdocs_default_unit: kdocsDefaultUnit.value.trim(),
kdocs_sheet_name: kdocsSheetName.value.trim(),
kdocs_sheet_index: Number(kdocsSheetIndex.value) || 0,
kdocs_unit_column: kdocsUnitColumn.value.trim().toUpperCase(),
kdocs_image_column: kdocsImageColumn.value.trim().toUpperCase(),
kdocs_admin_notify_enabled: kdocsAdminNotifyEnabled.value ? 1 : 0,
kdocs_admin_notify_email: kdocsAdminNotifyEmail.value.trim(),
}
try {
const res = await updateSystemConfig(payload)
ElMessage.success(res?.message || '表格配置已更新')
} catch {
// handled by interceptor
}
}
async function refreshKdocsStatus() {
try {
kdocsStatus.value = await fetchKdocsStatus()
} catch {
// handled by interceptor
}
}
async function onFetchKdocsQr() {
try {
const res = await fetchKdocsQr()
if (res?.logged_in) {
ElMessage.success('当前已登录,无需扫码')
await refreshKdocsStatus()
return
}
kdocsQrImage.value = res?.qr_image || ''
if (!kdocsQrImage.value) {
ElMessage.warning('未获取到二维码')
return
}
kdocsQrOpen.value = true
} catch {
// handled by interceptor
}
}
async function onClearKdocsLogin() {
try {
await clearKdocsLogin()
ElMessage.success('登录态已清除')
await refreshKdocsStatus()
} catch {
// handled by interceptor
}
}
async function onTestProxy() { async function onTestProxy() {
if (!proxyApiUrl.value.trim()) { if (!proxyApiUrl.value.trim()) {
ElMessage.error('请先输入代理API地址') ElMessage.error('请先输入代理API地址')
@@ -357,6 +445,73 @@ onMounted(loadAll)
<el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button> <el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button>
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">金山文档上传</h3>
<el-form label-width="130px">
<el-form-item label="启用上传">
<el-switch v-model="kdocsEnabled" />
<div class="help">表格结构变化时可先关闭避免错误上传</div>
</el-form-item>
<el-form-item label="文档链接">
<el-input v-model="kdocsDocUrl" placeholder="https://kdocs.cn/..." />
</el-form-item>
<el-form-item label="默认县区">
<el-input v-model="kdocsDefaultUnit" placeholder="如:道县(用户可覆盖)" />
</el-form-item>
<el-form-item label="Sheet名称">
<el-input v-model="kdocsSheetName" placeholder="留空使用第一个Sheet" />
</el-form-item>
<el-form-item label="Sheet序号">
<el-input-number v-model="kdocsSheetIndex" :min="0" :max="50" />
<div class="help">0 表示第一个Sheet</div>
</el-form-item>
<el-form-item label="县区列">
<el-input v-model="kdocsUnitColumn" placeholder="A" style="max-width: 120px" />
</el-form-item>
<el-form-item label="图片列">
<el-input v-model="kdocsImageColumn" placeholder="D" style="max-width: 120px" />
</el-form-item>
<el-form-item label="管理员通知">
<el-switch v-model="kdocsAdminNotifyEnabled" />
</el-form-item>
<el-form-item label="通知邮箱">
<el-input v-model="kdocsAdminNotifyEmail" placeholder="admin@example.com" />
</el-form-item>
</el-form>
<div class="row-actions">
<el-button type="primary" @click="saveKdocsConfig">保存表格配置</el-button>
<el-button @click="refreshKdocsStatus">刷新状态</el-button>
<el-button type="success" plain @click="onFetchKdocsQr">获取二维码</el-button>
<el-button type="danger" plain @click="onClearKdocsLogin">清除登录</el-button>
</div>
<div class="help">
登录状态
<span v-if="kdocsStatus.last_login_ok === true">已登录</span>
<span v-else-if="kdocsStatus.login_required">需要扫码</span>
<span v-else>未知</span>
· 队列 {{ kdocsStatus.queue_size || 0 }}
<span v-if="kdocsStatus.last_error">· 最近错误{{ kdocsStatus.last_error }}</span>
</div>
</el-card>
<el-dialog v-model="kdocsQrOpen" title="扫码登录" width="min(420px, 92vw)">
<div class="kdocs-qr">
<img v-if="kdocsQrImage" :src="`data:image/png;base64,${kdocsQrImage}`" alt="KDocs QR" />
<div class="help">请使用管理员微信扫码登录</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
@@ -378,6 +533,22 @@ onMounted(loadAll)
font-weight: 800; font-weight: 800;
} }
.kdocs-qr {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.kdocs-qr img {
width: 260px;
max-width: 100%;
border: 1px solid var(--app-border);
border-radius: 8px;
padding: 8px;
background: #fff;
}
.help { .help {
margin-top: 6px; margin-top: 6px;
font-size: 12px; font-size: 12px;

View File

@@ -30,3 +30,12 @@ export async function changePassword(payload) {
return data return data
} }
export async function fetchKdocsSettings() {
const { data } = await publicApi.get('/user/kdocs')
return data
}
export async function updateKdocsSettings(payload) {
const { data } = await publicApi.post('/user/kdocs', payload)
return data
}

View File

@@ -11,7 +11,9 @@ import {
changePassword, changePassword,
fetchEmailNotify, fetchEmailNotify,
fetchUserEmail, fetchUserEmail,
fetchKdocsSettings,
unbindEmail, unbindEmail,
updateKdocsSettings,
updateEmailNotify, updateEmailNotify,
} from '../api/settings' } from '../api/settings'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
@@ -111,6 +113,10 @@ const passwordForm = reactive({
confirm_password: '', confirm_password: '',
}) })
const kdocsLoading = ref(false)
const kdocsSaving = ref(false)
const kdocsUnitValue = ref('')
function syncIsMobile() { function syncIsMobile() {
isMobile.value = Boolean(mediaQuery?.matches) isMobile.value = Boolean(mediaQuery?.matches)
if (!isMobile.value) drawerOpen.value = false if (!isMobile.value) drawerOpen.value = false
@@ -231,7 +237,7 @@ async function openSettings() {
} }
async function loadSettings() { async function loadSettings() {
await Promise.all([loadEmailInfo(), loadEmailNotify()]) await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings()])
} }
async function loadEmailInfo() { async function loadEmailInfo() {
@@ -262,6 +268,30 @@ async function loadEmailNotify() {
} }
} }
async function loadKdocsSettings() {
kdocsLoading.value = true
try {
const data = await fetchKdocsSettings()
kdocsUnitValue.value = data?.kdocs_unit || ''
} catch {
kdocsUnitValue.value = ''
} finally {
kdocsLoading.value = false
}
}
async function saveKdocsSettings() {
kdocsSaving.value = true
try {
await updateKdocsSettings({ kdocs_unit: kdocsUnitValue.value.trim() })
ElMessage.success('已更新表格县区设置')
} catch {
// handled by interceptor
} finally {
kdocsSaving.value = false
}
}
async function onBindEmail() { async function onBindEmail() {
const email = bindEmailValue.value.trim().toLowerCase() const email = bindEmailValue.value.trim().toLowerCase()
if (!email) { if (!email) {
@@ -635,6 +665,24 @@ async function dismissAnnouncementPermanently() {
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="表格上传" name="kdocs">
<div v-loading="kdocsLoading" class="settings-section">
<el-form label-position="top">
<el-form-item label="县区(可选)">
<el-input v-model="kdocsUnitValue" placeholder="留空使用系统默认县区" />
</el-form-item>
<el-button type="primary" :loading="kdocsSaving" @click="saveKdocsSettings">保存</el-button>
</el-form>
<el-alert
type="info"
:closable="false"
title="自动上传开关在“账号管理”页面设置(测试功能)。"
show-icon
class="settings-hint"
/>
</div>
</el-tab-pane>
<el-tab-pane label="VIP信息" name="vip"> <el-tab-pane label="VIP信息" name="vip">
<div class="settings-section"> <div class="settings-section">
<el-alert <el-alert

View File

@@ -15,6 +15,7 @@ import {
updateAccount, updateAccount,
updateAccountRemark, updateAccountRemark,
} from '../api/accounts' } from '../api/accounts'
import { fetchKdocsSettings, updateKdocsSettings } from '../api/settings'
import { fetchRunStats } from '../api/stats' import { fetchRunStats } from '../api/stats'
import { useSocket } from '../composables/useSocket' import { useSocket } from '../composables/useSocket'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
@@ -57,6 +58,9 @@ watch(batchEnableScreenshot, (value) => {
} }
}) })
const kdocsAutoUpload = ref(false)
const kdocsSettingsLoading = ref(false)
const addOpen = ref(false) const addOpen = ref(false)
const editOpen = ref(false) const editOpen = ref(false)
const upgradeOpen = ref(false) const upgradeOpen = ref(false)
@@ -189,6 +193,30 @@ async function refreshAccounts() {
} }
} }
async function loadKdocsSettings() {
kdocsSettingsLoading.value = true
try {
const data = await fetchKdocsSettings()
kdocsAutoUpload.value = Number(data?.kdocs_auto_upload || 0) === 1
} catch {
kdocsAutoUpload.value = false
} finally {
kdocsSettingsLoading.value = false
}
}
async function onToggleKdocsAutoUpload(value) {
kdocsSettingsLoading.value = true
try {
await updateKdocsSettings({ kdocs_auto_upload: value ? 1 : 0 })
ElMessage.success(value ? '已开启自动上传(测试)' : '已关闭自动上传')
} catch (e) {
kdocsAutoUpload.value = !value
} finally {
kdocsSettingsLoading.value = false
}
}
async function onStart(acc) { async function onStart(acc) {
try { try {
await startAccount(acc.id, { browse_type: browseTypeById[acc.id] || '应读', enable_screenshot: batchEnableScreenshot.value }) await startAccount(acc.id, { browse_type: browseTypeById[acc.id] || '应读', enable_screenshot: batchEnableScreenshot.value })
@@ -524,6 +552,7 @@ onMounted(async () => {
unbindSocket = bindSocket() unbindSocket = bindSocket()
await refreshAccounts() await refreshAccounts()
await loadKdocsSettings()
await refreshStats() await refreshStats()
syncStatsPolling() syncStatsPolling()
}) })
@@ -612,6 +641,15 @@ onBeforeUnmount(() => {
<el-option v-for="opt in browseTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option v-for="opt in browseTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select> </el-select>
<el-switch v-model="batchEnableScreenshot" inline-prompt active-text="截图" inactive-text="不截图" /> <el-switch v-model="batchEnableScreenshot" inline-prompt active-text="截图" inactive-text="不截图" />
<el-switch
v-model="kdocsAutoUpload"
:disabled="kdocsSettingsLoading"
inline-prompt
active-text="上传"
inactive-text="不传"
@change="onToggleKdocsAutoUpload"
/>
<span class="app-muted">表格(测试)</span>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">

View File

@@ -122,6 +122,7 @@ class Config:
# ==================== 浏览器配置 ==================== # ==================== 浏览器配置 ====================
SCREENSHOTS_DIR = os.environ.get('SCREENSHOTS_DIR', '截图') SCREENSHOTS_DIR = os.environ.get('SCREENSHOTS_DIR', '截图')
COOKIES_DIR = os.environ.get('COOKIES_DIR', 'data/cookies') COOKIES_DIR = os.environ.get('COOKIES_DIR', 'data/cookies')
KDOCS_LOGIN_STATE_FILE = os.environ.get('KDOCS_LOGIN_STATE_FILE', 'data/kdocs_login_state.json')
# ==================== 公告图片上传配置 ==================== # ==================== 公告图片上传配置 ====================
ANNOUNCEMENT_IMAGE_DIR = os.environ.get('ANNOUNCEMENT_IMAGE_DIR', 'static/announcements') ANNOUNCEMENT_IMAGE_DIR = os.environ.get('ANNOUNCEMENT_IMAGE_DIR', 'static/announcements')

View File

@@ -100,6 +100,7 @@ from db.users import (
get_pending_users, get_pending_users,
get_user_by_id, get_user_by_id,
get_user_by_username, get_user_by_username,
get_user_kdocs_settings,
get_user_stats, get_user_stats,
get_user_vip_info, get_user_vip_info,
get_vip_config, get_vip_config,
@@ -108,6 +109,7 @@ from db.users import (
remove_user_vip, remove_user_vip,
set_default_vip_days, set_default_vip_days,
set_user_vip, set_user_vip,
update_user_kdocs_settings,
verify_user, verify_user,
) )
from db.security import record_login_context from db.security import record_login_context
@@ -118,7 +120,7 @@ config = get_config()
DB_FILE = config.DB_FILE DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理) # 数据库版本 (用于迁移管理)
DB_VERSION = 16 DB_VERSION = 17
# ==================== 系统配置缓存P1 / O-03 ==================== # ==================== 系统配置缓存P1 / O-03 ====================
@@ -194,6 +196,15 @@ def update_system_config(
auto_approve_enabled=None, auto_approve_enabled=None,
auto_approve_hourly_limit=None, auto_approve_hourly_limit=None,
auto_approve_vip_days=None, auto_approve_vip_days=None,
kdocs_enabled=None,
kdocs_doc_url=None,
kdocs_default_unit=None,
kdocs_sheet_name=None,
kdocs_sheet_index=None,
kdocs_unit_column=None,
kdocs_image_column=None,
kdocs_admin_notify_enabled=None,
kdocs_admin_notify_email=None,
): ):
"""更新系统配置(写入后立即失效缓存)。""" """更新系统配置(写入后立即失效缓存)。"""
ok = _update_system_config( ok = _update_system_config(
@@ -211,6 +222,15 @@ def update_system_config(
auto_approve_enabled=auto_approve_enabled, auto_approve_enabled=auto_approve_enabled,
auto_approve_hourly_limit=auto_approve_hourly_limit, auto_approve_hourly_limit=auto_approve_hourly_limit,
auto_approve_vip_days=auto_approve_vip_days, auto_approve_vip_days=auto_approve_vip_days,
kdocs_enabled=kdocs_enabled,
kdocs_doc_url=kdocs_doc_url,
kdocs_default_unit=kdocs_default_unit,
kdocs_sheet_name=kdocs_sheet_name,
kdocs_sheet_index=kdocs_sheet_index,
kdocs_unit_column=kdocs_unit_column,
kdocs_image_column=kdocs_image_column,
kdocs_admin_notify_enabled=kdocs_admin_notify_enabled,
kdocs_admin_notify_email=kdocs_admin_notify_email,
) )
if ok: if ok:
invalidate_system_config_cache() invalidate_system_config_cache()

View File

@@ -172,6 +172,15 @@ def get_system_config_raw() -> dict:
"auto_approve_enabled": 0, "auto_approve_enabled": 0,
"auto_approve_hourly_limit": 10, "auto_approve_hourly_limit": 10,
"auto_approve_vip_days": 7, "auto_approve_vip_days": 7,
"kdocs_enabled": 0,
"kdocs_doc_url": "",
"kdocs_default_unit": "",
"kdocs_sheet_name": "",
"kdocs_sheet_index": 0,
"kdocs_unit_column": "A",
"kdocs_image_column": "D",
"kdocs_admin_notify_enabled": 0,
"kdocs_admin_notify_email": "",
} }
@@ -191,6 +200,15 @@ def update_system_config(
auto_approve_enabled=None, auto_approve_enabled=None,
auto_approve_hourly_limit=None, auto_approve_hourly_limit=None,
auto_approve_vip_days=None, auto_approve_vip_days=None,
kdocs_enabled=None,
kdocs_doc_url=None,
kdocs_default_unit=None,
kdocs_sheet_name=None,
kdocs_sheet_index=None,
kdocs_unit_column=None,
kdocs_image_column=None,
kdocs_admin_notify_enabled=None,
kdocs_admin_notify_email=None,
) -> bool: ) -> bool:
"""更新系统配置仅更新DB不做缓存处理""" """更新系统配置仅更新DB不做缓存处理"""
allowed_fields = { allowed_fields = {
@@ -208,6 +226,15 @@ def update_system_config(
"auto_approve_enabled", "auto_approve_enabled",
"auto_approve_hourly_limit", "auto_approve_hourly_limit",
"auto_approve_vip_days", "auto_approve_vip_days",
"kdocs_enabled",
"kdocs_doc_url",
"kdocs_default_unit",
"kdocs_sheet_name",
"kdocs_sheet_index",
"kdocs_unit_column",
"kdocs_image_column",
"kdocs_admin_notify_enabled",
"kdocs_admin_notify_email",
"updated_at", "updated_at",
} }
@@ -258,6 +285,33 @@ def update_system_config(
if auto_approve_vip_days is not None: if auto_approve_vip_days is not None:
updates.append("auto_approve_vip_days = ?") updates.append("auto_approve_vip_days = ?")
params.append(auto_approve_vip_days) params.append(auto_approve_vip_days)
if kdocs_enabled is not None:
updates.append("kdocs_enabled = ?")
params.append(kdocs_enabled)
if kdocs_doc_url is not None:
updates.append("kdocs_doc_url = ?")
params.append(kdocs_doc_url)
if kdocs_default_unit is not None:
updates.append("kdocs_default_unit = ?")
params.append(kdocs_default_unit)
if kdocs_sheet_name is not None:
updates.append("kdocs_sheet_name = ?")
params.append(kdocs_sheet_name)
if kdocs_sheet_index is not None:
updates.append("kdocs_sheet_index = ?")
params.append(kdocs_sheet_index)
if kdocs_unit_column is not None:
updates.append("kdocs_unit_column = ?")
params.append(kdocs_unit_column)
if kdocs_image_column is not None:
updates.append("kdocs_image_column = ?")
params.append(kdocs_image_column)
if kdocs_admin_notify_enabled is not None:
updates.append("kdocs_admin_notify_enabled = ?")
params.append(kdocs_admin_notify_enabled)
if kdocs_admin_notify_email is not None:
updates.append("kdocs_admin_notify_email = ?")
params.append(kdocs_admin_notify_email)
if not updates: if not updates:
return False return False

View File

@@ -84,6 +84,9 @@ def migrate_database(conn, target_version: int) -> None:
if current_version < 16: if current_version < 16:
_migrate_to_v16(conn) _migrate_to_v16(conn)
current_version = 16 current_version = 16
if current_version < 17:
_migrate_to_v17(conn)
current_version = 17
if current_version != int(target_version): if current_version != int(target_version):
set_current_version(conn, int(target_version)) set_current_version(conn, int(target_version))
@@ -687,3 +690,41 @@ def _migrate_to_v16(conn):
cursor.execute("ALTER TABLE announcements ADD COLUMN image_url TEXT") cursor.execute("ALTER TABLE announcements ADD COLUMN image_url TEXT")
conn.commit() conn.commit()
print(" ✓ 添加 announcements.image_url 字段") print(" ✓ 添加 announcements.image_url 字段")
def _migrate_to_v17(conn):
"""迁移到版本17 - 金山文档上传配置与用户开关"""
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(system_config)")
columns = [col[1] for col in cursor.fetchall()]
system_fields = [
("kdocs_enabled", "INTEGER DEFAULT 0"),
("kdocs_doc_url", "TEXT DEFAULT ''"),
("kdocs_default_unit", "TEXT DEFAULT ''"),
("kdocs_sheet_name", "TEXT DEFAULT ''"),
("kdocs_sheet_index", "INTEGER DEFAULT 0"),
("kdocs_unit_column", "TEXT DEFAULT 'A'"),
("kdocs_image_column", "TEXT DEFAULT 'D'"),
("kdocs_admin_notify_enabled", "INTEGER DEFAULT 0"),
("kdocs_admin_notify_email", "TEXT DEFAULT ''"),
]
for field, ddl in system_fields:
if field not in columns:
cursor.execute(f"ALTER TABLE system_config ADD COLUMN {field} {ddl}")
print(f" ✓ 添加 system_config.{field} 字段")
cursor.execute("PRAGMA table_info(users)")
columns = [col[1] for col in cursor.fetchall()]
user_fields = [
("kdocs_unit", "TEXT DEFAULT ''"),
("kdocs_auto_upload", "INTEGER DEFAULT 0"),
]
for field, ddl in user_fields:
if field not in columns:
cursor.execute(f"ALTER TABLE users ADD COLUMN {field} {ddl}")
print(f" ✓ 添加 users.{field} 字段")
conn.commit()

View File

@@ -33,6 +33,8 @@ def ensure_schema(conn) -> None:
email TEXT, email TEXT,
email_verified INTEGER DEFAULT 0, email_verified INTEGER DEFAULT 0,
email_notify_enabled INTEGER DEFAULT 1, email_notify_enabled INTEGER DEFAULT 1,
kdocs_unit TEXT DEFAULT '',
kdocs_auto_upload INTEGER DEFAULT 0,
status TEXT DEFAULT 'approved', status TEXT DEFAULT 'approved',
vip_expire_time TIMESTAMP, vip_expire_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -213,6 +215,15 @@ def ensure_schema(conn) -> None:
auto_approve_enabled INTEGER DEFAULT 0, auto_approve_enabled INTEGER DEFAULT 0,
auto_approve_hourly_limit INTEGER DEFAULT 10, auto_approve_hourly_limit INTEGER DEFAULT 10,
auto_approve_vip_days INTEGER DEFAULT 7, auto_approve_vip_days INTEGER DEFAULT 7,
kdocs_enabled INTEGER DEFAULT 0,
kdocs_doc_url TEXT DEFAULT '',
kdocs_default_unit TEXT DEFAULT '',
kdocs_sheet_name TEXT DEFAULT '',
kdocs_sheet_index INTEGER DEFAULT 0,
kdocs_unit_column TEXT DEFAULT 'A',
kdocs_image_column TEXT DEFAULT 'D',
kdocs_admin_notify_enabled INTEGER DEFAULT 0,
kdocs_admin_notify_email TEXT DEFAULT '',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
""" """

View File

@@ -217,6 +217,39 @@ def get_user_by_id(user_id):
return dict(user) if user else None return dict(user) if user else None
def get_user_kdocs_settings(user_id):
"""获取用户的金山文档配置"""
user = get_user_by_id(user_id)
if not user:
return None
return {
"kdocs_unit": user.get("kdocs_unit") or "",
"kdocs_auto_upload": 1 if user.get("kdocs_auto_upload") else 0,
}
def update_user_kdocs_settings(user_id, *, kdocs_unit=None, kdocs_auto_upload=None) -> bool:
"""更新用户的金山文档配置"""
updates = []
params = []
if kdocs_unit is not None:
updates.append("kdocs_unit = ?")
params.append(kdocs_unit)
if kdocs_auto_upload is not None:
updates.append("kdocs_auto_upload = ?")
params.append(kdocs_auto_upload)
if not updates:
return False
params.append(user_id)
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute(f"UPDATE users SET {', '.join(updates)} WHERE id = ?", params)
conn.commit()
return cursor.rowcount > 0
def get_user_by_username(username): def get_user_by_username(username):
"""根据用户名获取用户""" """根据用户名获取用户"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:

View File

@@ -11,3 +11,4 @@ python-dotenv==1.0.0
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
cryptography>=41.0.0 cryptography>=41.0.0
Pillow>=10.0.0 Pillow>=10.0.0
playwright==1.42.0

View File

@@ -671,6 +671,15 @@ def update_system_config_api():
auto_approve_enabled = data.get("auto_approve_enabled") auto_approve_enabled = data.get("auto_approve_enabled")
auto_approve_hourly_limit = data.get("auto_approve_hourly_limit") auto_approve_hourly_limit = data.get("auto_approve_hourly_limit")
auto_approve_vip_days = data.get("auto_approve_vip_days") auto_approve_vip_days = data.get("auto_approve_vip_days")
kdocs_enabled = data.get("kdocs_enabled")
kdocs_doc_url = data.get("kdocs_doc_url")
kdocs_default_unit = data.get("kdocs_default_unit")
kdocs_sheet_name = data.get("kdocs_sheet_name")
kdocs_sheet_index = data.get("kdocs_sheet_index")
kdocs_unit_column = data.get("kdocs_unit_column")
kdocs_image_column = data.get("kdocs_image_column")
kdocs_admin_notify_enabled = data.get("kdocs_admin_notify_enabled")
kdocs_admin_notify_email = data.get("kdocs_admin_notify_email")
if max_concurrent is not None: if max_concurrent is not None:
if not isinstance(max_concurrent, int) or max_concurrent < 1: if not isinstance(max_concurrent, int) or max_concurrent < 1:
@@ -718,6 +727,66 @@ def update_system_config_api():
if not isinstance(auto_approve_vip_days, int) or auto_approve_vip_days < 0: if not isinstance(auto_approve_vip_days, int) or auto_approve_vip_days < 0:
return jsonify({"error": "注册赠送VIP天数不能为负数"}), 400 return jsonify({"error": "注册赠送VIP天数不能为负数"}), 400
if kdocs_enabled is not None:
if isinstance(kdocs_enabled, bool):
kdocs_enabled = 1 if kdocs_enabled else 0
if kdocs_enabled not in (0, 1):
return jsonify({"error": "表格上传开关必须是0或1"}), 400
if kdocs_doc_url is not None:
kdocs_doc_url = str(kdocs_doc_url or "").strip()
if kdocs_doc_url and not is_safe_outbound_url(kdocs_doc_url):
return jsonify({"error": "文档链接格式不正确"}), 400
if kdocs_default_unit is not None:
kdocs_default_unit = str(kdocs_default_unit or "").strip()
if len(kdocs_default_unit) > 50:
return jsonify({"error": "默认县区长度不能超过50"}), 400
if kdocs_sheet_name is not None:
kdocs_sheet_name = str(kdocs_sheet_name or "").strip()
if len(kdocs_sheet_name) > 50:
return jsonify({"error": "Sheet名称长度不能超过50"}), 400
if kdocs_sheet_index is not None:
try:
kdocs_sheet_index = int(kdocs_sheet_index)
except Exception:
return jsonify({"error": "Sheet序号必须是数字"}), 400
if kdocs_sheet_index < 0:
return jsonify({"error": "Sheet序号不能为负数"}), 400
if kdocs_unit_column is not None:
kdocs_unit_column = str(kdocs_unit_column or "").strip().upper()
if not kdocs_unit_column:
return jsonify({"error": "县区列不能为空"}), 400
import re
if not re.match(r"^[A-Z]{1,3}$", kdocs_unit_column):
return jsonify({"error": "县区列格式错误"}), 400
if kdocs_image_column is not None:
kdocs_image_column = str(kdocs_image_column or "").strip().upper()
if not kdocs_image_column:
return jsonify({"error": "图片列不能为空"}), 400
import re
if not re.match(r"^[A-Z]{1,3}$", kdocs_image_column):
return jsonify({"error": "图片列格式错误"}), 400
if kdocs_admin_notify_enabled is not None:
if isinstance(kdocs_admin_notify_enabled, bool):
kdocs_admin_notify_enabled = 1 if kdocs_admin_notify_enabled else 0
if kdocs_admin_notify_enabled not in (0, 1):
return jsonify({"error": "管理员通知开关必须是0或1"}), 400
if kdocs_admin_notify_email is not None:
kdocs_admin_notify_email = str(kdocs_admin_notify_email or "").strip()
if kdocs_admin_notify_email:
is_valid, error_msg = validate_email(kdocs_admin_notify_email)
if not is_valid:
return jsonify({"error": error_msg}), 400
old_config = database.get_system_config() or {} old_config = database.get_system_config() or {}
if not database.update_system_config( if not database.update_system_config(
@@ -732,6 +801,15 @@ def update_system_config_api():
auto_approve_enabled=auto_approve_enabled, auto_approve_enabled=auto_approve_enabled,
auto_approve_hourly_limit=auto_approve_hourly_limit, auto_approve_hourly_limit=auto_approve_hourly_limit,
auto_approve_vip_days=auto_approve_vip_days, auto_approve_vip_days=auto_approve_vip_days,
kdocs_enabled=kdocs_enabled,
kdocs_doc_url=kdocs_doc_url,
kdocs_default_unit=kdocs_default_unit,
kdocs_sheet_name=kdocs_sheet_name,
kdocs_sheet_index=kdocs_sheet_index,
kdocs_unit_column=kdocs_unit_column,
kdocs_image_column=kdocs_image_column,
kdocs_admin_notify_enabled=kdocs_admin_notify_enabled,
kdocs_admin_notify_email=kdocs_admin_notify_email,
): ):
return jsonify({"error": "更新失败"}), 400 return jsonify({"error": "更新失败"}), 400
@@ -763,6 +841,52 @@ def update_system_config_api():
return jsonify({"message": "系统配置已更新"}) return jsonify({"message": "系统配置已更新"})
@admin_api_bp.route("/kdocs/status", methods=["GET"])
@admin_required
def get_kdocs_status_api():
"""获取金山文档上传状态"""
try:
from services.kdocs_uploader import get_kdocs_uploader
uploader = get_kdocs_uploader()
status = uploader.get_status()
return jsonify(status)
except Exception as e:
return jsonify({"error": f"获取状态失败: {e}"}), 500
@admin_api_bp.route("/kdocs/qr", methods=["POST"])
@admin_required
def get_kdocs_qr_api():
"""获取金山文档登录二维码"""
try:
from services.kdocs_uploader import get_kdocs_uploader
uploader = get_kdocs_uploader()
result = uploader.request_qr()
if not result.get("success"):
return jsonify({"error": result.get("error", "获取二维码失败")}), 400
return jsonify(result)
except Exception as e:
return jsonify({"error": f"获取二维码失败: {e}"}), 500
@admin_api_bp.route("/kdocs/clear-login", methods=["POST"])
@admin_required
def clear_kdocs_login_api():
"""清除金山文档登录态"""
try:
from services.kdocs_uploader import get_kdocs_uploader
uploader = get_kdocs_uploader()
result = uploader.clear_login()
if not result.get("success"):
return jsonify({"error": result.get("error", "清除失败")}), 400
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": f"清除失败: {e}"}), 500
@admin_api_bp.route("/schedule/execute", methods=["POST"]) @admin_api_bp.route("/schedule/execute", methods=["POST"])
@admin_required @admin_required
def execute_schedule_now(): def execute_schedule_now():

View File

@@ -148,6 +148,50 @@ def get_user_email():
return jsonify({"email": user.get("email", ""), "email_verified": user.get("email_verified", False)}) return jsonify({"email": user.get("email", ""), "email_verified": user.get("email_verified", False)})
@api_user_bp.route("/api/user/kdocs", methods=["GET"])
@login_required
def get_user_kdocs_settings():
"""获取当前用户的金山文档设置"""
settings = database.get_user_kdocs_settings(current_user.id)
if not settings:
return jsonify({"kdocs_unit": "", "kdocs_auto_upload": 0})
return jsonify(settings)
@api_user_bp.route("/api/user/kdocs", methods=["POST"])
@login_required
def update_user_kdocs_settings():
"""更新当前用户的金山文档设置"""
data = request.get_json() or {}
kdocs_unit = data.get("kdocs_unit")
kdocs_auto_upload = data.get("kdocs_auto_upload")
if kdocs_unit is not None:
kdocs_unit = str(kdocs_unit or "").strip()
if len(kdocs_unit) > 50:
return jsonify({"error": "县区长度不能超过50"}), 400
if kdocs_auto_upload is not None:
if isinstance(kdocs_auto_upload, bool):
kdocs_auto_upload = 1 if kdocs_auto_upload else 0
try:
kdocs_auto_upload = int(kdocs_auto_upload)
except Exception:
return jsonify({"error": "自动上传开关必须是0或1"}), 400
if kdocs_auto_upload not in (0, 1):
return jsonify({"error": "自动上传开关必须是0或1"}), 400
if not database.update_user_kdocs_settings(
current_user.id,
kdocs_unit=kdocs_unit,
kdocs_auto_upload=kdocs_auto_upload,
):
return jsonify({"error": "更新失败"}), 400
settings = database.get_user_kdocs_settings(current_user.id) or {"kdocs_unit": "", "kdocs_auto_upload": 0}
return jsonify({"success": True, "settings": settings})
@api_user_bp.route("/api/user/bind-email", methods=["POST"]) @api_user_bp.route("/api/user/bind-email", methods=["POST"])
@login_required @login_required
@require_ip_not_locked @require_ip_not_locked

605
services/kdocs_uploader.py Normal file
View File

@@ -0,0 +1,605 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import base64
import os
import queue
import threading
import time
from typing import Any, Dict, Optional
import database
import email_service
from app_config import get_config
from services.client_log import log_to_client
from services.runtime import get_logger
try:
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
except Exception: # pragma: no cover - 运行环境缺少 playwright 时降级
sync_playwright = None
class PlaywrightTimeoutError(Exception):
pass
logger = get_logger("app")
config = get_config()
class KDocsUploader:
def __init__(self) -> None:
self._queue: queue.Queue = queue.Queue(maxsize=int(os.environ.get("KDOCS_QUEUE_MAXSIZE", "200")))
self._thread = threading.Thread(target=self._run, name="kdocs-uploader", daemon=True)
self._running = False
self._last_error: Optional[str] = None
self._last_success_at: Optional[float] = None
self._login_required = False
self._playwright = None
self._browser = None
self._context = None
self._page = None
self._last_qr_image: Optional[bytes] = None
self._last_login_check: float = 0.0
self._last_login_ok: Optional[bool] = None
def start(self) -> None:
if self._running:
return
self._running = True
self._thread.start()
def stop(self) -> None:
if not self._running:
return
self._running = False
self._queue.put({"action": "shutdown"})
def get_status(self) -> Dict[str, Any]:
return {
"queue_size": self._queue.qsize(),
"login_required": self._login_required,
"last_error": self._last_error,
"last_success_at": self._last_success_at,
"last_login_ok": self._last_login_ok,
}
def enqueue_upload(
self,
*,
user_id: int,
account_id: str,
unit: str,
name: str,
image_path: str,
) -> bool:
if not self._running:
self.start()
payload = {
"user_id": int(user_id),
"account_id": str(account_id),
"unit": unit,
"name": name,
"image_path": image_path,
}
try:
self._queue.put({"action": "upload", "payload": payload}, timeout=1)
return True
except queue.Full:
self._last_error = "上传队列已满"
return False
def request_qr(self, timeout: int = 30) -> Dict[str, Any]:
return self._submit_command("qr", timeout=timeout)
def clear_login(self, timeout: int = 20) -> Dict[str, Any]:
return self._submit_command("clear_login", timeout=timeout)
def refresh_login_status(self, timeout: int = 20) -> Dict[str, Any]:
return self._submit_command("status", timeout=timeout)
def _submit_command(self, action: str, timeout: int = 30) -> Dict[str, Any]:
if not self._running:
self.start()
resp_queue: queue.Queue = queue.Queue(maxsize=1)
self._queue.put({"action": action, "response": resp_queue})
try:
return resp_queue.get(timeout=timeout)
except queue.Empty:
return {"success": False, "error": "操作超时"}
def _run(self) -> None:
while True:
task = self._queue.get()
if not task:
continue
action = task.get("action")
if action == "shutdown":
break
try:
if action == "upload":
self._handle_upload(task.get("payload") or {})
elif action == "qr":
result = self._handle_qr()
task.get("response").put(result)
elif action == "clear_login":
result = self._handle_clear_login()
task.get("response").put(result)
elif action == "status":
result = self._handle_status_check()
task.get("response").put(result)
except Exception as e:
logger.warning(f"[KDocs] 处理任务失败: {e}")
self._cleanup_browser()
def _load_system_config(self) -> Dict[str, Any]:
return database.get_system_config() or {}
def _ensure_playwright(self) -> bool:
if sync_playwright is None:
self._last_error = "playwright 未安装"
return False
try:
if self._playwright is None:
self._playwright = sync_playwright().start()
if self._browser is None:
headless = os.environ.get("KDOCS_HEADLESS", "true").lower() != "false"
self._browser = self._playwright.chromium.launch(headless=headless)
if self._context is None:
storage_state = getattr(config, "KDOCS_LOGIN_STATE_FILE", "data/kdocs_login_state.json")
if os.path.exists(storage_state):
self._context = self._browser.new_context(storage_state=storage_state)
else:
self._context = self._browser.new_context()
if self._page is None or self._page.is_closed():
self._page = self._context.new_page()
self._page.set_default_timeout(int(getattr(config, "DEFAULT_TIMEOUT", 60000)))
return True
except Exception as e:
self._last_error = f"浏览器启动失败: {e}"
self._cleanup_browser()
return False
def _cleanup_browser(self) -> None:
try:
if self._page:
self._page.close()
except Exception:
pass
self._page = None
try:
if self._context:
self._context.close()
except Exception:
pass
self._context = None
try:
if self._browser:
self._browser.close()
except Exception:
pass
self._browser = None
try:
if self._playwright:
self._playwright.stop()
except Exception:
pass
self._playwright = None
def _open_document(self, doc_url: str) -> bool:
try:
self._page.goto(doc_url)
time.sleep(1)
return True
except Exception as e:
self._last_error = f"打开文档失败: {e}"
return False
def _is_logged_in(self) -> bool:
try:
login_btn = self._page.get_by_role("button", name="登录并加入编辑")
if login_btn.is_visible(timeout=1500):
return False
except Exception:
return True
return True
def _ensure_login_dialog(self) -> None:
try:
login_btn = self._page.get_by_role("button", name="登录并加入编辑")
if login_btn.is_visible(timeout=1500):
login_btn.click()
time.sleep(1)
except Exception:
pass
try:
wechat_btn = self._page.get_by_role("button", name="微信登录")
if wechat_btn.is_visible(timeout=3000):
wechat_btn.click()
time.sleep(1.5)
except Exception:
pass
def _capture_qr_image(self) -> Optional[bytes]:
candidates = [
self._page.locator("canvas"),
self._page.locator("img[alt*='二维码']"),
self._page.locator("img[src*='qr']"),
]
for locator in candidates:
try:
if locator.count() < 1:
continue
target = locator.first
target.wait_for(state="visible", timeout=5000)
return target.screenshot()
except PlaywrightTimeoutError:
continue
except Exception:
continue
try:
return self._page.screenshot(full_page=True)
except Exception:
return None
def _save_login_state(self) -> None:
try:
storage_state = getattr(config, "KDOCS_LOGIN_STATE_FILE", "data/kdocs_login_state.json")
os.makedirs(os.path.dirname(storage_state), exist_ok=True)
self._context.storage_state(path=storage_state)
except Exception as e:
logger.warning(f"[KDocs] 保存登录态失败: {e}")
def _handle_qr(self) -> Dict[str, Any]:
cfg = self._load_system_config()
doc_url = (cfg.get("kdocs_doc_url") or "").strip()
if not doc_url:
return {"success": False, "error": "未配置金山文档链接"}
if not self._ensure_playwright():
return {"success": False, "error": self._last_error or "浏览器不可用"}
if not self._open_document(doc_url):
return {"success": False, "error": self._last_error or "打开文档失败"}
if self._is_logged_in():
self._login_required = False
self._last_login_ok = True
self._save_login_state()
return {"success": True, "logged_in": True, "qr_image": ""}
self._ensure_login_dialog()
qr_image = self._capture_qr_image()
if not qr_image:
self._last_error = "二维码获取失败"
return {"success": False, "error": self._last_error}
self._last_qr_image = qr_image
self._login_required = True
return {
"success": True,
"logged_in": False,
"qr_image": base64.b64encode(qr_image).decode("ascii"),
}
def _handle_clear_login(self) -> Dict[str, Any]:
storage_state = getattr(config, "KDOCS_LOGIN_STATE_FILE", "data/kdocs_login_state.json")
try:
if os.path.exists(storage_state):
os.remove(storage_state)
except Exception as e:
return {"success": False, "error": f"清除登录态失败: {e}"}
self._login_required = False
self._last_login_ok = None
self._cleanup_browser()
return {"success": True}
def _handle_status_check(self) -> Dict[str, Any]:
cfg = self._load_system_config()
doc_url = (cfg.get("kdocs_doc_url") or "").strip()
if not doc_url:
return {"success": True, "logged_in": False, "error": "未配置文档链接"}
if not self._ensure_playwright():
return {"success": False, "logged_in": False, "error": self._last_error or "浏览器不可用"}
if not self._open_document(doc_url):
return {"success": False, "logged_in": False, "error": self._last_error or "打开文档失败"}
logged_in = self._is_logged_in()
self._last_login_ok = logged_in
self._login_required = not logged_in
if logged_in:
self._save_login_state()
return {"success": True, "logged_in": logged_in}
def _handle_upload(self, payload: Dict[str, Any]) -> None:
cfg = self._load_system_config()
if int(cfg.get("kdocs_enabled", 0) or 0) != 1:
return
doc_url = (cfg.get("kdocs_doc_url") or "").strip()
if not doc_url:
return
unit = (payload.get("unit") or "").strip()
name = (payload.get("name") or "").strip()
image_path = payload.get("image_path")
user_id = payload.get("user_id")
account_id = payload.get("account_id")
if not unit or not name:
return
if not image_path or not os.path.exists(image_path):
return
if not self._ensure_playwright():
self._notify_admin(unit, name, image_path, self._last_error or "浏览器不可用")
return
if not self._open_document(doc_url):
self._notify_admin(unit, name, image_path, self._last_error or "打开文档失败")
return
if not self._is_logged_in():
self._login_required = True
self._last_login_ok = False
self._notify_admin(unit, name, image_path, "登录已失效,请管理员重新扫码登录")
return
self._login_required = False
self._last_login_ok = True
sheet_name = (cfg.get("kdocs_sheet_name") or "").strip()
sheet_index = int(cfg.get("kdocs_sheet_index") or 0)
unit_col = (cfg.get("kdocs_unit_column") or "A").strip().upper()
image_col = (cfg.get("kdocs_image_column") or "D").strip().upper()
success = False
error_msg = ""
for attempt in range(2):
try:
if sheet_name or sheet_index:
self._select_sheet(sheet_name, sheet_index)
row_num = self._find_person_with_unit(unit, name, unit_col)
if row_num < 0:
error_msg = f"未找到人员: {unit}-{name}"
break
success = self._upload_image_to_cell(row_num, image_path, image_col)
if success:
break
except Exception as e:
error_msg = str(e)
if success:
self._last_success_at = time.time()
self._last_error = None
log_to_client(f"已上传表格截图: {unit}-{name}", user_id, account_id)
return
if not error_msg:
error_msg = "上传失败"
self._last_error = error_msg
self._notify_admin(unit, name, image_path, error_msg)
log_to_client(f"表格上传失败: {error_msg}", user_id, account_id)
def _notify_admin(self, unit: str, name: str, image_path: str, error: str) -> None:
cfg = self._load_system_config()
if int(cfg.get("kdocs_admin_notify_enabled", 0) or 0) != 1:
return
to_email = (cfg.get("kdocs_admin_notify_email") or "").strip()
if not to_email:
return
settings = email_service.get_email_settings()
if not settings.get("enabled", False):
return
subject = "金山文档上传失败提醒"
body = (
f"上传失败\n\n人员: {unit}-{name}\n图片: {image_path}\n错误: {error}\n\n"
"请检查登录状态或表格配置。"
)
try:
email_service.send_email_async(
to_email=to_email,
subject=subject,
body=body,
email_type="kdocs_upload_failed",
)
except Exception as e:
logger.warning(f"[KDocs] 发送管理员邮件失败: {e}")
def _select_sheet(self, sheet_name: str, sheet_index: int) -> None:
if sheet_name:
candidates = [
self._page.locator("[role='tab']").filter(has_text=sheet_name),
self._page.locator(".sheet-tab").filter(has_text=sheet_name),
self._page.locator(".sheet-tab-name").filter(has_text=sheet_name),
]
for locator in candidates:
try:
if locator.count() < 1:
continue
locator.first.click()
time.sleep(0.5)
return
except Exception:
continue
if sheet_index > 0:
idx = sheet_index - 1
candidates = [
self._page.locator("[role='tab']"),
self._page.locator(".sheet-tab"),
self._page.locator(".sheet-tab-name"),
]
for locator in candidates:
try:
if locator.count() <= idx:
continue
locator.nth(idx).click()
time.sleep(0.5)
return
except Exception:
continue
def _get_current_cell_address(self) -> str:
name_box = self._page.locator('#root input[type="text"]').first
return name_box.input_value()
def _navigate_to_cell(self, cell_address: str) -> None:
name_box = self._page.locator('#root input[type="text"]').first
name_box.click()
name_box.fill(cell_address)
name_box.press("Enter")
time.sleep(0.3)
def _get_cell_value(self, cell_address: str) -> str:
self._navigate_to_cell(cell_address)
time.sleep(0.3)
try:
formula_bar = self._page.locator('input[type="text"]').nth(1)
value = formula_bar.input_value()
if value:
return value.strip()
except Exception:
pass
try:
formula_inputs = self._page.locator('textbox')
for i in range(formula_inputs.count()):
try:
value = formula_inputs.nth(i).input_value()
if value:
import re
if not re.match(r"^[A-Z]+\d+$", value.strip()):
return value.strip()
except Exception:
continue
except Exception:
pass
return ""
def _search_person(self, name: str) -> None:
self._page.keyboard.press("Control+f")
time.sleep(0.3)
search_input = None
try:
search_input = self._page.get_by_role("textbox").nth(3)
search_input.fill(name)
except Exception:
try:
search_input = self._page.locator("input[placeholder*='查找']").first
search_input.fill(name)
except Exception:
pass
time.sleep(0.2)
try:
find_btn = self._page.get_by_role("button", name="查找").nth(2)
find_btn.click()
except Exception:
try:
self._page.get_by_role("button", name="查找").first.click()
except Exception:
pass
time.sleep(0.3)
def _find_next(self) -> None:
try:
find_btn = self._page.get_by_role("button", name="查找").nth(2)
find_btn.click()
except Exception:
try:
self._page.get_by_role("button", name="查找").first.click()
except Exception:
pass
time.sleep(0.3)
def _close_search(self) -> None:
self._page.keyboard.press("Escape")
time.sleep(0.2)
def _extract_row_number(self, cell_address: str) -> int:
import re
match = re.search(r"(\\d+)$", cell_address)
if match:
return int(match.group(1))
return -1
def _verify_unit_by_navigation(self, row_num: int, unit: str, unit_col: str) -> bool:
cell_address = f"{unit_col}{row_num}"
cell_value = self._get_cell_value(cell_address)
if cell_value:
return cell_value == unit
return False
def _find_person_with_unit(self, unit: str, name: str, unit_col: str, max_attempts: int = 50) -> int:
self._search_person(name)
found_rows = set()
for _ in range(max_attempts):
self._close_search()
current_address = self._get_current_cell_address()
row_num = self._extract_row_number(current_address)
if row_num == -1:
return -1
if row_num in found_rows:
return -1
found_rows.add(row_num)
if self._verify_unit_by_navigation(row_num, unit, unit_col):
return row_num
self._page.keyboard.press("Control+f")
time.sleep(0.2)
self._find_next()
return -1
def _upload_image_to_cell(self, row_num: int, image_path: str, image_col: str) -> bool:
cell_address = f"{image_col}{row_num}"
self._navigate_to_cell(cell_address)
time.sleep(0.3)
try:
self._page.keyboard.press("Delete")
time.sleep(0.1)
self._page.keyboard.press("Backspace")
except Exception:
pass
try:
insert_btn = self._page.get_by_role("button", name="插入")
insert_btn.click()
time.sleep(0.3)
except Exception as e:
raise RuntimeError(f"打开插入菜单失败: {e}")
try:
image_btn = self._page.get_by_role("button", name="图片")
image_btn.click()
time.sleep(0.3)
cell_image_option = self._page.get_by_role("option", name="单元格图片")
cell_image_option.click()
time.sleep(0.2)
except Exception as e:
raise RuntimeError(f"选择单元格图片失败: {e}")
try:
local_option = self._page.get_by_role("option", name="本地")
with self._page.expect_file_chooser() as fc_info:
local_option.click()
file_chooser = fc_info.value
file_chooser.set_files(image_path)
except Exception as e:
raise RuntimeError(f"上传文件失败: {e}")
time.sleep(2)
return True
_kdocs_uploader: Optional[KDocsUploader] = None
def get_kdocs_uploader() -> KDocsUploader:
global _kdocs_uploader
if _kdocs_uploader is None:
_kdocs_uploader = KDocsUploader()
_kdocs_uploader.start()
return _kdocs_uploader

View File

@@ -388,6 +388,29 @@ def take_screenshot_for_account(
account_name = account.remark if account.remark else account.username account_name = account.remark if account.remark else account.username
try:
if screenshot_path and result and result.get("success"):
cfg = database.get_system_config() or {}
if int(cfg.get("kdocs_enabled", 0) or 0) == 1:
doc_url = (cfg.get("kdocs_doc_url") or "").strip()
if doc_url:
user_cfg = database.get_user_kdocs_settings(user_id) or {}
if int(user_cfg.get("kdocs_auto_upload", 0) or 0) == 1:
unit = (user_cfg.get("kdocs_unit") or cfg.get("kdocs_default_unit") or "").strip()
name = (account.remark or "").strip()
if unit and name:
from services.kdocs_uploader import get_kdocs_uploader
get_kdocs_uploader().enqueue_upload(
user_id=user_id,
account_id=account_id,
unit=unit,
name=name,
image_path=screenshot_path,
)
except Exception as kdocs_error:
logger.warning(f"表格上传任务提交失败: {kdocs_error}")
if batch_id: if batch_id:
_batch_task_record_result( _batch_task_record_result(
batch_id=batch_id, batch_id=batch_id,

View File

@@ -1,34 +1,34 @@
{ {
"_email-Ct3wXHpw.js": { "_email-B1605y-H.js": {
"file": "assets/email-Ct3wXHpw.js", "file": "assets/email-B1605y-H.js",
"name": "email", "name": "email",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_system-BTlwVb4V.js": { "_system-C--cT22w.js": {
"file": "assets/system-BTlwVb4V.js", "file": "assets/system-C--cT22w.js",
"name": "system", "name": "system",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_tasks-DjrYX1kb.js": { "_tasks-BNEjyFS7.js": {
"file": "assets/tasks-DjrYX1kb.js", "file": "assets/tasks-BNEjyFS7.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-DXwhA9ak.js": { "_users-Z6zennED.js": {
"file": "assets/users-DXwhA9ak.js", "file": "assets/users-Z6zennED.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"index.html": { "index.html": {
"file": "assets/index-CRihQT8G.js", "file": "assets/index-a8w3mx3g.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -48,7 +48,7 @@
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-BVJSt6Za.js", "file": "assets/AnnouncementsPage-B4NSG18W.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -60,12 +60,12 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-BU-aLz58.js", "file": "assets/EmailPage-bnS2lxdA.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-Ct3wXHpw.js", "_email-B1605y-H.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -73,7 +73,7 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-BeAqV1pz.js", "file": "assets/FeedbacksPage-VbmcQLrX.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -85,13 +85,13 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-C25hwKy7.js", "file": "assets/LogsPage-CYxgYYkM.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DXwhA9ak.js", "_users-Z6zennED.js",
"_tasks-DjrYX1kb.js", "_tasks-BNEjyFS7.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -99,22 +99,22 @@
] ]
}, },
"src/pages/ReportPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/ReportPage-DcTJhEeS.js", "file": "assets/ReportPage-CWobMgMS.js",
"name": "ReportPage", "name": "ReportPage",
"src": "src/pages/ReportPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_email-Ct3wXHpw.js", "_email-B1605y-H.js",
"_tasks-DjrYX1kb.js", "_tasks-BNEjyFS7.js",
"_system-BTlwVb4V.js" "_system-C--cT22w.js"
], ],
"css": [ "css": [
"assets/ReportPage-Q8rCsG8A.css" "assets/ReportPage-Q8rCsG8A.css"
] ]
}, },
"src/pages/SecurityPage.vue": { "src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-6yFvpr3a.js", "file": "assets/SecurityPage-CBvaqgN-.js",
"name": "SecurityPage", "name": "SecurityPage",
"src": "src/pages/SecurityPage.vue", "src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -126,7 +126,7 @@
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-CixYahEQ.js", "file": "assets/SettingsPage-DTIxDo9J.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -138,25 +138,25 @@
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-oGtrzs5v.js", "file": "assets/SystemPage-C7s6uqSA.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_system-BTlwVb4V.js", "_system-C--cT22w.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/SystemPage-DdMZ1omu.css" "assets/SystemPage-H89YK9Pc.css"
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-1x6SHDoq.js", "file": "assets/UsersPage-CIttj4S8.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DXwhA9ak.js", "_users-Z6zennED.js",
"index.html" "index.html"
], ],
"css": [ "css": [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1 @@
.page-stack[data-v-965e79dd]{display:flex;flex-direction:column;gap:12px}.card[data-v-965e79dd]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-965e79dd]{margin:0 0 12px;font-size:14px;font-weight:800}.kdocs-qr[data-v-965e79dd]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-965e79dd]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-965e79dd]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-965e79dd]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{a as n}from"./index-CRihQT8G.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u}; import{a as n}from"./index-a8w3mx3g.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{a}from"./index-CRihQT8G.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function o(){const{data:t}=await a.post("/schedule/execute",{});return t}export{o as e,s as f,c as u}; import{a}from"./index-a8w3mx3g.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function o(){const{data:t}=await a.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};

View File

@@ -1 +1 @@
import{a}from"./index-CRihQT8G.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f}; import{a}from"./index-a8w3mx3g.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
{ {
"_accounts-XHQJZlkc.js": { "_accounts--hQB0HMJ.js": {
"file": "assets/accounts-XHQJZlkc.js", "file": "assets/accounts--hQB0HMJ.js",
"name": "accounts", "name": "accounts",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_auth-CXXxlo9_.js": { "_auth-Cm-kEDXF.js": {
"file": "assets/auth-CXXxlo9_.js", "file": "assets/auth-Cm-kEDXF.js",
"name": "auth", "name": "auth",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"index.html": { "index.html": {
"file": "assets/index-B3Praptc.js", "file": "assets/index-D7kwaKcv.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -28,68 +28,68 @@
"src/pages/ScreenshotsPage.vue" "src/pages/ScreenshotsPage.vue"
], ],
"css": [ "css": [
"assets/index-HG0IyblY.css" "assets/index-BVjJVlht.css"
] ]
}, },
"src/pages/AccountsPage.vue": { "src/pages/AccountsPage.vue": {
"file": "assets/AccountsPage-BWLUZGwv.js", "file": "assets/AccountsPage-DOK2DZIQ.js",
"name": "AccountsPage", "name": "AccountsPage",
"src": "src/pages/AccountsPage.vue", "src": "src/pages/AccountsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-XHQJZlkc.js", "_accounts--hQB0HMJ.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/AccountsPage-CkDdMK5Q.css" "assets/AccountsPage-Cwi1Wpgg.css"
] ]
}, },
"src/pages/LoginPage.vue": { "src/pages/LoginPage.vue": {
"file": "assets/LoginPage-iG0xN46c.js", "file": "assets/LoginPage-Bh9f42Ek.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-CXXxlo9_.js" "_auth-Cm-kEDXF.js"
], ],
"css": [ "css": [
"assets/LoginPage-CnwOLKJz.css" "assets/LoginPage-CnwOLKJz.css"
] ]
}, },
"src/pages/RegisterPage.vue": { "src/pages/RegisterPage.vue": {
"file": "assets/RegisterPage-DKtQs7_M.js", "file": "assets/RegisterPage-DXsTO3Cf.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-CXXxlo9_.js" "_auth-Cm-kEDXF.js"
], ],
"css": [ "css": [
"assets/RegisterPage-BOcNcW5D.css" "assets/RegisterPage-BOcNcW5D.css"
] ]
}, },
"src/pages/ResetPasswordPage.vue": { "src/pages/ResetPasswordPage.vue": {
"file": "assets/ResetPasswordPage-DY3NSKCZ.js", "file": "assets/ResetPasswordPage-CbAWo2Ml.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-CXXxlo9_.js" "_auth-Cm-kEDXF.js"
], ],
"css": [ "css": [
"assets/ResetPasswordPage-DybfLMAw.css" "assets/ResetPasswordPage-DybfLMAw.css"
] ]
}, },
"src/pages/SchedulesPage.vue": { "src/pages/SchedulesPage.vue": {
"file": "assets/SchedulesPage-CtGw4yPv.js", "file": "assets/SchedulesPage-CmS2H_0Y.js",
"name": "SchedulesPage", "name": "SchedulesPage",
"src": "src/pages/SchedulesPage.vue", "src": "src/pages/SchedulesPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-XHQJZlkc.js", "_accounts--hQB0HMJ.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -97,7 +97,7 @@
] ]
}, },
"src/pages/ScreenshotsPage.vue": { "src/pages/ScreenshotsPage.vue": {
"file": "assets/ScreenshotsPage-BfoUmvOa.js", "file": "assets/ScreenshotsPage-BU4GP4nV.js",
"name": "ScreenshotsPage", "name": "ScreenshotsPage",
"src": "src/pages/ScreenshotsPage.vue", "src": "src/pages/ScreenshotsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -109,7 +109,7 @@
] ]
}, },
"src/pages/VerifyResultPage.vue": { "src/pages/VerifyResultPage.vue": {
"file": "assets/VerifyResultPage-QlobZKo3.js", "file": "assets/VerifyResultPage-DUMGEC2F.js",
"name": "VerifyResultPage", "name": "VerifyResultPage",
"src": "src/pages/VerifyResultPage.vue", "src": "src/pages/VerifyResultPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1 +1 @@
import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as w,g as m,F as T,k,h as q,i as x,j as z,t as G,v as H,E as y}from"./index-B3Praptc.js";import{c as J}from"./auth-CXXxlo9_.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),_=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&&!_.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=H(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await J({token:r.value,new_password:o}),_.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const f=p?.response?.data;y.error(f?.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"),f=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return w(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=m("div",{class:"brand"},[m("div",{class:"brand-title"},"知识管理平台"),m("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(w(),v(T,{key:1},[_.value?(w(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:_.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(f,{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(f,{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"]),m("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(w(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(w(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),m("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},oe=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{oe 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 w,g as m,F as T,k,h as q,i as x,j as z,t as G,v as H,E as y}from"./index-D7kwaKcv.js";import{c as J}from"./auth-Cm-kEDXF.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),_=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&&!_.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=H(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await J({token:r.value,new_password:o}),_.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const f=p?.response?.data;y.error(f?.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"),f=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return w(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=m("div",{class:"brand"},[m("div",{class:"brand-title"},"知识管理平台"),m("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(w(),v(T,{key:1},[_.value?(w(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:_.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(f,{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(f,{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"]),m("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(w(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(w(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),m("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},oe=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{oe as default};

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<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-B3Praptc.js"></script> <script type="module" crossorigin src="./assets/index-D7kwaKcv.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-HG0IyblY.css"> <link rel="stylesheet" crossorigin href="./assets/index-BVjJVlht.css">
</head> </head>
<body> <body>
<noscript>该页面需要启用 JavaScript 才能使用。</noscript> <noscript>该页面需要启用 JavaScript 才能使用。</noscript>