Remove server remote browser support
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
<div class="shell-page page-section custom-pages-shell">
|
||||
<div class="page-header surface-card page-block">
|
||||
<div class="page-heading">
|
||||
<p class="page-kicker">Embedded Surfaces</p>
|
||||
<h2 class="page-title">自定义页面</h2>
|
||||
<p class="page-desc">嵌入外部网页到侧边栏,统一管理上游平台</p>
|
||||
<p class="page-kicker">External Consoles</p>
|
||||
<h2 class="page-title">上游网址管理</h2>
|
||||
<p class="page-desc">维护外部控制台入口,点击后使用本机浏览器打开</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
<el-icon><Plus /></el-icon> 添加页面
|
||||
<el-icon><Plus /></el-icon> 添加网址
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
<div class="card-url" :title="page.url">{{ page.url }}</div>
|
||||
</div>
|
||||
<div class="tag-group">
|
||||
<el-tag v-if="page.access_mode === 'proxy' || page.use_proxy" size="small" type="warning" class="proxy-tag">代理</el-tag>
|
||||
<el-tag v-else-if="page.access_mode === 'remote_browser'" size="small" type="success" class="proxy-tag">远程浏览器</el-tag>
|
||||
<el-tag v-if="!page.enabled" size="small" type="info" class="disabled-tag">已停用</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +32,7 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card-actions">
|
||||
<el-button size="small" text type="primary" @click="openViewer(page)">
|
||||
<el-button size="small" text type="primary" @click="openExternalPage(page)">
|
||||
<el-icon><Monitor /></el-icon> 打开
|
||||
</el-button>
|
||||
<el-button size="small" text @click="openEdit(page)">
|
||||
@@ -49,8 +47,8 @@
|
||||
|
||||
<div v-if="!loading && list.length === 0" class="empty-state">
|
||||
<el-icon :size="48" class="empty-icon"><Monitor /></el-icon>
|
||||
<p>还没有自定义页面</p>
|
||||
<p class="empty-sub">添加后可在侧边栏快速访问上游管理平台</p>
|
||||
<p>还没有上游网址</p>
|
||||
<p class="empty-sub">添加后可在侧边栏快速打开外部控制台</p>
|
||||
<el-button type="primary" @click="openCreate">立即添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +56,7 @@
|
||||
<!-- Create / Edit dialog -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingId ? '编辑页面' : '添加自定义页面'"
|
||||
:title="editingId ? '编辑网址' : '添加上游网址'"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
>
|
||||
@@ -86,72 +84,9 @@
|
||||
<el-input-number v-model="form.sort_order" :min="0" :max="999" style="width:140px" />
|
||||
<span class="form-hint">数字越小越靠前</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="访问模式">
|
||||
<el-radio-group v-model="form.access_mode" class="access-mode-group">
|
||||
<el-radio-button label="direct">直接嵌入</el-radio-button>
|
||||
<el-radio-button label="proxy">代理</el-radio-button>
|
||||
<el-radio-button label="remote_browser">远程浏览器</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="form-hint mode-hint">远程浏览器适合 Cookie、CSP、复杂 SPA 或拒绝 iframe 的站点。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.access_mode === 'remote_browser'" label="关联上游">
|
||||
<el-select v-model="form.linked_upstream_id" clearable placeholder="选择要一键刷新凭证的上游" style="width:100%" @change="handleLinkedUpstreamChange">
|
||||
<el-option v-for="u in upstreamList" :key="u.id" :label="`${u.name} (${u.base_url})`" :value="u.id" />
|
||||
</el-select>
|
||||
<div class="form-hint">关联后可在页面查看器中一键刷新该上游的认证凭证</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.access_mode === 'remote_browser' && form.linked_upstream_id" label="上游类型">
|
||||
<el-select v-model="form.upstream_platform" style="width:100%">
|
||||
<el-option label="Sub2API" value="sub2api" />
|
||||
<el-option label="New-API" value="new-api" />
|
||||
</el-select>
|
||||
<div class="form-hint">保存后会同步该关联上游的接口路径,刷新凭证时按此类型选择 Token 或 Cookie。</div>
|
||||
</el-form-item>
|
||||
<div class="login-section">
|
||||
<div class="login-section-head">
|
||||
<span>登录自动填充</span>
|
||||
<el-switch v-model="form.login_autofill_enabled" @change="loginAutofillTouched = true" />
|
||||
</div>
|
||||
<div class="form-hint login-hint">仅远程浏览器模式会执行;不填写提交按钮 selector 时只填账号密码。</div>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.login_username" autocomplete="username" placeholder="登录账号、邮箱或用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<div class="password-field">
|
||||
<el-input
|
||||
v-model="form.login_password"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
:disabled="form.login_password_clear"
|
||||
:placeholder="editingId && form.login_password_configured ? '留空保持原密码' : '登录密码'"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-if="editingId && form.login_password_configured"
|
||||
v-model="form.login_password_clear"
|
||||
@change="form.login_password = ''"
|
||||
>
|
||||
清空已保存密码
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-collapse class="selector-collapse">
|
||||
<el-collapse-item title="高级 selector" name="selectors">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.login_username_selector" placeholder="例:input[name='email']" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.login_password_selector" placeholder="例:input[type='password']" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提交按钮">
|
||||
<el-input v-model="form.login_submit_selector" placeholder="可选,例:button[type='submit']" />
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
@@ -163,7 +98,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, markRaw } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -171,9 +105,7 @@ import {
|
||||
SetUp, Reading, Cpu, DataLine, Grid, Connection,
|
||||
Ticket, Wallet, Key, Tools, Star, House,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { customPagesApi, upstreamsApi, type CustomPageAccessMode, type CustomPageData, type UpstreamData } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
import { customPagesApi, type CustomPageData, type CustomPageForm } from '@/api'
|
||||
|
||||
// ---- icon map ----
|
||||
const iconMap: Record<string, any> = {
|
||||
@@ -195,35 +127,19 @@ const iconMap: Record<string, any> = {
|
||||
|
||||
// ---- state ----
|
||||
const list = ref<CustomPageData[]>([])
|
||||
const upstreamList = ref<UpstreamData[]>([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const loginAutofillTouched = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
type UpstreamPlatform = 'sub2api' | 'new-api'
|
||||
|
||||
type PageFormState = {
|
||||
name: string
|
||||
url: string
|
||||
icon: string
|
||||
sort_order: number
|
||||
enabled: boolean
|
||||
use_proxy: boolean
|
||||
access_mode: CustomPageAccessMode
|
||||
description: string
|
||||
login_username: string
|
||||
login_password: string
|
||||
login_username_selector: string
|
||||
login_password_selector: string
|
||||
login_submit_selector: string
|
||||
login_autofill_enabled: boolean
|
||||
login_password_configured: boolean
|
||||
login_password_clear: boolean
|
||||
linked_upstream_id: number | null
|
||||
upstream_platform: UpstreamPlatform
|
||||
}
|
||||
|
||||
const defaultForm = (): PageFormState => ({
|
||||
@@ -232,19 +148,7 @@ const defaultForm = (): PageFormState => ({
|
||||
icon: 'Link',
|
||||
sort_order: 0,
|
||||
enabled: true,
|
||||
use_proxy: false,
|
||||
access_mode: 'direct',
|
||||
description: '',
|
||||
login_username: '',
|
||||
login_password: '',
|
||||
login_username_selector: '',
|
||||
login_password_selector: '',
|
||||
login_submit_selector: '',
|
||||
login_autofill_enabled: false,
|
||||
login_password_configured: false,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: null,
|
||||
upstream_platform: 'sub2api',
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
const rules = {
|
||||
@@ -255,9 +159,8 @@ const rules = {
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [pagesRes, upstreamsRes] = await Promise.all([customPagesApi.list(), upstreamsApi.list()])
|
||||
const pagesRes = await customPagesApi.list()
|
||||
list.value = pagesRes.data
|
||||
upstreamList.value = upstreamsRes.data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -265,105 +168,41 @@ async function loadList() {
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
loginAutofillTouched.value = false
|
||||
form.value = defaultForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(page: CustomPageData) {
|
||||
editingId.value = page.id
|
||||
loginAutofillTouched.value = false
|
||||
form.value = {
|
||||
name: page.name,
|
||||
url: page.url,
|
||||
icon: page.icon,
|
||||
sort_order: page.sort_order,
|
||||
enabled: page.enabled,
|
||||
use_proxy: page.access_mode === 'proxy' || page.use_proxy,
|
||||
access_mode: page.access_mode || (page.use_proxy ? 'proxy' : 'direct'),
|
||||
description: page.description || '',
|
||||
login_username: page.login_username || '',
|
||||
login_password: '',
|
||||
login_username_selector: page.login_username_selector || '',
|
||||
login_password_selector: page.login_password_selector || '',
|
||||
login_submit_selector: page.login_submit_selector || '',
|
||||
login_autofill_enabled: page.login_autofill_enabled,
|
||||
login_password_configured: page.login_password_configured,
|
||||
login_password_clear: false,
|
||||
linked_upstream_id: page.linked_upstream_id ?? null,
|
||||
upstream_platform: detectUpstreamPlatform(page.linked_upstream_id ?? null),
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function selectedUpstream(id = form.value.linked_upstream_id): UpstreamData | undefined {
|
||||
return upstreamList.value.find((u) => u.id === id)
|
||||
}
|
||||
|
||||
function detectUpstreamPlatform(id: number | null): UpstreamPlatform {
|
||||
const upstream = selectedUpstream(id)
|
||||
if (!upstream) return 'sub2api'
|
||||
const cfg = upstream.auth_config_masked || {}
|
||||
if (
|
||||
(upstream.groups_endpoint || '').replace(/\/+$/, '') === '/api/user/self/groups' ||
|
||||
cfg.login_path === '/api/user/login'
|
||||
) {
|
||||
return 'new-api'
|
||||
}
|
||||
return 'sub2api'
|
||||
}
|
||||
|
||||
function handleLinkedUpstreamChange(id: number | null) {
|
||||
form.value.upstream_platform = detectUpstreamPlatform(id)
|
||||
}
|
||||
|
||||
async function syncLinkedUpstreamPlatform() {
|
||||
if (form.value.access_mode !== 'remote_browser' || !form.value.linked_upstream_id) return
|
||||
if (form.value.upstream_platform === 'new-api') {
|
||||
await upstreamsApi.update(form.value.linked_upstream_id, {
|
||||
api_prefix: '',
|
||||
groups_endpoint: '/api/user/self/groups',
|
||||
rate_endpoint: '/api/user/self/groups',
|
||||
balance_endpoint: '/api/user/self',
|
||||
balance_response_path: 'data.quota',
|
||||
balance_divisor: 500000,
|
||||
auth_config: { login_path: '/api/user/login', username_field: 'username' },
|
||||
} as any)
|
||||
return
|
||||
}
|
||||
await upstreamsApi.update(form.value.linked_upstream_id, {
|
||||
api_prefix: '/api/v1',
|
||||
groups_endpoint: '/groups/available',
|
||||
rate_endpoint: '/groups/rates',
|
||||
balance_endpoint: '/auth/me',
|
||||
balance_response_path: 'data.balance',
|
||||
balance_divisor: 1,
|
||||
auth_config: { login_path: '/auth/login', username_field: 'email' },
|
||||
} as any)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
saving.value = true
|
||||
try {
|
||||
const { login_password_configured: _passwordConfigured, ...payload } = {
|
||||
...form.value,
|
||||
use_proxy: form.value.access_mode === 'proxy',
|
||||
const savePayload: CustomPageForm = {
|
||||
name: form.value.name,
|
||||
url: form.value.url,
|
||||
icon: form.value.icon,
|
||||
sort_order: form.value.sort_order,
|
||||
enabled: form.value.enabled,
|
||||
description: form.value.description,
|
||||
}
|
||||
delete (payload as any).upstream_platform
|
||||
const hasNewLoginCredentials = Boolean(payload.login_username?.trim() && payload.login_password?.trim())
|
||||
if (!loginAutofillTouched.value && hasNewLoginCredentials) {
|
||||
payload.login_autofill_enabled = true
|
||||
}
|
||||
const { login_autofill_enabled: _autofillEnabled, ...payloadWithoutAutofill } = payload
|
||||
const savePayload = loginAutofillTouched.value || hasNewLoginCredentials ? payload : payloadWithoutAutofill
|
||||
if (editingId.value) {
|
||||
await customPagesApi.update(editingId.value, savePayload)
|
||||
} else {
|
||||
await customPagesApi.create(savePayload)
|
||||
}
|
||||
await syncLinkedUpstreamPlatform()
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
loadList()
|
||||
@@ -386,8 +225,8 @@ async function confirmDelete(page: CustomPageData) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function openViewer(page: CustomPageData) {
|
||||
router.push(`/page/${page.id}`)
|
||||
function openExternalPage(page: CustomPageData) {
|
||||
window.open(page.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
onMounted(loadList)
|
||||
@@ -459,7 +298,6 @@ onMounted(loadList)
|
||||
}
|
||||
.disabled-tag { flex-shrink: 0; }
|
||||
.tag-group { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
.proxy-tag { flex-shrink: 0; }
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
@@ -482,38 +320,6 @@ onMounted(loadList)
|
||||
}
|
||||
|
||||
.form-hint { font-size: 12px; color: var(--text-muted); margin-left: 8px; }
|
||||
.mode-hint { margin-left: 0; margin-top: 6px; line-height: 1.4; }
|
||||
.access-mode-group { max-width: 100%; }
|
||||
|
||||
.login-section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.login-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
padding-left: 90px;
|
||||
}
|
||||
.login-hint {
|
||||
margin: 0 0 12px 90px;
|
||||
}
|
||||
.selector-collapse {
|
||||
margin-left: 90px;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.password-field {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user