feat: add admin social login bindings

This commit is contained in:
237899745
2026-05-27 21:24:48 +08:00
parent 5dbe666420
commit 89cb98233f
39 changed files with 904 additions and 123 deletions

View File

@@ -11,6 +11,7 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"axios": "^1.12.2", "axios": "^1.12.2",
"element-plus": "^2.11.3", "element-plus": "^2.11.3",
"qrcode.vue": "^3.6.0",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
@@ -900,7 +901,6 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@@ -1512,15 +1512,13 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
@@ -1614,7 +1612,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -1656,6 +1653,15 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "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/rollup": { "node_modules/rollup": {
"version": "4.53.3", "version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
@@ -1730,7 +1736,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -1805,7 +1810,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.25", "@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25", "@vue/compiler-sfc": "3.5.25",

View File

@@ -12,6 +12,7 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"axios": "^1.12.2", "axios": "^1.12.2",
"element-plus": "^2.11.3", "element-plus": "^2.11.3",
"qrcode.vue": "^3.6.0",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },

View File

@@ -44,3 +44,28 @@ export async function reportAdminPasskeyClientError(payload = {}) {
const { data } = await api.post('/admin/passkeys/client-error', payload) const { data } = await api.post('/admin/passkeys/client-error', payload)
return data return data
} }
export async function fetchAdminSocialBindings() {
const { data } = await api.get('/admin/social-bindings')
return data
}
export async function createAdminSocialLoginUrl(payload = {}) {
const { data } = await api.post('/admin/social-login-url', payload)
return data
}
export async function pollAdminSocialLogin(payload = {}) {
const { data } = await api.post('/admin/social-poll', payload)
return data
}
export async function bindAdminSocialCallback(provider, payload = {}) {
const { data } = await api.post(`/admin/social-bindings/${encodeURIComponent(provider)}/callback`, payload)
return data
}
export async function unbindAdminSocial(provider) {
const { data } = await api.delete(`/admin/social-bindings/${encodeURIComponent(provider)}`)
return data
}

View File

@@ -0,0 +1,73 @@
<script setup>
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
import { bindAdminSocialCallback } from '../api/admin'
const route = useRoute()
const statusText = ref('正在完成绑定')
onMounted(async () => {
const hashQuery = String(window.location.hash || '').split('?')[1] || ''
const params = new URLSearchParams(window.location.search || hashQuery)
const provider = String(params.get('provider') || params.get('type') || '').trim().toLowerCase()
const code = String(params.get('code') || '').trim()
const routeProvider = String(route.query?.provider || route.query?.type || '').trim().toLowerCase()
const routeCode = String(route.query?.code || '').trim()
const finalProvider = provider || routeProvider
const finalCode = code || routeCode
if (!finalProvider || !finalCode) {
ElMessage.error('快捷登录回调参数不完整')
window.location.replace('/yuyx/admin#/settings')
return
}
try {
await bindAdminSocialCallback(finalProvider, { provider: finalProvider, code: finalCode })
ElMessage.success('管理员快捷登录已绑定')
window.location.replace('/yuyx/admin#/settings')
} catch (error) {
const payload = error?.response?.data
statusText.value = payload?.error || '快捷登录绑定失败'
ElMessage.error(statusText.value)
window.setTimeout(() => {
window.location.replace('/yuyx/admin#/settings')
}, 1200)
}
})
</script>
<template>
<div class="callback-wrap">
<el-card shadow="never" class="callback-card">
<el-skeleton :rows="3" animated />
<div class="callback-text">{{ statusText }}</div>
</el-card>
</div>
</template>
<style scoped>
.callback-wrap {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: #f6f7fb;
}
.callback-card {
width: min(420px, 94vw);
border-radius: 12px;
border: 1px solid var(--app-border);
}
.callback-text {
margin-top: 12px;
color: var(--app-muted);
font-size: 13px;
text-align: center;
}
</style>

View File

