diff --git a/admin-frontend/src/api/system.js b/admin-frontend/src/api/system.js index c5afaf1..6364e1e 100644 --- a/admin-frontend/src/api/system.js +++ b/admin-frontend/src/api/system.js @@ -20,3 +20,19 @@ export async function executeScheduleNow() { const { data } = await api.post('/schedule/execute', {}) return data } + +export async function fetchSocialLoginConfig() { + const { data } = await api.get('/social-login/config') + return data +} + +export async function updateSocialLoginConfig(payload) { + const { data } = await api.post('/social-login/config', payload || {}) + systemConfigGetter.clear() + return data +} + +export async function testSocialLoginConfig(payload) { + const { data } = await api.post('/social-login/test', payload || {}) + return data +} diff --git a/admin-frontend/src/pages/SystemPage.vue b/admin-frontend/src/pages/SystemPage.vue index 0de2b02..3904669 100644 --- a/admin-frontend/src/pages/SystemPage.vue +++ b/admin-frontend/src/pages/SystemPage.vue @@ -2,7 +2,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' -import { fetchSystemConfig, updateSystemConfig } from '../api/system' +import { fetchSocialLoginConfig, fetchSystemConfig, testSocialLoginConfig, updateSocialLoginConfig, updateSystemConfig } from '../api/system' import { fetchKdocsQr, fetchKdocsStatus, clearKdocsLogin } from '../api/kdocs' import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy' import { getCachedKdocsStatus, preloadKdocsStatus, updateCachedKdocsStatus } from '../utils/kdocsStatusCache' @@ -22,6 +22,21 @@ const autoApproveEnabled = ref(false) const autoApproveHourlyLimit = ref(10) const autoApproveVipDays = ref(7) +const socialLoginEnabled = ref(false) +const socialLoginEndpoint = ref('https://www.spacezs.cn/connect.php') +const socialLoginAppid = ref('') +const socialLoginAppkey = ref('') +const socialLoginAppkeyMasked = ref('') +const socialLoginAppkeyConfigured = ref(false) +const socialLoginProviders = ref(['wx']) +const socialLoginSaving = ref(false) +const socialLoginTesting = ref(false) +const socialProviderOptions = [ + { label: 'QQ', value: 'qq' }, + { label: '微信', value: 'wx' }, + { label: '支付宝', value: 'alipay' }, +] + const kdocsEnabled = ref(false) const kdocsDocUrl = ref('') const kdocsDefaultUnit = ref('') @@ -83,9 +98,10 @@ function setKdocsHint(message) { async function loadAll() { loading.value = true try { - const [system, proxy] = await Promise.all([ + const [system, proxy, social] = await Promise.all([ fetchSystemConfig(), fetchProxyConfig(), + fetchSocialLoginConfig(), ]) maxConcurrentGlobal.value = system.max_concurrent_global ?? 2 @@ -112,6 +128,16 @@ async function loadAll() { kdocsRowEnd.value = system.kdocs_row_end ?? 0 kdocsAdminNotifyEnabled.value = (system.kdocs_admin_notify_enabled ?? 0) === 1 kdocsAdminNotifyEmail.value = system.kdocs_admin_notify_email || '' + + socialLoginEnabled.value = (social.social_login_enabled ?? 0) === 1 + socialLoginEndpoint.value = social.social_login_endpoint || 'https://www.spacezs.cn/connect.php' + socialLoginAppid.value = social.social_login_appid || '' + socialLoginAppkey.value = '' + socialLoginAppkeyMasked.value = social.social_login_appkey_masked || '' + socialLoginAppkeyConfigured.value = Boolean(social.social_login_appkey_configured) + socialLoginProviders.value = Array.isArray(social.social_login_providers) && social.social_login_providers.length + ? social.social_login_providers + : ['wx'] } catch { // handled by interceptor } finally { @@ -233,6 +259,78 @@ async function saveAutoApprove() { } } +function socialLoginPayload() { + return { + social_login_enabled: socialLoginEnabled.value ? 1 : 0, + social_login_endpoint: socialLoginEndpoint.value.trim() || 'https://www.spacezs.cn/connect.php', + social_login_appid: socialLoginAppid.value.trim(), + social_login_appkey: socialLoginAppkey.value.trim(), + social_login_providers: socialLoginProviders.value, + } +} + +function validateSocialLoginForm() { + if (!socialLoginEnabled.value) return true + if (!socialLoginProviders.value.length) { + ElMessage.error('请选择至少一种登录方式') + return false + } + if (!socialLoginEndpoint.value.trim()) { + ElMessage.error('请输入聚合接口地址') + return false + } + if (!socialLoginAppid.value.trim()) { + ElMessage.error('请输入 APPID') + return false + } + if (!socialLoginAppkey.value.trim() && !socialLoginAppkeyConfigured.value) { + ElMessage.error('请输入 APPKEY') + return false + } + return true +} + +async function saveSocialLogin() { + if (!validateSocialLoginForm()) return + socialLoginSaving.value = true + try { + const res = await updateSocialLoginConfig(socialLoginPayload()) + const cfg = res?.config || {} + socialLoginAppkey.value = '' + socialLoginAppkeyMasked.value = cfg.social_login_appkey_masked || socialLoginAppkeyMasked.value + socialLoginAppkeyConfigured.value = Boolean(cfg.social_login_appkey_configured) + ElMessage.success(res?.message || '聚合登录配置已保存') + } catch { + // handled by interceptor + } finally { + socialLoginSaving.value = false + } +} + +async function onTestSocialLogin() { + if (!validateSocialLoginForm()) return + socialLoginTesting.value = true + try { + const url = new URL(window.location.href) + url.pathname = '/login' + url.search = '' + url.hash = '' + const provider = socialLoginProviders.value[0] || 'wx' + const res = await testSocialLoginConfig({ + ...socialLoginPayload(), + provider, + redirect_uri: url.toString(), + }) + await ElMessageBox.alert(res?.success ? '连接正常' : '测试完成', '聚合登录测试', { + confirmButtonText: '知道了', + }) + } catch { + // handled by interceptor + } finally { + socialLoginTesting.value = false + } +} + async function saveKdocsConfig() { const payload = { kdocs_enabled: kdocsEnabled.value ? 1 : 0, @@ -459,6 +557,43 @@ onMounted(loadAll) 保存注册设置 + + + 聚合登录 + QQ、微信、支付宝快捷登录 + + + + + + + + + + {{ item.label }} + + + + + + + + + + + + + + + 当前:{{ socialLoginAppkeyMasked }} + + + + + 保存聚合登录 + 测试连接 + + @@ -743,6 +878,12 @@ onMounted(loadAll) } } +@media (min-width: 1201px) { + .social-card { + grid-column: span 3; + } +} + @media (max-width: 768px) { .config-grid { grid-template-columns: 1fr; diff --git a/app-frontend/package-lock.json b/app-frontend/package-lock.json index 68b8dd6..e149b46 100644 --- a/app-frontend/package-lock.json +++ b/app-frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.12.2", "element-plus": "^2.11.3", "pinia": "^3.0.3", + "qrcode.vue": "^3.6.0", "socket.io-client": "^4.8.1", "vue": "^3.5.24", "vue-router": "^4.6.3" @@ -1991,6 +1992,15 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/qrcode.vue": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.9.1.tgz", + "integrity": "sha512-CpHVRz5iveqwRFh+nzzSYV9hPWU6q+YSOKyq5ZievjQIBv4bIIDzajGgtNz/yYSlczjAkYM3GNAQJHwwCukMEQ==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", diff --git a/app-frontend/package.json b/app-frontend/package.json index 657c4df..f5b7316 100644 --- a/app-frontend/package.json +++ b/app-frontend/package.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "element-plus": "^2.11.3", "pinia": "^3.0.3", + "qrcode.vue": "^3.6.0", "socket.io-client": "^4.8.1", "vue": "^3.5.24", "vue-router": "^4.6.3" diff --git a/app-frontend/src/api/auth.js b/app-frontend/src/api/auth.js index 76a8414..16b1757 100644 --- a/app-frontend/src/api/auth.js +++ b/app-frontend/src/api/auth.js @@ -44,3 +44,23 @@ export async function confirmPasswordReset(payload) { const { data } = await publicApi.post('/reset-password-confirm', payload) return data } + +export async function fetchSocialConfig() { + const { data } = await publicApi.get('/auth/social/config') + return data +} + +export async function socialLoginUrl(payload) { + const { data } = await publicApi.post('/auth/social/login-url', payload || {}) + return data +} + +export async function socialPoll(payload) { + const { data } = await publicApi.post('/auth/social/poll', payload || {}) + return data +} + +export async function socialCallback(payload) { + const { data } = await publicApi.post('/auth/social/callback', payload || {}) + return data +} diff --git a/app-frontend/src/api/settings.js b/app-frontend/src/api/settings.js index 9636e47..fd05a65 100644 --- a/app-frontend/src/api/settings.js +++ b/app-frontend/src/api/settings.js @@ -69,3 +69,18 @@ export async function reportUserPasskeyClientError(payload) { const { data } = await publicApi.post('/user/passkeys/client-error', payload || {}) return data } + +export async function fetchSocialBindings() { + const { data } = await publicApi.get('/user/social-bindings') + return data +} + +export async function bindSocial(payload) { + const { data } = await publicApi.post('/user/social-bindings', payload || {}) + return data +} + +export async function unbindSocial(provider) { + const { data } = await publicApi.delete(`/user/social-bindings/${encodeURIComponent(provider)}`) + return data +} diff --git a/app-frontend/src/components/SocialLoginButtons.vue b/app-frontend/src/components/SocialLoginButtons.vue new file mode 100644 index 0000000..5fd6c44 --- /dev/null +++ b/app-frontend/src/components/SocialLoginButtons.vue @@ -0,0 +1,238 @@ + + + + + + {{ providerIcon(provider) }} + {{ mode === 'bind' ? `绑定${providerLabels[provider]}` : `${providerLabels[provider]}登录` }} + + + + + + {{ qrPrompt(qrProvider) }} + + + + + + diff --git a/app-frontend/src/layouts/AppLayout.vue b/app-frontend/src/layouts/AppLayout.vue index 99d47c2..daa386d 100644 --- a/app-frontend/src/layouts/AppLayout.vue +++ b/app-frontend/src/layouts/AppLayout.vue @@ -7,8 +7,10 @@ import 'element-plus/es/components/message/style/css' import 'element-plus/es/components/message-box/style/css' import { fetchActiveAnnouncement, dismissAnnouncement } from '../api/announcements' +import { fetchSocialConfig } from '../api/auth' import { fetchMyFeedbacks, submitFeedback } from '../api/feedback' import { + bindSocial, bindEmail, changePassword, createUserPasskeyOptions, @@ -18,11 +20,14 @@ import { fetchUserPasskeys, fetchUserEmail, fetchKdocsSettings, + fetchSocialBindings, reportUserPasskeyClientError, + unbindSocial, unbindEmail, updateKdocsSettings, updateEmailNotify, } from '../api/settings' +import SocialLoginButtons from '../components/SocialLoginButtons.vue' import { useUserStore } from '../stores/user' import { createPasskey, getPasskeyClientErrorMessage, isPasskeyAvailable } from '../utils/passkey' import { validateStrongPassword } from '../utils/password' @@ -132,6 +137,12 @@ const passkeyRegisterOptions = ref(null) const passkeyRegisterOptionsAt = ref(0) const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000 +const socialConfig = ref({ enabled: false, providers: [] }) +const socialBindings = ref([]) +const socialBindingsLoading = ref(false) +const socialBindLoading = ref(false) +const pendingSettingsBindKey = 'zsglpt_social_settings_bind_token' + function syncIsMobile() { isMobile.value = Boolean(mediaQuery?.matches) if (!isMobile.value) drawerOpen.value = false @@ -262,7 +273,76 @@ async function openSettings() { } async function loadSettings() { - await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings(), loadPasskeys()]) + await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings(), loadPasskeys(), loadSocialBindings()]) +} + +function socialBindRedirectUri() { + const url = new URL(window.location.href) + url.pathname = '/social-bind-callback' + url.search = '' + url.hash = '' + return url.toString() +} + +async function loadSocialBindings() { + socialBindingsLoading.value = true + try { + const [config, bindings] = await Promise.all([fetchSocialConfig(), fetchSocialBindings()]) + socialConfig.value = { + enabled: Boolean(config?.enabled), + providers: Array.isArray(config?.providers) ? config.providers : [], + } + socialBindings.value = Array.isArray(bindings?.items) ? bindings.items : [] + await consumePendingSocialBind() + } catch { + socialConfig.value = { enabled: false, providers: [] } + socialBindings.value = [] + } finally { + socialBindingsLoading.value = false + } +} + +async function consumePendingSocialBind() { + let token = '' + try { + token = window.sessionStorage.getItem(pendingSettingsBindKey) || '' + } catch { + token = '' + } + if (!token || socialBindLoading.value) return + socialBindLoading.value = true + try { + await bindSocial({ bind_token: token }) + window.sessionStorage.removeItem(pendingSettingsBindKey) + ElMessage.success('快捷登录已绑定') + const bindings = await fetchSocialBindings() + socialBindings.value = Array.isArray(bindings?.items) ? bindings.items : [] + } catch (e) { + const data = e?.response?.data + ElMessage.error(data?.error || '快捷登录绑定失败') + } finally { + socialBindLoading.value = false + } +} + +async function onUnbindSocial(item) { + try { + await ElMessageBox.confirm(`确定解绑${item?.provider_label || '快捷登录'}吗?`, '解绑快捷登录', { + confirmButtonText: '解绑', + cancelButtonText: '取消', + type: 'warning', + }) + } catch { + return + } + try { + await unbindSocial(item.provider) + ElMessage.success('已解绑') + await loadSocialBindings() + } catch (e) { + const data = e?.response?.data + ElMessage.error(data?.error || '解绑失败') + } } async function loadEmailInfo() { @@ -838,6 +918,37 @@ async function dismissAnnouncementPermanently() { + + + + + + + + + {{ item.provider_label }} + 已绑定 + 未绑定 + + + {{ item.nickname || '已授权账号' }} + + + 解绑 + + + + ElMessage.error(message)" + /> + + + + @@ -1101,6 +1212,43 @@ async function dismissAnnouncementPermanently() { margin-top: 10px; } +.social-binding-list { + display: grid; + gap: 10px; + margin-bottom: 14px; +} + +.social-binding-row { + min-height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--app-border); + border-radius: 10px; + background: #fff; +} + +.social-binding-main { + min-width: 0; +} + +.social-binding-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 800; +} + +.social-binding-meta { + margin-top: 4px; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .vip-info { margin-top: 12px; display: grid; diff --git a/app-frontend/src/pages/LoginPage.vue b/app-frontend/src/pages/LoginPage.vue index a7b3e83..165e5bf 100644 --- a/app-frontend/src/pages/LoginPage.vue +++ b/app-frontend/src/pages/LoginPage.vue @@ -1,6 +1,8 @@ + + + + + + {{ statusText }} + + + + + diff --git a/app-frontend/src/router/index.js b/app-frontend/src/router/index.js index 6e35aa2..40e791e 100644 --- a/app-frontend/src/router/index.js +++ b/app-frontend/src/router/index.js @@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router' const LoginPage = () => import('../pages/LoginPage.vue') const RegisterPage = () => import('../pages/RegisterPage.vue') const ResetPasswordPage = () => import('../pages/ResetPasswordPage.vue') +const SocialBindCallbackPage = () => import('../pages/SocialBindCallbackPage.vue') const VerifyResultPage = () => import('../pages/VerifyResultPage.vue') const AppLayout = () => import('../layouts/AppLayout.vue') @@ -15,6 +16,7 @@ const routes = [ { path: '/login', name: 'login', component: LoginPage }, { path: '/register', name: 'register', component: RegisterPage }, { path: '/reset-password/:token', name: 'reset_password', component: ResetPasswordPage }, + { path: '/social-bind-callback', name: 'social_bind_callback', component: SocialBindCallbackPage }, { path: '/api/verify-email/:token', name: 'verify_email', component: VerifyResultPage }, { path: '/api/verify-bind-email/:token', name: 'verify_bind_email', component: VerifyResultPage }, { diff --git a/app.py b/app.py index be6f0a2..c2b9c22 100644 --- a/app.py +++ b/app.py @@ -218,6 +218,9 @@ def enforce_csrf_protection(): "/api/login", "/api/auth/login", "/api/generate_captcha", + "/api/auth/social/login-url", + "/api/auth/social/poll", + "/api/auth/social/callback", "/yuyx/api/passkeys/login/options", "/yuyx/api/passkeys/login/verify", "/api/passkeys/login/options", diff --git a/database.py b/database.py index 575e508..96f5d84 100644 --- a/database.py +++ b/database.py @@ -105,12 +105,19 @@ from db.schedules import ( from db.tasks import create_task_log, delete_old_task_logs, get_task_logs, get_task_stats, get_user_run_stats from db.users import ( approve_user, + cleanup_expired_social_pending_binds, create_user, + create_social_pending_bind, delete_user, + delete_social_login_binding, + delete_social_pending_bind, extend_user_vip, + find_social_login_binding, + find_user_social_login_binding, get_all_users, get_users_count, get_pending_users, + get_social_pending_bind, get_user_by_id, get_user_by_username, get_user_kdocs_settings, @@ -122,7 +129,10 @@ from db.users import ( remove_user_vip, set_default_vip_days, set_user_vip, + list_social_login_bindings, + update_social_login_binding_profile, update_user_kdocs_settings, + upsert_social_login_binding, verify_user, ) from db.security import record_login_context @@ -134,7 +144,7 @@ logger = get_logger(__name__) DB_FILE = config.DB_FILE # 数据库版本 (用于迁移管理) -DB_VERSION = 21 +DB_VERSION = 22 # ==================== 系统配置缓存(P1 / O-03) ==================== @@ -261,6 +271,11 @@ def update_system_config( kdocs_row_start=None, kdocs_row_end=None, db_slow_query_ms=None, + social_login_enabled=None, + social_login_endpoint=None, + social_login_appid=None, + social_login_appkey=None, + social_login_providers=None, ): """更新系统配置(写入后立即失效缓存)。""" ok = _update_system_config( @@ -290,6 +305,11 @@ def update_system_config( kdocs_row_start=kdocs_row_start, kdocs_row_end=kdocs_row_end, db_slow_query_ms=db_slow_query_ms, + social_login_enabled=social_login_enabled, + social_login_endpoint=social_login_endpoint, + social_login_appid=social_login_appid, + social_login_appkey=social_login_appkey, + social_login_providers=social_login_providers, ) if ok: invalidate_system_config_cache() diff --git a/db/admin.py b/db/admin.py index b9b5255..a178af0 100644 --- a/db/admin.py +++ b/db/admin.py @@ -42,6 +42,11 @@ _DEFAULT_SYSTEM_CONFIG = { "kdocs_admin_notify_email": "", "kdocs_row_start": 0, "kdocs_row_end": 0, + "social_login_enabled": 0, + "social_login_endpoint": "https://www.spacezs.cn/connect.php", + "social_login_appid": "", + "social_login_appkey": "", + "social_login_providers": "qq,wx,alipay", } _SYSTEM_CONFIG_UPDATERS = ( @@ -71,6 +76,11 @@ _SYSTEM_CONFIG_UPDATERS = ( ("kdocs_admin_notify_email", "kdocs_admin_notify_email"), ("kdocs_row_start", "kdocs_row_start"), ("kdocs_row_end", "kdocs_row_end"), + ("social_login_enabled", "social_login_enabled"), + ("social_login_endpoint", "social_login_endpoint"), + ("social_login_appid", "social_login_appid"), + ("social_login_appkey", "social_login_appkey"), + ("social_login_providers", "social_login_providers"), ) @@ -316,6 +326,11 @@ def update_system_config( kdocs_row_start=None, kdocs_row_end=None, db_slow_query_ms=None, + social_login_enabled=None, + social_login_endpoint=None, + social_login_appid=None, + social_login_appkey=None, + social_login_providers=None, ) -> bool: """更新系统配置(仅更新DB,不做缓存处理)。""" arg_values = { @@ -345,6 +360,11 @@ def update_system_config( "kdocs_row_start": kdocs_row_start, "kdocs_row_end": kdocs_row_end, "db_slow_query_ms": db_slow_query_ms, + "social_login_enabled": social_login_enabled, + "social_login_endpoint": social_login_endpoint, + "social_login_appid": social_login_appid, + "social_login_appkey": social_login_appkey, + "social_login_providers": social_login_providers, } updates = [] diff --git a/db/migrations.py b/db/migrations.py index 573cb96..052d13a 100644 --- a/db/migrations.py +++ b/db/migrations.py @@ -76,6 +76,7 @@ def _get_migration_steps(): (19, _migrate_to_v19), (20, _migrate_to_v20), (21, _migrate_to_v21), + (22, _migrate_to_v22), ] @@ -933,3 +934,71 @@ def _migrate_to_v21(conn): ) conn.commit() + + +def _migrate_to_v22(conn): + """迁移到版本22 - Space 聚合登录配置、绑定与短期待绑定凭证。""" + cursor = conn.cursor() + + system_columns = _get_table_columns(cursor, "system_config") + system_fields = [ + ("social_login_enabled", "INTEGER DEFAULT 0"), + ("social_login_endpoint", "TEXT DEFAULT 'https://www.spacezs.cn/connect.php'"), + ("social_login_appid", "TEXT DEFAULT ''"), + ("social_login_appkey", "TEXT DEFAULT ''"), + ("social_login_providers", "TEXT DEFAULT 'qq,wx,alipay'"), + ] + for field, ddl in system_fields: + _add_column_if_missing( + cursor, + "system_config", + system_columns, + field, + ddl, + ok_message=f" [OK] 添加 system_config.{field} 字段", + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS social_login_bindings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + provider TEXT NOT NULL, + social_uid TEXT NOT NULL, + nickname TEXT DEFAULT '', + avatar_url TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP, + UNIQUE (provider, social_uid), + UNIQUE (user_id, provider), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + """ + ) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)") + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)" + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS social_pending_binds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT NOT NULL UNIQUE, + provider TEXT NOT NULL, + social_uid TEXT NOT NULL, + nickname TEXT DEFAULT '', + avatar_url TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL + ) + """ + ) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)") + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)" + ) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)") + + conn.commit() diff --git a/db/schema.py b/db/schema.py index 101fd32..5f21b20 100644 --- a/db/schema.py +++ b/db/schema.py @@ -246,11 +246,52 @@ def ensure_schema(conn) -> None: kdocs_admin_notify_email TEXT DEFAULT '', kdocs_row_start INTEGER DEFAULT 0, kdocs_row_end INTEGER DEFAULT 0, + social_login_enabled INTEGER DEFAULT 0, + social_login_endpoint TEXT DEFAULT 'https://www.spacezs.cn/connect.php', + social_login_appid TEXT DEFAULT '', + social_login_appkey TEXT DEFAULT '', + social_login_providers TEXT DEFAULT 'qq,wx,alipay', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) + # 聚合登录绑定表 + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS social_login_bindings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + provider TEXT NOT NULL, + social_uid TEXT NOT NULL, + nickname TEXT DEFAULT '', + avatar_url TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP, + UNIQUE (provider, social_uid), + UNIQUE (user_id, provider), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + """ + ) + + # 聚合登录短期待绑定凭证表 + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS social_pending_binds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT NOT NULL UNIQUE, + provider TEXT NOT NULL, + social_uid TEXT NOT NULL, + nickname TEXT DEFAULT '', + avatar_url TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL + ) + """ + ) + # 任务日志表 cursor.execute( """ @@ -389,6 +430,11 @@ def ensure_schema(conn) -> None: cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_ips_user ON login_ips(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner ON passkeys(owner_type, owner_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_created_at ON threat_events(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)") diff --git a/db/users.py b/db/users.py index 1a6e49c..a423a55 100644 --- a/db/users.py +++ b/db/users.py @@ -297,6 +297,156 @@ def get_user_by_username(username): return _get_user_by_field("username", username) +# ==================== 聚合登录绑定 ==================== + + +def cleanup_expired_social_pending_binds() -> None: + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM social_pending_binds WHERE expires_at < ?", (get_cst_now_str(),)) + conn.commit() + + +def create_social_pending_bind(*, token: str, provider: str, social_uid: str, nickname: str = "", avatar_url: str = "", expires_at: str) -> dict: + cleanup_expired_social_pending_binds() + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "DELETE FROM social_pending_binds WHERE provider = ? AND social_uid = ?", + (provider, social_uid), + ) + cursor.execute( + """ + INSERT INTO social_pending_binds (token, provider, social_uid, nickname, avatar_url, created_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + token, + provider, + social_uid, + str(nickname or "")[:128], + str(avatar_url or "")[:512], + get_cst_now_str(), + expires_at, + ), + ) + conn.commit() + cursor.execute("SELECT * FROM social_pending_binds WHERE token = ?", (token,)) + return _row_to_dict(cursor.fetchone()) + + +def get_social_pending_bind(token: str): + cleanup_expired_social_pending_binds() + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM social_pending_binds WHERE token = ?", ((token or "").strip(),)) + return _row_to_dict(cursor.fetchone()) + + +def delete_social_pending_bind(token: str) -> bool: + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM social_pending_binds WHERE token = ?", ((token or "").strip(),)) + conn.commit() + return cursor.rowcount > 0 + + +def find_social_login_binding(provider: str, social_uid: str): + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT * FROM social_login_bindings WHERE provider = ? AND social_uid = ?", + (provider, social_uid), + ) + return _row_to_dict(cursor.fetchone()) + + +def find_user_social_login_binding(user_id: int, provider: str): + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT * FROM social_login_bindings WHERE user_id = ? AND provider = ?", + (int(user_id), provider), + ) + return _row_to_dict(cursor.fetchone()) + + +def upsert_social_login_binding(*, user_id: int, provider: str, social_uid: str, nickname: str = "", avatar_url: str = ""): + with db_pool.get_db() as conn: + cursor = conn.cursor() + now = get_cst_now_str() + try: + cursor.execute( + """ + INSERT INTO social_login_bindings ( + user_id, provider, social_uid, nickname, avatar_url, created_at, updated_at, last_login_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id, provider) DO UPDATE SET + social_uid = excluded.social_uid, + nickname = excluded.nickname, + avatar_url = excluded.avatar_url, + updated_at = excluded.updated_at, + last_login_at = excluded.last_login_at + """, + ( + int(user_id), + provider, + social_uid, + str(nickname or "")[:128], + str(avatar_url or "")[:512], + now, + now, + now, + ), + ) + conn.commit() + except sqlite3.IntegrityError: + conn.rollback() + return None + return find_user_social_login_binding(user_id, provider) + + +def update_social_login_binding_profile(binding_id: int, *, nickname: str = "", avatar_url: str = "") -> bool: + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + """ + UPDATE social_login_bindings + SET nickname = ?, avatar_url = ?, updated_at = ?, last_login_at = ? + WHERE id = ? + """, + (str(nickname or "")[:128], str(avatar_url or "")[:512], get_cst_now_str(), get_cst_now_str(), int(binding_id)), + ) + conn.commit() + return cursor.rowcount > 0 + + +def list_social_login_bindings(user_id: int) -> list[dict]: + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT * FROM social_login_bindings + WHERE user_id = ? + ORDER BY created_at ASC + """, + (int(user_id),), + ) + return [dict(row) for row in cursor.fetchall()] + + +def delete_social_login_binding(user_id: int, provider: str) -> bool: + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute( + "DELETE FROM social_login_bindings WHERE user_id = ? AND provider = ?", + (int(user_id), provider), + ) + conn.commit() + return cursor.rowcount > 0 + + def _normalize_limit_offset(limit, offset, *, max_limit: int = 500): normalized_limit = None if limit is not None: diff --git a/routes/__init__.py b/routes/__init__.py index 439daa9..694d050 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -10,6 +10,7 @@ def register_blueprints(app) -> None: from routes.api_auth import api_auth_bp from routes.api_schedules import api_schedules_bp from routes.api_screenshots import api_screenshots_bp + from routes.api_social import api_social_bp from routes.api_user import api_user_bp from routes.health import health_bp from routes.pages import pages_bp @@ -17,6 +18,7 @@ def register_blueprints(app) -> None: app.register_blueprint(pages_bp) app.register_blueprint(health_bp) app.register_blueprint(api_auth_bp) + app.register_blueprint(api_social_bp) app.register_blueprint(api_user_bp) app.register_blueprint(api_accounts_bp) app.register_blueprint(api_screenshots_bp) diff --git a/routes/api_auth.py b/routes/api_auth.py index 26514d2..b852304 100644 --- a/routes/api_auth.py +++ b/routes/api_auth.py @@ -30,6 +30,7 @@ from services.passkeys import ( normalize_device_name, verify_authentication, ) +from services.social_login import parse_providers, provider_label from services.state import ( check_ip_request_rate, check_email_rate_limit, @@ -228,6 +229,7 @@ def register(): email = data.get("email", "").strip().lower() captcha_session = data.get("captcha_session", "") captcha_code = data.get("captcha", "").strip() + social_bind_token = str(data.get("social_bind_token") or "").strip() if not username or not password: return jsonify({"error": "用户名和密码不能为空"}), 400 @@ -273,6 +275,26 @@ def register(): user_id = database.create_user(username, password, email) if user_id: + social_bind_message = "" + if social_bind_token: + pending = database.get_social_pending_bind(social_bind_token) + if pending: + provider = str(pending.get("provider") or "").strip().lower() + social_uid = str(pending.get("social_uid") or "").strip() + enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers")) + existing_identity = database.find_social_login_binding(provider, social_uid) + if provider in enabled_providers and social_uid and not existing_identity: + binding = database.upsert_social_login_binding( + user_id=user_id, + provider=provider, + social_uid=social_uid, + nickname=pending.get("nickname") or "", + avatar_url=pending.get("avatar_url") or "", + ) + if binding: + database.delete_social_pending_bind(social_bind_token) + social_bind_message = f",已绑定{provider_label(provider)}" + if auto_approve_enabled: if auto_approve_vip_days > 0: database.set_user_vip(user_id, auto_approve_vip_days) @@ -281,7 +303,7 @@ def register(): result = email_service.send_register_verification_email(email=email, username=username, user_id=user_id) if result["success"]: message = _with_vip_suffix( - "注册成功!验证邮件已发送(可直接登录,建议完成邮箱验证)", + f"注册成功!验证邮件已发送(可直接登录,建议完成邮箱验证){social_bind_message}", auto_approve_enabled, auto_approve_vip_days, ) @@ -289,13 +311,15 @@ def register(): logger.error(f"注册验证邮件发送失败: {result['error']}") message = _with_vip_suffix( - f"注册成功,但验证邮件发送失败({result['error']})。你仍可直接登录", + f"注册成功,但验证邮件发送失败({result['error']})。你仍可直接登录{social_bind_message}", auto_approve_enabled, auto_approve_vip_days, ) return jsonify({"success": True, "message": message, "need_verify": True}) message = _with_vip_suffix("注册成功!可直接登录", auto_approve_enabled, auto_approve_vip_days) + if social_bind_message: + message = f"{message}{social_bind_message}" return jsonify({"success": True, "message": message}) return jsonify({"error": "用户名已存在"}), 400 diff --git a/routes/api_social.py b/routes/api_social.py new file mode 100644 index 0000000..08d9d67 --- /dev/null +++ b/routes/api_social.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +from datetime import timedelta +import database +from app_logger import get_logger +from db.utils import get_cst_now, get_cst_now_str +from flask import Blueprint, jsonify, request +from flask_login import current_user, login_required, login_user +from services.accounts_service import load_user_accounts +from services.models import User +from services.social_login import ( + BIND_TOKEN_TTL_SECONDS, + PROVIDER_LABELS, + SocialLoginError, + SpaceProfile, + admin_social_config_out, + encrypt_social_appkey, + fetch_social_login_url, + fetch_space_profile, + new_bind_token, + normalize_social_endpoint, + parse_providers, + poll_social_scan, + provider_label, + public_social_config, +) + +logger = get_logger("app") + +api_social_bp = Blueprint("api_social", __name__) + + +def _get_json_payload() -> dict: + data = request.get_json(silent=True) + return data if isinstance(data, dict) else {} + + +def _social_error(error: SocialLoginError): + return jsonify({"error": error.message}), error.status_code + + +def _allowed_redirect_hosts() -> set[str]: + hosts: set[str] = set() + host = request.headers.get("X-Forwarded-Host") or request.headers.get("Host") or "" + if host: + hosts.add(host.lower()) + hostname = host.split(":", 1)[0].lower() + if hostname: + hosts.add(hostname) + return hosts + + +def _create_pending_from_profile(profile: SpaceProfile) -> dict: + expires_at = (get_cst_now() + timedelta(seconds=BIND_TOKEN_TTL_SECONDS)).strftime("%Y-%m-%d %H:%M:%S") + return database.create_social_pending_bind( + token=new_bind_token(), + provider=profile.provider, + social_uid=profile.social_uid, + nickname=profile.nickname, + avatar_url=profile.avatar_url, + expires_at=expires_at, + ) + + +def _login_user_id(user_id: int) -> None: + user_obj = User(user_id) + login_user(user_obj) + load_user_accounts(user_id) + + +def _binding_row(provider: str, binding: dict | None) -> dict: + return { + "provider": provider, + "provider_label": provider_label(provider), + "bound": bool(binding), + "nickname": (binding or {}).get("nickname") or "", + "avatar_url": (binding or {}).get("avatar_url") or "", + "last_login_at": (binding or {}).get("last_login_at"), + "created_at": (binding or {}).get("created_at"), + } + + +@api_social_bp.route("/api/auth/social/config", methods=["GET"]) +def social_public_config(): + return jsonify(public_social_config(database.get_system_config())) + + +@api_social_bp.route("/api/auth/social/login-url", methods=["POST"]) +def social_login_url(): + data = _get_json_payload() + provider = str(data.get("provider") or "").strip().lower() + mode = "bind" if str(data.get("mode") or "").strip().lower() == "bind" else "login" + redirect_uri = str(data.get("redirect_uri") or "").strip() + + try: + result = fetch_social_login_url( + database.get_system_config(), + provider=provider, + mode=mode, + redirect_uri=redirect_uri, + allowed_hosts=_allowed_redirect_hosts(), + ) + except SocialLoginError as error: + logger.warning(f"[social/login-url] provider={provider or '-'} mode={mode} failed: {error.message}") + return _social_error(error) + return jsonify(result) + + +@api_social_bp.route("/api/auth/social/poll", methods=["POST"]) +def social_poll(): + data = _get_json_payload() + provider = str(data.get("provider") or "").strip().lower() + state = str(data.get("state") or "").strip() + try: + result = poll_social_scan(database.get_system_config(), provider=provider, state=state) + except SocialLoginError as error: + logger.warning(f"[social/poll] provider={provider or '-'} failed: {error.message}") + return _social_error(error) + return jsonify(result) + + +@api_social_bp.route("/api/auth/social/callback", methods=["POST"]) +def social_callback(): + data = _get_json_payload() + provider = str(data.get("provider") or data.get("type") or "").strip().lower() + code = str(data.get("code") or "").strip() + mode = "bind" if str(data.get("mode") or "").strip().lower() == "bind" else "login" + + try: + profile = fetch_space_profile(database.get_system_config(), provider=provider, code=code) + except SocialLoginError as error: + logger.warning(f"[social/callback] provider={provider or '-'} mode={mode} failed: {error.message}") + return _social_error(error) + + binding = database.find_social_login_binding(profile.provider, profile.social_uid) + if binding: + if mode == "bind": + current_id = int(getattr(current_user, "id", 0) or 0) + if not current_id or int(binding.get("user_id") or 0) != current_id: + return jsonify({"error": "该第三方账号已绑定其他用户"}), 409 + + user = database.get_user_by_id(int(binding["user_id"])) + if not user or user.get("status") != "approved": + return jsonify({"error": "绑定账号不可用"}), 401 + database.update_social_login_binding_profile( + int(binding["id"]), + nickname=profile.nickname, + avatar_url=profile.avatar_url, + ) + _login_user_id(int(user["id"])) + return jsonify( + { + "success": True, + "mode": mode, + "provider": profile.provider, + "provider_label": provider_label(profile.provider), + "bound": True, + "username": user.get("username") or "", + } + ) + + pending = _create_pending_from_profile(profile) + return jsonify( + { + "success": True, + "mode": mode, + "provider": profile.provider, + "provider_label": provider_label(profile.provider), + "requires_register": mode == "login", + "requires_existing_login": mode == "bind", + "bind_token": pending.get("token"), + "expires_in": BIND_TOKEN_TTL_SECONDS, + "nickname": pending.get("nickname") or "", + "avatar_url": pending.get("avatar_url") or "", + } + ) + + +@api_social_bp.route("/api/user/social-bindings", methods=["GET"]) +@login_required +def list_social_bindings(): + cfg = database.get_system_config() + providers = parse_providers(cfg.get("social_login_providers")) or list(PROVIDER_LABELS.keys()) + existing = { + item["provider"]: item + for item in database.list_social_login_bindings(int(current_user.id)) + } + return jsonify({"items": [_binding_row(provider, existing.get(provider)) for provider in providers]}) + + +@api_social_bp.route("/api/user/social-bindings", methods=["POST"]) +@login_required +def bind_social_account(): + data = _get_json_payload() + token = str(data.get("bind_token") or "").strip() + pending = database.get_social_pending_bind(token) + if not pending: + return jsonify({"error": "绑定凭证已过期,请重新授权"}), 404 + + provider = str(pending.get("provider") or "").strip().lower() + social_uid = str(pending.get("social_uid") or "").strip() + existing_identity = database.find_social_login_binding(provider, social_uid) + if existing_identity and int(existing_identity.get("user_id") or 0) != int(current_user.id): + return jsonify({"error": "该第三方账号已绑定其他用户"}), 409 + + existing_provider = database.find_user_social_login_binding(int(current_user.id), provider) + if existing_provider and str(existing_provider.get("social_uid") or "") != social_uid: + return jsonify({"error": f"当前账号已绑定{provider_label(provider)}"}), 409 + + binding = database.upsert_social_login_binding( + user_id=int(current_user.id), + provider=provider, + social_uid=social_uid, + nickname=pending.get("nickname") or "", + avatar_url=pending.get("avatar_url") or "", + ) + if not binding: + return jsonify({"error": "该第三方账号已绑定其他用户"}), 409 + + database.delete_social_pending_bind(token) + return jsonify({"success": True, "item": _binding_row(provider, binding)}) + + +@api_social_bp.route("/api/user/social-bindings/", methods=["DELETE"]) +@login_required +def unbind_social_account(provider): + provider = str(provider or "").strip().lower() + if provider not in PROVIDER_LABELS: + return jsonify({"error": "不支持的登录方式"}), 400 + if not database.delete_social_login_binding(int(current_user.id), provider): + return jsonify({"error": "绑定记录不存在"}), 404 + return jsonify({"success": True}) + + +@api_social_bp.route("/yuyx/api/social-login/config", methods=["GET"]) +def admin_social_config(): + from routes.decorators import admin_required + + protected = admin_required(lambda: jsonify(admin_social_config_out(database.get_system_config()))) + return protected() + + +@api_social_bp.route("/yuyx/api/social-login/test", methods=["POST"]) +def test_admin_social_config(): + from routes.decorators import admin_required + + @admin_required + def _inner(): + data = _get_json_payload() + provider = str(data.get("provider") or "wx").strip().lower() + appkey = str(data.get("social_login_appkey") or "").strip() + old_config = database.get_system_config() or {} + + try: + temp_config = { + **old_config, + "social_login_enabled": 1, + "social_login_endpoint": normalize_social_endpoint(str(data.get("social_login_endpoint") or "")), + "social_login_appid": str(data.get("social_login_appid") or "").strip(), + "social_login_appkey": encrypt_social_appkey(appkey) if appkey else str(old_config.get("social_login_appkey") or ""), + "social_login_providers": ",".join(parse_providers(data.get("social_login_providers"))), + } + redirect_uri = str(data.get("redirect_uri") or "").strip() + result = fetch_social_login_url( + temp_config, + provider=provider, + mode="login", + redirect_uri=redirect_uri, + allowed_hosts=_allowed_redirect_hosts(), + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + except SocialLoginError as error: + return _social_error(error) + + return jsonify( + { + "success": True, + "provider": result.get("provider"), + "has_url": bool(result.get("url")), + "has_scan_url": bool(result.get("scan_url")), + } + ) + + return _inner() + + +@api_social_bp.route("/yuyx/api/social-login/config", methods=["POST"]) +def update_admin_social_config(): + from routes.decorators import admin_required + + @admin_required + def _inner(): + data = _get_json_payload() + enabled = 1 if data.get("social_login_enabled") in (1, True, "1", "true", "on") else 0 + endpoint_raw = str(data.get("social_login_endpoint") or "").strip() + appid = str(data.get("social_login_appid") or "").strip() + appkey = str(data.get("social_login_appkey") or "").strip() + providers = parse_providers(data.get("social_login_providers")) + + try: + endpoint = normalize_social_endpoint(endpoint_raw) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + + old_config = database.get_system_config() or {} + old_key = str(old_config.get("social_login_appkey") or "") + + if enabled: + if not providers: + return jsonify({"error": "启用聚合登录时至少选择一种登录方式"}), 400 + if not appid: + return jsonify({"error": "启用聚合登录时必须填写 APPID"}), 400 + if not appkey and not old_key: + return jsonify({"error": "启用聚合登录时必须填写 APPKEY"}), 400 + + encrypted_key = encrypt_social_appkey(appkey) if appkey else old_key + ok = database.update_system_config( + social_login_enabled=enabled, + social_login_endpoint=endpoint, + social_login_appid=appid, + social_login_appkey=encrypted_key, + social_login_providers=",".join(providers), + ) + if not ok: + return jsonify({"error": "更新失败"}), 400 + + logger.info(f"[social/config] updated enabled={enabled} providers={','.join(providers)} at={get_cst_now_str()}") + return jsonify({"message": "聚合登录配置已更新", "config": admin_social_config_out(database.get_system_config())}) + + return _inner() diff --git a/services/social_login.py b/services/social_login.py new file mode 100644 index 0000000..fd86c89 --- /dev/null +++ b/services/social_login.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +import re +import secrets +from dataclasses import dataclass +from typing import Any +from urllib.parse import parse_qs, urlencode, urljoin, urlparse + +import requests + +from crypto_utils import decrypt_password, encrypt_password + +DEFAULT_SPACE_ENDPOINT = "https://www.spacezs.cn/connect.php" +PROVIDER_LABELS = {"qq": "QQ", "wx": "微信", "alipay": "支付宝"} +SUPPORTED_PROVIDERS = set(PROVIDER_LABELS) +BIND_TOKEN_TTL_SECONDS = 600 +SPACE_BROWSER_HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" + ), + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", +} + + +class SocialLoginError(Exception): + def __init__(self, message: str, status_code: int = 400): + super().__init__(message) + self.message = message + self.status_code = int(status_code or 400) + + +@dataclass +class SpaceProfile: + provider: str + social_uid: str + nickname: str = "" + avatar_url: str = "" + + +def provider_label(provider: str) -> str: + return PROVIDER_LABELS.get(provider, provider) + + +def normalize_social_endpoint(value: str) -> str: + raw = (value or DEFAULT_SPACE_ENDPOINT).strip() or DEFAULT_SPACE_ENDPOINT + parsed = urlparse(raw) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise ValueError("聚合接口地址必须是 http 或 https 地址") + path = parsed.path.rstrip("/") + if path in {"", "/"}: + return parsed._replace(path="/connect.php", params="", query="", fragment="").geturl() + if path.endswith("/connect.php"): + return parsed._replace(path=path, params="", query="", fragment="").geturl() + raise ValueError("聚合接口地址必须指向 connect.php") + + +def parse_providers(value: str | list[str] | tuple[str, ...] | None) -> list[str]: + items = value.split(",") if isinstance(value, str) else list(value or []) + result: list[str] = [] + for item in items: + provider = str(item or "").strip().lower() + if provider in SUPPORTED_PROVIDERS and provider not in result: + result.append(provider) + return result + + +def social_appkey(config: dict[str, Any]) -> str: + encrypted = str((config or {}).get("social_login_appkey") or "").strip() + if not encrypted: + return "" + return decrypt_password(encrypted) + + +def encrypt_social_appkey(value: str) -> str: + raw = str(value or "").strip() + return encrypt_password(raw) if raw else "" + + +def mask_secret(value: str) -> str: + if not value: + return "" + if len(value) <= 8: + return f"{value[:2]}***" + return f"{value[:4]}***{value[-4:]}" + + +def admin_social_config_out(config: dict[str, Any]) -> dict[str, Any]: + key = social_appkey(config) + return { + "social_login_enabled": int(config.get("social_login_enabled") or 0), + "social_login_endpoint": config.get("social_login_endpoint") or DEFAULT_SPACE_ENDPOINT, + "social_login_appid": config.get("social_login_appid") or "", + "social_login_appkey": "", + "social_login_appkey_configured": bool(key), + "social_login_appkey_masked": mask_secret(key), + "social_login_providers": parse_providers(config.get("social_login_providers")) or ["qq", "wx", "alipay"], + } + + +def public_social_config(config: dict[str, Any]) -> dict[str, Any]: + providers = parse_providers(config.get("social_login_providers")) + configured = bool(config.get("social_login_appid") and social_appkey(config)) + enabled = bool(int(config.get("social_login_enabled") or 0) == 1 and configured and providers) + return {"enabled": enabled, "providers": providers if enabled else []} + + +def validate_social_ready(config: dict[str, Any], provider: str) -> None: + provider = str(provider or "").strip().lower() + if int(config.get("social_login_enabled") or 0) != 1: + raise SocialLoginError("聚合登录未启用", 409) + if provider not in parse_providers(config.get("social_login_providers")): + raise SocialLoginError("该登录方式未启用", 400) + if not config.get("social_login_appid") or not social_appkey(config): + raise SocialLoginError("聚合登录配置不完整", 409) + + +def validate_redirect_uri(redirect_uri: str, *, allowed_hosts: set[str] | None = None) -> str: + uri = str(redirect_uri or "").strip() + parsed = urlparse(uri) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise SocialLoginError("redirect_uri 必须是完整的 http/https 地址", 400) + if allowed_hosts: + host = parsed.netloc.lower() + hostname = (parsed.hostname or "").lower() + if host not in allowed_hosts and hostname not in allowed_hosts: + raise SocialLoginError("redirect_uri 域名不在允许范围内", 400) + return uri + + +def _space_base_url(endpoint: str) -> str: + parsed = urlparse(endpoint) + return f"{parsed.scheme}://{parsed.netloc}" + + +def _parse_space_json(payload: dict[str, Any], error_message: str) -> dict[str, Any]: + try: + code_value = int(payload.get("code")) + except (TypeError, ValueError): + code_value = -1 + if code_value != 0: + raise SocialLoginError(str(payload.get("msg") or error_message), 409) + return payload + + +def parse_space_scan_page(html: str) -> tuple[str, str]: + qrcode_match = re.search(r'var\s+qrcode_url\s*=\s*["\']([^"\']+)["\']', html or "") + state_match = re.search(r'var\s+state\s*=\s*["\']([^"\']+)["\']', html or "") + qrcode_url = qrcode_match.group(1).strip() if qrcode_match else "" + state = state_match.group(1).strip() if state_match else "" + if not qrcode_url or not state: + raise SocialLoginError("聚合登录扫码页面解析失败", 409) + return qrcode_url, state + + +def fetch_social_login_url( + config: dict[str, Any], + *, + provider: str, + mode: str, + redirect_uri: str, + allowed_hosts: set[str] | None = None, +) -> dict[str, Any]: + provider = str(provider or "").strip().lower() + mode = "bind" if str(mode or "").strip().lower() == "bind" else "login" + validate_social_ready(config, provider) + redirect_uri = validate_redirect_uri(redirect_uri, allowed_hosts=allowed_hosts) + endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or "")) + params = { + "act": "login", + "appid": str(config.get("social_login_appid") or "").strip(), + "appkey": social_appkey(config), + "type": provider, + "redirect_uri": redirect_uri, + } + try: + response = requests.get(endpoint, params=params, timeout=15) + payload = response.json() + except ValueError as exc: + raise SocialLoginError("聚合接口未返回 JSON,请检查 endpoint 是否为 connect.php", 409) from exc + except requests.RequestException as exc: + raise SocialLoginError("聚合接口连接失败", 409) from exc + + data = _parse_space_json(payload, "获取聚合登录地址失败") + url = str(data.get("url") or "") + if not url: + raise SocialLoginError("聚合接口未返回授权地址", 409) + + result = { + "provider": provider, + "mode": mode, + "url": url, + "qrcode": str(data.get("qrcode") or ""), + "scan_url": "", + "scan_state": "", + "scan_poll_interval": 2, + } + if provider == "wx": + scan_page_url = result["qrcode"] or result["url"] + try: + scan_response = requests.get(scan_page_url, headers=SPACE_BROWSER_HEADERS, timeout=15) + except requests.RequestException as exc: + raise SocialLoginError("微信扫码页获取失败", 409) from exc + scan_url, state = parse_space_scan_page(scan_response.text) + result["scan_url"] = scan_url + result["scan_state"] = state + return result + + +def poll_social_scan(config: dict[str, Any], *, provider: str, state: str) -> dict[str, Any]: + provider = str(provider or "").strip().lower() + validate_social_ready(config, provider) + if provider != "wx": + raise SocialLoginError("该登录方式不需要轮询", 400) + state_value = str(state or "").strip() + if not state_value: + raise SocialLoginError("缺少扫码状态", 400) + + endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or "")) + base_url = _space_base_url(endpoint) + ajax_url = urljoin(base_url + "/", "ajax.php") + headers = { + **SPACE_BROWSER_HEADERS, + "Referer": f"{base_url}/jump.php?state={state_value}&client=1", + "X-Requested-With": "XMLHttpRequest", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + try: + response = requests.get(ajax_url, params={"act": "login", "state": state_value}, headers=headers, timeout=10) + payload = response.json() + except ValueError as exc: + raise SocialLoginError("扫码轮询未返回 JSON", 409) from exc + except requests.RequestException as exc: + raise SocialLoginError("扫码轮询连接失败", 409) from exc + + try: + code_value = int(payload.get("code")) + except (TypeError, ValueError): + code_value = -1 + if code_value == 1: + return {"status": "pending"} + if code_value == 0: + url = str(payload.get("url") or "") + if not url: + raise SocialLoginError("扫码成功但未返回回调地址", 409) + return {"status": "authorized", "url": url} + raise SocialLoginError(str(payload.get("msg") or "扫码登录失败"), 409) + + +def _profile_uid(payload: dict[str, Any]) -> str: + for key in ("social_uid", "uid", "openid", "unionid", "id"): + value = str(payload.get(key) or "").strip() + if value: + return value + raise SocialLoginError("聚合回调未返回用户唯一标识", 409) + + +def fetch_space_profile(config: dict[str, Any], *, provider: str, code: str) -> SpaceProfile: + provider = str(provider or "").strip().lower() + code_value = str(code or "").strip() + validate_social_ready(config, provider) + if not code_value: + raise SocialLoginError("缺少授权 code", 400) + endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or "")) + params = { + "act": "callback", + "appid": str(config.get("social_login_appid") or "").strip(), + "appkey": social_appkey(config), + "type": provider, + "code": code_value, + } + try: + response = requests.get(endpoint, params=params, timeout=15) + payload = response.json() + except ValueError as exc: + raise SocialLoginError("聚合回调接口未返回 JSON", 409) from exc + except requests.RequestException as exc: + raise SocialLoginError("聚合回调接口连接失败", 409) from exc + + data = _parse_space_json(payload, "聚合登录回调失败") + return SpaceProfile( + provider=provider, + social_uid=_profile_uid(data), + nickname=str(data.get("nickname") or data.get("nick") or data.get("name") or ""), + avatar_url=str(data.get("faceimg") or data.get("avatar") or data.get("avatar_url") or ""), + ) + + +def callback_mode_from_redirect_query(redirect_url: str, fallback: str = "login") -> str: + parsed = urlparse(redirect_url) + mode = parse_qs(parsed.query).get("mode", [fallback])[0] + return "bind" if mode == "bind" else "login" + + +def append_query(url: str, values: dict[str, str]) -> str: + parsed = urlparse(url) + query = parse_qs(parsed.query) + for key, value in values.items(): + query[key] = [value] + return parsed._replace(query=urlencode(query, doseq=True)).geturl() + + +def new_bind_token() -> str: + return secrets.token_urlsafe(32) diff --git a/static/admin/.vite/manifest.json b/static/admin/.vite/manifest.json index 3e56410..4c357fb 100644 --- a/static/admin/.vite/manifest.json +++ b/static/admin/.vite/manifest.json @@ -1,42 +1,42 @@ { - "_MetricGrid-BnihYB_8.js": { - "file": "assets/MetricGrid-BnihYB_8.js", + "_MetricGrid-BR486o_b.css": { + "file": "assets/MetricGrid-BR486o_b.css", + "src": "_MetricGrid-BR486o_b.css" + }, + "_MetricGrid-C3Xjc9mZ.js": { + "file": "assets/MetricGrid-C3Xjc9mZ.js", "name": "MetricGrid", "imports": [ "index.html", "_vendor-vue-CVxSw_oJ.js" ], "css": [ - "assets/MetricGrid-yP_dkP6X.css" + "assets/MetricGrid-BR486o_b.css" ] }, - "_MetricGrid-yP_dkP6X.css": { - "file": "assets/MetricGrid-yP_dkP6X.css", - "src": "_MetricGrid-yP_dkP6X.css" - }, - "_email-px7YBG2O.js": { - "file": "assets/email-px7YBG2O.js", + "_email-Mh1SHQbX.js": { + "file": "assets/email-Mh1SHQbX.js", "name": "email", "imports": [ "index.html" ] }, - "_system-ZDPnxnIu.js": { - "file": "assets/system-ZDPnxnIu.js", + "_system-CYbWdReq.js": { + "file": "assets/system-CYbWdReq.js", "name": "system", "imports": [ "index.html" ] }, - "_tasks-Bep0SUyu.js": { - "file": "assets/tasks-Bep0SUyu.js", + "_tasks-B7oNpIBD.js": { + "file": "assets/tasks-B7oNpIBD.js", "name": "tasks", "imports": [ "index.html" ] }, - "_users-te9ySk34.js": { - "file": "assets/users-te9ySk34.js", + "_users-DzDcz9C_.js": { + "file": "assets/users-DzDcz9C_.js", "name": "users", "imports": [ "index.html" @@ -73,7 +73,7 @@ "name": "vendor-vue" }, "index.html": { - "file": "assets/index-C1f9ticl.js", + "file": "assets/index-DOvMEmc8.js", "name": "index", "src": "index.html", "isEntry": true, @@ -95,11 +95,11 @@ "src/pages/SettingsPage.vue" ], "css": [ - "assets/index-Dh0BbqTX.css" + "assets/index-CPs_XZ2s.css" ] }, "src/pages/AnnouncementsPage.vue": { - "file": "assets/AnnouncementsPage-C6UgwLIT.js", + "file": "assets/AnnouncementsPage-Dagm5PzE.js", "name": "AnnouncementsPage", "src": "src/pages/AnnouncementsPage.vue", "isDynamicEntry": true, @@ -111,52 +111,52 @@ "_vendor-axios-B9ygI19o.js" ], "css": [ - "assets/AnnouncementsPage-DOwZaaOu.css" + "assets/AnnouncementsPage-tpO97PUg.css" ] }, "src/pages/EmailPage.vue": { - "file": "assets/EmailPage-CATruPK6.js", + "file": "assets/EmailPage-DiZA9Kx_.js", "name": "EmailPage", "src": "src/pages/EmailPage.vue", "isDynamicEntry": true, "imports": [ - "_email-px7YBG2O.js", + "_email-Mh1SHQbX.js", "index.html", - "_MetricGrid-BnihYB_8.js", + "_MetricGrid-C3Xjc9mZ.js", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", "_vendor-axios-B9ygI19o.js", "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/EmailPage-BmPCDPYC.css" + "assets/EmailPage-CTHxGzDv.css" ] }, "src/pages/FeedbacksPage.vue": { - "file": "assets/FeedbacksPage-BAnFKHSL.js", + "file": "assets/FeedbacksPage-DrMVqBKf.js", "name": "FeedbacksPage", "src": "src/pages/FeedbacksPage.vue", "isDynamicEntry": true, "imports": [ "index.html", - "_MetricGrid-BnihYB_8.js", + "_MetricGrid-C3Xjc9mZ.js", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", "_vendor-axios-B9ygI19o.js", "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/FeedbacksPage-mrXjCiV2.css" + "assets/FeedbacksPage-CPmSqIaj.css" ] }, "src/pages/LogsPage.vue": { - "file": "assets/LogsPage-DFPeq0bL.js", + "file": "assets/LogsPage-Cy6Q0ave.js", "name": "LogsPage", "src": "src/pages/LogsPage.vue", "isDynamicEntry": true, "imports": [ - "_users-te9ySk34.js", - "_tasks-Bep0SUyu.js", + "_users-DzDcz9C_.js", + "_tasks-B7oNpIBD.js", "index.html", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", @@ -164,48 +164,48 @@ "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/LogsPage-D1bozCEo.css" + "assets/LogsPage-BUgx3sZr.css" ] }, "src/pages/ReportPage.vue": { - "file": "assets/ReportPage-fzVH-d9u.js", + "file": "assets/ReportPage-BMEJM5Hr.js", "name": "ReportPage", "src": "src/pages/ReportPage.vue", "isDynamicEntry": true, "imports": [ "_vendor-element-B5S5pUKo.js", "index.html", - "_email-px7YBG2O.js", - "_tasks-Bep0SUyu.js", - "_system-ZDPnxnIu.js", - "_MetricGrid-BnihYB_8.js", + "_email-Mh1SHQbX.js", + "_tasks-B7oNpIBD.js", + "_system-CYbWdReq.js", + "_MetricGrid-C3Xjc9mZ.js", "_vendor-vue-CVxSw_oJ.js", "_vendor-misc-BeoNyvBp.js", "_vendor-axios-B9ygI19o.js" ], "css": [ - "assets/ReportPage-BCQBCnjY.css" + "assets/ReportPage-IaDpUFfl.css" ] }, "src/pages/SecurityPage.vue": { - "file": "assets/SecurityPage-xwMQfhuh.js", + "file": "assets/SecurityPage-yzYEGeTN.js", "name": "SecurityPage", "src": "src/pages/SecurityPage.vue", "isDynamicEntry": true, "imports": [ "index.html", - "_MetricGrid-BnihYB_8.js", + "_MetricGrid-C3Xjc9mZ.js", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", "_vendor-axios-B9ygI19o.js", "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/SecurityPage-DN76ndc_.css" + "assets/SecurityPage-C2-mJ7eD.css" ] }, "src/pages/SettingsPage.vue": { - "file": "assets/SettingsPage-DRqlQLxJ.js", + "file": "assets/SettingsPage-DF5fL8gq.js", "name": "SettingsPage", "src": "src/pages/SettingsPage.vue", "isDynamicEntry": true, @@ -217,16 +217,16 @@ "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/SettingsPage-BAa-Qu3q.css" + "assets/SettingsPage-D-iYz1zh.css" ] }, "src/pages/SystemPage.vue": { - "file": "assets/SystemPage-D3eBPCNe.js", + "file": "assets/SystemPage-DrM9-RI5.js", "name": "SystemPage", "src": "src/pages/SystemPage.vue", "isDynamicEntry": true, "imports": [ - "_system-ZDPnxnIu.js", + "_system-CYbWdReq.js", "index.html", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", @@ -234,16 +234,16 @@ "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/SystemPage-BhhEz4Qz.css" + "assets/SystemPage-CTs6qr36.css" ] }, "src/pages/UsersPage.vue": { - "file": "assets/UsersPage-DJZUCpfb.js", + "file": "assets/UsersPage-RI5S3snx.js", "name": "UsersPage", "src": "src/pages/UsersPage.vue", "isDynamicEntry": true, "imports": [ - "_users-te9ySk34.js", + "_users-DzDcz9C_.js", "index.html", "_vendor-element-B5S5pUKo.js", "_vendor-vue-CVxSw_oJ.js", @@ -251,7 +251,7 @@ "_vendor-misc-BeoNyvBp.js" ], "css": [ - "assets/UsersPage-BNDnhJe0.css" + "assets/UsersPage-CgYh6JHW.css" ] } } \ No newline at end of file diff --git a/static/admin/assets/AnnouncementsPage-DOwZaaOu.css b/static/admin/assets/AnnouncementsPage-DOwZaaOu.css deleted file mode 100644 index 119ce8d..0000000 --- a/static/admin/assets/AnnouncementsPage-DOwZaaOu.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-6f55521c]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-6f55521c]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-6f55521c]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-6f55521c]{margin-top:10px;font-size:12px;color:var(--app-muted)}.image-preview[data-v-6f55521c]{margin:6px 0 2px;display:flex;justify-content:flex-start}.image-preview img[data-v-6f55521c]{max-width:280px;max-height:160px;border-radius:8px;border:1px solid var(--app-border);object-fit:contain}.image-upload-row[data-v-6f55521c]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.image-input[data-v-6f55521c]{display:none}.image-url[data-v-6f55521c]{font-size:12px;color:var(--app-muted);word-break:break-all}.announcement-view[data-v-6f55521c]{display:flex;flex-direction:column;gap:12px}.announcement-view-text[data-v-6f55521c]{white-space:pre-wrap;line-height:1.6;font-size:14px}.announcement-view-image[data-v-6f55521c]{max-width:100%;max-height:320px;border-radius:10px;border:1px solid var(--app-border);object-fit:contain}.table-wrap[data-v-6f55521c]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-6f55521c]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-6f55521c]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/AnnouncementsPage-C6UgwLIT.js b/static/admin/assets/AnnouncementsPage-Dagm5PzE.js similarity index 64% rename from static/admin/assets/AnnouncementsPage-C6UgwLIT.js rename to static/admin/assets/AnnouncementsPage-Dagm5PzE.js index 74154c7..21a383e 100644 --- a/static/admin/assets/AnnouncementsPage-C6UgwLIT.js +++ b/static/admin/assets/AnnouncementsPage-Dagm5PzE.js @@ -1 +1 @@ -import{p as J,a as r,E as C}from"./vendor-element-B5S5pUKo.js";import{a as w,_ as W}from"./index-C1f9ticl.js";import{r as v,o as K,aj as p,ap as O,n as h,q as f,t as u,L as n,E as t,G as A,D as x,u as Q,I as c,J as D,F as X,a8 as $}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-misc-BeoNyvBp.js";import"./vendor-axios-B9ygI19o.js";async function Y(){const{data:o}=await w.get("/announcements");return o}async function Z(o){const{data:s}=await w.post("/announcements",o);return s}async function ee(o){const s=new FormData;s.append("file",o);const{data:_}=await w.post("/announcements/upload_image",s);return _}async function te(o){const{data:s}=await w.post(`/announcements/${o}/activate`);return s}async function ne(o){const{data:s}=await w.post(`/announcements/${o}/deactivate`);return s}async function ae(o){const{data:s}=await w.delete(`/announcements/${o}`);return s}const le={class:"page-stack"},se={class:"image-upload-row"},oe={key:1,class:"image-url"},ie={key:0,class:"image-preview"},re=["src"],ue={class:"actions"},ce={class:"table-wrap"},de=["title"],me={key:1,class:"app-muted"},fe={class:"actions"},pe={__name:"AnnouncementsPage",setup(o){const s=v(""),_=v(""),m=v(""),g=v(null),B=v(!1),I=v(!1),T=v([]);async function k(){I.value=!0;try{T.value=await Y()}catch{T.value=[]}finally{I.value=!1}}function E(){s.value="",_.value="",m.value="",g.value&&(g.value.value="")}function N(){g.value?.click()}function U(){m.value="",g.value&&(g.value.value="")}async function P(l){const e=l.target?.files?.[0];if(e){if(e.type&&!e.type.startsWith("image/")){r.error("请选择图片文件"),l.target.value="";return}B.value=!0;try{const d=await ee(e);if(!d?.success||!d?.url){r.error(d?.error||"上传失败");return}m.value=d.url,r.success("上传成功")}catch{}finally{B.value=!1,l.target.value=""}}}async function z(l){const e=s.value.trim(),d=_.value.trim(),b=m.value.trim();if(!e||!d){r.error("标题和内容不能为空");return}try{const i=await Z({title:e,content:d,image_url:b,is_active:!!l});if(!i?.success){r.error(i?.error||"保存失败");return}r.success("保存成功"),E(),await k()}catch{}}async function L(l){const e=$("div",{class:"announcement-view"},[l.content?$("div",{class:"announcement-view-text"},l.content):null,l.image_url?$("img",{class:"announcement-view-image",src:l.image_url,alt:"公告图片"}):null]);await C.alert(e,l.title||"公告",{confirmButtonText:"关闭",dangerouslyUseHTMLString:!1})}async function R(l){try{await C.confirm("确定启用该公告吗?启用后将自动停用其他公告。","启用公告",{confirmButtonText:"启用",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await te(l.id);if(!e?.success){r.error(e?.error||"启用失败");return}r.success("已启用"),await k()}catch{}}async function S(l){try{await C.confirm("确定停用该公告吗?","停用公告",{confirmButtonText:"停用",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await ne(l.id);if(!e?.success){r.error(e?.error||"停用失败");return}r.success("已停用"),await k()}catch{}}async function j(l){try{await C.confirm("确定删除该公告吗?删除后无法恢复。","删除公告",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const e=await ae(l.id);if(!e?.success){r.error(e?.error||"删除失败");return}r.success("已删除"),await k()}catch{}}return K(k),(l,e)=>{const d=p("el-input"),b=p("el-form-item"),i=p("el-button"),q=p("el-form"),F=p("el-card"),y=p("el-table-column"),M=p("el-tag"),G=p("el-table"),H=O("loading");return f(),h("div",le,[e[17]||(e[17]=u("div",{class:"app-page-title"},[u("h2",null,"公告管理")],-1)),n(F,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:t(()=>[e[9]||(e[9]=u("h3",{class:"section-title"},"创建公告",-1)),n(q,{"label-width":"90px"},{default:t(()=>[n(b,{label:"公告标题"},{default:t(()=>[n(d,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=a=>s.value=a),placeholder:"请输入公告标题",maxlength:"100","show-word-limit":""},null,8,["modelValue"])]),_:1}),n(b,{label:"公告内容"},{default:t(()=>[n(d,{modelValue:_.value,"onUpdate:modelValue":e[1]||(e[1]=a=>_.value=a),type:"textarea",rows:5,placeholder:"请输入公告内容(将以弹窗形式展示)",maxlength:"2000","show-word-limit":""},null,8,["modelValue"])]),_:1}),n(b,{label:"公告图片"},{default:t(()=>[u("div",se,[n(i,{icon:Q(J),loading:B.value,onClick:N},{default:t(()=>[...e[4]||(e[4]=[c("上传图片",-1)])]),_:1},8,["icon","loading"]),m.value?(f(),x(i,{key:0,onClick:U},{default:t(()=>[...e[5]||(e[5]=[c("移除",-1)])]),_:1})):A("",!0),m.value?(f(),h("span",oe,D(m.value),1)):A("",!0),u("input",{ref_key:"imageInputRef",ref:g,class:"image-input",type:"file",accept:"image/*",onChange:P},null,544)])]),_:1})]),_:1}),m.value?(f(),h("div",ie,[u("img",{src:m.value,alt:"公告图片预览"},null,8,re)])):A("",!0),u("div",ue,[n(i,{type:"primary",onClick:e[2]||(e[2]=a=>z(!0))},{default:t(()=>[...e[6]||(e[6]=[c("发布并启用",-1)])]),_:1}),n(i,{onClick:e[3]||(e[3]=a=>z(!1))},{default:t(()=>[...e[7]||(e[7]=[c("保存但不启用",-1)])]),_:1}),n(i,{onClick:E},{default:t(()=>[...e[8]||(e[8]=[c("清空",-1)])]),_:1})]),e[10]||(e[10]=u("div",{class:"help"}," 说明:启用公告后,用户登录进入系统将弹窗提示;用户可选择“当次关闭”或“永久关闭本次公告”。 ",-1))]),_:1}),n(F,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:t(()=>[e[16]||(e[16]=u("h3",{class:"section-title"},"公告列表",-1)),u("div",ce,[X((f(),x(G,{data:T.value,style:{width:"100%"}},{default:t(()=>[n(y,{prop:"id",label:"ID",width:"80"}),n(y,{label:"标题","min-width":"240"},{default:t(({row:a})=>[u("span",{class:"ellipsis",title:a.title},D(a.title),9,de)]),_:1}),n(y,{label:"状态",width:"120"},{default:t(({row:a})=>[n(M,{type:a.is_active?"success":"info",effect:"light"},{default:t(()=>[c(D(a.is_active?"启用":"停用"),1)]),_:2},1032,["type"])]),_:1}),n(y,{label:"图片",width:"100"},{default:t(({row:a})=>[a.image_url?(f(),x(M,{key:0,type:"success",effect:"light"},{default:t(()=>[...e[11]||(e[11]=[c("有图",-1)])]),_:1})):(f(),h("span",me,"-"))]),_:1}),n(y,{prop:"created_at",label:"创建时间",width:"180"}),n(y,{label:"操作",width:"260",fixed:"right"},{default:t(({row:a})=>[u("div",fe,[n(i,{size:"small",onClick:V=>L(a)},{default:t(()=>[...e[12]||(e[12]=[c("查看",-1)])]),_:1},8,["onClick"]),a.is_active?(f(),x(i,{key:0,size:"small",onClick:V=>S(a)},{default:t(()=>[...e[13]||(e[13]=[c("停用",-1)])]),_:1},8,["onClick"])):(f(),x(i,{key:1,type:"success",size:"small",onClick:V=>R(a)},{default:t(()=>[...e[14]||(e[14]=[c("启用",-1)])]),_:1},8,["onClick"])),n(i,{type:"danger",size:"small",onClick:V=>j(a)},{default:t(()=>[...e[15]||(e[15]=[c("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[H,I.value]])])]),_:1})])}}},ke=W(pe,[["__scopeId","data-v-6f55521c"]]);export{ke as default}; +import{p as J,a as r,E as C}from"./vendor-element-B5S5pUKo.js";import{a as w,_ as W}from"./index-DOvMEmc8.js";import{r as v,o as K,aj as p,ap as O,n as h,q as f,t as u,L as a,E as t,G as A,D as x,u as Q,I as c,J as D,F as X,a8 as $}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-misc-BeoNyvBp.js";import"./vendor-axios-B9ygI19o.js";async function Y(){const{data:o}=await w.get("/announcements");return o}async function Z(o){const{data:s}=await w.post("/announcements",o);return s}async function ee(o){const s=new FormData;s.append("file",o);const{data:_}=await w.post("/announcements/upload_image",s);return _}async function te(o){const{data:s}=await w.post(`/announcements/${o}/activate`);return s}async function ae(o){const{data:s}=await w.post(`/announcements/${o}/deactivate`);return s}async function ne(o){const{data:s}=await w.delete(`/announcements/${o}`);return s}const le={class:"page-stack"},se={class:"image-upload-row"},oe={key:1,class:"image-url"},ie={key:0,class:"image-preview"},re=["src"],ue={class:"actions"},ce={class:"table-wrap"},de=["title"],me={key:1,class:"app-muted"},fe={class:"actions"},pe={__name:"AnnouncementsPage",setup(o){const s=v(""),_=v(""),m=v(""),g=v(null),B=v(!1),I=v(!1),T=v([]);async function k(){I.value=!0;try{T.value=await Y()}catch{T.value=[]}finally{I.value=!1}}function E(){s.value="",_.value="",m.value="",g.value&&(g.value.value="")}function N(){g.value?.click()}function U(){m.value="",g.value&&(g.value.value="")}async function P(l){const e=l.target?.files?.[0];if(e){if(e.type&&!e.type.startsWith("image/")){r.error("请选择图片文件"),l.target.value="";return}B.value=!0;try{const d=await ee(e);if(!d?.success||!d?.url){r.error(d?.error||"上传失败");return}m.value=d.url,r.success("上传成功")}catch{}finally{B.value=!1,l.target.value=""}}}async function z(l){const e=s.value.trim(),d=_.value.trim(),b=m.value.trim();if(!e||!d){r.error("标题和内容不能为空");return}try{const i=await Z({title:e,content:d,image_url:b,is_active:!!l});if(!i?.success){r.error(i?.error||"保存失败");return}r.success("保存成功"),E(),await k()}catch{}}async function L(l){const e=$("div",{class:"announcement-view"},[l.content?$("div",{class:"announcement-view-text"},l.content):null,l.image_url?$("img",{class:"announcement-view-image",src:l.image_url,alt:"公告图片"}):null]);await C.alert(e,l.title||"公告",{confirmButtonText:"关闭",dangerouslyUseHTMLString:!1})}async function R(l){try{await C.confirm("确定启用该公告吗?启用后将自动停用其他公告。","启用公告",{confirmButtonText:"启用",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await te(l.id);if(!e?.success){r.error(e?.error||"启用失败");return}r.success("已启用"),await k()}catch{}}async function S(l){try{await C.confirm("确定停用该公告吗?","停用公告",{confirmButtonText:"停用",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await ae(l.id);if(!e?.success){r.error(e?.error||"停用失败");return}r.success("已停用"),await k()}catch{}}async function j(l){try{await C.confirm("确定删除该公告吗?删除后无法恢复。","删除公告",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const e=await ne(l.id);if(!e?.success){r.error(e?.error||"删除失败");return}r.success("已删除"),await k()}catch{}}return K(k),(l,e)=>{const d=p("el-input"),b=p("el-form-item"),i=p("el-button"),q=p("el-form"),F=p("el-card"),y=p("el-table-column"),M=p("el-tag"),G=p("el-table"),H=O("loading");return f(),h("div",le,[e[17]||(e[17]=u("div",{class:"app-page-title"},[u("h2",null,"公告管理")],-1)),a(F,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:t(()=>[e[9]||(e[9]=u("h3",{class:"section-title"},"创建公告",-1)),a(q,{"label-width":"90px"},{default:t(()=>[a(b,{label:"公告标题"},{default:t(()=>[a(d,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=n=>s.value=n),placeholder:"请输入公告标题",maxlength:"100","show-word-limit":""},null,8,["modelValue"])]),_:1}),a(b,{label:"公告内容"},{default:t(()=>[a(d,{modelValue:_.value,"onUpdate:modelValue":e[1]||(e[1]=n=>_.value=n),type:"textarea",rows:5,placeholder:"请输入公告内容(将以弹窗形式展示)",maxlength:"2000","show-word-limit":""},null,8,["modelValue"])]),_:1}),a(b,{label:"公告图片"},{default:t(()=>[u("div",se,[a(i,{icon:Q(J),loading:B.value,onClick:N},{default:t(()=>[...e[4]||(e[4]=[c("上传图片",-1)])]),_:1},8,["icon","loading"]),m.value?(f(),x(i,{key:0,onClick:U},{default:t(()=>[...e[5]||(e[5]=[c("移除",-1)])]),_:1})):A("",!0),m.value?(f(),h("span",oe,D(m.value),1)):A("",!0),u("input",{ref_key:"imageInputRef",ref:g,class:"image-input",type:"file",accept:"image/*",onChange:P},null,544)])]),_:1})]),_:1}),m.value?(f(),h("div",ie,[u("img",{src:m.value,alt:"公告图片预览"},null,8,re)])):A("",!0),u("div",ue,[a(i,{type:"primary",onClick:e[2]||(e[2]=n=>z(!0))},{default:t(()=>[...e[6]||(e[6]=[c("发布并启用",-1)])]),_:1}),a(i,{onClick:e[3]||(e[3]=n=>z(!1))},{default:t(()=>[...e[7]||(e[7]=[c("保存但不启用",-1)])]),_:1}),a(i,{onClick:E},{default:t(()=>[...e[8]||(e[8]=[c("清空",-1)])]),_:1})]),e[10]||(e[10]=u("div",{class:"help"}," 说明:启用公告后,用户登录进入系统将弹窗提示;用户可选择“当次关闭”或“永久关闭本次公告”。 ",-1))]),_:1}),a(F,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:t(()=>[e[16]||(e[16]=u("h3",{class:"section-title"},"公告列表",-1)),u("div",ce,[X((f(),x(G,{data:T.value,style:{width:"100%"}},{default:t(()=>[a(y,{prop:"id",label:"ID",width:"80"}),a(y,{label:"标题","min-width":"240"},{default:t(({row:n})=>[u("span",{class:"ellipsis",title:n.title},D(n.title),9,de)]),_:1}),a(y,{label:"状态",width:"120"},{default:t(({row:n})=>[a(M,{type:n.is_active?"success":"info",effect:"light"},{default:t(()=>[c(D(n.is_active?"启用":"停用"),1)]),_:2},1032,["type"])]),_:1}),a(y,{label:"图片",width:"100"},{default:t(({row:n})=>[n.image_url?(f(),x(M,{key:0,type:"success",effect:"light"},{default:t(()=>[...e[11]||(e[11]=[c("有图",-1)])]),_:1})):(f(),h("span",me,"-"))]),_:1}),a(y,{prop:"created_at",label:"创建时间",width:"180"}),a(y,{label:"操作",width:"260",fixed:"right"},{default:t(({row:n})=>[u("div",fe,[a(i,{size:"small",onClick:V=>L(n)},{default:t(()=>[...e[12]||(e[12]=[c("查看",-1)])]),_:1},8,["onClick"]),n.is_active?(f(),x(i,{key:0,size:"small",onClick:V=>S(n)},{default:t(()=>[...e[13]||(e[13]=[c("停用",-1)])]),_:1},8,["onClick"])):(f(),x(i,{key:1,type:"success",size:"small",onClick:V=>R(n)},{default:t(()=>[...e[14]||(e[14]=[c("启用",-1)])]),_:1},8,["onClick"])),a(i,{type:"danger",size:"small",onClick:V=>j(n)},{default:t(()=>[...e[15]||(e[15]=[c("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[H,I.value]])])]),_:1})])}}},ke=W(pe,[["__scopeId","data-v-faabe575"]]);export{ke as default}; diff --git a/static/admin/assets/AnnouncementsPage-tpO97PUg.css b/static/admin/assets/AnnouncementsPage-tpO97PUg.css new file mode 100644 index 0000000..7f27184 --- /dev/null +++ b/static/admin/assets/AnnouncementsPage-tpO97PUg.css @@ -0,0 +1 @@ +.page-stack[data-v-faabe575]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-faabe575]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-faabe575]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-faabe575]{margin-top:10px;font-size:12px;color:var(--app-muted)}.image-preview[data-v-faabe575]{margin:6px 0 2px;display:flex;justify-content:flex-start}.image-preview img[data-v-faabe575]{max-width:280px;max-height:160px;border-radius:8px;border:1px solid var(--app-border);object-fit:contain}.image-upload-row[data-v-faabe575]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.image-input[data-v-faabe575]{display:none}.image-url[data-v-faabe575]{font-size:12px;color:var(--app-muted);word-break:break-all}.announcement-view[data-v-faabe575]{display:flex;flex-direction:column;gap:12px}.announcement-view-text[data-v-faabe575]{white-space:pre-wrap;line-height:1.6;font-size:14px}.announcement-view-image[data-v-faabe575]{max-width:100%;max-height:320px;border-radius:10px;border:1px solid var(--app-border);object-fit:contain}.table-wrap[data-v-faabe575]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-faabe575]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-faabe575]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/EmailPage-BmPCDPYC.css b/static/admin/assets/EmailPage-BmPCDPYC.css deleted file mode 100644 index 4f1a476..0000000 --- a/static/admin/assets/EmailPage-BmPCDPYC.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-4f511165]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-4f511165]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-4f511165]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-head[data-v-4f511165]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-4f511165]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-4f511165]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-4f511165]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.sub-stats[data-v-4f511165]{margin-top:12px}.ellipsis[data-v-4f511165]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-4f511165]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-4f511165]{font-size:12px}.dialog-actions[data-v-4f511165]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-4f511165]{flex:1} diff --git a/static/admin/assets/EmailPage-CTHxGzDv.css b/static/admin/assets/EmailPage-CTHxGzDv.css new file mode 100644 index 0000000..ff162e8 --- /dev/null +++ b/static/admin/assets/EmailPage-CTHxGzDv.css @@ -0,0 +1 @@ +.page-stack[data-v-b1cb7390]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-b1cb7390]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-b1cb7390]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-head[data-v-b1cb7390]{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-b1cb7390]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-b1cb7390]{margin-top:8px;font-size:12px;color:var(--app-muted)}.table-wrap[data-v-b1cb7390]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.sub-stats[data-v-b1cb7390]{margin-top:12px}.ellipsis[data-v-b1cb7390]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-b1cb7390]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-b1cb7390]{font-size:12px}.dialog-actions[data-v-b1cb7390]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.spacer[data-v-b1cb7390]{flex:1} diff --git a/static/admin/assets/EmailPage-CATruPK6.js b/static/admin/assets/EmailPage-DiZA9Kx_.js similarity index 98% rename from static/admin/assets/EmailPage-CATruPK6.js rename to static/admin/assets/EmailPage-DiZA9Kx_.js index b7ecf9f..213dcf9 100644 --- a/static/admin/assets/EmailPage-CATruPK6.js +++ b/static/admin/assets/EmailPage-DiZA9Kx_.js @@ -1 +1 @@ -import{a as De,c as He,b as Ne,f as Fe,u as Ie}from"./email-px7YBG2O.js";import{a as U,_ as ze}from"./index-C1f9ticl.js";import{M as me}from"./MetricGrid-BnihYB_8.js";import{E as B,a as d}from"./vendor-element-B5S5pUKo.js";import{r as m,S as pe,R as Qe,c as G,o as je,aj as c,ap as Ge,n as x,q as f,t as u,F as K,L as t,D as M,E as n,I as p,J as _,G as P,K as te,a3 as ce}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function Ke(){const{data:b}=await U.get("/smtp/configs");return b}async function Oe(b){const{data:v}=await U.post("/smtp/configs",b);return v}async function Re(b,v){const{data:g}=await U.put(`/smtp/configs/${b}`,v);return g}async function Je(b){const{data:v}=await U.delete(`/smtp/configs/${b}`);return v}async function We(b,v){const{data:g}=await U.post(`/smtp/configs/${b}/test`,{email:v});return g}async function Xe(b){const{data:v}=await U.post(`/smtp/configs/${b}/primary`);return v}async function Ye(){const{data:b}=await U.post("/smtp/configs/primary/clear");return b}const Ze={class:"page-stack"},el={class:"help app-muted"},ll={class:"section-head"},tl={class:"table-wrap"},al={class:"sub-stats"},sl={class:"help app-muted"},nl={class:"section-head"},ol={class:"toolbar"},il={class:"table-wrap"},ul=["title"],rl=["title"],dl=["title"],ml={class:"pagination"},pl={class:"page-hint app-muted"},cl={style:{width:"100%"}},fl={key:0,class:"help"},_l={key:0},bl={key:0},vl={key:0},yl={class:"dialog-actions"},fe=15,gl={__name:"EmailPage",setup(b){const v=m(!1),g=m(!1),o=pe({enabled:!1,failover_enabled:!0,register_verify_enabled:!1,login_alert_enabled:!0,task_notify_enabled:!1,base_url:"",updated_at:null});let L=null;async function ae(){v.value=!0;try{const s=await Ne();o.enabled=!!s.enabled,o.failover_enabled=!!s.failover_enabled,o.register_verify_enabled=!!s.register_verify_enabled,o.login_alert_enabled=s.login_alert_enabled===void 0?!0:!!s.login_alert_enabled,o.task_notify_enabled=!!s.task_notify_enabled,o.base_url=s.base_url||"",o.updated_at=s.updated_at||null}catch{}finally{v.value=!1}}async function _e(){if(!v.value){g.value=!0;try{const s=await Ie({enabled:o.enabled,failover_enabled:o.failover_enabled,register_verify_enabled:o.register_verify_enabled,login_alert_enabled:o.login_alert_enabled,task_notify_enabled:o.task_notify_enabled,base_url:(o.base_url||"").trim()});if(!s?.success){d.error(s?.error||"更新失败");return}d.success("邮件设置已更新"),await ae()}catch{}finally{g.value=!1}}}function E(){L&&window.clearTimeout(L),L=window.setTimeout(_e,300)}Qe(()=>{L&&window.clearTimeout(L),L=null});const O=m(!1),R=m([]),S=m(!1),k=m(!1),J=m(!1),H=m(!1),a=pe({id:null,name:"默认配置",enabled:!0,host:"",port:465,username:"",password:"",use_ssl:!0,use_tls:!1,sender_name:"自动化学习",sender_email:"",daily_limit:0,priority:0}),N=[{key:"custom",label:"自定义(手动填写)",defaults:null,note:"适用于其他邮箱/自建SMTP",links:[]},{key:"gmail",label:"Gmail",defaults:{host:"smtp.gmail.com",port:465,use_ssl:!0,use_tls:!1},note:"通常需要开启两步验证并创建应用专用密码(App Password)",links:[{label:"SMTP 设置说明",url:"https://support.google.com/mail/answer/7126229?hl=zh-Hans"},{label:"App Password",url:"https://myaccount.google.com/apppasswords"}]},{key:"qq",label:"QQ 邮箱",defaults:{host:"smtp.qq.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并获取授权码(不是QQ登录密码)",links:[{label:"QQ邮箱 SMTP 帮助",url:"https://service.mail.qq.com/cgi-bin/help?subtype=1&id=28&no=1001256"}]},{key:"163",label:"163 邮箱",defaults:{host:"smtp.163.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并使用授权码/客户端授权密码",links:[{label:"网易邮箱 SMTP 帮助",url:"https://help.mail.163.com/faqDetail.do?code=d7a5dc8471a22b76"}]},{key:"126",label:"126 邮箱",defaults:{host:"smtp.126.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并使用授权码/客户端授权密码",links:[{label:"网易邮箱帮助",url:"https://help.mail.163.com/"}]},{key:"outlook",label:"Outlook/Hotmail",defaults:{host:"smtp-mail.outlook.com",port:587,use_ssl:!1,use_tls:!0},note:"建议使用 TLS 587(部分账号需开启 SMTP AUTH)",links:[{label:"微软 SMTP 设置",url:"https://support.microsoft.com/office/pop-imap-and-smtp-settings-for-outlook-com-d088b0b7-0d38-4f9a-bc5d-509f9e4c6d3d"}]},{key:"office365",label:"Microsoft 365/Exchange",defaults:{host:"smtp.office365.com",port:587,use_ssl:!1,use_tls:!0},note:"企业邮箱常用配置(需启用 SMTP AUTH)",links:[{label:"微软官方说明",url:"https://learn.microsoft.com/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission"}]},{key:"icloud",label:"iCloud",defaults:{host:"smtp.mail.me.com",port:587,use_ssl:!1,use_tls:!0},note:"需要在 Apple ID 中生成“App 专用密码”",links:[{label:"Apple 邮件服务器设置",url:"https://support.apple.com/zh-cn/HT202304"}]},{key:"tencent_exmail",label:"腾讯企业邮箱",defaults:{host:"smtp.exmail.qq.com",port:465,use_ssl:!0,use_tls:!1},note:"企业邮箱常用配置",links:[{label:"腾讯企业邮箱帮助",url:"https://service.exmail.qq.com/cgi-bin/help?subtype=1&id=23&no=1001068"}]},{key:"aliyun_exmail",label:"阿里企业邮箱",defaults:{host:"smtp.mxhichina.com",port:465,use_ssl:!0,use_tls:!1},note:"企业邮箱常用配置",links:[{label:"阿里云文档",url:"https://help.aliyun.com/document_detail/50652.html"}]}],q=m("custom"),V=G(()=>N.find(s=>s.key===q.value)||N[0]),be=G(()=>k.value&&J.value?"留空保持不变":"SMTP密码或授权码");function ve(s){const e=String(s?.host||"").trim().toLowerCase();return e&&{"smtp.gmail.com":"gmail","smtp.qq.com":"qq","smtp.163.com":"163","smtp.126.com":"126","smtp-mail.outlook.com":"outlook","smtp.office365.com":"office365","smtp.mail.me.com":"icloud","smtp.exmail.qq.com":"tencent_exmail","smtp.mxhichina.com":"aliyun_exmail"}[e]||"custom"}function ye(s){const e=N.find(i=>i.key===s);!e||!e.defaults||(a.host=e.defaults.host,a.port=e.defaults.port,a.use_ssl=e.defaults.use_ssl,a.use_tls=e.defaults.use_tls)}function se(){a.id=null,a.name="默认配置",a.enabled=!0,a.host="",a.port=465,a.username="",a.password="",a.use_ssl=!0,a.use_tls=!1,a.sender_name="自动化学习",a.sender_email="",a.daily_limit=0,a.priority=0,J.value=!1,H.value=!1,q.value="custom"}async function $(){O.value=!0;try{R.value=await Ke()}catch{R.value=[]}finally{O.value=!1}}function ge(){k.value=!1,se(),q.value="custom",S.value=!0}function ke(s){k.value=!0,se(),a.id=s.id,a.name=s.name||"默认配置",a.enabled=!!s.enabled,a.host=s.host||"",a.port=s.port||465,a.username=s.username||"",a.password="",a.use_ssl=!!s.use_ssl,a.use_tls=!!s.use_tls,a.sender_name=s.sender_name||"自动化学习",a.sender_email=s.sender_email||"",a.daily_limit=s.daily_limit??0,a.priority=s.priority??0,J.value=!!s.has_password,H.value=!!s.is_primary,q.value=ve(s),S.value=!0}function ne(s){return s.is_primary?{label:"主",type:"warning"}:s.enabled?{label:"备用",type:"success"}:{label:"禁用",type:"info"}}function he(s){return s.daily_limit&&s.daily_limit>0?`${s.daily_sent}/${s.daily_limit}`:`${s.daily_sent}/∞`}async function Ve(){if(!a.host.trim()){d.error("SMTP服务器地址不能为空");return}if(!a.username.trim()){d.error("SMTP用户名不能为空");return}const s={name:a.name.trim()||"默认配置",enabled:!!a.enabled,priority:Number(a.priority)||0,host:a.host.trim(),port:Number(a.port)||465,username:a.username.trim(),use_ssl:!!a.use_ssl,use_tls:!!a.use_tls,sender_name:(a.sender_name||"").trim(),sender_email:(a.sender_email||"").trim(),daily_limit:Number(a.daily_limit)||0};try{if(k.value){const e={...s};a.password&&(e.password=a.password);const i=await Re(a.id,e);if(!i?.success){d.error(i?.error||"更新失败");return}d.success("保存成功")}else{const e={...s};a.password&&(e.password=a.password);const i=await Oe(e);if(!i?.success){d.error(i?.error||"创建失败");return}d.success("创建成功")}S.value=!1,await $()}catch{}}async function we(){if(!k.value||!a.id){d.error("请先保存配置后再测试");return}let s;try{const e=await B.prompt("请输入测试收件邮箱","测试连接",{inputPlaceholder:"name@example.com",confirmButtonText:"发送测试邮件",cancelButtonText:"取消",inputValidator:i=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(i||"").trim()),inputErrorMessage:"邮箱格式不正确"});s=String(e.value||"").trim()}catch{return}try{const e=await We(a.id,s);e?.success?(d.success("测试成功,邮件已发送"),await $()):await B.alert(e?.error||"测试失败","测试失败",{confirmButtonText:"知道了"})}catch{}}async function Se(){if(!(!k.value||!a.id)){try{await B.confirm("确定将该配置设为主配置吗?","设为主配置",{confirmButtonText:"设为主配置",cancelButtonText:"取消",type:"warning"})}catch{return}try{const s=await Xe(a.id);if(!s?.success){d.error(s?.error||"设置失败");return}d.success("已设为主配置"),S.value=!1,await $()}catch{}}}async function Te(){if(k.value){try{await B.confirm("确定取消主配置吗?取消后将按优先级选择可用SMTP。","取消主配置",{confirmButtonText:"取消主配置",cancelButtonText:"保留",type:"warning"})}catch{return}try{const s=await Ye();if(!s?.success){d.error(s?.error||"操作失败");return}d.success("已取消主配置"),S.value=!1,await $()}catch{}}}async function xe(){if(!(!k.value||!a.id)){try{await B.confirm("确定删除该SMTP配置吗?此操作不可恢复。","删除配置",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const s=await Je(a.id);if(!s?.success){d.error(s?.error||"删除失败");return}d.success("已删除"),S.value=!1,await $()}catch{}}}const A=m(!1),h=m({}),W=m(!1),F=m(""),I=m(""),z=m(1),X=m([]),Y=m(0),Z=m(1);function Pe(s){return{register:"注册验证",reset:"密码重置",bind:"邮箱绑定",task_complete:"任务完成",security_alert:"安全告警"}[s]||s}function oe(s){return s?.username&&s?.user_id?`${s.username} (#${s.user_id})`:s?.user_id?`用户#${s.user_id}`:"系统"}const Ce=G(()=>[{key:"total_sent",label:"总发送",value:h.value?.total_sent||0,tone:"blue"},{key:"total_success",label:"成功",value:h.value?.total_success||0,tone:"green"},{key:"total_failed",label:"失败",value:h.value?.total_failed||0,tone:"red"},{key:"success_rate",label:"成功率",value:`${h.value?.success_rate||0}%`,tone:"purple"}]),Be=G(()=>[{key:"register_sent",label:"注册验证",value:h.value?.register_sent||0,tone:"cyan"},{key:"reset_sent",label:"密码重置",value:h.value?.reset_sent||0,tone:"orange"},{key:"bind_sent",label:"邮箱绑定",value:h.value?.bind_sent||0,tone:"purple"},{key:"task_complete_sent",label:"任务完成",value:h.value?.task_complete_sent||0,tone:"green"}]);async function Me(){A.value=!0;try{h.value=await Fe()}catch{h.value={}}finally{A.value=!1}}async function D(s=1){W.value=!0;try{const e={page:s,page_size:fe};F.value&&(e.type=F.value),I.value&&(e.status=I.value);const i=await De(e);X.value=i?.logs||[],Y.value=i?.total||0,z.value=i?.page||s,Z.value=i?.total_pages||1}catch{X.value=[],Y.value=0,Z.value=1}finally{W.value=!1}}async function Ue(){let s;try{const e=await B.prompt("请输入保留天数(将删除该天数之前的日志)","清理日志",{inputValue:"30",confirmButtonText:"清理",cancelButtonText:"取消",inputValidator:i=>{const r=parseInt(String(i),10);return Number.isFinite(r)&&r>=7},inputErrorMessage:"天数必须大于等于7"});s=parseInt(String(e.value),10)}catch{return}try{await B.confirm(`确定删除 ${s} 天之前的邮件日志吗?`,"二次确认",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await He(s);if(!e?.success){d.error(e?.error||"清理失败");return}d.success(`已清理 ${e.deleted} 条日志`),await D(1)}catch{}}async function Le(){await Promise.all([ae(),$(),Me(),D(1)])}return je(Le),(s,e)=>{const i=c("el-switch"),r=c("el-form-item"),Ee=c("el-divider"),C=c("el-input"),ie=c("el-form"),Q=c("el-card"),T=c("el-button"),ue=c("el-tag"),y=c("el-table-column"),re=c("el-table"),w=c("el-option"),ee=c("el-select"),qe=c("el-pagination"),$e=c("el-link"),le=c("el-input-number"),Ae=c("el-dialog"),j=Ge("loading");return f(),x("div",Ze,[e[43]||(e[43]=u("div",{class:"app-page-title"},[u("h2",null,"邮件配置")],-1)),K((f(),M(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[e[29]||(e[29]=u("h3",{class:"section-title"},"全局设置",-1)),t(ie,{"label-width":"140px"},{default:n(()=>[t(r,{label:"启用邮件功能"},{default:n(()=>[t(i,{modelValue:o.enabled,"onUpdate:modelValue":e[0]||(e[0]=l=>o.enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"启用故障转移"},{default:n(()=>[t(i,{modelValue:o.failover_enabled,"onUpdate:modelValue":e[1]||(e[1]=l=>o.failover_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"启用注册邮箱验证"},{default:n(()=>[t(i,{modelValue:o.register_verify_enabled,"onUpdate:modelValue":e[2]||(e[2]=l=>o.register_verify_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(Ee,{"content-position":"left"},{default:n(()=>[...e[26]||(e[26]=[p("通知设置",-1)])]),_:1}),t(r,{label:"启用任务完成通知"},{default:n(()=>[t(i,{modelValue:o.task_notify_enabled,"onUpdate:modelValue":e[3]||(e[3]=l=>o.task_notify_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"新设备登录提醒"},{default:n(()=>[t(i,{modelValue:o.login_alert_enabled,"onUpdate:modelValue":e[4]||(e[4]=l=>o.login_alert_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"]),e[27]||(e[27]=u("div",{class:"help"},"当检测到新设备或新IP登录时,发送邮件提醒用户",-1))]),_:1}),t(r,{label:"网站基础URL"},{default:n(()=>[t(C,{modelValue:o.base_url,"onUpdate:modelValue":e[5]||(e[5]=l=>o.base_url=l),placeholder:"例如: https://example.com",disabled:g.value,onBlur:E},null,8,["modelValue","disabled"]),e[28]||(e[28]=u("div",{class:"help"},"用于生成邮件中的验证链接,留空则使用默认配置。",-1))]),_:1})]),_:1}),u("div",el,"最近更新时间:"+_(o.updated_at||"-"),1)]),_:1})),[[j,v.value]]),t(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[u("div",ll,[e[31]||(e[31]=u("h3",{class:"section-title"},"SMTP配置列表",-1)),t(T,{type:"primary",onClick:ge},{default:n(()=>[...e[30]||(e[30]=[p("+ 添加配置",-1)])]),_:1})]),u("div",tl,[K((f(),M(re,{data:R.value,style:{width:"100%"}},{default:n(()=>[t(y,{label:"状态",width:"90"},{default:n(({row:l})=>[t(ue,{type:ne(l).type,effect:"light"},{default:n(()=>[p(_(ne(l).label),1)]),_:2},1032,["type"])]),_:1}),t(y,{prop:"name",label:"名称","min-width":"160"}),t(y,{label:"服务器","min-width":"200"},{default:n(({row:l})=>[p(_(l.host)+":"+_(l.port),1)]),_:1}),t(y,{label:"今日/限额",width:"110"},{default:n(({row:l})=>[p(_(he(l)),1)]),_:1}),t(y,{label:"成功率",width:"100"},{default:n(({row:l})=>[p(_(l.success_rate)+"%",1)]),_:1}),t(y,{label:"操作",width:"120",fixed:"right"},{default:n(({row:l})=>[t(T,{size:"small",onClick:de=>ke(l)},{default:n(()=>[...e[32]||(e[32]=[p("编辑",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[j,O.value]])])]),_:1}),K((f(),M(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[e[33]||(e[33]=u("h3",{class:"section-title"},"邮件发送统计",-1)),t(me,{items:Ce.value,loading:A.value,"min-width":160},null,8,["items","loading"]),u("div",al,[t(me,{items:Be.value,loading:A.value,"min-width":150},null,8,["items","loading"])]),u("div",sl,"最后更新:"+_(h.value.last_updated||"-"),1)]),_:1})),[[j,A.value]]),t(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[u("div",nl,[e[35]||(e[35]=u("h3",{class:"section-title"},"邮件发送日志",-1)),u("div",ol,[t(ee,{modelValue:F.value,"onUpdate:modelValue":e[6]||(e[6]=l=>F.value=l),style:{width:"140px"},onChange:e[7]||(e[7]=l=>D(1))},{default:n(()=>[t(w,{label:"全部类型",value:""}),t(w,{label:"注册验证",value:"register"}),t(w,{label:"密码重置",value:"reset"}),t(w,{label:"邮箱绑定",value:"bind"}),t(w,{label:"任务完成",value:"task_complete"}),t(w,{label:"安全告警",value:"security_alert"})]),_:1},8,["modelValue"]),t(ee,{modelValue:I.value,"onUpdate:modelValue":e[8]||(e[8]=l=>I.value=l),style:{width:"120px"},onChange:e[9]||(e[9]=l=>D(1))},{default:n(()=>[t(w,{label:"全部状态",value:""}),t(w,{label:"成功",value:"success"}),t(w,{label:"失败",value:"failed"})]),_:1},8,["modelValue"]),t(T,{type:"danger",plain:"",onClick:Ue},{default:n(()=>[...e[34]||(e[34]=[p("清理日志",-1)])]),_:1})])]),u("div",il,[K((f(),M(re,{data:X.value,style:{width:"100%"}},{default:n(()=>[t(y,{prop:"created_at",label:"时间",width:"180"}),t(y,{prop:"email_to",label:"收件人","min-width":"180"}),t(y,{label:"来源用户","min-width":"160"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:oe(l)},_(oe(l)),9,ul)]),_:1}),t(y,{label:"类型",width:"120"},{default:n(({row:l})=>[p(_(Pe(l.email_type)),1)]),_:1}),t(y,{label:"主题","min-width":"220"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:l.subject},_(l.subject),9,rl)]),_:1}),t(y,{label:"状态",width:"90"},{default:n(({row:l})=>[t(ue,{type:l.status==="success"?"success":"danger",effect:"light"},{default:n(()=>[p(_(l.status==="success"?"成功":"失败"),1)]),_:2},1032,["type"])]),_:1}),t(y,{label:"错误","min-width":"200"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:l.error_message||""},_(l.error_message||"-"),9,dl)]),_:1})]),_:1},8,["data"])),[[j,W.value]])]),u("div",ml,[t(qe,{"current-page":z.value,"onUpdate:currentPage":e[10]||(e[10]=l=>z.value=l),"page-size":fe,total:Y.value,layout:"prev, pager, next, ->, total",onCurrentChange:D},null,8,["current-page","total"]),u("div",pl,"第 "+_(z.value)+" / "+_(Z.value)+" 页",1)])]),_:1}),t(Ae,{modelValue:S.value,"onUpdate:modelValue":e[25]||(e[25]=l=>S.value=l),title:k.value?"编辑SMTP配置":"添加SMTP配置",width:"min(560px, 92vw)"},{footer:n(()=>[u("div",yl,[t(T,{onClick:we},{default:n(()=>[...e[36]||(e[36]=[p("测试连接",-1)])]),_:1}),k.value&&H.value?(f(),M(T,{key:0,type:"warning",plain:"",onClick:Te},{default:n(()=>[...e[37]||(e[37]=[p("取消主配置",-1)])]),_:1})):P("",!0),k.value&&!H.value?(f(),M(T,{key:1,onClick:Se},{default:n(()=>[...e[38]||(e[38]=[p("设为主配置",-1)])]),_:1})):P("",!0),k.value?(f(),M(T,{key:2,type:"danger",plain:"",onClick:xe},{default:n(()=>[...e[39]||(e[39]=[p("删除配置",-1)])]),_:1})):P("",!0),e[42]||(e[42]=u("div",{class:"spacer"},null,-1)),t(T,{onClick:e[24]||(e[24]=l=>S.value=!1)},{default:n(()=>[...e[40]||(e[40]=[p("取消",-1)])]),_:1}),t(T,{type:"primary",onClick:Ve},{default:n(()=>[...e[41]||(e[41]=[p("保存",-1)])]),_:1})])]),default:n(()=>[t(ie,{"label-width":"120px"},{default:n(()=>[t(r,{label:"名称"},{default:n(()=>[t(C,{modelValue:a.name,"onUpdate:modelValue":e[11]||(e[11]=l=>a.name=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"启用"},{default:n(()=>[t(i,{modelValue:a.enabled,"onUpdate:modelValue":e[12]||(e[12]=l=>a.enabled=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"邮箱模板"},{default:n(()=>[u("div",cl,[t(ee,{modelValue:q.value,"onUpdate:modelValue":e[13]||(e[13]=l=>q.value=l),placeholder:"选择常用邮箱模板",style:{width:"100%"},onChange:ye},{default:n(()=>[(f(),x(te,null,ce(N,l=>t(w,{key:l.key,label:l.label,value:l.key},null,8,["label","value"])),64))]),_:1},8,["modelValue"]),V.value.note||V.value.links&&V.value.links.length?(f(),x("div",fl,[V.value.note?(f(),x("span",_l,_(V.value.note),1)):P("",!0),V.value.links&&V.value.links.length?(f(),x(te,{key:1},[V.value.note?(f(),x("span",bl," · ")):P("",!0),(f(!0),x(te,null,ce(V.value.links,(l,de)=>(f(),x("span",{key:l.url},[t($e,{href:l.url,target:"_blank",type:"primary",underline:!1},{default:n(()=>[p(_(l.label),1)]),_:2},1032,["href"]),de[t(C,{modelValue:a.host,"onUpdate:modelValue":e[14]||(e[14]=l=>a.host=l),placeholder:"smtp.example.com"},null,8,["modelValue"])]),_:1}),t(r,{label:"端口"},{default:n(()=>[t(le,{modelValue:a.port,"onUpdate:modelValue":e[15]||(e[15]=l=>a.port=l),min:1,max:65535},null,8,["modelValue"])]),_:1}),t(r,{label:"用户名"},{default:n(()=>[t(C,{modelValue:a.username,"onUpdate:modelValue":e[16]||(e[16]=l=>a.username=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"密码"},{default:n(()=>[t(C,{modelValue:a.password,"onUpdate:modelValue":e[17]||(e[17]=l=>a.password=l),type:"password","show-password":"",placeholder:be.value},null,8,["modelValue","placeholder"])]),_:1}),t(r,{label:"SSL"},{default:n(()=>[t(i,{modelValue:a.use_ssl,"onUpdate:modelValue":e[18]||(e[18]=l=>a.use_ssl=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"TLS"},{default:n(()=>[t(i,{modelValue:a.use_tls,"onUpdate:modelValue":e[19]||(e[19]=l=>a.use_tls=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"发件人名称"},{default:n(()=>[t(C,{modelValue:a.sender_name,"onUpdate:modelValue":e[20]||(e[20]=l=>a.sender_name=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"发件人邮箱"},{default:n(()=>[t(C,{modelValue:a.sender_email,"onUpdate:modelValue":e[21]||(e[21]=l=>a.sender_email=l),placeholder:"可选"},null,8,["modelValue"])]),_:1}),t(r,{label:"每日限额"},{default:n(()=>[t(le,{modelValue:a.daily_limit,"onUpdate:modelValue":e[22]||(e[22]=l=>a.daily_limit=l),min:0,max:1e6},null,8,["modelValue"])]),_:1}),t(r,{label:"优先级"},{default:n(()=>[t(le,{modelValue:a.priority,"onUpdate:modelValue":e[23]||(e[23]=l=>a.priority=l),min:0,max:1e3},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue","title"])])}}},Pl=ze(gl,[["__scopeId","data-v-4f511165"]]);export{Pl as default}; +import{a as De,c as He,b as Ne,f as Fe,u as Ie}from"./email-Mh1SHQbX.js";import{a as U,_ as ze}from"./index-DOvMEmc8.js";import{M as me}from"./MetricGrid-C3Xjc9mZ.js";import{E as B,a as d}from"./vendor-element-B5S5pUKo.js";import{r as m,S as pe,R as Qe,c as G,o as je,aj as c,ap as Ge,n as x,q as f,t as u,F as K,L as t,D as M,E as n,I as p,J as _,G as P,K as te,a3 as ce}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function Ke(){const{data:b}=await U.get("/smtp/configs");return b}async function Oe(b){const{data:v}=await U.post("/smtp/configs",b);return v}async function Re(b,v){const{data:g}=await U.put(`/smtp/configs/${b}`,v);return g}async function Je(b){const{data:v}=await U.delete(`/smtp/configs/${b}`);return v}async function We(b,v){const{data:g}=await U.post(`/smtp/configs/${b}/test`,{email:v});return g}async function Xe(b){const{data:v}=await U.post(`/smtp/configs/${b}/primary`);return v}async function Ye(){const{data:b}=await U.post("/smtp/configs/primary/clear");return b}const Ze={class:"page-stack"},el={class:"help app-muted"},ll={class:"section-head"},tl={class:"table-wrap"},al={class:"sub-stats"},sl={class:"help app-muted"},nl={class:"section-head"},ol={class:"toolbar"},il={class:"table-wrap"},ul=["title"],rl=["title"],dl=["title"],ml={class:"pagination"},pl={class:"page-hint app-muted"},cl={style:{width:"100%"}},fl={key:0,class:"help"},_l={key:0},bl={key:0},vl={key:0},yl={class:"dialog-actions"},fe=15,gl={__name:"EmailPage",setup(b){const v=m(!1),g=m(!1),o=pe({enabled:!1,failover_enabled:!0,register_verify_enabled:!1,login_alert_enabled:!0,task_notify_enabled:!1,base_url:"",updated_at:null});let L=null;async function ae(){v.value=!0;try{const s=await Ne();o.enabled=!!s.enabled,o.failover_enabled=!!s.failover_enabled,o.register_verify_enabled=!!s.register_verify_enabled,o.login_alert_enabled=s.login_alert_enabled===void 0?!0:!!s.login_alert_enabled,o.task_notify_enabled=!!s.task_notify_enabled,o.base_url=s.base_url||"",o.updated_at=s.updated_at||null}catch{}finally{v.value=!1}}async function _e(){if(!v.value){g.value=!0;try{const s=await Ie({enabled:o.enabled,failover_enabled:o.failover_enabled,register_verify_enabled:o.register_verify_enabled,login_alert_enabled:o.login_alert_enabled,task_notify_enabled:o.task_notify_enabled,base_url:(o.base_url||"").trim()});if(!s?.success){d.error(s?.error||"更新失败");return}d.success("邮件设置已更新"),await ae()}catch{}finally{g.value=!1}}}function E(){L&&window.clearTimeout(L),L=window.setTimeout(_e,300)}Qe(()=>{L&&window.clearTimeout(L),L=null});const O=m(!1),R=m([]),S=m(!1),k=m(!1),J=m(!1),H=m(!1),a=pe({id:null,name:"默认配置",enabled:!0,host:"",port:465,username:"",password:"",use_ssl:!0,use_tls:!1,sender_name:"自动化学习",sender_email:"",daily_limit:0,priority:0}),N=[{key:"custom",label:"自定义(手动填写)",defaults:null,note:"适用于其他邮箱/自建SMTP",links:[]},{key:"gmail",label:"Gmail",defaults:{host:"smtp.gmail.com",port:465,use_ssl:!0,use_tls:!1},note:"通常需要开启两步验证并创建应用专用密码(App Password)",links:[{label:"SMTP 设置说明",url:"https://support.google.com/mail/answer/7126229?hl=zh-Hans"},{label:"App Password",url:"https://myaccount.google.com/apppasswords"}]},{key:"qq",label:"QQ 邮箱",defaults:{host:"smtp.qq.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并获取授权码(不是QQ登录密码)",links:[{label:"QQ邮箱 SMTP 帮助",url:"https://service.mail.qq.com/cgi-bin/help?subtype=1&id=28&no=1001256"}]},{key:"163",label:"163 邮箱",defaults:{host:"smtp.163.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并使用授权码/客户端授权密码",links:[{label:"网易邮箱 SMTP 帮助",url:"https://help.mail.163.com/faqDetail.do?code=d7a5dc8471a22b76"}]},{key:"126",label:"126 邮箱",defaults:{host:"smtp.126.com",port:465,use_ssl:!0,use_tls:!1},note:"需要在邮箱设置中开启 SMTP 并使用授权码/客户端授权密码",links:[{label:"网易邮箱帮助",url:"https://help.mail.163.com/"}]},{key:"outlook",label:"Outlook/Hotmail",defaults:{host:"smtp-mail.outlook.com",port:587,use_ssl:!1,use_tls:!0},note:"建议使用 TLS 587(部分账号需开启 SMTP AUTH)",links:[{label:"微软 SMTP 设置",url:"https://support.microsoft.com/office/pop-imap-and-smtp-settings-for-outlook-com-d088b0b7-0d38-4f9a-bc5d-509f9e4c6d3d"}]},{key:"office365",label:"Microsoft 365/Exchange",defaults:{host:"smtp.office365.com",port:587,use_ssl:!1,use_tls:!0},note:"企业邮箱常用配置(需启用 SMTP AUTH)",links:[{label:"微软官方说明",url:"https://learn.microsoft.com/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission"}]},{key:"icloud",label:"iCloud",defaults:{host:"smtp.mail.me.com",port:587,use_ssl:!1,use_tls:!0},note:"需要在 Apple ID 中生成“App 专用密码”",links:[{label:"Apple 邮件服务器设置",url:"https://support.apple.com/zh-cn/HT202304"}]},{key:"tencent_exmail",label:"腾讯企业邮箱",defaults:{host:"smtp.exmail.qq.com",port:465,use_ssl:!0,use_tls:!1},note:"企业邮箱常用配置",links:[{label:"腾讯企业邮箱帮助",url:"https://service.exmail.qq.com/cgi-bin/help?subtype=1&id=23&no=1001068"}]},{key:"aliyun_exmail",label:"阿里企业邮箱",defaults:{host:"smtp.mxhichina.com",port:465,use_ssl:!0,use_tls:!1},note:"企业邮箱常用配置",links:[{label:"阿里云文档",url:"https://help.aliyun.com/document_detail/50652.html"}]}],q=m("custom"),V=G(()=>N.find(s=>s.key===q.value)||N[0]),be=G(()=>k.value&&J.value?"留空保持不变":"SMTP密码或授权码");function ve(s){const e=String(s?.host||"").trim().toLowerCase();return e&&{"smtp.gmail.com":"gmail","smtp.qq.com":"qq","smtp.163.com":"163","smtp.126.com":"126","smtp-mail.outlook.com":"outlook","smtp.office365.com":"office365","smtp.mail.me.com":"icloud","smtp.exmail.qq.com":"tencent_exmail","smtp.mxhichina.com":"aliyun_exmail"}[e]||"custom"}function ye(s){const e=N.find(i=>i.key===s);!e||!e.defaults||(a.host=e.defaults.host,a.port=e.defaults.port,a.use_ssl=e.defaults.use_ssl,a.use_tls=e.defaults.use_tls)}function se(){a.id=null,a.name="默认配置",a.enabled=!0,a.host="",a.port=465,a.username="",a.password="",a.use_ssl=!0,a.use_tls=!1,a.sender_name="自动化学习",a.sender_email="",a.daily_limit=0,a.priority=0,J.value=!1,H.value=!1,q.value="custom"}async function $(){O.value=!0;try{R.value=await Ke()}catch{R.value=[]}finally{O.value=!1}}function ge(){k.value=!1,se(),q.value="custom",S.value=!0}function ke(s){k.value=!0,se(),a.id=s.id,a.name=s.name||"默认配置",a.enabled=!!s.enabled,a.host=s.host||"",a.port=s.port||465,a.username=s.username||"",a.password="",a.use_ssl=!!s.use_ssl,a.use_tls=!!s.use_tls,a.sender_name=s.sender_name||"自动化学习",a.sender_email=s.sender_email||"",a.daily_limit=s.daily_limit??0,a.priority=s.priority??0,J.value=!!s.has_password,H.value=!!s.is_primary,q.value=ve(s),S.value=!0}function ne(s){return s.is_primary?{label:"主",type:"warning"}:s.enabled?{label:"备用",type:"success"}:{label:"禁用",type:"info"}}function he(s){return s.daily_limit&&s.daily_limit>0?`${s.daily_sent}/${s.daily_limit}`:`${s.daily_sent}/∞`}async function Ve(){if(!a.host.trim()){d.error("SMTP服务器地址不能为空");return}if(!a.username.trim()){d.error("SMTP用户名不能为空");return}const s={name:a.name.trim()||"默认配置",enabled:!!a.enabled,priority:Number(a.priority)||0,host:a.host.trim(),port:Number(a.port)||465,username:a.username.trim(),use_ssl:!!a.use_ssl,use_tls:!!a.use_tls,sender_name:(a.sender_name||"").trim(),sender_email:(a.sender_email||"").trim(),daily_limit:Number(a.daily_limit)||0};try{if(k.value){const e={...s};a.password&&(e.password=a.password);const i=await Re(a.id,e);if(!i?.success){d.error(i?.error||"更新失败");return}d.success("保存成功")}else{const e={...s};a.password&&(e.password=a.password);const i=await Oe(e);if(!i?.success){d.error(i?.error||"创建失败");return}d.success("创建成功")}S.value=!1,await $()}catch{}}async function we(){if(!k.value||!a.id){d.error("请先保存配置后再测试");return}let s;try{const e=await B.prompt("请输入测试收件邮箱","测试连接",{inputPlaceholder:"name@example.com",confirmButtonText:"发送测试邮件",cancelButtonText:"取消",inputValidator:i=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(i||"").trim()),inputErrorMessage:"邮箱格式不正确"});s=String(e.value||"").trim()}catch{return}try{const e=await We(a.id,s);e?.success?(d.success("测试成功,邮件已发送"),await $()):await B.alert(e?.error||"测试失败","测试失败",{confirmButtonText:"知道了"})}catch{}}async function Se(){if(!(!k.value||!a.id)){try{await B.confirm("确定将该配置设为主配置吗?","设为主配置",{confirmButtonText:"设为主配置",cancelButtonText:"取消",type:"warning"})}catch{return}try{const s=await Xe(a.id);if(!s?.success){d.error(s?.error||"设置失败");return}d.success("已设为主配置"),S.value=!1,await $()}catch{}}}async function Te(){if(k.value){try{await B.confirm("确定取消主配置吗?取消后将按优先级选择可用SMTP。","取消主配置",{confirmButtonText:"取消主配置",cancelButtonText:"保留",type:"warning"})}catch{return}try{const s=await Ye();if(!s?.success){d.error(s?.error||"操作失败");return}d.success("已取消主配置"),S.value=!1,await $()}catch{}}}async function xe(){if(!(!k.value||!a.id)){try{await B.confirm("确定删除该SMTP配置吗?此操作不可恢复。","删除配置",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const s=await Je(a.id);if(!s?.success){d.error(s?.error||"删除失败");return}d.success("已删除"),S.value=!1,await $()}catch{}}}const A=m(!1),h=m({}),W=m(!1),F=m(""),I=m(""),z=m(1),X=m([]),Y=m(0),Z=m(1);function Pe(s){return{register:"注册验证",reset:"密码重置",bind:"邮箱绑定",task_complete:"任务完成",security_alert:"安全告警"}[s]||s}function oe(s){return s?.username&&s?.user_id?`${s.username} (#${s.user_id})`:s?.user_id?`用户#${s.user_id}`:"系统"}const Ce=G(()=>[{key:"total_sent",label:"总发送",value:h.value?.total_sent||0,tone:"blue"},{key:"total_success",label:"成功",value:h.value?.total_success||0,tone:"green"},{key:"total_failed",label:"失败",value:h.value?.total_failed||0,tone:"red"},{key:"success_rate",label:"成功率",value:`${h.value?.success_rate||0}%`,tone:"purple"}]),Be=G(()=>[{key:"register_sent",label:"注册验证",value:h.value?.register_sent||0,tone:"cyan"},{key:"reset_sent",label:"密码重置",value:h.value?.reset_sent||0,tone:"orange"},{key:"bind_sent",label:"邮箱绑定",value:h.value?.bind_sent||0,tone:"purple"},{key:"task_complete_sent",label:"任务完成",value:h.value?.task_complete_sent||0,tone:"green"}]);async function Me(){A.value=!0;try{h.value=await Fe()}catch{h.value={}}finally{A.value=!1}}async function D(s=1){W.value=!0;try{const e={page:s,page_size:fe};F.value&&(e.type=F.value),I.value&&(e.status=I.value);const i=await De(e);X.value=i?.logs||[],Y.value=i?.total||0,z.value=i?.page||s,Z.value=i?.total_pages||1}catch{X.value=[],Y.value=0,Z.value=1}finally{W.value=!1}}async function Ue(){let s;try{const e=await B.prompt("请输入保留天数(将删除该天数之前的日志)","清理日志",{inputValue:"30",confirmButtonText:"清理",cancelButtonText:"取消",inputValidator:i=>{const r=parseInt(String(i),10);return Number.isFinite(r)&&r>=7},inputErrorMessage:"天数必须大于等于7"});s=parseInt(String(e.value),10)}catch{return}try{await B.confirm(`确定删除 ${s} 天之前的邮件日志吗?`,"二次确认",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await He(s);if(!e?.success){d.error(e?.error||"清理失败");return}d.success(`已清理 ${e.deleted} 条日志`),await D(1)}catch{}}async function Le(){await Promise.all([ae(),$(),Me(),D(1)])}return je(Le),(s,e)=>{const i=c("el-switch"),r=c("el-form-item"),Ee=c("el-divider"),C=c("el-input"),ie=c("el-form"),Q=c("el-card"),T=c("el-button"),ue=c("el-tag"),y=c("el-table-column"),re=c("el-table"),w=c("el-option"),ee=c("el-select"),qe=c("el-pagination"),$e=c("el-link"),le=c("el-input-number"),Ae=c("el-dialog"),j=Ge("loading");return f(),x("div",Ze,[e[43]||(e[43]=u("div",{class:"app-page-title"},[u("h2",null,"邮件配置")],-1)),K((f(),M(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[e[29]||(e[29]=u("h3",{class:"section-title"},"全局设置",-1)),t(ie,{"label-width":"140px"},{default:n(()=>[t(r,{label:"启用邮件功能"},{default:n(()=>[t(i,{modelValue:o.enabled,"onUpdate:modelValue":e[0]||(e[0]=l=>o.enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"启用故障转移"},{default:n(()=>[t(i,{modelValue:o.failover_enabled,"onUpdate:modelValue":e[1]||(e[1]=l=>o.failover_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"启用注册邮箱验证"},{default:n(()=>[t(i,{modelValue:o.register_verify_enabled,"onUpdate:modelValue":e[2]||(e[2]=l=>o.register_verify_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(Ee,{"content-position":"left"},{default:n(()=>[...e[26]||(e[26]=[p("通知设置",-1)])]),_:1}),t(r,{label:"启用任务完成通知"},{default:n(()=>[t(i,{modelValue:o.task_notify_enabled,"onUpdate:modelValue":e[3]||(e[3]=l=>o.task_notify_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"])]),_:1}),t(r,{label:"新设备登录提醒"},{default:n(()=>[t(i,{modelValue:o.login_alert_enabled,"onUpdate:modelValue":e[4]||(e[4]=l=>o.login_alert_enabled=l),disabled:g.value,onChange:E},null,8,["modelValue","disabled"]),e[27]||(e[27]=u("div",{class:"help"},"当检测到新设备或新IP登录时,发送邮件提醒用户",-1))]),_:1}),t(r,{label:"网站基础URL"},{default:n(()=>[t(C,{modelValue:o.base_url,"onUpdate:modelValue":e[5]||(e[5]=l=>o.base_url=l),placeholder:"例如: https://example.com",disabled:g.value,onBlur:E},null,8,["modelValue","disabled"]),e[28]||(e[28]=u("div",{class:"help"},"用于生成邮件中的验证链接,留空则使用默认配置。",-1))]),_:1})]),_:1}),u("div",el,"最近更新时间:"+_(o.updated_at||"-"),1)]),_:1})),[[j,v.value]]),t(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[u("div",ll,[e[31]||(e[31]=u("h3",{class:"section-title"},"SMTP配置列表",-1)),t(T,{type:"primary",onClick:ge},{default:n(()=>[...e[30]||(e[30]=[p("+ 添加配置",-1)])]),_:1})]),u("div",tl,[K((f(),M(re,{data:R.value,style:{width:"100%"}},{default:n(()=>[t(y,{label:"状态",width:"90"},{default:n(({row:l})=>[t(ue,{type:ne(l).type,effect:"light"},{default:n(()=>[p(_(ne(l).label),1)]),_:2},1032,["type"])]),_:1}),t(y,{prop:"name",label:"名称","min-width":"160"}),t(y,{label:"服务器","min-width":"200"},{default:n(({row:l})=>[p(_(l.host)+":"+_(l.port),1)]),_:1}),t(y,{label:"今日/限额",width:"110"},{default:n(({row:l})=>[p(_(he(l)),1)]),_:1}),t(y,{label:"成功率",width:"100"},{default:n(({row:l})=>[p(_(l.success_rate)+"%",1)]),_:1}),t(y,{label:"操作",width:"120",fixed:"right"},{default:n(({row:l})=>[t(T,{size:"small",onClick:de=>ke(l)},{default:n(()=>[...e[32]||(e[32]=[p("编辑",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[j,O.value]])])]),_:1}),K((f(),M(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[e[33]||(e[33]=u("h3",{class:"section-title"},"邮件发送统计",-1)),t(me,{items:Ce.value,loading:A.value,"min-width":160},null,8,["items","loading"]),u("div",al,[t(me,{items:Be.value,loading:A.value,"min-width":150},null,8,["items","loading"])]),u("div",sl,"最后更新:"+_(h.value.last_updated||"-"),1)]),_:1})),[[j,A.value]]),t(Q,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:n(()=>[u("div",nl,[e[35]||(e[35]=u("h3",{class:"section-title"},"邮件发送日志",-1)),u("div",ol,[t(ee,{modelValue:F.value,"onUpdate:modelValue":e[6]||(e[6]=l=>F.value=l),style:{width:"140px"},onChange:e[7]||(e[7]=l=>D(1))},{default:n(()=>[t(w,{label:"全部类型",value:""}),t(w,{label:"注册验证",value:"register"}),t(w,{label:"密码重置",value:"reset"}),t(w,{label:"邮箱绑定",value:"bind"}),t(w,{label:"任务完成",value:"task_complete"}),t(w,{label:"安全告警",value:"security_alert"})]),_:1},8,["modelValue"]),t(ee,{modelValue:I.value,"onUpdate:modelValue":e[8]||(e[8]=l=>I.value=l),style:{width:"120px"},onChange:e[9]||(e[9]=l=>D(1))},{default:n(()=>[t(w,{label:"全部状态",value:""}),t(w,{label:"成功",value:"success"}),t(w,{label:"失败",value:"failed"})]),_:1},8,["modelValue"]),t(T,{type:"danger",plain:"",onClick:Ue},{default:n(()=>[...e[34]||(e[34]=[p("清理日志",-1)])]),_:1})])]),u("div",il,[K((f(),M(re,{data:X.value,style:{width:"100%"}},{default:n(()=>[t(y,{prop:"created_at",label:"时间",width:"180"}),t(y,{prop:"email_to",label:"收件人","min-width":"180"}),t(y,{label:"来源用户","min-width":"160"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:oe(l)},_(oe(l)),9,ul)]),_:1}),t(y,{label:"类型",width:"120"},{default:n(({row:l})=>[p(_(Pe(l.email_type)),1)]),_:1}),t(y,{label:"主题","min-width":"220"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:l.subject},_(l.subject),9,rl)]),_:1}),t(y,{label:"状态",width:"90"},{default:n(({row:l})=>[t(ue,{type:l.status==="success"?"success":"danger",effect:"light"},{default:n(()=>[p(_(l.status==="success"?"成功":"失败"),1)]),_:2},1032,["type"])]),_:1}),t(y,{label:"错误","min-width":"200"},{default:n(({row:l})=>[u("span",{class:"ellipsis",title:l.error_message||""},_(l.error_message||"-"),9,dl)]),_:1})]),_:1},8,["data"])),[[j,W.value]])]),u("div",ml,[t(qe,{"current-page":z.value,"onUpdate:currentPage":e[10]||(e[10]=l=>z.value=l),"page-size":fe,total:Y.value,layout:"prev, pager, next, ->, total",onCurrentChange:D},null,8,["current-page","total"]),u("div",pl,"第 "+_(z.value)+" / "+_(Z.value)+" 页",1)])]),_:1}),t(Ae,{modelValue:S.value,"onUpdate:modelValue":e[25]||(e[25]=l=>S.value=l),title:k.value?"编辑SMTP配置":"添加SMTP配置",width:"min(560px, 92vw)"},{footer:n(()=>[u("div",yl,[t(T,{onClick:we},{default:n(()=>[...e[36]||(e[36]=[p("测试连接",-1)])]),_:1}),k.value&&H.value?(f(),M(T,{key:0,type:"warning",plain:"",onClick:Te},{default:n(()=>[...e[37]||(e[37]=[p("取消主配置",-1)])]),_:1})):P("",!0),k.value&&!H.value?(f(),M(T,{key:1,onClick:Se},{default:n(()=>[...e[38]||(e[38]=[p("设为主配置",-1)])]),_:1})):P("",!0),k.value?(f(),M(T,{key:2,type:"danger",plain:"",onClick:xe},{default:n(()=>[...e[39]||(e[39]=[p("删除配置",-1)])]),_:1})):P("",!0),e[42]||(e[42]=u("div",{class:"spacer"},null,-1)),t(T,{onClick:e[24]||(e[24]=l=>S.value=!1)},{default:n(()=>[...e[40]||(e[40]=[p("取消",-1)])]),_:1}),t(T,{type:"primary",onClick:Ve},{default:n(()=>[...e[41]||(e[41]=[p("保存",-1)])]),_:1})])]),default:n(()=>[t(ie,{"label-width":"120px"},{default:n(()=>[t(r,{label:"名称"},{default:n(()=>[t(C,{modelValue:a.name,"onUpdate:modelValue":e[11]||(e[11]=l=>a.name=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"启用"},{default:n(()=>[t(i,{modelValue:a.enabled,"onUpdate:modelValue":e[12]||(e[12]=l=>a.enabled=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"邮箱模板"},{default:n(()=>[u("div",cl,[t(ee,{modelValue:q.value,"onUpdate:modelValue":e[13]||(e[13]=l=>q.value=l),placeholder:"选择常用邮箱模板",style:{width:"100%"},onChange:ye},{default:n(()=>[(f(),x(te,null,ce(N,l=>t(w,{key:l.key,label:l.label,value:l.key},null,8,["label","value"])),64))]),_:1},8,["modelValue"]),V.value.note||V.value.links&&V.value.links.length?(f(),x("div",fl,[V.value.note?(f(),x("span",_l,_(V.value.note),1)):P("",!0),V.value.links&&V.value.links.length?(f(),x(te,{key:1},[V.value.note?(f(),x("span",bl," · ")):P("",!0),(f(!0),x(te,null,ce(V.value.links,(l,de)=>(f(),x("span",{key:l.url},[t($e,{href:l.url,target:"_blank",type:"primary",underline:!1},{default:n(()=>[p(_(l.label),1)]),_:2},1032,["href"]),de[t(C,{modelValue:a.host,"onUpdate:modelValue":e[14]||(e[14]=l=>a.host=l),placeholder:"smtp.example.com"},null,8,["modelValue"])]),_:1}),t(r,{label:"端口"},{default:n(()=>[t(le,{modelValue:a.port,"onUpdate:modelValue":e[15]||(e[15]=l=>a.port=l),min:1,max:65535},null,8,["modelValue"])]),_:1}),t(r,{label:"用户名"},{default:n(()=>[t(C,{modelValue:a.username,"onUpdate:modelValue":e[16]||(e[16]=l=>a.username=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"密码"},{default:n(()=>[t(C,{modelValue:a.password,"onUpdate:modelValue":e[17]||(e[17]=l=>a.password=l),type:"password","show-password":"",placeholder:be.value},null,8,["modelValue","placeholder"])]),_:1}),t(r,{label:"SSL"},{default:n(()=>[t(i,{modelValue:a.use_ssl,"onUpdate:modelValue":e[18]||(e[18]=l=>a.use_ssl=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"TLS"},{default:n(()=>[t(i,{modelValue:a.use_tls,"onUpdate:modelValue":e[19]||(e[19]=l=>a.use_tls=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"发件人名称"},{default:n(()=>[t(C,{modelValue:a.sender_name,"onUpdate:modelValue":e[20]||(e[20]=l=>a.sender_name=l)},null,8,["modelValue"])]),_:1}),t(r,{label:"发件人邮箱"},{default:n(()=>[t(C,{modelValue:a.sender_email,"onUpdate:modelValue":e[21]||(e[21]=l=>a.sender_email=l),placeholder:"可选"},null,8,["modelValue"])]),_:1}),t(r,{label:"每日限额"},{default:n(()=>[t(le,{modelValue:a.daily_limit,"onUpdate:modelValue":e[22]||(e[22]=l=>a.daily_limit=l),min:0,max:1e6},null,8,["modelValue"])]),_:1}),t(r,{label:"优先级"},{default:n(()=>[t(le,{modelValue:a.priority,"onUpdate:modelValue":e[23]||(e[23]=l=>a.priority=l),min:0,max:1e3},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue","title"])])}}},Pl=ze(gl,[["__scopeId","data-v-b1cb7390"]]);export{Pl as default}; diff --git a/static/admin/assets/FeedbacksPage-CPmSqIaj.css b/static/admin/assets/FeedbacksPage-CPmSqIaj.css new file mode 100644 index 0000000..6c99f12 --- /dev/null +++ b/static/admin/assets/FeedbacksPage-CPmSqIaj.css @@ -0,0 +1 @@ +.page-stack[data-v-2e7ce230]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-2e7ce230]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-2e7ce230]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-head[data-v-2e7ce230]{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-2e7ce230]{margin:0;font-size:15px;font-weight:800}.table-wrap[data-v-2e7ce230]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-2e7ce230]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-2e7ce230]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/FeedbacksPage-BAnFKHSL.js b/static/admin/assets/FeedbacksPage-DrMVqBKf.js similarity index 96% rename from static/admin/assets/FeedbacksPage-BAnFKHSL.js rename to static/admin/assets/FeedbacksPage-DrMVqBKf.js index 10ff1a4..e7e2607 100644 --- a/static/admin/assets/FeedbacksPage-BAnFKHSL.js +++ b/static/admin/assets/FeedbacksPage-DrMVqBKf.js @@ -1 +1 @@ -import{_ as $,b as j,r as G,d as L,e as q}from"./index-C1f9ticl.js";import{M as J}from"./MetricGrid-BnihYB_8.js";import{E as w,a as k}from"./vendor-element-B5S5pUKo.js";import{i as K,r as f,c as O,o as R,aj as i,ap as U,n as x,q as v,t as s,L as t,E as l,K as F,a3 as A,J as d,F as H,D as Q,I as p,G as W}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";const X={class:"page-stack"},Y={class:"app-page-title"},Z={class:"toolbar"},ee={class:"section-head"},te={class:"app-muted"},ae={class:"table-wrap"},le={class:"ellipsis"},ne={class:"ellipsis"},se={class:"ellipsis"},oe={class:"actions"},ie={__name:"FeedbacksPage",setup(ce){const T=K("refreshNavBadges",null),u=f(!1),b=f(""),c=f({total:0,pending:0,replied:0,closed:0}),_=f([]),V=[{label:"全部状态",value:""},{label:"待处理",value:"pending"},{label:"已回复",value:"replied"},{label:"已关闭",value:"closed"}],M=O(()=>[{key:"total",label:"总反馈",value:c.value.total||0,tone:"blue"},{key:"pending",label:"待处理",value:c.value.pending||0,tone:"orange"},{key:"replied",label:"已回复",value:c.value.replied||0,tone:"green"},{key:"closed",label:"已关闭",value:c.value.closed||0,tone:"purple"}]);function B(n){return n==="pending"?{label:"待处理",type:"warning"}:n==="replied"?{label:"已回复",type:"success"}:n==="closed"?{label:"已关闭",type:"info"}:{label:n||"-",type:"info"}}async function r(){u.value=!0;try{const n=await j(b.value);_.value=n?.feedbacks||[],c.value=n?.stats||{total:0,pending:0,replied:0,closed:0}}catch{_.value=[],c.value={total:0,pending:0,replied:0,closed:0}}finally{u.value=!1}await T?.({pendingFeedbacks:c.value.pending||0})}async function D(n){let a;try{a=(await w.prompt("请输入回复内容","回复反馈",{inputType:"textarea",inputPlaceholder:"回复内容",confirmButtonText:"提交",cancelButtonText:"取消",inputValidator:g=>!!String(g||"").trim(),inputErrorMessage:"回复内容不能为空"})).value}catch{return}try{const m=await G(n.id,String(a||"").trim());k.success(m?.message||"回复成功"),await r()}catch{}}async function E(n){try{await w.confirm("确定要关闭这个反馈吗?","关闭反馈",{confirmButtonText:"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}try{const a=await L(n.id);k.success(a?.message||"反馈已关闭"),await r()}catch{}}async function N(n){try{await w.confirm("确定要删除这个反馈吗?此操作不可恢复!","删除反馈",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const a=await q(n.id);k.success(a?.message||"反馈已删除"),await r()}catch{}}return R(r),(n,a)=>{const m=i("el-option"),g=i("el-select"),o=i("el-table-column"),y=i("el-tooltip"),I=i("el-tag"),h=i("el-button"),z=i("el-table"),P=i("el-card"),S=U("loading");return v(),x("div",X,[s("div",Y,[a[1]||(a[1]=s("h2",null,"反馈管理",-1)),s("div",Z,[t(g,{modelValue:b.value,"onUpdate:modelValue":a[0]||(a[0]=e=>b.value=e),style:{width:"160px"},onChange:r},{default:l(()=>[(v(),x(F,null,A(V,e=>t(m,{key:e.value,label:e.label,value:e.value},null,8,["label","value"])),64))]),_:1},8,["modelValue"])])]),t(J,{items:M.value,loading:u.value,"min-width":165},null,8,["items","loading"]),t(P,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[s("div",ee,[a[2]||(a[2]=s("h3",{class:"section-title"},"反馈列表",-1)),s("div",te,"共 "+d(_.value.length)+" 条(当前筛选)",1)]),s("div",ae,[H((v(),Q(z,{data:_.value,style:{width:"100%"}},{default:l(()=>[t(o,{prop:"id",label:"ID",width:"80"}),t(o,{prop:"username",label:"用户",width:"140"}),t(o,{label:"标题","min-width":"180"},{default:l(({row:e})=>[t(y,{content:e.title,placement:"top","show-after":300},{default:l(()=>[s("span",le,d(e.title),1)]),_:2},1032,["content"])]),_:1}),t(o,{label:"描述","min-width":"220"},{default:l(({row:e})=>[t(y,{content:e.description,placement:"top","show-after":300},{default:l(()=>[s("span",ne,d(e.description),1)]),_:2},1032,["content"])]),_:1}),t(o,{prop:"contact",label:"联系方式","min-width":"160"},{default:l(({row:e})=>[p(d(e.contact||"-"),1)]),_:1}),t(o,{label:"状态",width:"110"},{default:l(({row:e})=>[t(I,{type:B(e.status).type,effect:"light"},{default:l(()=>[p(d(B(e.status).label),1)]),_:2},1032,["type"])]),_:1}),t(o,{prop:"created_at",label:"提交时间",width:"180"}),t(o,{label:"回复","min-width":"180"},{default:l(({row:e})=>[t(y,{content:e.admin_reply||"",placement:"top","show-after":300},{default:l(()=>[s("span",se,d(e.admin_reply||"-"),1)]),_:2},1032,["content"])]),_:1}),t(o,{label:"操作",width:"220",fixed:"right"},{default:l(({row:e})=>[s("div",oe,[e.status!=="closed"?(v(),x(F,{key:0},[t(h,{type:"primary",size:"small",onClick:C=>D(e)},{default:l(()=>[...a[3]||(a[3]=[p("回复",-1)])]),_:1},8,["onClick"]),t(h,{size:"small",onClick:C=>E(e)},{default:l(()=>[...a[4]||(a[4]=[p("关闭",-1)])]),_:1},8,["onClick"])],64)):W("",!0),t(h,{type:"danger",size:"small",onClick:C=>N(e)},{default:l(()=>[...a[5]||(a[5]=[p("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[S,u.value]])])]),_:1})])}}},fe=$(ie,[["__scopeId","data-v-910fe89b"]]);export{fe as default}; +import{_ as $,b as j,r as G,d as L,e as q}from"./index-DOvMEmc8.js";import{M as J}from"./MetricGrid-C3Xjc9mZ.js";import{E as w,a as k}from"./vendor-element-B5S5pUKo.js";import{i as K,r as f,c as O,o as R,aj as i,ap as U,n as x,q as v,t as s,L as t,E as l,K as F,a3 as A,J as d,F as H,D as Q,I as p,G as W}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";const X={class:"page-stack"},Y={class:"app-page-title"},Z={class:"toolbar"},ee={class:"section-head"},te={class:"app-muted"},ae={class:"table-wrap"},le={class:"ellipsis"},ne={class:"ellipsis"},se={class:"ellipsis"},oe={class:"actions"},ie={__name:"FeedbacksPage",setup(ce){const T=K("refreshNavBadges",null),u=f(!1),b=f(""),c=f({total:0,pending:0,replied:0,closed:0}),_=f([]),V=[{label:"全部状态",value:""},{label:"待处理",value:"pending"},{label:"已回复",value:"replied"},{label:"已关闭",value:"closed"}],M=O(()=>[{key:"total",label:"总反馈",value:c.value.total||0,tone:"blue"},{key:"pending",label:"待处理",value:c.value.pending||0,tone:"orange"},{key:"replied",label:"已回复",value:c.value.replied||0,tone:"green"},{key:"closed",label:"已关闭",value:c.value.closed||0,tone:"purple"}]);function B(n){return n==="pending"?{label:"待处理",type:"warning"}:n==="replied"?{label:"已回复",type:"success"}:n==="closed"?{label:"已关闭",type:"info"}:{label:n||"-",type:"info"}}async function r(){u.value=!0;try{const n=await j(b.value);_.value=n?.feedbacks||[],c.value=n?.stats||{total:0,pending:0,replied:0,closed:0}}catch{_.value=[],c.value={total:0,pending:0,replied:0,closed:0}}finally{u.value=!1}await T?.({pendingFeedbacks:c.value.pending||0})}async function D(n){let a;try{a=(await w.prompt("请输入回复内容","回复反馈",{inputType:"textarea",inputPlaceholder:"回复内容",confirmButtonText:"提交",cancelButtonText:"取消",inputValidator:g=>!!String(g||"").trim(),inputErrorMessage:"回复内容不能为空"})).value}catch{return}try{const m=await G(n.id,String(a||"").trim());k.success(m?.message||"回复成功"),await r()}catch{}}async function E(n){try{await w.confirm("确定要关闭这个反馈吗?","关闭反馈",{confirmButtonText:"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}try{const a=await L(n.id);k.success(a?.message||"反馈已关闭"),await r()}catch{}}async function N(n){try{await w.confirm("确定要删除这个反馈吗?此操作不可恢复!","删除反馈",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{const a=await q(n.id);k.success(a?.message||"反馈已删除"),await r()}catch{}}return R(r),(n,a)=>{const m=i("el-option"),g=i("el-select"),o=i("el-table-column"),y=i("el-tooltip"),I=i("el-tag"),h=i("el-button"),z=i("el-table"),P=i("el-card"),S=U("loading");return v(),x("div",X,[s("div",Y,[a[1]||(a[1]=s("h2",null,"反馈管理",-1)),s("div",Z,[t(g,{modelValue:b.value,"onUpdate:modelValue":a[0]||(a[0]=e=>b.value=e),style:{width:"160px"},onChange:r},{default:l(()=>[(v(),x(F,null,A(V,e=>t(m,{key:e.value,label:e.label,value:e.value},null,8,["label","value"])),64))]),_:1},8,["modelValue"])])]),t(J,{items:M.value,loading:u.value,"min-width":165},null,8,["items","loading"]),t(P,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[s("div",ee,[a[2]||(a[2]=s("h3",{class:"section-title"},"反馈列表",-1)),s("div",te,"共 "+d(_.value.length)+" 条(当前筛选)",1)]),s("div",ae,[H((v(),Q(z,{data:_.value,style:{width:"100%"}},{default:l(()=>[t(o,{prop:"id",label:"ID",width:"80"}),t(o,{prop:"username",label:"用户",width:"140"}),t(o,{label:"标题","min-width":"180"},{default:l(({row:e})=>[t(y,{content:e.title,placement:"top","show-after":300},{default:l(()=>[s("span",le,d(e.title),1)]),_:2},1032,["content"])]),_:1}),t(o,{label:"描述","min-width":"220"},{default:l(({row:e})=>[t(y,{content:e.description,placement:"top","show-after":300},{default:l(()=>[s("span",ne,d(e.description),1)]),_:2},1032,["content"])]),_:1}),t(o,{prop:"contact",label:"联系方式","min-width":"160"},{default:l(({row:e})=>[p(d(e.contact||"-"),1)]),_:1}),t(o,{label:"状态",width:"110"},{default:l(({row:e})=>[t(I,{type:B(e.status).type,effect:"light"},{default:l(()=>[p(d(B(e.status).label),1)]),_:2},1032,["type"])]),_:1}),t(o,{prop:"created_at",label:"提交时间",width:"180"}),t(o,{label:"回复","min-width":"180"},{default:l(({row:e})=>[t(y,{content:e.admin_reply||"",placement:"top","show-after":300},{default:l(()=>[s("span",se,d(e.admin_reply||"-"),1)]),_:2},1032,["content"])]),_:1}),t(o,{label:"操作",width:"220",fixed:"right"},{default:l(({row:e})=>[s("div",oe,[e.status!=="closed"?(v(),x(F,{key:0},[t(h,{type:"primary",size:"small",onClick:C=>D(e)},{default:l(()=>[...a[3]||(a[3]=[p("回复",-1)])]),_:1},8,["onClick"]),t(h,{size:"small",onClick:C=>E(e)},{default:l(()=>[...a[4]||(a[4]=[p("关闭",-1)])]),_:1},8,["onClick"])],64)):W("",!0),t(h,{type:"danger",size:"small",onClick:C=>N(e)},{default:l(()=>[...a[5]||(a[5]=[p("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[S,u.value]])])]),_:1})])}}},fe=$(ie,[["__scopeId","data-v-2e7ce230"]]);export{fe as default}; diff --git a/static/admin/assets/FeedbacksPage-mrXjCiV2.css b/static/admin/assets/FeedbacksPage-mrXjCiV2.css deleted file mode 100644 index 1263905..0000000 --- a/static/admin/assets/FeedbacksPage-mrXjCiV2.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-910fe89b]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-910fe89b]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-910fe89b]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-head[data-v-910fe89b]{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.section-title[data-v-910fe89b]{margin:0;font-size:15px;font-weight:800}.table-wrap[data-v-910fe89b]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-910fe89b]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions[data-v-910fe89b]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/LogsPage-BUgx3sZr.css b/static/admin/assets/LogsPage-BUgx3sZr.css new file mode 100644 index 0000000..39c0074 --- /dev/null +++ b/static/admin/assets/LogsPage-BUgx3sZr.css @@ -0,0 +1 @@ +.page-stack[data-v-2627b2c9]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-2627b2c9]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.filters[data-v-2627b2c9]{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.table-wrap[data-v-2627b2c9]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-2627b2c9]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-2627b2c9]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-2627b2c9]{font-size:12px} diff --git a/static/admin/assets/LogsPage-DFPeq0bL.js b/static/admin/assets/LogsPage-Cy6Q0ave.js similarity index 97% rename from static/admin/assets/LogsPage-DFPeq0bL.js rename to static/admin/assets/LogsPage-Cy6Q0ave.js index df8db0f..46e87ec 100644 --- a/static/admin/assets/LogsPage-DFPeq0bL.js +++ b/static/admin/assets/LogsPage-Cy6Q0ave.js @@ -1 +1 @@ -import{f as K}from"./users-te9ySk34.js";import{g as G,h as H}from"./tasks-Bep0SUyu.js";import{_ as Q}from"./index-C1f9ticl.js";import{E as N,a as X}from"./vendor-element-B5S5pUKo.js";import{r as u,c as Z,o as ee,aj as r,ap as te,n as P,q as b,t as f,L as t,E as l,K as ae,a3 as le,D as F,I as m,F as oe,J as d}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";function ne(p){return String(p||"").trim()}function se(p){return!p.startsWith("user_scheduled")||!p.includes(":")?"":p.split(":",2)[1]||""}function ue(p){const s=ne(p);if(!s||s==="manual")return{group:"manual",label:"手动",type:"success",tooltip:""};if(s==="scheduled")return{group:"scheduled",label:"定时任务",type:"primary",tooltip:"系统定时"};if(s.startsWith("user_scheduled")){const i=se(s),v=String(i||"").replace(/^batch_/,"");return{group:"scheduled",label:"定时任务",type:"primary",tooltip:v?`用户定时批次:${v}`:"用户定时"}}return{group:"manual",label:"手动",type:"success",tooltip:{batch:"手动批量",manual_screenshot:"手动截图",immediate:"立即执行",resumed:"断点恢复"}[s]||s}}const re={class:"page-stack"},ie={class:"filters"},ce={class:"table-wrap"},de={class:"ellipsis"},pe={class:"pagination"},me={class:"page-hint app-muted"},B=20,fe={__name:"LogsPage",setup(p){const s=u(!1),S=u([]),y=u(0),i=u(1),v=u(!1),T=u([]),h=u(""),w=u(""),V=u(""),x=u(""),k=u(""),$=Z(()=>Math.max(1,Math.ceil((y.value||0)/B)));function Y(o){if(o==null)return"-";const e=Number(o);return Number.isFinite(e)?e<60?`${e}秒`:`${Math.floor(e/60)}分${e%60}秒`:"-"}function _(o){const e=ue(o);return{key:e.group,label:e.label,type:e.type,tooltip:e.tooltip}}function I(o){return o==="success"?{label:"成功",type:"success"}:o==="failed"?{label:"失败",type:"danger"}:{label:o||"-",type:"info"}}async function z(){v.value=!0;try{const o=await K();T.value=(o||[]).map(e=>({id:e.id,username:e.username}))}catch{T.value=[]}finally{v.value=!1}}async function M(){s.value=!0;try{const o=(i.value-1)*B,e={limit:B,offset:o};h.value&&(e.date=h.value),w.value&&(e.status=w.value),V.value&&(e.source=V.value),x.value&&(e.user_id=x.value),k.value&&(e.account=k.value);const g=await G(e);S.value=g?.logs||[],y.value=g?.total||0}catch{S.value=[],y.value=0}finally{s.value=!1}}function O(){i.value=1,M()}function j(){h.value="",w.value="",V.value="",x.value="",k.value="",i.value=1,M()}async function R(){let o;try{const e=await N.prompt("请输入要清理多少天前的日志(默认30天)","清理旧日志",{inputValue:"30",confirmButtonText:"下一步",cancelButtonText:"取消",inputValidator:g=>{const n=parseInt(String(g),10);return Number.isFinite(n)&&n>=1},inputErrorMessage:"请输入有效的天数(大于0的整数)"});o=parseInt(String(e.value),10)}catch{return}try{await N.confirm(`确定要删除 ${o} 天前的所有日志吗?此操作不可恢复!`,"二次确认",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await H(o);X.success(e?.message||"清理成功"),i.value=1,await M()}catch{}}return ee(async()=>{await z(),await M()}),(o,e)=>{const g=r("el-date-picker"),n=r("el-option"),U=r("el-select"),W=r("el-input"),C=r("el-button"),L=r("el-card"),c=r("el-table-column"),D=r("el-tag"),E=r("el-tooltip"),q=r("el-table"),A=r("el-pagination"),J=te("loading");return b(),P("div",re,[e[9]||(e[9]=f("div",{class:"app-page-title"},[f("h2",null,"任务日志")],-1)),t(L,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[f("div",ie,[t(g,{modelValue:h.value,"onUpdate:modelValue":e[0]||(e[0]=a=>h.value=a),type:"date","value-format":"YYYY-MM-DD",placeholder:"日期",style:{width:"150px"}},null,8,["modelValue"]),t(U,{modelValue:w.value,"onUpdate:modelValue":e[1]||(e[1]=a=>w.value=a),placeholder:"状态",style:{width:"120px"}},{default:l(()=>[t(n,{label:"全部",value:""}),t(n,{label:"成功",value:"success"}),t(n,{label:"失败",value:"failed"})]),_:1},8,["modelValue"]),t(U,{modelValue:V.value,"onUpdate:modelValue":e[2]||(e[2]=a=>V.value=a),placeholder:"来源",style:{width:"120px"}},{default:l(()=>[t(n,{label:"全部",value:""}),t(n,{label:"手动",value:"manual"}),t(n,{label:"定时任务(系统)",value:"scheduled"}),t(n,{label:"定时任务(用户)",value:"user_scheduled"}),t(n,{label:"手动(批量)",value:"batch"}),t(n,{label:"手动(截图)",value:"manual_screenshot"}),t(n,{label:"手动(立即)",value:"immediate"}),t(n,{label:"手动(恢复)",value:"resumed"})]),_:1},8,["modelValue"]),t(U,{modelValue:x.value,"onUpdate:modelValue":e[3]||(e[3]=a=>x.value=a),placeholder:"用户",style:{width:"140px"},loading:v.value,filterable:"",clearable:""},{default:l(()=>[t(n,{label:"全部",value:""}),(b(!0),P(ae,null,le(T.value,a=>(b(),F(n,{key:a.id,label:a.username,value:String(a.id)},null,8,["label","value"]))),128))]),_:1},8,["modelValue","loading"]),t(W,{modelValue:k.value,"onUpdate:modelValue":e[4]||(e[4]=a=>k.value=a),placeholder:"账号关键字",style:{width:"170px"},clearable:""},null,8,["modelValue"]),t(C,{type:"primary",onClick:O},{default:l(()=>[...e[6]||(e[6]=[m("筛选",-1)])]),_:1}),t(C,{onClick:j},{default:l(()=>[...e[7]||(e[7]=[m("重置",-1)])]),_:1}),t(C,{type:"danger",plain:"",onClick:R},{default:l(()=>[...e[8]||(e[8]=[m("清理旧日志",-1)])]),_:1})])]),_:1}),t(L,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[f("div",ce,[oe((b(),F(q,{data:S.value,style:{width:"100%"}},{default:l(()=>[t(c,{prop:"created_at",label:"时间",width:"180"}),t(c,{label:"来源",width:"110"},{default:l(({row:a})=>[_(a.source).tooltip?(b(),F(E,{key:0,content:_(a.source).tooltip,placement:"top","show-after":300},{default:l(()=>[t(D,{type:_(a.source).type,effect:"light"},{default:l(()=>[m(d(_(a.source).label),1)]),_:2},1032,["type"])]),_:2},1032,["content"])):(b(),F(D,{key:1,type:_(a.source).type,effect:"light"},{default:l(()=>[m(d(_(a.source).label),1)]),_:2},1032,["type"]))]),_:1}),t(c,{prop:"user_username",label:"用户",width:"140"}),t(c,{prop:"username",label:"账号",width:"160"}),t(c,{prop:"browse_type",label:"浏览类型",width:"120"}),t(c,{label:"状态",width:"90"},{default:l(({row:a})=>[t(D,{type:I(a.status).type,effect:"light"},{default:l(()=>[m(d(I(a.status).label),1)]),_:2},1032,["type"])]),_:1}),t(c,{label:"内容/附件",width:"110"},{default:l(({row:a})=>[m(d(a.total_items)+" / "+d(a.total_attachments),1)]),_:1}),t(c,{label:"用时",width:"90"},{default:l(({row:a})=>[m(d(Y(a.duration)),1)]),_:1}),t(c,{label:"失败原因","min-width":"220"},{default:l(({row:a})=>[t(E,{content:a.error_message||"",placement:"top","show-after":300},{default:l(()=>[f("span",de,d(a.error_message||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[J,s.value]])]),f("div",pe,[t(A,{"current-page":i.value,"onUpdate:currentPage":e[5]||(e[5]=a=>i.value=a),"page-size":B,total:y.value,layout:"prev, pager, next, jumper, ->, total",onCurrentChange:M},null,8,["current-page","total"]),f("div",me,"第 "+d(i.value)+" / "+d($.value)+" 页",1)])]),_:1})])}}},Ve=Q(fe,[["__scopeId","data-v-8803eb08"]]);export{Ve as default}; +import{f as K}from"./users-DzDcz9C_.js";import{g as G,h as H}from"./tasks-B7oNpIBD.js";import{_ as Q}from"./index-DOvMEmc8.js";import{E as N,a as X}from"./vendor-element-B5S5pUKo.js";import{r as u,c as Z,o as ee,aj as r,ap as te,n as P,q as b,t as f,L as t,E as l,K as ae,a3 as le,D as F,I as m,F as oe,J as d}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";function ne(p){return String(p||"").trim()}function se(p){return!p.startsWith("user_scheduled")||!p.includes(":")?"":p.split(":",2)[1]||""}function ue(p){const s=ne(p);if(!s||s==="manual")return{group:"manual",label:"手动",type:"success",tooltip:""};if(s==="scheduled")return{group:"scheduled",label:"定时任务",type:"primary",tooltip:"系统定时"};if(s.startsWith("user_scheduled")){const i=se(s),v=String(i||"").replace(/^batch_/,"");return{group:"scheduled",label:"定时任务",type:"primary",tooltip:v?`用户定时批次:${v}`:"用户定时"}}return{group:"manual",label:"手动",type:"success",tooltip:{batch:"手动批量",manual_screenshot:"手动截图",immediate:"立即执行",resumed:"断点恢复"}[s]||s}}const re={class:"page-stack"},ie={class:"filters"},ce={class:"table-wrap"},de={class:"ellipsis"},pe={class:"pagination"},me={class:"page-hint app-muted"},B=20,fe={__name:"LogsPage",setup(p){const s=u(!1),S=u([]),y=u(0),i=u(1),v=u(!1),T=u([]),h=u(""),w=u(""),V=u(""),x=u(""),k=u(""),$=Z(()=>Math.max(1,Math.ceil((y.value||0)/B)));function Y(o){if(o==null)return"-";const e=Number(o);return Number.isFinite(e)?e<60?`${e}秒`:`${Math.floor(e/60)}分${e%60}秒`:"-"}function _(o){const e=ue(o);return{key:e.group,label:e.label,type:e.type,tooltip:e.tooltip}}function I(o){return o==="success"?{label:"成功",type:"success"}:o==="failed"?{label:"失败",type:"danger"}:{label:o||"-",type:"info"}}async function z(){v.value=!0;try{const o=await K();T.value=(o||[]).map(e=>({id:e.id,username:e.username}))}catch{T.value=[]}finally{v.value=!1}}async function M(){s.value=!0;try{const o=(i.value-1)*B,e={limit:B,offset:o};h.value&&(e.date=h.value),w.value&&(e.status=w.value),V.value&&(e.source=V.value),x.value&&(e.user_id=x.value),k.value&&(e.account=k.value);const g=await G(e);S.value=g?.logs||[],y.value=g?.total||0}catch{S.value=[],y.value=0}finally{s.value=!1}}function O(){i.value=1,M()}function j(){h.value="",w.value="",V.value="",x.value="",k.value="",i.value=1,M()}async function R(){let o;try{const e=await N.prompt("请输入要清理多少天前的日志(默认30天)","清理旧日志",{inputValue:"30",confirmButtonText:"下一步",cancelButtonText:"取消",inputValidator:g=>{const n=parseInt(String(g),10);return Number.isFinite(n)&&n>=1},inputErrorMessage:"请输入有效的天数(大于0的整数)"});o=parseInt(String(e.value),10)}catch{return}try{await N.confirm(`确定要删除 ${o} 天前的所有日志吗?此操作不可恢复!`,"二次确认",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await H(o);X.success(e?.message||"清理成功"),i.value=1,await M()}catch{}}return ee(async()=>{await z(),await M()}),(o,e)=>{const g=r("el-date-picker"),n=r("el-option"),U=r("el-select"),W=r("el-input"),C=r("el-button"),L=r("el-card"),c=r("el-table-column"),D=r("el-tag"),E=r("el-tooltip"),q=r("el-table"),A=r("el-pagination"),J=te("loading");return b(),P("div",re,[e[9]||(e[9]=f("div",{class:"app-page-title"},[f("h2",null,"任务日志")],-1)),t(L,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[f("div",ie,[t(g,{modelValue:h.value,"onUpdate:modelValue":e[0]||(e[0]=a=>h.value=a),type:"date","value-format":"YYYY-MM-DD",placeholder:"日期",style:{width:"150px"}},null,8,["modelValue"]),t(U,{modelValue:w.value,"onUpdate:modelValue":e[1]||(e[1]=a=>w.value=a),placeholder:"状态",style:{width:"120px"}},{default:l(()=>[t(n,{label:"全部",value:""}),t(n,{label:"成功",value:"success"}),t(n,{label:"失败",value:"failed"})]),_:1},8,["modelValue"]),t(U,{modelValue:V.value,"onUpdate:modelValue":e[2]||(e[2]=a=>V.value=a),placeholder:"来源",style:{width:"120px"}},{default:l(()=>[t(n,{label:"全部",value:""}),t(n,{label:"手动",value:"manual"}),t(n,{label:"定时任务(系统)",value:"scheduled"}),t(n,{label:"定时任务(用户)",value:"user_scheduled"}),t(n,{label:"手动(批量)",value:"batch"}),t(n,{label:"手动(截图)",value:"manual_screenshot"}),t(n,{label:"手动(立即)",value:"immediate"}),t(n,{label:"手动(恢复)",value:"resumed"})]),_:1},8,["modelValue"]),t(U,{modelValue:x.value,"onUpdate:modelValue":e[3]||(e[3]=a=>x.value=a),placeholder:"用户",style:{width:"140px"},loading:v.value,filterable:"",clearable:""},{default:l(()=>[t(n,{label:"全部",value:""}),(b(!0),P(ae,null,le(T.value,a=>(b(),F(n,{key:a.id,label:a.username,value:String(a.id)},null,8,["label","value"]))),128))]),_:1},8,["modelValue","loading"]),t(W,{modelValue:k.value,"onUpdate:modelValue":e[4]||(e[4]=a=>k.value=a),placeholder:"账号关键字",style:{width:"170px"},clearable:""},null,8,["modelValue"]),t(C,{type:"primary",onClick:O},{default:l(()=>[...e[6]||(e[6]=[m("筛选",-1)])]),_:1}),t(C,{onClick:j},{default:l(()=>[...e[7]||(e[7]=[m("重置",-1)])]),_:1}),t(C,{type:"danger",plain:"",onClick:R},{default:l(()=>[...e[8]||(e[8]=[m("清理旧日志",-1)])]),_:1})])]),_:1}),t(L,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[f("div",ce,[oe((b(),F(q,{data:S.value,style:{width:"100%"}},{default:l(()=>[t(c,{prop:"created_at",label:"时间",width:"180"}),t(c,{label:"来源",width:"110"},{default:l(({row:a})=>[_(a.source).tooltip?(b(),F(E,{key:0,content:_(a.source).tooltip,placement:"top","show-after":300},{default:l(()=>[t(D,{type:_(a.source).type,effect:"light"},{default:l(()=>[m(d(_(a.source).label),1)]),_:2},1032,["type"])]),_:2},1032,["content"])):(b(),F(D,{key:1,type:_(a.source).type,effect:"light"},{default:l(()=>[m(d(_(a.source).label),1)]),_:2},1032,["type"]))]),_:1}),t(c,{prop:"user_username",label:"用户",width:"140"}),t(c,{prop:"username",label:"账号",width:"160"}),t(c,{prop:"browse_type",label:"浏览类型",width:"120"}),t(c,{label:"状态",width:"90"},{default:l(({row:a})=>[t(D,{type:I(a.status).type,effect:"light"},{default:l(()=>[m(d(I(a.status).label),1)]),_:2},1032,["type"])]),_:1}),t(c,{label:"内容/附件",width:"110"},{default:l(({row:a})=>[m(d(a.total_items)+" / "+d(a.total_attachments),1)]),_:1}),t(c,{label:"用时",width:"90"},{default:l(({row:a})=>[m(d(Y(a.duration)),1)]),_:1}),t(c,{label:"失败原因","min-width":"220"},{default:l(({row:a})=>[t(E,{content:a.error_message||"",placement:"top","show-after":300},{default:l(()=>[f("span",de,d(a.error_message||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[J,s.value]])]),f("div",pe,[t(A,{"current-page":i.value,"onUpdate:currentPage":e[5]||(e[5]=a=>i.value=a),"page-size":B,total:y.value,layout:"prev, pager, next, jumper, ->, total",onCurrentChange:M},null,8,["current-page","total"]),f("div",me,"第 "+d(i.value)+" / "+d($.value)+" 页",1)])]),_:1})])}}},Ve=Q(fe,[["__scopeId","data-v-2627b2c9"]]);export{Ve as default}; diff --git a/static/admin/assets/LogsPage-D1bozCEo.css b/static/admin/assets/LogsPage-D1bozCEo.css deleted file mode 100644 index 683d739..0000000 --- a/static/admin/assets/LogsPage-D1bozCEo.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-8803eb08]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-8803eb08]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.filters[data-v-8803eb08]{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.table-wrap[data-v-8803eb08]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-8803eb08]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pagination[data-v-8803eb08]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-8803eb08]{font-size:12px} diff --git a/static/admin/assets/MetricGrid-yP_dkP6X.css b/static/admin/assets/MetricGrid-BR486o_b.css similarity index 50% rename from static/admin/assets/MetricGrid-yP_dkP6X.css rename to static/admin/assets/MetricGrid-BR486o_b.css index e14db4a..2041328 100644 --- a/static/admin/assets/MetricGrid-yP_dkP6X.css +++ b/static/admin/assets/MetricGrid-BR486o_b.css @@ -1 +1 @@ -.metric-grid[data-v-00e217d4]{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--metric-min),1fr));gap:12px}.metric-card[data-v-00e217d4]{position:relative;overflow:hidden;border-radius:14px;border:1px solid var(--app-border);background:linear-gradient(180deg,#fffffffa,#fafcffe6);box-shadow:var(--app-shadow-soft);padding:13px 14px;min-height:104px}.metric-card[data-v-00e217d4]:before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--metric-top, #3b82f6)}.metric-top[data-v-00e217d4]{display:flex;align-items:center;gap:8px}.metric-icon[data-v-00e217d4]{width:26px;height:26px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--metric-icon-bg, rgba(59, 130, 246, .12));color:var(--metric-icon-color, #1d4ed8)}.metric-label[data-v-00e217d4]{font-size:12px;color:#475569;font-weight:700;line-height:1.4}.metric-value[data-v-00e217d4]{margin-top:10px;font-size:26px;line-height:1.05;font-weight:900;color:#0f172a}.metric-hint[data-v-00e217d4]{margin-top:8px;font-size:12px;line-height:1.4}.metric-tone--blue[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #3b82f6, #06b6d4);--metric-icon-bg: rgba(59, 130, 246, .14);--metric-icon-color: #1d4ed8}.metric-tone--green[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #10b981, #22c55e);--metric-icon-bg: rgba(16, 185, 129, .14);--metric-icon-color: #047857}.metric-tone--purple[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #8b5cf6, #ec4899);--metric-icon-bg: rgba(139, 92, 246, .14);--metric-icon-color: #6d28d9}.metric-tone--orange[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #f59e0b, #f97316);--metric-icon-bg: rgba(245, 158, 11, .14);--metric-icon-color: #b45309}.metric-tone--red[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #ef4444, #f43f5e);--metric-icon-bg: rgba(239, 68, 68, .14);--metric-icon-color: #b91c1c}.metric-tone--cyan[data-v-00e217d4]{--metric-top: linear-gradient(90deg, #06b6d4, #3b82f6);--metric-icon-bg: rgba(6, 182, 212, .14);--metric-icon-color: #0e7490}@media(max-width:768px){.metric-grid[data-v-00e217d4]{grid-template-columns:repeat(2,minmax(0,1fr))}.metric-card[data-v-00e217d4]{min-height:96px}.metric-value[data-v-00e217d4]{font-size:22px}}@media(max-width:480px){.metric-grid[data-v-00e217d4]{grid-template-columns:1fr}} +.metric-grid[data-v-28727c73]{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--metric-min),1fr));gap:12px}.metric-card[data-v-28727c73]{position:relative;overflow:hidden;border-radius:14px;border:1px solid var(--app-border);background:linear-gradient(180deg,#fffffffa,#fafcffe6);box-shadow:var(--app-shadow-soft);padding:13px 14px;min-height:104px}.metric-card[data-v-28727c73]:before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--metric-top, #3b82f6)}.metric-top[data-v-28727c73]{display:flex;align-items:center;gap:8px}.metric-icon[data-v-28727c73]{width:26px;height:26px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--metric-icon-bg, rgba(59, 130, 246, .12));color:var(--metric-icon-color, #1d4ed8)}.metric-label[data-v-28727c73]{font-size:12px;color:#475569;font-weight:700;line-height:1.4}.metric-value[data-v-28727c73]{margin-top:10px;font-size:26px;line-height:1.05;font-weight:900;color:#0f172a}.metric-hint[data-v-28727c73]{margin-top:8px;font-size:12px;line-height:1.4}.metric-tone--blue[data-v-28727c73]{--metric-top: linear-gradient(90deg, #3b82f6, #06b6d4);--metric-icon-bg: rgba(59, 130, 246, .14);--metric-icon-color: #1d4ed8}.metric-tone--green[data-v-28727c73]{--metric-top: linear-gradient(90deg, #10b981, #22c55e);--metric-icon-bg: rgba(16, 185, 129, .14);--metric-icon-color: #047857}.metric-tone--purple[data-v-28727c73]{--metric-top: linear-gradient(90deg, #8b5cf6, #ec4899);--metric-icon-bg: rgba(139, 92, 246, .14);--metric-icon-color: #6d28d9}.metric-tone--orange[data-v-28727c73]{--metric-top: linear-gradient(90deg, #f59e0b, #f97316);--metric-icon-bg: rgba(245, 158, 11, .14);--metric-icon-color: #b45309}.metric-tone--red[data-v-28727c73]{--metric-top: linear-gradient(90deg, #ef4444, #f43f5e);--metric-icon-bg: rgba(239, 68, 68, .14);--metric-icon-color: #b91c1c}.metric-tone--cyan[data-v-28727c73]{--metric-top: linear-gradient(90deg, #06b6d4, #3b82f6);--metric-icon-bg: rgba(6, 182, 212, .14);--metric-icon-color: #0e7490}@media(max-width:768px){.metric-grid[data-v-28727c73]{grid-template-columns:repeat(2,minmax(0,1fr))}.metric-card[data-v-28727c73]{min-height:96px}.metric-value[data-v-28727c73]{font-size:22px}}@media(max-width:480px){.metric-grid[data-v-28727c73]{grid-template-columns:1fr}} diff --git a/static/admin/assets/MetricGrid-BnihYB_8.js b/static/admin/assets/MetricGrid-C3Xjc9mZ.js similarity index 91% rename from static/admin/assets/MetricGrid-BnihYB_8.js rename to static/admin/assets/MetricGrid-C3Xjc9mZ.js index 4412335..dff9f12 100644 --- a/static/admin/assets/MetricGrid-BnihYB_8.js +++ b/static/admin/assets/MetricGrid-C3Xjc9mZ.js @@ -1 +1 @@ -import{_}from"./index-C1f9ticl.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-00e217d4"]]);export{w as M}; +import{_}from"./index-DOvMEmc8.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-28727c73"]]);export{w as M}; diff --git a/static/admin/assets/ReportPage-BCQBCnjY.css b/static/admin/assets/ReportPage-BCQBCnjY.css deleted file mode 100644 index 9b02a30..0000000 --- a/static/admin/assets/ReportPage-BCQBCnjY.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-d610e788]{display:flex;flex-direction:column;gap:14px}.report-hero[data-v-d610e788]{position:relative;overflow:hidden;border-radius:18px;border:1px solid rgba(17,24,39,.1);background:radial-gradient(circle at 10% 10%,rgba(59,130,246,.18),transparent 48%),radial-gradient(circle at 80% 0%,rgba(236,72,153,.16),transparent 45%),radial-gradient(circle at 90% 90%,rgba(16,185,129,.14),transparent 42%),#ffffffb8;box-shadow:0 14px 40px #0f172a14;-webkit-backdrop-filter:saturate(180%) blur(12px);backdrop-filter:saturate(180%) blur(12px);padding:16px}.hero-head[data-v-d610e788]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}.hero-main h2[data-v-d610e788]{margin:0;font-size:19px;font-weight:900;letter-spacing:.2px}.hero-meta[data-v-d610e788]{margin-top:6px;font-size:12px;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.hero-dot[data-v-d610e788]{opacity:.65}.hero-actions[data-v-d610e788]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.mobile-report[data-v-d610e788]{display:grid;gap:12px;grid-template-columns:repeat(3,minmax(0,1fr))}.mobile-module-card[data-v-d610e788]{position:relative;overflow:hidden;border-radius:14px;border:1px solid rgba(17,24,39,.12);background:#ffffffe6;box-shadow:var(--app-shadow-soft)}.mobile-module-card[data-v-d610e788]:before{content:"";position:absolute;left:0;top:0;right:0;height:3px;background:var(--mobile-accent, #3b82f6)}.mobile-tone-blue[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #3b82f6, #06b6d4)}.mobile-tone-cyan[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #06b6d4, #3b82f6)}.mobile-tone-purple[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #8b5cf6, #ec4899)}.mobile-tone-orange[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #f59e0b, #f97316)}.mobile-tone-green[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #10b981, #22c55e)}.mobile-tone-red[data-v-d610e788]{--mobile-accent: linear-gradient(90deg, #ef4444, #f43f5e)}.mobile-module-head[data-v-d610e788]{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.mobile-module-title[data-v-d610e788]{font-size:13px;font-weight:900;color:#0f172a}.mobile-module-desc[data-v-d610e788]{min-width:0;max-width:68%;font-size:11px;line-height:1.4;text-align:right}.mobile-metrics[data-v-d610e788]{margin-top:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mobile-metric-item[data-v-d610e788]{padding:8px 10px;border-radius:10px;border:1px solid rgba(17,24,39,.08);background:#f8fafce6}.mobile-metric-label[data-v-d610e788]{font-size:11px;line-height:1.35}.mobile-metric-value[data-v-d610e788]{margin-top:4px;font-size:14px;font-weight:800;color:#0f172a;line-height:1.3;word-break:break-word}.module-extra-actions[data-v-d610e788]{margin-top:10px;display:flex;justify-content:flex-end}.request-dialog-summary[data-v-d610e788]{display:flex;flex-wrap:wrap;gap:10px 16px;font-size:12px;margin-bottom:12px}.request-dialog-block+.request-dialog-block[data-v-d610e788]{margin-top:14px}.request-dialog-title[data-v-d610e788]{font-size:13px;font-weight:800;margin-bottom:8px}.hero-overview-grid[data-v-d610e788]{display:none}.panel[data-v-d610e788]{border-radius:18px;border:1px solid rgba(17,24,39,.1);background:#ffffffb8;box-shadow:var(--app-shadow);-webkit-backdrop-filter:saturate(180%) blur(10px);backdrop-filter:saturate(180%) blur(10px)}.panel-head[data-v-d610e788]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}.head-left[data-v-d610e788]{display:flex;align-items:center;gap:12px;min-width:0}.head-text[data-v-d610e788]{min-width:0}.head-icon[data-v-d610e788]{width:40px;height:40px;border-radius:16px;display:flex;align-items:center;justify-content:center;border:1px solid rgba(17,24,39,.08);flex:0 0 auto}.tone-blue[data-v-d610e788]{background:#3b82f61f;color:#1d4ed8}.tone-cyan[data-v-d610e788]{background:#22d3ee1f;color:#0369a1}.tone-purple[data-v-d610e788]{background:#8b5cf61f;color:#6d28d9}.tone-orange[data-v-d610e788]{background:#f59e0b1f;color:#b45309}.tone-green[data-v-d610e788]{background:#10b9811f;color:#047857}.tone-red[data-v-d610e788]{background:#ef44441f;color:#b91c1c}.panel-title[data-v-d610e788]{font-size:14px;font-weight:900}.panel-sub[data-v-d610e788]{margin-top:4px;font-size:12px;color:var(--app-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.metrics-block[data-v-d610e788]{display:flex;flex-direction:column;gap:10px}.block-title[data-v-d610e788]{font-size:13px;font-weight:900;letter-spacing:.2px}.divider[data-v-d610e788]{height:1px;background:linear-gradient(90deg,transparent,rgba(17,24,39,.12),transparent);margin:14px 0}.queue-tabs[data-v-d610e788] .el-tabs__header{margin:0 0 10px}.tab-label[data-v-d610e788]{display:inline-flex;align-items:center;gap:6px}.table-wrap[data-v-d610e788]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.help[data-v-d610e788]{margin-top:10px;font-size:12px}.resource-grid[data-v-d610e788]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-item[data-v-d610e788]{border:1px solid rgba(17,24,39,.08);border-radius:16px;padding:12px;background:#ffffffb3}.resource-k[data-v-d610e788]{font-size:12px;margin-bottom:8px}.resource-sub[data-v-d610e788]{margin-top:8px;font-size:12px}.config-grid[data-v-d610e788]{display:grid;grid-template-columns:1fr;gap:10px}.config-item[data-v-d610e788]{border:1px solid rgba(17,24,39,.08);border-radius:16px;padding:12px;background:#ffffffb3}.config-k[data-v-d610e788]{font-size:12px}.config-v[data-v-d610e788]{margin-top:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.config-inline[data-v-d610e788]{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.config-split[data-v-d610e788]{opacity:.65}.config-sub[data-v-d610e788]{margin-top:8px;font-size:12px}.err[data-v-d610e788]{color:#b91c1c}[data-v-d610e788] .el-table{--el-table-border-color: rgba(17, 24, 39, .08);--el-table-header-bg-color: rgba(246, 247, 251, .8)}[data-v-d610e788] .el-table th.el-table__cell{background:#f6f7fbcc}@media(max-width:1200px){.mobile-report[data-v-d610e788]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:768px){.mobile-report[data-v-d610e788]{grid-template-columns:1fr;gap:10px}.report-hero[data-v-d610e788]{border-radius:14px;padding:12px}.hero-main h2[data-v-d610e788]{font-size:17px}.hero-meta[data-v-d610e788]{margin-top:4px;gap:6px;font-size:11px}.resource-grid[data-v-d610e788]{grid-template-columns:1fr}}@media(max-width:420px){.mobile-metrics[data-v-d610e788]{grid-template-columns:1fr}.mobile-module-desc[data-v-d610e788]{max-width:62%}} diff --git a/static/admin/assets/ReportPage-fzVH-d9u.js b/static/admin/assets/ReportPage-BMEJM5Hr.js similarity index 98% rename from static/admin/assets/ReportPage-fzVH-d9u.js rename to static/admin/assets/ReportPage-BMEJM5Hr.js index d39b07c..978accc 100644 --- a/static/admin/assets/ReportPage-fzVH-d9u.js +++ b/static/admin/assets/ReportPage-BMEJM5Hr.js @@ -1 +1 @@ -import{f as tl,g as al,h as sl,k as ul,j as ol,n as nl,o as rl}from"./vendor-element-B5S5pUKo.js";import{c as il,a as cl,_ as vl,f as dl}from"./index-C1f9ticl.js";import{f as _l}from"./email-px7YBG2O.js";import{f as ml,a as pl,b as bl,c as fl,d as hl,e as wl}from"./tasks-Bep0SUyu.js";import{f as gl}from"./system-ZDPnxnIu.js";import{M as kl}from"./MetricGrid-BnihYB_8.js";import{i as de,r as m,c as a,o as yl,O as Sl,aj as R,n as S,q as k,t as s,L as u,G as D,J as d,K as _e,a3 as me,E as w,I as pe,D as ql,y as xl}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-misc-BeoNyvBp.js";import"./vendor-axios-B9ygI19o.js";const $l=il(async()=>{const{data:P}=await cl.get("/browser_pool/stats");return P},4e3);async function Tl(P={}){return $l.run(P)}const Ll={class:"page-stack"},Rl={class:"report-hero"},Ml={class:"hero-head"},Al={class:"hero-main"},Pl={class:"hero-meta app-muted"},Cl={key:0},Nl={key:1,class:"hero-dot"},Il={key:2},Dl={class:"mobile-report"},El={class:"mobile-module-head"},Wl={class:"mobile-module-title"},Ql={class:"mobile-module-desc app-muted"},Vl={class:"mobile-metrics"},zl={class:"mobile-metric-label app-muted"},Bl={class:"mobile-metric-value"},Ol={key:0,class:"module-extra-actions"},Ul={class:"request-dialog-summary app-muted"},Fl={class:"request-dialog-block"},Gl={class:"table-wrap"},jl={class:"request-dialog-block"},Hl={class:"table-wrap"},Zl={class:"request-dialog-summary app-muted"},Yl={class:"request-dialog-block"},Jl={class:"table-wrap"},Kl={class:"request-dialog-block"},Xl={class:"table-wrap"},et=3,lt=5e3,tt=2e4,at={__name:"ReportPage",setup(P){const be=de("refreshStats",null),fe=de("adminStats",null),E=m(!1),W=m(!1),T=m(""),Q=m(null),b=m(null),p=m(null),y=m(null),g=m(null),C=m(null),L=m(null),f=m(null),v=m(null),r=m(null),V=m(!1),z=m(!1);m("running");function he(){try{T.value=new Date().toLocaleString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{T.value=""}}function l(t){const e=Number(t);return Number.isFinite(e)?e:0}function we(t){const e=String(t??"").trim();if(!e)return 0;const o=e.endsWith("%")?e.slice(0,-1):e,_=Number(o);return!Number.isFinite(_)||_<0?0:_>1e3?1e3:_}function B(t){return`${Math.round(we(t))}%`}function c(t){const e=Number(t);return!Number.isFinite(e)||e<0?"-":e>=100?`${Math.round(e)}ms`:`${e.toFixed(1)}ms`}function Z(t){const e=Number(t);if(!Number.isFinite(e)||e<=0)return"-";try{return new Date(e*1e3).toLocaleTimeString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{return"-"}}function O(t){const e=Number(t);if(!Number.isFinite(e)||e<=0)return"-";try{return new Date(e*1e3).toLocaleString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{return"-"}}function ge(t){const e=String(t??"").trim();return!e||e==="manual"?"手动":e==="scheduled"?"系统定时":e==="batch"?"批量执行":e==="resumed"?"断点续跑":e.startsWith("user_scheduled:")?"用户定时":e}const Y=a(()=>{const t=fe?.value||{},e=l(b.value?.max_concurrent);return[{label:"总用户数",value:l(t.total_users),icon:tl,tone:"blue"},{label:"今日注册",value:l(t.new_users_today),icon:al,tone:"green"},{label:"近7天注册",value:l(t.new_users_7d),icon:sl,tone:"purple"},{label:"总账号数",value:l(t.total_accounts),icon:ul,tone:"cyan"},{label:"VIP用户",value:l(t.vip_users),icon:ol,tone:"orange"},{label:"运行中任务",value:l(b.value?.running_count),icon:nl,tone:"green",sub:e?`并发上限 ${e}`:""},{label:"排队任务",value:l(b.value?.queuing_count),icon:rl,tone:"purple"}]}),h=a(()=>Q.value?.today||{}),q=a(()=>Q.value?.total||{});a(()=>b.value?.running||[]);const N=a(()=>b.value?.queuing||[]),ke=a(()=>l(b.value?.running_count)),ye=a(()=>l(b.value?.queuing_count)),Se=a(()=>{const t=L.value?.workers;return Array.isArray(t)?[...t].sort((e,o)=>l(e?.worker_id)-l(o?.worker_id)):[]}),J=a(()=>l(L.value?.total_workers)),U=a(()=>Se.value.filter(t=>!!t?.has_browser).length),K=a(()=>l(L.value?.idle_workers)),X=a(()=>l(L.value?.queue_size)),F=a(()=>l(L.value?.active_workers)),ee=a(()=>{const t=l(h.value.success_tasks),e=l(h.value.failed_tasks),o=t+e;return o>0?Math.round(t/o*1e3)/10:0}),G=a(()=>l(p.value?.success_rate));a(()=>[{label:"总任务",value:l(h.value.total_tasks),tone:"blue"},{label:"成功",value:l(h.value.success_tasks),tone:"green"},{label:"失败",value:l(h.value.failed_tasks),tone:"red"},{label:"浏览内容",value:l(h.value.total_items),tone:"purple"},{label:"查看附件",value:l(h.value.total_attachments),tone:"cyan"}]),a(()=>[{label:"总任务",value:l(q.value.total_tasks),tone:"blue"},{label:"成功",value:l(q.value.success_tasks),tone:"green"},{label:"失败",value:l(q.value.failed_tasks),tone:"red"},{label:"浏览内容",value:l(q.value.total_items),tone:"purple"},{label:"查看附件",value:l(q.value.total_attachments),tone:"cyan"}]),a(()=>[{label:"总发送",value:l(p.value?.total_sent),tone:"blue"},{label:"成功",value:l(p.value?.total_success),tone:"green"},{label:"失败",value:l(p.value?.total_failed),tone:"red"},{label:"成功率",value:`${G.value}%`,tone:"purple"}]),a(()=>[{label:"注册验证",value:l(p.value?.register_sent),tone:"cyan"},{label:"密码重置",value:l(p.value?.reset_sent),tone:"orange"},{label:"邮箱绑定",value:l(p.value?.bind_sent),tone:"purple"},{label:"任务完成",value:l(p.value?.task_complete_sent),tone:"green"}]),a(()=>[{label:"总反馈",value:l(y.value?.total),tone:"blue"},{label:"待处理",value:l(y.value?.pending),tone:"orange"},{label:"已回复",value:l(y.value?.replied),tone:"green"}]),a(()=>[{label:"总 Worker",value:J.value,tone:"blue"},{label:"活跃 Worker",value:U.value,tone:"green"},{label:"空闲 Worker",value:K.value,tone:"cyan"},{label:"忙碌 Worker",value:F.value,tone:"orange"},{label:"队列",value:X.value,tone:"purple"}]);const qe=a(()=>(f.value?.schedule_enabled??0)===1),xe=a(()=>f.value?.schedule_time||"-"),$e=a(()=>f.value?.schedule_browse_type||"-"),Te=a(()=>String(f.value?.schedule_weekdays||"").trim());a(()=>{const t=Te.value;if(!t)return"";const e={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},o=t.split(",").map(_=>_.trim()).filter(Boolean);return o.length?o.map(_=>e[Number(_)]||_).join("、"):t});const Le=a(()=>(f.value?.proxy_enabled??0)===1);a(()=>f.value?.proxy_api_url||"");const le=a(()=>l(f.value?.proxy_expire_minutes)),j=a(()=>l(f.value?.max_concurrent_global)),Re=a(()=>l(f.value?.max_concurrent_per_account)),Me=a(()=>l(f.value?.max_screenshot_concurrent)),Ae=a(()=>{const t=l(b.value?.running_count),e=l(b.value?.queuing_count),o=l(b.value?.max_concurrent);return`运行中 ${t} / 排队 ${e} / 并发上限 ${o||j.value||"-"}`}),Pe=a(()=>Y.value.map(t=>({label:t.label,value:t.sub?`${t.value}(${t.sub})`:t.value}))),Ce=a(()=>[{label:"今日总任务",value:l(h.value.total_tasks)},{label:"今日成功",value:l(h.value.success_tasks)},{label:"今日失败",value:l(h.value.failed_tasks)},{label:"今日成功率",value:`${ee.value}%`},{label:"累计任务",value:l(q.value.total_tasks)},{label:"累计成功",value:l(q.value.success_tasks)}]),Ne=a(()=>[{label:"运行中",value:ke.value},{label:"排队中",value:ye.value},{label:"并发上限",value:l(b.value?.max_concurrent)||j.value||"-"},{label:"排队首条来源",value:ge(N.value[0]?.source)},{label:"排队首条状态",value:N.value[0]?.detail_status||N.value[0]?.status||"-"},{label:"最长等待",value:N.value[0]?.elapsed_display||"-"}]),Ie=a(()=>[{label:"总发送",value:l(p.value?.total_sent)},{label:"成功",value:l(p.value?.total_success)},{label:"失败",value:l(p.value?.total_failed)},{label:"成功率",value:`${G.value}%`},{label:"注册验证",value:l(p.value?.register_sent)},{label:"重置密码",value:l(p.value?.reset_sent)}]),De=a(()=>[{label:"总反馈",value:l(y.value?.total)},{label:"待处理",value:l(y.value?.pending)},{label:"已回复",value:l(y.value?.replied)}]),Ee=a(()=>[{label:"CPU",value:B(g.value?.cpu_percent)},{label:"内存",value:B(g.value?.memory_percent)},{label:"磁盘",value:B(g.value?.disk_percent)},{label:"容器状态",value:C.value?.status||"-"},{label:"容器名",value:C.value?.container_name||"-"},{label:"容器运行",value:C.value?.uptime||"-"}]),We=a(()=>[{label:"总 Worker",value:J.value},{label:"活跃 Worker",value:U.value},{label:"忙碌 Worker",value:F.value},{label:"空闲 Worker",value:K.value},{label:"任务队列",value:X.value}]),Qe=a(()=>{const t=v.value?.top_paths;return Array.isArray(t)?t.slice(0,3):[]}),Ve=a(()=>{const t=[{label:"总请求",value:l(v.value?.total_requests)},{label:"API请求",value:l(v.value?.api_requests)},{label:"慢请求",value:l(v.value?.slow_requests)},{label:"错误请求",value:l(v.value?.error_requests)}];return Qe.value.forEach((e,o)=>{const _=String(e?.path||"-");t.push({label:`慢接口${o+1}`,value:`${_} · 峰值 ${c(e?.max_ms)}`})}),t}),ze=a(()=>{const t=c(v.value?.avg_duration_ms),e=c(v.value?.max_duration_ms),o=Z(v.value?.last_request_ts),_=c(v.value?.slow_threshold_ms);return`均值 ${t} · 峰值 ${e} · 慢阈 ${_} · 最近 ${o}`}),Be=a(()=>(Array.isArray(r.value?.top_sql)?r.value.top_sql:[]).slice(0,3)),te=a(()=>{const t=l(r.value?.window_seconds);return t<=0?24:Math.max(1,Math.round(t/3600))}),Oe=a(()=>{const t=[{label:`慢SQL(${te.value}h)`,value:l(r.value?.total_slow_queries)},{label:"去重SQL",value:l(r.value?.unique_sql)},{label:"平均耗时",value:c(r.value?.avg_duration_ms)},{label:"峰值耗时",value:c(r.value?.max_duration_ms)}];return Be.value.forEach((e,o)=>{t.push({label:`慢SQL${o+1}`,value:`${c(e?.max_ms)} · ${String(e?.sql||"-")}`})}),t}),Ue=a(()=>{const t=c(r.value?.slow_threshold_ms),e=Z(r.value?.last_slow_ts);return`窗口 ${te.value}h · 慢阈 ${t} · 最近 ${e}`}),Fe=a(()=>(Array.isArray(r.value?.top_sql)?r.value.top_sql:[]).map((e,o)=>({rank:o+1,sql:String(e?.sql||"-"),count:l(e?.count),avg_ms:c(e?.avg_ms),max_ms:c(e?.max_ms),last_seen:O(e?.last_ts),sample_params:String(e?.sample_params||"-")}))),Ge=a(()=>[...Array.isArray(r.value?.recent_slow_sql)?r.value.recent_slow_sql:[]].sort((e,o)=>Number(o?.time||0)-Number(e?.time||0)).map(e=>({time_text:O(e?.time),sql:String(e?.sql||"-"),duration_ms:c(e?.duration_ms),params:String(e?.params||"-")}))),je=a(()=>(Array.isArray(v.value?.top_paths)?v.value.top_paths:[]).map((e,o)=>({rank:o+1,path:String(e?.path||"-"),count:l(e?.count),avg_ms:c(e?.avg_ms),max_ms:c(e?.max_ms),status_5xx:l(e?.status_5xx)}))),He=a(()=>[...Array.isArray(v.value?.recent_slow)?v.value.recent_slow:[]].sort((e,o)=>Number(o?.time||0)-Number(e?.time||0)).map(e=>({time_text:O(e?.time),method:String(e?.method||"-").toUpperCase(),path:String(e?.path||"-"),status:l(e?.status),duration_ms:c(e?.duration_ms)})));function Ze(t){const e=l(t);return e>=500?"danger":e>=400?"warning":e>=300?"info":"success"}function Ye(){V.value=!0}function Je(){z.value=!0}const ae=a(()=>{const t=r.value?.slow_threshold_ms;if(t!=null)return c(t);const e=f.value?.db_slow_query_ms;return e!=null?c(e):"-"}),Ke=a(()=>[{label:"定时任务",value:qe.value?"启用":"关闭"},{label:"执行时间",value:xe.value||"-"},{label:"浏览类型",value:$e.value||"-"},{label:"代理",value:Le.value?"启用":"关闭"},{label:"代理有效期",value:le.value?`${le.value} 分钟`:"-"},{label:"全局并发",value:j.value||"-"},{label:"单账号并发",value:Re.value||"-"},{label:"截图并发",value:Me.value||"-"},{label:"慢SQL阈值",value:ae.value}]),Xe=a(()=>[{key:"overview",title:"平台概览",desc:T.value?`更新 ${T.value}`:"核心指标",tone:"blue",items:Pe.value},{key:"task",title:"任务概览",desc:l(h.value.total_tasks)>0?`今日成功率 ${ee.value}%`:"今日暂无任务",tone:"purple",items:Ce.value},{key:"queue",title:"队列监控",desc:Ae.value,tone:"blue",items:Ne.value},{key:"email",title:"邮件报表",desc:`成功率 ${G.value}%`,tone:"cyan",items:Ie.value},{key:"feedback",title:"反馈概览",desc:`待处理 ${l(y.value?.pending)} 条`,tone:"orange",items:De.value},{key:"resource",title:"系统资源",desc:g.value?.uptime?`运行 ${g.value.uptime}`:"运行状态获取中",tone:"green",items:Ee.value},{key:"request",title:"接口性能",desc:ze.value,tone:"purple",items:Ve.value},{key:"slow_sql",title:"慢SQL监控",desc:Ue.value,tone:"red",items:Oe.value},{key:"worker",title:"截图线程池",desc:`活跃 ${U.value} · 忙碌 ${F.value}`,tone:"cyan",items:We.value},{key:"config",title:"配置概览",desc:"并发 / 代理 / 定时任务",tone:"red",items:Ke.value}]);let se=1;async function ue(t={}){const e=t.showLoading??!0;if(W.value)return;const o=!!t.forceStatsSync,_=o||se%et===0;se+=1,W.value=!0,e&&(E.value=!0);try{const[n,x,I,A,i,$,re,ie,ce,ve]=await Promise.allSettled([ml(),pl(),_l(),dl(),bl(),fl(),Tl(),hl(),wl(),gl()]);n.status==="fulfilled"&&(Q.value=n.value),x.status==="fulfilled"&&(b.value=x.value),I.status==="fulfilled"&&(p.value=I.value),A.status==="fulfilled"&&(y.value=A.value),i.status==="fulfilled"&&(g.value=i.value),$.status==="fulfilled"&&(C.value=$.value),re.status==="fulfilled"&&(L.value=re.value),ie.status==="fulfilled"&&(v.value=ie.value),ce.status==="fulfilled"&&(r.value=ce.value),ve.status==="fulfilled"&&(f.value=ve.value),_&&await be?.({force:o}),he()}finally{W.value=!1,e&&(E.value=!1)}}let M=null;function el(){return typeof document>"u"?!1:document.visibilityState==="hidden"}function ll(){return el()?tt:lt}function oe(){M&&(clearTimeout(M),M=null)}function H(){oe(),M=window.setTimeout(async()=>{M=null,await ue({showLoading:!1}).catch(()=>{}),H()},ll())}function ne(){H()}return yl(()=>{ue({showLoading:!1}).catch(()=>{}).finally(()=>{H()}),window.addEventListener("visibilitychange",ne)}),Sl(()=>{oe(),window.removeEventListener("visibilitychange",ne)}),(t,e)=>{const o=R("el-button"),_=R("el-card"),n=R("el-table-column"),x=R("el-table"),I=R("el-tag"),A=R("el-dialog");return k(),S("div",Ll,[s("section",Rl,[s("div",Ml,[s("div",Al,[e[3]||(e[3]=s("h2",null,"报表中心",-1)),s("div",Pl,[T.value?(k(),S("span",Cl,"更新时间:"+d(T.value),1)):D("",!0),e[2]||(e[2]=s("span",{class:"hero-dot"},"·",-1)),s("span",null,"慢SQL阈值 "+d(ae.value),1),g.value?.uptime?(k(),S("span",Nl,"·")):D("",!0),g.value?.uptime?(k(),S("span",Il,"运行 "+d(g.value.uptime),1)):D("",!0)])])]),u(kl,{class:"hero-overview-grid",items:Y.value,loading:E.value,"min-width":165},null,8,["items","loading"])]),s("section",Dl,[(k(!0),S(_e,null,me(Xe.value,i=>(k(),ql(_,{key:i.key,shadow:"never",class:xl(["mobile-module-card",`mobile-tone-${i.tone}`]),"body-style":{padding:"12px"}},{default:w(()=>[s("div",El,[s("div",Wl,d(i.title),1),s("div",Ql,d(i.desc),1)]),s("div",Vl,[(k(!0),S(_e,null,me(i.items,$=>(k(),S("div",{key:`${i.key}-${$.label}`,class:"mobile-metric-item"},[s("div",zl,d($.label),1),s("div",Bl,d($.value),1)]))),128))]),i.key==="request"||i.key==="slow_sql"?(k(),S("div",Ol,[u(o,{size:"small",type:"primary",plain:"",onClick:$=>i.key==="request"?Ye():Je()},{default:w(()=>[pe(d(i.key==="request"?"查看慢接口详情":"查看慢SQL详情"),1)]),_:2},1032,["onClick"])])):D("",!0)]),_:2},1032,["class"]))),128))]),u(A,{modelValue:V.value,"onUpdate:modelValue":e[0]||(e[0]=i=>V.value=i),title:"慢接口详情",width:"min(1080px, 96vw)"},{default:w(()=>[s("div",Ul,[s("span",null,"总请求:"+d(l(v.value?.total_requests)),1),s("span",null,"API请求:"+d(l(v.value?.api_requests)),1),s("span",null,"慢请求:"+d(l(v.value?.slow_requests)),1),s("span",null,"错误请求:"+d(l(v.value?.error_requests)),1)]),s("div",Fl,[e[4]||(e[4]=s("div",{class:"request-dialog-title"},"慢接口排行榜",-1)),s("div",Gl,[u(x,{data:je.value,size:"small","max-height":"280"},{default:w(()=>[u(n,{prop:"rank",label:"#",width:"60"}),u(n,{prop:"path",label:"接口路径","min-width":"340","show-overflow-tooltip":""}),u(n,{prop:"count",label:"请求数",width:"100"}),u(n,{prop:"avg_ms",label:"平均耗时",width:"120"}),u(n,{prop:"max_ms",label:"峰值耗时",width:"120"}),u(n,{prop:"status_5xx",label:"5xx",width:"90"})]),_:1},8,["data"])])]),s("div",jl,[e[5]||(e[5]=s("div",{class:"request-dialog-title"},"最近慢请求",-1)),s("div",Hl,[u(x,{data:He.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"time_text",label:"时间",width:"180"}),u(n,{prop:"method",label:"方法",width:"90"}),u(n,{prop:"path",label:"接口路径","min-width":"320","show-overflow-tooltip":""}),u(n,{label:"状态",width:"100"},{default:w(i=>[u(I,{size:"small",type:Ze(i.row.status)},{default:w(()=>[pe(d(i.row.status||"-"),1)]),_:2},1032,["type"])]),_:1}),u(n,{prop:"duration_ms",label:"耗时",width:"110"})]),_:1},8,["data"])])])]),_:1},8,["modelValue"]),u(A,{modelValue:z.value,"onUpdate:modelValue":e[1]||(e[1]=i=>z.value=i),title:"慢SQL详情(近24小时)",width:"min(1080px, 96vw)"},{default:w(()=>[s("div",Zl,[s("span",null,"慢SQL总数:"+d(l(r.value?.total_slow_queries)),1),s("span",null,"去重SQL:"+d(l(r.value?.unique_sql)),1),s("span",null,"平均耗时:"+d(c(r.value?.avg_duration_ms)),1),s("span",null,"峰值耗时:"+d(c(r.value?.max_duration_ms)),1),s("span",null,"慢阈值:"+d(c(r.value?.slow_threshold_ms)),1)]),s("div",Yl,[e[6]||(e[6]=s("div",{class:"request-dialog-title"},"TOP 慢SQL(按出现次数)",-1)),s("div",Jl,[u(x,{data:Fe.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"rank",label:"#",width:"60"}),u(n,{prop:"sql",label:"SQL","min-width":"400","show-overflow-tooltip":""}),u(n,{prop:"count",label:"次数",width:"90"}),u(n,{prop:"avg_ms",label:"平均耗时",width:"120"}),u(n,{prop:"max_ms",label:"峰值耗时",width:"120"}),u(n,{prop:"last_seen",label:"最近出现",width:"180"}),u(n,{prop:"sample_params",label:"参数样本","min-width":"140","show-overflow-tooltip":""})]),_:1},8,["data"])])]),s("div",Kl,[e[7]||(e[7]=s("div",{class:"request-dialog-title"},"最近慢SQL",-1)),s("div",Xl,[u(x,{data:Ge.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"time_text",label:"时间",width:"180"}),u(n,{prop:"sql",label:"SQL","min-width":"420","show-overflow-tooltip":""}),u(n,{prop:"duration_ms",label:"耗时",width:"110"}),u(n,{prop:"params",label:"参数","min-width":"130","show-overflow-tooltip":""})]),_:1},8,["data"])])])]),_:1},8,["modelValue"])])}}},_t=vl(at,[["__scopeId","data-v-d610e788"]]);export{_t as default}; +import{f as tl,g as al,h as sl,k as ul,j as ol,n as nl,o as rl}from"./vendor-element-B5S5pUKo.js";import{c as il,a as cl,_ as vl,f as dl}from"./index-DOvMEmc8.js";import{f as _l}from"./email-Mh1SHQbX.js";import{f as ml,a as pl,b as bl,c as fl,d as hl,e as wl}from"./tasks-B7oNpIBD.js";import{f as gl}from"./system-CYbWdReq.js";import{M as kl}from"./MetricGrid-C3Xjc9mZ.js";import{i as de,r as m,c as a,o as yl,O as Sl,aj as R,n as S,q as k,t as s,L as u,G as D,J as d,K as _e,a3 as me,E as w,I as pe,D as ql,y as xl}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-misc-BeoNyvBp.js";import"./vendor-axios-B9ygI19o.js";const $l=il(async()=>{const{data:P}=await cl.get("/browser_pool/stats");return P},4e3);async function Tl(P={}){return $l.run(P)}const Ll={class:"page-stack"},Rl={class:"report-hero"},Ml={class:"hero-head"},Al={class:"hero-main"},Pl={class:"hero-meta app-muted"},Cl={key:0},Nl={key:1,class:"hero-dot"},Il={key:2},Dl={class:"mobile-report"},El={class:"mobile-module-head"},Wl={class:"mobile-module-title"},Ql={class:"mobile-module-desc app-muted"},Vl={class:"mobile-metrics"},zl={class:"mobile-metric-label app-muted"},Bl={class:"mobile-metric-value"},Ol={key:0,class:"module-extra-actions"},Ul={class:"request-dialog-summary app-muted"},Fl={class:"request-dialog-block"},Gl={class:"table-wrap"},jl={class:"request-dialog-block"},Hl={class:"table-wrap"},Zl={class:"request-dialog-summary app-muted"},Yl={class:"request-dialog-block"},Jl={class:"table-wrap"},Kl={class:"request-dialog-block"},Xl={class:"table-wrap"},et=3,lt=5e3,tt=2e4,at={__name:"ReportPage",setup(P){const be=de("refreshStats",null),fe=de("adminStats",null),E=m(!1),W=m(!1),T=m(""),Q=m(null),b=m(null),p=m(null),y=m(null),g=m(null),C=m(null),L=m(null),f=m(null),v=m(null),r=m(null),V=m(!1),z=m(!1);m("running");function he(){try{T.value=new Date().toLocaleString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{T.value=""}}function l(t){const e=Number(t);return Number.isFinite(e)?e:0}function we(t){const e=String(t??"").trim();if(!e)return 0;const o=e.endsWith("%")?e.slice(0,-1):e,_=Number(o);return!Number.isFinite(_)||_<0?0:_>1e3?1e3:_}function B(t){return`${Math.round(we(t))}%`}function c(t){const e=Number(t);return!Number.isFinite(e)||e<0?"-":e>=100?`${Math.round(e)}ms`:`${e.toFixed(1)}ms`}function Z(t){const e=Number(t);if(!Number.isFinite(e)||e<=0)return"-";try{return new Date(e*1e3).toLocaleTimeString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{return"-"}}function O(t){const e=Number(t);if(!Number.isFinite(e)||e<=0)return"-";try{return new Date(e*1e3).toLocaleString("zh-CN",{hour12:!1,timeZone:"Asia/Shanghai"})}catch{return"-"}}function ge(t){const e=String(t??"").trim();return!e||e==="manual"?"手动":e==="scheduled"?"系统定时":e==="batch"?"批量执行":e==="resumed"?"断点续跑":e.startsWith("user_scheduled:")?"用户定时":e}const Y=a(()=>{const t=fe?.value||{},e=l(b.value?.max_concurrent);return[{label:"总用户数",value:l(t.total_users),icon:tl,tone:"blue"},{label:"今日注册",value:l(t.new_users_today),icon:al,tone:"green"},{label:"近7天注册",value:l(t.new_users_7d),icon:sl,tone:"purple"},{label:"总账号数",value:l(t.total_accounts),icon:ul,tone:"cyan"},{label:"VIP用户",value:l(t.vip_users),icon:ol,tone:"orange"},{label:"运行中任务",value:l(b.value?.running_count),icon:nl,tone:"green",sub:e?`并发上限 ${e}`:""},{label:"排队任务",value:l(b.value?.queuing_count),icon:rl,tone:"purple"}]}),h=a(()=>Q.value?.today||{}),q=a(()=>Q.value?.total||{});a(()=>b.value?.running||[]);const N=a(()=>b.value?.queuing||[]),ke=a(()=>l(b.value?.running_count)),ye=a(()=>l(b.value?.queuing_count)),Se=a(()=>{const t=L.value?.workers;return Array.isArray(t)?[...t].sort((e,o)=>l(e?.worker_id)-l(o?.worker_id)):[]}),J=a(()=>l(L.value?.total_workers)),U=a(()=>Se.value.filter(t=>!!t?.has_browser).length),K=a(()=>l(L.value?.idle_workers)),X=a(()=>l(L.value?.queue_size)),F=a(()=>l(L.value?.active_workers)),ee=a(()=>{const t=l(h.value.success_tasks),e=l(h.value.failed_tasks),o=t+e;return o>0?Math.round(t/o*1e3)/10:0}),G=a(()=>l(p.value?.success_rate));a(()=>[{label:"总任务",value:l(h.value.total_tasks),tone:"blue"},{label:"成功",value:l(h.value.success_tasks),tone:"green"},{label:"失败",value:l(h.value.failed_tasks),tone:"red"},{label:"浏览内容",value:l(h.value.total_items),tone:"purple"},{label:"查看附件",value:l(h.value.total_attachments),tone:"cyan"}]),a(()=>[{label:"总任务",value:l(q.value.total_tasks),tone:"blue"},{label:"成功",value:l(q.value.success_tasks),tone:"green"},{label:"失败",value:l(q.value.failed_tasks),tone:"red"},{label:"浏览内容",value:l(q.value.total_items),tone:"purple"},{label:"查看附件",value:l(q.value.total_attachments),tone:"cyan"}]),a(()=>[{label:"总发送",value:l(p.value?.total_sent),tone:"blue"},{label:"成功",value:l(p.value?.total_success),tone:"green"},{label:"失败",value:l(p.value?.total_failed),tone:"red"},{label:"成功率",value:`${G.value}%`,tone:"purple"}]),a(()=>[{label:"注册验证",value:l(p.value?.register_sent),tone:"cyan"},{label:"密码重置",value:l(p.value?.reset_sent),tone:"orange"},{label:"邮箱绑定",value:l(p.value?.bind_sent),tone:"purple"},{label:"任务完成",value:l(p.value?.task_complete_sent),tone:"green"}]),a(()=>[{label:"总反馈",value:l(y.value?.total),tone:"blue"},{label:"待处理",value:l(y.value?.pending),tone:"orange"},{label:"已回复",value:l(y.value?.replied),tone:"green"}]),a(()=>[{label:"总 Worker",value:J.value,tone:"blue"},{label:"活跃 Worker",value:U.value,tone:"green"},{label:"空闲 Worker",value:K.value,tone:"cyan"},{label:"忙碌 Worker",value:F.value,tone:"orange"},{label:"队列",value:X.value,tone:"purple"}]);const qe=a(()=>(f.value?.schedule_enabled??0)===1),xe=a(()=>f.value?.schedule_time||"-"),$e=a(()=>f.value?.schedule_browse_type||"-"),Te=a(()=>String(f.value?.schedule_weekdays||"").trim());a(()=>{const t=Te.value;if(!t)return"";const e={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},o=t.split(",").map(_=>_.trim()).filter(Boolean);return o.length?o.map(_=>e[Number(_)]||_).join("、"):t});const Le=a(()=>(f.value?.proxy_enabled??0)===1);a(()=>f.value?.proxy_api_url||"");const le=a(()=>l(f.value?.proxy_expire_minutes)),j=a(()=>l(f.value?.max_concurrent_global)),Re=a(()=>l(f.value?.max_concurrent_per_account)),Me=a(()=>l(f.value?.max_screenshot_concurrent)),Ae=a(()=>{const t=l(b.value?.running_count),e=l(b.value?.queuing_count),o=l(b.value?.max_concurrent);return`运行中 ${t} / 排队 ${e} / 并发上限 ${o||j.value||"-"}`}),Pe=a(()=>Y.value.map(t=>({label:t.label,value:t.sub?`${t.value}(${t.sub})`:t.value}))),Ce=a(()=>[{label:"今日总任务",value:l(h.value.total_tasks)},{label:"今日成功",value:l(h.value.success_tasks)},{label:"今日失败",value:l(h.value.failed_tasks)},{label:"今日成功率",value:`${ee.value}%`},{label:"累计任务",value:l(q.value.total_tasks)},{label:"累计成功",value:l(q.value.success_tasks)}]),Ne=a(()=>[{label:"运行中",value:ke.value},{label:"排队中",value:ye.value},{label:"并发上限",value:l(b.value?.max_concurrent)||j.value||"-"},{label:"排队首条来源",value:ge(N.value[0]?.source)},{label:"排队首条状态",value:N.value[0]?.detail_status||N.value[0]?.status||"-"},{label:"最长等待",value:N.value[0]?.elapsed_display||"-"}]),Ie=a(()=>[{label:"总发送",value:l(p.value?.total_sent)},{label:"成功",value:l(p.value?.total_success)},{label:"失败",value:l(p.value?.total_failed)},{label:"成功率",value:`${G.value}%`},{label:"注册验证",value:l(p.value?.register_sent)},{label:"重置密码",value:l(p.value?.reset_sent)}]),De=a(()=>[{label:"总反馈",value:l(y.value?.total)},{label:"待处理",value:l(y.value?.pending)},{label:"已回复",value:l(y.value?.replied)}]),Ee=a(()=>[{label:"CPU",value:B(g.value?.cpu_percent)},{label:"内存",value:B(g.value?.memory_percent)},{label:"磁盘",value:B(g.value?.disk_percent)},{label:"容器状态",value:C.value?.status||"-"},{label:"容器名",value:C.value?.container_name||"-"},{label:"容器运行",value:C.value?.uptime||"-"}]),We=a(()=>[{label:"总 Worker",value:J.value},{label:"活跃 Worker",value:U.value},{label:"忙碌 Worker",value:F.value},{label:"空闲 Worker",value:K.value},{label:"任务队列",value:X.value}]),Qe=a(()=>{const t=v.value?.top_paths;return Array.isArray(t)?t.slice(0,3):[]}),Ve=a(()=>{const t=[{label:"总请求",value:l(v.value?.total_requests)},{label:"API请求",value:l(v.value?.api_requests)},{label:"慢请求",value:l(v.value?.slow_requests)},{label:"错误请求",value:l(v.value?.error_requests)}];return Qe.value.forEach((e,o)=>{const _=String(e?.path||"-");t.push({label:`慢接口${o+1}`,value:`${_} · 峰值 ${c(e?.max_ms)}`})}),t}),ze=a(()=>{const t=c(v.value?.avg_duration_ms),e=c(v.value?.max_duration_ms),o=Z(v.value?.last_request_ts),_=c(v.value?.slow_threshold_ms);return`均值 ${t} · 峰值 ${e} · 慢阈 ${_} · 最近 ${o}`}),Be=a(()=>(Array.isArray(r.value?.top_sql)?r.value.top_sql:[]).slice(0,3)),te=a(()=>{const t=l(r.value?.window_seconds);return t<=0?24:Math.max(1,Math.round(t/3600))}),Oe=a(()=>{const t=[{label:`慢SQL(${te.value}h)`,value:l(r.value?.total_slow_queries)},{label:"去重SQL",value:l(r.value?.unique_sql)},{label:"平均耗时",value:c(r.value?.avg_duration_ms)},{label:"峰值耗时",value:c(r.value?.max_duration_ms)}];return Be.value.forEach((e,o)=>{t.push({label:`慢SQL${o+1}`,value:`${c(e?.max_ms)} · ${String(e?.sql||"-")}`})}),t}),Ue=a(()=>{const t=c(r.value?.slow_threshold_ms),e=Z(r.value?.last_slow_ts);return`窗口 ${te.value}h · 慢阈 ${t} · 最近 ${e}`}),Fe=a(()=>(Array.isArray(r.value?.top_sql)?r.value.top_sql:[]).map((e,o)=>({rank:o+1,sql:String(e?.sql||"-"),count:l(e?.count),avg_ms:c(e?.avg_ms),max_ms:c(e?.max_ms),last_seen:O(e?.last_ts),sample_params:String(e?.sample_params||"-")}))),Ge=a(()=>[...Array.isArray(r.value?.recent_slow_sql)?r.value.recent_slow_sql:[]].sort((e,o)=>Number(o?.time||0)-Number(e?.time||0)).map(e=>({time_text:O(e?.time),sql:String(e?.sql||"-"),duration_ms:c(e?.duration_ms),params:String(e?.params||"-")}))),je=a(()=>(Array.isArray(v.value?.top_paths)?v.value.top_paths:[]).map((e,o)=>({rank:o+1,path:String(e?.path||"-"),count:l(e?.count),avg_ms:c(e?.avg_ms),max_ms:c(e?.max_ms),status_5xx:l(e?.status_5xx)}))),He=a(()=>[...Array.isArray(v.value?.recent_slow)?v.value.recent_slow:[]].sort((e,o)=>Number(o?.time||0)-Number(e?.time||0)).map(e=>({time_text:O(e?.time),method:String(e?.method||"-").toUpperCase(),path:String(e?.path||"-"),status:l(e?.status),duration_ms:c(e?.duration_ms)})));function Ze(t){const e=l(t);return e>=500?"danger":e>=400?"warning":e>=300?"info":"success"}function Ye(){V.value=!0}function Je(){z.value=!0}const ae=a(()=>{const t=r.value?.slow_threshold_ms;if(t!=null)return c(t);const e=f.value?.db_slow_query_ms;return e!=null?c(e):"-"}),Ke=a(()=>[{label:"定时任务",value:qe.value?"启用":"关闭"},{label:"执行时间",value:xe.value||"-"},{label:"浏览类型",value:$e.value||"-"},{label:"代理",value:Le.value?"启用":"关闭"},{label:"代理有效期",value:le.value?`${le.value} 分钟`:"-"},{label:"全局并发",value:j.value||"-"},{label:"单账号并发",value:Re.value||"-"},{label:"截图并发",value:Me.value||"-"},{label:"慢SQL阈值",value:ae.value}]),Xe=a(()=>[{key:"overview",title:"平台概览",desc:T.value?`更新 ${T.value}`:"核心指标",tone:"blue",items:Pe.value},{key:"task",title:"任务概览",desc:l(h.value.total_tasks)>0?`今日成功率 ${ee.value}%`:"今日暂无任务",tone:"purple",items:Ce.value},{key:"queue",title:"队列监控",desc:Ae.value,tone:"blue",items:Ne.value},{key:"email",title:"邮件报表",desc:`成功率 ${G.value}%`,tone:"cyan",items:Ie.value},{key:"feedback",title:"反馈概览",desc:`待处理 ${l(y.value?.pending)} 条`,tone:"orange",items:De.value},{key:"resource",title:"系统资源",desc:g.value?.uptime?`运行 ${g.value.uptime}`:"运行状态获取中",tone:"green",items:Ee.value},{key:"request",title:"接口性能",desc:ze.value,tone:"purple",items:Ve.value},{key:"slow_sql",title:"慢SQL监控",desc:Ue.value,tone:"red",items:Oe.value},{key:"worker",title:"截图线程池",desc:`活跃 ${U.value} · 忙碌 ${F.value}`,tone:"cyan",items:We.value},{key:"config",title:"配置概览",desc:"并发 / 代理 / 定时任务",tone:"red",items:Ke.value}]);let se=1;async function ue(t={}){const e=t.showLoading??!0;if(W.value)return;const o=!!t.forceStatsSync,_=o||se%et===0;se+=1,W.value=!0,e&&(E.value=!0);try{const[n,x,I,A,i,$,re,ie,ce,ve]=await Promise.allSettled([ml(),pl(),_l(),dl(),bl(),fl(),Tl(),hl(),wl(),gl()]);n.status==="fulfilled"&&(Q.value=n.value),x.status==="fulfilled"&&(b.value=x.value),I.status==="fulfilled"&&(p.value=I.value),A.status==="fulfilled"&&(y.value=A.value),i.status==="fulfilled"&&(g.value=i.value),$.status==="fulfilled"&&(C.value=$.value),re.status==="fulfilled"&&(L.value=re.value),ie.status==="fulfilled"&&(v.value=ie.value),ce.status==="fulfilled"&&(r.value=ce.value),ve.status==="fulfilled"&&(f.value=ve.value),_&&await be?.({force:o}),he()}finally{W.value=!1,e&&(E.value=!1)}}let M=null;function el(){return typeof document>"u"?!1:document.visibilityState==="hidden"}function ll(){return el()?tt:lt}function oe(){M&&(clearTimeout(M),M=null)}function H(){oe(),M=window.setTimeout(async()=>{M=null,await ue({showLoading:!1}).catch(()=>{}),H()},ll())}function ne(){H()}return yl(()=>{ue({showLoading:!1}).catch(()=>{}).finally(()=>{H()}),window.addEventListener("visibilitychange",ne)}),Sl(()=>{oe(),window.removeEventListener("visibilitychange",ne)}),(t,e)=>{const o=R("el-button"),_=R("el-card"),n=R("el-table-column"),x=R("el-table"),I=R("el-tag"),A=R("el-dialog");return k(),S("div",Ll,[s("section",Rl,[s("div",Ml,[s("div",Al,[e[3]||(e[3]=s("h2",null,"报表中心",-1)),s("div",Pl,[T.value?(k(),S("span",Cl,"更新时间:"+d(T.value),1)):D("",!0),e[2]||(e[2]=s("span",{class:"hero-dot"},"·",-1)),s("span",null,"慢SQL阈值 "+d(ae.value),1),g.value?.uptime?(k(),S("span",Nl,"·")):D("",!0),g.value?.uptime?(k(),S("span",Il,"运行 "+d(g.value.uptime),1)):D("",!0)])])]),u(kl,{class:"hero-overview-grid",items:Y.value,loading:E.value,"min-width":165},null,8,["items","loading"])]),s("section",Dl,[(k(!0),S(_e,null,me(Xe.value,i=>(k(),ql(_,{key:i.key,shadow:"never",class:xl(["mobile-module-card",`mobile-tone-${i.tone}`]),"body-style":{padding:"12px"}},{default:w(()=>[s("div",El,[s("div",Wl,d(i.title),1),s("div",Ql,d(i.desc),1)]),s("div",Vl,[(k(!0),S(_e,null,me(i.items,$=>(k(),S("div",{key:`${i.key}-${$.label}`,class:"mobile-metric-item"},[s("div",zl,d($.label),1),s("div",Bl,d($.value),1)]))),128))]),i.key==="request"||i.key==="slow_sql"?(k(),S("div",Ol,[u(o,{size:"small",type:"primary",plain:"",onClick:$=>i.key==="request"?Ye():Je()},{default:w(()=>[pe(d(i.key==="request"?"查看慢接口详情":"查看慢SQL详情"),1)]),_:2},1032,["onClick"])])):D("",!0)]),_:2},1032,["class"]))),128))]),u(A,{modelValue:V.value,"onUpdate:modelValue":e[0]||(e[0]=i=>V.value=i),title:"慢接口详情",width:"min(1080px, 96vw)"},{default:w(()=>[s("div",Ul,[s("span",null,"总请求:"+d(l(v.value?.total_requests)),1),s("span",null,"API请求:"+d(l(v.value?.api_requests)),1),s("span",null,"慢请求:"+d(l(v.value?.slow_requests)),1),s("span",null,"错误请求:"+d(l(v.value?.error_requests)),1)]),s("div",Fl,[e[4]||(e[4]=s("div",{class:"request-dialog-title"},"慢接口排行榜",-1)),s("div",Gl,[u(x,{data:je.value,size:"small","max-height":"280"},{default:w(()=>[u(n,{prop:"rank",label:"#",width:"60"}),u(n,{prop:"path",label:"接口路径","min-width":"340","show-overflow-tooltip":""}),u(n,{prop:"count",label:"请求数",width:"100"}),u(n,{prop:"avg_ms",label:"平均耗时",width:"120"}),u(n,{prop:"max_ms",label:"峰值耗时",width:"120"}),u(n,{prop:"status_5xx",label:"5xx",width:"90"})]),_:1},8,["data"])])]),s("div",jl,[e[5]||(e[5]=s("div",{class:"request-dialog-title"},"最近慢请求",-1)),s("div",Hl,[u(x,{data:He.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"time_text",label:"时间",width:"180"}),u(n,{prop:"method",label:"方法",width:"90"}),u(n,{prop:"path",label:"接口路径","min-width":"320","show-overflow-tooltip":""}),u(n,{label:"状态",width:"100"},{default:w(i=>[u(I,{size:"small",type:Ze(i.row.status)},{default:w(()=>[pe(d(i.row.status||"-"),1)]),_:2},1032,["type"])]),_:1}),u(n,{prop:"duration_ms",label:"耗时",width:"110"})]),_:1},8,["data"])])])]),_:1},8,["modelValue"]),u(A,{modelValue:z.value,"onUpdate:modelValue":e[1]||(e[1]=i=>z.value=i),title:"慢SQL详情(近24小时)",width:"min(1080px, 96vw)"},{default:w(()=>[s("div",Zl,[s("span",null,"慢SQL总数:"+d(l(r.value?.total_slow_queries)),1),s("span",null,"去重SQL:"+d(l(r.value?.unique_sql)),1),s("span",null,"平均耗时:"+d(c(r.value?.avg_duration_ms)),1),s("span",null,"峰值耗时:"+d(c(r.value?.max_duration_ms)),1),s("span",null,"慢阈值:"+d(c(r.value?.slow_threshold_ms)),1)]),s("div",Yl,[e[6]||(e[6]=s("div",{class:"request-dialog-title"},"TOP 慢SQL(按出现次数)",-1)),s("div",Jl,[u(x,{data:Fe.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"rank",label:"#",width:"60"}),u(n,{prop:"sql",label:"SQL","min-width":"400","show-overflow-tooltip":""}),u(n,{prop:"count",label:"次数",width:"90"}),u(n,{prop:"avg_ms",label:"平均耗时",width:"120"}),u(n,{prop:"max_ms",label:"峰值耗时",width:"120"}),u(n,{prop:"last_seen",label:"最近出现",width:"180"}),u(n,{prop:"sample_params",label:"参数样本","min-width":"140","show-overflow-tooltip":""})]),_:1},8,["data"])])]),s("div",Kl,[e[7]||(e[7]=s("div",{class:"request-dialog-title"},"最近慢SQL",-1)),s("div",Xl,[u(x,{data:Ge.value,size:"small","max-height":"320"},{default:w(()=>[u(n,{prop:"time_text",label:"时间",width:"180"}),u(n,{prop:"sql",label:"SQL","min-width":"420","show-overflow-tooltip":""}),u(n,{prop:"duration_ms",label:"耗时",width:"110"}),u(n,{prop:"params",label:"参数","min-width":"130","show-overflow-tooltip":""})]),_:1},8,["data"])])])]),_:1},8,["modelValue"])])}}},_t=vl(at,[["__scopeId","data-v-6101337a"]]);export{_t as default}; diff --git a/static/admin/assets/ReportPage-IaDpUFfl.css b/static/admin/assets/ReportPage-IaDpUFfl.css new file mode 100644 index 0000000..c8edcdc --- /dev/null +++ b/static/admin/assets/ReportPage-IaDpUFfl.css @@ -0,0 +1 @@ +.page-stack[data-v-6101337a]{display:flex;flex-direction:column;gap:14px}.report-hero[data-v-6101337a]{position:relative;overflow:hidden;border-radius:18px;border:1px solid rgba(17,24,39,.1);background:radial-gradient(circle at 10% 10%,rgba(59,130,246,.18),transparent 48%),radial-gradient(circle at 80% 0%,rgba(236,72,153,.16),transparent 45%),radial-gradient(circle at 90% 90%,rgba(16,185,129,.14),transparent 42%),#ffffffb8;box-shadow:0 14px 40px #0f172a14;-webkit-backdrop-filter:saturate(180%) blur(12px);backdrop-filter:saturate(180%) blur(12px);padding:16px}.hero-head[data-v-6101337a]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}.hero-main h2[data-v-6101337a]{margin:0;font-size:19px;font-weight:900;letter-spacing:.2px}.hero-meta[data-v-6101337a]{margin-top:6px;font-size:12px;display:flex;align-items:center;flex-wrap:wrap;gap:8px}.hero-dot[data-v-6101337a]{opacity:.65}.hero-actions[data-v-6101337a]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.mobile-report[data-v-6101337a]{display:grid;gap:12px;grid-template-columns:repeat(3,minmax(0,1fr))}.mobile-module-card[data-v-6101337a]{position:relative;overflow:hidden;border-radius:14px;border:1px solid rgba(17,24,39,.12);background:#ffffffe6;box-shadow:var(--app-shadow-soft)}.mobile-module-card[data-v-6101337a]:before{content:"";position:absolute;left:0;top:0;right:0;height:3px;background:var(--mobile-accent, #3b82f6)}.mobile-tone-blue[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #3b82f6, #06b6d4)}.mobile-tone-cyan[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #06b6d4, #3b82f6)}.mobile-tone-purple[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #8b5cf6, #ec4899)}.mobile-tone-orange[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #f59e0b, #f97316)}.mobile-tone-green[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #10b981, #22c55e)}.mobile-tone-red[data-v-6101337a]{--mobile-accent: linear-gradient(90deg, #ef4444, #f43f5e)}.mobile-module-head[data-v-6101337a]{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.mobile-module-title[data-v-6101337a]{font-size:13px;font-weight:900;color:#0f172a}.mobile-module-desc[data-v-6101337a]{min-width:0;max-width:68%;font-size:11px;line-height:1.4;text-align:right}.mobile-metrics[data-v-6101337a]{margin-top:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mobile-metric-item[data-v-6101337a]{padding:8px 10px;border-radius:10px;border:1px solid rgba(17,24,39,.08);background:#f8fafce6}.mobile-metric-label[data-v-6101337a]{font-size:11px;line-height:1.35}.mobile-metric-value[data-v-6101337a]{margin-top:4px;font-size:14px;font-weight:800;color:#0f172a;line-height:1.3;word-break:break-word}.module-extra-actions[data-v-6101337a]{margin-top:10px;display:flex;justify-content:flex-end}.request-dialog-summary[data-v-6101337a]{display:flex;flex-wrap:wrap;gap:10px 16px;font-size:12px;margin-bottom:12px}.request-dialog-block+.request-dialog-block[data-v-6101337a]{margin-top:14px}.request-dialog-title[data-v-6101337a]{font-size:13px;font-weight:800;margin-bottom:8px}.hero-overview-grid[data-v-6101337a]{display:none}.panel[data-v-6101337a]{border-radius:18px;border:1px solid rgba(17,24,39,.1);background:#ffffffb8;box-shadow:var(--app-shadow);-webkit-backdrop-filter:saturate(180%) blur(10px);backdrop-filter:saturate(180%) blur(10px)}.panel-head[data-v-6101337a]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}.head-left[data-v-6101337a]{display:flex;align-items:center;gap:12px;min-width:0}.head-text[data-v-6101337a]{min-width:0}.head-icon[data-v-6101337a]{width:40px;height:40px;border-radius:16px;display:flex;align-items:center;justify-content:center;border:1px solid rgba(17,24,39,.08);flex:0 0 auto}.tone-blue[data-v-6101337a]{background:#3b82f61f;color:#1d4ed8}.tone-cyan[data-v-6101337a]{background:#22d3ee1f;color:#0369a1}.tone-purple[data-v-6101337a]{background:#8b5cf61f;color:#6d28d9}.tone-orange[data-v-6101337a]{background:#f59e0b1f;color:#b45309}.tone-green[data-v-6101337a]{background:#10b9811f;color:#047857}.tone-red[data-v-6101337a]{background:#ef44441f;color:#b91c1c}.panel-title[data-v-6101337a]{font-size:14px;font-weight:900}.panel-sub[data-v-6101337a]{margin-top:4px;font-size:12px;color:var(--app-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.metrics-block[data-v-6101337a]{display:flex;flex-direction:column;gap:10px}.block-title[data-v-6101337a]{font-size:13px;font-weight:900;letter-spacing:.2px}.divider[data-v-6101337a]{height:1px;background:linear-gradient(90deg,transparent,rgba(17,24,39,.12),transparent);margin:14px 0}.queue-tabs[data-v-6101337a] .el-tabs__header{margin:0 0 10px}.tab-label[data-v-6101337a]{display:inline-flex;align-items:center;gap:6px}.table-wrap[data-v-6101337a]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.help[data-v-6101337a]{margin-top:10px;font-size:12px}.resource-grid[data-v-6101337a]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-item[data-v-6101337a]{border:1px solid rgba(17,24,39,.08);border-radius:16px;padding:12px;background:#ffffffb3}.resource-k[data-v-6101337a]{font-size:12px;margin-bottom:8px}.resource-sub[data-v-6101337a]{margin-top:8px;font-size:12px}.config-grid[data-v-6101337a]{display:grid;grid-template-columns:1fr;gap:10px}.config-item[data-v-6101337a]{border:1px solid rgba(17,24,39,.08);border-radius:16px;padding:12px;background:#ffffffb3}.config-k[data-v-6101337a]{font-size:12px}.config-v[data-v-6101337a]{margin-top:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.config-inline[data-v-6101337a]{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.config-split[data-v-6101337a]{opacity:.65}.config-sub[data-v-6101337a]{margin-top:8px;font-size:12px}.err[data-v-6101337a]{color:#b91c1c}[data-v-6101337a] .el-table{--el-table-border-color: rgba(17, 24, 39, .08);--el-table-header-bg-color: rgba(246, 247, 251, .8)}[data-v-6101337a] .el-table th.el-table__cell{background:#f6f7fbcc}@media(max-width:1200px){.mobile-report[data-v-6101337a]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:768px){.mobile-report[data-v-6101337a]{grid-template-columns:1fr;gap:10px}.report-hero[data-v-6101337a]{border-radius:14px;padding:12px}.hero-main h2[data-v-6101337a]{font-size:17px}.hero-meta[data-v-6101337a]{margin-top:4px;gap:6px;font-size:11px}.resource-grid[data-v-6101337a]{grid-template-columns:1fr}}@media(max-width:420px){.mobile-metrics[data-v-6101337a]{grid-template-columns:1fr}.mobile-module-desc[data-v-6101337a]{max-width:62%}} diff --git a/static/admin/assets/SecurityPage-C2-mJ7eD.css b/static/admin/assets/SecurityPage-C2-mJ7eD.css new file mode 100644 index 0000000..bd729f4 --- /dev/null +++ b/static/admin/assets/SecurityPage-C2-mJ7eD.css @@ -0,0 +1 @@ +.page-stack[data-v-c89794eb]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-c89794eb]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-c89794eb]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.sub-card[data-v-c89794eb]{margin-top:12px;border-radius:var(--app-radius);border:1px solid var(--app-border)}.filters[data-v-c89794eb]{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px}.table-wrap[data-v-c89794eb]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-c89794eb]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mono[data-v-c89794eb]{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.pagination[data-v-c89794eb]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-c89794eb]{font-size:12px}.inner-tabs[data-v-c89794eb]{margin-top:6px}.risk-head[data-v-c89794eb]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.risk-title[data-v-c89794eb]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dialog-actions[data-v-c89794eb]{display:flex;align-items:center;gap:10px}.spacer[data-v-c89794eb]{flex:1} diff --git a/static/admin/assets/SecurityPage-DN76ndc_.css b/static/admin/assets/SecurityPage-DN76ndc_.css deleted file mode 100644 index 4428459..0000000 --- a/static/admin/assets/SecurityPage-DN76ndc_.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-94e0bde6]{display:flex;flex-direction:column;gap:14px;min-width:0}.toolbar[data-v-94e0bde6]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-94e0bde6]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.sub-card[data-v-94e0bde6]{margin-top:12px;border-radius:var(--app-radius);border:1px solid var(--app-border)}.filters[data-v-94e0bde6]{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px}.table-wrap[data-v-94e0bde6]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.ellipsis[data-v-94e0bde6]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mono[data-v-94e0bde6]{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.pagination[data-v-94e0bde6]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-94e0bde6]{font-size:12px}.inner-tabs[data-v-94e0bde6]{margin-top:6px}.risk-head[data-v-94e0bde6]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.risk-title[data-v-94e0bde6]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dialog-actions[data-v-94e0bde6]{display:flex;align-items:center;gap:10px}.spacer[data-v-94e0bde6]{flex:1} diff --git a/static/admin/assets/SecurityPage-xwMQfhuh.js b/static/admin/assets/SecurityPage-yzYEGeTN.js similarity index 99% rename from static/admin/assets/SecurityPage-xwMQfhuh.js rename to static/admin/assets/SecurityPage-yzYEGeTN.js index abef170..45ee885 100644 --- a/static/admin/assets/SecurityPage-xwMQfhuh.js +++ b/static/admin/assets/SecurityPage-yzYEGeTN.js @@ -1,5 +1,5 @@ -import{a as g,_ as qe}from"./index-C1f9ticl.js";import{M as Ee}from"./MetricGrid-BnihYB_8.js";import{E as ee,a as w}from"./vendor-element-B5S5pUKo.js";import{r as d,c as oe,o as ze,aj as v,ap as Ae,n as P,q as c,t as p,L as a,E as l,I as i,K as Ge,a3 as Ke,D as b,F as te,J as f,G as ue}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function Oe(){const{data:u}=await g.get("/admin/security/dashboard");return u}async function He(u){const{data:m}=await g.get("/admin/security/threats",{params:u});return m}async function Je(){const{data:u}=await g.get("/admin/security/banned-ips");return u}async function Qe(){const{data:u}=await g.get("/admin/security/banned-users");return u}async function We(u){const{data:m}=await g.post("/admin/security/ban-ip",u);return m}async function Xe(u){const{data:m}=await g.post("/admin/security/unban-ip",{ip:u});return m}async function Ye(u){const{data:m}=await g.post("/admin/security/ban-user",u);return m}async function Ze(u){const{data:m}=await g.post("/admin/security/unban-user",{user_id:u});return m}async function et(u){const m=encodeURIComponent(String(u||"").trim()),{data:V}=await g.get(`/admin/security/ip-risk/${m}`);return V}async function tt(u){const{data:m}=await g.post("/admin/security/ip-risk/clear",{ip:u});return m}async function at(u){const m=encodeURIComponent(String(u||"").trim()),{data:V}=await g.get(`/admin/security/user-risk/${m}`);return V}async function lt(){const{data:u}=await g.post("/admin/security/cleanup",{});return u}const nt={class:"page-stack"},it={class:"app-page-title"},st={class:"toolbar"},ot={class:"filters"},ut={class:"table-wrap"},rt={key:1},dt={key:1},pt={class:"mono ellipsis"},ct={class:"ellipsis"},ft={class:"pagination"},mt={class:"page-hint app-muted"},vt={class:"toolbar"},_t={class:"table-wrap"},yt={class:"table-wrap"},bt={class:"filters"},gt={class:"filters"},kt={class:"risk-head"},ht={class:"risk-title"},wt={key:0},It={key:1},Vt={class:"toolbar"},xt={class:"table-wrap"},Ct={class:"mono ellipsis"},St={class:"ellipsis"},Tt={class:"dialog-actions"},re=20,Pt={__name:"SecurityPage",setup(u){const m=d("threats"),V=d(!1),L=d(null),ae=d(!1),q=d([]),E=d(0),C=d(1),U=d(""),B=d(""),$=d(!1),de=d([]),pe=d([]),ce=d("ips"),S=d(!1),R=d(!1),o=d({kind:"ip",ip:"",user_id:"",reason:"",duration_hours:24,permanent:!1}),z=d("ip"),k=d(!1),A=d(""),G=d(""),_=d(null),h=d(""),Ie=["sql_injection","xss","path_traversal","command_injection","ssrf","scanner","bruteforce","csrf","xxe","file_upload"];function le(n){const e=Number(n);return Number.isFinite(e)?e:0}function D(n){const e=Number(n||0);return e>=80?{label:"高",type:"danger"}:e>=50?{label:"中",type:"warning"}:{label:"低",type:"success"}}function fe(n){const e=String(n||"").trim();return e||"永久"}function me(n){const e=[];return n?.field_name&&e.push(`字段: ${n.field_name}`),n?.rule&&e.push(`规则: ${n.rule}`),n?.matched&&e.push(`匹配: ${n.matched}`),n?.value_preview&&e.push(`值: ${n.value_preview}`),e.length?e.join(" · "):"-"}function K(n){const e=String(n?.request_method||"").trim(),s=String(n?.request_path||"").trim();return`${e} ${s}`.trim()||"-"}const Ve=oe(()=>{const n=new Set(Ie),e=L.value?.recent_threat_events||[];for(const s of e){const y=String(s?.threat_type||"").trim();y&&n.add(y)}for(const s of q.value||[]){const y=String(s?.threat_type||"").trim();y&&n.add(y)}return Array.from(n).sort((s,y)=>s.localeCompare(y)).map(s=>({label:s,value:s}))}),xe=oe(()=>{const n=L.value||{};return[{key:"threat_events_24h",label:"最近24小时威胁事件",value:le(n.threat_events_24h),tone:"red",hint:"用于衡量当前攻击面活跃度"},{key:"banned_ip_count",label:"当前封禁 IP 数",value:le(n.banned_ip_count),tone:"orange",hint:"自动与人工封禁总量"},{key:"banned_user_count",label:"当前封禁用户数",value:le(n.banned_user_count),tone:"purple",hint:"高风险账户拦截情况"}]}),Ce=oe(()=>Math.max(1,Math.ceil((E.value||0)/re)));async function N(){V.value=!0;try{L.value=await Oe()}catch{L.value=null}finally{V.value=!1}}async function O(){ae.value=!0;try{const n={page:C.value,per_page:re};U.value&&(n.event_type=U.value),B.value&&(n.severity=B.value);const e=await He(n);q.value=e?.items||[],E.value=e?.total||0}catch{q.value=[],E.value=0}finally{ae.value=!1}}async function F(){if(!$.value){$.value=!0;try{const[n,e]=await Promise.allSettled([Je(),Qe()]);de.value=n.status==="fulfilled"?n.value?.items||[]:[],pe.value=e.status==="fulfilled"?e.value?.items||[]:[]}finally{$.value=!1}}}async function ve(){await Promise.allSettled([N(),O(),F()])}function Se(){C.value=1,O()}function Te(){U.value="",B.value="",C.value=1,O()}function _e(){o.value={kind:"ip",ip:"",user_id:"",reason:"",duration_hours:24,permanent:!1}}function H(n="ip",e={}){_e(),o.value.kind=n==="user"?"user":"ip",o.value.kind==="ip"?o.value.ip=String(e.ip||"").trim():o.value.user_id=String(e.user_id||"").trim(),e.reason&&(o.value.reason=String(e.reason||"").trim()),S.value=!0}async function Pe(){const n=o.value.kind,e=String(o.value.reason||"").trim(),s=!!o.value.permanent,y=Number(o.value.duration_hours||24);if(!e){w.error("原因不能为空");return}if(n==="ip"){const I=String(o.value.ip||"").trim();if(!I){w.error("IP不能为空");return}R.value=!0;try{await We({ip:I,reason:e,duration_hours:y,permanent:s}),w.success("IP已封禁"),S.value=!1,await Promise.allSettled([N(),F()])}catch{}finally{R.value=!1}return}const Q=String(o.value.user_id||"").trim(),r=Number.parseInt(Q,10);if(!Number.isFinite(r)){w.error("用户ID无效");return}R.value=!0;try{await Ye({user_id:r,reason:e,duration_hours:y,permanent:s}),w.success("用户已封禁"),S.value=!1,await Promise.allSettled([N(),F()])}catch{}finally{R.value=!1}}async function ye(n){const e=String(n||"").trim();if(e){try{await ee.confirm(`确定解除对 IP ${e} 的封禁吗?`,"解除封禁",{confirmButtonText:"解除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Xe(e),w.success("已解除IP封禁"),await Promise.allSettled([N(),F()])}catch{}}}async function be(n){const e=Number.parseInt(String(n||"").trim(),10);if(Number.isFinite(e)){try{await ee.confirm(`确定解除对 用户ID ${e} 的封禁吗?`,"解除封禁",{confirmButtonText:"解除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Ze(e),w.success("已解除用户封禁"),await Promise.allSettled([N(),F()])}catch{}}}function ge(n){const e=String(n||"").trim();e&&(m.value="risk",z.value="ip",A.value=e,J())}function ke(n){const e=String(n||"").trim();e&&(m.value="risk",z.value="user",G.value=e,ne())}async function J(){const n=String(A.value||"").trim();if(!n){w.error("请输入IP");return}k.value=!0;try{_.value=await et(n),h.value="ip"}catch{_.value=null,h.value=""}finally{k.value=!1}}async function ne(){const n=String(G.value||"").trim(),e=Number.parseInt(n,10);if(!Number.isFinite(e)){w.error("请输入有效的用户ID");return}k.value=!0;try{_.value=await at(e),h.value="user"}catch{_.value=null,h.value=""}finally{k.value=!1}}function Ue(){!_.value||!h.value||(h.value==="ip"?H("ip",{ip:_.value?.ip,reason:"风险查询手动封禁"}):H("user",{user_id:_.value?.user_id,reason:"风险查询手动封禁"}))}async function Be(){!_.value||!h.value||(h.value==="ip"?(await ye(_.value?.ip),await J()):(await be(_.value?.user_id),await ne()))}async function $e(){if(h.value!=="ip")return;const n=String(_.value?.ip||"").trim();if(n){try{await ee.confirm(`确定清除 IP ${n} 的风险分吗? +import{a as g,_ as qe}from"./index-DOvMEmc8.js";import{M as Ee}from"./MetricGrid-C3Xjc9mZ.js";import{E as ee,a as w}from"./vendor-element-B5S5pUKo.js";import{r as d,c as oe,o as ze,aj as v,ap as Ae,n as P,q as c,t as p,L as a,E as l,I as i,K as Ge,a3 as Ke,D as b,F as te,J as f,G as ue}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function Oe(){const{data:u}=await g.get("/admin/security/dashboard");return u}async function He(u){const{data:m}=await g.get("/admin/security/threats",{params:u});return m}async function Je(){const{data:u}=await g.get("/admin/security/banned-ips");return u}async function Qe(){const{data:u}=await g.get("/admin/security/banned-users");return u}async function We(u){const{data:m}=await g.post("/admin/security/ban-ip",u);return m}async function Xe(u){const{data:m}=await g.post("/admin/security/unban-ip",{ip:u});return m}async function Ye(u){const{data:m}=await g.post("/admin/security/ban-user",u);return m}async function Ze(u){const{data:m}=await g.post("/admin/security/unban-user",{user_id:u});return m}async function et(u){const m=encodeURIComponent(String(u||"").trim()),{data:V}=await g.get(`/admin/security/ip-risk/${m}`);return V}async function tt(u){const{data:m}=await g.post("/admin/security/ip-risk/clear",{ip:u});return m}async function at(u){const m=encodeURIComponent(String(u||"").trim()),{data:V}=await g.get(`/admin/security/user-risk/${m}`);return V}async function lt(){const{data:u}=await g.post("/admin/security/cleanup",{});return u}const nt={class:"page-stack"},it={class:"app-page-title"},st={class:"toolbar"},ot={class:"filters"},ut={class:"table-wrap"},rt={key:1},dt={key:1},pt={class:"mono ellipsis"},ct={class:"ellipsis"},ft={class:"pagination"},mt={class:"page-hint app-muted"},vt={class:"toolbar"},_t={class:"table-wrap"},yt={class:"table-wrap"},bt={class:"filters"},gt={class:"filters"},kt={class:"risk-head"},ht={class:"risk-title"},wt={key:0},It={key:1},Vt={class:"toolbar"},xt={class:"table-wrap"},Ct={class:"mono ellipsis"},St={class:"ellipsis"},Tt={class:"dialog-actions"},re=20,Pt={__name:"SecurityPage",setup(u){const m=d("threats"),V=d(!1),L=d(null),ae=d(!1),q=d([]),E=d(0),C=d(1),U=d(""),B=d(""),$=d(!1),de=d([]),pe=d([]),ce=d("ips"),S=d(!1),R=d(!1),o=d({kind:"ip",ip:"",user_id:"",reason:"",duration_hours:24,permanent:!1}),z=d("ip"),k=d(!1),A=d(""),G=d(""),_=d(null),h=d(""),Ie=["sql_injection","xss","path_traversal","command_injection","ssrf","scanner","bruteforce","csrf","xxe","file_upload"];function le(n){const e=Number(n);return Number.isFinite(e)?e:0}function D(n){const e=Number(n||0);return e>=80?{label:"高",type:"danger"}:e>=50?{label:"中",type:"warning"}:{label:"低",type:"success"}}function fe(n){const e=String(n||"").trim();return e||"永久"}function me(n){const e=[];return n?.field_name&&e.push(`字段: ${n.field_name}`),n?.rule&&e.push(`规则: ${n.rule}`),n?.matched&&e.push(`匹配: ${n.matched}`),n?.value_preview&&e.push(`值: ${n.value_preview}`),e.length?e.join(" · "):"-"}function K(n){const e=String(n?.request_method||"").trim(),s=String(n?.request_path||"").trim();return`${e} ${s}`.trim()||"-"}const Ve=oe(()=>{const n=new Set(Ie),e=L.value?.recent_threat_events||[];for(const s of e){const y=String(s?.threat_type||"").trim();y&&n.add(y)}for(const s of q.value||[]){const y=String(s?.threat_type||"").trim();y&&n.add(y)}return Array.from(n).sort((s,y)=>s.localeCompare(y)).map(s=>({label:s,value:s}))}),xe=oe(()=>{const n=L.value||{};return[{key:"threat_events_24h",label:"最近24小时威胁事件",value:le(n.threat_events_24h),tone:"red",hint:"用于衡量当前攻击面活跃度"},{key:"banned_ip_count",label:"当前封禁 IP 数",value:le(n.banned_ip_count),tone:"orange",hint:"自动与人工封禁总量"},{key:"banned_user_count",label:"当前封禁用户数",value:le(n.banned_user_count),tone:"purple",hint:"高风险账户拦截情况"}]}),Ce=oe(()=>Math.max(1,Math.ceil((E.value||0)/re)));async function N(){V.value=!0;try{L.value=await Oe()}catch{L.value=null}finally{V.value=!1}}async function O(){ae.value=!0;try{const n={page:C.value,per_page:re};U.value&&(n.event_type=U.value),B.value&&(n.severity=B.value);const e=await He(n);q.value=e?.items||[],E.value=e?.total||0}catch{q.value=[],E.value=0}finally{ae.value=!1}}async function F(){if(!$.value){$.value=!0;try{const[n,e]=await Promise.allSettled([Je(),Qe()]);de.value=n.status==="fulfilled"?n.value?.items||[]:[],pe.value=e.status==="fulfilled"?e.value?.items||[]:[]}finally{$.value=!1}}}async function ve(){await Promise.allSettled([N(),O(),F()])}function Se(){C.value=1,O()}function Te(){U.value="",B.value="",C.value=1,O()}function _e(){o.value={kind:"ip",ip:"",user_id:"",reason:"",duration_hours:24,permanent:!1}}function H(n="ip",e={}){_e(),o.value.kind=n==="user"?"user":"ip",o.value.kind==="ip"?o.value.ip=String(e.ip||"").trim():o.value.user_id=String(e.user_id||"").trim(),e.reason&&(o.value.reason=String(e.reason||"").trim()),S.value=!0}async function Pe(){const n=o.value.kind,e=String(o.value.reason||"").trim(),s=!!o.value.permanent,y=Number(o.value.duration_hours||24);if(!e){w.error("原因不能为空");return}if(n==="ip"){const I=String(o.value.ip||"").trim();if(!I){w.error("IP不能为空");return}R.value=!0;try{await We({ip:I,reason:e,duration_hours:y,permanent:s}),w.success("IP已封禁"),S.value=!1,await Promise.allSettled([N(),F()])}catch{}finally{R.value=!1}return}const Q=String(o.value.user_id||"").trim(),r=Number.parseInt(Q,10);if(!Number.isFinite(r)){w.error("用户ID无效");return}R.value=!0;try{await Ye({user_id:r,reason:e,duration_hours:y,permanent:s}),w.success("用户已封禁"),S.value=!1,await Promise.allSettled([N(),F()])}catch{}finally{R.value=!1}}async function ye(n){const e=String(n||"").trim();if(e){try{await ee.confirm(`确定解除对 IP ${e} 的封禁吗?`,"解除封禁",{confirmButtonText:"解除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Xe(e),w.success("已解除IP封禁"),await Promise.allSettled([N(),F()])}catch{}}}async function be(n){const e=Number.parseInt(String(n||"").trim(),10);if(Number.isFinite(e)){try{await ee.confirm(`确定解除对 用户ID ${e} 的封禁吗?`,"解除封禁",{confirmButtonText:"解除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Ze(e),w.success("已解除用户封禁"),await Promise.allSettled([N(),F()])}catch{}}}function ge(n){const e=String(n||"").trim();e&&(m.value="risk",z.value="ip",A.value=e,J())}function ke(n){const e=String(n||"").trim();e&&(m.value="risk",z.value="user",G.value=e,ne())}async function J(){const n=String(A.value||"").trim();if(!n){w.error("请输入IP");return}k.value=!0;try{_.value=await et(n),h.value="ip"}catch{_.value=null,h.value=""}finally{k.value=!1}}async function ne(){const n=String(G.value||"").trim(),e=Number.parseInt(n,10);if(!Number.isFinite(e)){w.error("请输入有效的用户ID");return}k.value=!0;try{_.value=await at(e),h.value="user"}catch{_.value=null,h.value=""}finally{k.value=!1}}function Ue(){!_.value||!h.value||(h.value==="ip"?H("ip",{ip:_.value?.ip,reason:"风险查询手动封禁"}):H("user",{user_id:_.value?.user_id,reason:"风险查询手动封禁"}))}async function Be(){!_.value||!h.value||(h.value==="ip"?(await ye(_.value?.ip),await J()):(await be(_.value?.user_id),await ne()))}async function $e(){if(h.value!=="ip")return;const n=String(_.value?.ip||"").trim();if(n){try{await ee.confirm(`确定清除 IP ${n} 的风险分吗? 清除风险分不会删除威胁历史,也不会解除封禁。`,"清除风险分",{confirmButtonText:"清除",cancelButtonText:"取消",type:"warning"})}catch{return}if(!k.value){k.value=!0;try{await tt(n),w.success("IP风险分已清零")}catch{}finally{k.value=!1}await J()}}}const ie=d(!1);async function Re(){try{await ee.confirm(`确定清理过期封禁记录,并衰减风险分吗? -该操作不会影响仍在有效期内的封禁。`,"清理过期记录",{confirmButtonText:"清理",cancelButtonText:"取消",type:"warning"})}catch{return}ie.value=!0;try{await lt(),w.success("清理完成"),await ve()}catch{}finally{ie.value=!1}}return ze(async()=>{await ve()}),(n,e)=>{const s=v("el-button"),y=v("el-option"),Q=v("el-select"),r=v("el-table-column"),I=v("el-tag"),W=v("el-link"),X=v("el-tooltip"),Y=v("el-table"),De=v("el-pagination"),x=v("el-tab-pane"),se=v("el-tabs"),M=v("el-input"),he=v("el-card"),we=v("el-radio-button"),Ne=v("el-radio-group"),T=v("el-form-item"),Fe=v("el-switch"),Me=v("el-input-number"),je=v("el-form"),Le=v("el-dialog"),Z=Ae("loading");return c(),P("div",nt,[p("div",it,[e[20]||(e[20]=p("h2",null,"安全防护",-1)),p("div",st,[a(s,{type:"warning",plain:"",loading:ie.value,onClick:Re},{default:l(()=>[...e[18]||(e[18]=[i("清理过期记录",-1)])]),_:1},8,["loading"]),a(s,{type:"primary",onClick:e[0]||(e[0]=t=>H())},{default:l(()=>[...e[19]||(e[19]=[i("手动封禁",-1)])]),_:1})])]),a(Ee,{items:xe.value,loading:V.value,"min-width":220},null,8,["items","loading"]),a(he,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[a(se,{modelValue:m.value,"onUpdate:modelValue":e[9]||(e[9]=t=>m.value=t)},{default:l(()=>[a(x,{label:"威胁事件",name:"threats"},{default:l(()=>[p("div",ot,[a(Q,{modelValue:U.value,"onUpdate:modelValue":e[1]||(e[1]=t=>U.value=t),placeholder:"类型",style:{width:"220px"},filterable:"",clearable:"","allow-create":"","default-first-option":""},{default:l(()=>[a(y,{label:"全部",value:""}),(c(!0),P(Ge,null,Ke(Ve.value,t=>(c(),b(y,{key:t.value,label:t.label,value:t.value},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),a(Q,{modelValue:B.value,"onUpdate:modelValue":e[2]||(e[2]=t=>B.value=t),placeholder:"严重程度",style:{width:"200px"},clearable:""},{default:l(()=>[a(y,{label:"全部",value:""}),a(y,{label:"高风险(>=80)",value:"high"}),a(y,{label:"中风险(50-79)",value:"medium"}),a(y,{label:"低风险(<50)",value:"low"})]),_:1},8,["modelValue"]),a(s,{type:"primary",onClick:Se},{default:l(()=>[...e[21]||(e[21]=[i("筛选",-1)])]),_:1}),a(s,{onClick:Te},{default:l(()=>[...e[22]||(e[22]=[i("重置",-1)])]),_:1})]),p("div",ut,[te((c(),b(Y,{data:q.value,style:{width:"100%"}},{default:l(()=>[a(r,{prop:"created_at",label:"时间",width:"180"}),a(r,{label:"类型",width:"170"},{default:l(({row:t})=>[a(I,{effect:"light",type:"info"},{default:l(()=>[i(f(t.threat_type||"unknown"),1)]),_:2},1024)]),_:1}),a(r,{label:"严重程度",width:"120"},{default:l(({row:t})=>[a(I,{type:D(t.score).type,effect:"light"},{default:l(()=>[i(f(D(t.score).label)+" ("+f(t.score??0)+") ",1)]),_:2},1032,["type"])]),_:1}),a(r,{label:"IP",width:"150"},{default:l(({row:t})=>[t.ip?(c(),b(W,{key:0,type:"primary",underline:!1,onClick:j=>ge(t.ip)},{default:l(()=>[i(f(t.ip),1)]),_:2},1032,["onClick"])):(c(),P("span",rt,"-"))]),_:1}),a(r,{label:"用户",width:"120"},{default:l(({row:t})=>[t.user_id!==null&&t.user_id!==void 0?(c(),b(W,{key:0,type:"primary",underline:!1,onClick:j=>ke(t.user_id)},{default:l(()=>[i(f(t.user_id),1)]),_:2},1032,["onClick"])):(c(),P("span",dt,"-"))]),_:1}),a(r,{label:"操作路径","min-width":"220"},{default:l(({row:t})=>[a(X,{content:K(t),placement:"top","show-after":300},{default:l(()=>[p("span",pt,f(K(t)),1)]),_:2},1032,["content"])]),_:1}),a(r,{label:"Payload预览","min-width":"240"},{default:l(({row:t})=>[a(X,{content:me(t),placement:"top","show-after":300},{default:l(()=>[p("span",ct,f(t.value_preview||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[Z,ae.value]])]),p("div",ft,[a(De,{"current-page":C.value,"onUpdate:currentPage":e[3]||(e[3]=t=>C.value=t),"page-size":re,total:E.value,layout:"prev, pager, next, jumper, ->, total",onCurrentChange:O},null,8,["current-page","total"]),p("div",mt,"第 "+f(C.value)+" / "+f(Ce.value)+" 页",1)])]),_:1}),a(x,{label:"封禁管理",name:"bans"},{default:l(()=>[p("div",vt,[a(s,{type:"primary",onClick:e[4]||(e[4]=t=>H())},{default:l(()=>[...e[23]||(e[23]=[i("手动封禁",-1)])]),_:1})]),a(se,{modelValue:ce.value,"onUpdate:modelValue":e[5]||(e[5]=t=>ce.value=t),class:"inner-tabs"},{default:l(()=>[a(x,{label:"IP黑名单",name:"ips"},{default:l(()=>[p("div",_t,[te((c(),b(Y,{data:de.value,style:{width:"100%"}},{default:l(()=>[a(r,{label:"IP",width:"180"},{default:l(({row:t})=>[a(W,{type:"primary",underline:!1,onClick:j=>ge(t.ip)},{default:l(()=>[i(f(t.ip||"-"),1)]),_:2},1032,["onClick"])]),_:1}),a(r,{prop:"reason",label:"原因","min-width":"260"}),a(r,{label:"过期时间",width:"190"},{default:l(({row:t})=>[i(f(fe(t.expires_at)),1)]),_:1}),a(r,{label:"操作",width:"120",fixed:"right"},{default:l(({row:t})=>[a(s,{size:"small",type:"danger",plain:"",onClick:j=>ye(t.ip)},{default:l(()=>[...e[24]||(e[24]=[i("解除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[Z,$.value]])])]),_:1}),a(x,{label:"用户黑名单",name:"users"},{default:l(()=>[p("div",yt,[te((c(),b(Y,{data:pe.value,style:{width:"100%"}},{default:l(()=>[a(r,{label:"用户ID",width:"180"},{default:l(({row:t})=>[a(W,{type:"primary",underline:!1,onClick:j=>ke(t.user_id)},{default:l(()=>[i(f(t.user_id??"-"),1)]),_:2},1032,["onClick"])]),_:1}),a(r,{prop:"reason",label:"原因","min-width":"260"}),a(r,{label:"过期时间",width:"190"},{default:l(({row:t})=>[i(f(fe(t.expires_at)),1)]),_:1}),a(r,{label:"操作",width:"120",fixed:"right"},{default:l(({row:t})=>[a(s,{size:"small",type:"danger",plain:"",onClick:j=>be(t.user_id)},{default:l(()=>[...e[25]||(e[25]=[i("解除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[Z,$.value]])])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(x,{label:"风险查询",name:"risk"},{default:l(()=>[a(se,{modelValue:z.value,"onUpdate:modelValue":e[8]||(e[8]=t=>z.value=t),class:"inner-tabs"},{default:l(()=>[a(x,{label:"IP查询",name:"ip"},{default:l(()=>[p("div",bt,[a(M,{modelValue:A.value,"onUpdate:modelValue":e[6]||(e[6]=t=>A.value=t),placeholder:"输入IP,如 1.2.3.4",style:{width:"260px"},clearable:""},null,8,["modelValue"]),a(s,{type:"primary",loading:k.value,onClick:J},{default:l(()=>[...e[26]||(e[26]=[i("查询",-1)])]),_:1},8,["loading"])])]),_:1}),a(x,{label:"用户查询",name:"user"},{default:l(()=>[p("div",gt,[a(M,{modelValue:G.value,"onUpdate:modelValue":e[7]||(e[7]=t=>G.value=t),placeholder:"输入用户ID,如 123",style:{width:"260px"},clearable:""},null,8,["modelValue"]),a(s,{type:"primary",loading:k.value,onClick:ne},{default:l(()=>[...e[27]||(e[27]=[i("查询",-1)])]),_:1},8,["loading"])])]),_:1})]),_:1},8,["modelValue"]),_.value?(c(),b(he,{key:0,shadow:"never","body-style":{padding:"16px"},class:"sub-card"},{default:l(()=>[p("div",kt,[p("div",ht,[h.value==="ip"?(c(),P("strong",wt,"IP: "+f(_.value.ip),1)):(c(),P("strong",It,"用户ID: "+f(_.value.user_id),1)),e[30]||(e[30]=p("span",{class:"app-muted"},"风险分",-1)),a(I,{type:D(_.value.risk_score).type,effect:"light"},{default:l(()=>[i(f(_.value.risk_score??0),1)]),_:1},8,["type"]),_.value.is_banned?(c(),b(I,{key:2,type:"danger",effect:"light"},{default:l(()=>[...e[28]||(e[28]=[i("已封禁",-1)])]),_:1})):(c(),b(I,{key:3,type:"success",effect:"light"},{default:l(()=>[...e[29]||(e[29]=[i("未封禁",-1)])]),_:1}))]),p("div",Vt,[_.value.is_banned?(c(),b(s,{key:1,type:"danger",plain:"",onClick:Be},{default:l(()=>[...e[32]||(e[32]=[i("解除封禁",-1)])]),_:1})):(c(),b(s,{key:0,type:"primary",plain:"",onClick:Ue},{default:l(()=>[...e[31]||(e[31]=[i("封禁",-1)])]),_:1})),h.value==="ip"?(c(),b(s,{key:2,type:"warning",plain:"",loading:k.value,onClick:$e},{default:l(()=>[...e[33]||(e[33]=[i(" 清除风险分 ",-1)])]),_:1},8,["loading"])):ue("",!0)])]),p("div",xt,[te((c(),b(Y,{data:_.value.threat_history||[],style:{width:"100%"}},{default:l(()=>[a(r,{prop:"created_at",label:"时间",width:"180"}),a(r,{label:"类型",width:"170"},{default:l(({row:t})=>[a(I,{effect:"light",type:"info"},{default:l(()=>[i(f(t.threat_type||"unknown"),1)]),_:2},1024)]),_:1}),a(r,{label:"严重程度",width:"120"},{default:l(({row:t})=>[a(I,{type:D(t.score).type,effect:"light"},{default:l(()=>[i(f(D(t.score).label)+" ("+f(t.score??0)+") ",1)]),_:2},1032,["type"])]),_:1}),a(r,{label:"操作路径","min-width":"220"},{default:l(({row:t})=>[a(X,{content:K(t),placement:"top","show-after":300},{default:l(()=>[p("span",Ct,f(K(t)),1)]),_:2},1032,["content"])]),_:1}),a(r,{label:"Payload预览","min-width":"240"},{default:l(({row:t})=>[a(X,{content:me(t),placement:"top","show-after":300},{default:l(()=>[p("span",St,f(t.value_preview||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[Z,k.value]])])]),_:1})):ue("",!0)]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(Le,{modelValue:S.value,"onUpdate:modelValue":e[17]||(e[17]=t=>S.value=t),title:"手动封禁",width:"min(520px, 92vw)",onClosed:_e},{footer:l(()=>[p("div",Tt,[e[38]||(e[38]=p("div",{class:"spacer"},null,-1)),a(s,{onClick:e[16]||(e[16]=t=>S.value=!1)},{default:l(()=>[...e[36]||(e[36]=[i("取消",-1)])]),_:1}),a(s,{type:"primary",loading:R.value,onClick:Pe},{default:l(()=>[...e[37]||(e[37]=[i("确认封禁",-1)])]),_:1},8,["loading"])])]),default:l(()=>[a(je,{"label-width":"120px"},{default:l(()=>[a(T,{label:"类型"},{default:l(()=>[a(Ne,{modelValue:o.value.kind,"onUpdate:modelValue":e[10]||(e[10]=t=>o.value.kind=t)},{default:l(()=>[a(we,{label:"ip"},{default:l(()=>[...e[34]||(e[34]=[i("IP",-1)])]),_:1}),a(we,{label:"user"},{default:l(()=>[...e[35]||(e[35]=[i("用户",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.value.kind==="ip"?(c(),b(T,{key:0,label:"IP"},{default:l(()=>[a(M,{modelValue:o.value.ip,"onUpdate:modelValue":e[11]||(e[11]=t=>o.value.ip=t),placeholder:"例如 1.2.3.4"},null,8,["modelValue"])]),_:1})):(c(),b(T,{key:1,label:"用户ID"},{default:l(()=>[a(M,{modelValue:o.value.user_id,"onUpdate:modelValue":e[12]||(e[12]=t=>o.value.user_id=t),placeholder:"例如 123"},null,8,["modelValue"])]),_:1})),a(T,{label:"原因"},{default:l(()=>[a(M,{modelValue:o.value.reason,"onUpdate:modelValue":e[13]||(e[13]=t=>o.value.reason=t),type:"textarea",rows:3,placeholder:"请输入封禁原因"},null,8,["modelValue"])]),_:1}),a(T,{label:"永久封禁"},{default:l(()=>[a(Fe,{modelValue:o.value.permanent,"onUpdate:modelValue":e[14]||(e[14]=t=>o.value.permanent=t)},null,8,["modelValue"])]),_:1}),o.value.permanent?ue("",!0):(c(),b(T,{key:2,label:"持续(小时)"},{default:l(()=>[a(Me,{modelValue:o.value.duration_hours,"onUpdate:modelValue":e[15]||(e[15]=t=>o.value.duration_hours=t),min:1,max:8760},null,8,["modelValue"])]),_:1}))]),_:1})]),_:1},8,["modelValue"])])}}},Ft=qe(Pt,[["__scopeId","data-v-94e0bde6"]]);export{Ft as default}; +该操作不会影响仍在有效期内的封禁。`,"清理过期记录",{confirmButtonText:"清理",cancelButtonText:"取消",type:"warning"})}catch{return}ie.value=!0;try{await lt(),w.success("清理完成"),await ve()}catch{}finally{ie.value=!1}}return ze(async()=>{await ve()}),(n,e)=>{const s=v("el-button"),y=v("el-option"),Q=v("el-select"),r=v("el-table-column"),I=v("el-tag"),W=v("el-link"),X=v("el-tooltip"),Y=v("el-table"),De=v("el-pagination"),x=v("el-tab-pane"),se=v("el-tabs"),M=v("el-input"),he=v("el-card"),we=v("el-radio-button"),Ne=v("el-radio-group"),T=v("el-form-item"),Fe=v("el-switch"),Me=v("el-input-number"),je=v("el-form"),Le=v("el-dialog"),Z=Ae("loading");return c(),P("div",nt,[p("div",it,[e[20]||(e[20]=p("h2",null,"安全防护",-1)),p("div",st,[a(s,{type:"warning",plain:"",loading:ie.value,onClick:Re},{default:l(()=>[...e[18]||(e[18]=[i("清理过期记录",-1)])]),_:1},8,["loading"]),a(s,{type:"primary",onClick:e[0]||(e[0]=t=>H())},{default:l(()=>[...e[19]||(e[19]=[i("手动封禁",-1)])]),_:1})])]),a(Ee,{items:xe.value,loading:V.value,"min-width":220},null,8,["items","loading"]),a(he,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:l(()=>[a(se,{modelValue:m.value,"onUpdate:modelValue":e[9]||(e[9]=t=>m.value=t)},{default:l(()=>[a(x,{label:"威胁事件",name:"threats"},{default:l(()=>[p("div",ot,[a(Q,{modelValue:U.value,"onUpdate:modelValue":e[1]||(e[1]=t=>U.value=t),placeholder:"类型",style:{width:"220px"},filterable:"",clearable:"","allow-create":"","default-first-option":""},{default:l(()=>[a(y,{label:"全部",value:""}),(c(!0),P(Ge,null,Ke(Ve.value,t=>(c(),b(y,{key:t.value,label:t.label,value:t.value},null,8,["label","value"]))),128))]),_:1},8,["modelValue"]),a(Q,{modelValue:B.value,"onUpdate:modelValue":e[2]||(e[2]=t=>B.value=t),placeholder:"严重程度",style:{width:"200px"},clearable:""},{default:l(()=>[a(y,{label:"全部",value:""}),a(y,{label:"高风险(>=80)",value:"high"}),a(y,{label:"中风险(50-79)",value:"medium"}),a(y,{label:"低风险(<50)",value:"low"})]),_:1},8,["modelValue"]),a(s,{type:"primary",onClick:Se},{default:l(()=>[...e[21]||(e[21]=[i("筛选",-1)])]),_:1}),a(s,{onClick:Te},{default:l(()=>[...e[22]||(e[22]=[i("重置",-1)])]),_:1})]),p("div",ut,[te((c(),b(Y,{data:q.value,style:{width:"100%"}},{default:l(()=>[a(r,{prop:"created_at",label:"时间",width:"180"}),a(r,{label:"类型",width:"170"},{default:l(({row:t})=>[a(I,{effect:"light",type:"info"},{default:l(()=>[i(f(t.threat_type||"unknown"),1)]),_:2},1024)]),_:1}),a(r,{label:"严重程度",width:"120"},{default:l(({row:t})=>[a(I,{type:D(t.score).type,effect:"light"},{default:l(()=>[i(f(D(t.score).label)+" ("+f(t.score??0)+") ",1)]),_:2},1032,["type"])]),_:1}),a(r,{label:"IP",width:"150"},{default:l(({row:t})=>[t.ip?(c(),b(W,{key:0,type:"primary",underline:!1,onClick:j=>ge(t.ip)},{default:l(()=>[i(f(t.ip),1)]),_:2},1032,["onClick"])):(c(),P("span",rt,"-"))]),_:1}),a(r,{label:"用户",width:"120"},{default:l(({row:t})=>[t.user_id!==null&&t.user_id!==void 0?(c(),b(W,{key:0,type:"primary",underline:!1,onClick:j=>ke(t.user_id)},{default:l(()=>[i(f(t.user_id),1)]),_:2},1032,["onClick"])):(c(),P("span",dt,"-"))]),_:1}),a(r,{label:"操作路径","min-width":"220"},{default:l(({row:t})=>[a(X,{content:K(t),placement:"top","show-after":300},{default:l(()=>[p("span",pt,f(K(t)),1)]),_:2},1032,["content"])]),_:1}),a(r,{label:"Payload预览","min-width":"240"},{default:l(({row:t})=>[a(X,{content:me(t),placement:"top","show-after":300},{default:l(()=>[p("span",ct,f(t.value_preview||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[Z,ae.value]])]),p("div",ft,[a(De,{"current-page":C.value,"onUpdate:currentPage":e[3]||(e[3]=t=>C.value=t),"page-size":re,total:E.value,layout:"prev, pager, next, jumper, ->, total",onCurrentChange:O},null,8,["current-page","total"]),p("div",mt,"第 "+f(C.value)+" / "+f(Ce.value)+" 页",1)])]),_:1}),a(x,{label:"封禁管理",name:"bans"},{default:l(()=>[p("div",vt,[a(s,{type:"primary",onClick:e[4]||(e[4]=t=>H())},{default:l(()=>[...e[23]||(e[23]=[i("手动封禁",-1)])]),_:1})]),a(se,{modelValue:ce.value,"onUpdate:modelValue":e[5]||(e[5]=t=>ce.value=t),class:"inner-tabs"},{default:l(()=>[a(x,{label:"IP黑名单",name:"ips"},{default:l(()=>[p("div",_t,[te((c(),b(Y,{data:de.value,style:{width:"100%"}},{default:l(()=>[a(r,{label:"IP",width:"180"},{default:l(({row:t})=>[a(W,{type:"primary",underline:!1,onClick:j=>ge(t.ip)},{default:l(()=>[i(f(t.ip||"-"),1)]),_:2},1032,["onClick"])]),_:1}),a(r,{prop:"reason",label:"原因","min-width":"260"}),a(r,{label:"过期时间",width:"190"},{default:l(({row:t})=>[i(f(fe(t.expires_at)),1)]),_:1}),a(r,{label:"操作",width:"120",fixed:"right"},{default:l(({row:t})=>[a(s,{size:"small",type:"danger",plain:"",onClick:j=>ye(t.ip)},{default:l(()=>[...e[24]||(e[24]=[i("解除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[Z,$.value]])])]),_:1}),a(x,{label:"用户黑名单",name:"users"},{default:l(()=>[p("div",yt,[te((c(),b(Y,{data:pe.value,style:{width:"100%"}},{default:l(()=>[a(r,{label:"用户ID",width:"180"},{default:l(({row:t})=>[a(W,{type:"primary",underline:!1,onClick:j=>ke(t.user_id)},{default:l(()=>[i(f(t.user_id??"-"),1)]),_:2},1032,["onClick"])]),_:1}),a(r,{prop:"reason",label:"原因","min-width":"260"}),a(r,{label:"过期时间",width:"190"},{default:l(({row:t})=>[i(f(fe(t.expires_at)),1)]),_:1}),a(r,{label:"操作",width:"120",fixed:"right"},{default:l(({row:t})=>[a(s,{size:"small",type:"danger",plain:"",onClick:j=>be(t.user_id)},{default:l(()=>[...e[25]||(e[25]=[i("解除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[Z,$.value]])])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(x,{label:"风险查询",name:"risk"},{default:l(()=>[a(se,{modelValue:z.value,"onUpdate:modelValue":e[8]||(e[8]=t=>z.value=t),class:"inner-tabs"},{default:l(()=>[a(x,{label:"IP查询",name:"ip"},{default:l(()=>[p("div",bt,[a(M,{modelValue:A.value,"onUpdate:modelValue":e[6]||(e[6]=t=>A.value=t),placeholder:"输入IP,如 1.2.3.4",style:{width:"260px"},clearable:""},null,8,["modelValue"]),a(s,{type:"primary",loading:k.value,onClick:J},{default:l(()=>[...e[26]||(e[26]=[i("查询",-1)])]),_:1},8,["loading"])])]),_:1}),a(x,{label:"用户查询",name:"user"},{default:l(()=>[p("div",gt,[a(M,{modelValue:G.value,"onUpdate:modelValue":e[7]||(e[7]=t=>G.value=t),placeholder:"输入用户ID,如 123",style:{width:"260px"},clearable:""},null,8,["modelValue"]),a(s,{type:"primary",loading:k.value,onClick:ne},{default:l(()=>[...e[27]||(e[27]=[i("查询",-1)])]),_:1},8,["loading"])])]),_:1})]),_:1},8,["modelValue"]),_.value?(c(),b(he,{key:0,shadow:"never","body-style":{padding:"16px"},class:"sub-card"},{default:l(()=>[p("div",kt,[p("div",ht,[h.value==="ip"?(c(),P("strong",wt,"IP: "+f(_.value.ip),1)):(c(),P("strong",It,"用户ID: "+f(_.value.user_id),1)),e[30]||(e[30]=p("span",{class:"app-muted"},"风险分",-1)),a(I,{type:D(_.value.risk_score).type,effect:"light"},{default:l(()=>[i(f(_.value.risk_score??0),1)]),_:1},8,["type"]),_.value.is_banned?(c(),b(I,{key:2,type:"danger",effect:"light"},{default:l(()=>[...e[28]||(e[28]=[i("已封禁",-1)])]),_:1})):(c(),b(I,{key:3,type:"success",effect:"light"},{default:l(()=>[...e[29]||(e[29]=[i("未封禁",-1)])]),_:1}))]),p("div",Vt,[_.value.is_banned?(c(),b(s,{key:1,type:"danger",plain:"",onClick:Be},{default:l(()=>[...e[32]||(e[32]=[i("解除封禁",-1)])]),_:1})):(c(),b(s,{key:0,type:"primary",plain:"",onClick:Ue},{default:l(()=>[...e[31]||(e[31]=[i("封禁",-1)])]),_:1})),h.value==="ip"?(c(),b(s,{key:2,type:"warning",plain:"",loading:k.value,onClick:$e},{default:l(()=>[...e[33]||(e[33]=[i(" 清除风险分 ",-1)])]),_:1},8,["loading"])):ue("",!0)])]),p("div",xt,[te((c(),b(Y,{data:_.value.threat_history||[],style:{width:"100%"}},{default:l(()=>[a(r,{prop:"created_at",label:"时间",width:"180"}),a(r,{label:"类型",width:"170"},{default:l(({row:t})=>[a(I,{effect:"light",type:"info"},{default:l(()=>[i(f(t.threat_type||"unknown"),1)]),_:2},1024)]),_:1}),a(r,{label:"严重程度",width:"120"},{default:l(({row:t})=>[a(I,{type:D(t.score).type,effect:"light"},{default:l(()=>[i(f(D(t.score).label)+" ("+f(t.score??0)+") ",1)]),_:2},1032,["type"])]),_:1}),a(r,{label:"操作路径","min-width":"220"},{default:l(({row:t})=>[a(X,{content:K(t),placement:"top","show-after":300},{default:l(()=>[p("span",Ct,f(K(t)),1)]),_:2},1032,["content"])]),_:1}),a(r,{label:"Payload预览","min-width":"240"},{default:l(({row:t})=>[a(X,{content:me(t),placement:"top","show-after":300},{default:l(()=>[p("span",St,f(t.value_preview||"-"),1)]),_:2},1032,["content"])]),_:1})]),_:1},8,["data"])),[[Z,k.value]])])]),_:1})):ue("",!0)]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(Le,{modelValue:S.value,"onUpdate:modelValue":e[17]||(e[17]=t=>S.value=t),title:"手动封禁",width:"min(520px, 92vw)",onClosed:_e},{footer:l(()=>[p("div",Tt,[e[38]||(e[38]=p("div",{class:"spacer"},null,-1)),a(s,{onClick:e[16]||(e[16]=t=>S.value=!1)},{default:l(()=>[...e[36]||(e[36]=[i("取消",-1)])]),_:1}),a(s,{type:"primary",loading:R.value,onClick:Pe},{default:l(()=>[...e[37]||(e[37]=[i("确认封禁",-1)])]),_:1},8,["loading"])])]),default:l(()=>[a(je,{"label-width":"120px"},{default:l(()=>[a(T,{label:"类型"},{default:l(()=>[a(Ne,{modelValue:o.value.kind,"onUpdate:modelValue":e[10]||(e[10]=t=>o.value.kind=t)},{default:l(()=>[a(we,{label:"ip"},{default:l(()=>[...e[34]||(e[34]=[i("IP",-1)])]),_:1}),a(we,{label:"user"},{default:l(()=>[...e[35]||(e[35]=[i("用户",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.value.kind==="ip"?(c(),b(T,{key:0,label:"IP"},{default:l(()=>[a(M,{modelValue:o.value.ip,"onUpdate:modelValue":e[11]||(e[11]=t=>o.value.ip=t),placeholder:"例如 1.2.3.4"},null,8,["modelValue"])]),_:1})):(c(),b(T,{key:1,label:"用户ID"},{default:l(()=>[a(M,{modelValue:o.value.user_id,"onUpdate:modelValue":e[12]||(e[12]=t=>o.value.user_id=t),placeholder:"例如 123"},null,8,["modelValue"])]),_:1})),a(T,{label:"原因"},{default:l(()=>[a(M,{modelValue:o.value.reason,"onUpdate:modelValue":e[13]||(e[13]=t=>o.value.reason=t),type:"textarea",rows:3,placeholder:"请输入封禁原因"},null,8,["modelValue"])]),_:1}),a(T,{label:"永久封禁"},{default:l(()=>[a(Fe,{modelValue:o.value.permanent,"onUpdate:modelValue":e[14]||(e[14]=t=>o.value.permanent=t)},null,8,["modelValue"])]),_:1}),o.value.permanent?ue("",!0):(c(),b(T,{key:2,label:"持续(小时)"},{default:l(()=>[a(Me,{modelValue:o.value.duration_hours,"onUpdate:modelValue":e[15]||(e[15]=t=>o.value.duration_hours=t),min:1,max:8760},null,8,["modelValue"])]),_:1}))]),_:1})]),_:1},8,["modelValue"])])}}},Ft=qe(Pt,[["__scopeId","data-v-c89794eb"]]);export{Ft as default}; diff --git a/static/admin/assets/SettingsPage-BAa-Qu3q.css b/static/admin/assets/SettingsPage-BAa-Qu3q.css deleted file mode 100644 index a4f0e76..0000000 --- a/static/admin/assets/SettingsPage-BAa-Qu3q.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-fb202365]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-fb202365]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-fb202365]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-fb202365]{margin-top:10px;font-size:12px;color:var(--app-muted)}.help-alert[data-v-fb202365]{margin-bottom:12px} diff --git a/static/admin/assets/SettingsPage-D-iYz1zh.css b/static/admin/assets/SettingsPage-D-iYz1zh.css new file mode 100644 index 0000000..fa64dc3 --- /dev/null +++ b/static/admin/assets/SettingsPage-D-iYz1zh.css @@ -0,0 +1 @@ +.page-stack[data-v-1418d488]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-1418d488]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-1418d488]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-1418d488]{margin-top:10px;font-size:12px;color:var(--app-muted)}.help-alert[data-v-1418d488]{margin-bottom:12px} diff --git a/static/admin/assets/SettingsPage-DRqlQLxJ.js b/static/admin/assets/SettingsPage-DF5fL8gq.js similarity index 98% rename from static/admin/assets/SettingsPage-DRqlQLxJ.js rename to static/admin/assets/SettingsPage-DF5fL8gq.js index 415e6da..2f2ad25 100644 --- a/static/admin/assets/SettingsPage-DRqlQLxJ.js +++ b/static/admin/assets/SettingsPage-DF5fL8gq.js @@ -1 +1 @@ -import{a as v,_ as L}from"./index-C1f9ticl.js";import{a as u,E as O}from"./vendor-element-B5S5pUKo.js";import{r as p,o as Y,aj as m,ap as Z,n as N,q as C,t as b,L as r,E as o,I as S,F as Q,D as K}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function W(t){const{data:e}=await v.put("/admin/username",{new_username:t});return e}async function ee(t={}){const e=String(t.currentPassword||""),n=String(t.newPassword||""),{data:l}=await v.put("/admin/password",{current_password:e,new_password:n});return l}async function te(){const{data:t}=await v.post("/logout");return t}async function ae(){const{data:t}=await v.get("/admin/passkeys");return t}async function $(t={}){const{data:e}=await v.post("/admin/passkeys/register/options",t);return e}async function ne(t={}){const{data:e}=await v.post("/admin/passkeys/register/verify",t);return e}async function se(t){const{data:e}=await v.delete(`/admin/passkeys/${t}`);return e}async function re(t={}){const{data:e}=await v.post("/admin/passkeys/client-error",t);return e}function le(t){if(!t||typeof t!="object")throw new Error("Passkey参数无效");return t.publicKey&&typeof t.publicKey=="object"?t.publicKey:t}function D(t){const e=String(t||""),n="=".repeat((4-e.length%4)%4),l=(e+n).replace(/-/g,"+").replace(/_/g,"/"),y=window.atob(l),i=new Uint8Array(y.length);for(let w=0;w({...l,id:D(l.id)}))),n}function ie(t){if(!t)return null;const e=t.response||{},n={id:t.id,rawId:h(t.rawId),type:t.type,authenticatorAttachment:t.authenticatorAttachment||void 0,response:{}};return e.clientDataJSON&&(n.response.clientDataJSON=h(e.clientDataJSON)),e.attestationObject&&(n.response.attestationObject=h(e.attestationObject)),e.authenticatorData&&(n.response.authenticatorData=h(e.authenticatorData)),e.signature&&(n.response.signature=h(e.signature)),e.userHandle?n.response.userHandle=h(e.userHandle):n.response.userHandle=null,typeof e.getTransports=="function"&&(n.response.transports=e.getTransports()||[]),n}function ue(){return typeof window<"u"&&window.isSecureContext&&!!window.PublicKeyCredential&&!!navigator.credentials}function de(){const t=String(window?.navigator?.userAgent||"");return/MiuiBrowser|XiaoMi\/MiuiBrowser/i.test(t)}function ce(t,e="Passkey操作"){const n=String(t?.name||"").trim(),l=String(t?.message||"").trim();return n==="NotAllowedError"?`${e}未完成(可能已取消、超时或设备未响应)`:n==="NotReadableError"?/credential manager/i.test(l)&&de()?"当前小米浏览器与系统凭据管理器兼容性较差,请改用系统 Chrome 或 Edge 后重试。":/credential manager/i.test(l)?"系统凭据管理器返回异常,请确认已设置系统锁屏并改用系统 Chrome/Edge 后重试。":l||`${e}失败(设备读取异常)`:n==="SecurityError"?"当前环境安全策略不满足 Passkey 要求,请确认使用 HTTPS 且证书有效。":l||`${e}失败`}async function pe(t){const e=oe(t),n=await navigator.credentials.create({publicKey:e});return ie(n)}const fe={class:"page-stack"},me=24e4,ye={__name:"SettingsPage",setup(t){const e=p(""),n=p(""),l=p(""),y=p(""),i=p(!1),w=p(!1),V=p(!1),A=p(""),k=p([]),g=p(null),_=p(0);function H(s){const a=String(s||"");return a.length<8?{ok:!1,message:"密码长度至少8位"}:a.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(a)||!/\d/.test(a)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function U(){try{await te()}catch{}finally{window.location.href="/yuyx"}}async function I(){const s=e.value.trim();if(!s){u.error("请输入新用户名");return}try{await O.confirm(`确定将管理员用户名修改为「${s}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=!0;try{await W(s),u.success("用户名修改成功,请重新登录"),e.value="",setTimeout(U,1200)}catch{}finally{i.value=!1}}async function R(){const s=n.value,a=l.value,d=y.value;if(!s){u.error("请输入当前密码");return}if(!a){u.error("请输入新密码");return}const f=H(a);if(!f.ok){u.error(f.message);return}if(a!==d){u.error("两次输入的新密码不一致");return}try{await O.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=!0;try{await ee({currentPassword:s,newPassword:a}),u.success("密码修改成功,请重新登录"),n.value="",l.value="",y.value="",setTimeout(U,1200)}catch{}finally{i.value=!1}}async function T(){w.value=!0;try{const s=await ae();k.value=Array.isArray(s?.items)?s.items:[],k.value.length<3?await M():(g.value=null,_.value=0)}catch{k.value=[],g.value=null,_.value=0}finally{w.value=!1}}function j(){return!g.value||Date.now()-Number(_.value||0)>me?null:g.value}async function M(){try{const s=await $({});g.value=s,_.value=Date.now()}catch{g.value=null,_.value=0}}async function z(){if(!ue()){u.error("当前浏览器或环境不支持Passkey(需 HTTPS)");return}if(k.value.length>=3){u.error("最多可绑定3台设备");return}V.value=!0;try{let s=j();s||(s=await $({}));const a=await pe(s?.publicKey||{});await ne({credential:a,device_name:A.value.trim()}),g.value=null,_.value=0,A.value="",u.success("Passkey设备添加成功"),await T()}catch(s){try{await re({stage:"register",source:"admin-settings",name:s?.name||"",message:s?.message||"",code:s?.code||"",user_agent:navigator.userAgent||""})}catch{}g.value=null,_.value=0,await M();const d=s?.response?.data?.error||ce(s,"Passkey注册");u.error(d)}finally{V.value=!1}}async function J(s){try{await O.confirm(`确定删除设备「${s?.device_name||"未命名设备"}」吗?`,"删除Passkey设备",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await se(s.id),u.success("设备已删除"),await T()}catch(a){const d=a?.response?.data;u.error(d?.error||"删除失败")}}return Y(()=>{T()}),(s,a)=>{const d=m("el-input"),f=m("el-form-item"),E=m("el-form"),x=m("el-button"),B=m("el-card"),F=m("el-alert"),X=m("el-empty"),P=m("el-table-column"),q=m("el-table"),G=Z("loading");return C(),N("div",fe,[a[13]||(a[13]=b("div",{class:"app-page-title"},[b("h2",null,"设置"),b("span",{class:"app-muted"},"管理员账号设置")],-1)),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[6]||(a[6]=b("h3",{class:"section-title"},"修改管理员用户名",-1)),r(E,{"label-width":"120px"},{default:o(()=>[r(f,{label:"新用户名"},{default:o(()=>[r(d,{modelValue:e.value,"onUpdate:modelValue":a[0]||(a[0]=c=>e.value=c),placeholder:"输入新用户名",disabled:i.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),r(x,{type:"primary",loading:i.value,onClick:I},{default:o(()=>[...a[5]||(a[5]=[S("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[8]||(a[8]=b("h3",{class:"section-title"},"修改管理员密码",-1)),r(E,{"label-width":"120px"},{default:o(()=>[r(f,{label:"当前密码"},{default:o(()=>[r(d,{modelValue:n.value,"onUpdate:modelValue":a[1]||(a[1]=c=>n.value=c),type:"password","show-password":"",placeholder:"输入当前密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1}),r(f,{label:"新密码"},{default:o(()=>[r(d,{modelValue:l.value,"onUpdate:modelValue":a[2]||(a[2]=c=>l.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1}),r(f,{label:"确认新密码"},{default:o(()=>[r(d,{modelValue:y.value,"onUpdate:modelValue":a[3]||(a[3]=c=>y.value=c),type:"password","show-password":"",placeholder:"再次输入新密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),r(x,{type:"primary",loading:i.value,onClick:R},{default:o(()=>[...a[7]||(a[7]=[S("保存密码",-1)])]),_:1},8,["loading"]),a[9]||(a[9]=b("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1}),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[12]||(a[12]=b("h3",{class:"section-title"},"Passkey设备",-1)),r(F,{type:"info",closable:!1,title:"最多可绑定3台设备,可用于管理员无密码登录。","show-icon":"",class:"help-alert"}),r(E,{inline:""},{default:o(()=>[r(f,{label:"设备备注"},{default:o(()=>[r(d,{modelValue:A.value,"onUpdate:modelValue":a[4]||(a[4]=c=>A.value=c),placeholder:"例如:值班iPhone / 办公Mac",maxlength:"40","show-word-limit":""},null,8,["modelValue"])]),_:1}),r(f,null,{default:o(()=>[r(x,{type:"primary",loading:V.value,onClick:z},{default:o(()=>[...a[10]||(a[10]=[S("添加Passkey设备",-1)])]),_:1},8,["loading"])]),_:1})]),_:1}),Q((C(),N("div",null,[k.value.length===0?(C(),K(X,{key:0,description:"暂无Passkey设备"})):(C(),K(q,{key:1,data:k.value,size:"small",style:{width:"100%"}},{default:o(()=>[r(P,{prop:"device_name",label:"设备备注","min-width":"160"}),r(P,{prop:"credential_id_preview",label:"凭据ID","min-width":"180"}),r(P,{prop:"last_used_at",label:"最近使用","min-width":"140"}),r(P,{prop:"created_at",label:"创建时间","min-width":"140"}),r(P,{label:"操作",width:"100",fixed:"right"},{default:o(({row:c})=>[r(x,{type:"danger",text:"",onClick:we=>J(c)},{default:o(()=>[...a[11]||(a[11]=[S("删除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"]))])),[[G,w.value]])]),_:1})])}}},he=L(ye,[["__scopeId","data-v-fb202365"]]);export{he as default}; +import{a as v,_ as L}from"./index-DOvMEmc8.js";import{a as u,E as O}from"./vendor-element-B5S5pUKo.js";import{r as p,o as Y,aj as m,ap as Z,n as N,q as C,t as b,L as r,E as o,I as S,F as Q,D as K}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function W(t){const{data:e}=await v.put("/admin/username",{new_username:t});return e}async function ee(t={}){const e=String(t.currentPassword||""),n=String(t.newPassword||""),{data:l}=await v.put("/admin/password",{current_password:e,new_password:n});return l}async function te(){const{data:t}=await v.post("/logout");return t}async function ae(){const{data:t}=await v.get("/admin/passkeys");return t}async function $(t={}){const{data:e}=await v.post("/admin/passkeys/register/options",t);return e}async function ne(t={}){const{data:e}=await v.post("/admin/passkeys/register/verify",t);return e}async function se(t){const{data:e}=await v.delete(`/admin/passkeys/${t}`);return e}async function re(t={}){const{data:e}=await v.post("/admin/passkeys/client-error",t);return e}function le(t){if(!t||typeof t!="object")throw new Error("Passkey参数无效");return t.publicKey&&typeof t.publicKey=="object"?t.publicKey:t}function D(t){const e=String(t||""),n="=".repeat((4-e.length%4)%4),l=(e+n).replace(/-/g,"+").replace(/_/g,"/"),y=window.atob(l),i=new Uint8Array(y.length);for(let w=0;w({...l,id:D(l.id)}))),n}function ie(t){if(!t)return null;const e=t.response||{},n={id:t.id,rawId:h(t.rawId),type:t.type,authenticatorAttachment:t.authenticatorAttachment||void 0,response:{}};return e.clientDataJSON&&(n.response.clientDataJSON=h(e.clientDataJSON)),e.attestationObject&&(n.response.attestationObject=h(e.attestationObject)),e.authenticatorData&&(n.response.authenticatorData=h(e.authenticatorData)),e.signature&&(n.response.signature=h(e.signature)),e.userHandle?n.response.userHandle=h(e.userHandle):n.response.userHandle=null,typeof e.getTransports=="function"&&(n.response.transports=e.getTransports()||[]),n}function ue(){return typeof window<"u"&&window.isSecureContext&&!!window.PublicKeyCredential&&!!navigator.credentials}function de(){const t=String(window?.navigator?.userAgent||"");return/MiuiBrowser|XiaoMi\/MiuiBrowser/i.test(t)}function ce(t,e="Passkey操作"){const n=String(t?.name||"").trim(),l=String(t?.message||"").trim();return n==="NotAllowedError"?`${e}未完成(可能已取消、超时或设备未响应)`:n==="NotReadableError"?/credential manager/i.test(l)&&de()?"当前小米浏览器与系统凭据管理器兼容性较差,请改用系统 Chrome 或 Edge 后重试。":/credential manager/i.test(l)?"系统凭据管理器返回异常,请确认已设置系统锁屏并改用系统 Chrome/Edge 后重试。":l||`${e}失败(设备读取异常)`:n==="SecurityError"?"当前环境安全策略不满足 Passkey 要求,请确认使用 HTTPS 且证书有效。":l||`${e}失败`}async function pe(t){const e=oe(t),n=await navigator.credentials.create({publicKey:e});return ie(n)}const fe={class:"page-stack"},me=24e4,ye={__name:"SettingsPage",setup(t){const e=p(""),n=p(""),l=p(""),y=p(""),i=p(!1),w=p(!1),V=p(!1),A=p(""),k=p([]),g=p(null),_=p(0);function H(s){const a=String(s||"");return a.length<8?{ok:!1,message:"密码长度至少8位"}:a.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(a)||!/\d/.test(a)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function U(){try{await te()}catch{}finally{window.location.href="/yuyx"}}async function I(){const s=e.value.trim();if(!s){u.error("请输入新用户名");return}try{await O.confirm(`确定将管理员用户名修改为「${s}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=!0;try{await W(s),u.success("用户名修改成功,请重新登录"),e.value="",setTimeout(U,1200)}catch{}finally{i.value=!1}}async function R(){const s=n.value,a=l.value,d=y.value;if(!s){u.error("请输入当前密码");return}if(!a){u.error("请输入新密码");return}const f=H(a);if(!f.ok){u.error(f.message);return}if(a!==d){u.error("两次输入的新密码不一致");return}try{await O.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=!0;try{await ee({currentPassword:s,newPassword:a}),u.success("密码修改成功,请重新登录"),n.value="",l.value="",y.value="",setTimeout(U,1200)}catch{}finally{i.value=!1}}async function T(){w.value=!0;try{const s=await ae();k.value=Array.isArray(s?.items)?s.items:[],k.value.length<3?await M():(g.value=null,_.value=0)}catch{k.value=[],g.value=null,_.value=0}finally{w.value=!1}}function j(){return!g.value||Date.now()-Number(_.value||0)>me?null:g.value}async function M(){try{const s=await $({});g.value=s,_.value=Date.now()}catch{g.value=null,_.value=0}}async function z(){if(!ue()){u.error("当前浏览器或环境不支持Passkey(需 HTTPS)");return}if(k.value.length>=3){u.error("最多可绑定3台设备");return}V.value=!0;try{let s=j();s||(s=await $({}));const a=await pe(s?.publicKey||{});await ne({credential:a,device_name:A.value.trim()}),g.value=null,_.value=0,A.value="",u.success("Passkey设备添加成功"),await T()}catch(s){try{await re({stage:"register",source:"admin-settings",name:s?.name||"",message:s?.message||"",code:s?.code||"",user_agent:navigator.userAgent||""})}catch{}g.value=null,_.value=0,await M();const d=s?.response?.data?.error||ce(s,"Passkey注册");u.error(d)}finally{V.value=!1}}async function J(s){try{await O.confirm(`确定删除设备「${s?.device_name||"未命名设备"}」吗?`,"删除Passkey设备",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await se(s.id),u.success("设备已删除"),await T()}catch(a){const d=a?.response?.data;u.error(d?.error||"删除失败")}}return Y(()=>{T()}),(s,a)=>{const d=m("el-input"),f=m("el-form-item"),E=m("el-form"),x=m("el-button"),B=m("el-card"),F=m("el-alert"),X=m("el-empty"),P=m("el-table-column"),q=m("el-table"),G=Z("loading");return C(),N("div",fe,[a[13]||(a[13]=b("div",{class:"app-page-title"},[b("h2",null,"设置"),b("span",{class:"app-muted"},"管理员账号设置")],-1)),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[6]||(a[6]=b("h3",{class:"section-title"},"修改管理员用户名",-1)),r(E,{"label-width":"120px"},{default:o(()=>[r(f,{label:"新用户名"},{default:o(()=>[r(d,{modelValue:e.value,"onUpdate:modelValue":a[0]||(a[0]=c=>e.value=c),placeholder:"输入新用户名",disabled:i.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),r(x,{type:"primary",loading:i.value,onClick:I},{default:o(()=>[...a[5]||(a[5]=[S("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[8]||(a[8]=b("h3",{class:"section-title"},"修改管理员密码",-1)),r(E,{"label-width":"120px"},{default:o(()=>[r(f,{label:"当前密码"},{default:o(()=>[r(d,{modelValue:n.value,"onUpdate:modelValue":a[1]||(a[1]=c=>n.value=c),type:"password","show-password":"",placeholder:"输入当前密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1}),r(f,{label:"新密码"},{default:o(()=>[r(d,{modelValue:l.value,"onUpdate:modelValue":a[2]||(a[2]=c=>l.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1}),r(f,{label:"确认新密码"},{default:o(()=>[r(d,{modelValue:y.value,"onUpdate:modelValue":a[3]||(a[3]=c=>y.value=c),type:"password","show-password":"",placeholder:"再次输入新密码",disabled:i.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),r(x,{type:"primary",loading:i.value,onClick:R},{default:o(()=>[...a[7]||(a[7]=[S("保存密码",-1)])]),_:1},8,["loading"]),a[9]||(a[9]=b("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1}),r(B,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[a[12]||(a[12]=b("h3",{class:"section-title"},"Passkey设备",-1)),r(F,{type:"info",closable:!1,title:"最多可绑定3台设备,可用于管理员无密码登录。","show-icon":"",class:"help-alert"}),r(E,{inline:""},{default:o(()=>[r(f,{label:"设备备注"},{default:o(()=>[r(d,{modelValue:A.value,"onUpdate:modelValue":a[4]||(a[4]=c=>A.value=c),placeholder:"例如:值班iPhone / 办公Mac",maxlength:"40","show-word-limit":""},null,8,["modelValue"])]),_:1}),r(f,null,{default:o(()=>[r(x,{type:"primary",loading:V.value,onClick:z},{default:o(()=>[...a[10]||(a[10]=[S("添加Passkey设备",-1)])]),_:1},8,["loading"])]),_:1})]),_:1}),Q((C(),N("div",null,[k.value.length===0?(C(),K(X,{key:0,description:"暂无Passkey设备"})):(C(),K(q,{key:1,data:k.value,size:"small",style:{width:"100%"}},{default:o(()=>[r(P,{prop:"device_name",label:"设备备注","min-width":"160"}),r(P,{prop:"credential_id_preview",label:"凭据ID","min-width":"180"}),r(P,{prop:"last_used_at",label:"最近使用","min-width":"140"}),r(P,{prop:"created_at",label:"创建时间","min-width":"140"}),r(P,{label:"操作",width:"100",fixed:"right"},{default:o(({row:c})=>[r(x,{type:"danger",text:"",onClick:we=>J(c)},{default:o(()=>[...a[11]||(a[11]=[S("删除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"]))])),[[G,w.value]])]),_:1})])}}},he=L(ye,[["__scopeId","data-v-1418d488"]]);export{he as default}; diff --git a/static/admin/assets/SystemPage-BhhEz4Qz.css b/static/admin/assets/SystemPage-BhhEz4Qz.css deleted file mode 100644 index 041cecc..0000000 --- a/static/admin/assets/SystemPage-BhhEz4Qz.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-ca313705]{display:flex;flex-direction:column;gap:14px;min-width:0}.config-grid[data-v-ca313705]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card[data-v-ca313705]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-card[data-v-ca313705]{min-width:0}.section-title[data-v-ca313705]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.section-sub[data-v-ca313705]{margin-top:6px;margin-bottom:10px;font-size:12px}.section-head[data-v-ca313705]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}.status-inline[data-v-ca313705]{font-size:12px;display:inline-flex;align-items:center;gap:6px}.status-chip[data-v-ca313705]{display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:999px;font-size:12px;font-weight:700;border:1px solid transparent}.status-chip.is-checking[data-v-ca313705]{color:#1d4ed8;background:#dbeafe;border-color:#93c5fd}.status-chip.is-online[data-v-ca313705]{color:#065f46;background:#d1fae5;border-color:#6ee7b7}.status-chip.is-offline[data-v-ca313705]{color:#92400e;background:#fef3c7;border-color:#fcd34d}.status-chip.is-error[data-v-ca313705]{color:#991b1b;background:#fee2e2;border-color:#fca5a5}.status-chip.is-unknown[data-v-ca313705]{color:#374151;background:#f3f4f6;border-color:#d1d5db}.status-dots[data-v-ca313705]{display:inline-flex;align-items:center;gap:3px;margin-left:3px}.status-dots i[data-v-ca313705]{width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.25;animation:dotPulse-ca313705 1.2s infinite ease-in-out}.status-dots i[data-v-ca313705]:nth-child(2){animation-delay:.2s}.status-dots i[data-v-ca313705]:nth-child(3){animation-delay:.4s}@keyframes dotPulse-ca313705{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-1px)}}.kdocs-form[data-v-ca313705]{margin-top:6px}.kdocs-inline[data-v-ca313705]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;width:100%}.kdocs-range[data-v-ca313705]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.kdocs-qr[data-v-ca313705]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-ca313705]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-ca313705]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-ca313705]{display:flex;flex-wrap:wrap;gap:10px}@media(max-width:1200px){.config-grid[data-v-ca313705]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:768px){.config-grid[data-v-ca313705],.kdocs-inline[data-v-ca313705]{grid-template-columns:1fr}.kdocs-range[data-v-ca313705]{align-items:stretch}} diff --git a/static/admin/assets/SystemPage-CTs6qr36.css b/static/admin/assets/SystemPage-CTs6qr36.css new file mode 100644 index 0000000..e07bf65 --- /dev/null +++ b/static/admin/assets/SystemPage-CTs6qr36.css @@ -0,0 +1 @@ +.page-stack[data-v-3820d834]{display:flex;flex-direction:column;gap:14px;min-width:0}.config-grid[data-v-3820d834]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card[data-v-3820d834]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-card[data-v-3820d834]{min-width:0}.section-title[data-v-3820d834]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.section-sub[data-v-3820d834]{margin-top:6px;margin-bottom:10px;font-size:12px}.section-head[data-v-3820d834]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}.status-inline[data-v-3820d834]{font-size:12px;display:inline-flex;align-items:center;gap:6px}.status-chip[data-v-3820d834]{display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:999px;font-size:12px;font-weight:700;border:1px solid transparent}.status-chip.is-checking[data-v-3820d834]{color:#1d4ed8;background:#dbeafe;border-color:#93c5fd}.status-chip.is-online[data-v-3820d834]{color:#065f46;background:#d1fae5;border-color:#6ee7b7}.status-chip.is-offline[data-v-3820d834]{color:#92400e;background:#fef3c7;border-color:#fcd34d}.status-chip.is-error[data-v-3820d834]{color:#991b1b;background:#fee2e2;border-color:#fca5a5}.status-chip.is-unknown[data-v-3820d834]{color:#374151;background:#f3f4f6;border-color:#d1d5db}.status-dots[data-v-3820d834]{display:inline-flex;align-items:center;gap:3px;margin-left:3px}.status-dots i[data-v-3820d834]{width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.25;animation:dotPulse-3820d834 1.2s infinite ease-in-out}.status-dots i[data-v-3820d834]:nth-child(2){animation-delay:.2s}.status-dots i[data-v-3820d834]:nth-child(3){animation-delay:.4s}@keyframes dotPulse-3820d834{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-1px)}}.kdocs-form[data-v-3820d834]{margin-top:6px}.kdocs-inline[data-v-3820d834]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;width:100%}.kdocs-range[data-v-3820d834]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.kdocs-qr[data-v-3820d834]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-3820d834]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-3820d834]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-3820d834]{display:flex;flex-wrap:wrap;gap:10px}@media(max-width:1200px){.config-grid[data-v-3820d834]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1201px){.social-card[data-v-3820d834]{grid-column:span 3}}@media(max-width:768px){.config-grid[data-v-3820d834],.kdocs-inline[data-v-3820d834]{grid-template-columns:1fr}.kdocs-range[data-v-3820d834]{align-items:stretch}} diff --git a/static/admin/assets/SystemPage-D3eBPCNe.js b/static/admin/assets/SystemPage-D3eBPCNe.js deleted file mode 100644 index d1d1c0e..0000000 --- a/static/admin/assets/SystemPage-D3eBPCNe.js +++ /dev/null @@ -1,6 +0,0 @@ -import{f as Ae,u as se}from"./system-ZDPnxnIu.js";import{a as ne,_ as Ie,g as ve,h as Ne,i as Ke,u as ue,j as ce,p as De}from"./index-C1f9ticl.js";import{E as pe,a as m}from"./vendor-element-B5S5pUKo.js";import{r as u,c as ee,l as Ee,R as Le,o as Qe,aj as v,ap as Te,F as qe,q as U,n as P,t as o,L as l,E as s,I as c,G as le,y as Be,J as ae}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function Me(){const{data:g}=await ne.get("/proxy/config");return g}async function he(g){const{data:p}=await ne.post("/proxy/config",g);return p}async function $e(g){const{data:p}=await ne.post("/proxy/test",g);return p}const Re={class:"page-stack"},Fe={class:"config-grid"},ze={class:"row-actions"},He={class:"row-actions"},je={class:"row-actions"},Ge={class:"section-head"},Oe={class:"status-inline app-muted"},Je={key:0,class:"status-dots","aria-hidden":"true"},We={class:"kdocs-inline"},Xe={class:"kdocs-range"},Ye={class:"row-actions"},Ze={key:0,class:"help"},el={key:1,class:"help"},ll={class:"kdocs-qr"},al=["src"],tl={__name:"SystemPage",setup(g){const p=u(!1),A=u(2),I=u(1),N=u(3),K=u(120),S=u(!1),f=u(""),D=u(3),E=u(!1),L=u(10),Q=u(7),T=u(!1),q=u(""),B=u(""),M=u(""),h=u(0),$=u("A"),R=u("D"),F=u(0),z=u(0),H=u(!1),j=u(""),de=ve({maxAgeMs:600*1e3}),r=u(de||{}),V=u(!1),x=u(""),te=u(!1),k=u(!1),b=u(!1),w=u(!1),C=u(!de),G=u("");let O=null;const ie=ee(()=>k.value||b.value||w.value),oe=ee(()=>C.value||k.value||te.value),J=ee(()=>{if(oe.value)return"检测中";const t=r.value||{};return t?.logged_in===!0||t?.last_login_ok===!0?"已登录":t?.logged_in===!1||t?.last_login_ok===!1||t?.login_required===!0?"未登录":t?.last_error?"异常":"未知"}),fe=ee(()=>oe.value?"is-checking":J.value==="已登录"?"is-online":J.value==="未登录"?"is-offline":J.value==="异常"?"is-error":"is-unknown");function i(t){if(!t){G.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});G.value=`${t} (${e})`}async function _e(){p.value=!0;try{const[e,d]=await Promise.all([Ae(),Me()]);A.value=e.max_concurrent_global??2,I.value=e.max_concurrent_per_account??1,N.value=e.max_screenshot_concurrent??3,K.value=e.db_slow_query_ms??120,E.value=(e.auto_approve_enabled??0)===1,L.value=e.auto_approve_hourly_limit??10,Q.value=e.auto_approve_vip_days??7,S.value=(d.proxy_enabled??0)===1,f.value=d.proxy_api_url||"",D.value=d.proxy_expire_minutes??3,T.value=(e.kdocs_enabled??0)===1,q.value=e.kdocs_doc_url||"",B.value=e.kdocs_default_unit||"",M.value=e.kdocs_sheet_name||"",h.value=e.kdocs_sheet_index??0,$.value=(e.kdocs_unit_column||"A").toUpperCase(),R.value=(e.kdocs_image_column||"D").toUpperCase(),F.value=e.kdocs_row_start??0,z.value=e.kdocs_row_end??0,H.value=(e.kdocs_admin_notify_enabled??0)===1,j.value=e.kdocs_admin_notify_email||""}catch{}finally{p.value=!1}const t=ve({maxAgeMs:600*1e3});t&&(r.value=t,C.value=!1),ye()}async function ye(){if(!(C.value||k.value)){C.value=!0;try{const t=await De({force:!1,maxAgeMs:6e4,silent:!0,live:0});r.value=t||{}}catch{}finally{C.value=!1}}}async function ge(){const t={max_concurrent_global:Number(A.value),max_concurrent_per_account:Number(I.value),max_screenshot_concurrent:Number(N.value),db_slow_query_ms:Number(K.value)};try{await pe.confirm(`确定更新并发配置吗? - -全局并发数: ${t.max_concurrent_global} -单账号并发数: ${t.max_concurrent_per_account} -截图并发数: ${t.max_screenshot_concurrent} -慢 SQL 阈值: ${t.db_slow_query_ms}ms`,"保存并发配置",{confirmButtonText:"保存",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await se(t);m.success(e?.message||"并发配置已更新")}catch{}}async function Ve(){if(S.value&&!f.value.trim()){m.error("启用代理时,API地址不能为空");return}const t={proxy_enabled:S.value?1:0,proxy_api_url:f.value.trim(),proxy_expire_minutes:Number(D.value)||3};try{const e=await he(t);m.success(e?.message||"代理配置已更新")}catch{}}async function xe(){if(!f.value.trim()){m.error("请先输入代理API地址");return}try{const t=await $e({api_url:f.value.trim()});await pe.alert(t?.message||"测试完成","代理测试",{confirmButtonText:"知道了"})}catch{}}async function ke(){const t=Number(L.value),e=Number(Q.value);if(!Number.isFinite(t)||t<1){m.error("每小时注册限制必须大于0");return}if(!Number.isFinite(e)||e<0){m.error("VIP天数不能为负数");return}const d={auto_approve_enabled:E.value?1:0,auto_approve_hourly_limit:t,auto_approve_vip_days:e};try{const n=await se(d);m.success(n?.message||"注册设置已保存")}catch{}}async function be(){const t={kdocs_enabled:T.value?1:0,kdocs_doc_url:q.value.trim(),kdocs_default_unit:B.value.trim(),kdocs_sheet_name:M.value.trim(),kdocs_sheet_index:Number(h.value)||0,kdocs_unit_column:$.value.trim().toUpperCase(),kdocs_image_column:R.value.trim().toUpperCase(),kdocs_row_start:Number(F.value)||0,kdocs_row_end:Number(z.value)||0,kdocs_admin_notify_enabled:H.value?1:0,kdocs_admin_notify_email:j.value.trim()};try{const e=await se(t);m.success(e?.message||"表格配置已更新")}catch{}}async function re(){if(!k.value){k.value=!0,i("正在刷新状态");try{const t=await ce({live:1});r.value=t||{},ue(r.value),i("状态已刷新")}catch{i("刷新失败,请稍后重试")}finally{k.value=!1}}}async function me(){try{const t=await ce({live:1});r.value=t||{},ue(r.value),(t?.logged_in===!0||t?.last_login_ok===!0)&&(m.success("扫码成功,已登录"),i("扫码成功,已登录"),V.value=!1,W())}catch{}}function we(){W(),te.value=!0,i("扫码检测中"),me(),O=setInterval(me,2e3)}function W(){O&&(clearInterval(O),O=null),te.value=!1}async function Se(){if(!b.value){b.value=!0,i("正在获取二维码");try{x.value="";const t=await Ne();if(x.value=t?.qr_image||"",!x.value){if(t?.logged_in){m.success("当前已登录,无需扫码"),i("当前已登录,无需扫码"),await re();return}m.warning("未获取到二维码"),i("未获取到二维码");return}i("二维码已获取"),V.value=!0}catch{i("获取二维码失败")}finally{b.value=!1}}}async function Ce(){if(!w.value){w.value=!0,i("正在清除登录态");try{await Ke(),V.value=!1,x.value="",r.value=ue({...r.value||{},logged_in:!1,last_login_ok:!1,login_required:!0}),m.success("登录态已清除"),i("登录态已清除"),await re()}catch{i("清除登录态失败")}finally{w.value=!1}}}return Ee(V,t=>{t?we():W()}),Le(()=>{W()}),Qe(_e),(t,e)=>{const d=v("el-input-number"),n=v("el-form-item"),X=v("el-form"),_=v("el-button"),Y=v("el-card"),Z=v("el-switch"),y=v("el-input"),Ue=v("el-dialog"),Pe=Te("loading");return qe((U(),P("div",Re,[e[50]||(e[50]=o("div",{class:"app-page-title"},[o("h2",null,"系统配置")],-1)),o("div",Fe,[l(Y,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:s(()=>[e[27]||(e[27]=o("h3",{class:"section-title"},"并发配置",-1)),e[28]||(e[28]=o("div",{class:"section-sub app-muted"},"控制任务与截图的并发资源上限",-1)),l(X,{"label-width":"122px"},{default:s(()=>[l(n,{label:"全局最大并发数"},{default:s(()=>[l(d,{modelValue:A.value,"onUpdate:modelValue":e[0]||(e[0]=a=>A.value=a),min:1,max:200},null,8,["modelValue"]),e[22]||(e[22]=o("div",{class:"help"},"同时最多运行账号数(浏览任务 API 执行,资源占用较低)。",-1))]),_:1}),l(n,{label:"单账号最大并发数"},{default:s(()=>[l(d,{modelValue:I.value,"onUpdate:modelValue":e[1]||(e[1]=a=>I.value=a),min:1,max:50},null,8,["modelValue"]),e[23]||(e[23]=o("div",{class:"help"},"建议保持为 1,避免同账号任务抢占。",-1))]),_:1}),l(n,{label:"截图最大并发数"},{default:s(()=>[l(d,{modelValue:N.value,"onUpdate:modelValue":e[2]||(e[2]=a=>N.value=a),min:1,max:50},null,8,["modelValue"]),e[24]||(e[24]=o("div",{class:"help"},"截图资源占用较低,可按机器性能逐步提高。",-1))]),_:1}),l(n,{label:"慢 SQL 阈值(ms)"},{default:s(()=>[l(d,{modelValue:K.value,"onUpdate:modelValue":e[3]||(e[3]=a=>K.value=a),min:0,max:6e4},null,8,["modelValue"]),e[25]||(e[25]=o("div",{class:"help"},"低于该阈值不会计入慢 SQL(0 表示关闭慢 SQL 采样)。",-1))]),_:1})]),_:1}),o("div",ze,[l(_,{type:"primary",onClick:ge},{default:s(()=>[...e[26]||(e[26]=[c("保存并发配置",-1)])]),_:1})])]),_:1}),l(Y,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:s(()=>[e[33]||(e[33]=o("h3",{class:"section-title"},"代理设置",-1)),e[34]||(e[34]=o("div",{class:"section-sub app-muted"},"用于任务出网代理与连接有效期管理",-1)),l(X,{"label-width":"122px"},{default:s(()=>[l(n,{label:"启用 IP 代理"},{default:s(()=>[l(Z,{modelValue:S.value,"onUpdate:modelValue":e[4]||(e[4]=a=>S.value=a)},null,8,["modelValue"]),e[29]||(e[29]=o("div",{class:"help"},"开启后,浏览任务通过代理访问,失败自动重试。",-1))]),_:1}),l(n,{label:"代理 API 地址"},{default:s(()=>[l(y,{modelValue:f.value,"onUpdate:modelValue":e[5]||(e[5]=a=>f.value=a),placeholder:"http://api.xxx/Tools/IP.ashx?..."},null,8,["modelValue"]),e[30]||(e[30]=o("div",{class:"help"},"API 应返回 `IP:PORT`(例:123.45.67.89:8888)。",-1))]),_:1}),l(n,{label:"有效期(分钟)"},{default:s(()=>[l(d,{modelValue:D.value,"onUpdate:modelValue":e[6]||(e[6]=a=>D.value=a),min:1,max:60},null,8,["modelValue"])]),_:1})]),_:1}),o("div",He,[l(_,{type:"primary",onClick:Ve},{default:s(()=>[...e[31]||(e[31]=[c("保存代理配置",-1)])]),_:1}),l(_,{onClick:xe},{default:s(()=>[...e[32]||(e[32]=[c("测试代理",-1)])]),_:1})])]),_:1}),l(Y,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:s(()=>[e[37]||(e[37]=o("h3",{class:"section-title"},"注册设置",-1)),e[38]||(e[38]=o("div",{class:"section-sub app-muted"},"控制注册节流与新用户赠送 VIP",-1)),l(X,{"label-width":"122px"},{default:s(()=>[l(n,{label:"注册赠送 VIP"},{default:s(()=>[l(Z,{modelValue:E.value,"onUpdate:modelValue":e[7]||(e[7]=a=>E.value=a)},null,8,["modelValue"]),e[35]||(e[35]=o("div",{class:"help"},"开启后,新用户注册成功自动赠送下方设定的 VIP 天数。",-1))]),_:1}),l(n,{label:"每小时注册限制"},{default:s(()=>[l(d,{modelValue:L.value,"onUpdate:modelValue":e[8]||(e[8]=a=>L.value=a),min:1,max:1e4},null,8,["modelValue"])]),_:1}),l(n,{label:"赠送 VIP 天数"},{default:s(()=>[l(d,{modelValue:Q.value,"onUpdate:modelValue":e[9]||(e[9]=a=>Q.value=a),min:0,max:999999},null,8,["modelValue"])]),_:1})]),_:1}),o("div",je,[l(_,{type:"primary",onClick:ke},{default:s(()=>[...e[36]||(e[36]=[c("保存注册设置",-1)])]),_:1})])]),_:1})]),l(Y,{shadow:"never","body-style":{padding:"16px"},class:"card kdocs-card"},{default:s(()=>[o("div",Ge,[e[41]||(e[41]=o("h3",{class:"section-title"},"金山文档上传",-1)),o("div",Oe,[e[40]||(e[40]=o("span",null,"登录状态:",-1)),o("span",{class:Be(["status-chip",fe.value])},[c(ae(J.value)+" ",1),oe.value?(U(),P("span",Je,[...e[39]||(e[39]=[o("i",null,null,-1),o("i",null,null,-1),o("i",null,null,-1)])])):le("",!0)],2),o("span",null,"· 待上传 "+ae(r.value.queue_size||0),1)])]),l(X,{"label-width":"118px",class:"kdocs-form"},{default:s(()=>[l(n,{label:"启用上传"},{default:s(()=>[l(Z,{modelValue:T.value,"onUpdate:modelValue":e[10]||(e[10]=a=>T.value=a)},null,8,["modelValue"]),e[42]||(e[42]=o("div",{class:"help"},"表格结构变化时可先关闭,避免错误上传。",-1))]),_:1}),l(n,{label:"文档链接"},{default:s(()=>[l(y,{modelValue:q.value,"onUpdate:modelValue":e[11]||(e[11]=a=>q.value=a),placeholder:"https://kdocs.cn/..."},null,8,["modelValue"])]),_:1}),l(n,{label:"默认县区"},{default:s(()=>[l(y,{modelValue:B.value,"onUpdate:modelValue":e[12]||(e[12]=a=>B.value=a),placeholder:"如:道县(用户可覆盖)"},null,8,["modelValue"])]),_:1}),l(n,{label:"Sheet 名称"},{default:s(()=>[l(y,{modelValue:M.value,"onUpdate:modelValue":e[13]||(e[13]=a=>M.value=a),placeholder:"留空使用第一个 Sheet"},null,8,["modelValue"])]),_:1}),l(n,{label:"Sheet 序号"},{default:s(()=>[l(d,{modelValue:h.value,"onUpdate:modelValue":e[14]||(e[14]=a=>h.value=a),min:0,max:50},null,8,["modelValue"]),e[43]||(e[43]=o("div",{class:"help"},"0 表示第一个 Sheet。",-1))]),_:1}),l(n,{label:"列配置"},{default:s(()=>[o("div",We,[l(y,{modelValue:$.value,"onUpdate:modelValue":e[15]||(e[15]=a=>$.value=a),placeholder:"县区列,如 A"},null,8,["modelValue"]),l(y,{modelValue:R.value,"onUpdate:modelValue":e[16]||(e[16]=a=>R.value=a),placeholder:"图片列,如 D"},null,8,["modelValue"])])]),_:1}),l(n,{label:"有效行范围"},{default:s(()=>[o("div",Xe,[l(d,{modelValue:F.value,"onUpdate:modelValue":e[17]||(e[17]=a=>F.value=a),min:0,max:1e4,placeholder:"起始行",style:{width:"140px"}},null,8,["modelValue"]),e[44]||(e[44]=o("span",{class:"app-muted"},"至",-1)),l(d,{modelValue:z.value,"onUpdate:modelValue":e[18]||(e[18]=a=>z.value=a),min:0,max:1e4,placeholder:"结束行",style:{width:"140px"}},null,8,["modelValue"])]),e[45]||(e[45]=o("div",{class:"help"},"用于限制上传区间(如 50-100),0 表示不限制。",-1))]),_:1}),l(n,{label:"管理员通知"},{default:s(()=>[l(Z,{modelValue:H.value,"onUpdate:modelValue":e[19]||(e[19]=a=>H.value=a)},null,8,["modelValue"])]),_:1}),l(n,{label:"通知邮箱"},{default:s(()=>[l(y,{modelValue:j.value,"onUpdate:modelValue":e[20]||(e[20]=a=>j.value=a),placeholder:"admin@example.com"},null,8,["modelValue"])]),_:1})]),_:1}),o("div",Ye,[l(_,{type:"primary",onClick:be},{default:s(()=>[...e[46]||(e[46]=[c("保存表格配置",-1)])]),_:1}),l(_,{type:"success",plain:"",loading:b.value,disabled:ie.value&&!b.value,onClick:Se},{default:s(()=>[...e[47]||(e[47]=[c(" 获取二维码 ",-1)])]),_:1},8,["loading","disabled"]),l(_,{type:"danger",plain:"",loading:w.value,disabled:ie.value&&!w.value,onClick:Ce},{default:s(()=>[...e[48]||(e[48]=[c(" 清除登录 ",-1)])]),_:1},8,["loading","disabled"])]),r.value.last_error?(U(),P("div",Ze,"最近错误:"+ae(r.value.last_error),1)):le("",!0),G.value?(U(),P("div",el,"操作提示:"+ae(G.value),1)):le("",!0)]),_:1}),l(Ue,{modelValue:V.value,"onUpdate:modelValue":e[21]||(e[21]=a=>V.value=a),title:"扫码登录",width:"min(420px, 92vw)"},{default:s(()=>[o("div",ll,[x.value?(U(),P("img",{key:0,src:`data:image/png;base64,${x.value}`,alt:"KDocs QR"},null,8,al)):le("",!0),e[49]||(e[49]=o("div",{class:"help"},"请使用管理员微信扫码登录。",-1))])]),_:1},8,["modelValue"])])),[[Pe,p.value]])}}},rl=Ie(tl,[["__scopeId","data-v-ca313705"]]);export{rl as default}; diff --git a/static/admin/assets/SystemPage-DrM9-RI5.js b/static/admin/assets/SystemPage-DrM9-RI5.js new file mode 100644 index 0000000..ded5a8d --- /dev/null +++ b/static/admin/assets/SystemPage-DrM9-RI5.js @@ -0,0 +1,6 @@ +import{f as Fe,a as He,u as me,b as Oe,t as je}from"./system-CYbWdReq.js";import{a as ge,_ as Ge,g as Se,h as Ye,i as Je,u as fe,j as Pe,p as We}from"./index-DOvMEmc8.js";import{E as _e,a as d}from"./vendor-element-B5S5pUKo.js";import{r as s,c as de,l as Xe,R as Ze,o as el,aj as m,ap as ll,F as al,q as y,n as b,t as n,L as l,E as t,I as v,K as ol,a3 as tl,J as A,G as T,y as sl}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function nl(){const{data:k}=await ge.get("/proxy/config");return k}async function ul(k){const{data:_}=await ge.post("/proxy/config",k);return _}async function il(k){const{data:_}=await ge.post("/proxy/test",k);return _}const dl={class:"page-stack"},rl={class:"config-grid"},cl={class:"row-actions"},vl={class:"row-actions"},pl={class:"row-actions"},ml={key:0,class:"help"},fl={class:"row-actions"},_l={class:"section-head"},gl={class:"status-inline app-muted"},yl={key:0,class:"status-dots","aria-hidden":"true"},bl={class:"kdocs-inline"},kl={class:"kdocs-range"},Vl={class:"row-actions"},xl={key:0,class:"help"},wl={key:1,class:"help"},Sl={class:"kdocs-qr"},Pl=["src"],Cl={__name:"SystemPage",setup(k){const _=s(!1),B=s(2),q=s(1),M=s(3),z=s(120),L=s(!1),g=s(""),R=s(3),$=s(!1),F=s(10),H=s(7),I=s(!1),h=s("https://www.spacezs.cn/connect.php"),K=s(""),V=s(""),O=s(""),j=s(!1),x=s(["wx"]),re=s(!1),ce=s(!1),Ce=[{label:"QQ",value:"qq"},{label:"微信",value:"wx"},{label:"支付宝",value:"alipay"}],G=s(!1),Y=s(""),J=s(""),W=s(""),X=s(0),Z=s("A"),ee=s("D"),le=s(0),ae=s(0),oe=s(!1),te=s(""),ye=Se({maxAgeMs:600*1e3}),c=s(ye||{}),w=s(!1),S=s(""),ve=s(!1),P=s(!1),C=s(!1),U=s(!1),N=s(!ye),se=s("");let ne=null;const be=de(()=>P.value||C.value||U.value),pe=de(()=>N.value||P.value||ve.value),ue=de(()=>{if(pe.value)return"检测中";const o=c.value||{};return o?.logged_in===!0||o?.last_login_ok===!0?"已登录":o?.logged_in===!1||o?.last_login_ok===!1||o?.login_required===!0?"未登录":o?.last_error?"异常":"未知"}),Ue=de(()=>pe.value?"is-checking":ue.value==="已登录"?"is-online":ue.value==="未登录"?"is-offline":ue.value==="异常"?"is-error":"is-unknown");function r(o){if(!o){se.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});se.value=`${o} (${e})`}async function Ae(){_.value=!0;try{const[e,i,u]=await Promise.all([Fe(),nl(),He()]);B.value=e.max_concurrent_global??2,q.value=e.max_concurrent_per_account??1,M.value=e.max_screenshot_concurrent??3,z.value=e.db_slow_query_ms??120,$.value=(e.auto_approve_enabled??0)===1,F.value=e.auto_approve_hourly_limit??10,H.value=e.auto_approve_vip_days??7,L.value=(i.proxy_enabled??0)===1,g.value=i.proxy_api_url||"",R.value=i.proxy_expire_minutes??3,G.value=(e.kdocs_enabled??0)===1,Y.value=e.kdocs_doc_url||"",J.value=e.kdocs_default_unit||"",W.value=e.kdocs_sheet_name||"",X.value=e.kdocs_sheet_index??0,Z.value=(e.kdocs_unit_column||"A").toUpperCase(),ee.value=(e.kdocs_image_column||"D").toUpperCase(),le.value=e.kdocs_row_start??0,ae.value=e.kdocs_row_end??0,oe.value=(e.kdocs_admin_notify_enabled??0)===1,te.value=e.kdocs_admin_notify_email||"",I.value=(u.social_login_enabled??0)===1,h.value=u.social_login_endpoint||"https://www.spacezs.cn/connect.php",K.value=u.social_login_appid||"",V.value="",O.value=u.social_login_appkey_masked||"",j.value=!!u.social_login_appkey_configured,x.value=Array.isArray(u.social_login_providers)&&u.social_login_providers.length?u.social_login_providers:["wx"]}catch{}finally{_.value=!1}const o=Se({maxAgeMs:600*1e3});o&&(c.value=o,N.value=!1),Le()}async function Le(){if(!(N.value||P.value)){N.value=!0;try{const o=await We({force:!1,maxAgeMs:6e4,silent:!0,live:0});c.value=o||{}}catch{}finally{N.value=!1}}}async function Ie(){const o={max_concurrent_global:Number(B.value),max_concurrent_per_account:Number(q.value),max_screenshot_concurrent:Number(M.value),db_slow_query_ms:Number(z.value)};try{await _e.confirm(`确定更新并发配置吗? + +全局并发数: ${o.max_concurrent_global} +单账号并发数: ${o.max_concurrent_per_account} +截图并发数: ${o.max_screenshot_concurrent} +慢 SQL 阈值: ${o.db_slow_query_ms}ms`,"保存并发配置",{confirmButtonText:"保存",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await me(o);d.success(e?.message||"并发配置已更新")}catch{}}async function he(){if(L.value&&!g.value.trim()){d.error("启用代理时,API地址不能为空");return}const o={proxy_enabled:L.value?1:0,proxy_api_url:g.value.trim(),proxy_expire_minutes:Number(R.value)||3};try{const e=await ul(o);d.success(e?.message||"代理配置已更新")}catch{}}async function Ke(){if(!g.value.trim()){d.error("请先输入代理API地址");return}try{const o=await il({api_url:g.value.trim()});await _e.alert(o?.message||"测试完成","代理测试",{confirmButtonText:"知道了"})}catch{}}async function Ne(){const o=Number(F.value),e=Number(H.value);if(!Number.isFinite(o)||o<1){d.error("每小时注册限制必须大于0");return}if(!Number.isFinite(e)||e<0){d.error("VIP天数不能为负数");return}const i={auto_approve_enabled:$.value?1:0,auto_approve_hourly_limit:o,auto_approve_vip_days:e};try{const u=await me(i);d.success(u?.message||"注册设置已保存")}catch{}}function ke(){return{social_login_enabled:I.value?1:0,social_login_endpoint:h.value.trim()||"https://www.spacezs.cn/connect.php",social_login_appid:K.value.trim(),social_login_appkey:V.value.trim(),social_login_providers:x.value}}function Ve(){return I.value?x.value.length?h.value.trim()?K.value.trim()?!V.value.trim()&&!j.value?(d.error("请输入 APPKEY"),!1):!0:(d.error("请输入 APPID"),!1):(d.error("请输入聚合接口地址"),!1):(d.error("请选择至少一种登录方式"),!1):!0}async function De(){if(Ve()){re.value=!0;try{const o=await Oe(ke()),e=o?.config||{};V.value="",O.value=e.social_login_appkey_masked||O.value,j.value=!!e.social_login_appkey_configured,d.success(o?.message||"聚合登录配置已保存")}catch{}finally{re.value=!1}}}async function Ee(){if(Ve()){ce.value=!0;try{const o=new URL(window.location.href);o.pathname="/login",o.search="",o.hash="";const e=x.value[0]||"wx",i=await je({...ke(),provider:e,redirect_uri:o.toString()});await _e.alert(i?.success?"连接正常":"测试完成","聚合登录测试",{confirmButtonText:"知道了"})}catch{}finally{ce.value=!1}}}async function Qe(){const o={kdocs_enabled:G.value?1:0,kdocs_doc_url:Y.value.trim(),kdocs_default_unit:J.value.trim(),kdocs_sheet_name:W.value.trim(),kdocs_sheet_index:Number(X.value)||0,kdocs_unit_column:Z.value.trim().toUpperCase(),kdocs_image_column:ee.value.trim().toUpperCase(),kdocs_row_start:Number(le.value)||0,kdocs_row_end:Number(ae.value)||0,kdocs_admin_notify_enabled:oe.value?1:0,kdocs_admin_notify_email:te.value.trim()};try{const e=await me(o);d.success(e?.message||"表格配置已更新")}catch{}}async function xe(){if(!P.value){P.value=!0,r("正在刷新状态");try{const o=await Pe({live:1});c.value=o||{},fe(c.value),r("状态已刷新")}catch{r("刷新失败,请稍后重试")}finally{P.value=!1}}}async function we(){try{const o=await Pe({live:1});c.value=o||{},fe(c.value),(o?.logged_in===!0||o?.last_login_ok===!0)&&(d.success("扫码成功,已登录"),r("扫码成功,已登录"),w.value=!1,ie())}catch{}}function Te(){ie(),ve.value=!0,r("扫码检测中"),we(),ne=setInterval(we,2e3)}function ie(){ne&&(clearInterval(ne),ne=null),ve.value=!1}async function Be(){if(!C.value){C.value=!0,r("正在获取二维码");try{S.value="";const o=await Ye();if(S.value=o?.qr_image||"",!S.value){if(o?.logged_in){d.success("当前已登录,无需扫码"),r("当前已登录,无需扫码"),await xe();return}d.warning("未获取到二维码"),r("未获取到二维码");return}r("二维码已获取"),w.value=!0}catch{r("获取二维码失败")}finally{C.value=!1}}}async function qe(){if(!U.value){U.value=!0,r("正在清除登录态");try{await Je(),w.value=!1,S.value="",c.value=fe({...c.value||{},logged_in:!1,last_login_ok:!1,login_required:!0}),d.success("登录态已清除"),r("登录态已清除"),await xe()}catch{r("清除登录态失败")}finally{U.value=!1}}}return Xe(w,o=>{o?Te():ie()}),Ze(()=>{ie()}),el(Ae),(o,e)=>{const i=m("el-input-number"),u=m("el-form-item"),D=m("el-form"),f=m("el-button"),E=m("el-card"),Q=m("el-switch"),p=m("el-input"),Me=m("el-checkbox"),ze=m("el-checkbox-group"),Re=m("el-dialog"),$e=ll("loading");return al((y(),b("div",dl,[e[59]||(e[59]=n("div",{class:"app-page-title"},[n("h2",null,"系统配置")],-1)),n("div",rl,[l(E,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:t(()=>[e[32]||(e[32]=n("h3",{class:"section-title"},"并发配置",-1)),e[33]||(e[33]=n("div",{class:"section-sub app-muted"},"控制任务与截图的并发资源上限",-1)),l(D,{"label-width":"122px"},{default:t(()=>[l(u,{label:"全局最大并发数"},{default:t(()=>[l(i,{modelValue:B.value,"onUpdate:modelValue":e[0]||(e[0]=a=>B.value=a),min:1,max:200},null,8,["modelValue"]),e[27]||(e[27]=n("div",{class:"help"},"同时最多运行账号数(浏览任务 API 执行,资源占用较低)。",-1))]),_:1}),l(u,{label:"单账号最大并发数"},{default:t(()=>[l(i,{modelValue:q.value,"onUpdate:modelValue":e[1]||(e[1]=a=>q.value=a),min:1,max:50},null,8,["modelValue"]),e[28]||(e[28]=n("div",{class:"help"},"建议保持为 1,避免同账号任务抢占。",-1))]),_:1}),l(u,{label:"截图最大并发数"},{default:t(()=>[l(i,{modelValue:M.value,"onUpdate:modelValue":e[2]||(e[2]=a=>M.value=a),min:1,max:50},null,8,["modelValue"]),e[29]||(e[29]=n("div",{class:"help"},"截图资源占用较低,可按机器性能逐步提高。",-1))]),_:1}),l(u,{label:"慢 SQL 阈值(ms)"},{default:t(()=>[l(i,{modelValue:z.value,"onUpdate:modelValue":e[3]||(e[3]=a=>z.value=a),min:0,max:6e4},null,8,["modelValue"]),e[30]||(e[30]=n("div",{class:"help"},"低于该阈值不会计入慢 SQL(0 表示关闭慢 SQL 采样)。",-1))]),_:1})]),_:1}),n("div",cl,[l(f,{type:"primary",onClick:Ie},{default:t(()=>[...e[31]||(e[31]=[v("保存并发配置",-1)])]),_:1})])]),_:1}),l(E,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:t(()=>[e[38]||(e[38]=n("h3",{class:"section-title"},"代理设置",-1)),e[39]||(e[39]=n("div",{class:"section-sub app-muted"},"用于任务出网代理与连接有效期管理",-1)),l(D,{"label-width":"122px"},{default:t(()=>[l(u,{label:"启用 IP 代理"},{default:t(()=>[l(Q,{modelValue:L.value,"onUpdate:modelValue":e[4]||(e[4]=a=>L.value=a)},null,8,["modelValue"]),e[34]||(e[34]=n("div",{class:"help"},"开启后,浏览任务通过代理访问,失败自动重试。",-1))]),_:1}),l(u,{label:"代理 API 地址"},{default:t(()=>[l(p,{modelValue:g.value,"onUpdate:modelValue":e[5]||(e[5]=a=>g.value=a),placeholder:"http://api.xxx/Tools/IP.ashx?..."},null,8,["modelValue"]),e[35]||(e[35]=n("div",{class:"help"},"API 应返回 `IP:PORT`(例:123.45.67.89:8888)。",-1))]),_:1}),l(u,{label:"有效期(分钟)"},{default:t(()=>[l(i,{modelValue:R.value,"onUpdate:modelValue":e[6]||(e[6]=a=>R.value=a),min:1,max:60},null,8,["modelValue"])]),_:1})]),_:1}),n("div",vl,[l(f,{type:"primary",onClick:he},{default:t(()=>[...e[36]||(e[36]=[v("保存代理配置",-1)])]),_:1}),l(f,{onClick:Ke},{default:t(()=>[...e[37]||(e[37]=[v("测试代理",-1)])]),_:1})])]),_:1}),l(E,{shadow:"never","body-style":{padding:"16px"},class:"card section-card"},{default:t(()=>[e[42]||(e[42]=n("h3",{class:"section-title"},"注册设置",-1)),e[43]||(e[43]=n("div",{class:"section-sub app-muted"},"控制注册节流与新用户赠送 VIP",-1)),l(D,{"label-width":"122px"},{default:t(()=>[l(u,{label:"注册赠送 VIP"},{default:t(()=>[l(Q,{modelValue:$.value,"onUpdate:modelValue":e[7]||(e[7]=a=>$.value=a)},null,8,["modelValue"]),e[40]||(e[40]=n("div",{class:"help"},"开启后,新用户注册成功自动赠送下方设定的 VIP 天数。",-1))]),_:1}),l(u,{label:"每小时注册限制"},{default:t(()=>[l(i,{modelValue:F.value,"onUpdate:modelValue":e[8]||(e[8]=a=>F.value=a),min:1,max:1e4},null,8,["modelValue"])]),_:1}),l(u,{label:"赠送 VIP 天数"},{default:t(()=>[l(i,{modelValue:H.value,"onUpdate:modelValue":e[9]||(e[9]=a=>H.value=a),min:0,max:999999},null,8,["modelValue"])]),_:1})]),_:1}),n("div",pl,[l(f,{type:"primary",onClick:Ne},{default:t(()=>[...e[41]||(e[41]=[v("保存注册设置",-1)])]),_:1})])]),_:1}),l(E,{shadow:"never","body-style":{padding:"16px"},class:"card section-card social-card"},{default:t(()=>[e[46]||(e[46]=n("h3",{class:"section-title"},"聚合登录",-1)),e[47]||(e[47]=n("div",{class:"section-sub app-muted"},"QQ、微信、支付宝快捷登录",-1)),l(D,{"label-width":"122px"},{default:t(()=>[l(u,{label:"启用"},{default:t(()=>[l(Q,{modelValue:I.value,"onUpdate:modelValue":e[10]||(e[10]=a=>I.value=a)},null,8,["modelValue"])]),_:1}),l(u,{label:"登录方式"},{default:t(()=>[l(ze,{modelValue:x.value,"onUpdate:modelValue":e[11]||(e[11]=a=>x.value=a)},{default:t(()=>[(y(),b(ol,null,tl(Ce,a=>l(Me,{key:a.value,label:a.value},{default:t(()=>[v(A(a.label),1)]),_:2},1032,["label"])),64))]),_:1},8,["modelValue"])]),_:1}),l(u,{label:"接口地址"},{default:t(()=>[l(p,{modelValue:h.value,"onUpdate:modelValue":e[12]||(e[12]=a=>h.value=a),placeholder:"https://www.spacezs.cn/connect.php"},null,8,["modelValue"])]),_:1}),l(u,{label:"APPID"},{default:t(()=>[l(p,{modelValue:K.value,"onUpdate:modelValue":e[13]||(e[13]=a=>K.value=a),placeholder:"Space APPID"},null,8,["modelValue"])]),_:1}),l(u,{label:"APPKEY"},{default:t(()=>[l(p,{modelValue:V.value,"onUpdate:modelValue":e[14]||(e[14]=a=>V.value=a),type:"password","show-password":"",placeholder:"留空则保持当前密钥"},null,8,["modelValue"]),j.value?(y(),b("div",ml,"当前:"+A(O.value),1)):T("",!0)]),_:1})]),_:1}),n("div",fl,[l(f,{type:"primary",loading:re.value,onClick:De},{default:t(()=>[...e[44]||(e[44]=[v("保存聚合登录",-1)])]),_:1},8,["loading"]),l(f,{loading:ce.value,onClick:Ee},{default:t(()=>[...e[45]||(e[45]=[v("测试连接",-1)])]),_:1},8,["loading"])])]),_:1})]),l(E,{shadow:"never","body-style":{padding:"16px"},class:"card kdocs-card"},{default:t(()=>[n("div",_l,[e[50]||(e[50]=n("h3",{class:"section-title"},"金山文档上传",-1)),n("div",gl,[e[49]||(e[49]=n("span",null,"登录状态:",-1)),n("span",{class:sl(["status-chip",Ue.value])},[v(A(ue.value)+" ",1),pe.value?(y(),b("span",yl,[...e[48]||(e[48]=[n("i",null,null,-1),n("i",null,null,-1),n("i",null,null,-1)])])):T("",!0)],2),n("span",null,"· 待上传 "+A(c.value.queue_size||0),1)])]),l(D,{"label-width":"118px",class:"kdocs-form"},{default:t(()=>[l(u,{label:"启用上传"},{default:t(()=>[l(Q,{modelValue:G.value,"onUpdate:modelValue":e[15]||(e[15]=a=>G.value=a)},null,8,["modelValue"]),e[51]||(e[51]=n("div",{class:"help"},"表格结构变化时可先关闭,避免错误上传。",-1))]),_:1}),l(u,{label:"文档链接"},{default:t(()=>[l(p,{modelValue:Y.value,"onUpdate:modelValue":e[16]||(e[16]=a=>Y.value=a),placeholder:"https://kdocs.cn/..."},null,8,["modelValue"])]),_:1}),l(u,{label:"默认县区"},{default:t(()=>[l(p,{modelValue:J.value,"onUpdate:modelValue":e[17]||(e[17]=a=>J.value=a),placeholder:"如:道县(用户可覆盖)"},null,8,["modelValue"])]),_:1}),l(u,{label:"Sheet 名称"},{default:t(()=>[l(p,{modelValue:W.value,"onUpdate:modelValue":e[18]||(e[18]=a=>W.value=a),placeholder:"留空使用第一个 Sheet"},null,8,["modelValue"])]),_:1}),l(u,{label:"Sheet 序号"},{default:t(()=>[l(i,{modelValue:X.value,"onUpdate:modelValue":e[19]||(e[19]=a=>X.value=a),min:0,max:50},null,8,["modelValue"]),e[52]||(e[52]=n("div",{class:"help"},"0 表示第一个 Sheet。",-1))]),_:1}),l(u,{label:"列配置"},{default:t(()=>[n("div",bl,[l(p,{modelValue:Z.value,"onUpdate:modelValue":e[20]||(e[20]=a=>Z.value=a),placeholder:"县区列,如 A"},null,8,["modelValue"]),l(p,{modelValue:ee.value,"onUpdate:modelValue":e[21]||(e[21]=a=>ee.value=a),placeholder:"图片列,如 D"},null,8,["modelValue"])])]),_:1}),l(u,{label:"有效行范围"},{default:t(()=>[n("div",kl,[l(i,{modelValue:le.value,"onUpdate:modelValue":e[22]||(e[22]=a=>le.value=a),min:0,max:1e4,placeholder:"起始行",style:{width:"140px"}},null,8,["modelValue"]),e[53]||(e[53]=n("span",{class:"app-muted"},"至",-1)),l(i,{modelValue:ae.value,"onUpdate:modelValue":e[23]||(e[23]=a=>ae.value=a),min:0,max:1e4,placeholder:"结束行",style:{width:"140px"}},null,8,["modelValue"])]),e[54]||(e[54]=n("div",{class:"help"},"用于限制上传区间(如 50-100),0 表示不限制。",-1))]),_:1}),l(u,{label:"管理员通知"},{default:t(()=>[l(Q,{modelValue:oe.value,"onUpdate:modelValue":e[24]||(e[24]=a=>oe.value=a)},null,8,["modelValue"])]),_:1}),l(u,{label:"通知邮箱"},{default:t(()=>[l(p,{modelValue:te.value,"onUpdate:modelValue":e[25]||(e[25]=a=>te.value=a),placeholder:"admin@example.com"},null,8,["modelValue"])]),_:1})]),_:1}),n("div",Vl,[l(f,{type:"primary",onClick:Qe},{default:t(()=>[...e[55]||(e[55]=[v("保存表格配置",-1)])]),_:1}),l(f,{type:"success",plain:"",loading:C.value,disabled:be.value&&!C.value,onClick:Be},{default:t(()=>[...e[56]||(e[56]=[v(" 获取二维码 ",-1)])]),_:1},8,["loading","disabled"]),l(f,{type:"danger",plain:"",loading:U.value,disabled:be.value&&!U.value,onClick:qe},{default:t(()=>[...e[57]||(e[57]=[v(" 清除登录 ",-1)])]),_:1},8,["loading","disabled"])]),c.value.last_error?(y(),b("div",xl,"最近错误:"+A(c.value.last_error),1)):T("",!0),se.value?(y(),b("div",wl,"操作提示:"+A(se.value),1)):T("",!0)]),_:1}),l(Re,{modelValue:w.value,"onUpdate:modelValue":e[26]||(e[26]=a=>w.value=a),title:"扫码登录",width:"min(420px, 92vw)"},{default:t(()=>[n("div",Sl,[S.value?(y(),b("img",{key:0,src:`data:image/png;base64,${S.value}`,alt:"KDocs QR"},null,8,Pl)):T("",!0),e[58]||(e[58]=n("div",{class:"help"},"请使用管理员微信扫码登录。",-1))])]),_:1},8,["modelValue"])])),[[$e,_.value]])}}},Nl=Ge(Cl,[["__scopeId","data-v-3820d834"]]);export{Nl as default}; diff --git a/static/admin/assets/UsersPage-BNDnhJe0.css b/static/admin/assets/UsersPage-BNDnhJe0.css deleted file mode 100644 index faecdcd..0000000 --- a/static/admin/assets/UsersPage-BNDnhJe0.css +++ /dev/null @@ -1 +0,0 @@ -.page-stack[data-v-11f4f270]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-11f4f270]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-11f4f270]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-11f4f270]{margin-top:10px;font-size:12px}.table-wrap[data-v-11f4f270]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.user-block[data-v-11f4f270]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-11f4f270]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-11f4f270]{font-size:12px}.vip-sub[data-v-11f4f270]{font-size:12px;color:#7c3aed}.actions[data-v-11f4f270]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/UsersPage-CgYh6JHW.css b/static/admin/assets/UsersPage-CgYh6JHW.css new file mode 100644 index 0000000..cd99aa2 --- /dev/null +++ b/static/admin/assets/UsersPage-CgYh6JHW.css @@ -0,0 +1 @@ +.page-stack[data-v-3e2dc1eb]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-3e2dc1eb]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-3e2dc1eb]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-3e2dc1eb]{margin-top:10px;font-size:12px}.table-wrap[data-v-3e2dc1eb]{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.user-block[data-v-3e2dc1eb]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-3e2dc1eb]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-3e2dc1eb]{font-size:12px}.vip-sub[data-v-3e2dc1eb]{font-size:12px;color:#7c3aed}.actions[data-v-3e2dc1eb]{display:flex;flex-wrap:wrap;gap:8px} diff --git a/static/admin/assets/UsersPage-DJZUCpfb.js b/static/admin/assets/UsersPage-RI5S3snx.js similarity index 98% rename from static/admin/assets/UsersPage-DJZUCpfb.js rename to static/admin/assets/UsersPage-RI5S3snx.js index 3f6cf4a..4f95a33 100644 --- a/static/admin/assets/UsersPage-DJZUCpfb.js +++ b/static/admin/assets/UsersPage-RI5S3snx.js @@ -1 +1 @@ -import{a as W,r as Z,s as F,b as G,c as J,d as H,f as K}from"./users-te9ySk34.js";import{_ as O}from"./index-C1f9ticl.js";import{E as v,a as h}from"./vendor-element-B5S5pUKo.js";import{i as Q,r as P,o as X,aj as _,ap as Y,n as V,q as r,t as y,L as l,E as s,F as ee,D as f,G as p,J as b,I as c}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";function I(g){if(!g)return null;if(g instanceof Date)return g;let a=String(g).trim();if(!a)return null;/^\d{4}-\d{2}-\d{2}$/.test(a)&&(a=`${a}T00:00:00`);let o=a.includes("T")?a:a.replace(" ","T");o=o.replace(/\.(\d{3})\d+/,".$1"),/([zZ]|[+-]\d{2}:\d{2})$/.test(o)||(o=`${o}+08:00`);const u=new Date(o);return Number.isNaN(u.getTime())?null:u}function D(g){const a=String(g||"");if(!a)return{ok:!1,message:"密码不能为空"};if(a.length<8)return{ok:!1,message:"密码长度不能少于8个字符"};if(a.length>128)return{ok:!1,message:"密码长度不能超过128个字符"};const o=/[a-zA-Z]/.test(a),x=/\d/.test(a);return!o||!x?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}const te={class:"page-stack"},ne={class:"table-wrap"},se={class:"user-block"},ae={class:"user-main"},ie={key:0,class:"app-muted user-sub"},re={key:1,class:"vip-sub"},oe={key:0,class:"app-muted"},le={class:"actions"},ce={__name:"UsersPage",setup(g){const a=Q("refreshStats",null),o=P(!1),x=P([]);function u(n){const e=n?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const i=I(e);return i?i.getTime()>Date.now():!1}function C(n){const e=n?.vip_expire_time;if(!e||!u(n))return"";if(String(e).startsWith("2099-12-31"))return"永久VIP";const i=I(e);if(!i)return`到期: ${e}`;const d=Math.ceil((i.getTime()-Date.now())/(1e3*60*60*24));return`到期: ${e}(剩${d}天)`}function B(n){return n==="rejected"?{label:"禁用",type:"danger"}:{label:"正常",type:"success"}}async function w(){o.value=!0;try{x.value=await K()}catch{x.value=[]}finally{o.value=!1}}async function U(){await w()}async function z(n){try{await v.confirm(`确定启用用户「${n.username}」吗?启用后用户可正常登录。`,"启用用户",{confirmButtonText:"启用",cancelButtonText:"取消",type:"success"})}catch{return}try{await W(n.id),h.success("用户已启用"),await w(),await a?.({force:!0})}catch{}}async function S(n){try{await v.confirm(`确定禁用用户「${n.username}」吗?禁用后用户将无法登录。`,"禁用用户",{confirmButtonText:"禁用",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Z(n.id),h.success("用户已禁用"),await w(),await a?.({force:!0})}catch{}}async function E(n){try{await v.confirm(`确定删除用户「${n.username}」吗?此操作将删除该用户的所有数据,不可恢复!`,"删除用户",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{await H(n.id),h.success("用户已删除"),await w(),await a?.({force:!0})}catch{}}async function $(n,e){const i={7:"一周",30:"一个月",365:"一年",999999:"永久"}[e]||`${e}天`;try{await v.confirm(`确定为用户「${n.username}」开通 ${i} VIP 吗?`,"设置VIP",{confirmButtonText:"确认",cancelButtonText:"取消",type:"warning"})}catch{return}try{const d=await F(n.id,e);h.success(d?.message||"VIP设置成功"),await w(),await a?.({force:!0})}catch{}}async function N(n){try{await v.confirm(`确定移除用户「${n.username}」的 VIP 吗?`,"移除VIP",{confirmButtonText:"移除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await G(n.id);h.success(e?.message||"VIP已移除"),await w(),await a?.({force:!0})}catch{}}async function M(n){let e;try{e=(await v.prompt("请输入新密码(至少8位且包含字母和数字)","重置密码",{confirmButtonText:"提交",cancelButtonText:"取消",inputType:"password",inputPlaceholder:"新密码",inputValidator:k=>D(k).ok,inputErrorMessage:"密码至少8位且包含字母和数字"})).value}catch{return}const i=D(e);if(!i.ok){h.error(i.message);return}try{await v.confirm(`确定将用户「${n.username}」的密码重置为该新密码吗?`,"二次确认",{confirmButtonText:"确认重置",cancelButtonText:"取消",type:"warning"})}catch{return}try{const d=await J(n.id,e);h.success(d?.message||"密码重置成功")}catch{}}return X(U),(n,e)=>{const i=_("el-table-column"),d=_("el-tag"),k=_("el-button"),T=_("el-dropdown-item"),j=_("el-dropdown-menu"),L=_("el-dropdown"),A=_("el-table"),R=_("el-card"),q=Y("loading");return r(),V("div",te,[e[11]||(e[11]=y("div",{class:"app-page-title"},[y("h2",null,"用户")],-1)),l(R,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[y("div",ne,[ee((r(),f(A,{data:x.value,style:{width:"100%"}},{default:s(()=>[l(i,{prop:"id",label:"ID",width:"80"}),l(i,{label:"用户","min-width":"240"},{default:s(({row:t})=>[y("div",se,[y("div",ae,[y("strong",null,b(t.username),1),u(t)?(r(),f(d,{key:0,type:"warning",effect:"light",size:"small"},{default:s(()=>[...e[0]||(e[0]=[c("VIP",-1)])]),_:1})):p("",!0)]),t.email?(r(),V("div",ie,b(t.email),1)):p("",!0),C(t)?(r(),V("div",re,b(C(t)),1)):p("",!0)])]),_:1}),l(i,{label:"状态",width:"120"},{default:s(({row:t})=>[l(d,{type:B(t.status).type,effect:"light"},{default:s(()=>[c(b(B(t.status).label),1)]),_:2},1032,["type"])]),_:1}),l(i,{label:"时间","min-width":"220"},{default:s(({row:t})=>[y("div",null,b(t.created_at),1),t.vip_expire_time?(r(),V("div",oe,"VIP到期: "+b(t.vip_expire_time),1)):p("",!0)]),_:1}),l(i,{label:"操作",width:"280",fixed:"right"},{default:s(({row:t})=>[y("div",le,[t.status==="rejected"?(r(),f(k,{key:0,type:"success",size:"small",onClick:m=>z(t)},{default:s(()=>[...e[1]||(e[1]=[c("启用",-1)])]),_:1},8,["onClick"])):(r(),f(k,{key:1,type:"warning",size:"small",onClick:m=>S(t)},{default:s(()=>[...e[2]||(e[2]=[c("禁用",-1)])]),_:1},8,["onClick"])),l(L,{trigger:"click"},{dropdown:s(()=>[l(j,null,{default:s(()=>[u(t)?p("",!0):(r(),f(T,{key:0,onClick:m=>$(t,7)},{default:s(()=>[...e[4]||(e[4]=[c("开通一周",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:1,onClick:m=>$(t,30)},{default:s(()=>[...e[5]||(e[5]=[c("开通一月",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:2,onClick:m=>$(t,365)},{default:s(()=>[...e[6]||(e[6]=[c("开通一年",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:3,onClick:m=>$(t,999999)},{default:s(()=>[...e[7]||(e[7]=[c("永久VIP",-1)])]),_:1},8,["onClick"])),u(t)?(r(),f(T,{key:4,onClick:m=>N(t)},{default:s(()=>[...e[8]||(e[8]=[c("移除VIP",-1)])]),_:1},8,["onClick"])):p("",!0)]),_:2},1024)]),default:s(()=>[l(k,{size:"small"},{default:s(()=>[...e[3]||(e[3]=[c("VIP",-1)])]),_:1})]),_:2},1024),l(k,{size:"small",onClick:m=>M(t)},{default:s(()=>[...e[9]||(e[9]=[c("重置密码",-1)])]),_:1},8,["onClick"]),l(k,{type:"danger",size:"small",onClick:m=>E(t)},{default:s(()=>[...e[10]||(e[10]=[c("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[q,o.value]])])]),_:1})])}}},ye=O(ce,[["__scopeId","data-v-11f4f270"]]);export{ye as default}; +import{a as W,r as Z,s as F,b as G,c as J,d as H,f as K}from"./users-DzDcz9C_.js";import{_ as O}from"./index-DOvMEmc8.js";import{E as v,a as h}from"./vendor-element-B5S5pUKo.js";import{i as Q,r as P,o as X,aj as _,ap as Y,n as V,q as r,t as y,L as l,E as s,F as ee,D as f,G as p,J as b,I as c}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";function I(g){if(!g)return null;if(g instanceof Date)return g;let a=String(g).trim();if(!a)return null;/^\d{4}-\d{2}-\d{2}$/.test(a)&&(a=`${a}T00:00:00`);let o=a.includes("T")?a:a.replace(" ","T");o=o.replace(/\.(\d{3})\d+/,".$1"),/([zZ]|[+-]\d{2}:\d{2})$/.test(o)||(o=`${o}+08:00`);const u=new Date(o);return Number.isNaN(u.getTime())?null:u}function D(g){const a=String(g||"");if(!a)return{ok:!1,message:"密码不能为空"};if(a.length<8)return{ok:!1,message:"密码长度不能少于8个字符"};if(a.length>128)return{ok:!1,message:"密码长度不能超过128个字符"};const o=/[a-zA-Z]/.test(a),x=/\d/.test(a);return!o||!x?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}const te={class:"page-stack"},ne={class:"table-wrap"},se={class:"user-block"},ae={class:"user-main"},ie={key:0,class:"app-muted user-sub"},re={key:1,class:"vip-sub"},oe={key:0,class:"app-muted"},le={class:"actions"},ce={__name:"UsersPage",setup(g){const a=Q("refreshStats",null),o=P(!1),x=P([]);function u(n){const e=n?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const i=I(e);return i?i.getTime()>Date.now():!1}function C(n){const e=n?.vip_expire_time;if(!e||!u(n))return"";if(String(e).startsWith("2099-12-31"))return"永久VIP";const i=I(e);if(!i)return`到期: ${e}`;const d=Math.ceil((i.getTime()-Date.now())/(1e3*60*60*24));return`到期: ${e}(剩${d}天)`}function B(n){return n==="rejected"?{label:"禁用",type:"danger"}:{label:"正常",type:"success"}}async function w(){o.value=!0;try{x.value=await K()}catch{x.value=[]}finally{o.value=!1}}async function U(){await w()}async function z(n){try{await v.confirm(`确定启用用户「${n.username}」吗?启用后用户可正常登录。`,"启用用户",{confirmButtonText:"启用",cancelButtonText:"取消",type:"success"})}catch{return}try{await W(n.id),h.success("用户已启用"),await w(),await a?.({force:!0})}catch{}}async function S(n){try{await v.confirm(`确定禁用用户「${n.username}」吗?禁用后用户将无法登录。`,"禁用用户",{confirmButtonText:"禁用",cancelButtonText:"取消",type:"warning"})}catch{return}try{await Z(n.id),h.success("用户已禁用"),await w(),await a?.({force:!0})}catch{}}async function E(n){try{await v.confirm(`确定删除用户「${n.username}」吗?此操作将删除该用户的所有数据,不可恢复!`,"删除用户",{confirmButtonText:"删除",cancelButtonText:"取消",type:"error"})}catch{return}try{await H(n.id),h.success("用户已删除"),await w(),await a?.({force:!0})}catch{}}async function $(n,e){const i={7:"一周",30:"一个月",365:"一年",999999:"永久"}[e]||`${e}天`;try{await v.confirm(`确定为用户「${n.username}」开通 ${i} VIP 吗?`,"设置VIP",{confirmButtonText:"确认",cancelButtonText:"取消",type:"warning"})}catch{return}try{const d=await F(n.id,e);h.success(d?.message||"VIP设置成功"),await w(),await a?.({force:!0})}catch{}}async function N(n){try{await v.confirm(`确定移除用户「${n.username}」的 VIP 吗?`,"移除VIP",{confirmButtonText:"移除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await G(n.id);h.success(e?.message||"VIP已移除"),await w(),await a?.({force:!0})}catch{}}async function M(n){let e;try{e=(await v.prompt("请输入新密码(至少8位且包含字母和数字)","重置密码",{confirmButtonText:"提交",cancelButtonText:"取消",inputType:"password",inputPlaceholder:"新密码",inputValidator:k=>D(k).ok,inputErrorMessage:"密码至少8位且包含字母和数字"})).value}catch{return}const i=D(e);if(!i.ok){h.error(i.message);return}try{await v.confirm(`确定将用户「${n.username}」的密码重置为该新密码吗?`,"二次确认",{confirmButtonText:"确认重置",cancelButtonText:"取消",type:"warning"})}catch{return}try{const d=await J(n.id,e);h.success(d?.message||"密码重置成功")}catch{}}return X(U),(n,e)=>{const i=_("el-table-column"),d=_("el-tag"),k=_("el-button"),T=_("el-dropdown-item"),j=_("el-dropdown-menu"),L=_("el-dropdown"),A=_("el-table"),R=_("el-card"),q=Y("loading");return r(),V("div",te,[e[11]||(e[11]=y("div",{class:"app-page-title"},[y("h2",null,"用户")],-1)),l(R,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[y("div",ne,[ee((r(),f(A,{data:x.value,style:{width:"100%"}},{default:s(()=>[l(i,{prop:"id",label:"ID",width:"80"}),l(i,{label:"用户","min-width":"240"},{default:s(({row:t})=>[y("div",se,[y("div",ae,[y("strong",null,b(t.username),1),u(t)?(r(),f(d,{key:0,type:"warning",effect:"light",size:"small"},{default:s(()=>[...e[0]||(e[0]=[c("VIP",-1)])]),_:1})):p("",!0)]),t.email?(r(),V("div",ie,b(t.email),1)):p("",!0),C(t)?(r(),V("div",re,b(C(t)),1)):p("",!0)])]),_:1}),l(i,{label:"状态",width:"120"},{default:s(({row:t})=>[l(d,{type:B(t.status).type,effect:"light"},{default:s(()=>[c(b(B(t.status).label),1)]),_:2},1032,["type"])]),_:1}),l(i,{label:"时间","min-width":"220"},{default:s(({row:t})=>[y("div",null,b(t.created_at),1),t.vip_expire_time?(r(),V("div",oe,"VIP到期: "+b(t.vip_expire_time),1)):p("",!0)]),_:1}),l(i,{label:"操作",width:"280",fixed:"right"},{default:s(({row:t})=>[y("div",le,[t.status==="rejected"?(r(),f(k,{key:0,type:"success",size:"small",onClick:m=>z(t)},{default:s(()=>[...e[1]||(e[1]=[c("启用",-1)])]),_:1},8,["onClick"])):(r(),f(k,{key:1,type:"warning",size:"small",onClick:m=>S(t)},{default:s(()=>[...e[2]||(e[2]=[c("禁用",-1)])]),_:1},8,["onClick"])),l(L,{trigger:"click"},{dropdown:s(()=>[l(j,null,{default:s(()=>[u(t)?p("",!0):(r(),f(T,{key:0,onClick:m=>$(t,7)},{default:s(()=>[...e[4]||(e[4]=[c("开通一周",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:1,onClick:m=>$(t,30)},{default:s(()=>[...e[5]||(e[5]=[c("开通一月",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:2,onClick:m=>$(t,365)},{default:s(()=>[...e[6]||(e[6]=[c("开通一年",-1)])]),_:1},8,["onClick"])),u(t)?p("",!0):(r(),f(T,{key:3,onClick:m=>$(t,999999)},{default:s(()=>[...e[7]||(e[7]=[c("永久VIP",-1)])]),_:1},8,["onClick"])),u(t)?(r(),f(T,{key:4,onClick:m=>N(t)},{default:s(()=>[...e[8]||(e[8]=[c("移除VIP",-1)])]),_:1},8,["onClick"])):p("",!0)]),_:2},1024)]),default:s(()=>[l(k,{size:"small"},{default:s(()=>[...e[3]||(e[3]=[c("VIP",-1)])]),_:1})]),_:2},1024),l(k,{size:"small",onClick:m=>M(t)},{default:s(()=>[...e[9]||(e[9]=[c("重置密码",-1)])]),_:1},8,["onClick"]),l(k,{type:"danger",size:"small",onClick:m=>E(t)},{default:s(()=>[...e[10]||(e[10]=[c("删除",-1)])]),_:1},8,["onClick"])])]),_:1})]),_:1},8,["data"])),[[q,o.value]])])]),_:1})])}}},ye=O(ce,[["__scopeId","data-v-3e2dc1eb"]]);export{ye as default}; diff --git a/static/admin/assets/email-px7YBG2O.js b/static/admin/assets/email-Mh1SHQbX.js similarity index 88% rename from static/admin/assets/email-px7YBG2O.js rename to static/admin/assets/email-Mh1SHQbX.js index d7e7ca4..6db2f8e 100644 --- a/static/admin/assets/email-px7YBG2O.js +++ b/static/admin/assets/email-Mh1SHQbX.js @@ -1 +1 @@ -import{c as s,a as e}from"./index-C1f9ticl.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u}; +import{c as s,a as e}from"./index-DOvMEmc8.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u}; diff --git a/static/admin/assets/index-Dh0BbqTX.css b/static/admin/assets/index-CPs_XZ2s.css similarity index 66% rename from static/admin/assets/index-Dh0BbqTX.css rename to static/admin/assets/index-CPs_XZ2s.css index 1929763..01fe5d7 100644 --- a/static/admin/assets/index-Dh0BbqTX.css +++ b/static/admin/assets/index-CPs_XZ2s.css @@ -1 +1 @@ -.layout-root[data-v-c6ed865d]{height:100%}.layout-aside[data-v-c6ed865d]{background:linear-gradient(180deg,#fffffffa,#f8fafcf0);border-right:1px solid var(--app-border);box-shadow:4px 0 16px #0f172a0a}.brand[data-v-c6ed865d],.drawer-brand[data-v-c6ed865d]{padding:18px 16px 14px}.brand[data-v-c6ed865d]{border-bottom:1px solid rgba(15,23,42,.06)}.brand-title[data-v-c6ed865d]{font-size:16px;font-weight:800;letter-spacing:.2px}.brand-sub[data-v-c6ed865d]{margin-top:4px;font-size:12px}.aside-menu[data-v-c6ed865d]{border-right:none;padding:8px;background:transparent}.aside-menu[data-v-c6ed865d] .el-menu-item{height:42px;line-height:42px;margin:3px 0;border-radius:10px;color:#334155;font-weight:600}.aside-menu[data-v-c6ed865d] .el-menu-item .el-icon{margin-right:10px}.aside-menu[data-v-c6ed865d] .el-menu-item:hover{background:#3b82f614;color:#1d4ed8}.aside-menu[data-v-c6ed865d] .el-menu-item.is-active{background:linear-gradient(135deg,#2563eb1f,#7c3aed1a);color:#1e40af}.menu-label[data-v-c6ed865d]{display:inline-flex;align-items:center;min-width:0}.menu-badge[data-v-c6ed865d]{display:inline-flex;align-items:center}.fallback-card[data-v-c6ed865d]{min-height:160px;border-radius:var(--app-radius-lg);border:1px solid var(--app-border)}.layout-header[data-v-c6ed865d]{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:12px;height:58px;padding:0 18px;background:#ffffffc7;-webkit-backdrop-filter:saturate(180%) blur(10px);backdrop-filter:saturate(180%) blur(10px);border-bottom:1px solid var(--app-border)}.header-left[data-v-c6ed865d]{display:flex;align-items:center;gap:10px;min-width:0}.header-title[data-v-c6ed865d]{font-size:15px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-menu-btn[data-v-c6ed865d]{padding-left:0;padding-right:0}.header-right[data-v-c6ed865d]{display:flex;align-items:center;gap:12px}.admin-name[data-v-c6ed865d]{display:flex;align-items:baseline;gap:8px;font-size:13px;color:#334155}.admin-name strong[data-v-c6ed865d]{color:#0f172a;font-weight:800}.logout-btn[data-v-c6ed865d]{min-width:74px}.layout-main[data-v-c6ed865d]{padding:18px}.main-shell[data-v-c6ed865d]{width:100%;max-width:1600px;margin:0 auto}@media(max-width:768px){.layout-header[data-v-c6ed865d]{flex-wrap:wrap;height:auto;padding:10px 12px}.header-right[data-v-c6ed865d]{width:100%;justify-content:flex-end}.admin-name .app-muted[data-v-c6ed865d],.admin-name strong[data-v-c6ed865d]{display:none}.layout-main[data-v-c6ed865d]{padding:12px}}:root{--app-bg: #f4f6fb;--app-text: #111827;--app-muted: #6b7280;--app-border: rgba(15, 23, 42, .1);--app-border-strong: rgba(15, 23, 42, .14);--app-radius: 12px;--app-radius-lg: 14px;--app-shadow-soft: 0 8px 24px rgba(15, 23, 42, .05);--app-shadow: 0 12px 30px rgba(15, 23, 42, .08);--app-card-bg: rgba(255, 255, 255, .94);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html,body,#app{height:100%}*{box-sizing:border-box}body{margin:0;color:var(--app-text);background:radial-gradient(1200px 500px at -10% -10%,rgba(59,130,246,.12),transparent 55%),radial-gradient(1000px 420px at 110% 0%,rgba(139,92,246,.1),transparent 50%),var(--app-bg)}a{color:inherit;text-decoration:none}.app-page-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.app-page-title h2{margin:0;font-size:19px;font-weight:800;letter-spacing:.2px}.app-muted{color:var(--app-muted)}.page-stack{display:flex;flex-direction:column;gap:14px;min-width:0}.el-card{border-radius:var(--app-radius-lg);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.el-button{border-radius:10px;font-weight:600}.el-input__wrapper,.el-textarea__inner,.el-select__wrapper,.el-input-number,.el-picker__wrapper{border-radius:10px}.el-table{border-radius:10px;overflow:hidden}.el-table th.el-table__cell{background:#f8fafc;color:#334155;font-weight:700}.el-table td.el-table__cell,.el-table th.el-table__cell{padding-top:11px;padding-bottom:11px}.el-table .el-table__row:hover>td.el-table__cell{background:#f8fbff}.el-tag{border-radius:999px}.el-dialog{border-radius:var(--app-radius-lg)}@media(max-width:768px){.app-page-title{flex-wrap:wrap;align-items:flex-start}.app-page-title h2{font-size:17px}.el-dialog{max-width:92vw}.el-form-item{flex-direction:column;align-items:stretch}.el-form-item__label{width:auto!important;justify-content:flex-start!important;padding:0 0 6px!important;line-height:1.4;text-align:left!important}.el-form-item__content{margin-left:0!important;width:100%}}.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.section-title{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.table-wrap{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.pagination{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint{font-size:12px}.el-tabs__item{font-weight:700}.el-form-item{margin-bottom:18px}@media(max-width:768px){.pagination{justify-content:flex-start}}@media(max-width:900px){.toolbar{width:100%}.toolbar>*{min-width:0}}@media(max-width:768px){.app-page-title>div{width:100%}.app-page-title .toolbar{width:100%}.toolbar>*{flex:1 1 calc(50% - 6px)}.toolbar .el-button,.toolbar .el-select,.toolbar .el-input,.toolbar .el-input-number{width:100%!important}.section-head{align-items:flex-start}.section-head>*{width:100%}.table-wrap{-webkit-overflow-scrolling:touch}.table-wrap .el-table{min-width:700px}.el-pagination{width:100%;justify-content:flex-start}}@media(max-width:520px){.toolbar>*{flex-basis:100%}.table-wrap .el-table{min-width:620px}} +.layout-root[data-v-520fd502]{height:100%}.layout-aside[data-v-520fd502]{background:linear-gradient(180deg,#fffffffa,#f8fafcf0);border-right:1px solid var(--app-border);box-shadow:4px 0 16px #0f172a0a}.brand[data-v-520fd502],.drawer-brand[data-v-520fd502]{padding:18px 16px 14px}.brand[data-v-520fd502]{border-bottom:1px solid rgba(15,23,42,.06)}.brand-title[data-v-520fd502]{font-size:16px;font-weight:800;letter-spacing:.2px}.brand-sub[data-v-520fd502]{margin-top:4px;font-size:12px}.aside-menu[data-v-520fd502]{border-right:none;padding:8px;background:transparent}.aside-menu[data-v-520fd502] .el-menu-item{height:42px;line-height:42px;margin:3px 0;border-radius:10px;color:#334155;font-weight:600}.aside-menu[data-v-520fd502] .el-menu-item .el-icon{margin-right:10px}.aside-menu[data-v-520fd502] .el-menu-item:hover{background:#3b82f614;color:#1d4ed8}.aside-menu[data-v-520fd502] .el-menu-item.is-active{background:linear-gradient(135deg,#2563eb1f,#7c3aed1a);color:#1e40af}.menu-label[data-v-520fd502]{display:inline-flex;align-items:center;min-width:0}.menu-badge[data-v-520fd502]{display:inline-flex;align-items:center}.fallback-card[data-v-520fd502]{min-height:160px;border-radius:var(--app-radius-lg);border:1px solid var(--app-border)}.layout-header[data-v-520fd502]{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:12px;height:58px;padding:0 18px;background:#ffffffc7;-webkit-backdrop-filter:saturate(180%) blur(10px);backdrop-filter:saturate(180%) blur(10px);border-bottom:1px solid var(--app-border)}.header-left[data-v-520fd502]{display:flex;align-items:center;gap:10px;min-width:0}.header-title[data-v-520fd502]{font-size:15px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-menu-btn[data-v-520fd502]{padding-left:0;padding-right:0}.header-right[data-v-520fd502]{display:flex;align-items:center;gap:12px}.admin-name[data-v-520fd502]{display:flex;align-items:baseline;gap:8px;font-size:13px;color:#334155}.admin-name strong[data-v-520fd502]{color:#0f172a;font-weight:800}.logout-btn[data-v-520fd502]{min-width:74px}.layout-main[data-v-520fd502]{padding:18px}.main-shell[data-v-520fd502]{width:100%;max-width:1600px;margin:0 auto}@media(max-width:768px){.layout-header[data-v-520fd502]{flex-wrap:wrap;height:auto;padding:10px 12px}.header-right[data-v-520fd502]{width:100%;justify-content:flex-end}.admin-name .app-muted[data-v-520fd502],.admin-name strong[data-v-520fd502]{display:none}.layout-main[data-v-520fd502]{padding:12px}}:root{--app-bg: #f4f6fb;--app-text: #111827;--app-muted: #6b7280;--app-border: rgba(15, 23, 42, .1);--app-border-strong: rgba(15, 23, 42, .14);--app-radius: 12px;--app-radius-lg: 14px;--app-shadow-soft: 0 8px 24px rgba(15, 23, 42, .05);--app-shadow: 0 12px 30px rgba(15, 23, 42, .08);--app-card-bg: rgba(255, 255, 255, .94);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html,body,#app{height:100%}*{box-sizing:border-box}body{margin:0;color:var(--app-text);background:radial-gradient(1200px 500px at -10% -10%,rgba(59,130,246,.12),transparent 55%),radial-gradient(1000px 420px at 110% 0%,rgba(139,92,246,.1),transparent 50%),var(--app-bg)}a{color:inherit;text-decoration:none}.app-page-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.app-page-title h2{margin:0;font-size:19px;font-weight:800;letter-spacing:.2px}.app-muted{color:var(--app-muted)}.page-stack{display:flex;flex-direction:column;gap:14px;min-width:0}.el-card{border-radius:var(--app-radius-lg);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.el-button{border-radius:10px;font-weight:600}.el-input__wrapper,.el-textarea__inner,.el-select__wrapper,.el-input-number,.el-picker__wrapper{border-radius:10px}.el-table{border-radius:10px;overflow:hidden}.el-table th.el-table__cell{background:#f8fafc;color:#334155;font-weight:700}.el-table td.el-table__cell,.el-table th.el-table__cell{padding-top:11px;padding-bottom:11px}.el-table .el-table__row:hover>td.el-table__cell{background:#f8fbff}.el-tag{border-radius:999px}.el-dialog{border-radius:var(--app-radius-lg)}@media(max-width:768px){.app-page-title{flex-wrap:wrap;align-items:flex-start}.app-page-title h2{font-size:17px}.el-dialog{max-width:92vw}.el-form-item{flex-direction:column;align-items:stretch}.el-form-item__label{width:auto!important;justify-content:flex-start!important;padding:0 0 6px!important;line-height:1.4;text-align:left!important}.el-form-item__content{margin-left:0!important;width:100%}}.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.section-title{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.table-wrap{overflow-x:auto;border-radius:10px;border:1px solid var(--app-border);background:#fff}.pagination{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint{font-size:12px}.el-tabs__item{font-weight:700}.el-form-item{margin-bottom:18px}@media(max-width:768px){.pagination{justify-content:flex-start}}@media(max-width:900px){.toolbar{width:100%}.toolbar>*{min-width:0}}@media(max-width:768px){.app-page-title>div{width:100%}.app-page-title .toolbar{width:100%}.toolbar>*{flex:1 1 calc(50% - 6px)}.toolbar .el-button,.toolbar .el-select,.toolbar .el-input,.toolbar .el-input-number{width:100%!important}.section-head{align-items:flex-start}.section-head>*{width:100%}.table-wrap{-webkit-overflow-scrolling:touch}.table-wrap .el-table{min-width:700px}.el-pagination{width:100%;justify-content:flex-start}}@media(max-width:520px){.toolbar>*{flex-basis:100%}.table-wrap .el-table{min-width:620px}} diff --git a/static/admin/assets/index-C1f9ticl.js b/static/admin/assets/index-DOvMEmc8.js similarity index 90% rename from static/admin/assets/index-C1f9ticl.js rename to static/admin/assets/index-DOvMEmc8.js index 40d8c53..5251037 100644 --- a/static/admin/assets/index-C1f9ticl.js +++ b/static/admin/assets/index-DOvMEmc8.js @@ -1,2 +1,2 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./ReportPage-fzVH-d9u.js","./vendor-element-B5S5pUKo.js","./vendor-vue-CVxSw_oJ.js","./vendor-misc-BeoNyvBp.js","./vendor-element-C68yOrAy.css","./email-px7YBG2O.js","./tasks-Bep0SUyu.js","./system-ZDPnxnIu.js","./MetricGrid-BnihYB_8.js","./MetricGrid-yP_dkP6X.css","./vendor-axios-B9ygI19o.js","./ReportPage-BCQBCnjY.css","./UsersPage-DJZUCpfb.js","./users-te9ySk34.js","./UsersPage-BNDnhJe0.css","./FeedbacksPage-BAnFKHSL.js","./FeedbacksPage-mrXjCiV2.css","./LogsPage-DFPeq0bL.js","./LogsPage-D1bozCEo.css","./AnnouncementsPage-C6UgwLIT.js","./AnnouncementsPage-DOwZaaOu.css","./EmailPage-CATruPK6.js","./EmailPage-BmPCDPYC.css","./SecurityPage-xwMQfhuh.js","./SecurityPage-DN76ndc_.css","./SystemPage-D3eBPCNe.js","./SystemPage-BhhEz4Qz.css","./SettingsPage-DRqlQLxJ.js","./SettingsPage-BAa-Qu3q.css"])))=>i.map(i=>d[i]); -import{aj as h,D as S,q as y,ax as Se,ay as ke,r as O,c as Z,o as Te,R as Ae,E as i,G as ee,L as _,t as m,n as I,K as te,a3 as ne,H as ae,J as C,I as se,az as Pe,p as $,aA as Re,aB as Le,as as xe}from"./vendor-vue-CVxSw_oJ.js";import{E as ie,a as le,d as Ne,u as Oe,c as Ce,l as Be,b as De,m as Me,e as Ve,t as Ie,s as Fe,i as Ke,z as $e}from"./vendor-element-B5S5pUKo.js";import{a as qe}from"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))s(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&s(l)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();const ue=(e,t)=>{const n=e.__vccOpts||e;for(const[s,a]of t)n[s]=a;return n},Ue={};function Ge(e,t){const n=h("RouterView");return y(),S(n)}const je=ue(Ue,[["render",Ge]]),He="modulepreload",Ye=function(e,t){return new URL(e,t).href},oe={},k=function(t,n,s){let a=Promise.resolve();if(n&&n.length>0){let T=function(f){return Promise.all(f.map(g=>Promise.resolve(g).then(w=>({status:"fulfilled",value:w}),w=>({status:"rejected",reason:w}))))};const l=document.getElementsByTagName("link"),r=document.querySelector("meta[property=csp-nonce]"),v=r?.nonce||r?.getAttribute("nonce");a=T(n.map(f=>{if(f=Ye(f,s),f in oe)return;oe[f]=!0;const g=f.endsWith(".css"),w=g?'[rel="stylesheet"]':"";if(s)for(let A=l.length-1;A>=0;A--){const E=l[A];if(E.href===f&&(!g||E.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${f}"]${w}`))return;const b=document.createElement("link");if(b.rel=g?"stylesheet":He,g||(b.as="script"),b.crossOrigin="",b.href=f,v&&b.setAttribute("nonce",v),document.head.appendChild(b),g)return new Promise((A,E)=>{b.addEventListener("load",A),b.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${f}`)))})}))}function o(l){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=l,window.dispatchEvent(r),!r.defaultPrevented)throw l}return a.then(l=>{for(const r of l||[])r.status==="rejected"&&o(r.reason);return t().catch(o)})};let re="",ce=0;const ze=new Set([408,425,429,500,502,503,504]),We=1,Je=300;function B(e,t,n=1500){const s=Date.now();e===re&&s-ce=We)return!1;const s=String(e?.code||"");if(s==="ECONNABORTED"||s==="ERR_NETWORK")return!0;const a=Number(e?.response?.status||0);return ze.has(a)}function et(e){return new Promise(t=>{window.setTimeout(t,Math.max(0,Number(e||0)))})}async function tt(e,t){const n=e?.config||{},s=Number(n.__retry_count||0);n.__retry_count=s+1;const a=Je*(s+1);return await et(a),t.request(n)}const p=qe.create({baseURL:"/yuyx/api",timeout:3e4,withCredentials:!0});let D=null;async function nt(){return D||(D=ie.prompt("请输入管理员密码进行二次确认","安全确认",{inputType:"password",inputPlaceholder:"管理员密码",confirmButtonText:"确认",cancelButtonText:"取消",inputValidator:e=>!!String(e||"").trim(),inputErrorMessage:"密码不能为空"}).then(async e=>{const t=String(e.value||"").trim();await p.post("/admin/reauth",{password:t}),le.success("已通过安全确认")}).finally(()=>{D=null}),D)}p.interceptors.request.use(e=>{const t=String(e?.method||"GET").toUpperCase();if(!["GET","HEAD","OPTIONS"].includes(t)){const n=Xe("csrf_token");n&&(e.headers=e.headers||{},e.headers["X-CSRF-Token"]=n)}return e});p.interceptors.response.use(e=>e,async e=>{const t=e?.response?.status,n=e?.response?.data,s=n?.error||n?.message||e?.message||"请求失败",a=!!e?.config?.__silent;if(n?.code==="reauth_required"&&e?.config&&!e.config.__reauth_retry)try{return e.config.__reauth_retry=!0,await nt(),p.request(e.config)}catch{return Promise.reject(e)}return Ze(e)?tt(e,p):(t===401?(a||B("401",s,3e3),(window.location?.pathname||"").startsWith("/yuyx")||(window.location.href="/yuyx")):t===403?a||B("403",s,5e3):t?a||B(`http:${t}:${s}`,s):e?.code==="ECONNABORTED"?a||B("timeout","请求超时",3e3):a||B(`net:${s}`,s,3e3),Promise.reject(e))});function de(e,t=0){let n=!1,s=null,a=0,o=null;async function l(v={}){const T=!!v?.force,f=Date.now();return!T&&n&&fe()).then(g=>{s=g,n=!0;const w=Math.max(0,Number(t)||0);return a=Date.now()+w,g}).finally(()=>{o=null})),o)}function r(){n=!1,s=null,a=0,o=null}return{run:l,clear:r}}const at=1e4,fe=de(async()=>{const{data:e}=await p.get("/feedbacks",{params:{limit:1,offset:0}});return e?.stats},at);async function Ut(e=""){const{data:t}=await p.get("/feedbacks",{params:e?{status:e}:{}});return t}async function st(e={}){return fe.run(e)}function q(){fe.clear()}async function Gt(e,t){const{data:n}=await p.post(`/feedbacks/${e}/reply`,{reply:t});return q(),n}async function jt(e){const{data:t}=await p.post(`/feedbacks/${e}/close`);return q(),t}async function Ht(e){const{data:t}=await p.delete(`/feedbacks/${e}`);return q(),t}const ot=15e3,rt=de(async()=>{const{data:e}=await p.get("/stats");return e},ot);async function ct(e={}){return rt.run(e)}async function it(e={},t={}){const{data:n}=await p.get("/kdocs/status",{params:e,...t});return n}async function Yt(e={}){const t={force:!0,...e},{data:n}=await p.post("/kdocs/qr",t);return n}async function zt(){const{data:e}=await p.post("/kdocs/clear-login",{});return e}const U="admin:kdocs:status:v1",me=300*1e3;let P=null,L=0,R=null;function G(){return Date.now()}function F(e){return!e||typeof e!="object"?{}:e}function lt(){try{const e=window.sessionStorage.getItem(U);if(!e)return null;const t=JSON.parse(e);if(!t||typeof t!="object")return null;const n=Number(t.updated_at||0),s=F(t.status);return n?{status:s,updatedAt:n}:null}catch{return null}}function ut(e,t){try{window.sessionStorage.setItem(U,JSON.stringify({status:F(e),updated_at:Number(t||G())}))}catch{}}function dt(){if(P!==null)return;const e=lt();e&&(P=e.status,L=e.updatedAt)}function pe(e){return P=F(e),L=G(),ut(P,L),P}function ft(e){if(P===null||!L)return!1;const t=Number(e);return!Number.isFinite(t)||t<0?!0:G()-L<=t}function mt(e={}){dt();const t=e.maxAgeMs??me;return ft(t)?F(P):null}function Wt(e){return pe(e)}function pt(){P=null,L=0,R=null;try{window.sessionStorage.removeItem(U)}catch{}}async function _t(e={}){const{force:t=!1,maxAgeMs:n=me,silent:s=!0,live:a=0}=e;if(!t){const r=mt({maxAgeMs:n});if(r)return r}return R||(R=it(a?{live:1}:{},{__silent:!!s,__no_retry:!0,timeout:8e3}).then(r=>pe(r||{})).finally(()=>{R=null}),R)}const ht={class:"menu-label"},yt={key:1,class:"menu-label"},gt={class:"header-left"},bt={class:"header-right"},vt={class:"admin-name"},wt={class:"main-shell"},Et={class:"menu-label"},St={key:1,class:"menu-label"},kt=6e4,Tt=18e4,At={__name:"AdminLayout",setup(e){const t=Se(),n=ke(),s=O({}),a=Z(()=>s.value?.admin_username||"");async function o(d={}){s.value=await ct(d)}const l=O(!1),r=O(0);let v=null;async function T(d=null){if(d&&typeof d=="object"){Object.prototype.hasOwnProperty.call(d,"pendingFeedbacks")&&(r.value=Number(d.pendingFeedbacks||0));return}if(!l.value){l.value=!0;try{const c=await st();r.value=Number(c?.pending||0)}finally{l.value=!1}}}function f(){return typeof document>"u"?!1:document.visibilityState==="hidden"}function g(){return f()?Tt:kt}function w(){v&&(window.clearTimeout(v),v=null)}function b(){w(),v=window.setTimeout(async()=>{v=null,await T().catch(()=>{}),b()},g())}function A(){b()}$("refreshStats",o),$("adminStats",s),$("refreshNavBadges",T);const E=O(!1),x=O(!1);let M;function K(){E.value=!!M?.matches,E.value||(x.value=!1)}Te(async()=>{M=window.matchMedia("(max-width: 768px)"),M.addEventListener?.("change",K),K(),_t({maxAgeMs:6e4,silent:!0}).catch(()=>{}),await o(),await T(),b(),window.addEventListener("visibilitychange",A)}),Ae(()=>{M?.removeEventListener?.("change",K),w(),window.removeEventListener("visibilitychange",A)});const j=[{path:"/reports",label:"报表",icon:Ne},{path:"/users",label:"用户",icon:Oe},{path:"/feedbacks",label:"反馈",icon:Ce,badgeKey:"feedbacks"},{path:"/logs",label:"任务日志",icon:Be},{path:"/announcements",label:"公告",icon:De},{path:"/email",label:"邮件",icon:Me},{path:"/security",label:"安全防护",icon:Ve},{path:"/system",label:"系统配置",icon:Ie},{path:"/settings",label:"设置",icon:Fe}],H=Z(()=>t.path);function V(d){return d?.badgeKey&&d.badgeKey==="feedbacks"?Number(r.value||0):0}async function _e(){let d=!1;try{await ie.confirm("确定退出管理员登录吗?","退出登录",{confirmButtonText:"退出",cancelButtonText:"取消",type:"warning"}),d=!0}catch(c){const N=String(c||"").toLowerCase();if(N==="cancel"||N==="close")return;try{d=window.confirm("确定退出管理员登录吗?")}catch{d=!1}}if(d)try{await p.post("/logout")}finally{pt(),window.location.href="/yuyx"}}async function Y(d){await n.push(d),x.value=!1}return(d,c)=>{const N=h("el-icon"),z=h("el-badge"),W=h("el-menu-item"),J=h("el-menu"),he=h("el-aside"),X=h("el-button"),ye=h("el-header"),ge=h("RouterView"),be=h("el-skeleton"),ve=h("el-card"),we=h("el-main"),Q=h("el-container"),Ee=h("el-drawer");return y(),S(Q,{class:"layout-root"},{default:i(()=>[E.value?ee("",!0):(y(),S(he,{key:0,width:"220px",class:"layout-aside"},{default:i(()=>[c[2]||(c[2]=m("div",{class:"brand"},[m("div",{class:"brand-title"},"后台管理"),m("div",{class:"brand-sub app-muted"},"知识管理平台")],-1)),_(J,{"default-active":H.value,class:"aside-menu",router:"",onSelect:Y},{default:i(()=>[(y(),I(te,null,ne(j,u=>_(W,{key:u.path,index:u.path},{default:i(()=>[_(N,null,{default:i(()=>[(y(),S(ae(u.icon)))]),_:2},1024),V(u)>0?(y(),S(z,{key:0,value:V(u),max:99,class:"menu-badge"},{default:i(()=>[m("span",ht,C(u.label),1)]),_:2},1032,["value"])):(y(),I("span",yt,C(u.label),1))]),_:2},1032,["index"])),64))]),_:1},8,["default-active"])]),_:1})),_(Q,null,{default:i(()=>[_(ye,{class:"layout-header"},{default:i(()=>[m("div",gt,[E.value?(y(),S(X,{key:0,text:"",class:"header-menu-btn",onClick:c[0]||(c[0]=u=>x.value=!0)},{default:i(()=>[...c[3]||(c[3]=[se(" 菜单 ",-1)])]),_:1})):ee("",!0),c[4]||(c[4]=m("div",{class:"header-title"},"后台管理系统",-1))]),m("div",bt,[m("div",vt,[c[5]||(c[5]=m("span",{class:"app-muted"},"管理员",-1)),m("strong",null,C(a.value||"-"),1)]),_(X,{type:"primary",plain:"",class:"logout-btn",onClick:_e},{default:i(()=>[...c[6]||(c[6]=[se("退出",-1)])]),_:1})])]),_:1}),_(we,{class:"layout-main"},{default:i(()=>[m("div",wt,[(y(),S(Pe,null,{default:i(()=>[_(ge)]),fallback:i(()=>[_(ve,{shadow:"never","body-style":{padding:"16px"},class:"fallback-card"},{default:i(()=>[_(be,{rows:5,animated:""})]),_:1})]),_:1}))])]),_:1})]),_:1}),_(Ee,{modelValue:x.value,"onUpdate:modelValue":c[1]||(c[1]=u=>x.value=u),size:"min(82vw, 280px)",direction:"ltr","with-header":!1},{default:i(()=>[c[7]||(c[7]=m("div",{class:"drawer-brand"},[m("div",{class:"brand-title"},"后台管理"),m("div",{class:"brand-sub app-muted"},"知识管理平台")],-1)),_(J,{"default-active":H.value,class:"aside-menu",router:"",onSelect:Y},{default:i(()=>[(y(),I(te,null,ne(j,u=>_(W,{key:u.path,index:u.path},{default:i(()=>[_(N,null,{default:i(()=>[(y(),S(ae(u.icon)))]),_:2},1024),V(u)>0?(y(),S(z,{key:0,value:V(u),max:99,class:"menu-badge"},{default:i(()=>[m("span",Et,C(u.label),1)]),_:2},1032,["value"])):(y(),I("span",St,C(u.label),1))]),_:2},1032,["index"])),64))]),_:1},8,["default-active"])]),_:1},8,["modelValue"])]),_:1})}}},Pt=ue(At,[["__scopeId","data-v-c6ed865d"]]),Rt=()=>k(()=>import("./ReportPage-fzVH-d9u.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11]),import.meta.url),Lt=()=>k(()=>import("./UsersPage-DJZUCpfb.js"),__vite__mapDeps([12,13,1,2,3,4,10,14]),import.meta.url),xt=()=>k(()=>import("./FeedbacksPage-BAnFKHSL.js"),__vite__mapDeps([15,8,2,9,1,3,4,10,16]),import.meta.url),Nt=()=>k(()=>import("./LogsPage-DFPeq0bL.js"),__vite__mapDeps([17,13,6,1,2,3,4,10,18]),import.meta.url),Ot=()=>k(()=>import("./AnnouncementsPage-C6UgwLIT.js"),__vite__mapDeps([19,1,2,3,4,10,20]),import.meta.url),Ct=()=>k(()=>import("./EmailPage-CATruPK6.js"),__vite__mapDeps([21,5,8,2,9,1,3,4,10,22]),import.meta.url),Bt=()=>k(()=>import("./SecurityPage-xwMQfhuh.js"),__vite__mapDeps([23,8,2,9,1,3,4,10,24]),import.meta.url),Dt=()=>k(()=>import("./SystemPage-D3eBPCNe.js"),__vite__mapDeps([25,7,1,2,3,4,10,26]),import.meta.url),Mt=()=>k(()=>import("./SettingsPage-DRqlQLxJ.js"),__vite__mapDeps([27,1,2,3,4,10,28]),import.meta.url),Vt=[{path:"/",component:Pt,children:[{path:"",redirect:"/reports"},{path:"/pending",redirect:"/reports"},{path:"/stats",redirect:"/reports"},{path:"/reports",name:"reports",component:Rt},{path:"/users",name:"users",component:Lt},{path:"/feedbacks",name:"feedbacks",component:xt},{path:"/logs",name:"logs",component:Nt},{path:"/announcements",name:"announcements",component:Ot},{path:"/email",name:"email",component:Ct},{path:"/security",name:"security",component:Bt},{path:"/system",name:"system",component:Dt},{path:"/settings",name:"settings",component:Mt}]}],It=Re({history:Le(),routes:Vt});xe(je).use(It).use(Ke,{locale:$e}).mount("#app");export{ue as _,p as a,Ut as b,de as c,jt as d,Ht as e,st as f,mt as g,Yt as h,zt as i,it as j,_t as p,Gt as r,Wt as u}; +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./ReportPage-BMEJM5Hr.js","./vendor-element-B5S5pUKo.js","./vendor-vue-CVxSw_oJ.js","./vendor-misc-BeoNyvBp.js","./vendor-element-C68yOrAy.css","./email-Mh1SHQbX.js","./tasks-B7oNpIBD.js","./system-CYbWdReq.js","./MetricGrid-C3Xjc9mZ.js","./MetricGrid-BR486o_b.css","./vendor-axios-B9ygI19o.js","./ReportPage-IaDpUFfl.css","./UsersPage-RI5S3snx.js","./users-DzDcz9C_.js","./UsersPage-CgYh6JHW.css","./FeedbacksPage-DrMVqBKf.js","./FeedbacksPage-CPmSqIaj.css","./LogsPage-Cy6Q0ave.js","./LogsPage-BUgx3sZr.css","./AnnouncementsPage-Dagm5PzE.js","./AnnouncementsPage-tpO97PUg.css","./EmailPage-DiZA9Kx_.js","./EmailPage-CTHxGzDv.css","./SecurityPage-yzYEGeTN.js","./SecurityPage-C2-mJ7eD.css","./SystemPage-DrM9-RI5.js","./SystemPage-CTs6qr36.css","./SettingsPage-DF5fL8gq.js","./SettingsPage-D-iYz1zh.css"])))=>i.map(i=>d[i]); +import{aj as h,D as S,q as y,ax as Se,ay as ke,r as O,c as Z,o as Te,R as Ae,E as i,G as ee,L as _,t as m,n as I,K as te,a3 as ne,H as ae,J as C,I as se,az as Pe,p as $,aA as Re,aB as Le,as as xe}from"./vendor-vue-CVxSw_oJ.js";import{E as ie,a as le,d as Ne,u as Oe,c as Ce,l as Be,b as De,m as Me,e as Ve,t as Ie,s as Fe,i as Ke,z as $e}from"./vendor-element-B5S5pUKo.js";import{a as qe}from"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))s(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&s(l)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();const ue=(e,t)=>{const n=e.__vccOpts||e;for(const[s,a]of t)n[s]=a;return n},Ue={};function Ge(e,t){const n=h("RouterView");return y(),S(n)}const je=ue(Ue,[["render",Ge]]),He="modulepreload",Ye=function(e,t){return new URL(e,t).href},oe={},k=function(t,n,s){let a=Promise.resolve();if(n&&n.length>0){let T=function(f){return Promise.all(f.map(g=>Promise.resolve(g).then(w=>({status:"fulfilled",value:w}),w=>({status:"rejected",reason:w}))))};const l=document.getElementsByTagName("link"),r=document.querySelector("meta[property=csp-nonce]"),v=r?.nonce||r?.getAttribute("nonce");a=T(n.map(f=>{if(f=Ye(f,s),f in oe)return;oe[f]=!0;const g=f.endsWith(".css"),w=g?'[rel="stylesheet"]':"";if(s)for(let A=l.length-1;A>=0;A--){const E=l[A];if(E.href===f&&(!g||E.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${f}"]${w}`))return;const b=document.createElement("link");if(b.rel=g?"stylesheet":He,g||(b.as="script"),b.crossOrigin="",b.href=f,v&&b.setAttribute("nonce",v),document.head.appendChild(b),g)return new Promise((A,E)=>{b.addEventListener("load",A),b.addEventListener("error",()=>E(new Error(`Unable to preload CSS for ${f}`)))})}))}function o(l){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=l,window.dispatchEvent(r),!r.defaultPrevented)throw l}return a.then(l=>{for(const r of l||[])r.status==="rejected"&&o(r.reason);return t().catch(o)})};let re="",ce=0;const ze=new Set([408,425,429,500,502,503,504]),We=1,Je=300;function B(e,t,n=1500){const s=Date.now();e===re&&s-ce=We)return!1;const s=String(e?.code||"");if(s==="ECONNABORTED"||s==="ERR_NETWORK")return!0;const a=Number(e?.response?.status||0);return ze.has(a)}function et(e){return new Promise(t=>{window.setTimeout(t,Math.max(0,Number(e||0)))})}async function tt(e,t){const n=e?.config||{},s=Number(n.__retry_count||0);n.__retry_count=s+1;const a=Je*(s+1);return await et(a),t.request(n)}const p=qe.create({baseURL:"/yuyx/api",timeout:3e4,withCredentials:!0});let D=null;async function nt(){return D||(D=ie.prompt("请输入管理员密码进行二次确认","安全确认",{inputType:"password",inputPlaceholder:"管理员密码",confirmButtonText:"确认",cancelButtonText:"取消",inputValidator:e=>!!String(e||"").trim(),inputErrorMessage:"密码不能为空"}).then(async e=>{const t=String(e.value||"").trim();await p.post("/admin/reauth",{password:t}),le.success("已通过安全确认")}).finally(()=>{D=null}),D)}p.interceptors.request.use(e=>{const t=String(e?.method||"GET").toUpperCase();if(!["GET","HEAD","OPTIONS"].includes(t)){const n=Xe("csrf_token");n&&(e.headers=e.headers||{},e.headers["X-CSRF-Token"]=n)}return e});p.interceptors.response.use(e=>e,async e=>{const t=e?.response?.status,n=e?.response?.data,s=n?.error||n?.message||e?.message||"请求失败",a=!!e?.config?.__silent;if(n?.code==="reauth_required"&&e?.config&&!e.config.__reauth_retry)try{return e.config.__reauth_retry=!0,await nt(),p.request(e.config)}catch{return Promise.reject(e)}return Ze(e)?tt(e,p):(t===401?(a||B("401",s,3e3),(window.location?.pathname||"").startsWith("/yuyx")||(window.location.href="/yuyx")):t===403?a||B("403",s,5e3):t?a||B(`http:${t}:${s}`,s):e?.code==="ECONNABORTED"?a||B("timeout","请求超时",3e3):a||B(`net:${s}`,s,3e3),Promise.reject(e))});function de(e,t=0){let n=!1,s=null,a=0,o=null;async function l(v={}){const T=!!v?.force,f=Date.now();return!T&&n&&fe()).then(g=>{s=g,n=!0;const w=Math.max(0,Number(t)||0);return a=Date.now()+w,g}).finally(()=>{o=null})),o)}function r(){n=!1,s=null,a=0,o=null}return{run:l,clear:r}}const at=1e4,fe=de(async()=>{const{data:e}=await p.get("/feedbacks",{params:{limit:1,offset:0}});return e?.stats},at);async function Ut(e=""){const{data:t}=await p.get("/feedbacks",{params:e?{status:e}:{}});return t}async function st(e={}){return fe.run(e)}function q(){fe.clear()}async function Gt(e,t){const{data:n}=await p.post(`/feedbacks/${e}/reply`,{reply:t});return q(),n}async function jt(e){const{data:t}=await p.post(`/feedbacks/${e}/close`);return q(),t}async function Ht(e){const{data:t}=await p.delete(`/feedbacks/${e}`);return q(),t}const ot=15e3,rt=de(async()=>{const{data:e}=await p.get("/stats");return e},ot);async function ct(e={}){return rt.run(e)}async function it(e={},t={}){const{data:n}=await p.get("/kdocs/status",{params:e,...t});return n}async function Yt(e={}){const t={force:!0,...e},{data:n}=await p.post("/kdocs/qr",t);return n}async function zt(){const{data:e}=await p.post("/kdocs/clear-login",{});return e}const U="admin:kdocs:status:v1",me=300*1e3;let P=null,L=0,R=null;function G(){return Date.now()}function F(e){return!e||typeof e!="object"?{}:e}function lt(){try{const e=window.sessionStorage.getItem(U);if(!e)return null;const t=JSON.parse(e);if(!t||typeof t!="object")return null;const n=Number(t.updated_at||0),s=F(t.status);return n?{status:s,updatedAt:n}:null}catch{return null}}function ut(e,t){try{window.sessionStorage.setItem(U,JSON.stringify({status:F(e),updated_at:Number(t||G())}))}catch{}}function dt(){if(P!==null)return;const e=lt();e&&(P=e.status,L=e.updatedAt)}function pe(e){return P=F(e),L=G(),ut(P,L),P}function ft(e){if(P===null||!L)return!1;const t=Number(e);return!Number.isFinite(t)||t<0?!0:G()-L<=t}function mt(e={}){dt();const t=e.maxAgeMs??me;return ft(t)?F(P):null}function Wt(e){return pe(e)}function pt(){P=null,L=0,R=null;try{window.sessionStorage.removeItem(U)}catch{}}async function _t(e={}){const{force:t=!1,maxAgeMs:n=me,silent:s=!0,live:a=0}=e;if(!t){const r=mt({maxAgeMs:n});if(r)return r}return R||(R=it(a?{live:1}:{},{__silent:!!s,__no_retry:!0,timeout:8e3}).then(r=>pe(r||{})).finally(()=>{R=null}),R)}const ht={class:"menu-label"},yt={key:1,class:"menu-label"},gt={class:"header-left"},bt={class:"header-right"},vt={class:"admin-name"},wt={class:"main-shell"},Et={class:"menu-label"},St={key:1,class:"menu-label"},kt=6e4,Tt=18e4,At={__name:"AdminLayout",setup(e){const t=Se(),n=ke(),s=O({}),a=Z(()=>s.value?.admin_username||"");async function o(d={}){s.value=await ct(d)}const l=O(!1),r=O(0);let v=null;async function T(d=null){if(d&&typeof d=="object"){Object.prototype.hasOwnProperty.call(d,"pendingFeedbacks")&&(r.value=Number(d.pendingFeedbacks||0));return}if(!l.value){l.value=!0;try{const c=await st();r.value=Number(c?.pending||0)}finally{l.value=!1}}}function f(){return typeof document>"u"?!1:document.visibilityState==="hidden"}function g(){return f()?Tt:kt}function w(){v&&(window.clearTimeout(v),v=null)}function b(){w(),v=window.setTimeout(async()=>{v=null,await T().catch(()=>{}),b()},g())}function A(){b()}$("refreshStats",o),$("adminStats",s),$("refreshNavBadges",T);const E=O(!1),x=O(!1);let M;function K(){E.value=!!M?.matches,E.value||(x.value=!1)}Te(async()=>{M=window.matchMedia("(max-width: 768px)"),M.addEventListener?.("change",K),K(),_t({maxAgeMs:6e4,silent:!0}).catch(()=>{}),await o(),await T(),b(),window.addEventListener("visibilitychange",A)}),Ae(()=>{M?.removeEventListener?.("change",K),w(),window.removeEventListener("visibilitychange",A)});const j=[{path:"/reports",label:"报表",icon:Ne},{path:"/users",label:"用户",icon:Oe},{path:"/feedbacks",label:"反馈",icon:Ce,badgeKey:"feedbacks"},{path:"/logs",label:"任务日志",icon:Be},{path:"/announcements",label:"公告",icon:De},{path:"/email",label:"邮件",icon:Me},{path:"/security",label:"安全防护",icon:Ve},{path:"/system",label:"系统配置",icon:Ie},{path:"/settings",label:"设置",icon:Fe}],H=Z(()=>t.path);function V(d){return d?.badgeKey&&d.badgeKey==="feedbacks"?Number(r.value||0):0}async function _e(){let d=!1;try{await ie.confirm("确定退出管理员登录吗?","退出登录",{confirmButtonText:"退出",cancelButtonText:"取消",type:"warning"}),d=!0}catch(c){const N=String(c||"").toLowerCase();if(N==="cancel"||N==="close")return;try{d=window.confirm("确定退出管理员登录吗?")}catch{d=!1}}if(d)try{await p.post("/logout")}finally{pt(),window.location.href="/yuyx"}}async function Y(d){await n.push(d),x.value=!1}return(d,c)=>{const N=h("el-icon"),z=h("el-badge"),W=h("el-menu-item"),J=h("el-menu"),he=h("el-aside"),X=h("el-button"),ye=h("el-header"),ge=h("RouterView"),be=h("el-skeleton"),ve=h("el-card"),we=h("el-main"),Q=h("el-container"),Ee=h("el-drawer");return y(),S(Q,{class:"layout-root"},{default:i(()=>[E.value?ee("",!0):(y(),S(he,{key:0,width:"220px",class:"layout-aside"},{default:i(()=>[c[2]||(c[2]=m("div",{class:"brand"},[m("div",{class:"brand-title"},"后台管理"),m("div",{class:"brand-sub app-muted"},"知识管理平台")],-1)),_(J,{"default-active":H.value,class:"aside-menu",router:"",onSelect:Y},{default:i(()=>[(y(),I(te,null,ne(j,u=>_(W,{key:u.path,index:u.path},{default:i(()=>[_(N,null,{default:i(()=>[(y(),S(ae(u.icon)))]),_:2},1024),V(u)>0?(y(),S(z,{key:0,value:V(u),max:99,class:"menu-badge"},{default:i(()=>[m("span",ht,C(u.label),1)]),_:2},1032,["value"])):(y(),I("span",yt,C(u.label),1))]),_:2},1032,["index"])),64))]),_:1},8,["default-active"])]),_:1})),_(Q,null,{default:i(()=>[_(ye,{class:"layout-header"},{default:i(()=>[m("div",gt,[E.value?(y(),S(X,{key:0,text:"",class:"header-menu-btn",onClick:c[0]||(c[0]=u=>x.value=!0)},{default:i(()=>[...c[3]||(c[3]=[se(" 菜单 ",-1)])]),_:1})):ee("",!0),c[4]||(c[4]=m("div",{class:"header-title"},"后台管理系统",-1))]),m("div",bt,[m("div",vt,[c[5]||(c[5]=m("span",{class:"app-muted"},"管理员",-1)),m("strong",null,C(a.value||"-"),1)]),_(X,{type:"primary",plain:"",class:"logout-btn",onClick:_e},{default:i(()=>[...c[6]||(c[6]=[se("退出",-1)])]),_:1})])]),_:1}),_(we,{class:"layout-main"},{default:i(()=>[m("div",wt,[(y(),S(Pe,null,{default:i(()=>[_(ge)]),fallback:i(()=>[_(ve,{shadow:"never","body-style":{padding:"16px"},class:"fallback-card"},{default:i(()=>[_(be,{rows:5,animated:""})]),_:1})]),_:1}))])]),_:1})]),_:1}),_(Ee,{modelValue:x.value,"onUpdate:modelValue":c[1]||(c[1]=u=>x.value=u),size:"min(82vw, 280px)",direction:"ltr","with-header":!1},{default:i(()=>[c[7]||(c[7]=m("div",{class:"drawer-brand"},[m("div",{class:"brand-title"},"后台管理"),m("div",{class:"brand-sub app-muted"},"知识管理平台")],-1)),_(J,{"default-active":H.value,class:"aside-menu",router:"",onSelect:Y},{default:i(()=>[(y(),I(te,null,ne(j,u=>_(W,{key:u.path,index:u.path},{default:i(()=>[_(N,null,{default:i(()=>[(y(),S(ae(u.icon)))]),_:2},1024),V(u)>0?(y(),S(z,{key:0,value:V(u),max:99,class:"menu-badge"},{default:i(()=>[m("span",Et,C(u.label),1)]),_:2},1032,["value"])):(y(),I("span",St,C(u.label),1))]),_:2},1032,["index"])),64))]),_:1},8,["default-active"])]),_:1},8,["modelValue"])]),_:1})}}},Pt=ue(At,[["__scopeId","data-v-520fd502"]]),Rt=()=>k(()=>import("./ReportPage-BMEJM5Hr.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11]),import.meta.url),Lt=()=>k(()=>import("./UsersPage-RI5S3snx.js"),__vite__mapDeps([12,13,1,2,3,4,10,14]),import.meta.url),xt=()=>k(()=>import("./FeedbacksPage-DrMVqBKf.js"),__vite__mapDeps([15,8,2,9,1,3,4,10,16]),import.meta.url),Nt=()=>k(()=>import("./LogsPage-Cy6Q0ave.js"),__vite__mapDeps([17,13,6,1,2,3,4,10,18]),import.meta.url),Ot=()=>k(()=>import("./AnnouncementsPage-Dagm5PzE.js"),__vite__mapDeps([19,1,2,3,4,10,20]),import.meta.url),Ct=()=>k(()=>import("./EmailPage-DiZA9Kx_.js"),__vite__mapDeps([21,5,8,2,9,1,3,4,10,22]),import.meta.url),Bt=()=>k(()=>import("./SecurityPage-yzYEGeTN.js"),__vite__mapDeps([23,8,2,9,1,3,4,10,24]),import.meta.url),Dt=()=>k(()=>import("./SystemPage-DrM9-RI5.js"),__vite__mapDeps([25,7,1,2,3,4,10,26]),import.meta.url),Mt=()=>k(()=>import("./SettingsPage-DF5fL8gq.js"),__vite__mapDeps([27,1,2,3,4,10,28]),import.meta.url),Vt=[{path:"/",component:Pt,children:[{path:"",redirect:"/reports"},{path:"/pending",redirect:"/reports"},{path:"/stats",redirect:"/reports"},{path:"/reports",name:"reports",component:Rt},{path:"/users",name:"users",component:Lt},{path:"/feedbacks",name:"feedbacks",component:xt},{path:"/logs",name:"logs",component:Nt},{path:"/announcements",name:"announcements",component:Ot},{path:"/email",name:"email",component:Ct},{path:"/security",name:"security",component:Bt},{path:"/system",name:"system",component:Dt},{path:"/settings",name:"settings",component:Mt}]}],It=Re({history:Le(),routes:Vt});xe(je).use(It).use(Ke,{locale:$e}).mount("#app");export{ue as _,p as a,Ut as b,de as c,jt as d,Ht as e,st as f,mt as g,Yt as h,zt as i,it as j,_t as p,Gt as r,Wt as u}; diff --git a/static/admin/assets/system-CYbWdReq.js b/static/admin/assets/system-CYbWdReq.js new file mode 100644 index 0000000..933b17c --- /dev/null +++ b/static/admin/assets/system-CYbWdReq.js @@ -0,0 +1 @@ +import{c as s,a as n}from"./index-DOvMEmc8.js";const o=s(async()=>{const{data:t}=await n.get("/system/config");return t},15e3);async function i(t={}){return o.run(t)}async function e(t){const{data:a}=await n.post("/system/config",t);return o.clear(),a}async function r(){const{data:t}=await n.get("/social-login/config");return t}async function f(t){const{data:a}=await n.post("/social-login/config",t||{});return o.clear(),a}async function g(t){const{data:a}=await n.post("/social-login/test",t||{});return a}export{r as a,f as b,i as f,g as t,e as u}; diff --git a/static/admin/assets/system-ZDPnxnIu.js b/static/admin/assets/system-ZDPnxnIu.js deleted file mode 100644 index 25fa192..0000000 --- a/static/admin/assets/system-ZDPnxnIu.js +++ /dev/null @@ -1 +0,0 @@ -import{c as s,a}from"./index-C1f9ticl.js";const e=s(async()=>{const{data:t}=await a.get("/system/config");return t},15e3);async function o(t={}){return e.run(t)}async function r(t){const{data:n}=await a.post("/system/config",t);return e.clear(),n}export{o as f,r as u}; diff --git a/static/admin/assets/tasks-Bep0SUyu.js b/static/admin/assets/tasks-B7oNpIBD.js similarity index 93% rename from static/admin/assets/tasks-Bep0SUyu.js rename to static/admin/assets/tasks-B7oNpIBD.js index 0aa11cf..653acb4 100644 --- a/static/admin/assets/tasks-Bep0SUyu.js +++ b/static/admin/assets/tasks-B7oNpIBD.js @@ -1 +1 @@ -import{c as s,a}from"./index-C1f9ticl.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h}; +import{c as s,a}from"./index-DOvMEmc8.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h}; diff --git a/static/admin/assets/users-te9ySk34.js b/static/admin/assets/users-DzDcz9C_.js similarity index 90% rename from static/admin/assets/users-te9ySk34.js rename to static/admin/assets/users-DzDcz9C_.js index 35d9d92..ccb411b 100644 --- a/static/admin/assets/users-te9ySk34.js +++ b/static/admin/assets/users-DzDcz9C_.js @@ -1 +1 @@ -import{a as t}from"./index-C1f9ticl.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-DOvMEmc8.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}; diff --git a/static/admin/index.html b/static/admin/index.html index 8727cf6..725cf66 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -5,15 +5,16 @@ 后台管理 - 知识管理平台 - + - + +