mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
feat(frontend): navy dark theme + rounded inbound/client corners
Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cedc46a14d
commit
90792e0f43
10 changed files with 587 additions and 638 deletions
|
|
@ -27,8 +27,15 @@ export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light'));
|
||||||
|
|
||||||
// AD-Vue 4 theme config consumed by every page's <a-config-provider>.
|
// AD-Vue 4 theme config consumed by every page's <a-config-provider>.
|
||||||
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
|
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
|
||||||
// blue primary. Ultra-dark layers deeper background tokens on top of
|
// blue primary. Dark uses a navy palette across page/cards/modals so
|
||||||
// darkAlgorithm so layouts/cards/popups all darken together.
|
// the sidebar blends with the rest of the surface; ultra-dark stays
|
||||||
|
// neutral black on top of darkAlgorithm.
|
||||||
|
const DARK_TOKENS = {
|
||||||
|
colorBgBase: '#0a1426',
|
||||||
|
colorBgLayout: '#0a1426',
|
||||||
|
colorBgContainer: '#142340',
|
||||||
|
colorBgElevated: '#1a2c4d',
|
||||||
|
};
|
||||||
const ULTRA_DARK_TOKENS = {
|
const ULTRA_DARK_TOKENS = {
|
||||||
colorBgBase: '#000',
|
colorBgBase: '#000',
|
||||||
colorBgLayout: '#000',
|
colorBgLayout: '#000',
|
||||||
|
|
@ -36,13 +43,45 @@ const ULTRA_DARK_TOKENS = {
|
||||||
colorBgElevated: '#141414',
|
colorBgElevated: '#141414',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// AD-Vue 4 hardcodes navy `#001529` / `#002140` as the Layout sider
|
||||||
|
// + trigger backgrounds and `#001529` / `#000c17` as the dark Menu item
|
||||||
|
// backgrounds (see node_modules/ant-design-vue/es/{layout,menu}/style/
|
||||||
|
// index.js). Override at the component-token level so the sider blends
|
||||||
|
// with darkAlgorithm's neutral surfaces.
|
||||||
|
// Dark theme uses a refined navy for the sidebar — distinct from the
|
||||||
|
// neutral ultra-dark and warmer than AD-Vue's stock #001529.
|
||||||
|
const DARK_LAYOUT_TOKENS = {
|
||||||
|
colorBgHeader: '#0d1d33',
|
||||||
|
colorBgTrigger: '#15294a',
|
||||||
|
colorBgBody: '#000',
|
||||||
|
};
|
||||||
|
const ULTRA_DARK_LAYOUT_TOKENS = {
|
||||||
|
colorBgHeader: '#0a0a0a',
|
||||||
|
colorBgTrigger: '#141414',
|
||||||
|
colorBgBody: '#000',
|
||||||
|
};
|
||||||
|
const DARK_MENU_TOKENS = {
|
||||||
|
colorItemBg: '#0d1d33',
|
||||||
|
colorSubItemBg: '#08142a',
|
||||||
|
menuSubMenuBg: '#0d1d33',
|
||||||
|
};
|
||||||
|
const ULTRA_DARK_MENU_TOKENS = {
|
||||||
|
colorItemBg: '#0a0a0a',
|
||||||
|
colorSubItemBg: '#000',
|
||||||
|
menuSubMenuBg: '#0a0a0a',
|
||||||
|
};
|
||||||
|
|
||||||
export const antdThemeConfig = computed(() => {
|
export const antdThemeConfig = computed(() => {
|
||||||
if (!theme.isDark) {
|
if (!theme.isDark) {
|
||||||
return { algorithm: antdTheme.defaultAlgorithm };
|
return { algorithm: antdTheme.defaultAlgorithm };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
algorithm: antdTheme.darkAlgorithm,
|
algorithm: antdTheme.darkAlgorithm,
|
||||||
token: theme.isUltra ? ULTRA_DARK_TOKENS : undefined,
|
token: theme.isUltra ? ULTRA_DARK_TOKENS : DARK_TOKENS,
|
||||||
|
components: {
|
||||||
|
Layout: theme.isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS,
|
||||||
|
Menu: theme.isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,22 +172,9 @@ async function submit() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="open" :title="t('pages.client.bulk')" :ok-text="t('create')" :cancel-text="t('close')"
|
||||||
:open="open"
|
:confirm-loading="saving" :mask-closable="false" @ok="submit" @cancel="close">
|
||||||
:title="t('pages.client.bulk')"
|
<a-form v-if="inbound" :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
:ok-text="t('create')"
|
|
||||||
:cancel-text="t('close')"
|
|
||||||
:confirm-loading="saving"
|
|
||||||
:mask-closable="false"
|
|
||||||
@ok="submit"
|
|
||||||
@cancel="close"
|
|
||||||
>
|
|
||||||
<a-form
|
|
||||||
v-if="inbound"
|
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
>
|
|
||||||
<a-form-item :label="t('pages.client.method')">
|
<a-form-item :label="t('pages.client.method')">
|
||||||
<a-select v-model:value="form.emailMethod">
|
<a-select v-model:value="form.emailMethod">
|
||||||
<a-select-option :value="0">Random</a-select-option>
|
<a-select-option :value="0">Random</a-select-option>
|
||||||
|
|
@ -251,10 +238,7 @@ async function submit() {
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item :label="t('pages.client.delayedStart')">
|
<a-form-item :label="t('pages.client.delayedStart')">
|
||||||
<a-switch
|
<a-switch v-model:checked="delayedStart" @click="form.expiryTime = 0" />
|
||||||
v-model:checked="delayedStart"
|
|
||||||
@click="form.expiryTime = 0"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
||||||
|
|
@ -263,14 +247,11 @@ async function submit() {
|
||||||
|
|
||||||
<a-form-item v-else>
|
<a-form-item v-else>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate') }}</a-tooltip>
|
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
|
||||||
|
}}</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-date-picker
|
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
v-model:value="expiryDate"
|
:style="{ width: '100%' }" />
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="form.expiryTime !== 0">
|
<a-form-item v-if="form.expiryTime !== 0">
|
||||||
|
|
|
||||||
|
|
@ -234,59 +234,47 @@ const title = computed(() =>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="open" :title="title"
|
||||||
:open="open"
|
:ok-text="mode === 'edit' ? t('pages.client.submitEdit') : t('pages.client.submitAdd')" :cancel-text="t('close')"
|
||||||
:title="title"
|
:confirm-loading="saving" :mask-closable="false" @ok="submit" @cancel="close">
|
||||||
:ok-text="mode === 'edit' ? t('pages.client.submitEdit') : t('pages.client.submitAdd')"
|
<a-tag v-if="mode === 'edit' && (isExpired || isTrafficExhausted)" color="red" class="status-banner">
|
||||||
:cancel-text="t('close')"
|
|
||||||
:confirm-loading="saving"
|
|
||||||
:mask-closable="false"
|
|
||||||
@ok="submit"
|
|
||||||
@cancel="close"
|
|
||||||
>
|
|
||||||
<a-tag
|
|
||||||
v-if="mode === 'edit' && (isExpired || isTrafficExhausted)"
|
|
||||||
color="red"
|
|
||||||
class="status-banner"
|
|
||||||
>
|
|
||||||
{{ t('depleted') }}
|
{{ t('depleted') }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
|
||||||
<a-form
|
<a-form v-if="client && inbound" layout="horizontal" :colon="false" :label-col="{ md: { span: 8 } }"
|
||||||
v-if="client && inbound"
|
:wrapper-col="{ md: { span: 14 } }">
|
||||||
layout="horizontal"
|
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
>
|
|
||||||
<a-form-item :label="t('enable')">
|
<a-form-item :label="t('enable')">
|
||||||
<a-switch v-model:checked="client.enable" />
|
<a-switch v-model:checked="client.enable" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ t('pages.inbounds.email') }} <SyncOutlined class="random-icon" @click="randomEmail" />
|
{{ t('pages.inbounds.email') }}
|
||||||
|
<SyncOutlined class="random-icon" @click="randomEmail" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="client.email" />
|
<a-input v-model:value="client.email" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="isTrojanOrSS">
|
<a-form-item v-if="isTrojanOrSS">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ t('password') }} <SyncOutlined class="random-icon" @click="randomPassword" />
|
{{ t('password') }}
|
||||||
|
<SyncOutlined class="random-icon" @click="randomPassword" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="client.password" />
|
<a-input v-model:value="client.password" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="protocol === Protocols.HYSTERIA">
|
<a-form-item v-if="protocol === Protocols.HYSTERIA">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ t('password') }} <SyncOutlined class="random-icon" @click="randomAuth" />
|
{{ t('password') }}
|
||||||
|
<SyncOutlined class="random-icon" @click="randomAuth" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="client.auth" />
|
<a-input v-model:value="client.auth" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="isVmessOrVless">
|
<a-form-item v-if="isVmessOrVless">
|
||||||
<template #label>
|
<template #label>
|
||||||
ID <SyncOutlined class="random-icon" @click="randomId" />
|
ID
|
||||||
|
<SyncOutlined class="random-icon" @click="randomId" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="client.id" />
|
<a-input v-model:value="client.id" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -301,7 +289,8 @@ const title = computed(() =>
|
||||||
|
|
||||||
<a-form-item v-if="client.email && subEnable">
|
<a-form-item v-if="client.email && subEnable">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ t('subscription.title') }} <SyncOutlined class="random-icon" @click="randomSubId" />
|
{{ t('subscription.title') }}
|
||||||
|
<SyncOutlined class="random-icon" @click="randomSubId" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="client.subId" />
|
<a-input v-model:value="client.subId" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -318,19 +307,14 @@ const title = computed(() =>
|
||||||
<a-input-number v-model:value="client.limitIp" :min="0" />
|
<a-input-number v-model:value="client.limitIp" :min="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item
|
<a-form-item v-if="ipLimitEnable && client.limitIp > 0 && client.email && mode === 'edit'"
|
||||||
v-if="ipLimitEnable && client.limitIp > 0 && client.email && mode === 'edit'"
|
:label="t('pages.inbounds.IPLimitlog')">
|
||||||
:label="t('pages.inbounds.IPLimitlog')"
|
<a-textarea v-model:value="clientIpsText" readonly :placeholder="t('pages.inbounds.IPLimitlogDesc')"
|
||||||
>
|
:auto-size="{ minRows: 3, maxRows: 8 }" @click="loadClientIps" />
|
||||||
<a-textarea
|
|
||||||
v-model:value="clientIpsText"
|
|
||||||
readonly
|
|
||||||
:placeholder="t('pages.inbounds.IPLimitlogDesc')"
|
|
||||||
:auto-size="{ minRows: 3, maxRows: 8 }"
|
|
||||||
@click="loadClientIps"
|
|
||||||
/>
|
|
||||||
<a-button type="link" size="small" danger @click="clearClientIps">
|
<a-button type="link" size="small" danger @click="clearClientIps">
|
||||||
<template #icon><DeleteOutlined /></template>
|
<template #icon>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</template>
|
||||||
{{ t('pages.inbounds.IPLimitlogclear') }}
|
{{ t('pages.inbounds.IPLimitlogclear') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -367,10 +351,7 @@ const title = computed(() =>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item :label="t('pages.client.delayedStart')">
|
<a-form-item :label="t('pages.client.delayedStart')">
|
||||||
<a-switch
|
<a-switch v-model:checked="delayedStart" @click="client.expiryTime = 0" />
|
||||||
v-model:checked="delayedStart"
|
|
||||||
@click="client.expiryTime = 0"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
||||||
|
|
@ -379,14 +360,11 @@ const title = computed(() =>
|
||||||
|
|
||||||
<a-form-item v-else>
|
<a-form-item v-else>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate') }}</a-tooltip>
|
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
|
||||||
|
}}</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-date-picker
|
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
v-model:value="expiryDate"
|
:style="{ width: '100%' }" />
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
<a-tag v-if="mode === 'edit' && isExpired" color="red">{{ t('depleted') }}</a-tag>
|
<a-tag v-if="mode === 'edit' && isExpired" color="red">{{ t('depleted') }}</a-tag>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,11 +221,8 @@ function rowKey(client) {
|
||||||
|
|
||||||
<!-- Enable switch (hidden on mobile, lives in dropdown) -->
|
<!-- Enable switch (hidden on mobile, lives in dropdown) -->
|
||||||
<div v-if="!isMobile" class="cell cell-enable">
|
<div v-if="!isMobile" class="cell cell-enable">
|
||||||
<a-switch
|
<a-switch :checked="client.enable" size="small"
|
||||||
:checked="client.enable"
|
@change="(next) => emit('toggle-enable-client', { dbInbound, client, next })" />
|
||||||
size="small"
|
|
||||||
@change="(next) => emit('toggle-enable-client', { dbInbound, client, next })"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Online tag (desktop only) -->
|
<!-- Online tag (desktop only) -->
|
||||||
|
|
@ -277,21 +274,11 @@ function rowKey(client) {
|
||||||
</template>
|
</template>
|
||||||
<div class="usage-bar">
|
<div class="usage-bar">
|
||||||
<span class="usage-text">{{ SizeFormatter.sizeFormat(getSum(client.email)) }}</span>
|
<span class="usage-text">{{ SizeFormatter.sizeFormat(getSum(client.email)) }}</span>
|
||||||
<a-progress
|
<a-progress v-if="!client.enable" :stroke-color="isDarkTheme ? 'rgb(72,84,105)' : '#bcbcbc'"
|
||||||
v-if="!client.enable"
|
:show-info="false" :percent="statsProgress(client.email)" size="small" />
|
||||||
:stroke-color="isDarkTheme ? 'rgb(72,84,105)' : '#bcbcbc'"
|
<a-progress v-else-if="client.totalGB > 0" :stroke-color="clientStatsColor(client.email)" :show-info="false"
|
||||||
:show-info="false"
|
:status="isClientDepleted(client.email) ? 'exception' : ''" :percent="statsProgress(client.email)"
|
||||||
:percent="statsProgress(client.email)"
|
size="small" />
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<a-progress
|
|
||||||
v-else-if="client.totalGB > 0"
|
|
||||||
:stroke-color="clientStatsColor(client.email)"
|
|
||||||
:show-info="false"
|
|
||||||
:status="isClientDepleted(client.email) ? 'exception' : ''"
|
|
||||||
:percent="statsProgress(client.email)"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<a-progress v-else :show-info="false" :percent="100" stroke-color="#722ed1" size="small" />
|
<a-progress v-else :show-info="false" :percent="100" stroke-color="#722ed1" size="small" />
|
||||||
<span class="usage-text">
|
<span class="usage-text">
|
||||||
<InfinityIcon v-if="isUnlimitedTotal(client)" />
|
<InfinityIcon v-if="isUnlimitedTotal(client)" />
|
||||||
|
|
@ -316,12 +303,8 @@ function rowKey(client) {
|
||||||
</template>
|
</template>
|
||||||
<div class="usage-bar">
|
<div class="usage-bar">
|
||||||
<span class="usage-text">{{ IntlUtil.formatRelativeTime(client.expiryTime) }}</span>
|
<span class="usage-text">{{ IntlUtil.formatRelativeTime(client.expiryTime) }}</span>
|
||||||
<a-progress
|
<a-progress :show-info="false" :status="isClientDepleted(client.email) ? 'exception' : ''"
|
||||||
:show-info="false"
|
:percent="expireProgress(client.expiryTime, client.reset)" size="small" />
|
||||||
:status="isClientDepleted(client.email) ? 'exception' : ''"
|
|
||||||
:percent="expireProgress(client.expiryTime, client.reset)"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<span class="usage-text">{{ client.reset }}d</span>
|
<span class="usage-text">{{ client.reset }}d</span>
|
||||||
</div>
|
</div>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
|
|
@ -331,19 +314,13 @@ function rowKey(client) {
|
||||||
<span v-if="client.expiryTime < 0">{{ t('pages.client.delayedStart') }}</span>
|
<span v-if="client.expiryTime < 0">{{ t('pages.client.delayedStart') }}</span>
|
||||||
<span v-else>{{ IntlUtil.formatDate(client.expiryTime) }}</span>
|
<span v-else>{{ IntlUtil.formatDate(client.expiryTime) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-tag
|
<a-tag :style="{ minWidth: '50px', border: 'none' }"
|
||||||
:style="{ minWidth: '50px', border: 'none' }"
|
:color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)">
|
||||||
:color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)"
|
|
||||||
>
|
|
||||||
{{ IntlUtil.formatRelativeTime(client.expiryTime) }}
|
{{ IntlUtil.formatRelativeTime(client.expiryTime) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-tag
|
<a-tag v-else :color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)" :style="{ border: 'none' }"
|
||||||
v-else
|
class="infinite-tag">
|
||||||
:color="ColorUtils.userExpiryColor(expireDiff, client, isDarkTheme)"
|
|
||||||
:style="{ border: 'none' }"
|
|
||||||
class="infinite-tag"
|
|
||||||
>
|
|
||||||
<InfinityIcon />
|
<InfinityIcon />
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -378,7 +355,9 @@ function rowKey(client) {
|
||||||
<a-tag v-else-if="client.expiryTime < 0" color="green">
|
<a-tag v-else-if="client.expiryTime < 0" color="green">
|
||||||
{{ -client.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
|
{{ -client.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
<a-tag v-else color="purple">
|
||||||
|
<InfinityIcon />
|
||||||
|
</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -402,21 +381,30 @@ function rowKey(client) {
|
||||||
.client-row {
|
.client-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
140px /* actions */
|
140px
|
||||||
60px /* enable */
|
/* actions */
|
||||||
80px /* online */
|
60px
|
||||||
minmax(160px, 2fr) /* client identity */
|
/* enable */
|
||||||
minmax(160px, 2fr) /* traffic */
|
80px
|
||||||
130px /* all-time */
|
/* online */
|
||||||
140px; /* expiry */
|
minmax(160px, 2fr)
|
||||||
|
/* client identity */
|
||||||
|
minmax(160px, 2fr)
|
||||||
|
/* traffic */
|
||||||
|
130px
|
||||||
|
/* all-time */
|
||||||
|
140px;
|
||||||
|
/* expiry */
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.12);
|
border-top: 1px solid rgba(128, 128, 128, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-row:last-child {
|
.client-row:last-child {
|
||||||
border-bottom: 1px solid rgba(128, 128, 128, 0.12);
|
border-bottom: 1px solid rgba(128, 128, 128, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-list-header {
|
.client-list-header {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -435,8 +423,10 @@ function rowKey(client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
min-width: 0; /* allow grid children to shrink instead of overflowing */
|
min-width: 0;
|
||||||
|
/* allow grid children to shrink instead of overflowing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-actions,
|
.cell-actions,
|
||||||
.cell-enable,
|
.cell-enable,
|
||||||
.cell-online,
|
.cell-online,
|
||||||
|
|
@ -449,22 +439,27 @@ function rowKey(client) {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-actions {
|
.cell-actions {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-client {
|
.cell-client {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-traffic,
|
.cell-traffic,
|
||||||
.cell-expiry {
|
.cell-expiry {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-list-header .cell {
|
.client-list-header .cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-list-header .cell-actions,
|
.client-list-header .cell-actions,
|
||||||
.client-list-header .cell-client {
|
.client-list-header .cell-client {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -478,13 +473,18 @@ function rowKey(client) {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: color 120ms ease;
|
transition: color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-icon:hover {
|
.row-icon:hover {
|
||||||
color: var(--ant-color-primary, #1677ff);
|
color: var(--ant-color-primary, #1677ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-icon.danger {
|
.row-icon.danger {
|
||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
.danger { color: #ff4d4f; }
|
|
||||||
|
.danger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
/* Client identity stack (badge + email + comment) */
|
/* Client identity stack (badge + email + comment) */
|
||||||
.client-id-stack {
|
.client-id-stack {
|
||||||
|
|
@ -494,6 +494,7 @@ function rowKey(client) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-email {
|
.client-email {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -501,6 +502,7 @@ function rowKey(client) {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-comment {
|
.client-comment {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
@ -517,10 +519,12 @@ function rowKey(client) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-text {
|
.usage-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-bar :deep(.ant-progress) {
|
.usage-bar :deep(.ant-progress) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -534,8 +538,15 @@ function rowKey(client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile popover content table */
|
/* Mobile popover content table */
|
||||||
.text-center { text-align: center; }
|
.text-center {
|
||||||
.num-cell { text-align: right; font-size: 12px; padding: 2px 6px; }
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-cell {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Strip AD-Vue's default expanded-cell padding so the grid sits
|
/* Strip AD-Vue's default expanded-cell padding so the grid sits
|
||||||
* flush against the inbound row's left/right edges. */
|
* flush against the inbound row's left/right edges. */
|
||||||
|
|
|
||||||
|
|
@ -502,17 +502,8 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="open" :title="title" :ok-text="okText" :cancel-text="t('close')" :confirm-loading="saving"
|
||||||
:open="open"
|
:mask-closable="false" width="780px" @ok="submit" @cancel="close">
|
||||||
:title="title"
|
|
||||||
:ok-text="okText"
|
|
||||||
:cancel-text="t('close')"
|
|
||||||
:confirm-loading="saving"
|
|
||||||
:mask-closable="false"
|
|
||||||
width="780px"
|
|
||||||
@ok="submit"
|
|
||||||
@cancel="close"
|
|
||||||
>
|
|
||||||
<a-tabs v-if="inbound && dbForm" default-active-key="basic">
|
<a-tabs v-if="inbound && dbForm" default-active-key="basic">
|
||||||
<!-- ============================== BASICS ============================== -->
|
<!-- ============================== BASICS ============================== -->
|
||||||
<a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
|
<a-tab-pane key="basic" :tab="t('pages.xray.basicTemplate')">
|
||||||
|
|
@ -549,14 +540,11 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate') }}</a-tooltip>
|
<a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
|
||||||
|
}}</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-date-picker
|
<a-date-picker v-model:value="expiryDate" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
v-model:value="expiryDate"
|
:style="{ width: '100%' }" />
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
@ -575,7 +563,8 @@ watch(
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip title="Friendly identifier">
|
<a-tooltip title="Friendly identifier">
|
||||||
Email <SyncOutlined class="random-icon" @click="randomEmail(firstClient)" />
|
Email
|
||||||
|
<SyncOutlined class="random-icon" @click="randomEmail(firstClient)" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="firstClient.email" />
|
<a-input v-model:value="firstClient.email" />
|
||||||
|
|
@ -584,7 +573,8 @@ watch(
|
||||||
<a-form-item v-if="protocol === Protocols.VMESS || protocol === Protocols.VLESS">
|
<a-form-item v-if="protocol === Protocols.VMESS || protocol === Protocols.VLESS">
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip title="Reset to a fresh UUID">
|
<a-tooltip title="Reset to a fresh UUID">
|
||||||
ID <SyncOutlined class="random-icon" @click="randomUuid(firstClient)" />
|
ID
|
||||||
|
<SyncOutlined class="random-icon" @click="randomUuid(firstClient)" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="firstClient.id" />
|
<a-input v-model:value="firstClient.id" />
|
||||||
|
|
@ -600,16 +590,9 @@ watch(
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip title="Reset to a fresh random value">
|
<a-tooltip title="Reset to a fresh random value">
|
||||||
Password
|
Password
|
||||||
<SyncOutlined
|
<SyncOutlined v-if="protocol === Protocols.SHADOWSOCKS" class="random-icon"
|
||||||
v-if="protocol === Protocols.SHADOWSOCKS"
|
@click="randomSSPassword(firstClient)" />
|
||||||
class="random-icon"
|
<SyncOutlined v-else class="random-icon" @click="randomPasswordSeq(firstClient)" />
|
||||||
@click="randomSSPassword(firstClient)"
|
|
||||||
/>
|
|
||||||
<SyncOutlined
|
|
||||||
v-else
|
|
||||||
class="random-icon"
|
|
||||||
@click="randomPasswordSeq(firstClient)"
|
|
||||||
/>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="firstClient.password" />
|
<a-input v-model:value="firstClient.password" />
|
||||||
|
|
@ -617,7 +600,9 @@ watch(
|
||||||
|
|
||||||
<a-form-item v-if="protocol === Protocols.HYSTERIA">
|
<a-form-item v-if="protocol === Protocols.HYSTERIA">
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip title="Reset"><span>Auth password</span> <SyncOutlined class="random-icon" @click="randomAuth(firstClient)" /></a-tooltip>
|
<a-tooltip title="Reset"><span>Auth password</span>
|
||||||
|
<SyncOutlined class="random-icon" @click="randomAuth(firstClient)" />
|
||||||
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="firstClient.auth" />
|
<a-input v-model:value="firstClient.auth" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -646,27 +631,22 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="Expiry">
|
<a-form-item label="Expiry">
|
||||||
<a-date-picker
|
<a-date-picker v-model:value="clientExpiryDate" :show-time="{ format: 'HH:mm:ss' }"
|
||||||
v-model:value="clientExpiryDate"
|
format="YYYY-MM-DD HH:mm:ss" :style="{ width: '100%' }" />
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
<a-collapse-panel
|
<a-collapse-panel key="summary" :header="`Clients: ${clientsArray.length}`">
|
||||||
key="summary"
|
|
||||||
:header="`Clients: ${clientsArray.length}`"
|
|
||||||
>
|
|
||||||
<table class="client-summary">
|
<table class="client-summary">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>{{ protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS ? 'Password' : (protocol === Protocols.HYSTERIA ? 'Auth' : 'ID') }}</th>
|
<th>{{ protocol === Protocols.TROJAN || protocol === Protocols.SHADOWSOCKS ? 'Password' : (protocol
|
||||||
|
===
|
||||||
|
Protocols.HYSTERIA ? 'Auth' : 'ID') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -681,13 +661,8 @@ watch(
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- VLess decryption / encryption -->
|
<!-- VLess decryption / encryption -->
|
||||||
<a-form
|
<a-form v-if="protocol === Protocols.VLESS" :colon="false" :label-col="{ md: { span: 8 } }"
|
||||||
v-if="protocol === Protocols.VLESS"
|
:wrapper-col="{ md: { span: 14 } }" class="mt-12">
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
class="mt-12"
|
|
||||||
>
|
|
||||||
<a-form-item label="Decryption">
|
<a-form-item label="Decryption">
|
||||||
<a-input v-model:value="inbound.settings.decryption" />
|
<a-input v-model:value="inbound.settings.decryption" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -708,13 +683,8 @@ watch(
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- Shadowsocks shared fields (method/network/ivCheck) -->
|
<!-- Shadowsocks shared fields (method/network/ivCheck) -->
|
||||||
<a-form
|
<a-form v-if="protocol === Protocols.SHADOWSOCKS" :colon="false" :label-col="{ md: { span: 8 } }"
|
||||||
v-if="protocol === Protocols.SHADOWSOCKS"
|
:wrapper-col="{ md: { span: 14 } }" class="mt-12">
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
class="mt-12"
|
|
||||||
>
|
|
||||||
<a-form-item label="Encryption method">
|
<a-form-item label="Encryption method">
|
||||||
<a-select v-model:value="inbound.settings.method" @change="onSSMethodChange">
|
<a-select v-model:value="inbound.settings.method" @change="onSSMethodChange">
|
||||||
<a-select-option v-for="(m, k) in SSMethods" :key="k" :value="m">{{ k }}</a-select-option>
|
<a-select-option v-for="(m, k) in SSMethods" :key="k" :value="m">{{ k }}</a-select-option>
|
||||||
|
|
@ -740,21 +710,15 @@ watch(
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- HTTP / Mixed accounts -->
|
<!-- HTTP / Mixed accounts -->
|
||||||
<a-form
|
<a-form v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED" :colon="false"
|
||||||
v-if="protocol === Protocols.HTTP || protocol === Protocols.MIXED"
|
:label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }" class="mt-12">
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
class="mt-12"
|
|
||||||
>
|
|
||||||
<a-form-item label="Accounts">
|
<a-form-item label="Accounts">
|
||||||
<a-button
|
<a-button size="small" @click="protocol === Protocols.HTTP
|
||||||
size="small"
|
? inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())
|
||||||
@click="protocol === Protocols.HTTP
|
: inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())">
|
||||||
? inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())
|
<template #icon>
|
||||||
: inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"
|
<PlusOutlined />
|
||||||
>
|
</template>
|
||||||
<template #icon><PlusOutlined /></template>
|
|
||||||
Add
|
Add
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -765,7 +729,9 @@ watch(
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="account.pass" placeholder="Password" />
|
<a-input :style="{ width: '45%' }" v-model:value="account.pass" placeholder="Password" />
|
||||||
<a-button @click="inbound.settings.delAccount(idx)">
|
<a-button @click="inbound.settings.delAccount(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -789,13 +755,8 @@ watch(
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- Tunnel -->
|
<!-- Tunnel -->
|
||||||
<a-form
|
<a-form v-if="protocol === Protocols.TUNNEL" :colon="false" :label-col="{ md: { span: 8 } }"
|
||||||
v-if="protocol === Protocols.TUNNEL"
|
:wrapper-col="{ md: { span: 14 } }" class="mt-12">
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
class="mt-12"
|
|
||||||
>
|
|
||||||
<a-form-item label="Address">
|
<a-form-item label="Address">
|
||||||
<a-input v-model:value="inbound.settings.address" />
|
<a-input v-model:value="inbound.settings.address" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -815,16 +776,12 @@ watch(
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- WireGuard -->
|
<!-- WireGuard -->
|
||||||
<a-form
|
<a-form v-if="protocol === Protocols.WIREGUARD" :colon="false" :label-col="{ md: { span: 8 } }"
|
||||||
v-if="protocol === Protocols.WIREGUARD"
|
:wrapper-col="{ md: { span: 14 } }" class="mt-12">
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
class="mt-12"
|
|
||||||
>
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
Secret key <SyncOutlined class="random-icon" @click="regenInboundWg" />
|
Secret key
|
||||||
|
<SyncOutlined class="random-icon" @click="regenInboundWg" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="inbound.settings.secretKey" />
|
<a-input v-model:value="inbound.settings.secretKey" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -839,22 +796,22 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Peers">
|
<a-form-item label="Peers">
|
||||||
<a-button size="small" @click="inbound.settings.addPeer()">
|
<a-button size="small" @click="inbound.settings.addPeer()">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
Add peer
|
Add peer
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<div v-for="(peer, idx) in inbound.settings.peers" :key="idx" class="wg-peer">
|
<div v-for="(peer, idx) in inbound.settings.peers" :key="idx" class="wg-peer">
|
||||||
<a-divider style="margin: 8px 0">
|
<a-divider style="margin: 8px 0">
|
||||||
Peer {{ idx + 1 }}
|
Peer {{ idx + 1 }}
|
||||||
<DeleteOutlined
|
<DeleteOutlined v-if="inbound.settings.peers.length > 1" class="danger-icon"
|
||||||
v-if="inbound.settings.peers.length > 1"
|
@click="inbound.settings.delPeer(idx)" />
|
||||||
class="danger-icon"
|
|
||||||
@click="inbound.settings.delPeer(idx)"
|
|
||||||
/>
|
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
Secret key <SyncOutlined class="random-icon" @click="regenWgKeypair(peer)" />
|
Secret key
|
||||||
|
<SyncOutlined class="random-icon" @click="regenWgKeypair(peer)" />
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value="peer.privateKey" />
|
<a-input v-model:value="peer.privateKey" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -866,17 +823,16 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Allowed IPs">
|
<a-form-item label="Allowed IPs">
|
||||||
<a-button size="small" @click="peer.allowedIPs.push('')">
|
<a-button size="small" @click="peer.allowedIPs.push('')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-input
|
<a-input v-for="(_ip, j) in peer.allowedIPs" :key="j" v-model:value="peer.allowedIPs[j]" class="mt-4">
|
||||||
v-for="(_ip, j) in peer.allowedIPs"
|
|
||||||
:key="j"
|
|
||||||
v-model:value="peer.allowedIPs[j]"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<template #addonAfter>
|
<template #addonAfter>
|
||||||
<a-button v-if="peer.allowedIPs.length > 1" size="small" @click="peer.allowedIPs.splice(j, 1)">
|
<a-button v-if="peer.allowedIPs.length > 1" size="small" @click="peer.allowedIPs.splice(j, 1)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
|
|
@ -892,25 +848,21 @@ watch(
|
||||||
<a-divider style="margin: 12px 0" />
|
<a-divider style="margin: 12px 0" />
|
||||||
<div class="fallbacks-header">
|
<div class="fallbacks-header">
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
title="Route incoming TLS traffic to a backend when it doesn't match a valid VLESS/Trojan handshake. Match by SNI, ALPN, and HTTP path; the most precise rule wins. Fallbacks require TCP+TLS transport."
|
title="Route incoming TLS traffic to a backend when it doesn't match a valid VLESS/Trojan handshake. Match by SNI, ALPN, and HTTP path; the most precise rule wins. Fallbacks require TCP+TLS transport.">
|
||||||
>
|
|
||||||
<span class="fallbacks-title">
|
<span class="fallbacks-title">
|
||||||
Fallbacks ({{ inbound.settings.fallbacks.length }})
|
Fallbacks ({{ inbound.settings.fallbacks.length }})
|
||||||
</span>
|
</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-button type="primary" size="small" @click="addFallback">
|
<a-button type="primary" size="small" @click="addFallback">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
Add
|
Add
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form
|
<a-form v-for="(fallback, idx) in inbound.settings.fallbacks" :key="idx" :colon="false"
|
||||||
v-for="(fallback, idx) in inbound.settings.fallbacks"
|
:label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
:key="idx"
|
|
||||||
:colon="false"
|
|
||||||
:label-col="{ md: { span: 8 } }"
|
|
||||||
:wrapper-col="{ md: { span: 14 } }"
|
|
||||||
>
|
|
||||||
<a-divider style="margin: 0">
|
<a-divider style="margin: 0">
|
||||||
Fallback {{ idx + 1 }}
|
Fallback {{ idx + 1 }}
|
||||||
<DeleteOutlined class="danger-icon" @click="delFallback(idx)" />
|
<DeleteOutlined class="danger-icon" @click="delFallback(idx)" />
|
||||||
|
|
@ -928,8 +880,7 @@ watch(
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
title="Match TLS ALPN. 'any' = no ALPN constraint. Use h2/http/1.1 split when the inbound advertises both."
|
title="Match TLS ALPN. 'any' = no ALPN constraint. Use h2/http/1.1 split when the inbound advertises both.">
|
||||||
>
|
|
||||||
ALPN
|
ALPN
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -940,42 +891,32 @@ watch(
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item
|
<a-form-item :validate-status="fallback.path && !fallback.path.startsWith('/') ? 'error' : ''"
|
||||||
:validate-status="fallback.path && !fallback.path.startsWith('/') ? 'error' : ''"
|
:help="fallback.path && !fallback.path.startsWith('/') ? 'Path must start with /' : ''">
|
||||||
:help="fallback.path && !fallback.path.startsWith('/') ? 'Path must start with /' : ''"
|
|
||||||
>
|
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
title="Match the HTTP request path of the first packet. Must start with '/'. Leave empty to match any."
|
title="Match the HTTP request path of the first packet. Must start with '/'. Leave empty to match any.">
|
||||||
>
|
|
||||||
Path
|
Path
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model:value.trim="fallback.path" placeholder="any (leave empty) or /ws" />
|
<a-input v-model:value.trim="fallback.path" placeholder="any (leave empty) or /ws" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item
|
<a-form-item :validate-status="!fallback.dest ? 'error' : ''"
|
||||||
:validate-status="!fallback.dest ? 'error' : ''"
|
:help="!fallback.dest ? 'Destination is required' : ''">
|
||||||
:help="!fallback.dest ? 'Destination is required' : ''"
|
|
||||||
>
|
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
title="Where matching traffic is forwarded. Accepts a port number (80), an addr:port (127.0.0.1:8080), or a Unix socket path (/dev/shm/x.sock or @abstract)."
|
title="Where matching traffic is forwarded. Accepts a port number (80), an addr:port (127.0.0.1:8080), or a Unix socket path (/dev/shm/x.sock or @abstract).">
|
||||||
>
|
|
||||||
Destination
|
Destination
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input
|
<a-input v-model:value.trim="fallback.dest" placeholder="80 | 127.0.0.1:8080 | /dev/shm/x.sock" />
|
||||||
v-model:value.trim="fallback.dest"
|
|
||||||
placeholder="80 | 127.0.0.1:8080 | /dev/shm/x.sock"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
title="PROXY protocol version sent to the destination. Off (0) for plain TCP; v1/v2 to preserve client IP if the backend supports it."
|
title="PROXY protocol version sent to the destination. Off (0) for plain TCP; v1/v2 to preserve client IP if the backend supports it.">
|
||||||
>
|
|
||||||
PROXY
|
PROXY
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -990,8 +931,8 @@ watch(
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- ============================== STREAM ============================== -->
|
<!-- ============================== STREAM ============================== -->
|
||||||
<a-tab-pane v-if="canEnableStream" key="stream" tab="Stream"
|
<a-tab-pane v-if="canEnableStream" key="stream"
|
||||||
><!-- "Stream" stays literal — it's a wire-format identifier -->
|
tab="Stream"><!-- "Stream" stays literal — it's a wire-format identifier -->
|
||||||
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
<a-form-item v-if="protocol !== Protocols.HYSTERIA" label="Transmission">
|
<a-form-item v-if="protocol !== Protocols.HYSTERIA" label="Transmission">
|
||||||
<a-select v-model:value="network" :style="{ width: '75%' }">
|
<a-select v-model:value="network" :style="{ width: '75%' }">
|
||||||
|
|
@ -1010,10 +951,8 @@ watch(
|
||||||
<a-switch v-model:checked="inbound.stream.tcp.acceptProxyProtocol" />
|
<a-switch v-model:checked="inbound.stream.tcp.acceptProxyProtocol" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="`HTTP ${t('camouflage')}`">
|
<a-form-item :label="`HTTP ${t('camouflage')}`">
|
||||||
<a-switch
|
<a-switch :checked="inbound.stream.tcp.type === 'http'"
|
||||||
:checked="inbound.stream.tcp.type === 'http'"
|
@change="(v) => (inbound.stream.tcp.type = v ? 'http' : 'none')" />
|
||||||
@change="(v) => (inbound.stream.tcp.type = v ? 'http' : 'none')"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<template v-if="inbound.stream.tcp.type === 'http'">
|
<template v-if="inbound.stream.tcp.type === 'http'">
|
||||||
|
|
@ -1028,19 +967,21 @@ watch(
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ t('pages.inbounds.stream.tcp.path') }}
|
{{ t('pages.inbounds.stream.tcp.path') }}
|
||||||
<a-button size="small" :style="{ marginLeft: '6px' }" @click="inbound.stream.tcp.request.addPath('/')">
|
<a-button size="small" :style="{ marginLeft: '6px' }"
|
||||||
<template #icon><PlusOutlined /></template>
|
@click="inbound.stream.tcp.request.addPath('/')">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-for="(_p, idx) in inbound.stream.tcp.request.path" :key="`tcp-path-${idx}`">
|
<template v-for="(_p, idx) in inbound.stream.tcp.request.path" :key="`tcp-path-${idx}`">
|
||||||
<a-input v-model:value="inbound.stream.tcp.request.path[idx]" class="mb-4">
|
<a-input v-model:value="inbound.stream.tcp.request.path[idx]" class="mb-4">
|
||||||
<template #addonAfter>
|
<template #addonAfter>
|
||||||
<a-button
|
<a-button v-if="inbound.stream.tcp.request.path.length > 1" size="small"
|
||||||
v-if="inbound.stream.tcp.request.path.length > 1"
|
@click="inbound.stream.tcp.request.removePath(idx)">
|
||||||
size="small"
|
<template #icon>
|
||||||
@click="inbound.stream.tcp.request.removePath(idx)"
|
<MinusOutlined />
|
||||||
>
|
</template>
|
||||||
<template #icon><MinusOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
|
|
@ -1048,17 +989,24 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
||||||
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">
|
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ span: 24 }">
|
<a-form-item :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group v-for="(h, idx) in inbound.stream.tcp.request.headers" :key="`tcp-rh-${idx}`" compact class="mb-8">
|
<a-input-group v-for="(h, idx) in inbound.stream.tcp.request.headers" :key="`tcp-rh-${idx}`" compact
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.name" :placeholder="t('pages.inbounds.stream.general.name')">
|
class="mb-8">
|
||||||
|
<a-input :style="{ width: '45%' }" v-model:value="h.name"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.name')">
|
||||||
<template #addonBefore>{{ idx + 1 }}</template>
|
<template #addonBefore>{{ idx + 1 }}</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.value" :placeholder="t('pages.inbounds.stream.general.value')" />
|
<a-input :style="{ width: '45%' }" v-model:value="h.value"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.value')" />
|
||||||
<a-button @click="inbound.stream.tcp.request.removeHeader(idx)">
|
<a-button @click="inbound.stream.tcp.request.removeHeader(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1075,18 +1023,26 @@ watch(
|
||||||
<a-input v-model:value="inbound.stream.tcp.response.reason" />
|
<a-input v-model:value="inbound.stream.tcp.response.reason" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.stream.tcp.responseHeader')">
|
<a-form-item :label="t('pages.inbounds.stream.tcp.responseHeader')">
|
||||||
<a-button size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
<a-button size="small"
|
||||||
<template #icon><PlusOutlined /></template>
|
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ span: 24 }">
|
<a-form-item :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group v-for="(h, idx) in inbound.stream.tcp.response.headers" :key="`tcp-rsh-${idx}`" compact class="mb-8">
|
<a-input-group v-for="(h, idx) in inbound.stream.tcp.response.headers" :key="`tcp-rsh-${idx}`" compact
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.name" :placeholder="t('pages.inbounds.stream.general.name')">
|
class="mb-8">
|
||||||
|
<a-input :style="{ width: '45%' }" v-model:value="h.name"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.name')">
|
||||||
<template #addonBefore>{{ idx + 1 }}</template>
|
<template #addonBefore>{{ idx + 1 }}</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.value" :placeholder="t('pages.inbounds.stream.general.value')" />
|
<a-input :style="{ width: '45%' }" v-model:value="h.value"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.value')" />
|
||||||
<a-button @click="inbound.stream.tcp.response.removeHeader(idx)">
|
<a-button @click="inbound.stream.tcp.response.removeHeader(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1131,17 +1087,23 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
||||||
<a-button size="small" @click="inbound.stream.ws.addHeader('', '')">
|
<a-button size="small" @click="inbound.stream.ws.addHeader('', '')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ span: 24 }">
|
<a-form-item :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group v-for="(h, idx) in inbound.stream.ws.headers" :key="`ws-h-${idx}`" compact class="mb-8">
|
<a-input-group v-for="(h, idx) in inbound.stream.ws.headers" :key="`ws-h-${idx}`" compact class="mb-8">
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.name" :placeholder="t('pages.inbounds.stream.general.name')">
|
<a-input :style="{ width: '45%' }" v-model:value="h.name"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.name')">
|
||||||
<template #addonBefore>{{ idx + 1 }}</template>
|
<template #addonBefore>{{ idx + 1 }}</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.value" :placeholder="t('pages.inbounds.stream.general.value')" />
|
<a-input :style="{ width: '45%' }" v-model:value="h.value"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.value')" />
|
||||||
<a-button @click="inbound.stream.ws.removeHeader(idx)">
|
<a-button @click="inbound.stream.ws.removeHeader(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1173,17 +1135,24 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
||||||
<a-button size="small" @click="inbound.stream.httpupgrade.addHeader('', '')">
|
<a-button size="small" @click="inbound.stream.httpupgrade.addHeader('', '')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ span: 24 }">
|
<a-form-item :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group v-for="(h, idx) in inbound.stream.httpupgrade.headers" :key="`hu-h-${idx}`" compact class="mb-8">
|
<a-input-group v-for="(h, idx) in inbound.stream.httpupgrade.headers" :key="`hu-h-${idx}`" compact
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.name" :placeholder="t('pages.inbounds.stream.general.name')">
|
class="mb-8">
|
||||||
|
<a-input :style="{ width: '45%' }" v-model:value="h.name"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.name')">
|
||||||
<template #addonBefore>{{ idx + 1 }}</template>
|
<template #addonBefore>{{ idx + 1 }}</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.value" :placeholder="t('pages.inbounds.stream.general.value')" />
|
<a-input :style="{ width: '45%' }" v-model:value="h.value"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.value')" />
|
||||||
<a-button @click="inbound.stream.httpupgrade.removeHeader(idx)">
|
<a-button @click="inbound.stream.httpupgrade.removeHeader(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1199,17 +1168,23 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
<a-form-item :label="t('pages.inbounds.stream.tcp.requestHeader')">
|
||||||
<a-button size="small" @click="inbound.stream.xhttp.addHeader('', '')">
|
<a-button size="small" @click="inbound.stream.xhttp.addHeader('', '')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :wrapper-col="{ span: 24 }">
|
<a-form-item :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group v-for="(h, idx) in inbound.stream.xhttp.headers" :key="`xh-h-${idx}`" compact class="mb-8">
|
<a-input-group v-for="(h, idx) in inbound.stream.xhttp.headers" :key="`xh-h-${idx}`" compact class="mb-8">
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.name" :placeholder="t('pages.inbounds.stream.general.name')">
|
<a-input :style="{ width: '45%' }" v-model:value="h.name"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.name')">
|
||||||
<template #addonBefore>{{ idx + 1 }}</template>
|
<template #addonBefore>{{ idx + 1 }}</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
<a-input :style="{ width: '45%' }" v-model:value="h.value" :placeholder="t('pages.inbounds.stream.general.value')" />
|
<a-input :style="{ width: '45%' }" v-model:value="h.value"
|
||||||
|
:placeholder="t('pages.inbounds.stream.general.value')" />
|
||||||
<a-button @click="inbound.stream.xhttp.removeHeader(idx)">
|
<a-button @click="inbound.stream.xhttp.removeHeader(idx)">
|
||||||
<template #icon><MinusOutlined /></template>
|
<template #icon>
|
||||||
|
<MinusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1228,7 +1203,8 @@ watch(
|
||||||
<a-input v-model:value="inbound.stream.xhttp.scStreamUpServerSecs" />
|
<a-input v-model:value="inbound.stream.xhttp.scStreamUpServerSecs" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Server Max Header Bytes">
|
<a-form-item label="Server Max Header Bytes">
|
||||||
<a-input-number v-model:value="inbound.stream.xhttp.serverMaxHeaderBytes" :min="0" placeholder="0 (default)" />
|
<a-input-number v-model:value="inbound.stream.xhttp.serverMaxHeaderBytes" :min="0"
|
||||||
|
placeholder="0 (default)" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Padding Bytes">
|
<a-form-item label="Padding Bytes">
|
||||||
<a-input v-model:value="inbound.stream.xhttp.xPaddingBytes" />
|
<a-input v-model:value="inbound.stream.xhttp.xPaddingBytes" />
|
||||||
|
|
@ -1271,8 +1247,7 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item
|
||||||
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
|
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
|
||||||
label="Session Key"
|
label="Session Key">
|
||||||
>
|
|
||||||
<a-input v-model:value="inbound.stream.xhttp.sessionKey" placeholder="x_session" />
|
<a-input v-model:value="inbound.stream.xhttp.sessionKey" placeholder="x_session" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Sequence Placement">
|
<a-form-item label="Sequence Placement">
|
||||||
|
|
@ -1284,10 +1259,8 @@ watch(
|
||||||
<a-select-option value="query">query</a-select-option>
|
<a-select-option value="query">query</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
|
||||||
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
|
label="Sequence Key">
|
||||||
label="Sequence Key"
|
|
||||||
>
|
|
||||||
<a-input v-model:value="inbound.stream.xhttp.seqKey" placeholder="x_seq" />
|
<a-input v-model:value="inbound.stream.xhttp.seqKey" placeholder="x_seq" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Uplink Data Placement">
|
<a-form-item v-if="inbound.stream.xhttp.mode === 'packet-up'" label="Uplink Data Placement">
|
||||||
|
|
@ -1301,8 +1274,7 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item
|
||||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
|
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
|
||||||
label="Uplink Data Key"
|
label="Uplink Data Key">
|
||||||
>
|
|
||||||
<a-input v-model:value="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data" />
|
<a-input v-model:value="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="No SSE Header">
|
<a-form-item label="No SSE Header">
|
||||||
|
|
@ -1327,7 +1299,8 @@ watch(
|
||||||
<a-form-item label="Cipher Suites">
|
<a-form-item label="Cipher Suites">
|
||||||
<a-select v-model:value="inbound.stream.tls.cipherSuites">
|
<a-select v-model:value="inbound.stream.tls.cipherSuites">
|
||||||
<a-select-option value="">Auto</a-select-option>
|
<a-select-option value="">Auto</a-select-option>
|
||||||
<a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label }}</a-select-option>
|
<a-select-option v-for="[label, val] in CIPHER_SUITES" :key="val" :value="val">{{ label
|
||||||
|
}}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Min/Max Version">
|
<a-form-item label="Min/Max Version">
|
||||||
|
|
@ -1347,12 +1320,8 @@ watch(
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="ALPN">
|
<a-form-item label="ALPN">
|
||||||
<a-select
|
<a-select v-model:value="inbound.stream.tls.alpn" mode="multiple" :style="{ width: '100%' }"
|
||||||
v-model:value="inbound.stream.tls.alpn"
|
:token-separators="[',']">
|
||||||
mode="multiple"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
:token-separators="[',']"
|
|
||||||
>
|
|
||||||
<a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
|
<a-select-option v-for="a in ALPNS" :key="a" :value="a">{{ a }}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1379,15 +1348,15 @@ watch(
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
|
<a-button v-if="idx === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button v-if="inbound.stream.tls.certs.length > 1" type="primary" size="small"
|
||||||
v-if="inbound.stream.tls.certs.length > 1"
|
@click="inbound.stream.tls.removeCert(idx)">
|
||||||
type="primary"
|
<template #icon>
|
||||||
size="small"
|
<MinusOutlined />
|
||||||
@click="inbound.stream.tls.removeCert(idx)"
|
</template>
|
||||||
>
|
|
||||||
<template #icon><MinusOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -1488,7 +1457,8 @@ watch(
|
||||||
<a-input v-model:value="inbound.stream.reality.settings.spiderX" />
|
<a-input v-model:value="inbound.stream.reality.settings.spiderX" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.publicKey')">
|
<a-form-item :label="t('pages.inbounds.publicKey')">
|
||||||
<a-textarea v-model:value="inbound.stream.reality.settings.publicKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
<a-textarea v-model:value="inbound.stream.reality.settings.publicKey"
|
||||||
|
:auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('pages.inbounds.privatekey')">
|
<a-form-item :label="t('pages.inbounds.privatekey')">
|
||||||
<a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
<a-textarea v-model:value="inbound.stream.reality.privateKey" :auto-size="{ minRows: 1, maxRows: 4 }" />
|
||||||
|
|
@ -1503,7 +1473,8 @@ watch(
|
||||||
<a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
<a-textarea v-model:value="inbound.stream.reality.mldsa65Seed" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="mldsa65 Verify">
|
<a-form-item label="mldsa65 Verify">
|
||||||
<a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
<a-textarea v-model:value="inbound.stream.reality.settings.mldsa65Verify"
|
||||||
|
:auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
|
@ -1517,23 +1488,16 @@ watch(
|
||||||
<a-divider :style="{ margin: '5px 0 0' }" />
|
<a-divider :style="{ margin: '5px 0 0' }" />
|
||||||
<a-form-item label="External Proxy">
|
<a-form-item label="External Proxy">
|
||||||
<a-switch v-model:checked="externalProxy" />
|
<a-switch v-model:checked="externalProxy" />
|
||||||
<a-button
|
<a-button v-if="externalProxy" size="small" type="primary" :style="{ marginLeft: '10px' }"
|
||||||
v-if="externalProxy"
|
@click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '' })">
|
||||||
size="small"
|
<template #icon>
|
||||||
type="primary"
|
<PlusOutlined />
|
||||||
:style="{ marginLeft: '10px' }"
|
</template>
|
||||||
@click="inbound.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '' })"
|
|
||||||
>
|
|
||||||
<template #icon><PlusOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="externalProxy" :wrapper-col="{ span: 24 }">
|
<a-form-item v-if="externalProxy" :wrapper-col="{ span: 24 }">
|
||||||
<a-input-group
|
<a-input-group v-for="(row, idx) in inbound.stream.externalProxy" :key="`ep-${idx}`" compact
|
||||||
v-for="(row, idx) in inbound.stream.externalProxy"
|
:style="{ margin: '8px 0' }">
|
||||||
:key="`ep-${idx}`"
|
|
||||||
compact
|
|
||||||
:style="{ margin: '8px 0' }"
|
|
||||||
>
|
|
||||||
<a-tooltip title="Force TLS">
|
<a-tooltip title="Force TLS">
|
||||||
<a-select v-model:value="row.forceTls" :style="{ width: '20%' }">
|
<a-select v-model:value="row.forceTls" :style="{ width: '20%' }">
|
||||||
<a-select-option value="same">{{ t('pages.inbounds.same') }}</a-select-option>
|
<a-select-option value="same">{{ t('pages.inbounds.same') }}</a-select-option>
|
||||||
|
|
@ -1616,12 +1580,8 @@ watch(
|
||||||
<a-input v-model:value="inbound.stream.sockopt.interfaceName" />
|
<a-input v-model:value="inbound.stream.sockopt.interfaceName" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Trusted X-Forwarded-For">
|
<a-form-item label="Trusted X-Forwarded-For">
|
||||||
<a-select
|
<a-select v-model:value="inbound.stream.sockopt.trustedXForwardedFor" mode="tags"
|
||||||
v-model:value="inbound.stream.sockopt.trustedXForwardedFor"
|
:style="{ width: '100%' }" :token-separators="[',']">
|
||||||
mode="tags"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
:token-separators="[',']"
|
|
||||||
>
|
|
||||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||||
|
|
@ -1636,8 +1596,7 @@ watch(
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- ============================== SNIFFING ============================== -->
|
<!-- ============================== SNIFFING ============================== -->
|
||||||
<a-tab-pane key="sniffing" tab="Sniffing"
|
<a-tab-pane key="sniffing" tab="Sniffing"><!-- "Sniffing" stays literal — xray config term -->
|
||||||
><!-- "Sniffing" stays literal — xray config term -->
|
|
||||||
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
<a-form :colon="false" :label-col="{ md: { span: 8 } }" :wrapper-col="{ md: { span: 14 } }">
|
||||||
<a-form-item label="Enabled">
|
<a-form-item label="Enabled">
|
||||||
<a-switch v-model:checked="inbound.sniffing.enabled" />
|
<a-switch v-model:checked="inbound.sniffing.enabled" />
|
||||||
|
|
@ -1655,22 +1614,12 @@ watch(
|
||||||
<a-switch v-model:checked="inbound.sniffing.routeOnly" />
|
<a-switch v-model:checked="inbound.sniffing.routeOnly" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="IPs excluded">
|
<a-form-item label="IPs excluded">
|
||||||
<a-select
|
<a-select v-model:value="inbound.sniffing.ipsExcluded" mode="tags" :token-separators="[',']"
|
||||||
v-model:value="inbound.sniffing.ipsExcluded"
|
placeholder="IP/CIDR/geoip:*/ext:*" :style="{ width: '100%' }" />
|
||||||
mode="tags"
|
|
||||||
:token-separators="[',']"
|
|
||||||
placeholder="IP/CIDR/geoip:*/ext:*"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Domains excluded">
|
<a-form-item label="Domains excluded">
|
||||||
<a-select
|
<a-select v-model:value="inbound.sniffing.domainsExcluded" mode="tags" :token-separators="[',']"
|
||||||
v-model:value="inbound.sniffing.domainsExcluded"
|
placeholder="domain:*/ext:*" :style="{ width: '100%' }" />
|
||||||
mode="tags"
|
|
||||||
:token-separators="[',']"
|
|
||||||
placeholder="domain:*/ext:*"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
@ -1678,28 +1627,17 @@ watch(
|
||||||
|
|
||||||
<!-- ============================== ADVANCED ============================== -->
|
<!-- ============================== ADVANCED ============================== -->
|
||||||
<a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')">
|
<a-tab-pane key="advanced" :tab="t('pages.xray.advancedTemplate')">
|
||||||
<a-alert
|
<a-alert type="info" show-icon
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form."
|
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form."
|
||||||
class="mb-12"
|
class="mb-12" />
|
||||||
/>
|
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="streamSettings">
|
<a-form-item label="streamSettings">
|
||||||
<a-textarea
|
<a-textarea v-model:value="advancedJson.stream" :auto-size="{ minRows: 10, maxRows: 24 }" spellcheck="false"
|
||||||
v-model:value="advancedJson.stream"
|
class="json-editor" />
|
||||||
:auto-size="{ minRows: 10, maxRows: 24 }"
|
|
||||||
spellcheck="false"
|
|
||||||
class="json-editor"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="sniffing (overrides the Sniffing tab when set)">
|
<a-form-item label="sniffing (overrides the Sniffing tab when set)">
|
||||||
<a-textarea
|
<a-textarea v-model:value="advancedJson.sniffing" :auto-size="{ minRows: 6, maxRows: 16 }"
|
||||||
v-model:value="advancedJson.sniffing"
|
spellcheck="false" class="json-editor" />
|
||||||
:auto-size="{ minRows: 6, maxRows: 16 }"
|
|
||||||
spellcheck="false"
|
|
||||||
class="json-editor"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
@ -1708,12 +1646,29 @@ watch(
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mt-4 { margin-top: 4px; }
|
.mt-4 {
|
||||||
.mt-8 { margin-top: 8px; }
|
margin-top: 4px;
|
||||||
.mt-12 { margin-top: 12px; }
|
}
|
||||||
.mb-4 { margin-bottom: 4px; }
|
|
||||||
.mb-8 { margin-bottom: 8px; }
|
.mt-8 {
|
||||||
.mb-12 { margin-bottom: 12px; }
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-12 {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-12 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.random-icon {
|
.random-icon {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|
@ -1736,6 +1691,7 @@ watch(
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-summary th,
|
.client-summary th,
|
||||||
.client-summary td {
|
.client-summary td {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
|
@ -1749,6 +1705,7 @@ watch(
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fallbacks-title {
|
.fallbacks-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
|
|
@ -259,13 +259,7 @@ const showSubscriptionTab = computed(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal :open="open" :title="t('pages.inbounds.inboundData')" :footer="null" width="640px" @cancel="close">
|
||||||
:open="open"
|
|
||||||
:title="t('pages.inbounds.inboundData')"
|
|
||||||
:footer="null"
|
|
||||||
width="640px"
|
|
||||||
@cancel="close"
|
|
||||||
>
|
|
||||||
<template v-if="dbInbound && inbound">
|
<template v-if="dbInbound && inbound">
|
||||||
<a-tabs v-model:active-key="activeTab">
|
<a-tabs v-model:active-key="activeTab">
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
|
|
@ -336,7 +330,9 @@ const showSubscriptionTab = computed(
|
||||||
<code class="value-code">{{ encryptionLabel }}</code>
|
<code class="value-code">{{ encryptionLabel }}</code>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" class="value-copy" @click="copyText(encryptionLabel)">
|
<a-button size="small" class="value-copy" @click="copyText(encryptionLabel)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -487,15 +483,16 @@ const showSubscriptionTab = computed(
|
||||||
<a-tag color="green">Peer {{ idx + 1 }} config</a-tag>
|
<a-tag color="green">Peer {{ idx + 1 }} config</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(wireguardConfigs[idx])">
|
<a-button size="small" @click="copyText(wireguardConfigs[idx])">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip :title="t('download')">
|
<a-tooltip :title="t('download')">
|
||||||
<a-button
|
<a-button size="small" @click="downloadText(wireguardConfigs[idx], `peer-${idx + 1}.conf`)">
|
||||||
size="small"
|
<template #icon>
|
||||||
@click="downloadText(wireguardConfigs[idx], `peer-${idx + 1}.conf`)"
|
<DownloadOutlined />
|
||||||
>
|
</template>
|
||||||
<template #icon><DownloadOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -510,7 +507,9 @@ const showSubscriptionTab = computed(
|
||||||
<a-tag color="green">Peer {{ idx + 1 }} link</a-tag>
|
<a-tag color="green">Peer {{ idx + 1 }} link</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(wireguardLinks[idx])">
|
<a-button size="small" @click="copyText(wireguardLinks[idx])">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -525,16 +524,14 @@ const showSubscriptionTab = computed(
|
||||||
<!-- Single-user SS share link (no QR) -->
|
<!-- Single-user SS share link (no QR) -->
|
||||||
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0">
|
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0">
|
||||||
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
|
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
|
||||||
<div
|
<div v-for="(link, idx) in links" :key="idx" class="link-panel">
|
||||||
v-for="(link, idx) in links"
|
|
||||||
:key="idx"
|
|
||||||
class="link-panel"
|
|
||||||
>
|
|
||||||
<div class="link-panel-header">
|
<div class="link-panel-header">
|
||||||
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
|
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(link.link)">
|
<a-button size="small" @click="copyText(link.link)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -626,12 +623,8 @@ const showSubscriptionTab = computed(
|
||||||
<td>
|
<td>
|
||||||
<div class="ip-log">
|
<div class="ip-log">
|
||||||
<div v-if="clientIpsArray.length > 0">
|
<div v-if="clientIpsArray.length > 0">
|
||||||
<a-tag
|
<a-tag v-for="(item, idx) in clientIpsArray" :key="idx" color="blue" class="ip-log-row">{{ item
|
||||||
v-for="(item, idx) in clientIpsArray"
|
}}</a-tag>
|
||||||
:key="idx"
|
|
||||||
color="blue"
|
|
||||||
class="ip-log-row"
|
|
||||||
>{{ item }}</a-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<a-tag v-else>{{ clientIpsText || t('tgbot.noIpRecord') }}</a-tag>
|
<a-tag v-else>{{ clientIpsText || t('tgbot.noIpRecord') }}</a-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -658,27 +651,26 @@ const showSubscriptionTab = computed(
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a-tag
|
<a-tag v-if="clientStats && clientSettings.totalGB > 0" :color="statsColor(clientStats)">{{
|
||||||
v-if="clientStats && clientSettings.totalGB > 0"
|
getRemainingStats() }}</a-tag>
|
||||||
:color="statsColor(clientStats)"
|
|
||||||
>{{ getRemainingStats() }}</a-tag>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag
|
<a-tag v-if="clientSettings.totalGB > 0" :color="clientStats ? statsColor(clientStats) : 'default'">{{
|
||||||
v-if="clientSettings.totalGB > 0"
|
SizeFormatter.sizeFormat(clientSettings.totalGB) }}</a-tag>
|
||||||
:color="clientStats ? statsColor(clientStats) : 'default'"
|
<a-tag v-else color="purple">
|
||||||
>{{ SizeFormatter.sizeFormat(clientSettings.totalGB) }}</a-tag>
|
<InfinityIcon />
|
||||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag
|
<a-tag v-if="clientSettings.expiryTime > 0"
|
||||||
v-if="clientSettings.expiryTime > 0"
|
:color="ColorUtils.usageColor(Date.now(), expireDiff, clientSettings.expiryTime)">{{
|
||||||
:color="ColorUtils.usageColor(Date.now(), expireDiff, clientSettings.expiryTime)"
|
IntlUtil.formatDate(clientSettings.expiryTime) }}</a-tag>
|
||||||
>{{ IntlUtil.formatDate(clientSettings.expiryTime) }}</a-tag>
|
|
||||||
<a-tag v-else-if="clientSettings.expiryTime < 0" color="green">
|
<a-tag v-else-if="clientSettings.expiryTime < 0" color="green">
|
||||||
{{ clientSettings.expiryTime / -86400000 }} {{ t('day') }}
|
{{ clientSettings.expiryTime / -86400000 }} {{ t('day') }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
<a-tag v-else color="purple">
|
||||||
|
<InfinityIcon />
|
||||||
|
</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -691,7 +683,9 @@ const showSubscriptionTab = computed(
|
||||||
<a-tag color="blue">{{ clientSettings.tgId }}</a-tag>
|
<a-tag color="blue">{{ clientSettings.tgId }}</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(clientSettings.tgId)">
|
<a-button size="small" @click="copyText(clientSettings.tgId)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -700,16 +694,14 @@ const showSubscriptionTab = computed(
|
||||||
<!-- Per-client share links (no QR) -->
|
<!-- Per-client share links (no QR) -->
|
||||||
<template v-if="dbInbound.hasLink() && links.length > 0">
|
<template v-if="dbInbound.hasLink() && links.length > 0">
|
||||||
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
|
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
|
||||||
<div
|
<div v-for="(link, idx) in links" :key="idx" class="link-panel">
|
||||||
v-for="(link, idx) in links"
|
|
||||||
:key="idx"
|
|
||||||
class="link-panel"
|
|
||||||
>
|
|
||||||
<div class="link-panel-header">
|
<div class="link-panel-header">
|
||||||
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
|
<a-tag color="green">{{ link.remark || `Link ${idx + 1}` }}</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(link.link)">
|
<a-button size="small" @click="copyText(link.link)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -727,16 +719,13 @@ const showSubscriptionTab = computed(
|
||||||
<a-tag color="green">{{ t('subscription.title') }}</a-tag>
|
<a-tag color="green">{{ t('subscription.title') }}</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(subLink)">
|
<a-button size="small" @click="copyText(subLink)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a :href="subLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subLink }}</a>
|
||||||
:href="subLink"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="link-panel-anchor"
|
|
||||||
>{{ subLink }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="subSettings.subJsonEnable && subJsonLink" class="link-panel">
|
<div v-if="subSettings.subJsonEnable && subJsonLink" class="link-panel">
|
||||||
|
|
@ -744,16 +733,14 @@ const showSubscriptionTab = computed(
|
||||||
<a-tag color="green">JSON</a-tag>
|
<a-tag color="green">JSON</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copyText(subJsonLink)">
|
<a-button size="small" @click="copyText(subJsonLink)">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a :href="subJsonLink" target="_blank" rel="noopener noreferrer" class="link-panel-anchor">{{ subJsonLink
|
||||||
:href="subJsonLink"
|
}}</a>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="link-panel-anchor"
|
|
||||||
>{{ subJsonLink }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|
@ -766,14 +753,17 @@ const showSubscriptionTab = computed(
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table.block {
|
.info-table.block {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table td,
|
.info-table td,
|
||||||
.info-table th {
|
.info-table th {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table th {
|
.info-table th {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -795,6 +785,7 @@ const showSubscriptionTab = computed(
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 140px minmax(0, 1fr);
|
grid-template-columns: 140px minmax(0, 1fr);
|
||||||
|
|
@ -803,30 +794,36 @@ const showSubscriptionTab = computed(
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px solid rgba(128, 128, 128, 0.12);
|
border-bottom: 1px solid rgba(128, 128, 128, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row:last-child {
|
.info-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row dt {
|
.info-row dt {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row dd {
|
.info-row dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-tag {
|
.value-tag {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-block {
|
.value-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-code {
|
.value-code {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
|
@ -839,9 +836,11 @@ const showSubscriptionTab = computed(
|
||||||
user-select: all;
|
user-select: all;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.dark) .value-code {
|
:global(body.dark) .value-code {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-copy {
|
.value-copy {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -853,6 +852,7 @@ const showSubscriptionTab = computed(
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-line span {
|
.security-line span {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
@ -875,12 +875,14 @@ const showSubscriptionTab = computed(
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-log-row {
|
.ip-log-row {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-log-actions {
|
.ip-log-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
@ -907,12 +909,14 @@ const showSubscriptionTab = computed(
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-panel-header {
|
.link-panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-panel-text {
|
.link-panel-text {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -923,9 +927,11 @@ const showSubscriptionTab = computed(
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.dark) .link-panel-text {
|
:global(body.dark) .link-panel-text {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-panel-anchor {
|
.link-panel-anchor {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
@ -938,13 +944,16 @@ const showSubscriptionTab = computed(
|
||||||
text-decoration-color: rgba(22, 119, 255, 0.4);
|
text-decoration-color: rgba(22, 119, 255, 0.4);
|
||||||
transition: background 120ms ease, text-decoration-color 120ms ease;
|
transition: background 120ms ease, text-decoration-color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-panel-anchor:hover {
|
.link-panel-anchor:hover {
|
||||||
background: rgba(22, 119, 255, 0.08);
|
background: rgba(22, 119, 255, 0.08);
|
||||||
text-decoration-color: var(--ant-color-primary, #1677ff);
|
text-decoration-color: var(--ant-color-primary, #1677ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.dark) .link-panel-anchor {
|
:global(body.dark) .link-panel-anchor {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(body.dark) .link-panel-anchor:hover {
|
:global(body.dark) .link-panel-anchor:hover {
|
||||||
background: rgba(22, 119, 255, 0.16);
|
background: rgba(22, 119, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,12 +216,16 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #title>
|
<template #title>
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" @click="emit('add-inbound')">
|
<a-button type="primary" @click="emit('add-inbound')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
<template v-if="!isMobile">{{ t('pages.inbounds.addInbound') }}</template>
|
<template v-if="!isMobile">{{ t('pages.inbounds.addInbound') }}</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a-button type="primary">
|
<a-button type="primary">
|
||||||
<template #icon><MenuOutlined /></template>
|
<template #icon>
|
||||||
|
<MenuOutlined />
|
||||||
|
</template>
|
||||||
<template v-if="!isMobile">{{ t('pages.inbounds.generalActions') }}</template>
|
<template v-if="!isMobile">{{ t('pages.inbounds.generalActions') }}</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
|
|
@ -253,7 +257,9 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button-group>
|
<a-button-group>
|
||||||
<a-button :loading="refreshing" @click="emit('refresh')">
|
<a-button :loading="refreshing" @click="emit('refresh')">
|
||||||
<template #icon><SyncOutlined /></template>
|
<template #icon>
|
||||||
|
<SyncOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-popover placement="bottomRight" trigger="click">
|
<a-popover placement="bottomRight" trigger="click">
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|
@ -265,11 +271,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-space direction="vertical">
|
<a-space direction="vertical">
|
||||||
<span>{{ t('pages.inbounds.autoRefreshInterval') }}</span>
|
<span>{{ t('pages.inbounds.autoRefreshInterval') }}</span>
|
||||||
<a-select
|
<a-select v-model:value="refreshIntervalMs" :disabled="!isRefreshEnabled" :style="{ width: '100%' }">
|
||||||
v-model:value="refreshIntervalMs"
|
|
||||||
:disabled="!isRefreshEnabled"
|
|
||||||
:style="{ width: '100%' }"
|
|
||||||
>
|
|
||||||
<a-select-option v-for="key in [5, 10, 30, 60]" :key="key" :value="key * 1000">
|
<a-select-option v-for="key in [5, 10, 30, 60]" :key="key" :value="key * 1000">
|
||||||
{{ key }}s
|
{{ key }}s
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
|
|
@ -277,7 +279,9 @@ function showQrCodeMenu(dbInbound) {
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<a-button>
|
<a-button>
|
||||||
<template #icon><DownOutlined /></template>
|
<template #icon>
|
||||||
|
<DownOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</a-button-group>
|
</a-button-group>
|
||||||
|
|
@ -287,23 +291,17 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<!-- Search / filter toolbar -->
|
<!-- Search / filter toolbar -->
|
||||||
<div :class="isMobile ? 'filter-bar mobile' : 'filter-bar'">
|
<div :class="isMobile ? 'filter-bar mobile' : 'filter-bar'">
|
||||||
<a-switch v-model:checked="enableFilter" @change="onToggleFilter">
|
<a-switch v-model:checked="enableFilter" @change="onToggleFilter">
|
||||||
<template #checkedChildren><SearchOutlined /></template>
|
<template #checkedChildren>
|
||||||
<template #unCheckedChildren><FilterOutlined /></template>
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
|
<template #unCheckedChildren>
|
||||||
|
<FilterOutlined />
|
||||||
|
</template>
|
||||||
</a-switch>
|
</a-switch>
|
||||||
<a-input
|
<a-input v-if="!enableFilter" v-model:value="searchKey" :placeholder="t('search')" autofocus
|
||||||
v-if="!enableFilter"
|
:size="isMobile ? 'small' : 'middle'" :style="{ maxWidth: '300px' }" />
|
||||||
v-model:value="searchKey"
|
<a-radio-group v-if="enableFilter" v-model:value="filterBy" button-style="solid"
|
||||||
:placeholder="t('search')"
|
:size="isMobile ? 'small' : 'middle'">
|
||||||
autofocus
|
|
||||||
:size="isMobile ? 'small' : 'middle'"
|
|
||||||
:style="{ maxWidth: '300px' }"
|
|
||||||
/>
|
|
||||||
<a-radio-group
|
|
||||||
v-if="enableFilter"
|
|
||||||
v-model:value="filterBy"
|
|
||||||
button-style="solid"
|
|
||||||
:size="isMobile ? 'small' : 'middle'"
|
|
||||||
>
|
|
||||||
<a-radio-button value="">{{ t('none') }}</a-radio-button>
|
<a-radio-button value="">{{ t('none') }}</a-radio-button>
|
||||||
<a-radio-button value="active">{{ t('subscription.active') }}</a-radio-button>
|
<a-radio-button value="active">{{ t('subscription.active') }}</a-radio-button>
|
||||||
<a-radio-button value="deactive">{{ t('disabled') }}</a-radio-button>
|
<a-radio-button value="deactive">{{ t('disabled') }}</a-radio-button>
|
||||||
|
|
@ -313,36 +311,21 @@ function showQrCodeMenu(dbInbound) {
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-table
|
<a-table :columns="columns" :data-source="visibleInbounds" :row-key="(r) => r.id"
|
||||||
:columns="columns"
|
:pagination="paginationFor(visibleInbounds)" :scroll="isMobile ? {} : { x: 1000 }"
|
||||||
:data-source="visibleInbounds"
|
:style="{ marginTop: '10px' }" size="small"
|
||||||
:row-key="(r) => r.id"
|
:row-class-name="(r) => (r.isMultiUser() ? '' : 'hide-expand-icon')">
|
||||||
:pagination="paginationFor(visibleInbounds)"
|
|
||||||
:scroll="isMobile ? {} : { x: 1000 }"
|
|
||||||
:style="{ marginTop: '10px' }"
|
|
||||||
size="small"
|
|
||||||
:row-class-name="(r) => (r.isMultiUser() ? '' : 'hide-expand-icon')"
|
|
||||||
>
|
|
||||||
<!-- Per-inbound client list, expanded by clicking the row's
|
<!-- Per-inbound client list, expanded by clicking the row's
|
||||||
default expand chevron. Hidden via row-class-name for
|
default expand chevron. Hidden via row-class-name for
|
||||||
non-multi-user inbounds (matches legacy behavior). -->
|
non-multi-user inbounds (matches legacy behavior). -->
|
||||||
<template #expandedRowRender="{ record }">
|
<template #expandedRowRender="{ record }">
|
||||||
<ClientRowTable
|
<ClientRowTable v-if="record.isMultiUser()" :db-inbound="record" :is-mobile="isMobile"
|
||||||
v-if="record.isMultiUser()"
|
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
|
||||||
:db-inbound="record"
|
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme" @edit-client="(p) => emit('edit-client', p)"
|
||||||
:is-mobile="isMobile"
|
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-client', p)"
|
||||||
:traffic-diff="trafficDiff"
|
|
||||||
:expire-diff="expireDiff"
|
|
||||||
:online-clients="onlineClients"
|
|
||||||
:last-online-map="lastOnlineMap"
|
|
||||||
:is-dark-theme="isDarkTheme"
|
|
||||||
@edit-client="(p) => emit('edit-client', p)"
|
|
||||||
@qrcode-client="(p) => emit('qrcode-client', p)"
|
|
||||||
@info-client="(p) => emit('info-client', p)"
|
|
||||||
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||||
@delete-client="(p) => emit('delete-client', p)"
|
@delete-client="(p) => emit('delete-client', p)"
|
||||||
@toggle-enable-client="(p) => emit('toggle-enable-client', p)"
|
@toggle-enable-client="(p) => emit('toggle-enable-client', p)" />
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
|
|
@ -352,16 +335,28 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<MoreOutlined class="row-action-trigger" @click.prevent />
|
<MoreOutlined class="row-action-trigger" @click.prevent />
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
||||||
<a-menu-item key="edit"><EditOutlined /> {{ t('edit') }}</a-menu-item>
|
<a-menu-item key="edit">
|
||||||
|
<EditOutlined /> {{ t('edit') }}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
||||||
<QrcodeOutlined /> {{ t('qrCode') }}
|
<QrcodeOutlined /> {{ t('qrCode') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<template v-if="record.isMultiUser()">
|
<template v-if="record.isMultiUser()">
|
||||||
<a-menu-item key="addClient"><UserAddOutlined /> {{ t('pages.client.add') }}</a-menu-item>
|
<a-menu-item key="addClient">
|
||||||
<a-menu-item key="addBulkClient"><UsergroupAddOutlined /> {{ t('pages.client.bulk') }}</a-menu-item>
|
<UserAddOutlined /> {{ t('pages.client.add') }}
|
||||||
<a-menu-item key="copyClients"><CopyOutlined /> {{ t('pages.client.copyFromInbound') }}</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="resetClients"><FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}</a-menu-item>
|
<a-menu-item key="addBulkClient">
|
||||||
<a-menu-item key="export"><ExportOutlined /> {{ t('pages.inbounds.export') }}</a-menu-item>
|
<UsergroupAddOutlined /> {{ t('pages.client.bulk') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="copyClients">
|
||||||
|
<CopyOutlined /> {{ t('pages.client.copyFromInbound') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetClients">
|
||||||
|
<FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="export">
|
||||||
|
<ExportOutlined /> {{ t('pages.inbounds.export') }}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item v-if="subEnable" key="subs">
|
<a-menu-item v-if="subEnable" key="subs">
|
||||||
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
|
@ -370,11 +365,19 @@ function showQrCodeMenu(dbInbound) {
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-menu-item key="showInfo"><InfoCircleOutlined /> {{ t('info') }}</a-menu-item>
|
<a-menu-item key="showInfo">
|
||||||
|
<InfoCircleOutlined /> {{ t('info') }}
|
||||||
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item key="clipboard"><CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}</a-menu-item>
|
<a-menu-item key="clipboard">
|
||||||
<a-menu-item key="resetTraffic"><RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}</a-menu-item>
|
<CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}
|
||||||
<a-menu-item key="clone"><BlockOutlined /> {{ t('pages.inbounds.clone') }}</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetTraffic">
|
||||||
|
<RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="clone">
|
||||||
|
<BlockOutlined /> {{ t('pages.inbounds.clone') }}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="delete" class="danger-item">
|
<a-menu-item key="delete" class="danger-item">
|
||||||
<DeleteOutlined /> {{ t('delete') }}
|
<DeleteOutlined /> {{ t('delete') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
|
@ -385,10 +388,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
|
|
||||||
<!-- ============== Enable switch (desktop) ============== -->
|
<!-- ============== Enable switch (desktop) ============== -->
|
||||||
<template v-else-if="column.key === 'enable'">
|
<template v-else-if="column.key === 'enable'">
|
||||||
<a-switch
|
<a-switch :checked="record.enable" @change="(next) => onSwitchEnable(record, next)" />
|
||||||
:checked="record.enable"
|
|
||||||
@change="(next) => onSwitchEnable(record, next)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ============== Protocol tags ============== -->
|
<!-- ============== Protocol tags ============== -->
|
||||||
|
|
@ -417,13 +417,15 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
<a-tag color="red" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].depleted.length }}</a-tag>
|
<a-tag color="red" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].depleted.length
|
||||||
|
}}</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-popover v-if="clientCount[record.id].expiring.length" :title="t('depletingSoon')">
|
<a-popover v-if="clientCount[record.id].expiring.length" :title="t('depletingSoon')">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
<a-tag color="orange" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].expiring.length }}</a-tag>
|
<a-tag color="orange" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].expiring.length
|
||||||
|
}}</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-popover v-if="clientCount[record.id].online.length" :title="t('online')">
|
<a-popover v-if="clientCount[record.id].online.length" :title="t('online')">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
@ -468,14 +470,13 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template v-else-if="column.key === 'expiryTime'">
|
<template v-else-if="column.key === 'expiryTime'">
|
||||||
<a-popover v-if="record.expiryTime > 0">
|
<a-popover v-if="record.expiryTime > 0">
|
||||||
<template #content>{{ IntlUtil.formatDate(record.expiryTime) }}</template>
|
<template #content>{{ IntlUtil.formatDate(record.expiryTime) }}</template>
|
||||||
<a-tag
|
<a-tag :color="ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)" style="min-width: 50px">
|
||||||
:color="ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)"
|
|
||||||
style="min-width: 50px"
|
|
||||||
>
|
|
||||||
{{ IntlUtil.formatRelativeTime(record.expiryTime) }}
|
{{ IntlUtil.formatRelativeTime(record.expiryTime) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
<a-tag v-else color="purple">
|
||||||
|
<InfinityIcon />
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ============== Mobile info popover ============== -->
|
<!-- ============== Mobile info popover ============== -->
|
||||||
|
|
@ -510,7 +511,9 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<td>{{ t('pages.inbounds.expireDate') }}</td>
|
<td>{{ t('pages.inbounds.expireDate') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="record.expiryTime > 0">{{ IntlUtil.formatRelativeTime(record.expiryTime) }}</a-tag>
|
<a-tag v-if="record.expiryTime > 0">{{ IntlUtil.formatRelativeTime(record.expiryTime) }}</a-tag>
|
||||||
<a-tag v-else color="purple"><InfinityIcon /></a-tag>
|
<a-tag v-else color="purple">
|
||||||
|
<InfinityIcon />
|
||||||
|
</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -537,10 +540,12 @@ function showQrCodeMenu(dbInbound) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-bar.mobile {
|
.filter-bar.mobile {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.filter-bar.mobile > * {
|
|
||||||
|
.filter-bar.mobile>* {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -565,4 +570,33 @@ function showQrCodeMenu(dbInbound) {
|
||||||
:deep(.hide-expand-icon .ant-table-row-expand-icon) {
|
:deep(.hide-expand-icon .ant-table-row-expand-icon) {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Round the table's outer corners — AD-Vue gives .ant-table the radius
|
||||||
|
* token, but the inner header strip and footer touch the edges, so clip
|
||||||
|
* them here. */
|
||||||
|
:deep(.ant-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-container) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead > tr:first-child > *:first-child) {
|
||||||
|
border-start-start-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead > tr:first-child > *:last-child) {
|
||||||
|
border-start-end-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-tbody > tr:last-child > *:first-child) {
|
||||||
|
border-end-start-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-tbody > tr:last-child > *:last-child) {
|
||||||
|
border-end-end-radius: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -512,10 +512,7 @@ function onRowAction({ key, dbInbound }) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-config-provider :theme="antdThemeConfig">
|
<a-config-provider :theme="antdThemeConfig">
|
||||||
<a-layout
|
<a-layout class="inbounds-page" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
|
||||||
class="inbounds-page"
|
|
||||||
:class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }"
|
|
||||||
>
|
|
||||||
<AppSidebar :base-path="basePath" :request-uri="requestUri" />
|
<AppSidebar :base-path="basePath" :request-uri="requestUri" />
|
||||||
|
|
||||||
<a-layout class="content-shell">
|
<a-layout class="content-shell">
|
||||||
|
|
@ -529,32 +526,34 @@ function onRowAction({ key, dbInbound }) {
|
||||||
<a-card size="small" hoverable class="summary-card">
|
<a-card size="small" hoverable class="summary-card">
|
||||||
<a-row :gutter="[16, 12]">
|
<a-row :gutter="[16, 12]">
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic :title="t('pages.inbounds.totalDownUp')"
|
||||||
:title="t('pages.inbounds.totalDownUp')"
|
:value="`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`">
|
||||||
:value="`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`"
|
<template #prefix>
|
||||||
>
|
<SwapOutlined />
|
||||||
<template #prefix><SwapOutlined /></template>
|
</template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic :title="t('pages.inbounds.totalUsage')"
|
||||||
:title="t('pages.inbounds.totalUsage')"
|
:value="SizeFormatter.sizeFormat(totals.up + totals.down)">
|
||||||
:value="SizeFormatter.sizeFormat(totals.up + totals.down)"
|
<template #prefix>
|
||||||
>
|
<PieChartOutlined />
|
||||||
<template #prefix><PieChartOutlined /></template>
|
</template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic :title="t('pages.inbounds.allTimeTrafficUsage')"
|
||||||
:title="t('pages.inbounds.allTimeTrafficUsage')"
|
:value="SizeFormatter.sizeFormat(totals.allTime)">
|
||||||
:value="SizeFormatter.sizeFormat(totals.allTime)"
|
<template #prefix>
|
||||||
>
|
<HistoryOutlined />
|
||||||
<template #prefix><HistoryOutlined /></template>
|
</template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic :title="t('pages.inbounds.inboundCount')" :value="String(dbInbounds.length)">
|
<CustomStatistic :title="t('pages.inbounds.inboundCount')" :value="String(dbInbounds.length)">
|
||||||
<template #prefix><BarsOutlined /></template>
|
<template #prefix>
|
||||||
|
<BarsOutlined />
|
||||||
|
</template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="4">
|
<a-col :sm="24" :md="4">
|
||||||
|
|
@ -576,94 +575,35 @@ function onRowAction({ key, dbInbound }) {
|
||||||
|
|
||||||
<!-- Inbound list — toolbar, search/filter, columns, row actions -->
|
<!-- Inbound list — toolbar, search/filter, columns, row actions -->
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<InboundList
|
<InboundList :db-inbounds="dbInbounds" :client-count="clientCount" :online-clients="onlineClients"
|
||||||
:db-inbounds="dbInbounds"
|
:last-online-map="lastOnlineMap" :is-dark-theme="themeState.isDark" :refreshing="refreshing"
|
||||||
:client-count="clientCount"
|
:expire-diff="expireDiff" :traffic-diff="trafficDiff" :page-size="pageSize" :is-mobile="isMobile"
|
||||||
:online-clients="onlineClients"
|
:sub-enable="subSettings.enable" @refresh="refresh" @add-inbound="onAddInbound"
|
||||||
:last-online-map="lastOnlineMap"
|
@general-action="onGeneralAction" @row-action="onRowAction" @edit-client="onEditClient"
|
||||||
:is-dark-theme="themeState.isDark"
|
@qrcode-client="onQrcodeClient" @info-client="onInfoClient"
|
||||||
:refreshing="refreshing"
|
@reset-traffic-client="onResetTrafficClient" @delete-client="onDeleteClient"
|
||||||
:expire-diff="expireDiff"
|
@toggle-enable-client="onToggleEnableClient" />
|
||||||
:traffic-diff="trafficDiff"
|
|
||||||
:page-size="pageSize"
|
|
||||||
:is-mobile="isMobile"
|
|
||||||
:sub-enable="subSettings.enable"
|
|
||||||
@refresh="refresh"
|
|
||||||
@add-inbound="onAddInbound"
|
|
||||||
@general-action="onGeneralAction"
|
|
||||||
@row-action="onRowAction"
|
|
||||||
@edit-client="onEditClient"
|
|
||||||
@qrcode-client="onQrcodeClient"
|
|
||||||
@info-client="onInfoClient"
|
|
||||||
@reset-traffic-client="onResetTrafficClient"
|
|
||||||
@delete-client="onDeleteClient"
|
|
||||||
@toggle-enable-client="onToggleEnableClient"
|
|
||||||
/>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<InboundFormModal
|
<InboundFormModal v-model:open="formOpen" :mode="formMode" :db-inbound="formDbInbound" @saved="refresh" />
|
||||||
v-model:open="formOpen"
|
<ClientFormModal v-model:open="clientOpen" :mode="clientMode" :db-inbound="clientDbInbound"
|
||||||
:mode="formMode"
|
:client-index="clientIndex" :sub-enable="subSettings.enable" :tg-bot-enable="tgBotEnable"
|
||||||
:db-inbound="formDbInbound"
|
:ip-limit-enable="ipLimitEnable" :traffic-diff="trafficDiff" @saved="refresh" />
|
||||||
@saved="refresh"
|
<ClientBulkModal v-model:open="bulkOpen" :db-inbound="bulkDbInbound" :sub-enable="subSettings.enable"
|
||||||
/>
|
:tg-bot-enable="tgBotEnable" :ip-limit-enable="ipLimitEnable" @saved="refresh" />
|
||||||
<ClientFormModal
|
<InboundInfoModal v-model:open="infoOpen" :db-inbound="infoDbInbound" :client-index="infoClientIndex"
|
||||||
v-model:open="clientOpen"
|
:remark-model="remarkModel" :expire-diff="expireDiff" :traffic-diff="trafficDiff"
|
||||||
:mode="clientMode"
|
:ip-limit-enable="ipLimitEnable" :tg-bot-enable="tgBotEnable" :sub-settings="subSettings"
|
||||||
:db-inbound="clientDbInbound"
|
:last-online-map="lastOnlineMap" />
|
||||||
:client-index="clientIndex"
|
<QrCodeModal v-model:open="qrOpen" :db-inbound="qrDbInbound" :client="qrClient" :remark-model="remarkModel" />
|
||||||
:sub-enable="subSettings.enable"
|
|
||||||
:tg-bot-enable="tgBotEnable"
|
|
||||||
:ip-limit-enable="ipLimitEnable"
|
|
||||||
:traffic-diff="trafficDiff"
|
|
||||||
@saved="refresh"
|
|
||||||
/>
|
|
||||||
<ClientBulkModal
|
|
||||||
v-model:open="bulkOpen"
|
|
||||||
:db-inbound="bulkDbInbound"
|
|
||||||
:sub-enable="subSettings.enable"
|
|
||||||
:tg-bot-enable="tgBotEnable"
|
|
||||||
:ip-limit-enable="ipLimitEnable"
|
|
||||||
@saved="refresh"
|
|
||||||
/>
|
|
||||||
<InboundInfoModal
|
|
||||||
v-model:open="infoOpen"
|
|
||||||
:db-inbound="infoDbInbound"
|
|
||||||
:client-index="infoClientIndex"
|
|
||||||
:remark-model="remarkModel"
|
|
||||||
:expire-diff="expireDiff"
|
|
||||||
:traffic-diff="trafficDiff"
|
|
||||||
:ip-limit-enable="ipLimitEnable"
|
|
||||||
:tg-bot-enable="tgBotEnable"
|
|
||||||
:sub-settings="subSettings"
|
|
||||||
:last-online-map="lastOnlineMap"
|
|
||||||
/>
|
|
||||||
<QrCodeModal
|
|
||||||
v-model:open="qrOpen"
|
|
||||||
:db-inbound="qrDbInbound"
|
|
||||||
:client="qrClient"
|
|
||||||
:remark-model="remarkModel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextModal
|
<TextModal v-model:open="textOpen" :title="textTitle" :content="textContent" :file-name="textFileName" />
|
||||||
v-model:open="textOpen"
|
<PromptModal v-model:open="promptOpen" :title="promptTitle" :ok-text="promptOkText" :type="promptType"
|
||||||
:title="textTitle"
|
:initial-value="promptInitial" :loading="promptLoading" @confirm="onPromptConfirm" />
|
||||||
:content="textContent"
|
|
||||||
:file-name="textFileName"
|
|
||||||
/>
|
|
||||||
<PromptModal
|
|
||||||
v-model:open="promptOpen"
|
|
||||||
:title="promptTitle"
|
|
||||||
:ok-text="promptOkText"
|
|
||||||
:type="promptType"
|
|
||||||
:initial-value="promptInitial"
|
|
||||||
:loading="promptLoading"
|
|
||||||
@confirm="onPromptConfirm"
|
|
||||||
/>
|
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -692,10 +632,17 @@ function onRowAction({ key, dbInbound }) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-shell { background: transparent; }
|
.content-shell {
|
||||||
.content-area { padding: 24px; }
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-spacer { min-height: calc(100vh - 120px); }
|
.content-area {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spacer {
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
|
||||||
.summary-card {
|
.summary-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
||||||
|
|
@ -52,23 +52,11 @@ function close() {
|
||||||
<template>
|
<template>
|
||||||
<a-modal :open="open" :title="t('qrCode')" :footer="null" width="420px" @cancel="close">
|
<a-modal :open="open" :title="t('qrCode')" :footer="null" width="420px" @cancel="close">
|
||||||
<template v-if="dbInbound">
|
<template v-if="dbInbound">
|
||||||
<QrPanel
|
<QrPanel v-for="(link, idx) in links" :key="`l${idx}`" :value="link.link"
|
||||||
v-for="(link, idx) in links"
|
:remark="link.remark || `Link ${idx + 1}`" />
|
||||||
:key="`l${idx}`"
|
|
||||||
:value="link.link"
|
|
||||||
:remark="link.remark || `Link ${idx + 1}`"
|
|
||||||
/>
|
|
||||||
<template v-for="(cfg, idx) in wireguardConfigs" :key="`w${idx}`">
|
<template v-for="(cfg, idx) in wireguardConfigs" :key="`w${idx}`">
|
||||||
<QrPanel
|
<QrPanel :value="cfg" :remark="`Peer ${idx + 1} config`" :download-name="`peer-${idx + 1}.conf`" />
|
||||||
:value="cfg"
|
<QrPanel v-if="wireguardLinks[idx]" :value="wireguardLinks[idx]" :remark="`Peer ${idx + 1} link`" />
|
||||||
:remark="`Peer ${idx + 1} config`"
|
|
||||||
:download-name="`peer-${idx + 1}.conf`"
|
|
||||||
/>
|
|
||||||
<QrPanel
|
|
||||||
v-if="wireguardLinks[idx]"
|
|
||||||
:value="wireguardLinks[idx]"
|
|
||||||
:remark="`Peer ${idx + 1} link`"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,16 @@ function download() {
|
||||||
<a-tag color="green" class="qr-remark">{{ remark }}</a-tag>
|
<a-tag color="green" class="qr-remark">{{ remark }}</a-tag>
|
||||||
<a-tooltip :title="t('copy')">
|
<a-tooltip :title="t('copy')">
|
||||||
<a-button size="small" @click="copy">
|
<a-button size="small" @click="copy">
|
||||||
<template #icon><CopyOutlined /></template>
|
<template #icon>
|
||||||
|
<CopyOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip v-if="downloadName" :title="t('download')">
|
<a-tooltip v-if="downloadName" :title="t('download')">
|
||||||
<a-button size="small" @click="download">
|
<a-button size="small" @click="download">
|
||||||
<template #icon><DownloadOutlined /></template>
|
<template #icon>
|
||||||
|
<DownloadOutlined />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,6 +112,7 @@ function download() {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-panel-canvas canvas {
|
.qr-panel-canvas canvas {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue