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
+12 -8
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",
+1
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"
}, },
+25
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
}
@@ -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>
+324 -1
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>
+2
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,
+7 -1
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 ====================
+101
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:
+31
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()
+24
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)")
+2 -1
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,
+152 -1
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
+12 -1
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)
+90 -64
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"
@@ -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};
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_}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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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}}
@@ -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
@@ -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
+1
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};
@@ -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
@@ -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};
@@ -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};
@@ -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
File diff suppressed because one or more lines are too long
+3 -4
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>
</html> </html>