@@ -1,14 +1,19 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import QrcodeVue from 'qrcode.vue'
import { import {
createAdminSocialLoginUrl,
createAdminPasskeyOptions, createAdminPasskeyOptions,
createAdminPasskeyVerify, createAdminPasskeyVerify,
fetchAdminSocialBindings,
deleteAdminPasskey, deleteAdminPasskey,
fetchAdminPasskeys, fetchAdminPasskeys,
logout, logout,
pollAdminSocialLogin,
reportAdminPasskeyClientError, reportAdminPasskeyClientError,
unbindAdminSocial,
updateAdminPassword, updateAdminPassword,
updateAdminUsername, updateAdminUsername,
} from '../api/admin' } from '../api/admin'
@@ -26,6 +31,23 @@ const passkeyItems = ref([])
const passkeyRegisterOptions = ref(null) const passkeyRegisterOptions = ref(null)
const passkeyRegisterOptionsAt = ref(0) const passkeyRegisterOptionsAt = ref(0)
const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000 const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000
const socialBindingsLoading = ref(false)
const socialBindLoadingProvider = ref('')
const socialBindings = ref([])
const socialEnabled = ref(false)
const qrOpen = ref(false)
const qrValue = ref('')
const qrProvider = ref('wx')
let socialPollTimer = null
let socialPollStartedAt = 0
const providerLabels = {
qq: 'QQ',
wx: '微信',
alipay: '支付宝',
}
const visibleSocialBindings = computed(() => socialBindings.value.filter((item) => providerLabels[item.provider]))
function validateStrongPassword(value) { function validateStrongPassword(value) {
const text = String(value || '') const text = String(value || '')
@@ -233,8 +255,137 @@ async function removePasskey(item) {
} }
} }
function socialBindRedirectUri() {
const url = new URL(window.location.href)
url.pathname = '/yuyx/admin-social-bind-callback'
url.search = ''
url.hash = ''
return url.toString()
}
function socialProviderIcon(provider) {
if (provider === 'wx') return '微'
if (provider === 'qq') return 'Q'
return '支'
}
function socialQrPrompt(provider) {
if (provider === 'wx') return '请使用微信扫描二维码完成绑定'
if (provider === 'qq') return '请使用 QQ 扫描二维码完成绑定'
return '请使用支付宝扫描二维码完成绑定'
}
function stopSocialPolling() {
if (socialPollTimer) {
window.clearTimeout(socialPollTimer)
socialPollTimer = null
}
}
function closeSocialQr() {
stopSocialPolling()
qrOpen.value = false
qrValue.value = ''
}
function scheduleSocialPoll(provider, state, intervalSeconds) {
stopSocialPolling()
socialPollStartedAt = Date.now()
const tick = async () => {
if (Date.now() - socialPollStartedAt > 5 * 60 * 1000) {
closeSocialQr()
ElMessage.warning('二维码已过期,请重新获取')
return
}
try {
const result = await pollAdminSocialLogin({ provider, state })
if (result?.status === 'authorized' && result?.url) {
closeSocialQr()
window.location.assign(result.url)
return
}
socialPollTimer = window.setTimeout(tick, Math.max(Number(intervalSeconds || 2), 2) * 1000)
} catch (error) {
closeSocialQr()
const data = error?.response?.data
ElMessage.error(data?.error || '扫码状态获取失败,请重新尝试')
}
}
socialPollTimer = window.setTimeout(tick, Math.max(Number(intervalSeconds || 2), 2) * 1000)
}
async function loadSocialBindings() {
socialBindingsLoading.value = true
try {
const data = await fetchAdminSocialBindings()
socialEnabled.value = Boolean(data?.enabled)
socialBindings.value = Array.isArray(data?.items) ? data.items : []
} catch {
socialEnabled.value = false
socialBindings.value = []
} finally {
socialBindingsLoading.value = false
}
}
async function bindSocial(item) {
const provider = String(item?.provider || '').trim()
if (!provider || socialBindLoadingProvider.value) return
socialBindLoadingProvider.value = provider
try {
const data = await createAdminSocialLoginUrl({
provider,
redirect_uri: socialBindRedirectUri(),
})
if (provider !== 'wx') {
window.location.assign(data.url)
return
}
const value = data.scan_url || data.qrcode || data.url
if (!value || !data.scan_state) {
ElMessage.error('微信二维码获取失败')
return
}
qrProvider.value = provider
qrValue.value = value
qrOpen.value = true
scheduleSocialPoll(provider, data.scan_state, data.scan_poll_interval || 2)
} catch (error) {
const data = error?.response?.data
ElMessage.error(data?.error || '获取聚合登录地址失败')
} finally {
socialBindLoadingProvider.value = ''
}
}
async function unbindSocial(item) {
try {
await ElMessageBox.confirm(`确定解绑${item?.provider_label || '快捷登录'}吗?`, '解绑快捷登录', {
confirmButtonText: '解绑',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
await unbindAdminSocial(item.provider)
ElMessage.success('已解绑')
await loadSocialBindings()
} catch (error) {
const data = error?.response?.data
ElMessage.error(data?.error || '解绑失败')
}
}
onMounted(() => { onMounted(() => {
loadPasskeys() loadPasskeys()
loadSocialBindings()
})
onBeforeUnmount(() => {
stopSocialPolling()
}) })
</script> </script>
@@ -331,6 +482,66 @@ onMounted(() => {
</el-table> </el-table>
</div> </div>
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<div class="section-head">
<h3 class="section-title">快捷登录绑定</h3>
<el-tag v-if="socialEnabled" size="small" type="success">已启用</el-tag>
<el-tag v-else size="small" type="info">未启用</el-tag>
</div>
<el-alert
v-if="!socialEnabled"
type="warning"
:closable="false"
title="聚合登录未启用,请先在系统配置中填写并启用 Space 聚合登录。"
show-icon
class="help-alert"
/>
<div v-loading="socialBindingsLoading">
<el-empty v-if="visibleSocialBindings.length === 0" description="暂无可绑定的快捷登录方式" />
<div v-else class="social-list">
<div v-for="item in visibleSocialBindings" :key="item.provider" class="social-row">
<div class="social-provider">
<span class="social-icon" :class="`provider-${item.provider}`">{{ socialProviderIcon(item.provider) }}</span>
<div class="social-info">
<strong>{{ item.provider_label }}</strong>
<span v-if="item.bound">{{ item.nickname || '已绑定' }}</span>
<span v-else>未绑定</span>
</div>
</div>
<div class="social-actions">
<el-button
v-if="item.bound"
type="danger"
text
@click="unbindSocial(item)"
>
解绑
</el-button>
<el-button
v-else
type="primary"
plain
:disabled="!socialEnabled"
:loading="socialBindLoadingProvider === item.provider"
@click="bindSocial(item)"
>
绑定
</el-button>
</div>
</div>
</div>
</div>
<el-dialog v-model="qrOpen" :title="`${providerLabels[qrProvider]}绑定`" width="min(340px, 92vw)" @close="closeSocialQr">
<div class="social-qr-box">
<QrcodeVue v-if="qrValue" :value="qrValue" :size="220" level="M" />
<div class="social-qr-prompt">{{ socialQrPrompt(qrProvider) }}</div>
</div>
</el-dialog>
</el-card>
</div> </div>
</template> </template>
@@ -356,6 +567,18 @@ onMounted(() => {
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.section-head .section-title {
margin-bottom: 0;
}
.help { .help {
margin-top: 10px; margin-top: 10px;
font-size: 12px; font-size: 12px;
@@ -365,4 +588,104 @@ onMounted(() => {
.help-alert { .help-alert {
margin-bottom: 12px; margin-bottom: 12px;
} }
.social-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.social-row {
min-height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--app-border);
border-radius: 10px;
background: rgba(248, 250, 252, 0.72);
}
.social-provider {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.social-icon {
width: 30px;
height: 30px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
color: #fff;
font-size: 13px;
font-weight: 800;
}
.provider-wx {
background: #16a34a;
}
.provider-qq {
background: #2563eb;
}
.provider-alipay {
background: #1677ff;
}
.social-info {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.social-info strong {
font-size: 14px;
}
.social-info span {
max-width: min(52vw, 360px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--app-muted);
}
.social-actions {
flex: 0 0 auto;
}
.social-qr-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.social-qr-prompt {
font-size: 13px;
color: #374151;
text-align: center;
}
@media (max-width: 640px) {
.social-row {
align-items: flex-start;
flex-direction: column;
}
.social-actions {
width: 100%;
display: flex;
justify-content: flex-end;
}
}
</style> </style>

View File

@@ -11,8 +11,10 @@ const EmailPage = () => import('../pages/EmailPage.vue')
const SecurityPage = () => import('../pages/SecurityPage.vue') const SecurityPage = () => import('../pages/SecurityPage.vue')
const SystemPage = () => import('../pages/SystemPage.vue') const SystemPage = () => import('../pages/SystemPage.vue')
const SettingsPage = () => import('../pages/SettingsPage.vue') const SettingsPage = () => import('../pages/SettingsPage.vue')
const AdminSocialBindCallbackPage = () => import('../pages/AdminSocialBindCallbackPage.vue')
const routes = [ const routes = [
{ path: '/social-bind-callback', name: 'admin_social_bind_callback', component: AdminSocialBindCallbackPage },
{ {
path: '/', path: '/',
component: AdminLayout, component: AdminLayout,

View File

@@ -26,15 +26,21 @@ from db.migrations import migrate_database as _migrate_database
from db.admin import ( from db.admin import (
admin_reset_user_password, admin_reset_user_password,
clean_old_operation_logs, clean_old_operation_logs,
delete_admin_social_login_binding,
find_admin_social_login_binding,
find_admin_social_login_binding_by_identity,
get_admin_by_id, get_admin_by_id,
ensure_default_admin, ensure_default_admin,
get_admin_by_username, get_admin_by_username,
get_hourly_registration_count, get_hourly_registration_count,
get_system_config_raw as _get_system_config_raw, get_system_config_raw as _get_system_config_raw,
get_system_stats, get_system_stats,
list_admin_social_login_bindings,
update_admin_password, update_admin_password,
update_admin_username, update_admin_username,
update_system_config as _update_system_config, update_system_config as _update_system_config,
update_admin_social_login_binding_profile,
upsert_admin_social_login_binding,
verify_admin, verify_admin,
) )
from db.accounts import ( from db.accounts import (
@@ -144,7 +150,7 @@ logger = get_logger(__name__)
DB_FILE = config.DB_FILE DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理) # 数据库版本 (用于迁移管理)
DB_VERSION = 22 DB_VERSION = 23
# ==================== 系统配置缓存P1 / O-03 ==================== # ==================== 系统配置缓存P1 / O-03 ====================

View File

@@ -245,6 +245,107 @@ def update_admin_username(old_username: str, new_username: str) -> bool:
return False return False
# ==================== 管理员聚合登录绑定 ====================
def find_admin_social_login_binding_by_identity(provider: str, social_uid: str):
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM admin_social_login_bindings WHERE provider = ? AND social_uid = ?",
(provider, social_uid),
)
row = cursor.fetchone()
return dict(row) if row else None
def find_admin_social_login_binding(admin_id: int, provider: str):
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM admin_social_login_bindings WHERE admin_id = ? AND provider = ?",
(int(admin_id), provider),
)
row = cursor.fetchone()
return dict(row) if row else None
def upsert_admin_social_login_binding(*, admin_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 admin_social_login_bindings (
admin_id, provider, social_uid, nickname, avatar_url, created_at, updated_at, last_login_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(admin_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(admin_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_admin_social_login_binding(admin_id, provider)
def update_admin_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 admin_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_admin_social_login_bindings(admin_id: int) -> list[dict]:
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT * FROM admin_social_login_bindings
WHERE admin_id = ?
ORDER BY created_at ASC
""",
(int(admin_id),),
)
return [dict(row) for row in cursor.fetchall()]
def delete_admin_social_login_binding(admin_id: int, provider: str) -> bool:
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute(
"DELETE FROM admin_social_login_bindings WHERE admin_id = ? AND provider = ?",
(int(admin_id), provider),
)
conn.commit()
return cursor.rowcount > 0
def get_system_stats() -> dict: def get_system_stats() -> dict:
"""获取系统统计信息""" """获取系统统计信息"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:

View File

@@ -77,6 +77,7 @@ def _get_migration_steps():
(20, _migrate_to_v20), (20, _migrate_to_v20),
(21, _migrate_to_v21), (21, _migrate_to_v21),
(22, _migrate_to_v22), (22, _migrate_to_v22),
(23, _migrate_to_v23),
] ]
@@ -1002,3 +1003,33 @@ def _migrate_to_v22(conn):
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_social_pending_binds_expires ON social_pending_binds(expires_at)")
conn.commit() conn.commit()
def _migrate_to_v23(conn):
"""迁移到版本23 - 管理员聚合登录绑定。"""
cursor = conn.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS admin_social_login_bindings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
admin_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 (admin_id, provider),
FOREIGN KEY (admin_id) REFERENCES admins (id) ON DELETE CASCADE
)
"""
)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_admin ON admin_social_login_bindings(admin_id)")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_provider_uid ON admin_social_login_bindings(provider, social_uid)"
)
conn.commit()

View File

@@ -276,6 +276,26 @@ def ensure_schema(conn) -> None:
""" """
) )
# 管理员聚合登录绑定表
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS admin_social_login_bindings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
admin_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 (admin_id, provider),
FOREIGN KEY (admin_id) REFERENCES admins (id) ON DELETE CASCADE
)
"""
)
# 聚合登录短期待绑定凭证表 # 聚合登录短期待绑定凭证表
cursor.execute( cursor.execute(
""" """
@@ -432,6 +452,10 @@ def ensure_schema(conn) -> None:
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_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_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_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_admin ON admin_social_login_bindings(admin_id)")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_admin_social_login_bindings_provider_uid ON admin_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_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_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_social_pending_binds_expires ON social_pending_binds(expires_at)")

View File

@@ -283,7 +283,8 @@ def register():
social_uid = str(pending.get("social_uid") or "").strip() social_uid = str(pending.get("social_uid") or "").strip()
enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers")) enabled_providers = parse_providers((database.get_system_config() or {}).get("social_login_providers"))
existing_identity = database.find_social_login_binding(provider, social_uid) existing_identity = database.find_social_login_binding(provider, social_uid)
if provider in enabled_providers and social_uid and not existing_identity: existing_admin_identity = database.find_admin_social_login_binding_by_identity(provider, social_uid)
if provider in enabled_providers and social_uid and not existing_identity and not existing_admin_identity:
binding = database.upsert_social_login_binding( binding = database.upsert_social_login_binding(
user_id=user_id, user_id=user_id,
provider=provider, provider=provider,

View File

@@ -6,7 +6,7 @@ from datetime import timedelta
import database import database
from app_logger import get_logger from app_logger import get_logger
from db.utils import get_cst_now, get_cst_now_str from db.utils import get_cst_now, get_cst_now_str
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request, session
from flask_login import current_user, login_required, login_user from flask_login import current_user, login_required, login_user
from services.accounts_service import load_user_accounts from services.accounts_service import load_user_accounts
from services.models import User from services.models import User
@@ -82,6 +82,18 @@ def _binding_row(provider: str, binding: dict | None) -> dict:
} }
def _admin_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"]) @api_social_bp.route("/api/auth/social/config", methods=["GET"])
def social_public_config(): def social_public_config():
return jsonify(public_social_config(database.get_system_config())) return jsonify(public_social_config(database.get_system_config()))
@@ -135,6 +147,10 @@ def social_callback():
return _social_error(error) return _social_error(error)
binding = database.find_social_login_binding(profile.provider, profile.social_uid) binding = database.find_social_login_binding(profile.provider, profile.social_uid)
admin_binding = database.find_admin_social_login_binding_by_identity(profile.provider, profile.social_uid)
if admin_binding:
return jsonify({"error": "该第三方账号已绑定管理员账号"}), 409
if binding: if binding:
if mode == "bind": if mode == "bind":
current_id = int(getattr(current_user, "id", 0) or 0) current_id = int(getattr(current_user, "id", 0) or 0)
@@ -204,6 +220,9 @@ def bind_social_account():
existing_identity = database.find_social_login_binding(provider, social_uid) 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): if existing_identity and int(existing_identity.get("user_id") or 0) != int(current_user.id):
return jsonify({"error": "该第三方账号已绑定其他用户"}), 409 return jsonify({"error": "该第三方账号已绑定其他用户"}), 409
existing_admin_identity = database.find_admin_social_login_binding_by_identity(provider, social_uid)
if existing_admin_identity:
return jsonify({"error": "该第三方账号已绑定管理员账号"}), 409
existing_provider = database.find_user_social_login_binding(int(current_user.id), provider) 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: if existing_provider and str(existing_provider.get("social_uid") or "") != social_uid:
@@ -242,6 +261,138 @@ def admin_social_config():
return protected() return protected()
@api_social_bp.route("/yuyx/api/admin/social-bindings", methods=["GET"])
def list_admin_social_bindings():
from routes.decorators import admin_required
@admin_required
def _inner():
cfg = database.get_system_config()
providers = parse_providers(cfg.get("social_login_providers")) or list(PROVIDER_LABELS.keys())
admin_id = int(session.get("admin_id") or 0)
existing = {
item["provider"]: item
for item in database.list_admin_social_login_bindings(admin_id)
}
public_cfg = public_social_config(cfg)
return jsonify(
{
"enabled": bool(public_cfg.get("enabled")),
"providers": providers,
"items": [_admin_binding_row(provider, existing.get(provider)) for provider in providers],
}
)
return _inner()
@api_social_bp.route("/yuyx/api/admin/social-login-url", methods=["POST"])
def admin_social_login_url():
from routes.decorators import admin_required
@admin_required
def _inner():
data = _get_json_payload()
provider = str(data.get("provider") or "").strip().lower()
redirect_uri = str(data.get("redirect_uri") or "").strip()
try:
result = fetch_social_login_url(
database.get_system_config(),
provider=provider,
mode="bind",
redirect_uri=redirect_uri,
allowed_hosts=_allowed_redirect_hosts(),
)
except SocialLoginError as error:
logger.warning(f"[admin/social/login-url] provider={provider or '-'} failed: {error.message}")
return _social_error(error)
return jsonify(result)
return _inner()
@api_social_bp.route("/yuyx/api/admin/social-poll", methods=["POST"])
def admin_social_poll():
from routes.decorators import admin_required
@admin_required
def _inner():
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"[admin/social/poll] provider={provider or '-'} failed: {error.message}")
return _social_error(error)
return jsonify(result)
return _inner()
@api_social_bp.route("/yuyx/api/admin/social-bindings/<provider>/callback", methods=["POST"])
def bind_admin_social_callback(provider):
from routes.decorators import admin_required
@admin_required
def _inner():
data = _get_json_payload()
provider_value = str(data.get("provider") or provider or data.get("type") or "").strip().lower()
code = str(data.get("code") or "").strip()
admin_id = int(session.get("admin_id") or 0)
try:
profile = fetch_space_profile(database.get_system_config(), provider=provider_value, code=code)
except SocialLoginError as error:
logger.warning(f"[admin/social/callback] provider={provider_value or '-'} failed: {error.message}")
return _social_error(error)
user_identity = database.find_social_login_binding(profile.provider, profile.social_uid)
if user_identity:
return jsonify({"error": "该第三方账号已绑定普通用户"}), 409
existing_identity = database.find_admin_social_login_binding_by_identity(profile.provider, profile.social_uid)
if existing_identity and int(existing_identity.get("admin_id") or 0) != admin_id:
return jsonify({"error": "该第三方账号已绑定其他管理员"}), 409
existing_provider = database.find_admin_social_login_binding(admin_id, profile.provider)
if existing_provider and str(existing_provider.get("social_uid") or "") != profile.social_uid:
return jsonify({"error": f"当前管理员已绑定{provider_label(profile.provider)}"}), 409
binding = database.upsert_admin_social_login_binding(
admin_id=admin_id,
provider=profile.provider,
social_uid=profile.social_uid,
nickname=profile.nickname,
avatar_url=profile.avatar_url,
)
if not binding:
return jsonify({"error": "该第三方账号已绑定其他管理员"}), 409
logger.info(f"[admin/social/bind] admin_id={admin_id} provider={profile.provider}")
return jsonify({"success": True, "item": _admin_binding_row(profile.provider, binding)})
return _inner()
@api_social_bp.route("/yuyx/api/admin/social-bindings/<provider>", methods=["DELETE"])
def unbind_admin_social_account(provider):
from routes.decorators import admin_required
@admin_required
def _inner():
provider_value = str(provider or "").strip().lower()
if provider_value not in PROVIDER_LABELS:
return jsonify({"error": "不支持的登录方式"}), 400
admin_id = int(session.get("admin_id") or 0)
if not database.delete_admin_social_login_binding(admin_id, provider_value):
return jsonify({"error": "绑定记录不存在"}), 404
logger.info(f"[admin/social/unbind] admin_id={admin_id} provider={provider_value}")
return jsonify({"success": True})
return _inner()
@api_social_bp.route("/yuyx/api/social-login/test", methods=["POST"]) @api_social_bp.route("/yuyx/api/social-login/test", methods=["POST"])
def test_admin_social_config(): def test_admin_social_config():
from routes.decorators import admin_required from routes.decorators import admin_required

View File

@@ -6,7 +6,7 @@ import json
import os import os
from typing import Optional from typing import Optional
from flask import Blueprint, current_app, redirect, render_template, session, url_for from flask import Blueprint, current_app, redirect, render_template, request, session, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from routes.decorators import admin_required from routes.decorators import admin_required
@@ -192,3 +192,14 @@ def admin_page():
except Exception as e: except Exception as e:
logger.error(f"[admin_spa] 加载manifest失败: {e}") logger.error(f"[admin_spa] 加载manifest失败: {e}")
return "后台页面加载失败,请稍后重试", 500 return "后台页面加载失败,请稍后重试", 500
@pages_bp.route("/yuyx/admin-social-bind-callback")
@admin_required
def admin_social_bind_callback_page():
"""管理员快捷登录绑定回调页面(由后台 SPA 继续处理授权参数)。"""
query = request.query_string.decode("utf-8", "ignore")
target = "/yuyx/admin#/social-bind-callback"
if query:
target = f"{target}?{query}"
return redirect(target)

View File

@@ -3,8 +3,8 @@
"file": "assets/MetricGrid-BR486o_b.css", "file": "assets/MetricGrid-BR486o_b.css",
"src": "_MetricGrid-BR486o_b.css" "src": "_MetricGrid-BR486o_b.css"
}, },
"_MetricGrid-C3Xjc9mZ.js": { "_MetricGrid-kv-nSROj.js": {
"file": "assets/MetricGrid-C3Xjc9mZ.js", "file": "assets/MetricGrid-kv-nSROj.js",
"name": "MetricGrid", "name": "MetricGrid",
"imports": [ "imports": [
"index.html", "index.html",
@@ -14,29 +14,36 @@
"assets/MetricGrid-BR486o_b.css" "assets/MetricGrid-BR486o_b.css"
] ]
}, },
"_email-Mh1SHQbX.js": { "_admin-VsbfHbbH.js": {
"file": "assets/email-Mh1SHQbX.js", "file": "assets/admin-VsbfHbbH.js",
"name": "admin",
"imports": [
"index.html"
]
},
"_email-CgUCpCe3.js": {
"file": "assets/email-CgUCpCe3.js",
"name": "email", "name": "email",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_system-CYbWdReq.js": { "_system-CeiBEEoE.js": {
"file": "assets/system-CYbWdReq.js", "file": "assets/system-CeiBEEoE.js",
"name": "system", "name": "system",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_tasks-B7oNpIBD.js": { "_tasks-C6JkguA6.js": {
"file": "assets/tasks-B7oNpIBD.js", "file": "assets/tasks-C6JkguA6.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-DzDcz9C_.js": { "_users-D9XvGIoE.js": {
"file": "assets/users-DzDcz9C_.js", "file": "assets/users-D9XvGIoE.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
@@ -46,23 +53,23 @@
"file": "assets/vendor-axios-B9ygI19o.js", "file": "assets/vendor-axios-B9ygI19o.js",
"name": "vendor-axios" "name": "vendor-axios"
}, },
"_vendor-element-B5S5pUKo.js": { "_vendor-element-C68yOrAy.css": {
"file": "assets/vendor-element-B5S5pUKo.js", "file": "assets/vendor-element-C68yOrAy.css",
"src": "_vendor-element-C68yOrAy.css"
},
"_vendor-element-CIudPaVX.js": {
"file": "assets/vendor-element-CIudPaVX.js",
"name": "vendor-element", "name": "vendor-element",
"imports": [ "imports": [
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/vendor-element-C68yOrAy.css" "assets/vendor-element-C68yOrAy.css"
] ]
}, },
"_vendor-element-C68yOrAy.css": { "_vendor-misc-DszMq72k.js": {
"file": "assets/vendor-element-C68yOrAy.css", "file": "assets/vendor-misc-DszMq72k.js",
"src": "_vendor-element-C68yOrAy.css"
},
"_vendor-misc-BeoNyvBp.js": {
"file": "assets/vendor-misc-BeoNyvBp.js",
"name": "vendor-misc", "name": "vendor-misc",
"imports": [ "imports": [
"_vendor-vue-CVxSw_oJ.js" "_vendor-vue-CVxSw_oJ.js"
@@ -73,15 +80,15 @@
"name": "vendor-vue" "name": "vendor-vue"
}, },
"index.html": { "index.html": {
"file": "assets/index-DOvMEmc8.js", "file": "assets/index-6ynv0Z9Y.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"dynamicImports": [ "dynamicImports": [
"src/pages/ReportPage.vue", "src/pages/ReportPage.vue",
@@ -92,22 +99,40 @@
"src/pages/EmailPage.vue", "src/pages/EmailPage.vue",
"src/pages/SecurityPage.vue", "src/pages/SecurityPage.vue",
"src/pages/SystemPage.vue", "src/pages/SystemPage.vue",
"src/pages/SettingsPage.vue" "src/pages/SettingsPage.vue",
"src/pages/AdminSocialBindCallbackPage.vue"
], ],
"css": [ "css": [
"assets/index-CPs_XZ2s.css" "assets/index-CPs_XZ2s.css"
] ]
}, },
"src/pages/AdminSocialBindCallbackPage.vue": {
"file": "assets/AdminSocialBindCallbackPage-BsLZg3f-.js",
"name": "AdminSocialBindCallbackPage",
"src": "src/pages/AdminSocialBindCallbackPage.vue",
"isDynamicEntry": true,
"imports": [
"_vendor-vue-CVxSw_oJ.js",
"_admin-VsbfHbbH.js",
"index.html",
"_vendor-element-CIudPaVX.js",
"_vendor-axios-B9ygI19o.js",
"_vendor-misc-DszMq72k.js"
],
"css": [
"assets/AdminSocialBindCallbackPage-CXV1zZmY.css"
]
},
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-Dagm5PzE.js", "file": "assets/AnnouncementsPage-BcIVG51R.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"index.html", "index.html",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-misc-BeoNyvBp.js", "_vendor-misc-DszMq72k.js",
"_vendor-axios-B9ygI19o.js" "_vendor-axios-B9ygI19o.js"
], ],
"css": [ "css": [
@@ -115,72 +140,72 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-DiZA9Kx_.js", "file": "assets/EmailPage-B1uMhyWi.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-Mh1SHQbX.js", "_email-CgUCpCe3.js",
"index.html", "index.html",
"_MetricGrid-C3Xjc9mZ.js", "_MetricGrid-kv-nSROj.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/EmailPage-CTHxGzDv.css" "assets/EmailPage-CTHxGzDv.css"
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-DrMVqBKf.js", "file": "assets/FeedbacksPage-CG9FZytm.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-C3Xjc9mZ.js", "_MetricGrid-kv-nSROj.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/FeedbacksPage-CPmSqIaj.css" "assets/FeedbacksPage-CPmSqIaj.css"
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-Cy6Q0ave.js", "file": "assets/LogsPage-Ct-BSxV6.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DzDcz9C_.js", "_users-D9XvGIoE.js",
"_tasks-B7oNpIBD.js", "_tasks-C6JkguA6.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/LogsPage-BUgx3sZr.css" "assets/LogsPage-BUgx3sZr.css"
] ]
}, },
"src/pages/ReportPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/ReportPage-BMEJM5Hr.js", "file": "assets/ReportPage-2jS10KoG.js",
"name": "ReportPage", "name": "ReportPage",
"src": "src/pages/ReportPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"index.html", "index.html",
"_email-Mh1SHQbX.js", "_email-CgUCpCe3.js",
"_tasks-B7oNpIBD.js", "_tasks-C6JkguA6.js",
"_system-CYbWdReq.js", "_system-CeiBEEoE.js",
"_MetricGrid-C3Xjc9mZ.js", "_MetricGrid-kv-nSROj.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-misc-BeoNyvBp.js", "_vendor-misc-DszMq72k.js",
"_vendor-axios-B9ygI19o.js" "_vendor-axios-B9ygI19o.js"
], ],
"css": [ "css": [
@@ -188,67 +213,68 @@
] ]
}, },
"src/pages/SecurityPage.vue": { "src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-yzYEGeTN.js", "file": "assets/SecurityPage-93lfkhLF.js",
"name": "SecurityPage", "name": "SecurityPage",
"src": "src/pages/SecurityPage.vue", "src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-C3Xjc9mZ.js", "_MetricGrid-kv-nSROj.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/SecurityPage-C2-mJ7eD.css" "assets/SecurityPage-C2-mJ7eD.css"
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-DF5fL8gq.js", "file": "assets/SettingsPage-BbHyIZsy.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_vendor-misc-DszMq72k.js",
"_admin-VsbfHbbH.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js"
"_vendor-misc-BeoNyvBp.js"
], ],
"css": [ "css": [
"assets/SettingsPage-D-iYz1zh.css" "assets/SettingsPage-CjIQQfeg.css"
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DrM9-RI5.js", "file": "assets/SystemPage-D9T-fhw-.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_system-CYbWdReq.js", "_system-CeiBEEoE.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/SystemPage-CTs6qr36.css" "assets/SystemPage-CTs6qr36.css"
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-RI5S3snx.js", "file": "assets/UsersPage-2-Mno2hz.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DzDcz9C_.js", "_users-D9XvGIoE.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-CIudPaVX.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-DszMq72k.js"
], ],
"css": [ "css": [
"assets/UsersPage-CgYh6JHW.css" "assets/UsersPage-CgYh6JHW.css"

View File

@@ -0,0 +1 @@
import{ax as w,r as y,o as g,aj as l,n as v,q as f,L as d,E as h,t as k,J as x}from"./vendor-vue-CVxSw_oJ.js";import{i as S}from"./admin-VsbfHbbH.js";import{_ as C}from"./index-6ynv0Z9Y.js";import{a as n}from"./vendor-element-CIudPaVX.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-DszMq72k.js";const b={class:"callback-wrap"},B={class:"callback-text"},P={__name:"AdminSocialBindCallbackPage",setup(q){const o=w(),t=y("正在完成绑定");return g(async()=>{const c=String(window.location.hash||"").split("?")[1]||"",e=new URLSearchParams(window.location.search||c),a=String(e.get("provider")||e.get("type")||"").trim().toLowerCase(),r=String(e.get("code")||"").trim(),p=String(o.query?.provider||o.query?.type||"").trim().toLowerCase(),m=String(o.query?.code||"").trim(),s=a||p,i=r||m;if(!s||!i){n.error("快捷登录回调参数不完整"),window.location.replace("/yuyx/admin#/settings");return}try{await S(s,{provider:s,code:i}),n.success("管理员快捷登录已绑定"),window.location.replace("/yuyx/admin#/settings")}catch(u){const _=u?.response?.data;t.value=_?.error||"快捷登录绑定失败",n.error(t.value),window.setTimeout(()=>{window.location.replace("/yuyx/admin#/settings")},1200)}}),(c,e)=>{const a=l("el-skeleton"),r=l("el-card");return f(),v("div",b,[d(r,{shadow:"never",class:"callback-card"},{default:h(()=>[d(a,{rows:3,animated:""}),k("div",B,x(t.value),1)]),_:1})])}}},T=C(P,[["__scopeId","data-v-647766e7"]]);export{T as default};

View File

@@ -0,0 +1 @@
.callback-wrap[data-v-647766e7]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:#f6f7fb}.callback-card[data-v-647766e7]{width:min(420px,94vw);border-radius:12px;border:1px solid var(--app-border)}.callback-text[data-v-647766e7]{margin-top:12px;color:var(--app-muted);font-size:13px;text-align:center}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_}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}; import{_}from"./index-6ynv0Z9Y.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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-e6d9cfda]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-e6d9cfda]{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-e6d9cfda]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.section-head[data-v-e6d9cfda]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.section-head .section-title[data-v-e6d9cfda]{margin-bottom:0}.help[data-v-e6d9cfda]{margin-top:10px;font-size:12px;color:var(--app-muted)}.help-alert[data-v-e6d9cfda]{margin-bottom:12px}.social-list[data-v-e6d9cfda]{display:flex;flex-direction:column;gap:10px}.social-row[data-v-e6d9cfda]{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--app-border);border-radius:10px;background:#f8fafcb8}.social-provider[data-v-e6d9cfda]{display:flex;align-items:center;gap:10px;min-width:0}.social-icon[data-v-e6d9cfda]{width:30px;height:30px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;color:#fff;font-size:13px;font-weight:800}.provider-wx[data-v-e6d9cfda]{background:#16a34a}.provider-qq[data-v-e6d9cfda]{background:#2563eb}.provider-alipay[data-v-e6d9cfda]{background:#1677ff}.social-info[data-v-e6d9cfda]{min-width:0;display:flex;flex-direction:column;gap:2px}.social-info strong[data-v-e6d9cfda]{font-size:14px}.social-info span[data-v-e6d9cfda]{max-width:min(52vw,360px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--app-muted)}.social-actions[data-v-e6d9cfda]{flex:0 0 auto}.social-qr-box[data-v-e6d9cfda]{display:flex;flex-direction:column;align-items:center;gap:12px}.social-qr-prompt[data-v-e6d9cfda]{font-size:13px;color:#374151;text-align:center}@media(max-width:640px){.social-row[data-v-e6d9cfda]{align-items:flex-start;flex-direction:column}.social-actions[data-v-e6d9cfda]{width:100%;display:flex;justify-content:flex-end}}

View File

@@ -1 +0,0 @@
.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}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
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(`确定更新并发配置吗? import{f as Fe,a as He,u as me,b as Oe,t as je}from"./system-CeiBEEoE.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-6ynv0Z9Y.js";import{E as _e,a as d}from"./vendor-element-CIudPaVX.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-DszMq72k.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_global}
单账号并发数: ${o.max_concurrent_per_account} 单账号并发数: ${o.max_concurrent_per_account}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{a as s}from"./index-6ynv0Z9Y.js";async function e(a){const{data:n}=await s.put("/admin/username",{new_username:a});return n}async function r(a={}){const n=String(a.currentPassword||""),t=String(a.newPassword||""),{data:i}=await s.put("/admin/password",{current_password:n,new_password:t});return i}async function c(){const{data:a}=await s.post("/logout");return a}async function d(){const{data:a}=await s.get("/admin/passkeys");return a}async function u(a={}){const{data:n}=await s.post("/admin/passkeys/register/options",a);return n}async function m(a={}){const{data:n}=await s.post("/admin/passkeys/register/verify",a);return n}async function p(a){const{data:n}=await s.delete(`/admin/passkeys/${a}`);return n}async function l(a={}){const{data:n}=await s.post("/admin/passkeys/client-error",a);return n}async function w(){const{data:a}=await s.get("/admin/social-bindings");return a}async function y(a={}){const{data:n}=await s.post("/admin/social-login-url",a);return n}async function f(a={}){const{data:n}=await s.post("/admin/social-poll",a);return n}async function g(a,n={}){const{data:t}=await s.post(`/admin/social-bindings/${encodeURIComponent(a)}/callback`,n);return t}async function k(a){const{data:n}=await s.delete(`/admin/social-bindings/${encodeURIComponent(a)}`);return n}export{w as a,r as b,u as c,m as d,p as e,d as f,k as g,y as h,g as i,c as l,f as p,l as r,e as u};

View File

@@ -1 +1 @@
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}; import{c as s,a as e}from"./index-6ynv0Z9Y.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};

File diff suppressed because one or more lines are too long

View File

@@ -1 +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}; import{c as s,a as n}from"./index-6ynv0Z9Y.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};

View File

@@ -1 +1 @@
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}; import{c as s,a}from"./index-6ynv0Z9Y.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};

View File

@@ -1 +1 @@
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}; import{a as t}from"./index-6ynv0Z9Y.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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,16 +5,15 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" /> <link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title> <title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-DOvMEmc8.js"></script> <script type="module" crossorigin src="./assets/index-6ynv0Z9Y.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-misc-BeoNyvBp.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-misc-DszMq72k.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-element-B5S5pUKo.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-element-CIudPaVX.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-axios-B9ygI19o.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-axios-B9ygI19o.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-element-C68yOrAy.css"> <link rel="stylesheet" crossorigin href="./assets/vendor-element-C68yOrAy.css">
<link rel="stylesheet" crossorigin href="./assets/index-CPs_XZ2s.css"> <link rel="stylesheet" crossorigin href="./assets/index-CPs_XZ2s.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>