关于页运行状态、代理关联租户与步进框修复
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
||||
import AppInputNumber from '@/components/AppInputNumber.vue'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import ProxyTenantsModal from '@/components/proxy/ProxyTenantsModal.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import type { ProxyImportResult, ProxyInfo } from '@/types/api'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -120,6 +122,15 @@ async function remove(row: ProxyInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 关联租户管理 ----
|
||||
const showTenants = ref(false)
|
||||
const tenantsFor = ref<ProxyInfo | null>(null)
|
||||
|
||||
function openTenants(row: ProxyInfo) {
|
||||
tenantsFor.value = row
|
||||
showTenants.value = true
|
||||
}
|
||||
|
||||
// ---- 手动重测出口地区 ----
|
||||
const probingIds = ref(new Set<number>())
|
||||
|
||||
@@ -207,13 +218,15 @@ const columns: DataTableColumns<ProxyInfo> = [
|
||||
{
|
||||
title: '认证',
|
||||
key: 'username',
|
||||
width: 110,
|
||||
render: (row) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: 'text-[12.5px] text-ink-2' },
|
||||
row.username ? `${row.username}${row.passwordSet ? ' / ●●●' : ''}` : row.passwordSet ? '●●●' : '无',
|
||||
),
|
||||
width: 76,
|
||||
render: (row) => {
|
||||
const has = !!row.username || row.passwordSet
|
||||
// 悬停「是」可见用户名(无用户名则提示仅密码)
|
||||
const title = has ? (row.username ? `用户名 ${row.username}` : '仅密码认证') : undefined
|
||||
return h('span', { title }, [
|
||||
h(StatusBadge, { kind: has ? 'run' : 'check', label: has ? '是' : '否', dot: false }),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联租户',
|
||||
@@ -225,7 +238,7 @@ const columns: DataTableColumns<ProxyInfo> = [
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 168,
|
||||
width: 236,
|
||||
render: (row) =>
|
||||
h('div', { class: 'flex items-center gap-1' }, [
|
||||
h(
|
||||
@@ -233,6 +246,7 @@ const columns: DataTableColumns<ProxyInfo> = [
|
||||
{ size: 'tiny', quaternary: true, loading: probingIds.value.has(row.id), onClick: () => probe(row) },
|
||||
() => '重测',
|
||||
),
|
||||
h(NButton, { size: 'tiny', quaternary: true, onClick: () => openTenants(row) }, () => '关联租户'),
|
||||
h(NButton, { size: 'tiny', quaternary: true, onClick: () => openEdit(row) }, () => '编辑'),
|
||||
h(
|
||||
NPopconfirm,
|
||||
@@ -261,6 +275,12 @@ const columns: DataTableColumns<ProxyInfo> = [
|
||||
支持 SOCKS5 / HTTP / HTTPS;密码 AES 加密存储、不回显明文;地区为经代理实测的出口位置(ip-api.com),创建后自动检测;被租户关联的代理不可删除,请先解除关联
|
||||
</FootNote>
|
||||
|
||||
<ProxyTenantsModal
|
||||
v-model:show="showTenants"
|
||||
:proxy="tenantsFor"
|
||||
@saved="void proxies.run({ silent: true })"
|
||||
/>
|
||||
|
||||
<NModal
|
||||
v-model:show="showForm"
|
||||
preset="card"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NModal } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { listConfigs } from '@/api/configs'
|
||||
import { setProxyTenants } from '@/api/proxies'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import TenantPicker from '@/components/TenantPicker.vue'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import type { ProxyInfo } from '@/types/api'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
/** 代理侧关联租户管理:打开时回显当前关联名单,多选增删,保存即设置全集
|
||||
* (选中即关联,含从其他代理改挂;取消选中即解除)。 */
|
||||
const props = defineProps<{ show: boolean; proxy: ProxyInfo | null }>()
|
||||
const emit = defineEmits<{ 'update:show': [boolean]; saved: [] }>()
|
||||
|
||||
const message = useToast()
|
||||
const configs = useAsync(listConfigs, false)
|
||||
const selected = ref<number[]>([])
|
||||
const saving = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (v) => {
|
||||
if (!v || !props.proxy) return
|
||||
selected.value = []
|
||||
await configs.run()
|
||||
const pid = props.proxy.id
|
||||
selected.value = (configs.data.value ?? []).filter((c) => c.proxyId === pid).map((c) => c.id)
|
||||
},
|
||||
)
|
||||
|
||||
const all = computed(() => configs.data.value ?? [])
|
||||
|
||||
/** 当前选中名单(打开时即已关联名单),chips 展示并支持点 × 移除 */
|
||||
const picked = computed(() => all.value.filter((c) => selected.value.includes(c.id)))
|
||||
|
||||
/** 选中但已挂其他代理的租户:保存即改挂,给出警示 */
|
||||
const rebinding = computed(() =>
|
||||
picked.value.filter((c) => c.proxyId !== null && c.proxyId !== props.proxy?.id),
|
||||
)
|
||||
|
||||
function removeOne(id: number) {
|
||||
selected.value = selected.value.filter((v) => v !== id)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!props.proxy) return
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await setProxyTenants(props.proxy.id, selected.value)
|
||||
message.success(`已更新关联,当前 ${res.usedBy} 个租户经此代理出站`)
|
||||
emit('update:show', false)
|
||||
emit('saved')
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
:show="show"
|
||||
preset="card"
|
||||
:title="`关联租户 · ${proxy?.name ?? ''}`"
|
||||
:style="{ width: '480px', maxWidth: 'calc(100vw - 24px)' }"
|
||||
@update:show="emit('update:show', $event)"
|
||||
>
|
||||
<FormField label="关联租户" hint="选中即关联,取消选中即解除;关联后租户全部 SDK 请求经此代理出站">
|
||||
<TenantPicker
|
||||
v-model:values="selected"
|
||||
multiple
|
||||
:configs="all"
|
||||
:loading="configs.loading.value"
|
||||
placeholder="选择要关联的租户"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div v-if="picked.length" class="mb-3 flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="c in picked"
|
||||
:key="c.id"
|
||||
class="inline-flex items-center gap-1 rounded-full border border-line bg-wash px-2.5 py-0.5 text-[11.5px] font-medium text-ink-2"
|
||||
>
|
||||
{{ c.alias }}
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer text-ink-3 hover:text-err"
|
||||
:aria-label="`移除 ${c.alias}`"
|
||||
@click="removeOne(c.id)"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="mb-3 text-xs text-ink-3">暂无租户关联此代理</div>
|
||||
|
||||
<div
|
||||
v-if="rebinding.length"
|
||||
class="mb-3 flex items-start gap-2 rounded-lg border border-warn/30 bg-warn/10 px-3 py-2.5 text-xs leading-relaxed"
|
||||
>
|
||||
<svg
|
||||
class="mt-0.5 h-[14px] w-[14px] flex-none text-warn"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" />
|
||||
<path d="M12 9v4M12 17h.01" />
|
||||
</svg>
|
||||
<span>
|
||||
<template v-for="(c, i) in rebinding" :key="c.id">
|
||||
<template v-if="i > 0">、</template>
|
||||
<b>{{ c.alias }}</b>
|
||||
<span v-if="c.proxyName" class="text-ink-3">(现挂 {{ c.proxyName }})</span>
|
||||
</template>
|
||||
已关联其他代理,保存后将改挂到本代理
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<NButton size="small" type="primary" :loading="saving" @click="save">保存</NButton>
|
||||
<NButton size="small" quaternary @click="emit('update:show', false)">取消</NButton>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user